fix: map missing OpenAI credentials to 401 instead of 500

The OpenAI SDK raises openai.OpenAIError (e.g. "Missing credentials") at
client construction, before any HTTP request. These exceptions carry no
status_code, and the broad exception handlers in openai.py defaulted the
status to 500. Since 500 is retryable under standard retry policies, a
permanent, client-side configuration error (missing api_key) got retried
until attempts ran out, and the useful "Missing credentials" message
arrived last, surfaced as InternalServerError.

Add a _get_openai_exception_status_code helper that returns 401 for a
pre-request openai.OpenAIError (no status_code), so it maps to a
non-retryable AuthenticationError. Exceptions that already carry a
status_code (a real HTTP response) are unchanged, and non-OpenAI
exceptions keep the 500 default.

Applied to the completion and streaming handlers.

Fixes #35860

Co-authored-by: TRAE CLI <noreply@bytedance.com>
This commit is contained in:
richboyneedcash 2026-08-06 12:13:08 +08:00
parent ead62528e6
commit a1aa1528df
2 changed files with 77 additions and 3 deletions

View file

@ -318,6 +318,31 @@ 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
if isinstance(e, openai.OpenAIError):
# openai.OpenAIError with no status_code == raised before any HTTP
# exchange (e.g. missing api_key), which is an auth/config problem.
return 401
return 500
class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
def __init__(self) -> None:
super().__init__()
@ -797,7 +822,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)
@ -920,7 +945,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:
@ -1075,7 +1100,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,49 @@
"""
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 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_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