mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
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:
parent
530c0b2326
commit
61118b0eba
2 changed files with 120 additions and 8 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"] == {}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue