Repository navigation
fix(responses): backport mid-stream content_policy_violation fallback routing to stable/1.100.x - #41208
Conversation
…so content_policy_fallbacks fire Mid-stream error events on the streaming Responses API were all raised as APIError, so a content_policy_violation event never matched the router's content-policy fallback dispatch and the client got the raw error instead of the fallback model's answer. Map each error event's code and status through the existing exception_type mapping, matching the non-streaming path, and unwrap the typed ContentPolicyViolationError and ContextWindowExceededError so the router routes them to the configured content_policy_fallbacks and context_window_fallbacks. (cherry picked from commit 073d4fe)
…nks via anext Move the BaseLLMException import into _map_error_event_exception so the module no longer imports it at load time, clearing the module-level cyclic import CodeQL flagged. The class is used only on the cold error path. Replace the mutable list-append test collector with aiter/anext so the regression tests read the stream immutably. (cherry picked from commit c246372)
… and fix stale exception assertions (cherry picked from commit fff7a2c)
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Greptile SummaryThis PR maps Responses API stream error events through LiteLLM's standard exception mapping and enables content-policy fallback routing for mid-stream failures
Confidence Score: 5/5The PR appears safe to merge, with regression coverage for the corrected mid-stream content-policy fallback path No actionable failures remain after checking exception status preservation, fallback dispatch, stream-state handling, logging compatibility, and modified test integrity
|
| Filename | Overview |
|---|---|
| litellm/responses/streaming_iterator.py | Adds typed error-event mapping and fallback eligibility for content-policy, rate-limit, and server failures |
| litellm/router.py | Passes the inner content-policy exception to fallback dispatch while retaining stream continuation state |
| tests/test_litellm/responses/test_streaming_iterator_error_events.py | Adds focused coverage for typed error mapping, content-policy fallback wrapping, partial output, and failure logging |
| tests/test_litellm/test_router.py | Verifies that real transformed Responses events select the configured content-policy fallback |
| tests/router_unit_tests/test_router_aresponses_streaming_fallback.py | Updates the router fallback assertion to require the mapped rate-limit exception |
| tests/llm_responses_api_testing/test_openai_responses_api.py | Updates the integration expectation to the typed bad-request contract |
Reviews (1): Last reviewed commit: "test(responses): import import_module in..." | Re-trigger Greptile
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 40f4fa2. Configure here.
TLDR
Problem this solves:
content_policy_violationis a bareAPIErrorcontent_policy_fallbacksand the client gets the raw errorHow it solves it:
stable/1.100.xwith-xexception_type, same as non-streamingcontent_policy_violation, 429, and 5xx stay fallback-eligible mid-streamcontent_policy_fallbacks; other client errors raise typedBackport notes
-x: 073d4fe, c246372, and fff7a2c, the three commits of fix(responses): route mid-stream error events through exception_type so content_policy_fallbacks fire #40988litellm/responses/streaming_iterator.pyandlitellm/router.pyapplied clean, so the source hunks are the same lines as fix(responses): route mid-stream error events through exception_type so content_policy_fallbacks fire #40988tests/test_litellm/test_router.pyconflicted because main's copy had drifted past this line; the resolution keeps only the two new tests plus theLiteLLMLoggingimport they needfrom importlib import import_moduletotests/test_litellm/responses/test_streaming_iterator_error_events.py, since this line's copy of the file did not import it yet and the cherry-picked test raisedNameErrorwithout itexception_type;ContentPolicyViolationError, 429, and 5xx are wrapped inMidStreamFallbackErrorwith the typed original attached; every other 4xx (context window,invalid_prompt) raises directly; the router unwraps the typed original only for the content-policy dispatchAPIError) intest_streaming_iterator_error_events.py,test_router_aresponses_streaming_fallback.py, andtest_openai_responses_api.py; status codes and messages in those tests are unchanged, only the class narrowsUser Flow
Before: a developer whose app streams from the Responses API through a v1.100.x proxy loses the whole answer when the model flags the content mid-stream, even with a content-policy fallback model configured
gpt-responses-primaryfall back to a Gemini modelPOST https://litellm-domain/v1/responseswith{"model":"gpt-responses-primary","input":"Write a one sentence greeting.","stream":true}response.createdthenresponse.in_progresserrorevent with codecontent_policy_violation("This content was flagged for possible cybersecurity risk...")data: {"error": {"code": "400", ...}}and the stream endsAfter: the same request quietly continues on the configured content-policy fallback model, so the developer gets a complete answer
POST https://litellm-domain/v1/responseswithstream:trueresponse.createdthenresponse.in_progresserrorevent with codecontent_policy_violationresponse.createdfor the Gemini model,response.output_text.delta,response.completed) thendata: [DONE]Relevant issues
Backport of #40988 (main). Design context for the context-window row: #15785
Affected release
Linear ticket
Resolves LIT-7759
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Shared setup (same for both legs): a two-worker proxy (
--num_workers 2) per side, the Before one booted from thestable/1.100.xtip and the After one from this PR's tip, both on the same config. The primaries point at a local stand-in for the OpenAI Responses endpoint that replays the exact mid-streamerrorevent the customer saw (a genuine on-demand mid-stream refusal cannot be triggered reliably), one path per scenario; the fallback is a real Gemini deployment, so every fallback in the After leg is a real provider call. The bridge deployment and the extra scenario deployments only serve the live A/B rows below the main caseThe upstream stand-in emits, on the
/cpvpath:lit7759_cb.instanceis a 12-lineCustomLoggerwhoseasync_log_failure_eventappends the class ofkwargs["exception"], itsstatus_code, and the class of itsoriginal_exceptionto a file, so the failure-callback row below is observed rather than reasonedBefore (1dba17b)
stable/1.100.xtip:response.created,response.in_progress, then the raw error, and no fallback runs:After (40f4fa2)
Live A/B for the other surfaces on the same two proxies, run in the same order on both sides: base 1dba17b (the current
stable/1.100.xtip, so there is no merge-ref drift to re-run) vs head 40f4fa2. Each row matches the corresponding row #40988 recorded on main/v1/responsescontext-window (context_length_exceeded, "exceeds the context window"): base surfaceslitellm.APIError400 with"type": null, head surfaceslitellm.BadRequestError: OpenAIException - Your input exceeds the context window of this model. ...400 with"type": "invalid_request_error"; both end the stream with no fallback, provider text preserved under the new typed prefix/v1/responsesrate limit (rate_limit_exceeded): base surfaceslitellm.APIError429 with"type": null, head surfaceslitellm.RateLimitError: RateLimitError: OpenAIException - Rate limit reached for gpt-5.4 ...429 with"type": "throttling_error"; both end the stream with no fallback under this config, provider text preserved/v1/responsesnon-content-policy 400 (invalid_prompt): base surfaceslitellm.APIError400, head surfaceslitellm.BadRequestError: OpenAIException - Invalid prompt: your prompt was rejected by the server.400; both end the stream with no fallback/v1/chat/completionsstreaming over the Responses bridge (openai/responses/gpt-5.4) with the same content-policy event and a content-policy fallback configured: HTTP 400 on both sides,litellm.APIError: This content was flagged ...on base andlitellm.BadRequestError: litellm.ContentPolicyViolationError: ContentPolicyViolationError: OpenAIException - This content was flagged ...on head, no fallback on either side (LIT-7757)/v1/chat/completionsnon-streaming over the bridge with the content-policy event: both sides fall back togemini-3.8-flashwith HTTP 200 and a real greeting (10.77s on base, 2.08s on head, both Gemini latency), unchanged/v1/messagesstreaming over the bridge with the content-policy event: HTTP 200,event: message_startwith"model": "responses/gpt-5.4", then the stream ends with no error event on both sides, pre-existing (LIT-6871). On the head the first pass of this row landed 2s after the non-streaming bridge refusal and got the router's 5s single-deployment cooldown (429 No deployments available ... Try again in 5 seconds ... cooldown_list=[...]); the base logs the same "Attempting to add ... to cooldown list" for the same refusal but its slower Gemini call put this row 11s later, outside the window. Re-run 60s later on both sides, both attempt 1: HTTP 200message_startthen silent end, identicalasync_log_failure_eventreadingkwargs["exception"]): base getsAPIError400 for content-policy, context-window, and invalid_prompt,MidStreamFallbackError429 wrappingAPIErrorfor rate limit, andAPIError400 for the chat-bridge stream; head getsMidStreamFallbackError400 wrappingContentPolicyViolationErrorfor content-policy on/v1/responsesand on the chat and messages bridge streams,BadRequestError400 for context-window and invalid_prompt, andMidStreamFallbackError429 wrappingRateLimitErrorfor rate limit; the non-streaming chat-bridge row logsContentPolicyViolationErroron both sides; status codes match on every rowType
🐛 Bug Fix
Caveats (if any)
Medium
/v1/chat/completionsand/v1/messagesshare the same mid-stream gap on this line too, out of scope; this backport leaves them as fix(responses): route mid-stream error events through exception_type so content_policy_fallbacks fire #40988 did on main/v1/messagesover the bridge ends silently on any mid-stream error: LIT-6871Low
except litellm.APIErroraround a Responses stream no longer catches a mid-stream context-window, rate-limit, or other client error, since those typed exceptions do not subclasslitellm.APIError. Catchopenai.APIError(or the specific type) instead; the HTTP status is unchanged and the provider text stays in the message under the new typed prefixmessagenow readslitellm.BadRequestError: OpenAIException - <provider text>(429:litellm.RateLimitError: RateLimitError: OpenAIException - <provider text>, the non-streaming path's own wording) instead oflitellm.APIError: <provider text>, andtypeis populated (invalid_request_error,throttling_error) instead of null;codeand the provider text are unchanged, so only a client string-matching the oldlitellm.APIError:prefix noticesexception_classlabel now see the typed class (BadRequestError,MidStreamFallbackErrorcarrying aContentPolicyViolationError) instead ofAPIError; status codes unchangedcontent_policy_violationstill raiseBadRequestErrormid-stream with no fallback, the same as the merge base and as their non-streaming path; openai and azure, the mappings the customer's config uses, map it toContentPolicyViolationError/v1/messagesrow above hit it once on the head only because that leg ran sooner), pre-existing and unrelated to the mid-stream pathlitellm.MidStreamFallbackError:prefix, pre-existingexceeds the context windowwording still maps toBadRequestError: LIT-6511errorevent at the network boundary because a genuine mid-stream refusal cannot be triggered on demand; the two-worker proxies, the router, and the Gemini fallback are live, and the injected event uses the{"type": "error", "error": {"type", "code", "message"}}shape captured from a real OpenAI Responses streamstable/1.100.xlacks the main-only test fixes test(bedrock): drop EOL cohere.command-r-plus-v1:0 from local_testing #39608 (Bedrockcohere.command-r-plus-v1:0retired, three tests, LIT-6876) and test(responses): expect the 404 OpenAI now returns for an unknown model #39457 (test_bad_request_errorexpects a 400 where OpenAI now answers 404), andtest_router_timeoutplustest_openai_embedding_timeoutsare the known tiny-timeout flakes (LIT-6670, LIT-6675). None touch the Responses stream or router fallback paths, the branch's owntest_router.pyandtest_streaming_iterator.pycases pass inunit-test, and CircleCI is not a required check on this basestable/1.100.xtip (1dba17b), so the merged tree equals the head tree; if the line moves before this merges, the main case is re-run on the merged treeFinal Attestation
Note
Medium Risk
Changes mid-stream Responses error types and router fallback dispatch; callers that only catch
litellm.APIErrormay miss new typed exceptions, though HTTP status and fallback behavior are intentional.Overview
Mid-stream Responses API error and response.failed events no longer become generic
litellm.APIError. They are mapped throughlitellm.exception_type(same as non-streaming), then either raised directly or wrapped inMidStreamFallbackErrorfor router fallbacks.Fallback eligibility is centralized in
_mid_stream_fallback_eligible:ContentPolicyViolationError, 429, and 5xx stay eligible for mid-stream fallback; other 4xx (e.g. context length, invalid prompt) raise the typed client error with no wrapper. Failed-stream logging uses the same mapper so callbacks see the typed exception.On the router, when a streaming
aresponsesfallback runs, aMidStreamFallbackErrorwhoseoriginal_exceptionisContentPolicyViolationErroris unwrapped beforeasync_function_with_fallbacks_common_utils, socontent_policy_fallbacksdispatch matches the refusal type instead of the envelope.Tests were updated/added for typed exceptions, content-policy mid-stream wrapping, router content-policy fallback routing, and continuation input after partial output.
Reviewed by Cursor Bugbot for commit 40f4fa2. Bugbot is set up for automated code reviews on this repo. Configure here.