Skip to content

fix(router): wrap aresponses streaming iterator for mid-stream fallbacks - #28214

Closed
cwang-otto wants to merge 1 commit into
BerriAI:mainfrom
cwang-otto:fix/aresponses-streaming-fallback-upstream
Closed

fix(router): wrap aresponses streaming iterator for mid-stream fallbacks#28214
cwang-otto wants to merge 1 commit into
BerriAI:mainfrom
cwang-otto:fix/aresponses-streaming-fallback-upstream

Conversation

@cwang-otto

Copy link
Copy Markdown
Contributor

Summary

MidStreamFallbackError raised mid-stream during Router.aresponses(stream=True) bypasses the Router's fallback chain, so configured cross-provider fallbacks (e.g. anthropic → vertex_ai) never fire when the primary provider's stream fails mid-flight.

The chat completions path wraps its CustomStreamWrapper via _acompletion_streaming_iterator, which catches MidStreamFallbackError and re-enters async_function_with_fallbacks_common_utils. The Responses API path doesn't have an equivalent: it dispatches through _ageneric_api_call_with_fallbacks_ageneric_api_call_with_fallbacks_helper, which awaits litellm.aresponses(**response_kwargs) and returns the streaming iterator unwrapped. Any MidStreamFallbackError raised during iteration (e.g. by the underlying CustomStreamWrapper when LiteLLMCompletionStreamingIterator bridges through completion) propagates past the Router.

Observed in production: Anthropic socket timed out before the first chunk on Router.aresponses(stream=True). Configured anthropic → vertex_ai fallback was never invoked; the error surfaced to the caller.

Changes

  • New Router._aresponses_streaming_iterator mirroring _acompletion_streaming_iterator. Wraps BaseResponsesAPIStreamingIterator, catches MidStreamFallbackError, re-enters async_function_with_fallbacks_common_utils with original_function=_ageneric_api_call_with_fallbacks_helper and original_generic_function preserved so the helper invokes litellm.aresponses on each fallback attempt. The wrapper subclasses BaseResponsesAPIStreamingIterator (bypassing the parent constructor) to preserve isinstance compatibility with downstream consumers (proxy cursor endpoint at litellm/proxy/response_api_endpoints/endpoints.py and litellm/interactions/litellm_responses_transformation/handler.py).
  • New Router._aresponses_with_streaming_fallbacks dispatcher that snapshots kwargs (preserving original_generic_function), calls _ageneric_api_call_with_fallbacks, and wraps the result when stream=True and the response is a BaseResponsesAPIStreamingIterator.
  • Split "aresponses" out of the generic factory_function dispatch bucket so it flows through the new wrapper.

Scope

Pre-first-chunk retry only — covers the observed timeout-before-first-chunk case. The Responses-API input shape differs from chat completions, so the partial-content prefix assistant-message continuation used in _acompletion_streaming_iterator doesn't translate cleanly and is intentionally out of scope.

Test plan

  • New unit test tests/test_litellm/test_router.py::test_aresponses_streaming_iterator_fallback mirrors test_acompletion_streaming_iterator and asserts:
    • initial chunks stream through
    • MidStreamFallbackError triggers async_function_with_fallbacks_common_utils
    • kwargs["original_function"] is _ageneric_api_call_with_fallbacks_helper
    • kwargs["original_generic_function"] is litellm.aresponses
    • model_group is the original model and disable_fallbacks=False
    • fallback chunks stream through
    • wrapper preserves _hidden_params and isinstance(BaseResponsesAPIStreamingIterator)
  • All 7 existing _acompletion_streaming_iterator tests still pass.
  • uv run black . and uv run ruff check litellm/router.py clean on touched files.

MidStreamFallbackError raised during aresponses streaming was bypassing the Router's fallback chain because only the chat completions path wrapped its CustomStreamWrapper. The aresponses dispatch goes through _ageneric_api_call_with_fallbacks which returns the streaming iterator unwrapped, so any MidStreamFallbackError propagated past the Router and the configured cross-provider fallback (e.g. anthropic -> vertex_ai) never fired.

Add _aresponses_streaming_iterator (parity with _acompletion_streaming_iterator) plus a thin _aresponses_with_streaming_fallbacks dispatch wrapper. On MidStreamFallbackError, re-enter the chain via async_function_with_fallbacks_common_utils with original_function set to the per-attempt helper and original_generic_function preserved, so the helper invokes litellm.aresponses on each fallback attempt.

Scope: pre-first-chunk retry only. Responses-API input shape differs from chat completions, so partial-content continuation is intentionally out of scope.
@cwang-otto

Copy link
Copy Markdown
Contributor Author

