From b627f12b38cb8b4d64f60eaa1017b3e647367647 Mon Sep 17 00:00:00 2001 From: onatozmenn Date: Sat, 1 Aug 2026 14:24:32 +0300 Subject: [PATCH] fix(responses): initialize completed_response on the MCP gateway streaming iterator MCPEnhancedStreamingIterator has the same shape as the completion bridge that #35413 fixed: it subclasses BaseResponsesAPIStreamingIterator, never calls super().__init__, and never sets completed_response. It only catches StopAsyncIteration around the base iterator, so a MidStreamFallbackError propagates out of __anext__ and the Router reads completed_response off it in _extract_partial_responses_usage, raising AttributeError and masking the provider error. The iterator is reachable there: aresponses dispatches to aresponses_api_with_mcp when the request carries litellm_proxy MCP tools, which returns this iterator for stream=True, and the Router wraps any BaseResponsesAPIStreamingIterator. Initialize it to None alongside the other iterator state. With #35413 merged this was the last BaseResponsesAPIStreamingIterator subclass missing it. Signed-off-by: onatozmenn --- .../responses/mcp/mcp_streaming_iterator.py | 1 + .../mcp/test_mcp_streaming_iterator.py | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/litellm/responses/mcp/mcp_streaming_iterator.py b/litellm/responses/mcp/mcp_streaming_iterator.py index 186852f91c2..f9ff335d332 100644 --- a/litellm/responses/mcp/mcp_streaming_iterator.py +++ b/litellm/responses/mcp/mcp_streaming_iterator.py @@ -274,6 +274,7 @@ class MCPEnhancedStreamingIterator(BaseResponsesAPIStreamingIterator): # Streaming state management self.phase = "initial_response" # initial_response -> mcp_discovery -> (continue_initial_response <-> tool_execution) -> finished self.finished = False + self.completed_response: Any | None = None # Event queues and generation flags self.mcp_discovery_events: list[ResponsesAPIStreamingResponse] = ( diff --git a/tests/test_litellm/responses/mcp/test_mcp_streaming_iterator.py b/tests/test_litellm/responses/mcp/test_mcp_streaming_iterator.py index 24edf12fffe..3afaf2d52be 100644 --- a/tests/test_litellm/responses/mcp/test_mcp_streaming_iterator.py +++ b/tests/test_litellm/responses/mcp/test_mcp_streaming_iterator.py @@ -257,3 +257,42 @@ async def test_initial_call_failure_is_stashed_for_eager_reraise(monkeypatch): assert iterator._initial_creation_error is not None assert "initial boom" in str(iterator._initial_creation_error) + + +@pytest.mark.asyncio +async def test_mid_stream_error_leaves_completed_response_readable(monkeypatch): + """ + Regression test: on a mid-stream provider error the Router's fallback path + reads `completed_response` off this iterator directly (see + Router._extract_partial_responses_usage). This iterator skips + super().__init__(), so that read used to raise AttributeError and mask the + provider error, stopping the fallback from running. + """ + from litellm import Router + from litellm.exceptions import MidStreamFallbackError + + _mock_mcp_environment(monkeypatch) + + class _ErroringStream: + def __aiter__(self): + return self + + async def __anext__(self): + raise MidStreamFallbackError( + message="provider overloaded", + model="gpt-4", + llm_provider="openai", + ) + + iterator = MCPEnhancedStreamingIterator( + base_iterator=_ErroringStream(), + mcp_events=[], + tool_server_map={}, + original_request_params={"model": "gpt-4"}, + ) + + with pytest.raises(MidStreamFallbackError): + await iterator.__anext__() + + assert iterator.completed_response is None + assert Router._extract_partial_responses_usage(iterator) is None