Skip to content

fix(router): mid-stream fallback 400s on models without assistant prefill (Claude Sonnet 4.6+) - #30242

Open
cwang-otto wants to merge 8 commits into
BerriAI:litellm_internal_stagingfrom
cwang-otto:fix/midstream-fallback-prefill-claude46
Open

fix(router): mid-stream fallback 400s on models without assistant prefill (Claude Sonnet 4.6+)#30242
cwang-otto wants to merge 8 commits into
BerriAI:litellm_internal_stagingfrom
cwang-otto:fix/midstream-fallback-prefill-claude46

Conversation

@cwang-otto

@cwang-otto cwang-otto commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Mid-stream fallbacks fail deterministically on Claude Sonnet 4.6 / Opus 4.6+: the resume mechanism from #13149 appends the partial response as a prefixed assistant message, but Anthropic removed assistant prefill starting with these models (migration guide) — every fallback hop returns:

litellm.BadRequestError: AnthropicException - {"type":"invalid_request_error","message":"This model does not support assistant message prefill. The conversation must end with a user message."}

so a recoverable stream timeout becomes a hard failure for the entire fallback chain (anthropic -> vertex -> anthropic -> ... all 400). Same breakage reported across the ecosystem: livekit/agents#4907, crewAIInc/crewAI#4798, agno-agi/agno#7015.