Re-opening against shin_agent_oss_staging_05_19_2026 per the Guard main branch policy (see #28201 for the pattern).

@cwang-otto cwang-otto closed this May 19, 2026
@codspeed-hq

codspeed-hq Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing cwang-otto:fix/aresponses-streaming-fallback-upstream (9bc5e21) with main (a72414a)

Open in CodSpeed

@codecov

codecov Bot commented May 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.38462% with 16 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/router.py 75.38% 16 Missing ⚠️

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes mid-stream fallback handling for Router.aresponses(stream=True) by introducing a _aresponses_streaming_iterator wrapper (mirroring the existing chat-completions _acompletion_streaming_iterator) and a _aresponses_with_streaming_fallbacks dispatcher. Previously, a MidStreamFallbackError raised during streaming would propagate unhandled past the Router, so configured cross-provider fallbacks (e.g., anthropic → vertex_ai) never fired.

  • _aresponses_streaming_iterator wraps the returned BaseResponsesAPIStreamingIterator, catches MidStreamFallbackError, and re-enters async_function_with_fallbacks_common_utils with the per-attempt helper so fallback deployments are picked correctly; it subclasses BaseResponsesAPIStreamingIterator for isinstance compatibility while bypassing the parent constructor.
  • _aresponses_with_streaming_fallbacks snapshots kwargs before the initial attempt, calls _ageneric_api_call_with_fallbacks, and wraps the result when stream=True; the \"aresponses\" call type is split out of the generic factory dispatch bucket to route through this new wrapper.
  • A new mock-only unit test mirrors the existing test_acompletion_streaming_iterator suite and verifies chunk forwarding, fallback invocation, and correct kwarg threading.

Confidence Score: 4/5

The happy path (no MidStreamFallbackError) is unchanged; the new wrapper is only activated on streaming Responses API calls, making the blast radius of any regression small.

The change is narrowly scoped to the Responses API streaming path and does not touch the chat-completions or synchronous fallback paths. Two maintainability concerns are present: FallbackResponsesStreamWrapper leaves several BaseResponsesAPIStreamingIterator attributes uninitialised, and the shallow kwargs.copy() lets litellm_metadata be mutated by the primary attempt so fallback attempts carry a stale model-group in that dict.

litellm/router.py — specifically the FallbackResponsesStreamWrapper.init and the fallback_kwargs = kwargs.copy() snapshot in _aresponses_with_streaming_fallbacks.

Important Files Changed

Filename Overview
litellm/router.py Adds _aresponses_streaming_iterator and _aresponses_with_streaming_fallbacks to bring mid-stream fallback parity to the Responses API path; the FallbackResponsesStreamWrapper bypasses super().__init__() leaving several base-class attributes uninitialised (safe today but fragile), and the shallow kwargs.copy() snapshot can propagate stale litellm_metadata to fallback attempts.
tests/test_litellm/test_router.py Adds a well-structured mock-only unit test that verifies chunk forwarding, MidStreamFallbackError interception, correct kwargs passed to the fallback utils, and isinstance preservation; no real network calls.

Reviews (1): Last reviewed commit: "fix(router): wrap aresponses streaming i..." | Re-trigger Greptile

Comment thread litellm/router.py
Comment on lines +2241 to +2275
Subclasses BaseResponsesAPIStreamingIterator only for isinstance
compatibility (proxy + interactions code paths check the type).
Bypasses the parent constructor and delegates iteration to an
async generator.
"""

def __init__(self, async_generator: AsyncGenerator):
self._async_generator = async_generator
self.finished = False
# Preserve hidden params and identity attributes so response
# headers (model_id, api_base, additional_headers) still flow.
self._hidden_params = dict(
getattr(source_iterator, "_hidden_params", {}) or {}
)
self.model = getattr(source_iterator, "model", "")
self.custom_llm_provider = getattr(
source_iterator, "custom_llm_provider", None
)
self.logging_obj = getattr(source_iterator, "logging_obj", None)
self.litellm_metadata = getattr(
source_iterator, "litellm_metadata", None
)
self.responses_api_provider_config = getattr(
source_iterator, "responses_api_provider_config", None
)

def __aiter__(self):
return self

async def __anext__(self):
return await self._async_generator.__anext__()

async def aclose(self):
close = getattr(self._async_generator, "aclose", None)
if close is not None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Incomplete base-class attribute initialization

FallbackResponsesStreamWrapper intentionally bypasses super().__init__() but leaves many BaseResponsesAPIStreamingIterator instance attributes unset: completed_response, _stream_created_time, start_time, _failure_handled, response, request_data, call_type, _completed_response_cached, _completed_response_logged, _completed_response_cache_hit, and _persist_completed_response_before_logging.

Currently this is safe because FallbackResponsesStreamWrapper.__anext__ delegates entirely to the async generator (so base-class methods like _check_max_streaming_duration and _log_success are never called on the wrapper itself). However, any code path that receives a BaseResponsesAPIStreamingIterator and accesses instance.completed_response or similar (e.g., new post-stream logging hooks) will get AttributeError on this wrapper. Initializing these with safe sentinels (e.g., self.completed_response = None, self._failure_handled = False, self._stream_created_time = time.time()) would prevent that class of failure.

Comment thread litellm/router.py
Comment on lines +4417 to +4418
fallback_kwargs = kwargs.copy()
fallback_kwargs["original_generic_function"] = original_function

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Shallow copy shares mutable nested objects across primary and fallback attempts

fallback_kwargs = kwargs.copy() is a shallow copy. When _ageneric_api_call_with_fallbacks(**kwargs) runs, it calls _update_kwargs_before_fallbacks which does kwargs.setdefault("litellm_metadata", {}).update({"model_group": model, ...}). If litellm_metadata already exists in kwargs, the in-place .update() mutates the same dict object referenced by fallback_kwargs["litellm_metadata"]. As a result, by the time a MidStreamFallbackError triggers and stream_with_fallbacks calls _update_kwargs_before_fallbacks again (with the default metadata_variable_name="metadata", not "litellm_metadata"), fallback_kwargs["litellm_metadata"]["model_group"] still carries the primary deployment's model group rather than the fallback model group. This means the fallback attempt's litellm_metadata is stale, which can produce incorrect routing metadata in logs.

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.

1 participant