diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index bf2d588dd3a..0acf19ca5ca 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -220,8 +220,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 eaa03b67b40..785417105b0 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -43,7 +43,7 @@ from litellm.constants import ( BEDROCK_AGENT_RUNTIME_PASS_THROUGH_ROUTES, ) from litellm.litellm_core_utils.aws_partition import get_aws_dns_suffix -from litellm.llms.anthropic.common_utils import AnthropicModelInfo +from litellm.llms.anthropic.common_utils import AnthropicModelInfo, is_anthropic_oauth_key from litellm.llms.azure.passthrough.transformation import foreign_azure_deployment from litellm.llms.custom_httpx.http_handler import get_async_httpx_client from litellm.llms.deepgram.common_utils import ( @@ -850,7 +850,10 @@ async def anthropic_proxy_route( is_streaming_request: Final = await is_streaming_request_fn(request) ## CREATE PASS-THROUGH - auth_header: Final = AnthropicModelInfo.get_auth_header(anthropic_api_key or None) + 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) + ) endpoint_func: Final = create_pass_through_route( endpoint=endpoint, target=str(updated_url), 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 227921d6150..a44e8e9736b 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 @@ -3488,6 +3488,103 @@ class TestOpenAIPassthroughRoute: assert result == {"id": "asst_123", "object": "assistant"} +class TestAnthropicProxyRoute: + """Regression test for issue #37344.""" + + @pytest.mark.asyncio + async def test_anthropic_passthrough_omits_server_api_key_when_client_forwards_oauth(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 dict(call_args["custom_headers"]) == {"authorization": "Bearer sk-ant-oat01-canary"} + + @pytest.mark.asyncio + async def test_anthropic_passthrough_uses_server_api_key_without_client_oauth(self): + mock_request = MagicMock(spec=Request) + mock_request.method = "POST" + mock_request.headers = {} + 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"] == {"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 dict(call_args["custom_headers"]) == {"authorization": "bearer sk-ant-oat01-canary"} + + def _resolve_route_name(method: str, path: str) -> str | None: from starlette.routing import Match