From a634cb9b62ecfe7e37de8715bf2e2cd763b2a8b2 Mon Sep 17 00:00:00 2001 From: shivam Date: Wed, 29 Jul 2026 00:30:33 +0000 Subject: [PATCH] fix(responses): initialize completed_response and _hidden_params on the completion bridge iterator LiteLLMCompletionStreamingIterator subclasses ResponsesAPIStreamingIterator but never runs the base constructor, so it lacks completed_response and _hidden_params. When a provider errors before the first content chunk, the router reads completed_response directly in _extract_partial_responses_usage and raises AttributeError, and the missing _hidden_params makes the proxy pre-wrap the iterator for header attachment so the Responses mid-stream fallback path is skipped entirely. Initialize both on the bridge iterator so provider errors surface and fallbacks run. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../streaming_iterator.py | 7 +++- ...st_router_aresponses_streaming_fallback.py | 40 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/litellm/responses/litellm_completion_transformation/streaming_iterator.py b/litellm/responses/litellm_completion_transformation/streaming_iterator.py index cf69654d15d..2aded792f73 100644 --- a/litellm/responses/litellm_completion_transformation/streaming_iterator.py +++ b/litellm/responses/litellm_completion_transformation/streaming_iterator.py @@ -67,7 +67,12 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator): self.request_input: str | ResponseInputParam = request_input self.responses_api_request: ResponsesAPIOptionalRequestParams = responses_api_request self.custom_llm_provider: str | None = custom_llm_provider - self.litellm_metadata: dict | None = litellm_metadata or {} + self.litellm_metadata = litellm_metadata or {} + self.completed_response: Any | None = None + _wrapper_hidden_params = getattr(litellm_custom_stream_wrapper, "_hidden_params", None) + self._hidden_params: dict[str, Any] = ( + dict(_wrapper_hidden_params) if isinstance(_wrapper_hidden_params, dict) else {} + ) # Store lightweight dict snapshots for stream_chunk_builder to reduce # repeated Pydantic attribute access in end-of-stream assembly. self.collected_chat_completion_chunks: list[dict[str, Any]] = [] diff --git a/tests/router_unit_tests/test_router_aresponses_streaming_fallback.py b/tests/router_unit_tests/test_router_aresponses_streaming_fallback.py index 2fb7bdfceb5..a1ac020e209 100644 --- a/tests/router_unit_tests/test_router_aresponses_streaming_fallback.py +++ b/tests/router_unit_tests/test_router_aresponses_streaming_fallback.py @@ -483,3 +483,43 @@ async def test_aresponses_client_error_event_skips_fallback(): assert exc_info.value.status_code == 400 mock_fallback.assert_not_awaited() + + +# -------- regression: real bridge iterator honors the base contract (LIT-4912) -------- + + +def test_extract_partial_responses_usage_real_bridge_iterator_pre_first_chunk(): + """ + Regression for LIT-4912. + + When a provider errors before the first content chunk, the completion-bridge + iterator reaches _extract_partial_responses_usage with no collected chunks. + A real LiteLLMCompletionStreamingIterator (not a MagicMock standing in for + it) must expose the base-class contract so usage extraction returns None + instead of raising AttributeError on completed_response, and must carry + _hidden_params so the router does not pre-wrap it for header attachment and + thereby skip the mid-stream fallback path entirely. + """ + from litellm.responses.litellm_completion_transformation.streaming_iterator import ( + LiteLLMCompletionStreamingIterator, + ) + from litellm.responses.streaming_iterator import BaseResponsesAPIStreamingIterator + + class _FakeStreamWrapper: + def __init__(self) -> None: + self.logging_obj = MagicMock() + self._hidden_params = {"model_id": "deployment-123"} + + iterator = LiteLLMCompletionStreamingIterator( + model="anthropic/claude-3-5-sonnet-latest", + litellm_custom_stream_wrapper=_FakeStreamWrapper(), + request_input="hi", + responses_api_request={}, + ) + + assert isinstance(iterator, BaseResponsesAPIStreamingIterator) + assert iterator.completed_response is None + assert iterator._hidden_params == {"model_id": "deployment-123"} + assert not iterator.collected_chat_completion_chunks + + assert Router._extract_partial_responses_usage(iterator) is None