fix(proxy): explain missing Anthropic credentials on passthrough 401

When no Anthropic credential is configured on the proxy, the /anthropic
passthrough forwards the caller's own Authorization header (their LiteLLM
virtual key) to api.anthropic.com, which rejects it with the opaque error
'Invalid bearer token'. Enrich that 401 with a message explaining that no
Anthropic credentials were found on the proxy and how to configure them,
mirroring the vertex passthrough's missing-credentials handling. Requests
where the proxy has credentials, non-401 upstream failures, and callers
that bring their own valid Anthropic credentials are unaffected.
This commit is contained in:
mateo-berri 2026-06-11 19:59:28 +00:00
parent 530c0b2326
commit 61118b0eba
No known key found for this signature in database
2 changed files with 120 additions and 8 deletions

View file

@ -646,13 +646,22 @@ async def anthropic_proxy_route(
_forward_headers=True,
is_streaming_request=is_streaming_request,
) # dynamically construct pass-through endpoint based on incoming path
received_value = await endpoint_func(
request,
fastapi_response,
user_api_key_dict,
)
return received_value
try:
return await endpoint_func(
request,
fastapi_response,
user_api_key_dict,
)
except ProxyException as e:
if auth_header is None and e.code == "401":
e.message = (
"No Anthropic credentials found on the proxy. Set the ANTHROPIC_API_KEY "
"environment variable (or ANTHROPIC_AUTH_TOKEN for OAuth) on the proxy, "
"or configure Anthropic pass-through credentials. The incoming request "
"headers were forwarded to Anthropic as-is and the request failed with "
f"error: {e.message}"
)
raise e
# Bedrock endpoint actions - consolidated list used for model extraction and streaming detection

View file

@ -18,6 +18,7 @@ import litellm
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
BaseOpenAIPassThroughHandler,
RouteChecks,
anthropic_proxy_route,
bedrock_llm_proxy_route,
create_pass_through_route,
cursor_proxy_route,
@ -30,7 +31,7 @@ from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
vertex_proxy_route,
vllm_proxy_route,
)
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy._types import ProxyException, UserAPIKeyAuth
from litellm.types.passthrough_endpoints.vertex_ai import VertexPassThroughCredentials
@ -2970,3 +2971,105 @@ class TestCursorProxyRoute:
assert call_args["target"] == "https://api.cursor.com/v0/agents"
assert result["id"] == "bc_abc123"
assert result["status"] == "CREATING"
class TestAnthropicProxyRoute:
ANTHROPIC_401_BODY = '{"type":"error","error":{"type":"authentication_error","message":"Invalid bearer token"}}'
def _setup_mocks(self, monkeypatch, api_key, upstream_error):
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
monkeypatch.delenv("ANTHROPIC_AUTH_TOKEN", raising=False)
mock_router = MagicMock()
mock_router.get_credentials.return_value = api_key
monkeypatch.setattr(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.passthrough_endpoint_router",
mock_router,
)
monkeypatch.setattr(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.is_streaming_request_fn",
AsyncMock(return_value=False),
)
mock_create_route = MagicMock(
return_value=AsyncMock(side_effect=upstream_error)
)
monkeypatch.setattr(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.create_pass_through_route",
mock_create_route,
)
return mock_create_route
async def _call_route(self):
return await anthropic_proxy_route(
endpoint="v1/messages",
request=MagicMock(spec=Request),
fastapi_response=MagicMock(spec=Response),
user_api_key_dict=MagicMock(spec=UserAPIKeyAuth),
)
@pytest.mark.asyncio
async def test_upstream_401_without_proxy_credentials_explains_missing_key(
self, monkeypatch
):
upstream_error = ProxyException(
message=self.ANTHROPIC_401_BODY, type="None", param="None", code=401
)
self._setup_mocks(monkeypatch, api_key=None, upstream_error=upstream_error)
with pytest.raises(ProxyException) as exc_info:
await self._call_route()
assert exc_info.value.code == "401"
assert "No Anthropic credentials found on the proxy" in exc_info.value.message
assert "ANTHROPIC_API_KEY" in exc_info.value.message
assert self.ANTHROPIC_401_BODY in exc_info.value.message
@pytest.mark.asyncio
async def test_upstream_401_with_proxy_credentials_is_not_rewritten(
self, monkeypatch
):
upstream_error = ProxyException(
message=self.ANTHROPIC_401_BODY, type="None", param="None", code=401
)
mock_create_route = self._setup_mocks(
monkeypatch, api_key="sk-ant-proxy-key", upstream_error=upstream_error
)
with pytest.raises(ProxyException) as exc_info:
await self._call_route()
assert exc_info.value.message == self.ANTHROPIC_401_BODY
assert mock_create_route.call_args.kwargs["custom_headers"] == {
"x-api-key": "sk-ant-proxy-key"
}
@pytest.mark.asyncio
async def test_upstream_non_401_without_proxy_credentials_is_not_rewritten(
self, monkeypatch
):
upstream_error = ProxyException(
message="invalid request", type="None", param="None", code=400
)
self._setup_mocks(monkeypatch, api_key=None, upstream_error=upstream_error)
with pytest.raises(ProxyException) as exc_info:
await self._call_route()
assert exc_info.value.message == "invalid request"
@pytest.mark.asyncio
async def test_successful_response_is_returned_unchanged(self, monkeypatch):
self._setup_mocks(monkeypatch, api_key=None, upstream_error=None)
mock_create_route = MagicMock(
return_value=AsyncMock(return_value={"id": "msg_123"})
)
monkeypatch.setattr(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.create_pass_through_route",
mock_create_route,
)
result = await self._call_route()
assert result == {"id": "msg_123"}
assert mock_create_route.call_args.kwargs["custom_headers"] == {}