diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index c73376ba498..af1127839aa 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -75,8 +75,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 9a3bc82c6fa..4cd466ab6bb 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -26,7 +26,7 @@ from litellm.constants import ( ALLOWED_VERTEX_AI_PASSTHROUGH_HEADERS, BEDROCK_AGENT_RUNTIME_PASS_THROUGH_ROUTES, ) -from litellm.llms.anthropic.common_utils import AnthropicModelInfo +from litellm.llms.anthropic.common_utils import AnthropicModelInfo, is_anthropic_oauth_key from litellm.llms.vertex_ai.vertex_llm_base import VertexBase from litellm.proxy._types import * from litellm.proxy.auth.handle_jwt import JWTHandler @@ -643,7 +643,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 3b506324ad7..c9dcecd60d0 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 @@ -22,6 +22,7 @@ from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import ( BaseOpenAIPassThroughHandler, RouteChecks, _join_url_paths, + anthropic_proxy_route, azure_proxy_route, bedrock_llm_proxy_route, bedrock_proxy_route, @@ -3010,6 +3011,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 call_args["custom_headers"] == {} + + @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 call_args["custom_headers"] == {} + + def _resolve_route_name(method: str, path: str) -> str | None: from starlette.routing import Match