fix(proxy): stop answering with the literal string "None" on the proxy-server, auth, and health routes

The error objects these routes return defaulted `type` and `param` to the
four-character string "None", so a client's error handler matched no known
OpenAI type and fell into its generic branch.

Route them through the shared openai_error_payload helpers instead, so `type`
comes from the carried type or the HTTP status and `param` serializes as JSON
null.
This commit is contained in:
mateo-berri 2026-09-03 03:04:03 -07:00
parent bede8b5ea4
commit 32074cf4de
8 changed files with 161 additions and 87 deletions

View file

@ -24,6 +24,7 @@ from litellm.proxy.auth.auth_utils import (
is_invalid_virtual_key_error,
mark_invalid_virtual_key_error,
)
from litellm.proxy.common_utils.openai_error_payload import openai_error_param
from litellm.proxy.db.exception_handler import PrismaDBExceptionHandler
from litellm.types.services import ServiceTypes
@ -54,7 +55,7 @@ def _as_proxy_exception(e: Exception) -> ProxyException:
return ProxyException(
message=getattr(e, "detail", f"Authentication Error({e})"),
type=ProxyErrorTypes.auth_error,
param=getattr(e, "param", "None"),
param=openai_error_param(e),
code=getattr(e, "status_code", status.HTTP_401_UNAUTHORIZED),
)
if isinstance(e, ProxyException):
@ -63,13 +64,13 @@ def _as_proxy_exception(e: Exception) -> ProxyException:
return ProxyException(
message=PrismaDBExceptionHandler.database_unavailable_message(e),
type=ProxyErrorTypes.no_db_connection,
param="None",
param=None,
code=status.HTTP_503_SERVICE_UNAVAILABLE,
)
return ProxyException(
message="Authentication Error, " + str(e),
type=ProxyErrorTypes.auth_error,
param=getattr(e, "param", "None"),
param=openai_error_param(e),
code=status.HTTP_401_UNAUTHORIZED,
)

View file

@ -142,7 +142,7 @@ def jwks_unavailable_exception(error: JWKSUnreachableError) -> ProxyException:
f"unreachable, so the JWT signature could not be verified. Please retry shortly. Error: {error}"
),
type=ProxyErrorTypes.auth_provider_unavailable,
param="None",
param=None,
code=status.HTTP_503_SERVICE_UNAVAILABLE,
)

View file

