fix(anthropic): support Bearer auth for custom api_base endpoints (Fixes #30926)

This commit is contained in:
rupak-eng 2026-06-22 00:59:04 +05:30
parent 84c1414aef
commit b81d0a3754
7 changed files with 128 additions and 7 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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