From b81d0a37544b3eb009578052a0e0e1d0880b471f Mon Sep 17 00:00:00 2001 From: rupak-eng Date: Mon, 22 Jun 2026 00:59:04 +0530 Subject: [PATCH] fix(anthropic): support Bearer auth for custom api_base endpoints (Fixes #30926) --- .../llms/anthropic/batches/transformation.py | 2 +- litellm/llms/anthropic/chat/handler.py | 1 + litellm/llms/anthropic/common_utils.py | 19 +++- litellm/llms/anthropic/files/handler.py | 2 +- .../llms/anthropic/files/transformation.py | 2 +- .../llms/anthropic/skills/transformation.py | 4 +- .../anthropic/test_anthropic_common_utils.py | 105 ++++++++++++++++++ 7 files changed, 128 insertions(+), 7 deletions(-) diff --git a/litellm/llms/anthropic/batches/transformation.py b/litellm/llms/anthropic/batches/transformation.py index fd67a7fbaf1..0886313d7ce 100644 --- a/litellm/llms/anthropic/batches/transformation.py +++ b/litellm/llms/anthropic/batches/transformation.py @@ -43,7 +43,7 @@ class AnthropicBatchesConfig(BaseBatchesConfig): api_base: Optional[str] = None, ) -> dict: """Validate and prepare environment-specific headers and parameters.""" - auth_header = self.anthropic_model_info.get_auth_header(api_key) + auth_header = self.anthropic_model_info.get_auth_header(api_key, api_base) if auth_header is None: raise ValueError( "Missing Anthropic API Key - A call is being made to anthropic but no key is set either in the environment variables or via params" diff --git a/litellm/llms/anthropic/chat/handler.py b/litellm/llms/anthropic/chat/handler.py index 5d14f3cc4ae..30a16cbfdc4 100644 --- a/litellm/llms/anthropic/chat/handler.py +++ b/litellm/llms/anthropic/chat/handler.py @@ -364,6 +364,7 @@ class AnthropicChatCompletion(BaseLLM): messages=messages, optional_params={**optional_params, "is_vertex_request": is_vertex_request}, litellm_params=litellm_params, + api_base=api_base, ) config = ProviderConfigManager.get_provider_chat_config( diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 5741513903c..c48a4cb064a 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -552,6 +552,7 @@ class AnthropicModelInfo(BaseLLMModelInfo): user_anthropic_beta_headers: Optional[List[str]] = None, code_execution_tool_used: bool = False, container_with_skills_used: bool = False, + api_base: Optional[str] = None, ) -> dict: betas = set() # Anthropic no longer requires the prompt-caching beta header @@ -600,7 +601,12 @@ class AnthropicModelInfo(BaseLLMModelInfo): elif auth_token and not api_key: headers["authorization"] = f"Bearer {auth_token}" elif api_key: - headers["x-api-key"] = api_key + if api_key.startswith("Bearer "): + headers["authorization"] = api_key + elif api_base and "api.anthropic.com" not in api_base and not api_key.startswith("sk-ant-"): + headers["authorization"] = f"Bearer {api_key}" + else: + headers["x-api-key"] = api_key if user_anthropic_beta_headers is not None: betas.update(user_anthropic_beta_headers) @@ -629,6 +635,8 @@ class AnthropicModelInfo(BaseLLMModelInfo): api_key: Optional[str] = None, api_base: Optional[str] = None, ) -> Dict: + if api_base is None and isinstance(litellm_params, dict): + api_base = litellm_params.get("api_base") # Check for Anthropic OAuth token in headers headers, api_key = optionally_handle_anthropic_oauth( headers=headers, api_key=api_key @@ -684,6 +692,7 @@ class AnthropicModelInfo(BaseLLMModelInfo): effort_used=effort_used, code_execution_tool_used=code_execution_tool_used, container_with_skills_used=container_with_skills_used, + api_base=api_base, ) headers = {**headers, **anthropic_headers} @@ -719,7 +728,7 @@ class AnthropicModelInfo(BaseLLMModelInfo): return auth_token or get_secret_str("ANTHROPIC_AUTH_TOKEN") @staticmethod - def get_auth_header(api_key: Optional[str] = None) -> Optional[dict]: + def get_auth_header(api_key: Optional[str] = None, api_base: Optional[str] = None) -> Optional[dict]: """Resolve Anthropic credentials and return the appropriate auth header dict. Checks ANTHROPIC_API_KEY first (-> x-api-key), then @@ -730,6 +739,10 @@ class AnthropicModelInfo(BaseLLMModelInfo): if resolved_key is not None: if is_anthropic_oauth_key(resolved_key): return {"authorization": f"Bearer {resolved_key}"} + if resolved_key.startswith("Bearer "): + return {"authorization": resolved_key} + if api_base and "api.anthropic.com" not in api_base and not resolved_key.startswith("sk-ant-"): + return {"authorization": f"Bearer {resolved_key}"} return {"x-api-key": resolved_key} auth_token = AnthropicModelInfo.get_auth_token() if auth_token is not None: @@ -744,7 +757,7 @@ class AnthropicModelInfo(BaseLLMModelInfo): self, api_key: Optional[str] = None, api_base: Optional[str] = None ) -> List[str]: api_base = AnthropicModelInfo.get_api_base(api_base) - auth_header = AnthropicModelInfo.get_auth_header(api_key) + auth_header = AnthropicModelInfo.get_auth_header(api_key, api_base) if api_base is None or auth_header is None: raise ValueError( "ANTHROPIC_API_BASE/ANTHROPIC_BASE_URL or ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN is not set. Please set the environment variable, to query Anthropic's `/models` endpoint." diff --git a/litellm/llms/anthropic/files/handler.py b/litellm/llms/anthropic/files/handler.py index 56296df94a1..170cb086bb0 100644 --- a/litellm/llms/anthropic/files/handler.py +++ b/litellm/llms/anthropic/files/handler.py @@ -84,7 +84,7 @@ class AnthropicFilesHandler: # Get Anthropic API credentials api_base = self.anthropic_model_info.get_api_base(api_base) - auth_header = self.anthropic_model_info.get_auth_header(api_key) + auth_header = self.anthropic_model_info.get_auth_header(api_key, api_base) if auth_header is None: raise ValueError("Missing Anthropic API Key") diff --git a/litellm/llms/anthropic/files/transformation.py b/litellm/llms/anthropic/files/transformation.py index ea9bf00f505..c125f934875 100644 --- a/litellm/llms/anthropic/files/transformation.py +++ b/litellm/llms/anthropic/files/transformation.py @@ -95,7 +95,7 @@ class AnthropicFilesConfig(BaseFilesConfig): api_key: Optional[str] = None, api_base: Optional[str] = None, ) -> dict: - auth_header = AnthropicModelInfo.get_auth_header(api_key) + auth_header = AnthropicModelInfo.get_auth_header(api_key, api_base) if auth_header is None: raise ValueError( "Anthropic API key is required. Set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN environment variable or pass api_key parameter." diff --git a/litellm/llms/anthropic/skills/transformation.py b/litellm/llms/anthropic/skills/transformation.py index 4ea768b02af..2bfdf7ef4ba 100644 --- a/litellm/llms/anthropic/skills/transformation.py +++ b/litellm/llms/anthropic/skills/transformation.py @@ -38,10 +38,12 @@ class AnthropicSkillsConfig(BaseSkillsAPIConfig): # Get API key from litellm_params if available api_key = None + api_base = None if litellm_params is not None: api_key = litellm_params.api_key + api_base = litellm_params.api_base - auth_header = AnthropicModelInfo.get_auth_header(api_key) + auth_header = AnthropicModelInfo.get_auth_header(api_key, api_base) if auth_header is None: raise ValueError( "ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN is required for Skills API" diff --git a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py index a09e55d4ed7..3b42a3a3b45 100644 --- a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py +++ b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py @@ -159,6 +159,57 @@ class TestGetAnthropicHeaders: assert "authorization" not in headers assert "anthropic-dangerous-direct-browser-access" not in headers + def test_custom_api_base_uses_bearer_header(self): + """Custom api_base and non-standard API key should produce Authorization: Bearer header.""" + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + config = AnthropicModelInfo() + headers = config.get_anthropic_headers( + api_key="my-custom-ollama-token", + computer_tool_used=False, + prompt_caching_set=False, + pdf_used=False, + is_vertex_request=False, + api_base="https://ollama.com/", + ) + + assert headers["authorization"] == "Bearer my-custom-ollama-token" + assert "x-api-key" not in headers + + def test_custom_api_base_uses_bearer_header_already_starts_with_bearer(self): + """If the key already starts with Bearer, use it directly.""" + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + config = AnthropicModelInfo() + headers = config.get_anthropic_headers( + api_key="Bearer my-custom-ollama-token", + computer_tool_used=False, + prompt_caching_set=False, + pdf_used=False, + is_vertex_request=False, + api_base="https://ollama.com/", + ) + + assert headers["authorization"] == "Bearer my-custom-ollama-token" + assert "x-api-key" not in headers + + def test_custom_api_base_uses_x_api_key_when_standard_key(self): + """If the key is standard sk-ant- key, use x-api-key even with custom api_base.""" + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + config = AnthropicModelInfo() + headers = config.get_anthropic_headers( + api_key=FAKE_REGULAR_KEY, + computer_tool_used=False, + prompt_caching_set=False, + pdf_used=False, + is_vertex_request=False, + api_base="https://ollama.com/", + ) + + assert headers["x-api-key"] == FAKE_REGULAR_KEY + assert "authorization" not in headers + def test_oauth_includes_standard_headers(self): """OAuth path should still include standard Anthropic headers.""" from litellm.llms.anthropic.common_utils import AnthropicModelInfo @@ -242,6 +293,46 @@ class TestValidateEnvironmentOAuth: assert updated_headers["x-api-key"] == FAKE_REGULAR_KEY assert "authorization" not in updated_headers + + def test_custom_api_base_via_param(self): + """validate_environment with custom api_base via parameter and custom key should use Bearer.""" + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + config = AnthropicModelInfo() + headers = {} + + updated_headers = config.validate_environment( + headers=headers, + model="claude-sonnet-4-5-20250929", + messages=[{"role": "user", "content": "Hello"}], + optional_params={}, + litellm_params={}, + api_key="custom-api-key", + api_base="https://custom-gateway.com", + ) + + assert updated_headers["authorization"] == "Bearer custom-api-key" + assert "x-api-key" not in updated_headers + + def test_custom_api_base_via_litellm_params(self): + """validate_environment with custom api_base inside litellm_params should use Bearer.""" + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + config = AnthropicModelInfo() + headers = {} + + updated_headers = config.validate_environment( + headers=headers, + model="claude-sonnet-4-5-20250929", + messages=[{"role": "user", "content": "Hello"}], + optional_params={}, + litellm_params={"api_base": "https://custom-gateway.com"}, + api_key="custom-api-key", + api_base=None, + ) + + assert updated_headers["authorization"] == "Bearer custom-api-key" + assert "x-api-key" not in updated_headers assert "anthropic-dangerous-direct-browser-access" not in updated_headers @@ -1004,6 +1095,20 @@ class TestGetAuthHeader: result = AnthropicModelInfo.get_auth_header() assert result == {"authorization": f"Bearer {FAKE_OAUTH_TOKEN}"} + def test_custom_api_base_get_auth_header_uses_bearer(self): + """Non-standard API key and custom api_base should return Authorization: Bearer.""" + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + result = AnthropicModelInfo.get_auth_header(api_key="my-custom-key", api_base="https://custom-gateway.com") + assert result == {"authorization": "Bearer my-custom-key"} + + def test_custom_api_base_get_auth_header_uses_x_api_key_when_standard(self): + """Standard sk-ant- key with custom api_base should still return x-api-key.""" + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + result = AnthropicModelInfo.get_auth_header(api_key=FAKE_REGULAR_KEY, api_base="https://custom-gateway.com") + assert result == {"x-api-key": FAKE_REGULAR_KEY} + class TestGetApiBaseFallbackChain: """Tests for AnthropicModelInfo.get_api_base() fallback to ANTHROPIC_BASE_URL."""