diff --git a/litellm/router.py b/litellm/router.py index 7267f6eb3ba..1c658f9ef3a 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -3072,7 +3072,9 @@ class Router: # Native path: completed_response is set only if RESPONSE_COMPLETED # arrived before the error (uncommon mid-stream but worth checking). # Already ResponseAPIUsage-shaped — return as-is. - completed: Final = source_iterator.completed_response + # Use getattr because a bridge iterator whose __init__ never ran + # may not have the attribute at all (#38511). + completed: Final = getattr(source_iterator, "completed_response", None) if isinstance( completed, (ResponseCompletedEvent, ResponseFailedEvent, ResponseIncompleteEvent), 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 5370089eef5..1587c416c70 100644 --- a/tests/router_unit_tests/test_router_aresponses_streaming_fallback.py +++ b/tests/router_unit_tests/test_router_aresponses_streaming_fallback.py @@ -116,6 +116,33 @@ def test_extract_partial_responses_usage_bridge_iterator_no_completed_response() assert Router._extract_partial_responses_usage(iterator) is None +def test_extract_partial_responses_usage_no_completed_response_attr(): + """ + Regression for #38511: the native fallback path must not crash with + AttributeError when the source iterator lacks a ``completed_response`` + attribute entirely. The original error handler read + ``source_iterator.completed_response`` as a bare attribute access, which + masks the real streaming error with an AttributeError. Using getattr + lets the original exception propagate intact. + """ + from litellm.responses.litellm_completion_transformation.streaming_iterator import ( + LiteLLMCompletionStreamingIterator, + ) + + class _NoCompletedResponseBridge(LiteLLMCompletionStreamingIterator): + """A bridge iterator whose __init__ never ran, leaving no + ``completed_response`` attribute — the scenario that triggers #38511. + """ + + def __init__(self) -> None: + self.collected_chat_completion_chunks: list = [] + + iterator = _NoCompletedResponseBridge() + assert not hasattr(iterator, "completed_response") + usage = Router._extract_partial_responses_usage(iterator) + assert usage is None + + # -------- _combine_responses_fallback_usage --------