diff --git a/litellm/proxy/compliance_checks.py b/litellm/proxy/compliance_checks.py
index ff311911742..053c88d10ed 100644
--- a/litellm/proxy/compliance_checks.py
+++ b/litellm/proxy/compliance_checks.py
@@ -26,7 +26,8 @@ class ComplianceChecker:
def __init__(self, data: ComplianceCheckRequest):
self.data = data
- self.guardrails = data.guardrail_information or []
+ # a not_run entry records a guardrail that never evaluated the request, so it cannot evidence compliance
+ self.guardrails = [g for g in (data.guardrail_information or []) if g.get("guardrail_status") != "not_run"]
def _get_guardrails_by_mode(self, mode: str) -> list[dict]:
"""
diff --git a/litellm/proxy/guardrails/usage_endpoints.py b/litellm/proxy/guardrails/usage_endpoints.py
index 7a0edbddca8..75ffc1545dc 100644
--- a/litellm/proxy/guardrails/usage_endpoints.py
+++ b/litellm/proxy/guardrails/usage_endpoints.py
@@ -256,7 +256,7 @@ class UsageDetailResponse(BaseModel):
class UsageLogEntry(BaseModel):
id: str
timestamp: str
- action: str # blocked | passed | flagged
+ action: str # blocked | passed | flagged | not_run
score: float | None
latency_ms: float | None
model: str | None
@@ -691,7 +691,9 @@ def _usage_log_entry_from_row(
reason_val = None
if entry_for_guardrail:
st: Final = (entry_for_guardrail.get("guardrail_status") or "").lower()
- if "intervened" in st or "block" in st:
+ if st == "not_run":
+ action_val = "not_run"
+ elif "intervened" in st or "block" in st:
action_val = "blocked"
elif "fail" in st or "error" in st:
action_val = "flagged"
diff --git a/tests/test_litellm/proxy/guardrails/test_usage_endpoints.py b/tests/test_litellm/proxy/guardrails/test_usage_endpoints.py
index 1665fa03639..e469aef9a61 100644
--- a/tests/test_litellm/proxy/guardrails/test_usage_endpoints.py
+++ b/tests/test_litellm/proxy/guardrails/test_usage_endpoints.py
@@ -9,12 +9,10 @@ orphans), and logs missed their logical-name alias.
"""
from datetime import datetime
-from typing import Any, Optional
+from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
-
-
from fastapi import HTTPException
from prisma.errors import TableNotFoundError
@@ -47,7 +45,7 @@ def _yaml_guardrail(
guardrail_id: str = "yaml-1",
name: str = "yaml-pii",
provider: str = "presidio",
- info: Optional[dict] = None,
+ info: dict | None = None,
) -> Guardrail:
return Guardrail(
guardrail_id=guardrail_id,
@@ -433,3 +431,67 @@ async def test_detail_prev_trend_query_is_bounded():
prev_wheres = [w for w in wheres if "lt" in w.get("date", {})]
assert prev_wheres
assert all("gte" in w["date"] for w in prev_wheres)
+
+
+@pytest.mark.asyncio
+async def test_logs_report_not_run_entries_as_not_run_not_passed():
+ """LIT-6314: a guardrail that never scanned must not be reported as a pass in the drill-down."""
+ index_row = MagicMock()
+ index_row.request_id = "req-nr"
+ index_row.guardrail_id = "db-1"
+ index_row.start_time = datetime(2026, 4, 22)
+ spend_log = MagicMock()
+ spend_log.request_id = "req-nr"
+ spend_log.model = "gpt-4o-mini"
+ spend_log.startTime = datetime(2026, 4, 22)
+ spend_log.metadata = {
+ "guardrail_information": [
+ {"guardrail_name": "db-1", "guardrail_status": "not_run", "duration": 0.0},
+ ]
+ }
+ prisma = _prisma(find_unique=_db_row(), index_find_many=[index_row])
+ prisma.db.litellm_spendlogs.find_many = AsyncMock(return_value=[spend_log])
+ handler = _config_handler()
+ p1, p2 = _patches(prisma, handler)
+ with p1, p2:
+ resp = await guardrails_usage_logs(
+ guardrail_id="db-1",
+ policy_id=None,
+ page=1,
+ page_size=50,
+ action=None,
+ start_date=START,
+ end_date=END,
+ user_api_key_dict=ADMIN,
+ )
+ assert [log.action for log in resp.logs] == ["not_run"]
+
+
+@pytest.mark.asyncio
+async def test_logs_action_passed_filter_excludes_not_run_entries():
+ """LIT-6314: filtering the drill-down for passes must not return unscanned requests."""
+ index_row = MagicMock()
+ index_row.request_id = "req-nr"
+ index_row.guardrail_id = "db-1"
+ index_row.start_time = datetime(2026, 4, 22)
+ spend_log = MagicMock()
+ spend_log.request_id = "req-nr"
+ spend_log.model = "gpt-4o-mini"
+ spend_log.startTime = datetime(2026, 4, 22)
+ spend_log.metadata = {"guardrail_information": [{"guardrail_name": "db-1", "guardrail_status": "not_run"}]}
+ prisma = _prisma(find_unique=_db_row(), index_find_many=[index_row])
+ prisma.db.litellm_spendlogs.find_many = AsyncMock(return_value=[spend_log])
+ handler = _config_handler()
+ p1, p2 = _patches(prisma, handler)
+ with p1, p2:
+ resp = await guardrails_usage_logs(
+ guardrail_id="db-1",
+ policy_id=None,
+ page=1,
+ page_size=50,
+ action="passed",
+ start_date=START,
+ end_date=END,
+ user_api_key_dict=ADMIN,
+ )
+ assert resp.logs == []
diff --git a/tests/test_litellm/proxy/management_endpoints/test_compliance_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_compliance_endpoints.py
index dcbe515d5de..8382a5ada96 100644
--- a/tests/test_litellm/proxy/management_endpoints/test_compliance_endpoints.py
+++ b/tests/test_litellm/proxy/management_endpoints/test_compliance_endpoints.py
@@ -2,10 +2,8 @@
Unit tests for compliance check endpoints (EU AI Act and GDPR).
"""
-
import pytest
-
from litellm.proxy.compliance_checks import ComplianceChecker
from litellm.types.proxy.compliance_endpoints import ComplianceCheckRequest
@@ -591,3 +589,37 @@ class TestModeMatching:
continue
if matched:
assert mode in _guaranteed_modes(g_mode), (g_mode, mode)
+
+
+class TestNotRunGuardrails:
+ """LIT-6314 logs a not_run entry for a guardrail that message scoping left nothing to scan."""
+
+ def test_not_run_alone_never_evidences_compliance(self):
+ data = ComplianceCheckRequest(
+ request_id="req-601",
+ user_id="user-1",
+ model="gpt-4",
+ timestamp="2026-02-17T00:00:00Z",
+ guardrail_information=[
+ {"guardrail_name": "pii_detection", "guardrail_status": "not_run", "guardrail_mode": "pre_call"},
+ ],
+ )
+ results = {c.check_name: c.passed for c in ComplianceChecker(data).check_eu_ai_act()}
+ assert results["Guardrails applied"] is False
+ assert results["Content screened before LLM"] is False
+ assert results["Audit record complete"] is False
+
+ def test_not_run_sibling_does_not_fail_a_passing_request(self):
+ data = ComplianceCheckRequest(
+ request_id="req-602",
+ user_id="user-1",
+ model="gpt-4",
+ timestamp="2026-02-17T00:00:00Z",
+ pii_detected=True,
+ guardrail_information=[
+ {"guardrail_name": "pii_detection", "guardrail_status": "success", "guardrail_mode": "pre_call"},
+ {"guardrail_name": "system_only", "guardrail_status": "not_run", "guardrail_mode": "pre_call"},
+ ],
+ )
+ results = {c.check_name: c.passed for c in ComplianceChecker(data).check_gdpr()}
+ assert results["Sensitive data protected"] is True
diff --git a/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx b/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx
index 8d073feae82..8b4684e87b0 100644
--- a/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx
+++ b/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx
@@ -1,4 +1,4 @@
-import { CircleCheck, ChevronDown, TriangleAlert, X } from "lucide-react";
+import { CircleCheck, ChevronDown, MinusCircle, TriangleAlert, X } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import moment from "moment";
import React, { useState } from "react";
@@ -10,9 +10,16 @@ import type { LogEntry as ViewLogsLogEntry } from "@/components/view_logs/column
import type { LogEntry } from "./mockData";
const actionConfig: Record<
- "blocked" | "passed" | "flagged",
+ "blocked" | "passed" | "flagged" | "not_run",
{ icon: React.ElementType; color: string; bg: string; border: string; label: string }
> = {
+ not_run: {
+ icon: MinusCircle,
+ color: "text-muted-foreground",
+ bg: "bg-muted",
+ border: "border-border",
+ label: "Not run",
+ },
blocked: {
icon: X,
color: "text-destructive",
diff --git a/ui/litellm-dashboard/src/components/GuardrailsMonitor/mockData.ts b/ui/litellm-dashboard/src/components/GuardrailsMonitor/mockData.ts
index 7d99ebe7c44..717f0f459e4 100644
--- a/ui/litellm-dashboard/src/components/GuardrailsMonitor/mockData.ts
+++ b/ui/litellm-dashboard/src/components/GuardrailsMonitor/mockData.ts
@@ -43,7 +43,7 @@ export interface LogEntry {
input_snippet?: string;
output_snippet?: string;
score?: number;
- action: "blocked" | "passed" | "flagged";
+ action: "blocked" | "passed" | "flagged" | "not_run";
model?: string;
reason?: string;
latency_ms?: number;
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.test.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.test.tsx
index a679dc49427..961aad7dc9e 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.test.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.test.tsx
@@ -1,7 +1,7 @@
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
-import { LogDetailContent } from "./LogDetailContent";
+import { GuardrailJumpLink, LogDetailContent } from "./LogDetailContent";
import type { LogEntry } from "../columns";
vi.mock("../GuardrailViewer/GuardrailViewer", () => ({
@@ -489,3 +489,30 @@ describe("LogDetailContent", () => {
expect(within(descriptions).getByText("-")).toBeInTheDocument();
});
});
+
+describe("GuardrailJumpLink", () => {
+ it("does not render a not_run entry as a failure", () => {
+ render(
+ ,
+ );
+ expect(screen.getByText(/2 guardrails/)).toHaveTextContent("✓");
+ expect(screen.getByText(/2 guardrails/)).not.toHaveTextContent("✗");
+ });
+
+ it("still renders a real failure as failed", () => {
+ render(
+ ,
+ );
+ expect(screen.getByText(/2 guardrails/)).toHaveTextContent("✗");
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx
index 4c5c7b7b43f..105ae4add45 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx
@@ -636,7 +636,9 @@ function RequestResponseSection({
}
export function GuardrailJumpLink({ guardrailEntries }: { guardrailEntries: any[] }) {
- const allPassed = guardrailEntries.every((e) => {
+ // a not_run entry never evaluated the request, so it neither passes nor fails the banner
+ const evaluated = guardrailEntries.filter((e) => (e?.guardrail_status || e?.status) !== "not_run");
+ const allPassed = evaluated.every((e) => {
const status = e?.guardrail_status || e?.status;
return status === "pass" || status === "passed" || status === "success";
});