@ -40,6 +40,7 @@ from litellm.proxy.auth.auth_utils import (
_BANNED_REQUEST_BODY_PARAMS, # pyright: ignore[reportPrivateUsage] # one canonical list, shared with the request-body check
)
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.common_utils.openai_error_payload import openai_error_param
from litellm.proxy.db.exception_handler import PrismaDBExceptionHandler
from litellm.proxy.db.proxy_worker_heartbeat import count_live_proxy_workers
from litellm.proxy.health_check import (
@ -529,7 +530,7 @@ async def health_services_endpoint(
raise ProxyException(
message=getattr(e, "detail", f"Authentication Error({e})"),
type=ProxyErrorTypes.auth_error,
param=getattr(e, "param", "None"),
param=openai_error_param(e),
code=getattr(e, "status_code", status.HTTP_500_INTERNAL_SERVER_ERROR),
)
elif isinstance(e, ProxyException):
@ -537,7 +538,7 @@ async def health_services_endpoint(
raise ProxyException(
message="Authentication Error, " + str(e),
type=ProxyErrorTypes.auth_error,
param=getattr(e, "param", "None"),
param=openai_error_param(e),
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)

View file

@ -112,6 +112,11 @@ from litellm.proxy.common_utils.callback_utils import (
process_callback,
strip_callback_config,
)
from litellm.proxy.common_utils.openai_error_payload import (
error_status_code,
openai_error_param,
openai_error_type,
)
from litellm.proxy.common_utils.realtime_utils import _realtime_request_body
from litellm.router_utils.add_retry_fallback_headers import (
get_fallback_errors_from_headers,
@ -8160,9 +8165,9 @@ async def async_assistants_data_generator(response, user_api_key_dict: UserAPIKe
proxy_exception: Final = ProxyException(
message=getattr(e, "message", error_msg),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", 500),
type=openai_error_type(e, error_status_code(e, 500)),
param=openai_error_param(e),
code=error_status_code(e, 500),
)
error_returned: Final = json.dumps({"error": proxy_exception.to_dict()})
yield f"data: {error_returned}\n\n"
@ -8867,9 +8872,9 @@ async def async_data_generator(
proxy_exception: Final = ProxyException(
message=getattr(e, "message", error_msg),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", 500),
type=openai_error_type(e, error_status_code(e, 500)),
param=openai_error_param(e),
code=error_status_code(e, 500),
)
error_returned: Final = json.dumps({"error": proxy_exception.to_dict()})
stream_completed = True
@ -10819,10 +10824,10 @@ async def completion(
error_msg: Final = f"{e}"
raise ProxyException(
message=getattr(e, "message", error_msg),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
type=openai_error_type(e, error_status_code(e, 500)),
param=openai_error_param(e),
openai_code=getattr(e, "code", None),
code=getattr(e, "status_code", 500),
code=error_status_code(e, 500),
)
@ -11052,17 +11057,17 @@ async def moderations(
if isinstance(e, HTTPException):
raise ProxyException(
message=getattr(e, "message", str(e)),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", status.HTTP_400_BAD_REQUEST),
type=openai_error_type(e, error_status_code(e, status.HTTP_400_BAD_REQUEST)),
param=openai_error_param(e),
code=error_status_code(e, status.HTTP_400_BAD_REQUEST),
)
else:
error_msg: Final = f"{e}"
raise ProxyException(
message=getattr(e, "message", error_msg),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", 500),
type=openai_error_type(e, error_status_code(e, 500)),
param=openai_error_param(e),
code=error_status_code(e, 500),
)
@ -11199,10 +11204,10 @@ async def audio_speech(
raise e
raise ProxyException(
message=getattr(e, "message", f"{e}"),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
type=openai_error_type(e, error_status_code(e, 500)),
param=openai_error_param(e),
openai_code=getattr(e, "code", None),
code=getattr(e, "status_code", 500),
code=error_status_code(e, 500),
)
@ -11347,18 +11352,18 @@ async def audio_transcriptions(
if isinstance(e, HTTPException):
raise ProxyException(
message=getattr(e, "message", str(e.detail)),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", status.HTTP_400_BAD_REQUEST),
type=openai_error_type(e, error_status_code(e, status.HTTP_400_BAD_REQUEST)),
param=openai_error_param(e),
code=error_status_code(e, status.HTTP_400_BAD_REQUEST),
)
else:
error_msg: Final = f"{e}"
raise ProxyException(
message=getattr(e, "message", error_msg),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
type=openai_error_type(e, error_status_code(e, 500)),
param=openai_error_param(e),
openai_code=getattr(e, "code", None),
code=getattr(e, "status_code", 500),
code=error_status_code(e, 500),
)
@ -11645,18 +11650,18 @@ async def get_assistants(
if isinstance(e, HTTPException):
raise ProxyException(
message=getattr(e, "message", str(e.detail)),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", status.HTTP_400_BAD_REQUEST),
type=openai_error_type(e, error_status_code(e, status.HTTP_400_BAD_REQUEST)),
param=openai_error_param(e),
code=error_status_code(e, status.HTTP_400_BAD_REQUEST),
)
else:
error_msg: Final = f"{e}"
raise ProxyException(
message=getattr(e, "message", error_msg),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
type=openai_error_type(e, error_status_code(e, 500)),
param=openai_error_param(e),
openai_code=getattr(e, "code", None),
code=getattr(e, "status_code", 500),
code=error_status_code(e, 500),
)
@ -11736,16 +11741,16 @@ async def create_assistant(
if isinstance(e, HTTPException):
raise ProxyException(
message=getattr(e, "message", str(e.detail)),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", status.HTTP_400_BAD_REQUEST),
type=openai_error_type(e, error_status_code(e, status.HTTP_400_BAD_REQUEST)),
param=openai_error_param(e),
code=error_status_code(e, status.HTTP_400_BAD_REQUEST),
)
else:
error_msg: Final = f"{e}"
raise ProxyException(
message=getattr(e, "message", error_msg),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
type=openai_error_type(e, error_status_code(e, 500)),
param=openai_error_param(e),
code=getattr(e, "code", getattr(e, "status_code", 500)),
)
@ -11825,16 +11830,16 @@ async def delete_assistant(
if isinstance(e, HTTPException):
raise ProxyException(
message=getattr(e, "message", str(e.detail)),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", status.HTTP_400_BAD_REQUEST),
type=openai_error_type(e, error_status_code(e, status.HTTP_400_BAD_REQUEST)),
param=openai_error_param(e),
code=error_status_code(e, status.HTTP_400_BAD_REQUEST),
)
else:
error_msg: Final = f"{e}"
raise ProxyException(
message=getattr(e, "message", error_msg),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
type=openai_error_type(e, error_status_code(e, 500)),
param=openai_error_param(e),
code=getattr(e, "code", getattr(e, "status_code", 500)),
)
@ -11914,16 +11919,16 @@ async def create_threads(
if isinstance(e, HTTPException):
raise ProxyException(
message=getattr(e, "message", str(e.detail)),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", status.HTTP_400_BAD_REQUEST),
type=openai_error_type(e, error_status_code(e, status.HTTP_400_BAD_REQUEST)),
param=openai_error_param(e),
code=error_status_code(e, status.HTTP_400_BAD_REQUEST),
)
else:
error_msg: Final = f"{e}"
raise ProxyException(
message=getattr(e, "message", error_msg),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
type=openai_error_type(e, error_status_code(e, 500)),
param=openai_error_param(e),
code=getattr(e, "code", getattr(e, "status_code", 500)),
)
@ -12001,16 +12006,16 @@ async def get_thread(
if isinstance(e, HTTPException):
raise ProxyException(
message=getattr(e, "message", str(e.detail)),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", status.HTTP_400_BAD_REQUEST),
type=openai_error_type(e, error_status_code(e, status.HTTP_400_BAD_REQUEST)),
param=openai_error_param(e),
code=error_status_code(e, status.HTTP_400_BAD_REQUEST),
)
else:
error_msg: Final = f"{e}"
raise ProxyException(
message=getattr(e, "message", error_msg),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
type=openai_error_type(e, error_status_code(e, 500)),
param=openai_error_param(e),
code=getattr(e, "code", getattr(e, "status_code", 500)),
)
@ -12092,16 +12097,16 @@ async def add_messages(
if isinstance(e, HTTPException):
raise ProxyException(
message=getattr(e, "message", str(e.detail)),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", status.HTTP_400_BAD_REQUEST),
type=openai_error_type(e, error_status_code(e, status.HTTP_400_BAD_REQUEST)),
param=openai_error_param(e),
code=error_status_code(e, status.HTTP_400_BAD_REQUEST),
)
else:
error_msg: Final = f"{e}"
raise ProxyException(
message=getattr(e, "message", error_msg),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
type=openai_error_type(e, error_status_code(e, 500)),
param=openai_error_param(e),
code=getattr(e, "code", getattr(e, "status_code", 500)),
)
@ -12179,16 +12184,16 @@ async def get_messages(
if isinstance(e, HTTPException):
raise ProxyException(
message=getattr(e, "message", str(e.detail)),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", status.HTTP_400_BAD_REQUEST),
type=openai_error_type(e, error_status_code(e, status.HTTP_400_BAD_REQUEST)),
param=openai_error_param(e),
code=error_status_code(e, status.HTTP_400_BAD_REQUEST),
)
else:
error_msg: Final = f"{e}"
raise ProxyException(
message=getattr(e, "message", error_msg),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
type=openai_error_type(e, error_status_code(e, 500)),
param=openai_error_param(e),
code=getattr(e, "code", getattr(e, "status_code", 500)),
)
@ -12301,16 +12306,16 @@ async def run_thread(
if isinstance(e, HTTPException):
raise ProxyException(
message=getattr(e, "message", str(e.detail)),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", status.HTTP_400_BAD_REQUEST),
type=openai_error_type(e, error_status_code(e, status.HTTP_400_BAD_REQUEST)),
param=openai_error_param(e),
code=error_status_code(e, status.HTTP_400_BAD_REQUEST),
)
else:
error_msg: Final = f"{e}"
raise ProxyException(
message=getattr(e, "message", error_msg),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
type=openai_error_type(e, error_status_code(e, 500)),
param=openai_error_param(e),
code=getattr(e, "code", getattr(e, "status_code", 500)),
)
@ -13980,7 +13985,7 @@ async def model_streaming_metrics(
raise ProxyException(
message=CommonProxyErrors.db_not_connected_error.value,
type="internal_error",
param="None",
param=None,
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@ -14114,7 +14119,7 @@ async def model_metrics(
raise ProxyException(
message="Prisma Client is not initialized",
type="internal_error",
param="None",
param=None,
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
startTime = startTime or datetime.now() - timedelta(days=DAYS_IN_A_MONTH)
@ -14229,7 +14234,7 @@ async def model_metrics_slow_responses(
raise ProxyException(
message="Prisma Client is not initialized",
type="internal_error",
param="None",
param=None,
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
if api_key is None or api_key == "undefined":
@ -14318,7 +14323,7 @@ async def model_metrics_exceptions(
raise ProxyException(
message="Prisma Client is not initialized",
type="internal_error",
param="None",
param=None,
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@ -15247,7 +15252,7 @@ async def async_queue_request(
raise ProxyException(
message=getattr(e, "detail", f"Authentication Error({e})"),
type=ProxyErrorTypes.auth_error,
param=getattr(e, "param", "None"),
param=openai_error_param(e),
code=getattr(e, "status_code", status.HTTP_400_BAD_REQUEST),
)
elif isinstance(e, ProxyException):
@ -15255,7 +15260,7 @@ async def async_queue_request(
raise ProxyException(
message="Authentication Error, " + str(e),
type=ProxyErrorTypes.auth_error,
param=getattr(e, "param", "None"),
param=openai_error_param(e),
code=status.HTTP_400_BAD_REQUEST,
)
@ -15419,7 +15424,7 @@ async def login_v2(request: Request):
raise ProxyException(
message=getattr(e, "detail", str(e)),
type=ProxyErrorTypes.auth_error,
param=getattr(e, "param", "None"),
param=openai_error_param(e),
code=getattr(e, "status_code", status.HTTP_500_INTERNAL_SERVER_ERROR),
)
else:
@ -15427,7 +15432,7 @@ async def login_v2(request: Request):
raise ProxyException(
message=error_msg,
type=ProxyErrorTypes.auth_error,
param="None",
param=None,
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@ -15494,7 +15499,7 @@ async def login_v3(request: Request):
raise ProxyException(
message=getattr(e, "detail", str(e)),
type=ProxyErrorTypes.auth_error,
param=getattr(e, "param", "None"),
param=openai_error_param(e),
code=getattr(e, "status_code", status.HTTP_500_INTERNAL_SERVER_ERROR),
)
else:
@ -15502,7 +15507,7 @@ async def login_v3(request: Request):
raise ProxyException(
message=error_msg,
type=ProxyErrorTypes.auth_error,
param="None",
param=None,
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@ -15566,7 +15571,7 @@ async def login_v3_exchange(request: Request):
raise ProxyException(
message=str(e),
type=ProxyErrorTypes.auth_error,
param="None",
param=None,
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@ -16465,7 +16470,7 @@ async def update_config(
raise ProxyException(
message=getattr(e, "detail", f"Authentication Error({e})"),
type=ProxyErrorTypes.auth_error,
param=getattr(e, "param", "None"),
param=openai_error_param(e),
code=getattr(e, "status_code", status.HTTP_400_BAD_REQUEST),
)
elif isinstance(e, ProxyException):
@ -16473,7 +16478,7 @@ async def update_config(
raise ProxyException(
message="Authentication Error, " + str(e),
type=ProxyErrorTypes.auth_error,
param=getattr(e, "param", "None"),
param=openai_error_param(e),
code=status.HTTP_400_BAD_REQUEST,
)
@ -17464,7 +17469,7 @@ async def get_config(
raise ProxyException(
message=getattr(e, "detail", f"Authentication Error({e})"),
type=ProxyErrorTypes.auth_error,
param=getattr(e, "param", "None"),
param=openai_error_param(e),
code=getattr(e, "status_code", status.HTTP_400_BAD_REQUEST),
)
elif isinstance(e, ProxyException):
@ -17472,7 +17477,7 @@ async def get_config(
raise ProxyException(
message="Authentication Error, " + str(e),
type=ProxyErrorTypes.auth_error,
param=getattr(e, "param", "None"),
param=openai_error_param(e),
code=status.HTTP_400_BAD_REQUEST,
)

View file

@ -30,7 +30,10 @@ from litellm._logging import verbose_proxy_logger
from litellm.constants import INVALID_VIRTUAL_KEY_ERROR_MARKER
from litellm.exceptions import BudgetExceededError
from litellm.proxy._types import ProxyErrorTypes, ProxyException, UserAPIKeyAuth
from litellm.proxy.auth.auth_exception_handler import UserAPIKeyAuthExceptionHandler
from litellm.proxy.auth.auth_exception_handler import (
UserAPIKeyAuthExceptionHandler,
_as_proxy_exception,
)
class _EngineHttp500:
@ -871,3 +874,32 @@ async def test_handle_authentication_error_traceback_only_for_unexpected_errors(
assert records[0].levelname == expect_level
expected_logger_name = "LiteLLM Proxy.stdout" if expect_level == "WARNING" else "LiteLLM Proxy"
assert records[0].name == expected_logger_name
@pytest.mark.parametrize(
"auth_failure,expected_type",
[
pytest.param(
HTTPException(status_code=401, detail="Authentication Error, invalid key"),
ProxyErrorTypes.auth_error.value,
id="http_exception",
),
pytest.param(
httpx.ConnectError("All connection attempts failed"),
ProxyErrorTypes.no_db_connection.value,
id="database_unavailable",
),
pytest.param(
Exception("Invalid proxy server token passed"),
ProxyErrorTypes.auth_error.value,
id="bare_exception",
),
],
)
def test_auth_failure_error_body_is_openai_shaped(auth_failure, expected_type):
"""Every auth rejection must serialize as an OpenAI error object: a real
`type` string and a JSON null `param`, never the literal string "None"."""
body = json.loads(json.dumps({"error": _as_proxy_exception(auth_failure).to_dict()}))
assert body["error"]["type"] == expected_type
assert body["error"]["param"] is None

View file

@ -1,4 +1,5 @@
import asyncio
import json
import re
import time
from collections.abc import Mapping, Sequence
@ -30,6 +31,7 @@ from litellm.proxy.auth.handle_jwt import (
JWTAuthManager,
JWTHandler,
NoMatchingJWTPublicKeyError,
jwks_unavailable_exception,
)
@ -6734,3 +6736,13 @@ async def test_sync_user_role_and_teams_singular_claim_only_recognized_under_fla
}
assert mock_patch.call_args.kwargs["teams_ids_to_add_user_to"] == []
assert user.teams == []
def test_jwks_unavailable_exception_is_openai_shaped():
"""An unreachable JWKS endpoint must answer with a real `type` string and a
JSON null `param`, never the literal string "None"."""
body = json.loads(json.dumps({"error": jwks_unavailable_exception(JWKSUnreachableError("jwks down")).to_dict()}))
assert body["error"]["type"] == ProxyErrorTypes.auth_provider_unavailable.value
assert body["error"]["param"] is None
assert body["error"]["code"] == "503"

View file

@ -1089,6 +1089,18 @@ async def test_health_services_endpoint_rejects_unknown_service():
await health_services_endpoint(service="totally_unknown_service_xyz")
@pytest.mark.asyncio
async def test_health_services_endpoint_rejection_is_openai_shaped():
"""A caller mistake here must serialize as an OpenAI error object: a real
`type` string and a JSON null `param`, never the literal string "None"."""
with pytest.raises(ProxyException) as exc_info:
await health_services_endpoint(service="totally_unknown_service_xyz")
body = json.loads(json.dumps({"error": exc_info.value.to_dict()}))
assert body["error"]["type"] == "auth_error"
assert body["error"]["param"] is None
@pytest.mark.asyncio
@pytest.mark.parametrize(
"role",

View file

@ -12507,3 +12507,14 @@ def test_disabling_docs_does_not_disable_other_routes(monkeypatch):
assert client.get("/redoc").status_code == 404
assert client.get("/health/liveliness").status_code == 200
def test_assistants_error_body_is_openai_shaped(client_no_auth):
"""A failed /v1/assistants call must answer with an OpenAI error object: a
real `type` string and a JSON null `param`, never the literal string "None"."""
response = client_no_auth.get("/v1/assistants")
assert response.status_code >= 400
error = response.json()["error"]
assert error["type"] == "internal_server_error"
assert error["param"] is None