mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
fix(proxy): answer the Anthropic error envelope on the /anthropic pass-through
The pass-through relays upstream errors in Anthropic's shape, but the proxy's own refusals there (bad key, model access, budget) still answered the OpenAI body, so an Anthropic SDK pointed at /anthropic could not read them
This commit is contained in:
parent
f6d5cdd12d
commit
8208298f69
4 changed files with 22 additions and 13 deletions
|
|
@ -1,8 +1,8 @@
|
|||
"""Renders the Anthropic error envelope for the Messages routes, wherever the failure is raised.
|
||||
"""Renders the Anthropic error envelope for the Anthropic routes, wherever the failure is raised.
|
||||
|
||||
The central ``ProxyException`` handler answers every route with OpenAI's ``{"error": {...}}``
|
||||
body, which an Anthropic SDK cannot read. Auth, key, budget and model-access failures are
|
||||
raised by ``user_api_key_auth`` before the Messages handler runs, so the envelope is picked
|
||||
raised by ``user_api_key_auth`` before the route handler runs, so the envelope is picked
|
||||
from the request route rather than inside the handler.
|
||||
"""
|
||||
|
||||
|
|
@ -29,15 +29,17 @@ ANTHROPIC_MESSAGES_ROUTES: Final[frozenset[str]] = frozenset(
|
|||
"/claude_code_gateway/v1/messages/count_tokens",
|
||||
}
|
||||
)
|
||||
ANTHROPIC_PASS_THROUGH_PREFIX: Final = "/anthropic/"
|
||||
|
||||
|
||||
def is_anthropic_messages_route(route: str) -> bool:
|
||||
"""True for the Messages routes, whose callers read the Anthropic error envelope.
|
||||
def uses_anthropic_error_envelope(route: str) -> bool:
|
||||
"""True for the Messages routes and the Anthropic pass-through, whose callers read the Anthropic envelope.
|
||||
|
||||
Narrower than ``LiteLLMRoutes.anthropic_routes``: the Skills and marketplace routes there
|
||||
answer their own body shape, and ``/anthropic/v1/messages`` relays the upstream response.
|
||||
answer their own body shape. The pass-through relays upstream errors as Anthropic sent them,
|
||||
so the proxy's own refusals there answer the same envelope.
|
||||
"""
|
||||
return route in ANTHROPIC_MESSAGES_ROUTES
|
||||
return route in ANTHROPIC_MESSAGES_ROUTES or route.startswith(ANTHROPIC_PASS_THROUGH_PREFIX)
|
||||
|
||||
|
||||
def anthropic_error_status_code(exc: ProxyException) -> int:
|
||||
|
|
|
|||
|
|
@ -471,7 +471,7 @@ from litellm.proxy.common_request_processing import ( # noqa: F401, RUF100 # l
|
|||
)
|
||||
from litellm.proxy.common_utils.anthropic_error_payload import (
|
||||
anthropic_error_json_response,
|
||||
is_anthropic_messages_route,
|
||||
uses_anthropic_error_envelope,
|
||||
)
|
||||
from litellm.proxy.common_utils.auth_cache_invalidation_pubsub import (
|
||||
AuthCacheInvalidationSubscriber,
|
||||
|
|
@ -2192,7 +2192,7 @@ async def openai_exception_handler(request: Request, exc: ProxyException):
|
|||
# NOTE: DO NOT MODIFY THIS, its crucial to map to Openai exceptions
|
||||
log_model_access_denial(exc)
|
||||
headers: Final = exc.headers
|
||||
if is_anthropic_messages_route(get_request_route(request)):
|
||||
if uses_anthropic_error_envelope(get_request_route(request)):
|
||||
return anthropic_error_json_response(exc, request)
|
||||
error_dict: Final = with_call_id(
|
||||
JSON_OBJECT.validate_python(exc.to_dict()),
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ class _AnthropicErrorResponse(BaseModel):
|
|||
|
||||
GENERIC_ERROR: Final = _ProxyError(message=GENERIC_MESSAGE, type="token_not_found_in_db", param="key", code="401")
|
||||
MESSAGES_PATH: Final = "/v1/messages"
|
||||
ANTHROPIC_ENVELOPE_PATHS: Final = frozenset({MESSAGES_PATH, "/anthropic/v1/messages"})
|
||||
ANTHROPIC_GENERIC_ERROR: Final = _AnthropicErrorDetail(type="authentication_error", message=GENERIC_MESSAGE)
|
||||
|
||||
|
||||
|
|
@ -145,8 +146,8 @@ def _assert_messages_refusal(response: httpx.Response, *secrets: str) -> None:
|
|||
|
||||
|
||||
def _assert_refusal(path: str, response: httpx.Response, *secrets: str) -> None:
|
||||
"""The Messages routes answer the Anthropic envelope, every other route the OpenAI one."""
|
||||
if path == MESSAGES_PATH:
|
||||
"""The Messages and Anthropic pass-through routes answer the Anthropic envelope, every other route the OpenAI one."""
|
||||
if path in ANTHROPIC_ENVELOPE_PATHS:
|
||||
_assert_messages_refusal(response, *secrets)
|
||||
else:
|
||||
_assert_generic_refusal(response, *secrets)
|
||||
|
|
|
|||
|
|
@ -189,10 +189,15 @@ _MESSAGES_ROUTES: Final = (
|
|||
"/claude_code_gateway/v1/messages",
|
||||
"/claude_code_gateway/v1/messages/count_tokens",
|
||||
)
|
||||
_ANTHROPIC_PASS_THROUGH_ROUTES: Final = (
|
||||
"/anthropic/v1/messages",
|
||||
"/anthropic/v1/messages/count_tokens",
|
||||
"/anthropic/v1/models",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("path", _MESSAGES_ROUTES)
|
||||
@pytest.mark.parametrize("path", (*_MESSAGES_ROUTES, *_ANTHROPIC_PASS_THROUGH_ROUTES))
|
||||
async def test_openai_exception_handler_answers_the_anthropic_envelope_on_the_messages_routes(path):
|
||||
"""An auth failure raised before the Messages handler (user_api_key_auth) must reach an
|
||||
Anthropic SDK as {"type": "error", "error": {...}}, not the OpenAI {"error": {...}} body."""
|
||||
|
|
@ -305,14 +310,15 @@ async def test_openai_exception_handler_messages_route_copies_the_call_id_when_o
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_messages_request_with_no_key_answers_the_anthropic_authentication_error(client, monkeypatch):
|
||||
@pytest.mark.parametrize("path", ("/v1/messages", "/anthropic/v1/messages"))
|
||||
async def test_a_messages_request_with_no_key_answers_the_anthropic_authentication_error(client, monkeypatch, path):
|
||||
"""End to end through the real user_api_key_auth: a /v1/messages request with no
|
||||
credential is rejected before the handler, and the SDK must see Anthropic's
|
||||
authentication_error envelope, not OpenAI's {"error": {...}} body."""
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", "test-master-key")
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {})
|
||||
|
||||
response = client.post("/v1/messages", json={"model": "gpt-4", "messages": [{"role": "user", "content": "hi"}]})
|
||||
response = client.post(path, json={"model": "gpt-4", "messages": [{"role": "user", "content": "hi"}]})
|
||||
body = response.json()
|
||||
|
||||
assert response.status_code == 401
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue