Revert "fix(guardrails): keep legacy not_run neutral and stop labelling image-only input as skipped"

This reverts commit f78dd921c9.
This commit is contained in:
yucheng 2026-09-14 23:46:09 +00:00
parent f78dd921c9
commit bd9a87ea76
10 changed files with 26 additions and 61 deletions

View file

@ -210,7 +210,7 @@ class OpenAIChatCompletionsHandler(BaseTranslation):
task_mappings=tool_call_task_mappings,
)
elif not images_to_check and not guardrail_to_apply.records_own_guardrail_information:
elif not guardrail_to_apply.records_own_guardrail_information:
guardrail_to_apply.add_standard_logging_guardrail_information_to_request_data(
guardrail_json_response="no scannable content after message scoping",
request_data=data,

View file

@ -11,7 +11,6 @@ from litellm.types.proxy.compliance_endpoints import (
ComplianceCheckRequest,
ComplianceCheckResult,
)
from litellm.types.utils import UNEVALUATED_GUARDRAIL_STATUSES
class ComplianceChecker:
@ -27,11 +26,7 @@ class ComplianceChecker:
def __init__(self, data: ComplianceCheckRequest):
self.data = data
self.guardrails = [
g
for g in (data.guardrail_information or [])
if g.get("guardrail_status") not in UNEVALUATED_GUARDRAIL_STATUSES
]
self.guardrails = [g for g in (data.guardrail_information or []) if g.get("guardrail_status") != "skipped"]
def _get_guardrails_by_mode(self, mode: str) -> list[dict]:
"""

View file

@ -27,7 +27,6 @@ from litellm.repositories.table_repositories import (
DailyGuardrailUsageUnitsRepository,
SpendLogGuardrailIndexRepository,
)
from litellm.types.utils import UNEVALUATED_GUARDRAIL_STATUSES
if TYPE_CHECKING:
from prisma import types as prisma_types
@ -198,7 +197,7 @@ def guardrail_status_to_action(status: str | None) -> str:
if not status:
return "passed"
s: Final = (status or "").lower()
if s in UNEVALUATED_GUARDRAIL_STATUSES:
if s == "skipped":
return "skipped"
if "intervened" in s or "block" in s:
return "blocked"

View file

@ -3123,8 +3123,6 @@ GuardrailStatus = Literal[
"success", "guardrail_flagged", "guardrail_intervened", "guardrail_failed_to_respond", "not_run", "skipped"
]
UNEVALUATED_GUARDRAIL_STATUSES: Final[frozenset[GuardrailStatus]] = frozenset({"not_run", "skipped"})
# Fields on a guardrail record whose values can quote the caller's prompt: the payload sent to the
# guardrail, the provider response that echoes it back, and the two first-party hooks that inline
# prompt substrings (``block_code_execution`` and ``litellm_content_filter``). Every other field

View file

@ -1942,27 +1942,6 @@ class TestNoScannableContentRecordsSkipped:
assert guardrail.last_inputs is not None
assert all(e.get("guardrail_status") != "skipped" for e in self._recorded_entries(data))
@pytest.mark.asyncio
async def test_image_only_content_is_not_reported_as_skipped(self):
"""Images are only scanned alongside text, so an image-only request is a
pre-existing scan gap, not a message-scoping skip, and must not be labelled one"""
handler = OpenAIChatCompletionsHandler()
guardrail = MockGuardrail(guardrail_name="image-guardrail")
guardrail.skip_system_message_in_guardrail = True
data = {
"messages": [
{"role": "system", "content": "SYSTEM-PROMPT"},
{
"role": "user",
"content": [{"type": "image_url", "image_url": {"url": "https://example.com/cat.png"}}],
},
]
}
await handler.process_input_messages(data=data, guardrail_to_apply=guardrail)
assert self._recorded_entries(data) == []
class TestBuildBlockSseChunks:
"""build_block_sse_chunks turns a streaming ModifyResponseException into 200 SSE chunks"""

View file

@ -350,17 +350,15 @@ async def test_zero_and_non_int_usage_counters_are_skipped():
@pytest.mark.asyncio
@pytest.mark.parametrize("status", ["skipped", "not_run"])
async def test_skipped_entries_are_indexed_but_not_counted_as_evaluations(status: str):
async def test_skipped_entries_are_indexed_but_not_counted_as_evaluations():
"""
LIT-6314 records a skipped entry when message scoping leaves a guardrail
nothing to scan (older spend logs spell it not_run). The guardrail never
evaluated the request, so counting it as a passed evaluation would inflate
daily pass rates; it still gets an index row so per-request drill-down
finds the spend log.
nothing to scan. The guardrail never evaluated the request, so counting it
as a passed evaluation would inflate daily pass rates; it still gets an
index row so per-request drill-down finds the spend log.
"""
prisma = _prisma()
logs = [_payload("r1", guardrail_status=status), _payload("r2")]
logs = [_payload("r1", guardrail_status="skipped"), _payload("r2")]
await process_spend_logs_guardrail_usage(prisma, logs)

View file

@ -594,15 +594,14 @@ class TestModeMatching:
class TestSkippedGuardrails:
"""LIT-6314 logs a skipped entry for a guardrail that message scoping left nothing to scan."""
@pytest.mark.parametrize("status", ["skipped", "not_run"])
def test_skipped_alone_never_evidences_compliance(self, status: str):
def test_skipped_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": status, "guardrail_mode": "pre_call"},
{"guardrail_name": "pii_detection", "guardrail_status": "skipped", "guardrail_mode": "pre_call"},
],
)
results = {c.check_name: c.passed for c in ComplianceChecker(data).check_eu_ai_act()}

View file

@ -68,25 +68,22 @@ describe("GuardrailViewer", () => {
expect(screen.queryByText("FAILED")).not.toBeInTheDocument();
});
it.each(["skipped", "not_run"])(
"renders %s as SKIPPED (muted) and keeps it out of the evaluated and passed counts",
async (guardrail_status) => {
const user = userEvent.setup();
const data = makeGuardrailInformation({ ...skippedPreCall, guardrail_status });
renderWithProviders(<GuardrailViewer data={data} />);
it("renders skipped as SKIPPED (muted) and keeps it out of the evaluated and passed counts", async () => {
const user = userEvent.setup();
const data = makeGuardrailInformation(skippedPreCall);
renderWithProviders(<GuardrailViewer data={data} />);
expect(screen.getByText(/0 guardrails evaluated/)).toBeInTheDocument();
expect(screen.getByText(/0 Passed/)).toHaveClass("text-muted-foreground");
expect(screen.getByText(/1 Skipped/)).toBeInTheDocument();
const badge = screen.getByText("SKIPPED");
expect(badge).toHaveClass("text-muted-foreground");
expect(screen.queryByText("FAILED")).not.toBeInTheDocument();
expect(screen.queryByText(/^T\+/)).not.toBeInTheDocument();
expect(screen.getByText(/0 guardrails evaluated/)).toBeInTheDocument();
expect(screen.getByText(/0 Passed/)).toHaveClass("text-muted-foreground");
expect(screen.getByText(/1 Skipped/)).toBeInTheDocument();
const badge = screen.getByText("SKIPPED");
expect(badge).toHaveClass("text-muted-foreground");
expect(screen.queryByText("FAILED")).not.toBeInTheDocument();
expect(screen.queryByText(/^T\+/)).not.toBeInTheDocument();
await user.click(screen.getByText("pii-rail"));
expect(screen.getByText("no scannable content after message scoping")).toBeInTheDocument();
},
);
await user.click(screen.getByText("pii-rail"));
expect(screen.getByText("no scannable content after message scoping")).toBeInTheDocument();
});
it("anchors the lifecycle timeline on timed entries when an untimed skipped entry sorts first", () => {
const skipped = makeGuardrailInformation({ ...skippedPreCall, guardrail_name: "skipped-rail" });

View file

@ -140,7 +140,7 @@ const getEntryOutcome = (entry: GuardrailInformation): EntryOutcome => {
const status = (entry.guardrail_status ?? "").toLowerCase();
if (status === "success") return "passed";
if (status === "guardrail_flagged") return "flagged";
if (status === "skipped" || status === "not_run") return "skipped";
if (status === "skipped") return "skipped";
return "failed";
};

View file

@ -705,7 +705,7 @@ const GUARDRAIL_JUMP_LINK_STYLE = {
const isPassedStatus = (status: unknown) => status === "pass" || status === "passed" || status === "success";
const isFlaggedStatus = (status: unknown) => status === "flagged" || status === "guardrail_flagged";
const isSkippedStatus = (status: unknown) => status === "skipped" || status === "not_run";
const isSkippedStatus = (status: unknown) => status === "skipped";
const guardrailJumpLinkOutcome = (evaluated: unknown[]): keyof typeof GUARDRAIL_JUMP_LINK_STYLE => {
if (evaluated.length === 0) return "skipped";