From 147d80985f3c313b485834e6b141a6b07fb3b49e Mon Sep 17 00:00:00 2001 From: RoyVivat Date: Mon, 23 Mar 2026 16:30:35 -0700 Subject: [PATCH] fix: address issue #24436 --- .../get_provider_specific_headers.py | 19 ++++- .../test_provider_specific_headers.py | 82 +++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/litellm/litellm_core_utils/get_provider_specific_headers.py b/litellm/litellm_core_utils/get_provider_specific_headers.py index 69a7ec72073..3f290284292 100644 --- a/litellm/litellm_core_utils/get_provider_specific_headers.py +++ b/litellm/litellm_core_utils/get_provider_specific_headers.py @@ -1,7 +1,10 @@ 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 @@ -14,6 +17,10 @@ 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 """ @@ -24,6 +31,16 @@ class ProviderSpecificHeaderUtils: provider_list = [p.strip() for p in stored_providers.split(",")] if custom_llm_provider in provider_list: - return provider_specific_header.get("extra_headers", {}) + 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 {} diff --git a/tests/test_litellm/litellm_core_utils/test_provider_specific_headers.py b/tests/test_litellm/litellm_core_utils/test_provider_specific_headers.py index 293d6268eba..19962f3eb9a 100644 --- a/tests/test_litellm/litellm_core_utils/test_provider_specific_headers.py +++ b/tests/test_litellm/litellm_core_utils/test_provider_specific_headers.py @@ -112,3 +112,85 @@ class TestProviderSpecificHeaderUtils: provider_specific_header, None ) 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