mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
fix(proxy): name shared-handler errors by the status they answer
handle_exception_on_proxy pinned type=internal_server_error on every
exception it wrapped, so a management route rejecting a caller's own
argument answered {"type":"internal_server_error","code":"400"} and an
SDK branching on the type read its own bad request as a gateway outage.
Derive the type from the status the response actually carries, keeping
a type the exception already names.
This commit is contained in:
parent
7a5b8bce7e
commit
6a9cb48cf3
3 changed files with 100 additions and 7 deletions
|
|
@ -37,7 +37,11 @@ from litellm.proxy._types import (
|
|||
SpendLogsMetadata,
|
||||
SpendLogsPayload,
|
||||
)
|
||||
from litellm.proxy.common_utils.openai_error_payload import openai_error_param
|
||||
from litellm.proxy.common_utils.openai_error_payload import (
|
||||
error_status_code,
|
||||
openai_error_param,
|
||||
openai_error_type,
|
||||
)
|
||||
from litellm.proxy.spend_tracking.spend_log_error_logger import spend_log_error
|
||||
from litellm.types.guardrails import GuardrailEventHooks
|
||||
from litellm.types.proxy.model_listing import ModelInfoResponse
|
||||
|
|
@ -7096,18 +7100,19 @@ def handle_exception_on_proxy(e: Exception) -> ProxyException:
|
|||
verbose_proxy_logger.exception("Exception: %s", e)
|
||||
|
||||
if isinstance(e, HTTPException):
|
||||
http_status_code: Final = error_status_code(e, status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return ProxyException(
|
||||
message=getattr(e, "detail", f"error({e})"),
|
||||
type=ProxyErrorTypes.internal_server_error,
|
||||
type=openai_error_type(e, http_status_code),
|
||||
param=openai_error_param(e),
|
||||
code=getattr(e, "status_code", status.HTTP_500_INTERNAL_SERVER_ERROR),
|
||||
code=http_status_code,
|
||||
)
|
||||
elif isinstance(e, ProxyException):
|
||||
return e
|
||||
_status_code: Final = getattr(e, "status_code", status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
_status_code: Final = error_status_code(e, status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return ProxyException(
|
||||
message=str(e),
|
||||
type=ProxyErrorTypes.internal_server_error,
|
||||
type=openai_error_type(e, _status_code),
|
||||
param=openai_error_param(e),
|
||||
code=_status_code,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -86,3 +86,31 @@ def test_update_credential_still_answers_200_on_a_successful_write():
|
|||
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json()["success"] is True
|
||||
|
||||
|
||||
def _get_credential_by_name(name: str):
|
||||
missing = object()
|
||||
previous_override = app.dependency_overrides.get(user_api_key_auth, missing)
|
||||
app.dependency_overrides[user_api_key_auth] = _as_admin
|
||||
try:
|
||||
return client.get(f"/credentials/by_name/{name}", headers={"Authorization": "Bearer test-key"})
|
||||
finally:
|
||||
if previous_override is missing:
|
||||
app.dependency_overrides.pop(user_api_key_auth, None)
|
||||
else:
|
||||
app.dependency_overrides[user_api_key_auth] = previous_override
|
||||
|
||||
|
||||
def test_looking_up_a_missing_credential_is_labelled_a_client_error_not_a_gateway_failure():
|
||||
"""Naming a credential that does not exist is the caller's own mistake, and the shared
|
||||
handler used to label every one of these internal_server_error, so an SDK branching on
|
||||
the type read a 404 it can never retry past as a gateway outage and backed off forever."""
|
||||
response = _get_credential_by_name("definitely-not-there")
|
||||
|
||||
assert response.status_code == 404, f"lookup answered {response.status_code}: {response.text}"
|
||||
assert response.json()["error"] == {
|
||||
"message": "Credential not found. Got credential name: definitely-not-there",
|
||||
"type": "invalid_request_error",
|
||||
"param": None,
|
||||
"code": "404",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import json
|
|||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
import litellm
|
||||
from litellm.proxy._types import ProxyErrorTypes, ProxyException
|
||||
from litellm.proxy.utils import get_error_message_str, handle_exception_on_proxy
|
||||
|
||||
|
|
@ -93,7 +94,7 @@ def test_handle_exception_on_proxy_happy_path_http_exception():
|
|||
assert snapshot == {
|
||||
"is_proxy_exception": True,
|
||||
"message": "forbidden",
|
||||
"type": ProxyErrorTypes.internal_server_error.value,
|
||||
"type": "permission_error",
|
||||
"code": "403",
|
||||
}
|
||||
|
||||
|
|
@ -153,7 +154,7 @@ def test_handle_exception_on_proxy_uses_attached_status_code_when_present():
|
|||
assert snapshot == {
|
||||
"code": "418",
|
||||
"message": "teapot",
|
||||
"type": ProxyErrorTypes.internal_server_error.value,
|
||||
"type": "invalid_request_error",
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -171,3 +172,62 @@ def test_handle_exception_on_proxy_error_path_none_input_wraps_as_500():
|
|||
"code": "500",
|
||||
"type": ProxyErrorTypes.internal_server_error.value,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"status_code, expected_type",
|
||||
[
|
||||
(400, "invalid_request_error"),
|
||||
(401, "authentication_error"),
|
||||
(403, "permission_error"),
|
||||
(404, "invalid_request_error"),
|
||||
(409, "invalid_request_error"),
|
||||
(422, "invalid_request_error"),
|
||||
(429, "rate_limit_error"),
|
||||
(500, ProxyErrorTypes.internal_server_error.value),
|
||||
(503, ProxyErrorTypes.internal_server_error.value),
|
||||
],
|
||||
)
|
||||
def test_handle_exception_on_proxy_labels_an_http_exception_by_the_status_it_answers(
|
||||
status_code: int, expected_type: str
|
||||
):
|
||||
"""A management route that rejects a caller's own argument answers 400, and calling that
|
||||
internal_server_error tells the caller's retry loop the gateway is down when nothing but
|
||||
their request is wrong."""
|
||||
result = handle_exception_on_proxy(HTTPException(status_code=status_code, detail="boom"))
|
||||
|
||||
assert (result.code, result.type) == (str(status_code), expected_type)
|
||||
|
||||
|
||||
def _exception_carrying_status(carried: int) -> Exception:
|
||||
class _CarriedStatus(Exception):
|
||||
status_code = carried
|
||||
|
||||
return _CarriedStatus("boom")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"status_code, expected_type",
|
||||
[
|
||||
(400, "invalid_request_error"),
|
||||
(404, "invalid_request_error"),
|
||||
(429, "rate_limit_error"),
|
||||
(500, ProxyErrorTypes.internal_server_error.value),
|
||||
],
|
||||
)
|
||||
def test_handle_exception_on_proxy_labels_a_carried_status_the_same_way(status_code: int, expected_type: str):
|
||||
"""Routes also reach this handler with plain exceptions carrying their own status, and
|
||||
those must be named by that status rather than all landing on the 500 label."""
|
||||
result = handle_exception_on_proxy(_exception_carrying_status(status_code))
|
||||
|
||||
assert (result.code, result.type) == (str(status_code), expected_type)
|
||||
|
||||
|
||||
def test_handle_exception_on_proxy_keeps_the_type_the_exception_already_names():
|
||||
"""A rate limit litellm raised already names itself, and overwriting that with the status
|
||||
map's guess would drop what the client branches on."""
|
||||
result = handle_exception_on_proxy(
|
||||
litellm.RateLimitError(message="slow down", llm_provider="openai", model="gpt-4o")
|
||||
)
|
||||
|
||||
assert (result.code, result.type) == ("429", "throttling_error")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue