mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
Merge 11ae1d4997 into eb0e3f8c18
This commit is contained in:
commit
450b73a9fd
2 changed files with 96 additions and 3 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue