From 61118b0ebae76d0213ebc50fc7585c9bb15f328c Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:59:28 +0000 Subject: [PATCH] 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. --- .../llm_passthrough_endpoints.py | 23 ++-- .../test_llm_pass_through_endpoints.py | 105 +++++++++++++++++- 2 files changed, 120 insertions(+), 8 deletions(-) diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index c7db818a07e..1fe7deb4315 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -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 diff --git a/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py b/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py index 4eab1a4bf61..1a249019ab7 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py @@ -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"] == {}