This commit is contained in:
richboyneedcash 2026-08-27 19:26:53 -05:00 • committed by GitHub
commit 450b73a9fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 96 additions and 3 deletions

View file

@ -321,6 +321,38 @@ class OpenAIChatCompletionResponseIterator(BaseModelResponseIterator):
raise e
def _get_openai_exception_status_code(e: Exception) -> int:
"""
Determine the status code to attach to an ``OpenAIError`` raised from a
caught exception.
The OpenAI SDK raises ``openai.OpenAIError`` (e.g. "Missing credentials")
at *client construction*, before any HTTP request is made. Such exceptions
carry no ``status_code``. Defaulting these to 500 asserts that a server
responded with a server error, so the failure is treated as transient and
retried even though it is a permanent, client-side configuration problem.
For these pre-request client errors we return 401 so they surface as a
non-retryable ``AuthenticationError``. Any exception that already carries a
``status_code`` (i.e. a real HTTP response happened) keeps that code.
"""
status_code = getattr(e, "status_code", None)
if status_code is not None:
return status_code
# Match the *bare* ``openai.OpenAIError`` base class exactly. The
# missing-credentials error is raised as the base class at client
# construction (before any HTTP request), so it has no ``status_code`` and
# should surface as a non-retryable 401 ``AuthenticationError``.
#
# Do NOT use ``isinstance`` here: ``openai.APIConnectionError`` and
# ``openai.APITimeoutError`` subclass ``OpenAIError`` and also carry no
# ``status_code``, but they are genuinely transient and must stay retryable
# (keep the 500 default).
if type(e) is openai.OpenAIError:
return 401
return 500
class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
def __init__(self) -> None:
super().__init__()
@ -814,7 +846,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
except OpenAIError as e:
raise e
except Exception as e:
status_code: Final = getattr(e, "status_code", 500)
status_code: Final = _get_openai_exception_status_code(e)
error_headers = getattr(e, "headers", None)
error_text: Final = getattr(e, "text", str(e))
error_response: Final = getattr(e, "response", None)
@ -937,7 +969,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
# e.message
except Exception as e:
exception_response = getattr(e, "response", None)
status_code = getattr(e, "status_code", 500)
status_code = _get_openai_exception_status_code(e)
exception_body = getattr(e, "body", None)
error_headers = getattr(e, "headers", None)
if error_headers is None and exception_response:
@ -1092,7 +1124,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
raise e
error_headers = getattr(e, "headers", None)
status_code = getattr(e, "status_code", 500)
status_code = _get_openai_exception_status_code(e)
error_response = getattr(e, "response", None)
exception_body = getattr(e, "body", None)
if error_headers is None and error_response:

View file

@ -0,0 +1,61 @@
"""
Tests for _get_openai_exception_status_code.
Regression test for https://github.com/BerriAI/litellm/issues/35860:
a missing API key raises openai.OpenAIError at client construction (with no
status_code). Defaulting that to 500 makes it a retryable InternalServerError,
so a permanent credential/config error is retried instead of failing fast.
It should map to 401 (AuthenticationError) instead.
"""
import httpx
import openai
from litellm.llms.openai.openai import _get_openai_exception_status_code
class TestGetOpenAIExceptionStatusCode:
def test_openai_error_without_status_code_maps_to_401(self):
"""openai.OpenAIError with no status_code (pre-request, e.g. missing key) -> 401."""
err = openai.OpenAIError("Missing credentials. Please pass an `api_key`.")
assert not hasattr(err, "status_code") or getattr(err, "status_code") is None
assert _get_openai_exception_status_code(err) == 401
def test_transient_openai_subclasses_stay_retryable(self):
"""APIConnectionError / APITimeoutError subclass OpenAIError and carry no
status_code, but they are genuinely transient and must stay retryable
(500), not be turned into a non-retryable 401. Only the bare
OpenAIError base class (missing credentials) maps to 401."""
conn_err = openai.APIConnectionError(request=httpx.Request("POST", "https://api.openai.com/v1"))
timeout_err = openai.APITimeoutError(request=httpx.Request("POST", "https://api.openai.com/v1"))
assert _get_openai_exception_status_code(conn_err) == 500
assert _get_openai_exception_status_code(timeout_err) == 500
def test_existing_status_code_is_preserved(self):
"""An exception that already carries a status_code keeps it (real HTTP response)."""
class FakeHTTPError(Exception):
status_code = 429
assert _get_openai_exception_status_code(FakeHTTPError("rate limited")) == 429
def test_status_code_zero_is_preserved(self):
"""A falsy-but-present status_code (0) is still honored, not defaulted."""
class ZeroStatus(Exception):
status_code = 0
assert _get_openai_exception_status_code(ZeroStatus("weird")) == 0
def test_generic_exception_without_status_code_defaults_to_500(self):
"""A non-OpenAI exception with no status_code keeps the 500 default."""
assert _get_openai_exception_status_code(ValueError("boom")) == 500
def test_openai_error_subclass_with_status_code_kept(self):
"""A real openai HTTP error (has status_code) is unchanged."""
class FakeOpenAIHTTPError(openai.OpenAIError):
status_code = 400
assert _get_openai_exception_status_code(FakeOpenAIHTTPError("bad request")) == 400