diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 86378b97d2e..141f76cd968 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -189,6 +189,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): "response_format", "user", "web_search_options", + "custom_user_agent", ] if "claude-3-7-sonnet" in model or supports_reasoning( @@ -802,6 +803,8 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): ) elif param == "extra_headers": optional_params["extra_headers"] = value + elif param == "custom_user_agent": + optional_params["custom_user_agent"] = value ## handle thinking tokens self.update_optional_params_with_thinking_tokens( diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index cb23d21fbc9..1ab1ec7278d 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -397,6 +397,8 @@ class AnthropicModelInfo(BaseLLMModelInfo): api_key: Optional[str] = None, api_base: Optional[str] = None, ) -> Dict: + import os + # Check for Anthropic OAuth token in headers headers, api_key = optionally_handle_anthropic_oauth(headers=headers, api_key=api_key) if api_key is None: @@ -444,6 +446,16 @@ class AnthropicModelInfo(BaseLLMModelInfo): headers = {**headers, **anthropic_headers} + # Handle custom User-Agent header + # Priority: custom_user_agent parameter > ANTHROPIC_USER_AGENT env var > User-Agent in extra_headers > default + custom_user_agent = ( + optional_params.get("custom_user_agent") + or os.getenv("ANTHROPIC_USER_AGENT") + ) + + if custom_user_agent: + headers["User-Agent"] = custom_user_agent + return headers @staticmethod 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..1dee23aff8b 100644 --- a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py +++ b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py @@ -81,4 +81,101 @@ def test_regular_api_keys_still_work(): # 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 + assert "anthropic-dangerous-direct-browser-access" not in updated_headers + + +def test_custom_user_agent_via_parameter(): + """Test 5: Custom User-Agent via custom_user_agent parameter""" + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + config = AnthropicModelInfo() + custom_agent = "Claude Code/1.0" + + updated_headers = config.validate_environment( + headers={}, + model="claude-3-haiku-20240307", + messages=[{"role": "user", "content": "Hello"}], + optional_params={"custom_user_agent": custom_agent}, + litellm_params={}, + api_key="sk-ant-api03-test-key", + api_base=None, + ) + + assert updated_headers["User-Agent"] == custom_agent + + +def test_custom_user_agent_via_env_var(): + """Test 6: Custom User-Agent via ANTHROPIC_USER_AGENT environment variable""" + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + config = AnthropicModelInfo() + custom_agent = "Claude Code/1.0" + + # Set environment variable + os.environ["ANTHROPIC_USER_AGENT"] = custom_agent + + try: + updated_headers = config.validate_environment( + headers={}, + model="claude-3-haiku-20240307", + messages=[{"role": "user", "content": "Hello"}], + optional_params={}, + litellm_params={}, + api_key="sk-ant-api03-test-key", + api_base=None, + ) + + assert updated_headers["User-Agent"] == custom_agent + finally: + # Clean up + del os.environ["ANTHROPIC_USER_AGENT"] + + +def test_custom_user_agent_parameter_priority(): + """Test 7: custom_user_agent parameter takes priority over environment variable""" + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + config = AnthropicModelInfo() + param_agent = "Claude Code/2.0" + env_agent = "Claude Code/1.0" + + # Set environment variable + os.environ["ANTHROPIC_USER_AGENT"] = env_agent + + try: + updated_headers = config.validate_environment( + headers={}, + model="claude-3-haiku-20240307", + messages=[{"role": "user", "content": "Hello"}], + optional_params={"custom_user_agent": param_agent}, + litellm_params={}, + api_key="sk-ant-api03-test-key", + api_base=None, + ) + + # Parameter should take priority over environment variable + assert updated_headers["User-Agent"] == param_agent + finally: + # Clean up + del os.environ["ANTHROPIC_USER_AGENT"] + + +def test_no_custom_user_agent(): + """Test 8: Default User-Agent is preserved when no custom agent is provided""" + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + config = AnthropicModelInfo() + default_agent = "litellm/1.0.0" + + updated_headers = config.validate_environment( + headers={"User-Agent": default_agent}, + model="claude-3-haiku-20240307", + messages=[{"role": "user", "content": "Hello"}], + optional_params={}, + litellm_params={}, + api_key="sk-ant-api03-test-key", + api_base=None, + ) + + # Default User-Agent should be preserved + assert updated_headers["User-Agent"] == default_agent \ No newline at end of file