diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 1cdbd60f943..ccbb471fc1c 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -54,8 +54,10 @@ def is_anthropic_oauth_key(value: str | None) -> bool: """Check if a value contains an Anthropic OAuth token (sk-ant-oat*).""" if value is None: return False - # Handle both raw token and "Bearer " format - value = value.removeprefix("Bearer ") + # Handle both raw token and "Bearer " format, case-insensitive scheme + scheme, _, token = value.partition(" ") + if scheme.lower() == "bearer": + value = token return value.startswith(ANTHROPIC_OAUTH_TOKEN_PREFIX) diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index 0c8515e2bba..286b15e3b77 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -601,9 +601,6 @@ async def anthropic_proxy_route( is_streaming_request: Final = await is_streaming_request_fn(request) ## CREATE PASS-THROUGH - # A client-forwarded Anthropic OAuth token (e.g. Claude Code Max subscription auth) - # must not be sent alongside a server-configured x-api-key: Anthropic authenticates - # off x-api-key when both are present, silently discarding the client's own identity. client_forwards_own_oauth: Final = is_anthropic_oauth_key(request.headers.get("authorization")) auth_header: Final = ( None if client_forwards_own_oauth else AnthropicModelInfo.get_auth_header(anthropic_api_key or None) 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 21815a1476b..95f1345192a 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 @@ -2935,11 +2935,7 @@ class TestOpenAIPassthroughRoute: class TestAnthropicProxyRoute: - """Regression (issue #37344): a client forwarding its own Anthropic OAuth token - (e.g. a Claude Code Max subscription) must not also get the server-configured - ANTHROPIC_API_KEY injected as x-api-key. Anthropic authenticates off x-api-key - when both are present, silently discarding the client's forwarded identity. - """ + """Regression test for issue #37344.""" @pytest.mark.asyncio async def test_anthropic_passthrough_omits_server_api_key_when_client_forwards_oauth(self): @@ -3003,6 +2999,37 @@ class TestAnthropicProxyRoute: call_args = mock_create_route.call_args[1] assert call_args["custom_headers"] == {"x-api-key": "sk-ant-server-configured-key"} + @pytest.mark.asyncio + async def test_anthropic_passthrough_omits_server_api_key_for_lowercase_bearer_scheme(self): + mock_request = MagicMock(spec=Request) + mock_request.method = "POST" + mock_request.headers = {"authorization": "bearer sk-ant-oat01-canary"} + mock_request.query_params = {} + mock_response = MagicMock(spec=Response) + mock_user_api_key_dict = MagicMock() + + with ( + patch( + "litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.passthrough_endpoint_router.get_credentials", + return_value="sk-ant-server-configured-key", + ), + patch( + "litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.create_pass_through_route" + ) as mock_create_route, + ): + mock_endpoint_func = AsyncMock(return_value={"id": "msg_123"}) + mock_create_route.return_value = mock_endpoint_func + + await anthropic_proxy_route( + endpoint="v1/messages", + request=mock_request, + fastapi_response=mock_response, + user_api_key_dict=mock_user_api_key_dict, + ) + + call_args = mock_create_route.call_args[1] + assert call_args["custom_headers"] == {} + def _resolve_route_name(method: str, path: str) -> str | None: from starlette.routing import Match