diff --git a/litellm/llms/openai/openai.py b/litellm/llms/openai/openai.py index 4f7efaacbff..46ac9d4eef0 100644 --- a/litellm/llms/openai/openai.py +++ b/litellm/llms/openai/openai.py @@ -336,9 +336,16 @@ def _get_openai_exception_status_code(e: Exception) -> int: 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. + # 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 diff --git a/tests/test_litellm/llms/openai/test_openai_exception_status_code.py b/tests/test_litellm/llms/openai/test_openai_exception_status_code.py index 70d10024992..58564e1a23e 100644 --- a/tests/test_litellm/llms/openai/test_openai_exception_status_code.py +++ b/tests/test_litellm/llms/openai/test_openai_exception_status_code.py @@ -8,6 +8,7 @@ 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 @@ -20,6 +21,17 @@ class TestGetOpenAIExceptionStatusCode: 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)."""