fix(openai): preserve embedded provider error status

This commit is contained in:
Devin AI 2026-07-14 02:10:02 +00:00
parent 948a43cd64
commit 2e99d5d5fb
4 changed files with 46 additions and 5 deletions

View file

@ -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

View file

@ -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(

View file

@ -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"]):

View file

@ -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