From f7954617cf8c50dc31d8ed35c2836529dcd73a65 Mon Sep 17 00:00:00 2001 From: wasimat404 Date: Tue, 23 Jun 2026 21:54:56 +0000 Subject: [PATCH] fix(guardrails): content filter logs upstream errors as not_run, not guardrail failure When a provider error propagates through the content filter's streaming iterator hook, the guardrail never evaluated content. Previously the generic except arm labeled it guardrail_failed_to_respond, surfacing upstream errors as guardrail failures in logs and metrics. Catch openai.OpenAIError (the base of litellm's mapped provider exceptions), record status as not_run, and re-raise unchanged. Fixes #31004 --- .../litellm_content_filter/content_filter.py | 8 ++++ .../test_upstream_error_not_run.py | 42 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_upstream_error_not_run.py diff --git a/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py b/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py index c6dfe141ab5..00d3142ea44 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py +++ b/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py @@ -24,6 +24,7 @@ from typing import ( cast, ) +import openai import yaml from fastapi import HTTPException @@ -2042,6 +2043,13 @@ class ContentFilterGuardrail(CustomGuardrail): except HTTPException: status = "guardrail_intervened" raise + except openai.OpenAIError as e: + # Upstream provider/stream error propagating through the iterator. + # The guardrail never evaluated content, so it must not be recorded + # as a guardrail failure. "not_run" is the canonical status here. + status = "not_run" + exception_str = str(e) + raise except Exception as e: status = "guardrail_failed_to_respond" exception_str = str(e) diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_upstream_error_not_run.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_upstream_error_not_run.py new file mode 100644 index 00000000000..348f1f50803 --- /dev/null +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_upstream_error_not_run.py @@ -0,0 +1,42 @@ +import openai +from unittest.mock import MagicMock + +import pytest + +from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.content_filter import ( + ContentFilterGuardrail, +) +from litellm.types.guardrails import GuardrailEventHooks + + +@pytest.mark.asyncio +async def test_streaming_hook_upstream_error_is_not_run(): + """ + When the upstream stream raises a provider error (openai.OpenAIError + subclass), the content filter never evaluates content. The error must + propagate unchanged and be logged as "not_run", not as a guardrail + failure. Regression test for issue #31004. + """ + guardrail = ContentFilterGuardrail( + guardrail_name="test-streaming-upstream-error", + patterns=[], + event_hook=GuardrailEventHooks.during_call, + ) + + async def mock_stream(): + raise openai.APIError("Provider returned error", request=MagicMock(), body=None) + yield # pragma: no cover - makes this an async generator + + user_api_key_dict = MagicMock() + request_data: dict = {} + + with pytest.raises(openai.APIError): + async for _ in guardrail.async_post_call_streaming_iterator_hook( + user_api_key_dict=user_api_key_dict, + response=mock_stream(), + request_data=request_data, + ): + pass + + info = request_data["metadata"]["standard_logging_guardrail_information"][0] + assert info["guardrail_status"] == "not_run"