fix: address issue #24436

This commit is contained in:
RoyVivat 2026-03-23 16:46:31 -07:00
parent 147d80985f
commit 80d3d352a9
4 changed files with 125 additions and 114 deletions

View file

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

View file

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

View file

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

View file

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