mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-06 08:16:43 +00:00
fix: address issue #24436
This commit is contained in:
parent
147d80985f
commit
80d3d352a9
4 changed files with 125 additions and 114 deletions
|
|
@ -1,10 +1,7 @@
|
|||
from typing import Dict, Optional
|
||||
|
||||
from litellm.llms.anthropic.common_utils import is_anthropic_oauth_key
|
||||
from litellm.types.utils import ProviderSpecificHeader
|
||||
|
||||
_ANTHROPIC_PROVIDER = "anthropic"
|
||||
|
||||
|
||||
class ProviderSpecificHeaderUtils:
|
||||
@staticmethod
|
||||
|
|
@ -17,10 +14,6 @@ class ProviderSpecificHeaderUtils:
|
|||
|
||||
Supports comma-separated provider lists for headers that work across multiple providers.
|
||||
|
||||
Anthropic OAuth tokens (sk-ant-oat*) in the Authorization header are stripped for
|
||||
non-Anthropic providers to prevent them from overriding provider-specific auth
|
||||
(e.g. AWS SigV4 for Bedrock, service account credentials for Vertex AI).
|
||||
|
||||
Returns:
|
||||
Dict: The provider specific headers for the given custom llm provider
|
||||
"""
|
||||
|
|
@ -31,16 +24,6 @@ class ProviderSpecificHeaderUtils:
|
|||
provider_list = [p.strip() for p in stored_providers.split(",")]
|
||||
|
||||
if custom_llm_provider in provider_list:
|
||||
headers = provider_specific_header.get("extra_headers", {})
|
||||
# Anthropic OAuth tokens must not be forwarded to non-Anthropic providers.
|
||||
# Forwarding them would overwrite provider-specific auth headers
|
||||
# (e.g. Bedrock's SigV4 Authorization, Vertex AI service-account auth).
|
||||
if custom_llm_provider != _ANTHROPIC_PROVIDER:
|
||||
headers = {
|
||||
k: v
|
||||
for k, v in headers.items()
|
||||
if not (k.lower() == "authorization" and is_anthropic_oauth_key(v))
|
||||
}
|
||||
return headers
|
||||
return provider_specific_header.get("extra_headers", {})
|
||||
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -1928,29 +1928,42 @@ def add_provider_specific_headers_to_request(
|
|||
):
|
||||
from litellm.llms.anthropic.common_utils import is_anthropic_oauth_key
|
||||
|
||||
anthropic_headers = {}
|
||||
# boolean to indicate if a header was added
|
||||
added_header = False
|
||||
# Headers that work identically across Anthropic, Bedrock, and Vertex AI
|
||||
multi_provider_headers: dict = {}
|
||||
for header in ANTHROPIC_API_HEADERS:
|
||||
if header in headers:
|
||||
header_value = headers[header]
|
||||
anthropic_headers[header] = header_value
|
||||
added_header = True
|
||||
multi_provider_headers[header] = headers[header]
|
||||
|
||||
# Check for Authorization header with Anthropic OAuth token (sk-ant-oat*)
|
||||
# This needs to be handled via provider-specific headers to ensure it only
|
||||
# goes to Anthropic-compatible providers, not all providers in the router
|
||||
# Detect an Anthropic OAuth token (sk-ant-oat*) in the Authorization header.
|
||||
# OAuth tokens must be scoped to the Anthropic provider only — forwarding them
|
||||
# to Bedrock or Vertex AI would overwrite those providers' own auth headers
|
||||
# (AWS SigV4 Authorization for Bedrock, service-account credentials for Vertex AI),
|
||||
# causing 403 errors. See https://github.com/BerriAI/litellm/issues/24436
|
||||
oauth_header_key: str = ""
|
||||
oauth_header_value: str = ""
|
||||
for header, value in headers.items():
|
||||
if header.lower() == "authorization" and is_anthropic_oauth_key(value):
|
||||
anthropic_headers[header] = value
|
||||
added_header = True
|
||||
oauth_header_key = header
|
||||
oauth_header_value = value
|
||||
break
|
||||
if added_header is True:
|
||||
# Anthropic headers work across multiple providers
|
||||
# Store as comma-separated list so retrieval can match any of them
|
||||
|
||||
if oauth_header_key:
|
||||
# Scope ALL Anthropic headers (including the OAuth token) to the Anthropic
|
||||
# provider only. Users sending an OAuth token are targeting Anthropic directly;
|
||||
# Bedrock/Vertex have their own beta-header plumbing.
|
||||
data["provider_specific_header"] = ProviderSpecificHeader(
|
||||
custom_llm_provider=LlmProviders.ANTHROPIC.value,
|
||||
extra_headers={
|
||||
**multi_provider_headers,
|
||||
oauth_header_key: oauth_header_value,
|
||||
},
|
||||
)
|
||||
elif multi_provider_headers:
|
||||
# Regular Anthropic API headers (e.g. anthropic-beta, anthropic-version)
|
||||
# work across all three Anthropic-format providers.
|
||||
data["provider_specific_header"] = ProviderSpecificHeader(
|
||||
custom_llm_provider=f"{LlmProviders.ANTHROPIC.value},{LlmProviders.BEDROCK.value},{LlmProviders.VERTEX_AI.value}",
|
||||
extra_headers=anthropic_headers,
|
||||
extra_headers=multi_provider_headers,
|
||||
)
|
||||
|
||||
return
|
||||
|
|
|
|||
|
|
@ -113,84 +113,3 @@ class TestProviderSpecificHeaderUtils:
|
|||
)
|
||||
assert result == {}
|
||||
|
||||
def test_anthropic_oauth_token_not_forwarded_to_bedrock(self):
|
||||
"""
|
||||
Regression test for https://github.com/BerriAI/litellm/issues/24436.
|
||||
|
||||
When Claude Code sets an Anthropic OAuth token (sk-ant-oat*) in Authorization,
|
||||
it must NOT reach Bedrock — forwarding it overwrites the AWS SigV4 Authorization
|
||||
header and causes a 403 from Bedrock.
|
||||
"""
|
||||
oauth_token = "Bearer sk-ant-oat01-abc123xyz"
|
||||
provider_specific_header: ProviderSpecificHeader = {
|
||||
"custom_llm_provider": "anthropic,bedrock,vertex_ai",
|
||||
"extra_headers": {
|
||||
"Authorization": oauth_token,
|
||||
"anthropic-beta": "some-beta-feature",
|
||||
},
|
||||
}
|
||||
|
||||
# Anthropic should receive the OAuth token
|
||||
result = ProviderSpecificHeaderUtils.get_provider_specific_headers(
|
||||
provider_specific_header, "anthropic"
|
||||
)
|
||||
assert result["Authorization"] == oauth_token
|
||||
assert result["anthropic-beta"] == "some-beta-feature"
|
||||
|
||||
# Bedrock must NOT receive the OAuth token (it would overwrite SigV4 auth)
|
||||
result = ProviderSpecificHeaderUtils.get_provider_specific_headers(
|
||||
provider_specific_header, "bedrock"
|
||||
)
|
||||
assert "Authorization" not in result
|
||||
assert result.get("anthropic-beta") == "some-beta-feature"
|
||||
|
||||
# Vertex AI must NOT receive the OAuth token
|
||||
result = ProviderSpecificHeaderUtils.get_provider_specific_headers(
|
||||
provider_specific_header, "vertex_ai"
|
||||
)
|
||||
assert "Authorization" not in result
|
||||
assert result.get("anthropic-beta") == "some-beta-feature"
|
||||
|
||||
def test_anthropic_oauth_token_not_forwarded_to_bedrock_converse(self):
|
||||
"""Anthropic OAuth token must not be forwarded to bedrock_converse either."""
|
||||
oauth_token = "Bearer sk-ant-oat02-xyz789"
|
||||
provider_specific_header: ProviderSpecificHeader = {
|
||||
"custom_llm_provider": "anthropic,bedrock,bedrock_converse,vertex_ai",
|
||||
"extra_headers": {"Authorization": oauth_token},
|
||||
}
|
||||
|
||||
result = ProviderSpecificHeaderUtils.get_provider_specific_headers(
|
||||
provider_specific_header, "bedrock_converse"
|
||||
)
|
||||
assert result == {}
|
||||
|
||||
def test_non_oauth_authorization_still_forwarded_to_non_anthropic(self):
|
||||
"""A plain Bearer token (not Anthropic OAuth) should still pass through."""
|
||||
plain_token = "Bearer some-regular-api-key"
|
||||
provider_specific_header: ProviderSpecificHeader = {
|
||||
"custom_llm_provider": "openai,azure",
|
||||
"extra_headers": {"Authorization": plain_token},
|
||||
}
|
||||
|
||||
result = ProviderSpecificHeaderUtils.get_provider_specific_headers(
|
||||
provider_specific_header, "openai"
|
||||
)
|
||||
assert result["Authorization"] == plain_token
|
||||
|
||||
def test_anthropic_oauth_token_raw_format_not_forwarded_to_bedrock(self):
|
||||
"""Anthropic OAuth token in raw format (without 'Bearer ' prefix) is also stripped."""
|
||||
raw_oauth_token = "sk-ant-oat01-abc123"
|
||||
provider_specific_header: ProviderSpecificHeader = {
|
||||
"custom_llm_provider": "anthropic,bedrock,vertex_ai",
|
||||
"extra_headers": {"Authorization": raw_oauth_token},
|
||||
}
|
||||
|
||||
result = ProviderSpecificHeaderUtils.get_provider_specific_headers(
|
||||
provider_specific_header, "bedrock"
|
||||
)
|
||||
assert "Authorization" not in result
|
||||
|
||||
result = ProviderSpecificHeaderUtils.get_provider_specific_headers(
|
||||
provider_specific_header, "anthropic"
|
||||
)
|
||||
assert result["Authorization"] == raw_oauth_token
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from litellm.proxy.litellm_pre_call_utils import (
|
|||
_update_model_if_key_alias_exists,
|
||||
add_guardrails_from_policy_engine,
|
||||
add_litellm_data_to_request,
|
||||
add_provider_specific_headers_to_request,
|
||||
check_if_token_is_service_account,
|
||||
)
|
||||
|
||||
|
|
@ -1817,3 +1818,98 @@ async def test_bearer_token_not_in_debug_logs():
|
|||
f"Bearer token leaked in debug logs. "
|
||||
f"Found token in log output:\n{log_output[:500]}"
|
||||
)
|
||||
|
||||
|
||||
class TestAddProviderSpecificHeadersOAuthScoping:
|
||||
"""
|
||||
Regression tests for https://github.com/BerriAI/litellm/issues/24436.
|
||||
|
||||
When Claude Code sends an Anthropic OAuth token (sk-ant-oat*) via the
|
||||
Authorization header, that token must NOT be forwarded to non-Anthropic
|
||||
providers (Bedrock, Vertex AI). Doing so overwrites those providers'
|
||||
own auth headers (AWS SigV4, service-account credentials) and causes 403s.
|
||||
"""
|
||||
|
||||
def test_oauth_token_scoped_to_anthropic_only(self):
|
||||
"""provider_specific_header must list only 'anthropic' when an OAuth token is present."""
|
||||
data: dict = {}
|
||||
headers = {"Authorization": "Bearer sk-ant-oat01-abc123xyz"}
|
||||
|
||||
add_provider_specific_headers_to_request(data=data, headers=headers)
|
||||
|
||||
psh = data.get("provider_specific_header")
|
||||
assert psh is not None
|
||||
assert psh["custom_llm_provider"] == "anthropic"
|
||||
assert psh["extra_headers"]["Authorization"] == "Bearer sk-ant-oat01-abc123xyz"
|
||||
|
||||
def test_oauth_token_with_anthropic_beta_scoped_to_anthropic_only(self):
|
||||
"""
|
||||
When both an OAuth token and anthropic-beta are present, both are scoped to
|
||||
anthropic only. Verify this end-to-end: anthropic receives both headers, while
|
||||
bedrock and vertex_ai receive neither (not just "no Authorization" — they receive
|
||||
nothing at all, because a user with an OAuth token is targeting Anthropic directly).
|
||||
"""
|
||||
from litellm.litellm_core_utils.get_provider_specific_headers import (
|
||||
ProviderSpecificHeaderUtils,
|
||||
)
|
||||
|
||||
data: dict = {}
|
||||
headers = {
|
||||
"Authorization": "Bearer sk-ant-oat02-xyz789",
|
||||
"anthropic-beta": "some-beta-feature",
|
||||
}
|
||||
|
||||
add_provider_specific_headers_to_request(data=data, headers=headers)
|
||||
|
||||
psh = data.get("provider_specific_header")
|
||||
assert psh is not None
|
||||
assert psh["custom_llm_provider"] == "anthropic"
|
||||
assert psh["extra_headers"]["Authorization"] == "Bearer sk-ant-oat02-xyz789"
|
||||
assert psh["extra_headers"]["anthropic-beta"] == "some-beta-feature"
|
||||
|
||||
# Anthropic receives both headers
|
||||
anthropic_headers = ProviderSpecificHeaderUtils.get_provider_specific_headers(
|
||||
psh, "anthropic"
|
||||
)
|
||||
assert anthropic_headers["Authorization"] == "Bearer sk-ant-oat02-xyz789"
|
||||
assert anthropic_headers["anthropic-beta"] == "some-beta-feature"
|
||||
|
||||
# Bedrock and Vertex AI receive nothing — the OAuth token is not surgically
|
||||
# stripped; instead the whole header set is scoped to anthropic only
|
||||
assert ProviderSpecificHeaderUtils.get_provider_specific_headers(psh, "bedrock") == {}
|
||||
assert ProviderSpecificHeaderUtils.get_provider_specific_headers(psh, "vertex_ai") == {}
|
||||
assert ProviderSpecificHeaderUtils.get_provider_specific_headers(psh, "bedrock_converse") == {}
|
||||
|
||||
def test_non_oauth_anthropic_headers_forwarded_to_multi_providers(self):
|
||||
"""Regular Anthropic API headers without an OAuth token go to anthropic,bedrock,vertex_ai."""
|
||||
data: dict = {}
|
||||
headers = {"anthropic-beta": "some-beta-feature"}
|
||||
|
||||
add_provider_specific_headers_to_request(data=data, headers=headers)
|
||||
|
||||
psh = data.get("provider_specific_header")
|
||||
assert psh is not None
|
||||
assert "bedrock" in psh["custom_llm_provider"]
|
||||
assert "vertex_ai" in psh["custom_llm_provider"]
|
||||
assert psh["extra_headers"]["anthropic-beta"] == "some-beta-feature"
|
||||
assert "Authorization" not in psh["extra_headers"]
|
||||
|
||||
def test_no_provider_specific_header_when_no_relevant_headers(self):
|
||||
"""No provider_specific_header is set when there are no relevant headers."""
|
||||
data: dict = {}
|
||||
headers = {"x-some-other-header": "value"}
|
||||
|
||||
add_provider_specific_headers_to_request(data=data, headers=headers)
|
||||
|
||||
assert "provider_specific_header" not in data
|
||||
|
||||
def test_raw_oauth_token_scoped_to_anthropic_only(self):
|
||||
"""OAuth token without 'Bearer ' prefix is also scoped to anthropic only."""
|
||||
data: dict = {}
|
||||
headers = {"Authorization": "sk-ant-oat01-rawtoken"}
|
||||
|
||||
add_provider_specific_headers_to_request(data=data, headers=headers)
|
||||
|
||||
psh = data.get("provider_specific_header")
|
||||
assert psh is not None
|
||||
assert psh["custom_llm_provider"] == "anthropic"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue