feat: Add support for custom User-Agent header in Anthropic provider

Fixes #19017

This commit adds the ability to customize the User-Agent header for
Anthropic/Claude API requests to avoid credential blocks when using
restricted API keys (e.g., Claude Code credentials).

Changes:
- Added `custom_user_agent` parameter support in AnthropicConfig
- Added `ANTHROPIC_USER_AGENT` environment variable support
- Modified validate_environment() to set User-Agent header when custom
  value is provided
- Added comprehensive unit tests for custom User-Agent functionality

Priority order for User-Agent:
1. custom_user_agent parameter
2. ANTHROPIC_USER_AGENT environment variable
3. User-Agent in extra_headers
4. Default litellm/{version}

Users can now override the User-Agent by:
- Passing custom_user_agent parameter:
  litellm.completion(model="anthropic/...", custom_user_agent="Claude Code/1.0")
- Setting environment variable:
  export ANTHROPIC_USER_AGENT="Claude Code/1.0"
- Using extra_headers:
  litellm.completion(model="anthropic/...", extra_headers={"User-Agent": "..."})
This commit is contained in:
Claude 2026-01-21 05:48:23 +00:00
parent a02c43d300
commit 509dfd9a5f
No known key found for this signature in database
3 changed files with 113 additions and 1 deletions

View file

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

View file

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

View file

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