diff --git a/litellm/anthropic_beta_headers_config.json b/litellm/anthropic_beta_headers_config.json index 5edb8067a08..a066a5e95bc 100644 --- a/litellm/anthropic_beta_headers_config.json +++ b/litellm/anthropic_beta_headers_config.json @@ -19,6 +19,7 @@ "mcp-client-2025-11-20": "mcp-client-2025-11-20", "mcp-client-2025-04-04": "mcp-client-2025-04-04", "mcp-servers-2025-12-04": "mcp-servers-2025-12-04", + "oauth-2025-04-20": "oauth-2025-04-20", "output-128k-2025-02-19": "output-128k-2025-02-19", "prompt-caching-scope-2026-01-05": "prompt-caching-scope-2026-01-05", "skills-2025-10-02": "skills-2025-10-02", diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index cb23d21fbc9..c665e084261 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -38,9 +38,18 @@ def optionally_handle_anthropic_oauth( Returns: Tuple of (updated headers, api_key) """ + # Check Authorization header (passthrough / forwarded requests) auth_header = headers.get("authorization", "") if auth_header and auth_header.startswith(f"Bearer {ANTHROPIC_OAUTH_TOKEN_PREFIX}"): api_key = auth_header.replace("Bearer ", "") + headers.pop("x-api-key", None) + headers["anthropic-beta"] = ANTHROPIC_OAUTH_BETA_HEADER + headers["anthropic-dangerous-direct-browser-access"] = "true" + return headers, api_key + # Check api_key directly (standard chat/completion flow) + if api_key and api_key.startswith(ANTHROPIC_OAUTH_TOKEN_PREFIX): + headers.pop("x-api-key", None) + headers["authorization"] = f"Bearer {api_key}" headers["anthropic-beta"] = ANTHROPIC_OAUTH_BETA_HEADER headers["anthropic-dangerous-direct-browser-access"] = "true" return headers, api_key @@ -108,7 +117,9 @@ class AnthropicModelInfo(BaseLLMModelInfo): if tools is None: return False for tool in tools: - if "type" in tool and tool["type"].startswith(ANTHROPIC_HOSTED_TOOLS.WEB_SEARCH.value): + if "type" in tool and tool["type"].startswith( + ANTHROPIC_HOSTED_TOOLS.WEB_SEARCH.value + ): return True return False @@ -134,111 +145,126 @@ class AnthropicModelInfo(BaseLLMModelInfo): """ if not tools: return False - + for tool in tools: tool_type = tool.get("type", "") - if tool_type in ["tool_search_tool_regex_20251119", "tool_search_tool_bm25_20251119"]: + if tool_type in [ + "tool_search_tool_regex_20251119", + "tool_search_tool_bm25_20251119", + ]: return True return False - + def is_programmatic_tool_calling_used(self, tools: Optional[List]) -> bool: """ Check if programmatic tool calling is being used (tools with allowed_callers field). - + Returns True if any tool has allowed_callers containing 'code_execution_20250825'. """ if not tools: return False - + for tool in tools: # Check top-level allowed_callers allowed_callers = tool.get("allowed_callers", None) if allowed_callers and isinstance(allowed_callers, list): if "code_execution_20250825" in allowed_callers: return True - + # Check function.allowed_callers for OpenAI format tools function = tool.get("function", {}) if isinstance(function, dict): function_allowed_callers = function.get("allowed_callers", None) - if function_allowed_callers and isinstance(function_allowed_callers, list): + if function_allowed_callers and isinstance( + function_allowed_callers, list + ): if "code_execution_20250825" in function_allowed_callers: return True - + return False - + def is_input_examples_used(self, tools: Optional[List]) -> bool: """ Check if input_examples is being used in any tools. - + Returns True if any tool has input_examples field. """ if not tools: return False - + for tool in tools: # Check top-level input_examples input_examples = tool.get("input_examples", None) - if input_examples and isinstance(input_examples, list) and len(input_examples) > 0: + if ( + input_examples + and isinstance(input_examples, list) + and len(input_examples) > 0 + ): return True - + # Check function.input_examples for OpenAI format tools function = tool.get("function", {}) if isinstance(function, dict): function_input_examples = function.get("input_examples", None) - if function_input_examples and isinstance(function_input_examples, list) and len(function_input_examples) > 0: + if ( + function_input_examples + and isinstance(function_input_examples, list) + and len(function_input_examples) > 0 + ): return True - + return False - - def is_effort_used(self, optional_params: Optional[dict], model: Optional[str] = None) -> bool: + + def is_effort_used( + self, optional_params: Optional[dict], model: Optional[str] = None + ) -> bool: """ Check if effort parameter is being used. - + Returns True if effort-related parameters are present. """ if not optional_params: return False - + # Check if reasoning_effort is provided for Claude Opus 4.5 if model and ("opus-4-5" in model.lower() or "opus_4_5" in model.lower()): reasoning_effort = optional_params.get("reasoning_effort") if reasoning_effort and isinstance(reasoning_effort, str): return True - + # Check if output_config is directly provided output_config = optional_params.get("output_config") if output_config and isinstance(output_config, dict): effort = output_config.get("effort") if effort and isinstance(effort, str): return True - + return False def is_code_execution_tool_used(self, tools: Optional[List]) -> bool: """ Check if code execution tool is being used. - + Returns True if any tool has type "code_execution_20250825". """ if not tools: return False - + for tool in tools: tool_type = tool.get("type", "") if tool_type == "code_execution_20250825": return True return False - + def is_container_with_skills_used(self, optional_params: Optional[dict]) -> bool: """ Check if container with skills is being used. - + Returns True if optional_params contains container with skills. """ if not optional_params: return False - + container = optional_params.get("container") if container and isinstance(container, dict): skills = container.get("skills") @@ -256,10 +282,10 @@ class AnthropicModelInfo(BaseLLMModelInfo): def get_computer_tool_beta_header(self, computer_tool_version: str) -> str: """ Get the appropriate beta header for a given computer tool version. - + Args: computer_tool_version: The computer tool version (e.g., 'computer_20250124', 'computer_20241022') - + Returns: The corresponding beta header string """ @@ -282,37 +308,37 @@ class AnthropicModelInfo(BaseLLMModelInfo): ) -> List[str]: """ Get list of common beta headers based on the features that are active. - + Returns: List of beta header strings """ from litellm.types.llms.anthropic import ( ANTHROPIC_EFFORT_BETA_HEADER, ) - + betas = [] - + # Detect features effort_used = self.is_effort_used(optional_params, model) - + if effort_used: betas.append(ANTHROPIC_EFFORT_BETA_HEADER) # effort-2025-11-24 - + if computer_tool_used: beta_header = self.get_computer_tool_beta_header(computer_tool_used) betas.append(beta_header) - + # Anthropic no longer requires the prompt-caching beta header # Prompt caching now works automatically when cache_control is used in messages # Reference: https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching - + if file_id_used: betas.append("files-api-2025-04-14") betas.append("code-execution-2025-05-22") - + if mcp_server_used: betas.append("mcp-client-2025-04-04") - + return list(set(betas)) def get_anthropic_headers( @@ -351,27 +377,35 @@ class AnthropicModelInfo(BaseLLMModelInfo): # Tool search, programmatic tool calling, and input_examples all use the same beta header if tool_search_used or programmatic_tool_calling_used or input_examples_used: from litellm.types.llms.anthropic import ANTHROPIC_TOOL_SEARCH_BETA_HEADER + betas.add(ANTHROPIC_TOOL_SEARCH_BETA_HEADER) - + # Effort parameter uses a separate beta header if effort_used: from litellm.types.llms.anthropic import ANTHROPIC_EFFORT_BETA_HEADER + betas.add(ANTHROPIC_EFFORT_BETA_HEADER) - + # Code execution tool uses a separate beta header if code_execution_tool_used: betas.add("code-execution-2025-08-25") - + # Container with skills uses a separate beta header if container_with_skills_used: betas.add("skills-2025-10-02") + _is_oauth = api_key and api_key.startswith(ANTHROPIC_OAUTH_TOKEN_PREFIX) headers = { "anthropic-version": anthropic_version or "2023-06-01", - "x-api-key": api_key, "accept": "application/json", "content-type": "application/json", } + if _is_oauth: + headers["authorization"] = f"Bearer {api_key}" + headers["anthropic-dangerous-direct-browser-access"] = "true" + betas.add(ANTHROPIC_OAUTH_BETA_HEADER) + else: + headers["x-api-key"] = api_key if user_anthropic_beta_headers is not None: betas.update(user_anthropic_beta_headers) @@ -381,7 +415,10 @@ class AnthropicModelInfo(BaseLLMModelInfo): # Vertex AI requires web search beta header for web search to work if web_search_tool_used: from litellm.types.llms.anthropic import ANTHROPIC_BETA_HEADER_VALUES - headers["anthropic-beta"] = ANTHROPIC_BETA_HEADER_VALUES.WEB_SEARCH_2025_03_05.value + + headers[ + "anthropic-beta" + ] = ANTHROPIC_BETA_HEADER_VALUES.WEB_SEARCH_2025_03_05.value elif len(betas) > 0: headers["anthropic-beta"] = ",".join(betas) @@ -398,7 +435,9 @@ class AnthropicModelInfo(BaseLLMModelInfo): api_base: Optional[str] = None, ) -> Dict: # Check for Anthropic OAuth token in headers - headers, api_key = optionally_handle_anthropic_oauth(headers=headers, api_key=api_key) + headers, api_key = optionally_handle_anthropic_oauth( + headers=headers, api_key=api_key + ) if api_key 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", @@ -416,11 +455,15 @@ class AnthropicModelInfo(BaseLLMModelInfo): file_id_used = self.is_file_id_used(messages=messages) web_search_tool_used = self.is_web_search_tool_used(tools=tools) tool_search_used = self.is_tool_search_used(tools=tools) - programmatic_tool_calling_used = self.is_programmatic_tool_calling_used(tools=tools) + programmatic_tool_calling_used = self.is_programmatic_tool_calling_used( + tools=tools + ) input_examples_used = self.is_input_examples_used(tools=tools) effort_used = self.is_effort_used(optional_params=optional_params, model=model) code_execution_tool_used = self.is_code_execution_tool_used(tools=tools) - container_with_skills_used = self.is_container_with_skills_used(optional_params=optional_params) + container_with_skills_used = self.is_container_with_skills_used( + optional_params=optional_params + ) user_anthropic_beta_headers = self._get_user_anthropic_beta_headers( anthropic_beta_header=headers.get("anthropic-beta") ) @@ -499,7 +542,7 @@ class AnthropicModelInfo(BaseLLMModelInfo): def get_token_counter(self) -> Optional[BaseTokenCounter]: """ Factory method to create an Anthropic token counter. - + Returns: AnthropicTokenCounter instance for this provider. """ diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py index 8f2f3bf3545..8275ba2b3e1 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py @@ -49,15 +49,15 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): # TODO: Add Anthropic `metadata` support # "metadata", ] - + @staticmethod def _filter_billing_headers_from_system(system_param): """ Filter out x-anthropic-billing-header metadata from system parameter. - + Args: system_param: Can be a string or a list of system message content blocks - + Returns: Filtered system parameter (string or list), or None if all content was filtered """ @@ -74,7 +74,9 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): text = content_block.get("text", "") content_type = content_block.get("type", "") # Skip text blocks that start with billing header - if content_type == "text" and text.startswith("x-anthropic-billing-header:"): + if content_type == "text" and text.startswith( + "x-anthropic-billing-header:" + ): continue filtered_list.append(content_block) else: @@ -111,11 +113,13 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): import os # Check for Anthropic OAuth token in Authorization header - headers, api_key = optionally_handle_anthropic_oauth(headers=headers, api_key=api_key) + 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 api_key: + if "x-api-key" not in headers and "authorization" not in headers and api_key: headers["x-api-key"] = api_key if "anthropic-version" not in headers: headers["anthropic-version"] = DEFAULT_ANTHROPIC_API_VERSION @@ -149,7 +153,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): message="max_tokens is required for Anthropic /v1/messages API", status_code=400, ) - + # Filter out x-anthropic-billing-header from system messages system_param = anthropic_messages_optional_request_params.get("system") if system_param is not None: @@ -159,7 +163,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): else: # Remove system parameter if all content was filtered out anthropic_messages_optional_request_params.pop("system", None) - + ####### get required params for all anthropic messages requests ###### verbose_logger.debug(f"TRANSFORMATION DEBUG - Messages: {messages}") anthropic_messages_request: AnthropicMessagesRequest = AnthropicMessagesRequest( @@ -244,25 +248,29 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): edits = context_management_param.get("edits", []) has_compact = False has_other = False - + for edit in edits: edit_type = edit.get("type", "") if edit_type == "compact_20260112": has_compact = True else: has_other = True - + # Add compact header if any compact edits exist if has_compact: beta_values.add(ANTHROPIC_BETA_HEADER_VALUES.COMPACT_2026_01_12.value) - + # Add context management header if any other edits exist if has_other: - beta_values.add(ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_MANAGEMENT_2025_06_27.value) + beta_values.add( + ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_MANAGEMENT_2025_06_27.value + ) # Check for structured outputs if optional_params.get("output_format") is not None: - beta_values.add(ANTHROPIC_BETA_HEADER_VALUES.STRUCTURED_OUTPUT_2025_09_25.value) + beta_values.add( + ANTHROPIC_BETA_HEADER_VALUES.STRUCTURED_OUTPUT_2025_09_25.value + ) # Check for fast mode if optional_params.get("speed") == "fast": 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 0a397d116e7..a321a24540f 100644 --- a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py +++ b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py @@ -1,84 +1,285 @@ """ -Tests for Anthropic OAuth token handling for Claude Code Max integration. +Tests for Anthropic OAuth token 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. """ import os import sys -# Add litellm to path -sys.path.insert(0, os.path.abspath("../../../../..")) +sys.path.insert( + 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../..")) +) # Fake OAuth token for testing (not a real secret) FAKE_OAUTH_TOKEN = "sk-ant-oat01-fake-token-for-testing-123456789abcdef" +FAKE_REGULAR_KEY = "sk-ant-api03-regular-key-for-testing-123456789" -def test_oauth_detection_in_common_utils(): - """Test 1: OAuth token detection in common_utils""" - from litellm.llms.anthropic.common_utils import optionally_handle_anthropic_oauth +class TestOptionallyHandleAnthropicOAuth: + """Tests for optionally_handle_anthropic_oauth function.""" - headers = {"authorization": f"Bearer {FAKE_OAUTH_TOKEN}"} - updated_headers, extracted_api_key = optionally_handle_anthropic_oauth(headers, None) + def test_oauth_token_in_authorization_header(self): + """OAuth token in Authorization header should be detected and headers set correctly.""" + from litellm.llms.anthropic.common_utils import ( + optionally_handle_anthropic_oauth, + ) - assert extracted_api_key == FAKE_OAUTH_TOKEN - assert updated_headers["anthropic-beta"] == "oauth-2025-04-20" - assert updated_headers["anthropic-dangerous-direct-browser-access"] == "true" + headers = {"authorization": f"Bearer {FAKE_OAUTH_TOKEN}"} + updated_headers, extracted_api_key = optionally_handle_anthropic_oauth( + headers, None + ) + + assert extracted_api_key == FAKE_OAUTH_TOKEN + assert updated_headers["anthropic-beta"] == "oauth-2025-04-20" + assert updated_headers["anthropic-dangerous-direct-browser-access"] == "true" + assert "x-api-key" not in updated_headers + + def test_oauth_token_in_api_key_directly(self): + """OAuth token passed as api_key should set Authorization: Bearer header.""" + from litellm.llms.anthropic.common_utils import ( + optionally_handle_anthropic_oauth, + ) + + headers = {} + updated_headers, returned_api_key = optionally_handle_anthropic_oauth( + headers, FAKE_OAUTH_TOKEN + ) + + assert returned_api_key == FAKE_OAUTH_TOKEN + assert updated_headers["authorization"] == f"Bearer {FAKE_OAUTH_TOKEN}" + assert updated_headers["anthropic-beta"] == "oauth-2025-04-20" + assert updated_headers["anthropic-dangerous-direct-browser-access"] == "true" + assert "x-api-key" not in updated_headers + + def test_oauth_removes_existing_x_api_key(self): + """When OAuth is detected, any existing x-api-key should be removed.""" + from litellm.llms.anthropic.common_utils import ( + optionally_handle_anthropic_oauth, + ) + + headers = {"x-api-key": FAKE_OAUTH_TOKEN} + updated_headers, _ = optionally_handle_anthropic_oauth( + headers, FAKE_OAUTH_TOKEN + ) + + assert "x-api-key" not in updated_headers + assert updated_headers["authorization"] == f"Bearer {FAKE_OAUTH_TOKEN}" + + def test_regular_api_key_unchanged(self): + """Regular API keys (non-OAuth) should pass through unmodified.""" + from litellm.llms.anthropic.common_utils import ( + optionally_handle_anthropic_oauth, + ) + + headers = {} + updated_headers, returned_api_key = optionally_handle_anthropic_oauth( + headers, FAKE_REGULAR_KEY + ) + + assert returned_api_key == FAKE_REGULAR_KEY + assert "authorization" not in updated_headers + assert "anthropic-dangerous-direct-browser-access" not in updated_headers + assert "anthropic-beta" not in updated_headers + + def test_regular_key_in_authorization_header(self): + """Non-OAuth token in Authorization header should not trigger OAuth handling.""" + from litellm.llms.anthropic.common_utils import ( + optionally_handle_anthropic_oauth, + ) + + headers = {"authorization": f"Bearer {FAKE_REGULAR_KEY}"} + updated_headers, returned_api_key = optionally_handle_anthropic_oauth( + headers, FAKE_REGULAR_KEY + ) + + assert returned_api_key == FAKE_REGULAR_KEY + assert "anthropic-dangerous-direct-browser-access" not in updated_headers + + def test_none_api_key_no_error(self): + """None api_key with empty headers should not raise errors.""" + from litellm.llms.anthropic.common_utils import ( + optionally_handle_anthropic_oauth, + ) + + headers = {} + updated_headers, returned_api_key = optionally_handle_anthropic_oauth( + headers, None + ) + + assert returned_api_key is None + assert "authorization" not in updated_headers -def test_oauth_integration_in_validate_environment(): - """Test 2: OAuth integration in AnthropicConfig validate_environment""" - from litellm.llms.anthropic.common_utils import AnthropicModelInfo +class TestGetAnthropicHeaders: + """Tests for get_anthropic_headers method with OAuth support.""" - config = AnthropicModelInfo() - headers = {"authorization": f"Bearer {FAKE_OAUTH_TOKEN}"} + def test_oauth_token_uses_authorization_bearer(self): + """OAuth token should produce Authorization: Bearer header, not x-api-key.""" + from litellm.llms.anthropic.common_utils import AnthropicModelInfo - updated_headers = config.validate_environment( - headers=headers, - model="claude-3-haiku-20240307", - messages=[{"role": "user", "content": "Hello"}], - optional_params={}, - litellm_params={}, - api_key=None, - api_base=None, - ) + config = AnthropicModelInfo() + headers = config.get_anthropic_headers( + api_key=FAKE_OAUTH_TOKEN, + computer_tool_used=False, + prompt_caching_set=False, + pdf_used=False, + is_vertex_request=False, + ) - assert updated_headers["x-api-key"] == FAKE_OAUTH_TOKEN - assert updated_headers["anthropic-dangerous-direct-browser-access"] == "true" + assert headers["authorization"] == f"Bearer {FAKE_OAUTH_TOKEN}" + assert headers["anthropic-dangerous-direct-browser-access"] == "true" + assert "oauth-2025-04-20" in headers.get("anthropic-beta", "") + assert "x-api-key" not in headers + + def test_regular_key_uses_x_api_key(self): + """Regular API key should produce x-api-key header, not Authorization.""" + 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, + ) + + assert headers["x-api-key"] == FAKE_REGULAR_KEY + assert "authorization" not in headers + assert "anthropic-dangerous-direct-browser-access" 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 + + config = AnthropicModelInfo() + headers = config.get_anthropic_headers( + api_key=FAKE_OAUTH_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_oauth_detection_in_messages_transformation(): - """Test 3: OAuth detection in messages transformation""" - from litellm.llms.anthropic.experimental_pass_through.messages.transformation import ( - AnthropicMessagesConfig, - ) +class TestValidateEnvironmentOAuth: + """Tests for validate_environment with OAuth tokens.""" - config = AnthropicMessagesConfig() - headers = {"authorization": f"Bearer {FAKE_OAUTH_TOKEN}"} + def test_oauth_via_authorization_header(self): + """validate_environment should produce correct headers for OAuth tokens.""" + from litellm.llms.anthropic.common_utils import AnthropicModelInfo - updated_headers, _ = config.validate_anthropic_messages_environment( - headers=headers, - model="claude-3-haiku-20240307", - messages=[{"role": "user", "content": "Hello"}], - optional_params={}, - litellm_params={}, - api_key=None, - api_base=None, - ) + config = AnthropicModelInfo() + headers = {"authorization": f"Bearer {FAKE_OAUTH_TOKEN}"} - assert updated_headers["x-api-key"] == FAKE_OAUTH_TOKEN - assert "oauth-2025-04-20" in updated_headers["anthropic-beta"] - assert updated_headers["anthropic-dangerous-direct-browser-access"] == "true" + updated_headers = config.validate_environment( + headers=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_OAUTH_TOKEN}" + assert updated_headers["anthropic-dangerous-direct-browser-access"] == "true" + assert "oauth-2025-04-20" in updated_headers.get("anthropic-beta", "") + assert "x-api-key" not in updated_headers + + def test_oauth_via_api_key_param(self): + """validate_environment with OAuth token as api_key should use Bearer auth.""" + 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=FAKE_OAUTH_TOKEN, + api_base=None, + ) + + assert updated_headers["authorization"] == f"Bearer {FAKE_OAUTH_TOKEN}" + assert updated_headers["anthropic-dangerous-direct-browser-access"] == "true" + assert "x-api-key" not in updated_headers + + def test_regular_key_via_api_key_param(self): + """validate_environment with regular API key should use x-api-key.""" + 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=FAKE_REGULAR_KEY, + api_base=None, + ) + + assert updated_headers["x-api-key"] == FAKE_REGULAR_KEY + assert "authorization" not in updated_headers + assert "anthropic-dangerous-direct-browser-access" not in updated_headers -def test_regular_api_keys_still_work(): - """Test 4: Regular API keys still work (regression test)""" - from litellm.llms.anthropic.common_utils import optionally_handle_anthropic_oauth +class TestPassthroughOAuth: + """Tests for passthrough messages endpoint with OAuth tokens.""" - regular_key = "sk-ant-api03-regular-key-123" - headers = {"authorization": f"Bearer {regular_key}"} + def test_passthrough_oauth_no_x_api_key(self): + """Passthrough endpoint should not add x-api-key for OAuth tokens.""" + from litellm.llms.anthropic.experimental_pass_through.messages.transformation import ( + AnthropicMessagesConfig, + ) - updated_headers, extracted_api_key = optionally_handle_anthropic_oauth(headers, regular_key) + config = AnthropicMessagesConfig() + headers = {"authorization": f"Bearer {FAKE_OAUTH_TOKEN}"} - # Regular key should be unchanged - assert extracted_api_key == regular_key - # OAuth headers should NOT be added - assert "anthropic-dangerous-direct-browser-access" not in updated_headers \ No newline at end of file + updated_headers, _ = config.validate_anthropic_messages_environment( + headers=headers, + model="claude-sonnet-4-5-20250929", + messages=[{"role": "user", "content": "Hello"}], + optional_params={}, + litellm_params={}, + api_key=None, + api_base=None, + ) + + assert "oauth-2025-04-20" in updated_headers.get("anthropic-beta", "") + assert updated_headers["anthropic-dangerous-direct-browser-access"] == "true" + assert "x-api-key" not in updated_headers + + def test_passthrough_regular_key_uses_x_api_key(self): + """Passthrough endpoint should still use x-api-key for regular API keys.""" + from litellm.llms.anthropic.experimental_pass_through.messages.transformation import ( + AnthropicMessagesConfig, + ) + + config = AnthropicMessagesConfig() + headers = {} + + updated_headers, _ = config.validate_anthropic_messages_environment( + headers=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 updated_headers["x-api-key"] == FAKE_REGULAR_KEY + assert "authorization" not in updated_headers