From 2e99d5d5fbc6582a3af03be7500efa5b395ff62c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 02:10:02 +0000 Subject: [PATCH] fix(openai): preserve embedded provider error status --- litellm/exceptions.py | 3 ++- .../exception_mapping_utils.py | 1 + .../convert_dict_to_response.py | 23 ++++++++++++++---- .../llms/openai/test_openai_empty_response.py | 24 +++++++++++++++++++ 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/litellm/exceptions.py b/litellm/exceptions.py index aca3fb551cc..55ce18beea1 100644 --- a/litellm/exceptions.py +++ b/litellm/exceptions.py @@ -786,6 +786,7 @@ class APIError(openai.APIError): # type: ignore litellm_debug_info: Optional[str] = None, max_retries: Optional[int] = None, num_retries: Optional[int] = None, + body: dict | None = None, ): self.status_code = status_code self.message = "litellm.APIError: {}".format(message) @@ -796,7 +797,7 @@ class APIError(openai.APIError): # type: ignore self.num_retries = num_retries if request is None: request = httpx.Request(method="POST", url="https://api.openai.com/v1") - super().__init__(self.message, request=request, body=None) # type: ignore + super().__init__(self.message, request=request, body=body) # type: ignore def __str__(self): _message = self.message diff --git a/litellm/litellm_core_utils/exception_mapping_utils.py b/litellm/litellm_core_utils/exception_mapping_utils.py index fdab3d5b9d4..ec48319c92e 100644 --- a/litellm/litellm_core_utils/exception_mapping_utils.py +++ b/litellm/litellm_core_utils/exception_mapping_utils.py @@ -397,6 +397,7 @@ def _map_openai_exception( model=model, response=getattr(original_exception, "response", None), litellm_debug_info=extra_information, + body=getattr(original_exception, "body", None), ) elif original_exception.status_code == 401: raise AuthenticationError( diff --git a/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py b/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py index 47daf33824e..13fa85e4732 100644 --- a/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py +++ b/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py @@ -631,14 +631,29 @@ def convert_to_model_response_object( if not response_object.get("choices") or not isinstance(response_object["choices"], Iterable): from litellm.exceptions import APIError - raise APIError( - status_code=500, - message=( + response_status = response_object.get("status") + error_status = ( + response_status + if isinstance(response_status, int) + and not isinstance(response_status, bool) + and 400 <= response_status <= 599 + else 500 + ) + response_message = response_object.get("response") + error_message = ( + response_message + if isinstance(response_message, str) and response_message + else ( "LiteLLM: provider returned a response with no 'choices'. " f"Raw keys: {list(response_object.keys())}" - ), + ) + ) + raise APIError( + status_code=error_status, + message=error_message, llm_provider="", model="", + body=response_object, ) for idx, choice in enumerate(response_object["choices"]): diff --git a/tests/test_litellm/llms/openai/test_openai_empty_response.py b/tests/test_litellm/llms/openai/test_openai_empty_response.py index 8a0ff237869..eb01dcf08b1 100644 --- a/tests/test_litellm/llms/openai/test_openai_empty_response.py +++ b/tests/test_litellm/llms/openai/test_openai_empty_response.py @@ -7,9 +7,12 @@ import sys from unittest.mock import MagicMock, patch import pytest +import respx +from httpx import Response sys.path.insert(0, os.path.abspath("../../../..")) +import litellm from litellm.llms.openai.openai import OpenAIChatCompletion from litellm.llms.openai.common_utils import OpenAIError @@ -126,3 +129,24 @@ class TestEmptyResponseHandling: assert response == mock_stream assert headers == {"x-request-id": "123"} + + @respx.mock + def test_nonstandard_error_response_preserves_embedded_status_and_body(self): + response_body = {"response": "Token is invalid [2]", "status": 400} + respx.post("https://gateway.example.com/v1/chat/completions").mock( + return_value=Response(200, json=response_body) + ) + + with pytest.raises(litellm.BadRequestError) as exc_info: + litellm.completion( + model="openai/test-model", + messages=[{"role": "user", "content": "Hello"}], + api_base="https://gateway.example.com/v1", + api_key="test-key", + max_retries=0, + ) + + assert exc_info.value.status_code == 400 + assert exc_info.value.body["response"] == response_body["response"] + assert exc_info.value.body["status"] == response_body["status"] + assert "Token is invalid [2]" in exc_info.value.message