Changes

  • Registry: supports_assistant_prefill: false for all *sonnet-4-6* entries in both cost maps (the opus-4-6/4-7/4-8 entries were already false — sonnet-4-6 was missed). The capability pin in test_claude_sonnet_4_6_config.py is updated accordingly.
  • router_utils/fallback_event_handlers.py: new build_mid_stream_continuation_messages — when the registry explicitly marks the primary model OR any configured fallback target for the group as not supporting prefill, the partial response rides a trailing user message (the continuation pattern Anthropic's migration guide documents: "Your previous response was interrupted and ended with [previous_response]. Continue from where you left off."). All other models (capability true, absent, or unknown) keep the existing prefill-resume behavior byte-identical.
  • router.py: both injection sites (sync + async) now share the helper and pass the active fallback config.

Pre-Submission checklist

  • I have added meaningful tests (tests/test_litellm/router_utils/test_fallback_event_handlers.py — 10 cases: prefill-rejecting primaries -> user continuation; prefill-rejecting fallback TARGET -> user continuation; prefill-supporting / capability-absent / unknown / no-model-group / unrelated-fallback-group -> legacy prefill)
  • My PR passes all unit tests on make test-unit (new file 10/10; test_claude_sonnet_4_6_config.py 2/2; existing mid-stream fallback tests in tests/test_litellm/test_router.py all pass — gpt-4 resume behavior unchanged)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai (both findings addressed in cd53134)

Screenshots / Proof of Fix

$ pytest tests/test_litellm/router_utils/test_fallback_event_handlers.py tests/test_litellm/test_claude_sonnet_4_6_config.py -q
............                                                             [100%]
12 passed in 0.20s

$ pytest tests/test_litellm/test_router.py -q -k "streaming_iterator or mid_stream"
11 passed, 79 deselected

@cwang-otto

Copy link
Copy Markdown
Contributor Author

@greptileai

@codspeed-hq

codspeed-hq Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing cwang-otto:fix/midstream-fallback-prefill-claude46 (ea7f9ac) with main (343e453)

Open in CodSpeed

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.46154% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/router_utils/fallback_event_handlers.py 91.66% 2 Missing ⚠️
litellm/router.py 50.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@cwang-otto
cwang-otto changed the base branch from main to litellm_oss_branch June 11, 2026 21:20
@cwang-otto
cwang-otto requested a review from a team June 11, 2026 21:20
@cwang-otto
cwang-otto force-pushed the fix/midstream-fallback-prefill-claude46 branch from ea7f9ac to de98087 Compare June 11, 2026 21:20
@greptile-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes deterministic mid-stream fallback failures for Claude Sonnet/Opus 4.6+ by detecting when the primary model or any configured fallback target explicitly rejects assistant prefill, then substituting the documented user-message continuation pattern instead of the legacy prefilled-assistant-message resume.

  • Registry sweep: All 27 *sonnet-4-6*/*opus-4-6* entries across every provider variant (Bedrock, Vertex AI, Azure AI, OpenRouter, Vercel, Perplexity, GitHub Copilot, Snowflake) in both the root cost map and the bundled backup JSON now carry supports_assistant_prefill: false; the snowflake entry is newly added to the backup.
  • New build_mid_stream_continuation_messages helper: Handles flat-string and dict-format fallback configurations correctly — flat-string lists check every entry directly (avoiding the get_fallback_model_group pop-on-iterate side-effect), while dict-format lists use a shallow copy to protect the live fallback queue from mutation before the actual fallback execution runs.
  • Router wiring: Both the sync and async mid-stream fallback injection sites are updated identically to delegate to the helper, passing the active fallback config so cross-model chains (e.g. claude-3-5-sonnet → claude-sonnet-4-6) also switch to the safe continuation form.

Confidence Score: 5/5

Safe to merge — all previously flagged gaps are closed and no new regressions introduced.

The registry sweep is comprehensive: all 27 provider-specific sonnet-4-6/opus-4-6 entries in both JSON files now carry the flag, including the previously-missed snowflake entry. The routing logic is conservative (only deviates from the existing prefill path when the flag is explicitly false) so models without registry data retain unchanged behavior. The flat-string-format pop-mutation issue is resolved with a direct iteration over the full list. The test suite covers all edge cases with local-map pinning and no network calls.

No files require special attention.

Important Files Changed

Filename Overview
litellm/router_utils/fallback_event_handlers.py Adds build_mid_stream_continuation_messages helper that routes to user-message continuation for prefill-rejecting models; handles flat-string and dict-format fallback lists correctly with mutation-safe copy for dict path.
litellm/router.py Both sync and async mid-stream fallback injection sites replaced with build_mid_stream_continuation_messages; identical change correctly propagated to both paths.
model_prices_and_context_window.json All 27 sonnet-4-6/opus-4-6 entries across every provider (bedrock, azure_ai, vertex_ai, openrouter, vercel, perplexity, github_copilot, snowflake) now have supports_assistant_prefill: false; snowflake entry newly added.
litellm/model_prices_and_context_window_backup.json Registry backup kept in sync with root cost map; all sonnet-4-6/opus-4-6 entries updated to supports_assistant_prefill: false including newly added snowflake entry.
tests/test_litellm/router_utils/test_fallback_event_handlers.py New test file with 10 offline (mock-pinned) tests covering prefill-rejecting primaries and fallback targets, legacy prefill preservation, non-mutation of live fallback lists, and flat-string rejecter at non-first position.
tests/test_litellm/test_claude_sonnet_4_6_config.py Updates the capability pin assertion from is True to is False — correcting a wrong baseline expectation to match the Anthropic migration reality, not weakening coverage.

Reviews (9): Last reviewed commit: "fix(lint): use X | None over Optional in..." | Re-trigger Greptile

@greptile-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes mid-stream fallback failures on Claude Sonnet 4.6 / Opus 4.6+, where Anthropic's removal of assistant prefill caused every fallback hop to return a hard 400 error. The fix adds a build_mid_stream_continuation_messages helper that checks the model registry and, when supports_assistant_prefill is explicitly false, substitutes a trailing user message for the partial content instead of the prefill pattern; the registry entries for Sonnet 4.6 are updated accordingly.

  • fallback_event_handlers.py / router.py: new helper deduplicates and corrects continuation logic for both sync and async paths; the safe-fallback default preserves legacy prefill for all models without an explicit false in the registry.
  • Registry (model_prices_and_context_window.json + backup): 10 of 12 Sonnet 4.6 entries were flipped to false; openrouter/anthropic/claude-sonnet-4.6 still has true (both files) and snowflake/claude-sonnet-4-6 is missing the field entirely, leaving those two deployment routes still broken.
  • utils.py: new supports_assistant_prefill() public function is added but not exported from litellm/__init__.py, so it is unreachable through the module's public surface.

Confidence Score: 3/5

The core routing logic and test coverage are solid. The fix is incomplete in the registry: openrouter/anthropic/claude-sonnet-4.6 still has true in both JSON files, and snowflake/claude-sonnet-4-6 is missing the field — both deployment routes remain broken after this merges.

The build_mid_stream_continuation_messages helper and router call-sites are well-implemented and safe. The registry update that makes the whole fix work is incomplete: openrouter/anthropic/claude-sonnet-4.6 retains supports_assistant_prefill: true in both JSON files and snowflake/claude-sonnet-4-6 is missing the field entirely, reproducing the exact 400 errors the PR aims to eliminate for those two deployment routes.

model_prices_and_context_window.json and litellm/model_prices_and_context_window_backup.json — the openrouter/anthropic/claude-sonnet-4.6 entry needs supports_assistant_prefill flipped to false in both, and snowflake/claude-sonnet-4-6 needs the field added with false in the primary JSON.

Important Files Changed

Filename Overview
model_prices_and_context_window.json 10 of 12 sonnet-4-6 entries updated to supports_assistant_prefill=false; openrouter/anthropic/claude-sonnet-4.6 still has true and snowflake/claude-sonnet-4-6 is missing the field entirely — both leave those routing paths broken.
litellm/model_prices_and_context_window_backup.json 10 entries updated to false; openrouter/anthropic/claude-sonnet-4.6 entry still has supports_assistant_prefill=true (snowflake entry is not present in backup so no issue there).
litellm/router_utils/fallback_event_handlers.py New build_mid_stream_continuation_messages helper correctly routes to user-message continuation when registry explicitly marks supports_assistant_prefill=false; safe fallback to legacy prefill for all other cases.
litellm/router.py Both sync and async mid-stream fallback injection sites now share the new helper, deduplicating the continuation logic cleanly.
litellm/utils.py New supports_assistant_prefill() function added but not exported from litellm/init.py, leaving it unreachable through the public module API.
tests/test_litellm/router_utils/test_fallback_event_handlers.py New test file with 8 cases covering both the user-continuation and legacy-prefill paths; uses monkeypatching to pin the local registry — no real network calls.

Comments Outside Diff (3)

  1. model_prices_and_context_window.json, line 27632 (link)

    openrouter/anthropic/claude-sonnet-4.6 missed in the registry sweep

    This entry still has "supports_assistant_prefill": true while every other Sonnet 4.6 variant (bare, regional, Vertex, Azure AI, etc.) was flipped to false. Any fallback chain whose model_group is openrouter/anthropic/claude-sonnet-4.6 will still hit the legacy assistant-prefill path and receive the same 400 error this PR is fixing for all other deployment routes. The same entry in litellm/model_prices_and_context_window_backup.json also needs the corresponding update.

  2. model_prices_and_context_window.json, line 41975-41989 (link)

    The snowflake/claude-sonnet-4-6 entry has no supports_assistant_prefill field at all. Because _prefill_explicitly_unsupported returns True only when the field is explicitly false (model_info.get("supports_assistant_prefill") is False), a missing field evaluates to None is FalseFalse, so the legacy prefill path is used. Snowflake-routed Sonnet 4.6 fallbacks would still hit the 400 error this PR is meant to eliminate.

  3. litellm/utils.py, line 2451-2472 (link)

    supports_assistant_prefill not exported from public API

    The new supports_assistant_prefill() function is added to utils.py but is not wired into litellm/__init__.py. All other supports_* helpers (supports_function_calling, supports_url_context, supports_web_search, etc.) are listed there as Callable[..., bool] entries. Without this export, the function is unreachable through the module's public surface — making the PR description's claim of "public accessor" inaccurate. Additionally, _prefill_explicitly_unsupported bypasses this function entirely and calls get_model_info directly, so the new function serves neither the internal fix nor an accessible external caller in its current state.

Reviews (1): Last reviewed commit: "fix(router): mid-stream fallback 400s on..." | Re-trigger Greptile

Comment thread litellm/router_utils/fallback_event_handlers.py Outdated
Comment thread litellm/utils.py Outdated
@cwang-otto

Copy link
Copy Markdown
Contributor Author

@greptileai

Comment thread litellm/router_utils/fallback_event_handlers.py Outdated
@cwang-otto

Copy link
Copy Markdown
Contributor Author

@greptileai Both summary findings addressed: the live-fallbacks mutation was fixed in 4fdf8a6 (shallow copy + regression test asserting flat-string and dict formats stay unmutated — your summary reviewed cd53134, one commit earlier), and the three dot-variant registry keys (openrouter/anthropic/claude-sonnet-4.6, openrouter/anthropic/claude-opus-4.6, vercel_ai_gateway/anthropic/claude-opus-4.6) are flipped in 68df6e1 with a dot-variant test param added.

@cwang-otto

Copy link
Copy Markdown
Contributor Author

@greptileai

@greptile-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

All three findings are verified resolved in the current HEAD:

  • P1 (list mutation)list(fallbacks) shallow copy is in place at the get_fallback_model_group call site, with the comment explaining why. Regression test (test_fallbacks_list_is_not_mutated_by_capability_check) covers both flat-string and dict formats.
  • P2 (cross-model fallback gap)build_mid_stream_continuation_messages now resolves and checks all fallback targets for the model group, not just the primary. Covered by test_prefill_rejecting_fallback_target_gets_user_continuation.
  • P2 (public accessor / unknown-model default mismatch) — the public supports_assistant_prefill wrapper was dropped; routing reads get_model_info directly, so there's only one tri-state path and no disagreement on the unknown case.
  • Dot-variant registry keys — the three dot-variant entries are confirmed flipped in 68df6e1f75.

The implementation is clean. LGTM.

@cwang-otto

Copy link
Copy Markdown
Contributor Author

@greptileai

@greptile-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Want your agent to iterate on Greptile's feedback? Try greploops.

@Sameerlite

Copy link
Copy Markdown
Contributor

@cwang-otto can you please get greptile score to 5/5 and also please share a before and after to understand the fix betterly for maintainers. Thanks!

@cwang-otto

cwang-otto commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

@cwang-otto can you please get greptile score to 5/5 and also please share a before and after to understand the fix betterly for maintainers. Thanks!

@Sameerlite. Ah. thanks. Paste the before and after here first:

Verification (real API calls, otto's anthropic→vertex fallback chain)

Local harness (scripts/debug/test_midstream_prefill_fallback.py, not checked in): streams one real chunk, then injects the mid-stream failure exactly where litellm's streaming handler raises on socket timeouts — everything downstream (Router fallback resolution, continuation build, live API call) runs real.

pypi 1.87.0 (current) this PR
continuation sent to the fallback {"role": "assistant", "content": "The capital of France is", "prefix": true} {"role": "user", "content": "Your previous response was interrupted and ended with: The capital of France is …continue"}
live API response 400 — "This model does not support assistant message prefill. The conversation must end with a user message.", then "No fallback model group found" → the consuming async for raises mid-loop, turn dies (exit 1) — reproduces trace 97b386b3 200 — fallback chunks continue on the same iterator: 'The capital of France isParis.' (exit 0)
negative control n/a capability forced back to true → legacy prefill path fails with the same prefill 400 — proves the pass is caused by the fix

Also the score is 5/5 now !

@cwang-otto

Copy link
Copy Markdown
Contributor Author

Fixed in 81527f1. Re-swept the full 4-6/4-7 family (dash + dot keys, both registry files): the flagged perplexity/anthropic/claude-opus-4-7 plus two more stragglers that landed after the original sweep — perplexity/anthropic/claude-opus-4-6 and github_copilot/claude-opus-4.6-fast (the latter is mode:chat, so real fallback exposure). Zero entries in the family now lack supports_assistant_prefill:false.

@cwang-otto

Copy link
Copy Markdown
Contributor Author

@greptileai

@cwang-otto
cwang-otto force-pushed the fix/midstream-fallback-prefill-claude46 branch from 81527f1 to 35a55a8 Compare June 17, 2026 15:46
@cwang-otto
cwang-otto changed the base branch from litellm_oss_branch to litellm_internal_staging June 17, 2026 15:46
@cwang-otto
cwang-otto force-pushed the fix/midstream-fallback-prefill-claude46 branch from a2f00da to 43a3a11 Compare June 17, 2026 16:22
@Sameerlite

Copy link
Copy Markdown
Contributor

Thanks for the PR! A couple of things to get this over the finish line:

  • CI checks are currently failing — could you take a look? If any failures are pre-existing or unrelated to your change, a quick note in a comment helps us move faster.

Triggering Greptile for a code review in the meantime:

@greptileai

@cwang-otto

Copy link
Copy Markdown
Contributor Author

Thanks for the PR! A couple of things to get this over the finish line:

  • CI checks are currently failing — could you take a look? If any failures are pre-existing or unrelated to your change, a quick note in a comment helps us move faster.

Triggering Greptile for a code review in the meantime:

@greptileai

Ah looks like transient test infra issue. Re-triggering the CI tests.

@cwang-otto
cwang-otto force-pushed the fix/midstream-fallback-prefill-claude46 branch from 1975037 to 563a562 Compare June 18, 2026 04:10
…fill support

Anthropic removed assistant message prefill starting with Claude Sonnet 4.6 / Opus 4.6 (per the official migration guide it returns a 400: 'This model does not support assistant message prefill'). The mid-stream fallback resume (BerriAI#13149) appends the partial response as a prefixed assistant message, so every mid-stream fallback for these models fails deterministically across the whole fallback chain - converting recoverable stream timeouts into hard failures.

- registry: supports_assistant_prefill=false for all *sonnet-4-6* entries in both cost maps (opus-4-6/4-7/4-8/fable entries were already false)
- router_utils: build_mid_stream_continuation_messages - when the registry explicitly marks prefill unsupported, the partial response rides a trailing USER message (the continuation pattern Anthropic's migration guide documents); all other models keep the existing prefill-resume behavior unchanged
- router: both (sync + async) injection sites now share the helper
- utils: public supports_assistant_prefill() accessor (the registry field existed with 240 entries but had no supports_* accessor)
…ic accessor, update capability pin test

- Greptile: the same continuation messages go to every fallback target, so the user-message continuation now engages when the primary OR any configured fallback target for the group is explicitly marked prefill-unsupported (via get_fallback_model_group)
- Greptile: drop the supports_assistant_prefill() public accessor - _supports_factory's absent-key=False default contradicts this feature's absent-key=legacy-prefill routing; the routing reads get_model_info directly
- CI: test_claude_sonnet_4_6_config pinned the stale capability value; prefill was removed in Sonnet 4.6 so the pin is now False
…_ai_gateway)

The sweep matched the dash form (sonnet-4-6) and missed openrouter/anthropic/claude-sonnet-4.6, openrouter/anthropic/claude-opus-4.6, vercel_ai_gateway/anthropic/claude-opus-4.6 (dot form). Test param added for the dot variant.
… github_copilot opus-4.6-fast lack supports_assistant_prefill:false
…ages

Swap List[...] -> list[...] in the new helper so the change adds 0 UP006 violations; the litellm_internal_staging strict-rule budget is tighter than litellm_oss_branch and flagged the 4 added annotations.
…ry flag

Greptile findings:

1. build_mid_stream_continuation_messages now scans every entry of a flat string-format fallback list directly. get_fallback_model_group pops a single string mid-iteration, so a prefill-rejecting model at a non-first position was never capability-checked; since the continuation is built once and reused across all fallback hops, the chain still 400'd when it reached that model. Dict/standard format keeps using get_fallback_model_group (already returns the full group).

2. snowflake/claude-sonnet-4-6 gets supports_assistant_prefill:false in both the root map and the bundled backup (it existed only in root before; the backup is the offline/test source), completing the 4-6 sweep.

Tests: +snowflake registry param, +flat-string non-first-position -> user continuation, +flat-string all-supporting -> legacy prefill.
Rebased onto current litellm_internal_staging, whose strict-rule budget now tracks UP045 (non-pep604 annotations). Convert the 4 Optional[...] annotations the helper added to X | None so the change stays within budget.
@cwang-otto
cwang-otto force-pushed the fix/midstream-fallback-prefill-claude46 branch from 563a562 to fd2fce1 Compare June 18, 2026 04:22
@cwang-otto

Copy link
Copy Markdown
Contributor Author

@greptileai Both findings from the 4/5 review are addressed (head fd2fce1):

1. Flat string-format fallback gap. build_mid_stream_continuation_messages now checks every entry of a flat string-format fallback list directly, instead of routing through get_fallback_model_group. The latter pops a single string mid-iteration, so a prefill-rejecting model at a non-first position was never capability-checked — and since the continuation is built once and reused across every fallback hop, the chain still 400'd when it reached that model. Dict/standard-format fallbacks keep using get_fallback_model_group (it already returns the full group for the matching model). Covered by test_flat_string_fallback_prefill_rejecter_at_non_first_position (rejecter at index 1 → user continuation) and test_flat_string_fallback_all_prefill_supporting_keeps_legacy.

2. snowflake/claude-sonnet-4-6. Added supports_assistant_prefill: false to both the root cost map and the bundled ..._backup.json (the entry previously existed only in root; the backup is what the offline/local map loads, so a root-only flag would be invisible to tests and the offline path). This closes the last gap in the 4-6 sweep — no *sonnet-4-6 / *opus-4-6/4-7 entry lacks the flag now. Covered by adding snowflake/claude-sonnet-4-6 to the prefill-rejecting parametrized test.

All lint gates (strict-rule budget, type-discipline, basedpyright) and the unit suite pass.

@cwang-otto

Copy link
Copy Markdown
Contributor Author

@Sameerlite updated the PR. can take another look. Thanks!

@Sameerlite
Sameerlite requested a review from mateo-berri June 18, 2026 12:49
@Sameerlite

Copy link
Copy Markdown
Contributor

@mateo-berri can you review this please? thanks

@mateo-berri

Copy link
Copy Markdown
Contributor

Taking a look...

@cwang-otto

Copy link
Copy Markdown
Contributor Author

hey hey
@mateo-berri @Sameerlite anything else I need to do?

@Sameerlite

Copy link
Copy Markdown
Contributor

@mateo-berri bump on this

deepanshululla pushed a commit to deepanshululla/litellm that referenced this pull request Jul 29, 2026
Removing the continuation-prompt fallback (retrying with the partial
response as a prefixed assistant message) so a stream failing after
partial content always re-raises instead was a scope decision beyond
what this PR's title/issue (BerriAI#31874) describe, and it directly conflicts
with BerriAI#30242/BerriAI#30743, which are already fixing the same code path for
Anthropic's removal of assistant-message prefill on Sonnet 4.6+/Opus
4.6+. Landing this PR's version first would delete the branch those PRs
are patching; landing theirs first would have this PR undo their fix on
rebase.

Restores the original prefill-based continuation-resume behavior
(including the is_pre_first_chunk guard already in litellm_internal_staging)
in both _acompletion_streaming_iterator and _completion_streaming_iterator,
and removes _stream_chunks_have_generated_content along with the tests
that only existed to cover the guard. This PR now only touches the
deferred-stream eager-fetch fix and the header-stripping fixes; the
non-text-content re-raise idea becomes a follow-up PR built on top of
whichever of BerriAI#30242/BerriAI#30743 lands.
deepanshululla pushed a commit to deepanshululla/litellm that referenced this pull request Aug 4, 2026
Removing the continuation-prompt fallback (retrying with the partial
response as a prefixed assistant message) so a stream failing after
partial content always re-raises instead was a scope decision beyond
what this PR's title/issue (BerriAI#31874) describe, and it directly conflicts
with BerriAI#30242/BerriAI#30743, which are already fixing the same code path for
Anthropic's removal of assistant-message prefill on Sonnet 4.6+/Opus
4.6+. Landing this PR's version first would delete the branch those PRs
are patching; landing theirs first would have this PR undo their fix on
rebase.

Restores the original prefill-based continuation-resume behavior
(including the is_pre_first_chunk guard already in litellm_internal_staging)
in both _acompletion_streaming_iterator and _completion_streaming_iterator,
and removes _stream_chunks_have_generated_content along with the tests
that only existed to cover the guard. This PR now only touches the
deferred-stream eager-fetch fix and the header-stripping fixes; the
non-text-content re-raise idea becomes a follow-up PR built on top of
whichever of BerriAI#30242/BerriAI#30743 lands.
yassin-berriai pushed a commit that referenced this pull request Aug 4, 2026
…errors in _acompletion fallback path (#34627)

* fix(router): eagerly fetch deferred stream to surface HTTP errors in fallback path

Providers like Vertex AI and Bedrock defer their HTTP call until the first
__anext__ on the returned CustomStreamWrapper (completion_stream=None,
make_call set). Errors raised inside __anext__ (e.g. 429, 503) escape the
_acompletion try/except block, so fail_calls is never incremented, deployment
cooldown does not fire, and the standard fallback chain is bypassed.

Call fetch_stream() on the wrapper before delegating to
_acompletion_streaming_iterator when completion_stream is None and make_call
is set. Any HTTP error now propagates through _acompletion's except block,
increments fail_calls, and enters the normal retry/fallback chain.

Strip Content-Length, Transfer-Encoding, Content-Encoding, and Content-Type
from exception headers at the same point to prevent HTTP framing mismatches
when LiteLLM builds its own error response body.

Add a re-raise guard in _acompletion_streaming_iterator (async and sync paths)
so MidStreamFallbackError with already-generated content re-raises to the
caller instead of silently injecting a continuation prompt into a fresh request
to a fallback model.

Apply logging cleanup in async_function_with_fallbacks_common_utils: use
%s-style formatting and exc_info=True instead of f-strings with
traceback.format_exc().

* fix(router): undo success_calls on deferred-stream fetch failure; broaden header strip

* fix(router): extract header-strip helper to keep _acompletion under strict C901 threshold

* test(router): add unit tests for _strip_http_framing_headers to satisfy router coverage gate

* test(router): add sync _completion_streaming_iterator re-raise test for mid-chunk MidStreamFallbackError

* fix(router): restore Fallbacks context in no-fallback log; document update_team mcp_rpm_limit

The log and debug message when no fallback model group is found was missing
the Fallbacks list, making it hard to understand why routing failed.

Also adds the missing mcp_rpm_limit documentation to update_team to fix
the documentation_test_api_docs CI check.

* fix(router): preserve original traceback in deferred stream fetch error re-raise

Using bare `raise` instead of `raise fetch_err` keeps the full inner
traceback from fetch_stream() intact so the error origin is visible in
logs and debuggers without being anchored to this line.

* style(test): restore black-style formatting in test_router.py

An earlier commit on this branch collapsed the file's pre-existing
multi-line formatting into single lines while adding the deferred-stream
tests, producing a diff full of unrelated reformatting noise. Restores
the untouched code to its original formatting; the actual new/changed
test content is unaffected (verified via AST comparison).

* fix(router): re-raise mid-stream fallback on any generated content, not just text

The re-raise guard added for MidStreamFallbackError only checked
generated_content, which tracks text deltas alone. A stream that emitted a
tool-call or reasoning-only chunk before failing had generated_content=""
despite already streaming to the client, so the router silently retried
and the client saw duplicated/inconsistent output. The guard now also
inspects the wrapper's raw chunks for tool_calls/reasoning_content.

Also moves the deferred-stream HTTP-framing-header stripping out of
Router._acompletion into the proxy's _handle_llm_api_exception: Router is
used directly as an SDK as well as by the proxy, and stripping headers
there dropped legitimate provider metadata (content-type,
proxy-authenticate) for direct SDK callers who never see the proxy's own
response construction.

schema.d.ts regenerated via make pre-commit; unrelated to this change.

* test(router): add direct coverage for _stream_chunks_have_generated_content

CI's router_code_coverage check flags any router.py function never referenced
by name in a test file; the new helper was only exercised indirectly through
the mid-stream re-raise guard tests.

* revert(ui): drop incidental schema.d.ts regeneration

Committing router.py/common_request_processing.py touched
pre_commit_lint.sh's litellm/proxy trigger for the API-type-sync check,
which force-regenerated schema.d.ts even though neither file changes any
route or model. The regenerated ordering of two unrelated Union/enum
fields (stream_timeout, user_role) isn't stable across process
invocations even against completely unmodified backend code (confirmed
by regenerating twice against the pre-existing committed code and getting
the same diff both times), so this reverts to the original committed
file rather than chase non-deterministic output.

* fix(proxy): strip framing headers on the pre-existing ProxyException branch too

_handle_llm_api_exception filtered framing headers into a local `headers`
dict, but for an exception that's already a ProxyException, it merged
{**e.headers, **headers}: the original e.headers came first, so a framing
header present there but absent from the filtered `headers` (because it
was just stripped) was never overwritten and survived into the response
unfiltered. Filters the merged result instead of relying on the merge
order to do it implicitly.

* chore: retrigger CI (no GitHub Actions check-suite was created for the previous two pushes)

* fix(router): detect thinking_blocks as generated content in mid-stream guard

Greptile flagged that a thinking-only delta (Anthropic extended thinking,
Delta.thinking_blocks) wasn't recognized as already-streamed content, so
a stream that emitted only thinking blocks before failing could still
restart via fallback and append an unrelated response after content the
client already received.

* fix(proxy): strip browser-facing security headers from provider exceptions too

veria-ai flagged that the framing-header denylist still let a malicious or
misconfigured provider set browser-facing headers (Access-Control-Allow-Origin,
Content-Security-Policy, Clear-Site-Data, etc.) on the proxy's own error
response. Adds a dedicated _BROWSER_SECURITY_HEADERS set alongside the
existing framing one and strips both wherever provider exception headers
reach the client response.

* refactor(router): address maintainer review mechanicals

- List[ModelResponseStream] -> list[ModelResponseStream] in
  _stream_chunks_have_generated_content (ruff UP006 strict-budget gate)
- drop _strip_http_framing_headers and its 3 tests: the proxy inlines the
  filter directly now, so the helper has had no production caller since
  the header-stripping was moved out of Router
- move HTTP_FRAMING_HEADERS/BROWSER_SECURITY_HEADERS/
  UNSAFE_PROXY_RESPONSE_HEADERS from router.py into litellm/constants.py,
  removing the router.py <-> proxy import path the two CodeQL
  cyclic-import alerts were pointing at
- move the eager fetch_stream() call before success_calls/logging/
  _track_deployment_metrics instead of incrementing then compensating
  with a manual decrement on failure
- fix a dead assert message: `mock_fallback.assert_not_called(), "..."`
  built a tuple, not an assert-with-message; assert_not_called() already
  raises on its own so this just drops the inert string

* revert(router): pull mid-stream continuation-removal out of this PR

Removing the continuation-prompt fallback (retrying with the partial
response as a prefixed assistant message) so a stream failing after
partial content always re-raises instead was a scope decision beyond
what this PR's title/issue (#31874) describe, and it directly conflicts
with #30242/#30743, which are already fixing the same code path for
Anthropic's removal of assistant-message prefill on Sonnet 4.6+/Opus
4.6+. Landing this PR's version first would delete the branch those PRs
are patching; landing theirs first would have this PR undo their fix on
rebase.

Restores the original prefill-based continuation-resume behavior
(including the is_pre_first_chunk guard already in litellm_internal_staging)
in both _acompletion_streaming_iterator and _completion_streaming_iterator,
and removes _stream_chunks_have_generated_content along with the tests
that only existed to cover the guard. This PR now only touches the
deferred-stream eager-fetch fix and the header-stripping fixes; the
non-text-content re-raise idea becomes a follow-up PR built on top of
whichever of #30242/#30743 lands.

* fix(proxy): re-filter unsafe headers after the response-headers hook merge

_handle_llm_api_exception filtered provider/framing headers once, then
merged in post_call_response_headers_hook's return value afterward
without re-filtering. The ProxyException branch happened to re-filter
after its own header merge, but the HTTPException/httpx.HTTPStatusError/
generic-exception branches passed the post-hook headers straight through
unfiltered, so a callback hook (any custom guardrail/logging plugin)
returning an unsafe header would bypass the strip entirely for those
paths. Filters once, right after the hook merge, so every branch gets
the same guarantee.

* Revert "revert(router): pull mid-stream continuation-removal out of this PR"

This reverts commit c5ca101.

* fix(router): detect reasoning_items as generated content in mid-stream guard

Greptile flagged that a structured reasoning-only delta (Delta.reasoning_items,
the OpenAI Responses-API-style reasoning item) wasn't recognized as
already-streamed content by _stream_chunks_have_generated_content, alongside
the existing thinking_blocks/tool_calls checks, so a stream that emitted only
reasoning_items before failing could still restart via fallback.

* fix(router): annotate _stream_chunks_have_generated_content with Sequence, not list

The type_discipline_gate LIT001 check flags mutable-collection parameter
annotations. chunks is only iterated, never mutated, so Sequence is the
correct read-only annotation and clears the ratcheted budget ceiling.

* fix(router): surface original provider exception, not the internal wrapper, when mid-stream fallback gives up

When content has already streamed and MidStreamFallbackError carries
original_exception (e.g. RateLimitError), both the async and sync
streaming iterators bare-re-raised the wrapper itself, so the client
lost the specific error type/code/provider_specific_fields instead of
seeing the real provider error. The fallback-failure path a few lines
below already unwraps to original_exception for the same reason; apply
the same pattern here.

Also extend _stream_chunks_have_generated_content to recognize audio,
images, and annotations deltas as generated content, matching
is_chunk_non_empty's existing annotations check and Delta's treatment
of audio/images as first-class content fields — a stream carrying only
one of these before failing was not recognized as already-streamed,
so the router could still restart it via fallback after the client had
received real content.

* chore: retrigger CI (frontend-lint cancelled, schema.d.ts flake)

frontend-lint's check-run shows conclusion=cancelled on 70e47f4 with
no superseding run, and this PR touches no UI files. Verify schema.d.ts
matches the proxy OpenAPI spec is on the previously diagnosed
stream_timeout/user_role Union-ordering nondeterminism (e9fc5e5).
Empty commit to force a fresh CI run for both rather than a manual
rerun, which requires repo admin rights this fork PR doesn't have.

---------

Co-authored-by: Deepanshu <deepanshu.lulla@alpha-sense.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants