mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
fix(anthropic): support Bearer auth for custom api_base endpoints (Fixes #30926)
This commit is contained in:
parent
84c1414aef
commit
b81d0a3754
7 changed files with 128 additions and 7 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue