mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
refactor(sdk): move the None sentinel to constants and freeze the init kwargs filter
This commit is contained in:
parent
879fcd847f
commit
f4f1e2eace
7 changed files with 13 additions and 22 deletions
|
|
@ -1980,6 +1980,8 @@ UNSAFE_PROXY_RESPONSE_HEADERS: Final[frozenset[str]] = (
|
|||
HTTP_FRAMING_HEADERS | BROWSER_SECURITY_HEADERS | ORIGIN_SERVER_HEADERS
|
||||
)
|
||||
|
||||
STRINGIFIED_NONE: Final[str] = "None"
|
||||
|
||||
# A retrieved response replays the usage of the call that created it, so pricing these
|
||||
# read/management routes like inference bills the same tokens twice.
|
||||
NON_INFERENCE_CALL_TYPES: Final[frozenset[str]] = frozenset(
|
||||
|
|
|
|||
|
|
@ -235,7 +235,9 @@ class BadRequestError(openai.BadRequestError):
|
|||
self.litellm_debug_info = litellm_debug_info
|
||||
self.max_retries = max_retries
|
||||
self.num_retries = num_retries
|
||||
self.headers: dict[str, str] | None = {k: str(v) for k, v in headers.items()} if headers else None
|
||||
self.headers = (
|
||||
{k: str(v) for k, v in headers.items()} if headers else None # mutable-ok: the proxy updates it in place
|
||||
)
|
||||
# Use response if it's a valid httpx.Response with a request, otherwise use minimal error response
|
||||
# Note: We check _request (not .request property) to avoid RuntimeError when _request is None
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import json
|
|||
import re
|
||||
import traceback
|
||||
from collections.abc import Mapping
|
||||
from types import MappingProxyType
|
||||
from typing import Any, Final, Protocol, cast
|
||||
|
||||
import httpx
|
||||
|
|
@ -206,7 +207,7 @@ def _get_response_headers(original_exception: Exception) -> httpx.Headers | None
|
|||
|
||||
def _accepted_init_kwargs(exception_class: type[Exception], candidates: Mapping[str, object]) -> Mapping[str, object]:
|
||||
accepted: Final = inspect.signature(exception_class).parameters
|
||||
return {name: value for name, value in candidates.items() if name in accepted}
|
||||
return MappingProxyType({name: value for name, value in candidates.items() if name in accepted})
|
||||
|
||||
|
||||
def extract_and_raise_litellm_exception(
|
||||
|
|
@ -236,7 +237,9 @@ def extract_and_raise_litellm_exception(
|
|||
message=error_str,
|
||||
llm_provider=custom_llm_provider,
|
||||
model=model,
|
||||
**_accepted_init_kwargs(raised_exception_obj, {"response": response, "body": body, "headers": headers}),
|
||||
**_accepted_init_kwargs(
|
||||
raised_exception_obj, MappingProxyType({"response": response, "body": body, "headers": headers})
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from typing import Final
|
|||
|
||||
from fastapi import status
|
||||
|
||||
_STRINGIFIED_NONE: Final = "None"
|
||||
from litellm.constants import STRINGIFIED_NONE
|
||||
|
||||
_OPENAI_ERROR_TYPE_BY_STATUS: Final[Mapping[int, str]] = MappingProxyType(
|
||||
{
|
||||
|
|
@ -37,7 +37,7 @@ 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) and carried != _STRINGIFIED_NONE:
|
||||
if isinstance(carried, str) and carried != STRINGIFIED_NONE:
|
||||
return carried
|
||||
mapped: Final = _OPENAI_ERROR_TYPE_BY_STATUS.get(status_code)
|
||||
if mapped is not None:
|
||||
|
|
@ -51,4 +51,4 @@ 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) and carried != _STRINGIFIED_NONE else None
|
||||
return carried if isinstance(carried, str) and carried != STRINGIFIED_NONE else None
|
||||
|
|
|
|||
|
|
@ -1430,8 +1430,6 @@ def _openai_handler_error(
|
|||
status_code: int = 400,
|
||||
message: str = _GUARDRAIL_BLOCK_ERROR["message"],
|
||||
) -> OpenAIError:
|
||||
"""What litellm/llms/openai/openai.py raises after the openai SDK rejects a request:
|
||||
the SDK's str() carries the wire body, and the handler copies headers and body over."""
|
||||
wire_error = {**_GUARDRAIL_BLOCK_ERROR, "type": error_type, "code": str(status_code), "message": message}
|
||||
return OpenAIError(
|
||||
status_code=status_code,
|
||||
|
|
@ -1448,9 +1446,6 @@ _PROXY_HEADERS = {"x-litellm-call-id": "call-guardrail", "x-litellm-applied-guar
|
|||
("error_type", "status_code"), [("None", 400), ("invalid_request_error", 400), ("None", 422)]
|
||||
)
|
||||
def test_litellm_proxy_guardrail_block_keeps_body_and_headers(error_type: str, status_code: int):
|
||||
"""An SDK caller behind a proxy tells a guardrail block from any other 4xx by the body's
|
||||
provider_specific_fields and the proxy's x-litellm-* headers, so the mapped BadRequestError
|
||||
must carry both whichever error.type and status the proxy version on the other end emits."""
|
||||
with pytest.raises(litellm.BadRequestError) as exc_info:
|
||||
exception_type(
|
||||
model="claude-haiku-4-5",
|
||||
|
|
@ -1469,9 +1464,6 @@ def test_litellm_proxy_guardrail_block_keeps_body_and_headers(error_type: str, s
|
|||
"relayed_class", [litellm.BadRequestError, litellm.ContentPolicyViolationError]
|
||||
)
|
||||
def test_litellm_proxy_relayed_litellm_error_keeps_body_and_headers(relayed_class: type[litellm.BadRequestError]):
|
||||
"""A proxy relaying a provider's own litellm error names the class in the message, which
|
||||
re-raises that class on the SDK side before the generic 400 mapping runs; it must carry the
|
||||
body and the proxy headers the same way the generic mapping now does."""
|
||||
message = f"litellm.{relayed_class.__name__}: {_GUARDRAIL_BLOCK_ERROR['message']}"
|
||||
|
||||
with pytest.raises(relayed_class) as exc_info:
|
||||
|
|
@ -1489,8 +1481,6 @@ def test_litellm_proxy_relayed_litellm_error_keeps_body_and_headers(relayed_clas
|
|||
|
||||
|
||||
def test_openai_compatible_vendor_400_keeps_body_but_not_headers():
|
||||
"""A vendor's own response headers stay on e.response the way every other mapped provider
|
||||
error keeps them; only a LiteLLM proxy upstream puts headers on e.headers."""
|
||||
with pytest.raises(litellm.BadRequestError) as exc_info:
|
||||
exception_type(
|
||||
model="gpt-5.4-mini",
|
||||
|
|
|
|||
|
|
@ -146,9 +146,6 @@ def test_a_status_carried_by_an_exception_drives_the_type_it_reports():
|
|||
|
||||
|
||||
def test_a_stringified_none_type_or_param_is_treated_as_absent():
|
||||
"""A proxy fronting a proxy older than 1.102 receives {"type": "None", "param": "None"} on
|
||||
the wire; the SDK now keeps that body on the mapped exception, and re-emitting the literal
|
||||
is the exact bug this module exists to stop."""
|
||||
from litellm.exceptions import BadRequestError
|
||||
|
||||
carried = BadRequestError(
|
||||
|
|
|
|||
|
|
@ -4064,9 +4064,6 @@ class TestHandleLLMApiExceptionFramingHeaders:
|
|||
assert proxy_exc.headers["x-request-id"] == "abc-123"
|
||||
|
||||
async def test_strips_the_date_and_server_headers_of_an_upstream_litellm_proxy(self):
|
||||
"""A proxy fronting another LiteLLM proxy gets the upstream's date and server
|
||||
on the mapped exception; forwarding them would duplicate the Date header
|
||||
uvicorn adds to every response and leak the upstream server identity."""
|
||||
exc = litellm.BadRequestError(
|
||||
message="Content blocked",
|
||||
llm_provider="litellm_proxy",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue