From fbb21ae0c95ee73c3eda30fa92b7e37259b2d203 Mon Sep 17 00:00:00 2001 From: Genmin Date: Thu, 30 Apr 2026 14:47:50 -0700 Subject: [PATCH] fix: tolerate invalid midstream fallback status --- litellm/exceptions.py | 24 ++++++++-- .../test_exception_header_preservation.py | 45 +++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/litellm/exceptions.py b/litellm/exceptions.py index 8b005291556..c04e23a02d0 100644 --- a/litellm/exceptions.py +++ b/litellm/exceptions.py @@ -941,6 +941,24 @@ class BlockedPiiEntityError(Exception): class MidStreamFallbackError(ServiceUnavailableError): # type: ignore + @staticmethod + def _get_status_code(original_exception: Optional[Exception]) -> int: + if original_exception is None: + return 503 + + for original_status in ( + getattr(original_exception, "status_code", None), + getattr(getattr(original_exception, "response", None), "status_code", None), + ): + if original_status is None: + continue + try: + return int(original_status) + except (TypeError, ValueError): + continue + + return 503 + def __init__( self, message: str, @@ -954,8 +972,8 @@ class MidStreamFallbackError(ServiceUnavailableError): # type: ignore generated_content: str = "", is_pre_first_chunk: bool = False, ): - original_status = getattr(original_exception, "status_code", None) - self.status_code = int(original_status) if original_status is not None else 503 + original_status = self._get_status_code(original_exception) + self.status_code = original_status self.message = f"litellm.MidStreamFallbackError: {message}" self.model = model self.llm_provider = llm_provider @@ -997,7 +1015,7 @@ class MidStreamFallbackError(ServiceUnavailableError): # type: ignore ) # Restore the propagated status and original response/request objects - self.status_code = int(original_status) if original_status is not None else 503 + self.status_code = original_status self.response = _saved_response self.request = _saved_request self.message = _saved_message diff --git a/tests/test_litellm/test_exception_header_preservation.py b/tests/test_litellm/test_exception_header_preservation.py index 6ea478c633b..6311dcac13b 100644 --- a/tests/test_litellm/test_exception_header_preservation.py +++ b/tests/test_litellm/test_exception_header_preservation.py @@ -254,6 +254,51 @@ class TestExceptionAttributes: assert midstream_fallback.response.status_code == 503 assert str(midstream_fallback.response.request.url) == "https://openai.com/v1/" + def test_midstream_fallback_error_handles_non_numeric_status_code(self): + """ + Custom/OpenAI-mapped exceptions can expose non-HTTP status codes like + "litellm_error"; MidStreamFallbackError should still be constructible. + """ + + class NonNumericStatusError(Exception): + status_code = "litellm_error" + + midstream_error = MidStreamFallbackError( + message="stream broke", + model="gpt-4o-mini", + llm_provider="openai", + original_exception=NonNumericStatusError("rate limited"), + ) + + assert midstream_error.status_code == 503 + assert midstream_error.response.status_code == 503 + + def test_midstream_fallback_error_uses_response_status_code_fallback(self): + """ + If exception.status_code is malformed but response.status_code is valid, + preserve the underlying HTTP status. + """ + + class NonNumericStatusError(Exception): + status_code = "litellm_error" + + def __init__(self, response: httpx.Response): + self.response = response + + response = httpx.Response( + status_code=429, + request=httpx.Request("POST", "https://api.openai.com/v1/chat/completions"), + ) + midstream_error = MidStreamFallbackError( + message="stream broke", + model="gpt-4o-mini", + llm_provider="openai", + original_exception=NonNumericStatusError(response=response), + ) + + assert midstream_error.status_code == 429 + assert midstream_error.response.status_code == 429 + class TestProxyHeaderExtraction: """Test that proxy correctly extracts headers from exceptions."""