This commit is contained in:
Jialong(Bruce) Li 2026-09-29 01:16:23 +03:00 • committed by GitHub
commit d1277ccafc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 106 additions and 4 deletions

View file

@ -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 <token>" format
value = value.removeprefix("Bearer ")
# Handle both raw token and "Bearer <token>" format, case-insensitive scheme
scheme, _, token = value.partition(" ")
if scheme.lower() == "bearer":
value = token
return value.startswith(ANTHROPIC_OAUTH_TOKEN_PREFIX)

View file

@ -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),

View file

@ -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