From f415b72bcfa795c3673de5d13b68658fc9a3482e Mon Sep 17 00:00:00 2001 From: Devin Petersohn Date: Wed, 18 Mar 2026 15:20:45 -0700 Subject: [PATCH] feat(anthropic): support ANTHROPIC_AUTH_TOKEN and ANTHROPIC_BASE_URL env vars Co-Authored-By: Claude Signed-off-by: Devin Petersohn --- litellm/batches/main.py | 1 + litellm/constants.py | 1 + .../llms/anthropic/batches/transformation.py | 7 +- litellm/llms/anthropic/common_utils.py | 54 ++- .../messages/transformation.py | 10 +- litellm/llms/anthropic/files/handler.py | 10 +- .../llms/anthropic/files/transformation.py | 8 +- .../llms/anthropic/skills/transformation.py | 13 +- .../llm_passthrough_endpoints.py | 4 +- litellm/utils.py | 8 +- .../anthropic/test_anthropic_common_utils.py | 372 +++++++++++++++++- 11 files changed, 441 insertions(+), 47 deletions(-) diff --git a/litellm/batches/main.py b/litellm/batches/main.py index e176dc4292..17d73aae6a 100644 --- a/litellm/batches/main.py +++ b/litellm/batches/main.py @@ -524,6 +524,7 @@ def _handle_retrieve_batch_providers_without_provider_config( optional_params.api_base or litellm.api_base or get_secret_str("ANTHROPIC_API_BASE") + or get_secret_str("ANTHROPIC_BASE_URL") ) api_key = ( optional_params.api_key diff --git a/litellm/constants.py b/litellm/constants.py index 89c59ee932..c0dd115210 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -1444,6 +1444,7 @@ SENTRY_DENYLIST = [ "credential", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", + "ANTHROPIC_AUTH_TOKEN", "AZURE_API_KEY", "COHERE_API_KEY", "REPLICATE_API_KEY", diff --git a/litellm/llms/anthropic/batches/transformation.py b/litellm/llms/anthropic/batches/transformation.py index 699f133f0f..98c0588a09 100644 --- a/litellm/llms/anthropic/batches/transformation.py +++ b/litellm/llms/anthropic/batches/transformation.py @@ -42,9 +42,8 @@ class AnthropicBatchesConfig(BaseBatchesConfig): api_base: Optional[str] = None, ) -> dict: """Validate and prepare environment-specific headers and parameters.""" - # Resolve api_key from environment if not provided - api_key = api_key or self.anthropic_model_info.get_api_key() - if api_key is None: + auth_header = self.anthropic_model_info.get_auth_header(api_key) + 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" ) @@ -52,8 +51,8 @@ class AnthropicBatchesConfig(BaseBatchesConfig): "accept": "application/json", "anthropic-version": "2023-06-01", "content-type": "application/json", - "x-api-key": api_key, } + _headers.update(auth_header) # Add beta header for message batches if "anthropic-beta" not in headers: headers["anthropic-beta"] = "message-batches-2024-09-24" diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index ac35246787..7199e21014 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -359,9 +359,7 @@ class AnthropicModelInfo(BaseLLMModelInfo): Returns: List of beta header strings """ - from litellm.types.llms.anthropic import ( - ANTHROPIC_EFFORT_BETA_HEADER, - ) + from litellm.types.llms.anthropic import ANTHROPIC_EFFORT_BETA_HEADER betas = [] @@ -390,7 +388,8 @@ class AnthropicModelInfo(BaseLLMModelInfo): def get_anthropic_headers( self, - api_key: str, + api_key: Optional[str] = None, + auth_token: Optional[str] = None, anthropic_version: Optional[str] = None, computer_tool_used: Optional[str] = None, prompt_caching_set: bool = False, @@ -451,6 +450,8 @@ class AnthropicModelInfo(BaseLLMModelInfo): headers["authorization"] = f"Bearer {api_key}" headers["anthropic-dangerous-direct-browser-access"] = "true" betas.add(ANTHROPIC_OAUTH_BETA_HEADER) + elif auth_token and not api_key: + headers["authorization"] = f"Bearer {auth_token}" else: headers["x-api-key"] = api_key @@ -485,9 +486,13 @@ class AnthropicModelInfo(BaseLLMModelInfo): headers, api_key = optionally_handle_anthropic_oauth( headers=headers, api_key=api_key ) + # Resolve auth_token from ANTHROPIC_AUTH_TOKEN if api_key is not set + auth_token: Optional[str] = None if api_key is None: + auth_token = AnthropicModelInfo.get_auth_token() + if api_key is None and auth_token is None: raise litellm.AuthenticationError( - message="Missing Anthropic API Key - A call is being made to anthropic but no key is set either in the environment variables or via params. Please set `ANTHROPIC_API_KEY` in your environment vars", + message="Missing Anthropic API Key - A call is being made to anthropic but no key is set either in the environment variables or via params. Please set `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN` in your environment vars", llm_provider="anthropic", model=model, ) @@ -519,6 +524,7 @@ class AnthropicModelInfo(BaseLLMModelInfo): prompt_caching_set=prompt_caching_set, pdf_used=pdf_used, api_key=api_key, + auth_token=auth_token, file_id_used=file_id_used, web_search_tool_used=web_search_tool_used, is_vertex_request=optional_params.get("is_vertex_request", False), @@ -543,6 +549,7 @@ class AnthropicModelInfo(BaseLLMModelInfo): return ( api_base or get_secret_str("ANTHROPIC_API_BASE") + or get_secret_str("ANTHROPIC_BASE_URL") or "https://api.anthropic.com" ) @@ -552,6 +559,33 @@ class AnthropicModelInfo(BaseLLMModelInfo): return api_key or get_secret_str("ANTHROPIC_API_KEY") + @staticmethod + def get_auth_token(auth_token: Optional[str] = None) -> Optional[str]: + """Get auth token from ANTHROPIC_AUTH_TOKEN env var. + + Unlike api_key (which uses X-Api-Key header), auth_token uses + Authorization: Bearer header, matching the official Anthropic SDK behavior. + """ + from litellm.secret_managers.main import get_secret_str + + return auth_token or get_secret_str("ANTHROPIC_AUTH_TOKEN") + + @staticmethod + def get_auth_header(api_key: Optional[str] = None) -> Optional[dict]: + """Resolve Anthropic credentials and return the appropriate auth header dict. + + Checks ANTHROPIC_API_KEY first (-> x-api-key), then + ANTHROPIC_AUTH_TOKEN (-> Authorization: Bearer). + Returns None if neither is available. + """ + resolved_key = AnthropicModelInfo.get_api_key(api_key) + if resolved_key is not None: + return {"x-api-key": resolved_key} + auth_token = AnthropicModelInfo.get_auth_token() + if auth_token is not None: + return {"authorization": f"Bearer {auth_token}"} + return None + @staticmethod def get_base_model(model: Optional[str] = None) -> Optional[str]: return model.replace("anthropic/", "") if model else None @@ -560,14 +594,16 @@ class AnthropicModelInfo(BaseLLMModelInfo): self, api_key: Optional[str] = None, api_base: Optional[str] = None ) -> List[str]: api_base = AnthropicModelInfo.get_api_base(api_base) - api_key = AnthropicModelInfo.get_api_key(api_key) - if api_base is None or api_key is None: + auth_header = AnthropicModelInfo.get_auth_header(api_key) + if api_base is None or auth_header is None: raise ValueError( - "ANTHROPIC_API_BASE or ANTHROPIC_API_KEY is not set. Please set the environment variable, to query Anthropic's `/models` endpoint." + "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." ) + headers = {"anthropic-version": "2023-06-01"} + headers.update(auth_header) response = litellm.module_level_client.get( url=f"{api_base}/v1/models", - headers={"x-api-key": api_key, "anthropic-version": "2023-06-01"}, + headers=headers, ) try: diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py index e9ceea4822..8e7c850627 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py @@ -142,17 +142,15 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): api_key: Optional[str] = None, api_base: Optional[str] = None, ) -> Tuple[dict, Optional[str]]: - import os - # Check for Anthropic OAuth token in Authorization header headers, api_key = optionally_handle_anthropic_oauth( headers=headers, api_key=api_key ) - if api_key is None: - api_key = os.getenv("ANTHROPIC_API_KEY") - if "x-api-key" not in headers and "authorization" not in headers and api_key: - headers["x-api-key"] = api_key + if "x-api-key" not in headers and "authorization" not in headers: + auth_header = AnthropicModelInfo.get_auth_header(api_key) + if auth_header is not None: + headers.update(auth_header) if "anthropic-version" not in headers: headers["anthropic-version"] = DEFAULT_ANTHROPIC_API_VERSION if "content-type" not in headers: diff --git a/litellm/llms/anthropic/files/handler.py b/litellm/llms/anthropic/files/handler.py index 77cc8c2731..c56799f30c 100644 --- a/litellm/llms/anthropic/files/handler.py +++ b/litellm/llms/anthropic/files/handler.py @@ -8,10 +8,8 @@ import httpx import litellm from litellm._logging import verbose_logger from litellm._uuid import uuid -from litellm.llms.custom_httpx.http_handler import ( - get_async_httpx_client, -) from litellm.litellm_core_utils.litellm_logging import Logging +from litellm.llms.custom_httpx.http_handler import get_async_httpx_client from litellm.types.llms.openai import ( FileContentRequest, HttpxBinaryResponseContent, @@ -85,9 +83,9 @@ class AnthropicFilesHandler: # Get Anthropic API credentials api_base = self.anthropic_model_info.get_api_base(api_base) - api_key = api_key or self.anthropic_model_info.get_api_key() + auth_header = self.anthropic_model_info.get_auth_header(api_key) - if not api_key: + if auth_header is None: raise ValueError("Missing Anthropic API Key") # Construct the Anthropic batch results URL @@ -97,8 +95,8 @@ class AnthropicFilesHandler: headers = { "accept": "application/json", "anthropic-version": "2023-06-01", - "x-api-key": api_key, } + headers.update(auth_header) # Make the request to Anthropic async_client = get_async_httpx_client(llm_provider=LlmProviders.ANTHROPIC) diff --git a/litellm/llms/anthropic/files/transformation.py b/litellm/llms/anthropic/files/transformation.py index 98a548a136..0545cefb07 100644 --- a/litellm/llms/anthropic/files/transformation.py +++ b/litellm/llms/anthropic/files/transformation.py @@ -94,14 +94,14 @@ class AnthropicFilesConfig(BaseFilesConfig): api_key: Optional[str] = None, api_base: Optional[str] = None, ) -> dict: - api_key = AnthropicModelInfo.get_api_key(api_key) - if not api_key: + auth_header = AnthropicModelInfo.get_auth_header(api_key) + if auth_header is None: raise ValueError( - "Anthropic API key is required. Set ANTHROPIC_API_KEY environment variable or pass api_key parameter." + "Anthropic API key is required. Set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN environment variable or pass api_key parameter." ) headers.update( { - "x-api-key": api_key, + **auth_header, "anthropic-version": "2023-06-01", "anthropic-beta": ANTHROPIC_FILES_BETA_HEADER, } diff --git a/litellm/llms/anthropic/skills/transformation.py b/litellm/llms/anthropic/skills/transformation.py index af9863534e..f582aefd81 100644 --- a/litellm/llms/anthropic/skills/transformation.py +++ b/litellm/llms/anthropic/skills/transformation.py @@ -35,17 +35,16 @@ class AnthropicSkillsConfig(BaseSkillsAPIConfig): """Add Anthropic-specific headers""" from litellm.llms.anthropic.common_utils import AnthropicModelInfo - # Get API key + # Get API key from litellm_params if available api_key = None - if litellm_params: + if litellm_params is not None: api_key = litellm_params.api_key - api_key = AnthropicModelInfo.get_api_key(api_key) - if not api_key: - raise ValueError("ANTHROPIC_API_KEY is required for Skills API") + auth_header = AnthropicModelInfo.get_auth_header(api_key) + if auth_header is None: + raise ValueError("ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN is required for Skills API") - # Add required headers - headers["x-api-key"] = api_key + headers.update(auth_header) headers["anthropic-version"] = "2023-06-01" # Add beta header for skills API diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index 4e3e04a847..f6b83a68ad 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -585,7 +585,7 @@ async def anthropic_proxy_route( """ [Docs](https://docs.litellm.ai/docs/pass_through/anthropic_completion) """ - base_target_url = os.getenv("ANTHROPIC_API_BASE") or "https://api.anthropic.com" + base_target_url = os.getenv("ANTHROPIC_API_BASE") or os.getenv("ANTHROPIC_BASE_URL") or "https://api.anthropic.com" encoded_endpoint = httpx.URL(endpoint).path # Ensure endpoint starts with '/' for proper URL construction @@ -609,7 +609,7 @@ async def anthropic_proxy_route( endpoint_func = create_pass_through_route( endpoint=endpoint, target=str(updated_url), - custom_headers={"x-api-key": "{}".format(anthropic_api_key)}, + custom_headers={"x-api-key": "{}".format(anthropic_api_key)} if anthropic_api_key else {}, _forward_headers=True, is_streaming_request=is_streaming_request, ) # dynamically construct pass-through endpoint based on incoming path diff --git a/litellm/utils.py b/litellm/utils.py index 81d749ab82..77625809bf 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -6160,7 +6160,7 @@ def validate_environment( # noqa: PLR0915 ["AZURE_API_BASE", "AZURE_API_VERSION", "AZURE_API_KEY"] ) elif custom_llm_provider == "anthropic": - if "ANTHROPIC_API_KEY" in os.environ: + if "ANTHROPIC_API_KEY" in os.environ or "ANTHROPIC_AUTH_TOKEN" in os.environ: keys_in_environment = True else: missing_keys.append("ANTHROPIC_API_KEY") @@ -6399,7 +6399,7 @@ def validate_environment( # noqa: PLR0915 missing_keys.append("OPENAI_API_KEY") ## anthropic elif model in litellm.anthropic_models: - if "ANTHROPIC_API_KEY" in os.environ: + if "ANTHROPIC_API_KEY" in os.environ or "ANTHROPIC_AUTH_TOKEN" in os.environ: keys_in_environment = True else: missing_keys.append("ANTHROPIC_API_KEY") @@ -8593,9 +8593,7 @@ class ProviderConfigManager: return ManusFilesConfig() elif LlmProviders.ANTHROPIC == provider: - from litellm.llms.anthropic.files.transformation import ( - AnthropicFilesConfig, - ) + from litellm.llms.anthropic.files.transformation import AnthropicFilesConfig return AnthropicFilesConfig() return None 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 b4f2629f8f..fcec155bae 100644 --- a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py +++ b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py @@ -1,20 +1,27 @@ """ -Tests for Anthropic OAuth token handling in common_utils. +Tests for Anthropic authentication and environment variable handling in common_utils. -Verifies that OAuth tokens (sk-ant-oat*) are sent via Authorization: Bearer -instead of x-api-key, per Anthropic's OAuth specification. +Verifies that: +- OAuth tokens (sk-ant-oat*) produce Authorization: Bearer headers with OAuth beta flags. +- Regular API keys produce x-api-key headers. +- ANTHROPIC_AUTH_TOKEN produces Authorization: Bearer headers, + matching the official Anthropic SDK behavior. +- ANTHROPIC_BASE_URL is used as a fallback for base URL resolution. +- ANTHROPIC_API_KEY / ANTHROPIC_API_BASE take precedence over their aliases. """ import os import sys +from unittest.mock import patch sys.path.insert( 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../..")) ) -# Fake OAuth token for testing (not a real secret) +# Fake tokens for testing (not real secrets) FAKE_OAUTH_TOKEN = "sk-ant-oat01-fake-token-for-testing-123456789abcdef" FAKE_REGULAR_KEY = "sk-ant-api03-regular-key-for-testing-123456789" +FAKE_AUTH_TOKEN = "sk-ant-aut01-fake-auth-token-for-testing-123456789" class TestOptionallyHandleAnthropicOAuth: @@ -697,3 +704,360 @@ class TestProxyOAuthHeaderForwarding: assert cleaned["authorization"] == oauth_token # Proxy key must be stripped assert "x-litellm-api-key" not in cleaned + + +class TestGetAnthropicHeadersWithAuthToken: + """Tests for get_anthropic_headers with auth_token parameter.""" + + def test_auth_token_uses_bearer_header(self): + """auth_token should produce Authorization: Bearer header.""" + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + config = AnthropicModelInfo() + headers = config.get_anthropic_headers( + api_key=None, + auth_token=FAKE_AUTH_TOKEN, + computer_tool_used=False, + prompt_caching_set=False, + pdf_used=False, + is_vertex_request=False, + ) + + assert headers["authorization"] == f"Bearer {FAKE_AUTH_TOKEN}" + assert "x-api-key" not in headers + # auth_token should NOT set OAuth-specific flags + assert "anthropic-dangerous-direct-browser-access" not in headers + + def test_auth_token_includes_standard_headers(self): + """auth_token path should include standard Anthropic headers.""" + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + config = AnthropicModelInfo() + headers = config.get_anthropic_headers( + api_key=None, + auth_token=FAKE_AUTH_TOKEN, + computer_tool_used=False, + prompt_caching_set=False, + pdf_used=False, + is_vertex_request=False, + ) + + assert headers["anthropic-version"] == "2023-06-01" + assert headers["accept"] == "application/json" + assert headers["content-type"] == "application/json" + + def test_api_key_takes_precedence_over_auth_token(self): + """When both api_key and auth_token are provided, api_key wins.""" + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + config = AnthropicModelInfo() + headers = config.get_anthropic_headers( + api_key=FAKE_REGULAR_KEY, + auth_token=FAKE_AUTH_TOKEN, + computer_tool_used=False, + prompt_caching_set=False, + pdf_used=False, + is_vertex_request=False, + ) + + assert headers["x-api-key"] == FAKE_REGULAR_KEY + assert "authorization" not in headers + + +class TestValidateEnvironmentAuthToken: + """Tests for validate_environment with auth_token resolution.""" + + def test_auth_token_env_var_produces_bearer_header(self): + """validate_environment should use Bearer auth when only ANTHROPIC_AUTH_TOKEN is set.""" + from unittest.mock import patch as mock_patch + + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + config = AnthropicModelInfo() + with mock_patch.dict( + "os.environ", + {"ANTHROPIC_AUTH_TOKEN": FAKE_AUTH_TOKEN}, + clear=True, + ): + headers = config.validate_environment( + headers={}, + model="claude-sonnet-4-5-20250929", + messages=[{"role": "user", "content": "Hello"}], + optional_params={}, + litellm_params={}, + api_key=None, + api_base=None, + ) + + assert headers["authorization"] == f"Bearer {FAKE_AUTH_TOKEN}" + assert "x-api-key" not in headers + assert "anthropic-dangerous-direct-browser-access" not in headers + + def test_api_key_param_takes_precedence_over_auth_token_env_var(self): + """validate_environment should prefer explicit api_key over ANTHROPIC_AUTH_TOKEN.""" + from unittest.mock import patch as mock_patch + + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + config = AnthropicModelInfo() + with mock_patch.dict( + "os.environ", + {"ANTHROPIC_AUTH_TOKEN": FAKE_AUTH_TOKEN}, + clear=True, + ): + headers = config.validate_environment( + headers={}, + model="claude-sonnet-4-5-20250929", + messages=[{"role": "user", "content": "Hello"}], + optional_params={}, + litellm_params={}, + api_key=FAKE_REGULAR_KEY, + api_base=None, + ) + + assert headers["x-api-key"] == FAKE_REGULAR_KEY + assert "authorization" not in headers + + def test_raises_when_no_credentials(self): + """validate_environment should raise when neither API key nor auth token is available.""" + from unittest.mock import patch as mock_patch + + import pytest + + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + config = AnthropicModelInfo() + with mock_patch.dict("os.environ", {}, clear=True): + with pytest.raises( + Exception, match="ANTHROPIC_API_KEY.*ANTHROPIC_AUTH_TOKEN" + ): + config.validate_environment( + headers={}, + model="claude-sonnet-4-5-20250929", + messages=[{"role": "user", "content": "Hello"}], + optional_params={}, + litellm_params={}, + api_key=None, + api_base=None, + ) + + + + +class TestGetAuthToken: + """Tests for AnthropicModelInfo.get_auth_token() static method.""" + + def test_returns_env_var_value(self): + """get_auth_token returns the ANTHROPIC_AUTH_TOKEN env var value.""" + from unittest.mock import patch as mock_patch + + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + with mock_patch.dict( + "os.environ", {"ANTHROPIC_AUTH_TOKEN": FAKE_AUTH_TOKEN}, clear=True + ): + assert AnthropicModelInfo.get_auth_token() == FAKE_AUTH_TOKEN + + def test_returns_none_when_not_set(self): + """get_auth_token returns None when ANTHROPIC_AUTH_TOKEN is not set.""" + from unittest.mock import patch as mock_patch + + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + with mock_patch.dict("os.environ", {}, clear=True): + assert AnthropicModelInfo.get_auth_token() is None + + def test_explicit_param_takes_precedence(self): + """Explicit auth_token param takes precedence over env var.""" + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + explicit_token = "sk-ant-aut01-explicit-token-override-123456789" + assert AnthropicModelInfo.get_auth_token(explicit_token) == explicit_token + + +class TestGetAuthHeader: + """Tests for AnthropicModelInfo.get_auth_header() centralized helper.""" + + def test_returns_x_api_key_when_api_key_provided(self): + """Explicit api_key param should return x-api-key header.""" + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + result = AnthropicModelInfo.get_auth_header(api_key=FAKE_REGULAR_KEY) + assert result == {"x-api-key": FAKE_REGULAR_KEY} + + def test_returns_x_api_key_from_env(self): + """ANTHROPIC_API_KEY env var should return x-api-key header.""" + from unittest.mock import patch as mock_patch + + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + with mock_patch.dict( + "os.environ", + {"ANTHROPIC_API_KEY": FAKE_REGULAR_KEY}, + clear=True, + ): + result = AnthropicModelInfo.get_auth_header() + assert result == {"x-api-key": FAKE_REGULAR_KEY} + + def test_returns_bearer_from_auth_token_env(self): + """ANTHROPIC_AUTH_TOKEN env var should return Authorization: Bearer header.""" + from unittest.mock import patch as mock_patch + + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + with mock_patch.dict( + "os.environ", + {"ANTHROPIC_AUTH_TOKEN": FAKE_AUTH_TOKEN}, + clear=True, + ): + result = AnthropicModelInfo.get_auth_header() + assert result == {"authorization": f"Bearer {FAKE_AUTH_TOKEN}"} + + def test_api_key_takes_precedence_over_auth_token(self): + """ANTHROPIC_API_KEY should take precedence over ANTHROPIC_AUTH_TOKEN.""" + from unittest.mock import patch as mock_patch + + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + with mock_patch.dict( + "os.environ", + { + "ANTHROPIC_API_KEY": FAKE_REGULAR_KEY, + "ANTHROPIC_AUTH_TOKEN": FAKE_AUTH_TOKEN, + }, + clear=True, + ): + result = AnthropicModelInfo.get_auth_header() + assert result == {"x-api-key": FAKE_REGULAR_KEY} + + def test_explicit_api_key_overrides_env_auth_token(self): + """Explicit api_key param should override ANTHROPIC_AUTH_TOKEN env var.""" + from unittest.mock import patch as mock_patch + + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + with mock_patch.dict( + "os.environ", + {"ANTHROPIC_AUTH_TOKEN": FAKE_AUTH_TOKEN}, + clear=True, + ): + result = AnthropicModelInfo.get_auth_header(api_key=FAKE_REGULAR_KEY) + assert result == {"x-api-key": FAKE_REGULAR_KEY} + + def test_returns_none_when_no_credentials(self): + """Should return None when neither api_key nor auth_token is available.""" + from unittest.mock import patch as mock_patch + + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + with mock_patch.dict("os.environ", {}, clear=True): + result = AnthropicModelInfo.get_auth_header() + assert result is None + + +class TestGetApiBaseFallbackChain: + """Tests for AnthropicModelInfo.get_api_base() fallback to ANTHROPIC_BASE_URL.""" + + def test_explicit_param_takes_precedence(self): + """Explicit api_base param takes precedence over all env vars.""" + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + assert ( + AnthropicModelInfo.get_api_base("https://explicit.example.com") + == "https://explicit.example.com" + ) + + def test_defaults_to_anthropic_api(self): + """get_api_base returns the default Anthropic API base when no env vars are set.""" + from unittest.mock import patch as mock_patch + + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + with mock_patch.dict("os.environ", {}, clear=True): + assert AnthropicModelInfo.get_api_base() == "https://api.anthropic.com" + + def test_api_base_env_preferred_over_base_url_env(self): + """ANTHROPIC_API_BASE takes precedence over ANTHROPIC_BASE_URL.""" + from unittest.mock import patch as mock_patch + + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + with mock_patch.dict( + "os.environ", + { + "ANTHROPIC_API_BASE": "https://api-base.example.com", + "ANTHROPIC_BASE_URL": "https://base-url.example.com", + }, + clear=True, + ): + assert AnthropicModelInfo.get_api_base() == "https://api-base.example.com" + + def test_falls_back_to_base_url_env(self): + """get_api_base falls back to ANTHROPIC_BASE_URL when ANTHROPIC_API_BASE is not set.""" + from unittest.mock import patch as mock_patch + + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + with mock_patch.dict( + "os.environ", + {"ANTHROPIC_BASE_URL": "https://base-url.example.com"}, + clear=True, + ): + assert AnthropicModelInfo.get_api_base() == "https://base-url.example.com" + + +class TestPassthroughAuthToken: + """Tests for passthrough messages endpoint with ANTHROPIC_AUTH_TOKEN.""" + + def test_passthrough_auth_token_uses_bearer_header(self): + """Passthrough endpoint should use Bearer auth when only ANTHROPIC_AUTH_TOKEN is set.""" + from unittest.mock import patch as mock_patch + + from litellm.llms.anthropic.experimental_pass_through.messages.transformation import ( + AnthropicMessagesConfig, + ) + + config = AnthropicMessagesConfig() + with mock_patch.dict( + "os.environ", {"ANTHROPIC_AUTH_TOKEN": FAKE_AUTH_TOKEN}, clear=True + ): + updated_headers, _ = config.validate_anthropic_messages_environment( + headers={}, + model="claude-sonnet-4-5-20250929", + messages=[{"role": "user", "content": "Hello"}], + optional_params={}, + litellm_params={}, + api_key=None, + api_base=None, + ) + + assert updated_headers["authorization"] == f"Bearer {FAKE_AUTH_TOKEN}" + assert "x-api-key" not in updated_headers + assert "anthropic-dangerous-direct-browser-access" not in updated_headers + + def test_passthrough_api_key_takes_precedence(self): + """Passthrough endpoint should prefer ANTHROPIC_API_KEY over ANTHROPIC_AUTH_TOKEN.""" + from unittest.mock import patch as mock_patch + + from litellm.llms.anthropic.experimental_pass_through.messages.transformation import ( + AnthropicMessagesConfig, + ) + + config = AnthropicMessagesConfig() + with mock_patch.dict( + "os.environ", + {"ANTHROPIC_API_KEY": FAKE_REGULAR_KEY, "ANTHROPIC_AUTH_TOKEN": FAKE_AUTH_TOKEN}, + clear=True, + ): + updated_headers, _ = config.validate_anthropic_messages_environment( + headers={}, + model="claude-sonnet-4-5-20250929", + messages=[{"role": "user", "content": "Hello"}], + optional_params={}, + litellm_params={}, + api_key=None, + api_base=None, + ) + + assert updated_headers["x-api-key"] == FAKE_REGULAR_KEY + assert "authorization" not in updated_headers