Merge pull request #39536 from BerriAI/litellm_openai_error_payload_non_llm_routes

fix(proxy): stop shipping the literal string "None" as error type and param
This commit is contained in:
Mateo Wang 2026-09-08 16:41:05 -07:00 committed by GitHub
commit 2b9a69d783
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 747 additions and 167 deletions

View file

@ -25,6 +25,11 @@ from litellm.proxy.common_request_processing import (
proxy_exception_from_http_exception,
)
from litellm.proxy.common_utils.http_parsing_utils import _read_request_body
from litellm.proxy.common_utils.openai_error_payload import (
error_status_code,
openai_error_param,
openai_error_type,
)
from litellm.types.utils import TokenCountResponse
router: Final = APIRouter()
@ -243,9 +248,9 @@ async def anthropic_response(
return _anthropic_error_json_response(
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),
headers=headers,
),
request,

View file

@ -54,6 +54,12 @@ from litellm.proxy.common_utils.callback_utils import (
get_logging_caching_headers,
get_remaining_tokens_and_requests_from_request_data,
)
from litellm.proxy.common_utils.openai_error_payload import (
attribute_of,
error_status_code,
openai_error_param,
openai_error_type,
)
from litellm.proxy.common_utils.sse_keepalive import (
SSE_COMMENT_PING_BYTES,
coerce_keepalive_interval,
@ -464,46 +470,6 @@ def _stream_usage_tracking_updates(
}
def _getattr_object(value: object, name: str, default: object = None) -> object:
return getattr(value, name, default)
_OPENAI_ERROR_TYPE_BY_STATUS: Final[Mapping[int, str]] = MappingProxyType(
{
status.HTTP_401_UNAUTHORIZED: "authentication_error",
status.HTTP_403_FORBIDDEN: "permission_error",
status.HTTP_429_TOO_MANY_REQUESTS: "rate_limit_error",
}
)
def _error_status_code(exc: object, default: int) -> int:
"""The HTTP status an exception carries, or ``default`` when it carries none."""
carried: Final = _getattr_object(exc, "status_code")
return carried if isinstance(carried, int) and not isinstance(carried, bool) else default
def _openai_error_type(exc: object, status_code: int) -> str:
"""OpenAI types ``error.type`` as a required string, so an exception carrying none
falls back to the type its status code stands for."""
carried: Final = _getattr_object(exc, "type")
if isinstance(carried, str):
return carried
mapped: Final = _OPENAI_ERROR_TYPE_BY_STATUS.get(status_code)
if mapped is not None:
return mapped
if status_code < status.HTTP_500_INTERNAL_SERVER_ERROR:
return "invalid_request_error"
return "internal_server_error"
def _openai_error_param(exc: object) -> str | None:
"""OpenAI types ``error.param`` as nullable, so an exception carrying none
serializes as JSON ``null``."""
carried: Final = _getattr_object(exc, "param")
return carried if isinstance(carried, str) else None
class _UpstreamHttpResponse(Protocol):
@property
def status_code(self) -> int: ...
@ -573,15 +539,15 @@ def serialize_http_exception_detail(
def proxy_exception_from_http_exception(exc: HTTPException, headers: dict[str, str]) -> ProxyException:
raw_detail: Final = _getattr_object(exc, "detail", str(exc))
raw_detail: Final = attribute_of(exc, "detail", str(exc))
message, structured_fields = serialize_http_exception_detail(raw_detail)
existing_fields: Final = getattr(exc, "provider_specific_fields", None) or {}
merged_fields: Final = {**existing_fields, **structured_fields} if structured_fields else (existing_fields or None)
error_status: Final = _error_status_code(exc, status.HTTP_400_BAD_REQUEST)
error_status: Final = error_status_code(exc, status.HTTP_400_BAD_REQUEST)
return ProxyException(
message=message,
type=_openai_error_type(exc, error_status),
param=_openai_error_param(exc),
type=openai_error_type(exc, error_status),
param=openai_error_param(exc),
code=error_status,
provider_specific_fields=merged_fields,
headers=headers,
@ -865,8 +831,8 @@ def sse_error_payload(exc: BaseException) -> tuple[int, Mapping[str, object]]:
are byte-identical.
"""
# Preserve status code from HTTPException (e.g. guardrail blocks)
error_status: Final = _error_status_code(exc, status.HTTP_500_INTERNAL_SERVER_ERROR)
raw_detail: Final = _getattr_object(exc, "detail", "Error processing stream start")
error_status: Final = error_status_code(exc, status.HTTP_500_INTERNAL_SERVER_ERROR)
raw_detail: Final = attribute_of(exc, "detail", "Error processing stream start")
message, structured_fields = serialize_http_exception_detail(raw_detail)
existing_fields: Final = getattr(exc, "provider_specific_fields", None) or {}
@ -874,8 +840,8 @@ def sse_error_payload(exc: BaseException) -> tuple[int, Mapping[str, object]]:
error_obj: Final = {
"message": message,
"type": _openai_error_type(exc, error_status),
"param": _openai_error_param(exc),
"type": openai_error_type(exc, error_status),
"param": openai_error_param(exc),
"code": str(error_status),
}
if not merged_fields:
@ -2815,10 +2781,10 @@ class ProxyBaseLLMRequestProcessing:
``ResponsesAPIResponse`` directly. Handle both shapes so the
container-ownership recording path can walk ``.output`` either way.
"""
completed: Final = _getattr_object(stream_response, "completed_response")
completed: Final = attribute_of(stream_response, "completed_response")
if completed is None:
return None
response_obj: Final = _getattr_object(completed, "response")
response_obj: Final = attribute_of(completed, "response")
if response_obj is not None:
return response_obj
return completed
@ -3468,7 +3434,7 @@ class ProxyBaseLLMRequestProcessing:
headers = getattr(e, "headers", None) or {}
if not headers:
# Try to get headers from e.response.headers (httpx.Response)
_response: Final = _getattr_object(e, "response")
_response: Final = attribute_of(e, "response")
if _response is not None:
_response_headers: Final = getattr(_response, "headers", None)
if _response_headers:
@ -3543,8 +3509,8 @@ class ProxyBaseLLMRequestProcessing:
_code = status.HTTP_500_INTERNAL_SERVER_ERROR
raise ProxyException(
message=redact_internal_details_from_client_message(getattr(e, "message", error_msg)),
type=_openai_error_type(e, _code),
param=_openai_error_param(e),
type=openai_error_type(e, _code),
param=openai_error_param(e),
openai_code=getattr(e, "code", None),
code=_code,
provider_specific_fields=getattr(e, "provider_specific_fields", None),
@ -3754,11 +3720,11 @@ class ProxyBaseLLMRequestProcessing:
if isinstance(e, HTTPException):
raise e
stream_error_status: Final = _error_status_code(e, status.HTTP_500_INTERNAL_SERVER_ERROR)
stream_error_status: Final = error_status_code(e, status.HTTP_500_INTERNAL_SERVER_ERROR)
proxy_exception: Final = ProxyException(
message=redact_internal_details_from_client_message(getattr(e, "message", str(e))),
type=_openai_error_type(e, stream_error_status),
param=_openai_error_param(e),
type=openai_error_type(e, stream_error_status),
param=openai_error_param(e),
code=stream_error_status,
)
stream_completed = True

View file

@ -0,0 +1,52 @@
"""Shapes the ``error`` object the proxy answers with so it matches OpenAI's contract:
``type`` is a required string and ``param`` is nullable, neither of which the literal
string ``"None"`` satisfies."""
from collections.abc import Mapping
from types import MappingProxyType
from typing import Final
from fastapi import status
_OPENAI_ERROR_TYPE_BY_STATUS: Final[Mapping[int, str]] = MappingProxyType(
{
status.HTTP_401_UNAUTHORIZED: "authentication_error",
status.HTTP_403_FORBIDDEN: "permission_error",
status.HTTP_429_TOO_MANY_REQUESTS: "rate_limit_error",
}
)
def attribute_of(value: object, name: str, default: object = None) -> object:
return getattr(value, name, default)
def error_status_code(exc: object, default: int) -> int:
"""The HTTP status an exception carries as ``status_code`` or, the way ``ProxyException``
stores it, as a stringified ``code``; ``default`` when it carries neither."""
carried: Final = attribute_of(exc, "status_code")
if isinstance(carried, int) and not isinstance(carried, bool):
return carried
stringified: Final = attribute_of(exc, "code")
return int(stringified) if isinstance(stringified, str) and stringified.isdecimal() else default
def openai_error_type(exc: object, status_code: int) -> str:
"""OpenAI types ``error.type`` as a required string, so an exception carrying none
falls back to the type its status code stands for."""
carried: Final = attribute_of(exc, "type")
if isinstance(carried, str):
return carried
mapped: Final = _OPENAI_ERROR_TYPE_BY_STATUS.get(status_code)
if mapped is not None:
return mapped
if status_code < status.HTTP_500_INTERNAL_SERVER_ERROR:
return "invalid_request_error"
return "internal_server_error"
def openai_error_param(exc: object) -> str | None:
"""OpenAI types ``error.param`` as nullable, so an exception carrying none
serializes as JSON ``null``."""
carried: Final = attribute_of(exc, "param")
return carried if isinstance(carried, str) else None

View file

@ -20,6 +20,11 @@ from litellm.proxy.common_utils.http_parsing_utils import (
coerce_numeric_form_fields,
numeric_form_fields,
)
from litellm.proxy.common_utils.openai_error_payload import (
error_status_code,
openai_error_param,
openai_error_type,
)
from litellm.proxy.route_llm_request import route_request
from litellm.types.images.main import ImageEditRequestParams
from litellm.types.llms.openai import ChatCompletionUserMessage
@ -200,18 +205,18 @@ async def image_generation(
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"),
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),
)

View file

@ -45,6 +45,11 @@ from litellm.proxy.common_utils.openai_endpoint_utils import (
get_custom_llm_provider_from_request_headers,
get_custom_llm_provider_from_request_query,
)
from litellm.proxy.common_utils.openai_error_payload import (
error_status_code,
openai_error_param,
openai_error_type,
)
from litellm.proxy.openai_files_endpoints.batch_file_validation import (
check_batch_file_upload,
raise_batch_file_validation_failure,
@ -296,22 +301,22 @@ async def route_create_file(
if managed_files_obj is None:
raise ProxyException(
message="Managed files hook not found",
type="None",
param="None",
type=ProxyErrorTypes.internal_server_error.value,
param=None,
code=500,
)
if llm_router is None:
raise ProxyException(
message="LLM Router not found",
type="None",
param="None",
type=ProxyErrorTypes.internal_server_error.value,
param=None,
code=500,
)
if not isinstance(managed_files_obj, BaseFileEndpoints):
raise ProxyException(
message="Managed files hook is not a BaseFileEndpoints",
type="None",
param="None",
type=ProxyErrorTypes.internal_server_error.value,
param=None,
code=500,
)
# Managed files internally calls llm_router.acreate_file() which includes loadbalancing
@ -713,17 +718,17 @@ async def create_file(
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"),
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),
)
finally:
for spool in spools:
@ -812,22 +817,22 @@ async def get_file_content(
if managed_files_obj is None:
raise ProxyException(
message="Managed files hook not found",
type="None",
param="None",
type=ProxyErrorTypes.internal_server_error.value,
param=None,
code=500,
)
if llm_router is None:
raise ProxyException(
message="LLM Router not found",
type="None",
param="None",
type=ProxyErrorTypes.internal_server_error.value,
param=None,
code=500,
)
if not isinstance(managed_files_obj, BaseFileEndpoints):
raise ProxyException(
message="Managed files hook is not a BaseFileEndpoints",
type="None",
param="None",
type=ProxyErrorTypes.internal_server_error.value,
param=None,
code=500,
)
@ -1021,17 +1026,17 @@ async def get_file_content(
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"),
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),
)
@ -1151,15 +1156,15 @@ async def get_file(
if managed_files_obj is None:
raise ProxyException(
message="Managed files hook not found",
type="None",
param="None",
type=ProxyErrorTypes.internal_server_error.value,
param=None,
code=500,
)
if not isinstance(managed_files_obj, BaseFileEndpoints):
raise ProxyException(
message="Managed files hook is not a BaseFileEndpoints",
type="None",
param="None",
type=ProxyErrorTypes.internal_server_error.value,
param=None,
code=500,
)
response = await managed_files_obj.afile_retrieve(
@ -1215,17 +1220,17 @@ async def get_file(
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"),
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),
)
@ -1355,22 +1360,22 @@ async def delete_file(
if managed_files_obj is None:
raise ProxyException(
message="Managed files hook not found",
type="None",
param="None",
type=ProxyErrorTypes.internal_server_error.value,
param=None,
code=500,
)
if llm_router is None:
raise ProxyException(
message="LLM Router not found",
type="None",
param="None",
type=ProxyErrorTypes.internal_server_error.value,
param=None,
code=500,
)
if not isinstance(managed_files_obj, BaseFileEndpoints):
raise ProxyException(
message="Managed files hook is not a BaseFileEndpoints",
type="None",
param="None",
type=ProxyErrorTypes.internal_server_error.value,
param=None,
code=500,
)
@ -1427,17 +1432,17 @@ async def delete_file(
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"),
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),
)
@ -1629,15 +1634,15 @@ async def list_files(
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"),
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),
)

View file

@ -78,6 +78,11 @@ from litellm.proxy.common_utils.http_parsing_utils import (
_read_request_body,
_safe_get_request_headers,
)
from litellm.proxy.common_utils.openai_error_payload import (
error_status_code,
openai_error_param,
openai_error_type,
)
from litellm.proxy.common_utils.sse_keepalive import (
wrap_passthrough_sse_bytes_with_keepalive_pings,
)
@ -311,9 +316,9 @@ async def chat_completion_pass_through_endpoint(
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),
)
@ -1728,18 +1733,18 @@ async def pass_through_request(
if isinstance(e, HTTPException):
raise ProxyException(
message=getattr(e, "message", str(getattr(e, "detail", 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),
headers=custom_headers,
)
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),
headers=custom_headers,
)

View file

@ -17,6 +17,11 @@ from litellm.proxy.common_utils.encrypt_decrypt_utils import (
encrypt_value_helper,
)
from litellm.proxy.common_utils.http_parsing_utils import _read_request_body
from litellm.proxy.common_utils.openai_error_payload import (
error_status_code,
openai_error_param,
openai_error_type,
)
from litellm.types.realtime import (
RealtimeClientSecretRequest,
RealtimeClientSecretResponse,
@ -304,15 +309,15 @@ async def create_realtime_client_secret(
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", http_status.HTTP_400_BAD_REQUEST),
type=openai_error_type(e, error_status_code(e, http_status.HTTP_400_BAD_REQUEST)),
param=openai_error_param(e),
code=error_status_code(e, http_status.HTTP_400_BAD_REQUEST),
)
raise ProxyException(
message=getattr(e, "message", str(e)),
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),
)
if upstream_resp.status_code != 200:
@ -495,15 +500,15 @@ async def proxy_realtime_calls(
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", http_status.HTTP_400_BAD_REQUEST),
type=openai_error_type(e, error_status_code(e, http_status.HTTP_400_BAD_REQUEST)),
param=openai_error_param(e),
code=error_status_code(e, http_status.HTTP_400_BAD_REQUEST),
)
raise ProxyException(
message=getattr(e, "message", str(e)),
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),
)
return Response(
@ -608,15 +613,15 @@ async def create_realtime_transcription_session(
if isinstance(e, HTTPException):
raise ProxyException(
message=getattr(e, "detail", getattr(e, "message", str(e))),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", http_status.HTTP_400_BAD_REQUEST),
type=openai_error_type(e, error_status_code(e, http_status.HTTP_400_BAD_REQUEST)),
param=openai_error_param(e),
code=error_status_code(e, http_status.HTTP_400_BAD_REQUEST),
)
raise ProxyException(
message=getattr(e, "message", str(e)),
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),
)
if upstream_resp.status_code != 200:

View file

@ -11,6 +11,11 @@ from litellm._logging import verbose_proxy_logger
from litellm.proxy._types import *
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
from litellm.proxy.common_utils.openai_error_payload import (
error_status_code,
openai_error_param,
openai_error_type,
)
router: Final = APIRouter()
@ -112,15 +117,15 @@ async def rerank(
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),
)

View file

@ -37,6 +37,7 @@ from litellm.proxy._types import (
SpendLogsMetadata,
SpendLogsPayload,
)
from litellm.proxy.common_utils.openai_error_payload import openai_error_param
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
@ -7409,7 +7410,7 @@ def handle_exception_on_proxy(e: Exception) -> ProxyException:
return ProxyException(
message=getattr(e, "detail", f"error({e})"),
type=ProxyErrorTypes.internal_server_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):
@ -7418,7 +7419,7 @@ def handle_exception_on_proxy(e: Exception) -> ProxyException:
return ProxyException(
message=str(e),
type=ProxyErrorTypes.internal_server_error,
param=getattr(e, "param", "None"),
param=openai_error_param(e),
code=_status_code,
)

View file

@ -0,0 +1,145 @@
import json
import pytest
from fastapi import HTTPException
from litellm.proxy._types import ProxyErrorTypes, ProxyException
from litellm.proxy.common_utils.openai_error_payload import (
error_status_code,
openai_error_param,
openai_error_type,
)
@pytest.mark.parametrize(
"status_code, expected_type",
[
(400, "invalid_request_error"),
(401, "authentication_error"),
(403, "permission_error"),
(404, "invalid_request_error"),
(408, "invalid_request_error"),
(422, "invalid_request_error"),
(429, "rate_limit_error"),
(499, "invalid_request_error"),
(500, "internal_server_error"),
(502, "internal_server_error"),
(503, "internal_server_error"),
],
)
def test_status_code_decides_the_type_when_the_exception_carries_none(status_code: int, expected_type: str):
"""A route that raises a bare HTTPException carries no error type, so the status it
answered with is the only thing left to name the OpenAI type from."""
assert openai_error_type(HTTPException(status_code=status_code, detail="boom"), status_code) == expected_type
def test_a_carried_type_wins_over_the_one_the_status_would_imply():
"""A ProxyException raised mid-request already names its own type, and relabelling a
402 budget_exceeded as the status map's guess would lose what the client branches on."""
carried = ProxyException(
message="Budget has been exceeded",
type=ProxyErrorTypes.budget_exceeded.value,
param=None,
code=400,
)
assert openai_error_type(carried, 400) == ProxyErrorTypes.budget_exceeded.value
@pytest.mark.parametrize("carried_type", [None, 400, {"type": "invalid_request_error"}, ["invalid_request_error"]])
def test_a_non_string_carried_type_falls_back_to_the_status(carried_type: object):
"""OpenAI types error.type as a string, so anything else on the exception is not one and
must not reach the wire the way the literal "None" used to."""
class _Carrier(Exception):
type = carried_type
assert openai_error_type(_Carrier("boom"), 401) == "authentication_error"
def test_the_type_is_never_the_string_none_after_a_json_round_trip():
"""The bug this module exists for: json.dumps of a "None" default is indistinguishable
from a real type to a client's error handler."""
payload = json.loads(
json.dumps(
{
"type": openai_error_type(HTTPException(status_code=400, detail="boom"), 400),
"param": openai_error_param(HTTPException(status_code=400, detail="boom")),
}
)
)
assert payload == {"type": "invalid_request_error", "param": None}
def test_a_carried_param_names_the_offending_field():
carried = ProxyException(message="Invalid purpose", type="invalid_request_error", param="purpose", code=400)
assert openai_error_param(carried) == "purpose"
@pytest.mark.parametrize("exc", [HTTPException(status_code=400, detail="boom"), ValueError("boom"), None])
def test_param_is_json_null_when_the_exception_names_no_field(exc: Exception | None):
assert openai_error_param(exc) is None
def test_a_non_string_carried_param_is_json_null():
class _Carrier(Exception):
param = 42
assert openai_error_param(_Carrier("boom")) is None
def test_a_carried_status_code_wins_over_the_default():
assert error_status_code(HTTPException(status_code=429, detail="slow down"), 400) == 429
@pytest.mark.parametrize("default", [400, 500])
def test_the_default_status_stands_when_the_exception_carries_none(default: int):
assert error_status_code(ValueError("boom"), default) == default
@pytest.mark.parametrize("carried_status", [True, False, "429", None, 429.0])
def test_a_non_int_carried_status_falls_back_to_the_default(carried_status: object):
"""True is an int in Python but not an HTTP status, and a stringified one would break
every caller that compares the code numerically."""
class _Carrier(Exception):
status_code = carried_status
assert error_status_code(_Carrier("boom"), 500) == 500
def test_a_proxy_exception_keeps_the_status_it_was_raised_with():
"""ProxyException stores its status as the string ``code`` rather than ``status_code``,
so a route tail that rewraps one used to answer a 4xx rejection as a 500."""
rejection = ProxyException(message="session_id is required", type="bad_request_error", param="session_id", code=400)
assert error_status_code(rejection, 500) == 400
@pytest.mark.parametrize("carried_code", [None, "None", "", "rate_limited", "4xx", 404])
def test_a_code_that_is_not_a_decimal_string_falls_back_to_the_default(carried_code: object):
"""Only ProxyException's stringified status is a status; ``code`` on anything else
(OpenAI's ``invalid_api_key``, a stray int) says nothing about the HTTP answer."""
class _Carrier(Exception):
code = carried_code
assert error_status_code(_Carrier("boom"), 500) == 500
def test_a_status_code_wins_over_a_stringified_code():
class _Carrier(Exception):
status_code = 429
code = "400"
assert error_status_code(_Carrier("boom"), 500) == 429
def test_a_status_carried_by_an_exception_drives_the_type_it_reports():
"""The two helpers compose at every call site: the status the exception carries is what
names its type, not the default the route would have used."""
exc = HTTPException(status_code=403, detail="blocked by policy")
assert openai_error_type(exc, error_status_code(exc, 400)) == "permission_error"

View file

@ -5,12 +5,12 @@ from typing import Any, Dict
import orjson
import pytest
from fastapi import FastAPI
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from starlette.requests import Request
from starlette.responses import Response
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy._types import ProxyException, UserAPIKeyAuth
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.image_endpoints import endpoints
@ -167,3 +167,47 @@ def test_image_edit_multipart_n_that_is_not_a_number_is_left_alone(monkeypatch):
assert response.status_code == 200
assert captured["n"] == "two"
@pytest.mark.asyncio
async def test_a_model_the_router_cannot_serve_answers_an_openai_typed_error(monkeypatch: pytest.MonkeyPatch):
"""A bare HTTPException carries no type or param, so the tail used to ship the
literal string "None" in both fields."""
async def fake_add_litellm_data_to_request(**kwargs: object) -> object:
return kwargs["data"]
async def fake_pre_call_hook(*, user_api_key_dict: UserAPIKeyAuth, data: dict[str, object], call_type: str) -> dict[str, object]:
return data
async def fake_post_call_failure_hook(**_: object) -> None:
return None
async def failing_route_request(**_: object) -> None:
raise HTTPException(
status_code=404, detail={"error": "image_generation: Invalid model name passed in model=dall-e-3"}
)
monkeypatch.setattr("litellm.proxy.proxy_server.add_litellm_data_to_request", fake_add_litellm_data_to_request)
monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {})
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None)
monkeypatch.setattr("litellm.proxy.proxy_server.proxy_config", {})
monkeypatch.setattr(
"litellm.proxy.proxy_server.proxy_logging_obj",
SimpleNamespace(pre_call_hook=fake_pre_call_hook, post_call_failure_hook=fake_post_call_failure_hook),
)
monkeypatch.setattr("litellm.proxy.proxy_server.user_model", None)
monkeypatch.setattr("litellm.proxy.proxy_server.version", "test-version")
monkeypatch.setattr("litellm.proxy.image_endpoints.endpoints.route_request", failing_route_request)
body = orjson.dumps({"model": "dall-e-3", "prompt": "a lighthouse at dusk"})
async def receive() -> dict[str, object]:
return {"type": "http.request", "body": body, "more_body": False}
request = Request({"type": "http", "method": "POST", "path": "/v1/images/generations", "headers": []}, receive)
with pytest.raises(ProxyException) as raised:
await endpoints.image_generation(request=request, fastapi_response=Response(), user_api_key_dict=UserAPIKeyAuth())
assert (raised.value.type, raised.value.param, raised.value.code) == ("invalid_request_error", None, "404")

View file

@ -2920,9 +2920,9 @@ def test_unscoped_list_files_accepts_every_documented_purpose(
def test_list_files_reports_a_bad_target_model_names_as_a_400(
mocker: MockerFixture, monkeypatch, llm_router: Router
):
"""The exception tail reports an HTTPException with its own status and error
type rather than relabelling it, so a client that branches on either keeps
reading the same thing off a bad request."""
"""The exception tail answers with the OpenAI error object a client can branch on:
the type its 400 status stands for, and a JSON null param rather than the literal
string "None" no OpenAI SDK has a case for."""
_setup_unscoped_list_files_route(mocker, monkeypatch, llm_router, _permissive_afile_list)
response = _get_list_files("/v1/files?target_model_names=gpt-3.5-turbo,gpt-4o")
@ -2931,8 +2931,8 @@ def test_list_files_reports_a_bad_target_model_names_as_a_400(
assert response.json() == {
"error": {
"message": "target_model_names on list files must be a list of one model name. Example: ['gpt-4o']",
"type": "None",
"param": "None",
"type": "invalid_request_error",
"param": None,
"code": "400",
}
}
@ -4666,3 +4666,156 @@ def test_create_file_path_traversal_filename_rejected_before_forwarding(monkeypa
assert error["param"] == "file"
assert "traversal" in error["message"].lower()
assert forwarded_calls == []
def _setup_managed_file_route_answering_404(
mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch, llm_router: Router
) -> None:
"""Wire the single-file routes to a managed file store that knows no file, the way the
managed files hook answers once a file has been deleted or was never the caller's."""
import litellm.proxy.proxy_server as ps
from fastapi import HTTPException
from litellm.llms.base_llm.files.transformation import BaseFileEndpoints
from litellm.proxy._types import LitellmUserRoles
async def _file_not_found(file_id: str, **kwargs: object) -> None:
raise HTTPException(status_code=404, detail=f"File not found: {file_id}")
proxy_logging_obj = setup_proxy_logging_object(monkeypatch, llm_router)
managed_files = mocker.MagicMock(spec=BaseFileEndpoints)
managed_files.afile_retrieve = mocker.AsyncMock(side_effect=_file_not_found)
managed_files.afile_delete = mocker.AsyncMock(side_effect=_file_not_found)
managed_files.afile_content = mocker.AsyncMock(side_effect=_file_not_found)
proxy_logging_obj.proxy_hook_mapping["managed_files"] = managed_files
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", llm_router)
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
api_key="test-key",
user_role=LitellmUserRoles.INTERNAL_USER,
user_id="test-user",
)
def _call_managed_file_route(method: str, path: str) -> httpx.Response:
try:
return client.request(method, path, headers={"Authorization": "Bearer test-key"})
finally:
import litellm.proxy.proxy_server as ps
app.dependency_overrides.pop(ps.user_api_key_auth, None)
def _missing_managed_file_error(file_id: str) -> dict[str, dict[str, str | None]]:
return {
"error": {
"message": f"File not found: {file_id}",
"type": "invalid_request_error",
"param": None,
"code": "404",
}
}
def test_create_file_reports_a_half_specified_expires_after_as_a_400(
monkeypatch: pytest.MonkeyPatch, llm_router: Router
):
"""A 400 raised inside the route answers with the type a 400 stands for and a JSON null
param, not the literal string "None" in both fields, so a client can classify it."""
setup_proxy_logging_object(monkeypatch, llm_router)
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", llm_router)
response = client.post(
"/v1/files",
files={"file": ("mydata.jsonl", VALID_BATCH_LINE, "application/jsonl")},
data={"purpose": "batch", "target_model_names": "gpt-3.5-turbo", "expires_after[anchor]": "created_at"},
headers={"Authorization": "Bearer test-key"},
)
assert response.status_code == 400, response.text
error = response.json()["error"]
assert "expires_after[seconds]" in error["message"]
assert error["type"] == "invalid_request_error"
assert error["param"] is None
assert error["code"] == "400"
def test_get_file_reports_a_missing_managed_file_as_a_404(
mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch, llm_router: Router
):
_setup_managed_file_route_answering_404(mocker, monkeypatch, llm_router)
file_id = _unified_managed_file_id()
response = _call_managed_file_route("GET", f"/v1/files/{file_id}")
assert response.status_code == 404, response.text
assert response.json() == _missing_managed_file_error(file_id)
def test_delete_file_reports_a_missing_managed_file_as_a_404(
mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch, llm_router: Router
):
_setup_managed_file_route_answering_404(mocker, monkeypatch, llm_router)
file_id = _unified_managed_file_id()
response = _call_managed_file_route("DELETE", f"/v1/files/{file_id}")
assert response.status_code == 404, response.text
assert response.json() == _missing_managed_file_error(file_id)
def test_get_file_content_reports_a_missing_managed_file_as_a_404(
mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch, llm_router: Router
):
_setup_managed_file_route_answering_404(mocker, monkeypatch, llm_router)
file_id = _unified_managed_file_id()
response = _call_managed_file_route("GET", f"/v1/files/{file_id}/content")
assert response.status_code == 404, response.text
assert response.json() == _missing_managed_file_error(file_id)
def _setup_managed_file_stored_in_an_unknown_storage_backend(
mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch, llm_router: Router
) -> None:
"""Wire the content route to a managed file whose row names a storage backend the
factory does not know, which is the one in-route ProxyException on these routes."""
from types import SimpleNamespace
import litellm.proxy.proxy_server as ps
from litellm.llms.base_llm.files.transformation import BaseFileEndpoints
from litellm.proxy._types import LitellmUserRoles
proxy_logging_obj = setup_proxy_logging_object(monkeypatch, llm_router)
managed_files = mocker.MagicMock(spec=BaseFileEndpoints)
managed_files.prisma_client = mocker.MagicMock()
proxy_logging_obj.proxy_hook_mapping["managed_files"] = managed_files
repository = mocker.MagicMock()
repository.table.find_first = mocker.AsyncMock(
return_value=SimpleNamespace(storage_backend="ftp", storage_url="ftp://bucket/file")
)
monkeypatch.setattr(
"litellm.proxy.openai_files_endpoints.files_endpoints.ManagedFileRepository", lambda _prisma: repository
)
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", llm_router)
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
api_key="test-key",
user_role=LitellmUserRoles.INTERNAL_USER,
user_id="test-user",
)
def test_get_file_content_keeps_the_status_of_a_rejection_raised_inside_the_route(
mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch, llm_router: Router
):
"""A ProxyException raised inside the route carries its status as the string ``code``,
and the tail used to rebuild it as a 500 because it only read ``status_code``."""
_setup_managed_file_stored_in_an_unknown_storage_backend(mocker, monkeypatch, llm_router)
response = _call_managed_file_route("GET", f"/v1/files/{_unified_managed_file_id()}/content")
assert response.status_code == 400, response.text
error = response.json()["error"]
assert error["message"].startswith("Storage backend error")
assert (error["type"], error["param"], error["code"]) == ("invalid_request_error", "file_id", "400")

View file

@ -11,7 +11,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest
from fastapi import Request, UploadFile
from fastapi import Request, Response, UploadFile
from starlette.datastructures import FormData, Headers, QueryParams
from starlette.datastructures import UploadFile as StarletteUploadFile
@ -22,6 +22,7 @@ from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
InitPassThroughEndpointHelpers,
LITELLM_PASS_THROUGH_CUSTOM_BODY_STATE_KEY,
_registered_pass_through_routes,
chat_completion_pass_through_endpoint,
create_pass_through_route,
initialize_pass_through_endpoints,
pass_through_request,
@ -5837,3 +5838,38 @@ def test_passthrough_client_cannot_forge_session_id_omission(client_metadata_key
)
== "per-call-random-trace-id"
)
@pytest.mark.asyncio
async def test_chat_completion_pass_through_endpoint_answers_an_openai_typed_error_for_an_unknown_model(
monkeypatch: pytest.MonkeyPatch,
):
"""A bare HTTPException carries no type or param, so the tail used to ship the
literal string "None" in both fields."""
proxy_logging = MagicMock()
proxy_logging.pre_call_hook = AsyncMock(side_effect=lambda **kwargs: kwargs["data"])
proxy_logging.post_call_failure_hook = AsyncMock()
async def fake_add_litellm_data_to_request(**kwargs: object) -> object:
return kwargs["data"]
monkeypatch.setattr("litellm.proxy.proxy_server.proxy_logging_obj", proxy_logging)
monkeypatch.setattr("litellm.proxy.proxy_server.add_litellm_data_to_request", fake_add_litellm_data_to_request)
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None)
monkeypatch.setattr("litellm.proxy.proxy_server.user_model", None)
monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {})
request = MagicMock(spec=Request)
request.body = AsyncMock(
return_value=json.dumps({"model": "unknown-model", "messages": [{"role": "user", "content": "hi"}]}).encode()
)
with pytest.raises(ProxyException) as raised:
await chat_completion_pass_through_endpoint(
fastapi_response=Response(),
request=request,
adapter_id="anthropic",
user_api_key_dict=UserAPIKeyAuth(api_key="sk-test"),
)
assert (raised.value.type, raised.value.param, raised.value.code) == ("invalid_request_error", None, "400")

View file

@ -6,10 +6,13 @@ Tests for LiteLLM proxy realtime WebRTC HTTP endpoints:
import json
import time
from collections.abc import Awaitable
from typing import Protocol
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
@ -159,17 +162,27 @@ def mock_route_request_realtime_calls():
return _mock_route
class AddLitellmDataToRequest(Protocol):
def __call__(self, data: dict[str, object], **kwargs: object) -> Awaitable[dict[str, object]]: ...
class PreCallHook(Protocol):
def __call__(
self, user_api_key_dict: UserAPIKeyAuth, data: dict[str, object], call_type: str
) -> Awaitable[dict[str, object]]: ...
@pytest.fixture
def mock_add_litellm_data():
async def _mock(data, **kwargs):
def mock_add_litellm_data() -> AddLitellmDataToRequest:
async def _mock(data: dict[str, object], **kwargs: object) -> dict[str, object]:
return data
return _mock
@pytest.fixture
def mock_pre_call_hook():
async def _mock(user_api_key_dict, data, call_type):
def mock_pre_call_hook() -> PreCallHook:
async def _mock(user_api_key_dict: UserAPIKeyAuth, data: dict[str, object], call_type: str) -> dict[str, object]:
return data
return _mock
@ -1199,3 +1212,78 @@ async def test_transcription_sessions_wraps_route_exception(
assert "Model not allowed" in response.text
finally:
proxy_app.dependency_overrides.pop(user_api_key_auth, None)
def test_realtime_calls_upstream_rejection_answers_an_openai_typed_error(
proxy_app: FastAPI,
mock_add_litellm_data: AddLitellmDataToRequest,
mock_pre_call_hook: PreCallHook,
monkeypatch: pytest.MonkeyPatch,
):
"""A bare HTTPException carries no type or param, so the tail used to ship the
literal string "None" in both fields of the error the browser client reads."""
token_payload = _encode_realtime_token_payload(
ephemeral_key="fake_upstream_epk",
model_id="gpt-4o-realtime-preview",
user_id=None,
team_id=None,
expires_at=int(time.time()) + 3600,
)
encrypted_token = encrypt_value_helper(token_payload)
async def failing_route_request(*args: object, **kwargs: object) -> None:
raise HTTPException(
status_code=404,
detail={"error": "realtime: Invalid model name passed in model=gpt-4o-realtime-preview"},
)
proxy_logging = MagicMock()
proxy_logging.pre_call_hook = AsyncMock(side_effect=mock_pre_call_hook)
proxy_logging.post_call_failure_hook = AsyncMock()
monkeypatch.setattr("litellm.proxy.proxy_server.route_request", failing_route_request)
monkeypatch.setattr("litellm.proxy.proxy_server.add_litellm_data_to_request", mock_add_litellm_data)
monkeypatch.setattr("litellm.proxy.proxy_server.proxy_logging_obj", proxy_logging)
response = TestClient(proxy_app).post(
"/v1/realtime/calls",
headers={"Authorization": f"Bearer {encrypted_token}"},
content=b"v=0\r\no=- 0 0 IN IP4 0.0.0.0\r\ns=-\r\n",
)
assert response.status_code == 404
assert (response.json()["error"]["type"], response.json()["error"]["param"]) == ("invalid_request_error", None)
def test_transcription_sessions_rejection_answers_an_openai_typed_error(
proxy_app: FastAPI,
mock_add_litellm_data: AddLitellmDataToRequest,
mock_pre_call_hook: PreCallHook,
monkeypatch: pytest.MonkeyPatch,
):
"""A model the router cannot serve surfaces as a bare HTTPException, which this tail
used to relabel with the literal string "None" for both type and param."""
async def failing_route_request(*args: object, **kwargs: object) -> None:
raise HTTPException(
status_code=400,
detail={"error": "realtime: Invalid model name passed in model=no-such-transcribe"},
)
proxy_logging = MagicMock()
proxy_logging.pre_call_hook = AsyncMock(side_effect=mock_pre_call_hook)
proxy_logging.post_call_failure_hook = AsyncMock()
monkeypatch.setattr("litellm.proxy.proxy_server.route_request", failing_route_request)
monkeypatch.setattr("litellm.proxy.proxy_server.add_litellm_data_to_request", mock_add_litellm_data)
monkeypatch.setattr("litellm.proxy.proxy_server.proxy_logging_obj", proxy_logging)
proxy_app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(user_id="test-user")
try:
response = TestClient(proxy_app, raise_server_exceptions=False).post(
"/v1/realtime/transcription_sessions",
headers={"Authorization": "Bearer sk-test-master-key"},
json={"input_audio_transcription": {"model": "no-such-transcribe"}},
)
finally:
proxy_app.dependency_overrides.pop(user_api_key_auth, None)
assert response.status_code == 400
assert (response.json()["error"]["type"], response.json()["error"]["param"]) == ("invalid_request_error", None)

View file

@ -6,11 +6,11 @@ import json
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi import Request, Response
from fastapi import HTTPException, Request, Response
import litellm.proxy.common_request_processing as common_request_processing_mod
import litellm.proxy.proxy_server as proxy_server_mod
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy._types import ProxyException, UserAPIKeyAuth
from litellm.proxy.rerank_endpoints.endpoints import rerank
from litellm.types.utils import RerankResponse
@ -118,3 +118,55 @@ async def test_rerank_omits_detailed_timing_headers_when_disabled():
fastapi_response = await _call_rerank()
assert "x-litellm-timing-llm-api-ms" not in fastapi_response.headers
async def _rerank_failure(
failure: Exception, *, raised_before_routing: bool, monkeypatch: pytest.MonkeyPatch
) -> ProxyException:
proxy_logging_obj = MagicMock()
proxy_logging_obj.pre_call_hook = AsyncMock(
side_effect=failure if raised_before_routing else lambda **kwargs: kwargs["data"]
)
proxy_logging_obj.post_call_failure_hook = AsyncMock()
async def fake_add_litellm_data_to_request(**kwargs: object) -> object:
return kwargs["data"]
async def failing_route_request(**kwargs: object) -> None:
raise failure
monkeypatch.setattr(proxy_server_mod, "add_litellm_data_to_request", fake_add_litellm_data_to_request)
monkeypatch.setattr(proxy_server_mod, "route_request", failing_route_request)
monkeypatch.setattr(proxy_server_mod, "proxy_logging_obj", proxy_logging_obj)
monkeypatch.setattr(proxy_server_mod, "llm_router", MagicMock())
monkeypatch.setattr(proxy_server_mod, "version", "1.2.3")
with pytest.raises(ProxyException) as raised:
await rerank(
request=_build_request(),
fastapi_response=Response(),
user_api_key_dict=UserAPIKeyAuth(api_key="sk-test"),
)
return raised.value
@pytest.mark.asyncio
async def test_a_model_the_router_cannot_serve_answers_an_openai_typed_error(monkeypatch: pytest.MonkeyPatch):
"""A bare HTTPException carries no type or param, so the tail used to ship the
literal string "None" in both fields."""
failure = HTTPException(status_code=404, detail={"error": "rerank: Invalid model name passed in model=rerank-model"})
error = await _rerank_failure(failure, raised_before_routing=False, monkeypatch=monkeypatch)
assert (error.type, error.param, error.code) == ("invalid_request_error", None, "404")
@pytest.mark.asyncio
async def test_a_rejection_raised_before_routing_keeps_its_own_status(monkeypatch: pytest.MonkeyPatch):
"""A ProxyException stores its status as the string ``code``, which the tail used to
miss and rewrap as a 500 while keeping the 4xx type and param."""
rejection = ProxyException(message="session_id is required", type="bad_request_error", param="session_id", code=400)
error = await _rerank_failure(rejection, raised_before_routing=True, monkeypatch=monkeypatch)
assert (error.type, error.param, error.code) == ("bad_request_error", "session_id", "400")

View file

@ -2202,6 +2202,19 @@ class TestGuardrailBlockErrorPayloadNeverStringifiesNone:
assert frame["error"]["param"] is None
assert frame["error"]["code"] == "400"
def test_a_streaming_frame_keeps_the_status_a_proxy_exception_was_raised_with(self):
"""ProxyException stores its status as the string ``code``, so a 429 raised before the
first chunk used to reach the SSE frame as a 500."""
from litellm.proxy._types import ProxyException
from litellm.proxy.common_request_processing import sse_error_payload
error_status, error_obj = sse_error_payload(
ProxyException(message="Rate limit reached", type="rate_limit_error", param=None, code=429)
)
assert error_status == 429
assert (error_obj["type"], error_obj["code"]) == ("rate_limit_error", "429")
@pytest.mark.parametrize(
"status_code, expected_type",
[

View file

@ -135,7 +135,7 @@ def test_handle_exception_on_proxy_happy_path_generic_exception_defaults_to_500(
"message": "kaboom",
"type": ProxyErrorTypes.internal_server_error.value,
"code": "500",
"param": "None",
"param": None,
}