diff --git a/litellm/proxy/example_config_yaml/README_ANTHROPIC_USER_AGENT.md b/litellm/proxy/example_config_yaml/README_ANTHROPIC_USER_AGENT.md new file mode 100644 index 00000000000..b6230b4ad2a --- /dev/null +++ b/litellm/proxy/example_config_yaml/README_ANTHROPIC_USER_AGENT.md @@ -0,0 +1,118 @@ +# Custom User-Agent Configuration for Anthropic + +This guide explains how to configure custom User-Agent headers for Anthropic (Claude) API requests in LiteLLM Proxy. + +## Problem + +By default, LiteLLM adds `User-Agent: litellm/{version}` to all API requests. However, some Anthropic credentials are restricted to specific User-Agent values. For example, Claude Code credentials may return an error: + +``` +This credential is only authorized for use with Claude Code and cannot be used for other API requests. +``` + +## Solution + +LiteLLM now supports customizing the User-Agent header for Anthropic requests in three ways: + +### Option 1: Per-Model Configuration (Recommended) + +Add `custom_user_agent` to your model's `litellm_params` in the proxy config YAML: + +```yaml +model_list: + - model_name: claude-code + litellm_params: + model: anthropic/claude-3-5-sonnet-20241022 + api_key: os.environ/ANTHROPIC_API_KEY + custom_user_agent: "Claude Code/1.0" +``` + +### Option 2: Global Environment Variable + +Set the `ANTHROPIC_USER_AGENT` environment variable to apply a custom User-Agent to all Anthropic requests: + +```bash +export ANTHROPIC_USER_AGENT="Claude Code/1.0" +``` + +Then start your proxy: + +```bash +litellm --config /path/to/config.yaml +``` + +### Option 3: Via Extra Headers + +You can also set the User-Agent through `extra_headers`: + +```yaml +model_list: + - model_name: claude-code + litellm_params: + model: anthropic/claude-3-5-sonnet-20241022 + api_key: os.environ/ANTHROPIC_API_KEY + extra_headers: + User-Agent: "Claude Code/1.0" +``` + +## Priority Order + +If multiple User-Agent configurations are present, they are applied in this priority order: + +1. **`custom_user_agent` parameter** (highest priority) +2. **`ANTHROPIC_USER_AGENT` environment variable** +3. **`User-Agent` in `extra_headers`** +4. **Default `litellm/{version}`** (lowest priority) + +## Complete Example + +See [`anthropic_custom_user_agent_config.yaml`](./anthropic_custom_user_agent_config.yaml) for a complete working example. + +## Testing + +To verify your configuration works: + +```bash +curl -X POST http://localhost:4000/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-proxy-key" \ + -d '{ + "model": "claude-code", + "messages": [{"role": "user", "content": "Hello!"}] + }' +``` + +## Python SDK Usage + +This feature also works when using LiteLLM as a Python SDK: + +```python +import litellm + +# Option 1: Via parameter +response = litellm.completion( + model="anthropic/claude-3-5-sonnet-20241022", + messages=[{"role": "user", "content": "Hello"}], + custom_user_agent="Claude Code/1.0" +) + +# Option 2: Via environment variable +import os +os.environ["ANTHROPIC_USER_AGENT"] = "Claude Code/1.0" +response = litellm.completion( + model="anthropic/claude-3-5-sonnet-20241022", + messages=[{"role": "user", "content": "Hello"}] +) + +# Option 3: Via extra_headers +response = litellm.completion( + model="anthropic/claude-3-5-sonnet-20241022", + messages=[{"role": "user", "content": "Hello"}], + extra_headers={"User-Agent": "Claude Code/1.0"} +) +``` + +## Related + +- GitHub Issue: [#19017](https://github.com/BerriAI/litellm/issues/19017) +- Anthropic API Documentation: https://docs.anthropic.com/ diff --git a/litellm/proxy/example_config_yaml/anthropic_custom_user_agent_config.yaml b/litellm/proxy/example_config_yaml/anthropic_custom_user_agent_config.yaml new file mode 100644 index 00000000000..d5a6ee56a98 --- /dev/null +++ b/litellm/proxy/example_config_yaml/anthropic_custom_user_agent_config.yaml @@ -0,0 +1,39 @@ +# Example configuration for custom User-Agent header with Anthropic +# +# This is useful when you have Anthropic credentials that are restricted +# to specific User-Agent values (e.g., Claude Code credentials) +# +# See: https://github.com/BerriAI/litellm/issues/19017 + +model_list: + # Option 1: Set custom User-Agent per model + - model_name: claude-code + litellm_params: + model: anthropic/claude-3-5-sonnet-20241022 + api_key: os.environ/ANTHROPIC_API_KEY + # Custom User-Agent to avoid credential blocks + custom_user_agent: "Claude Code/1.0" + + # Option 2: Use default litellm User-Agent (no custom_user_agent specified) + - model_name: claude-regular + litellm_params: + model: anthropic/claude-3-5-sonnet-20241022 + api_key: os.environ/ANTHROPIC_API_KEY_REGULAR + # Will use default "litellm/{version}" User-Agent + + # Option 3: Set via environment variable (affects all Anthropic models) + # Set: export ANTHROPIC_USER_AGENT="Claude Code/1.0" + - model_name: claude-env + litellm_params: + model: anthropic/claude-3-5-sonnet-20241022 + api_key: os.environ/ANTHROPIC_API_KEY + # ANTHROPIC_USER_AGENT env var will be used if set + +# Alternative: Use extra_headers for complete control +model_list_with_extra_headers: + - model_name: claude-extra-headers + litellm_params: + model: anthropic/claude-3-5-sonnet-20241022 + api_key: os.environ/ANTHROPIC_API_KEY + extra_headers: + User-Agent: "Claude Code/1.0" diff --git a/tests/proxy_unit_tests/test_anthropic_custom_user_agent.py b/tests/proxy_unit_tests/test_anthropic_custom_user_agent.py new file mode 100644 index 00000000000..0925afd51b5 --- /dev/null +++ b/tests/proxy_unit_tests/test_anthropic_custom_user_agent.py @@ -0,0 +1,156 @@ +""" +Test custom User-Agent configuration for Anthropic provider in LiteLLM proxy. +""" +import os +import sys + +sys.path.insert(0, os.path.abspath("../..")) + +import pytest +from litellm.llms.anthropic.common_utils import AnthropicModelInfo +from litellm.llms.anthropic.chat.transformation import AnthropicConfig + + +def test_anthropic_config_supports_custom_user_agent(): + """Test that custom_user_agent is in supported params for Anthropic""" + config = AnthropicConfig() + supported_params = config.get_supported_openai_params("claude-3-5-sonnet-20241022") + + assert "custom_user_agent" in supported_params, \ + "custom_user_agent should be in supported OpenAI params" + + +def test_proxy_litellm_params_with_custom_user_agent(): + """Test that custom_user_agent from litellm_params is properly set""" + config = AnthropicModelInfo() + custom_agent = "Claude Code/1.0" + + # Simulate litellm_params from proxy YAML config + litellm_params = { + "model": "anthropic/claude-3-5-sonnet-20241022", + "api_key": "sk-ant-test-key", + "custom_user_agent": custom_agent, + } + + updated_headers = config.validate_environment( + headers={}, + model="claude-3-5-sonnet-20241022", + messages=[{"role": "user", "content": "Hello"}], + optional_params={"custom_user_agent": custom_agent}, + litellm_params={}, + api_key="sk-ant-test-key", + api_base=None, + ) + + assert updated_headers["User-Agent"] == custom_agent, \ + f"Expected User-Agent to be '{custom_agent}', got '{updated_headers.get('User-Agent')}'" + + +def test_proxy_env_var_anthropic_user_agent(): + """Test that ANTHROPIC_USER_AGENT environment variable works""" + config = AnthropicModelInfo() + custom_agent = "Claude Code Proxy/1.0" + + os.environ["ANTHROPIC_USER_AGENT"] = custom_agent + + try: + updated_headers = config.validate_environment( + headers={}, + model="claude-3-5-sonnet-20241022", + messages=[{"role": "user", "content": "Hello"}], + optional_params={}, + litellm_params={}, + api_key="sk-ant-test-key", + api_base=None, + ) + + assert updated_headers["User-Agent"] == custom_agent, \ + f"Expected User-Agent to be '{custom_agent}', got '{updated_headers.get('User-Agent')}'" + finally: + del os.environ["ANTHROPIC_USER_AGENT"] + + +def test_proxy_extra_headers_user_agent(): + """Test that User-Agent can be set via extra_headers""" + config = AnthropicModelInfo() + custom_agent = "Claude Code via Extra Headers/1.0" + + # Headers would come from extra_headers in litellm_params + headers = {"User-Agent": custom_agent} + + updated_headers = config.validate_environment( + headers=headers, + model="claude-3-5-sonnet-20241022", + messages=[{"role": "user", "content": "Hello"}], + optional_params={}, + litellm_params={}, + api_key="sk-ant-test-key", + api_base=None, + ) + + # Should preserve the User-Agent from extra_headers when no custom_user_agent is set + assert updated_headers["User-Agent"] == custom_agent, \ + f"Expected User-Agent to be '{custom_agent}', got '{updated_headers.get('User-Agent')}'" + + +def test_proxy_priority_order(): + """Test priority: custom_user_agent > ANTHROPIC_USER_AGENT > extra_headers""" + config = AnthropicModelInfo() + + param_agent = "Param Agent" + env_agent = "Env Agent" + header_agent = "Header Agent" + + os.environ["ANTHROPIC_USER_AGENT"] = env_agent + + try: + # Test 1: custom_user_agent parameter overrides environment variable + updated_headers = config.validate_environment( + headers={"User-Agent": header_agent}, + model="claude-3-5-sonnet-20241022", + messages=[{"role": "user", "content": "Hello"}], + optional_params={"custom_user_agent": param_agent}, + litellm_params={}, + api_key="sk-ant-test-key", + api_base=None, + ) + + assert updated_headers["User-Agent"] == param_agent, \ + "custom_user_agent parameter should have highest priority" + + # Test 2: Environment variable overrides extra_headers + updated_headers = config.validate_environment( + headers={"User-Agent": header_agent}, + model="claude-3-5-sonnet-20241022", + messages=[{"role": "user", "content": "Hello"}], + optional_params={}, + litellm_params={}, + api_key="sk-ant-test-key", + api_base=None, + ) + + assert updated_headers["User-Agent"] == env_agent, \ + "ANTHROPIC_USER_AGENT env var should override extra_headers" + + finally: + del os.environ["ANTHROPIC_USER_AGENT"] + + +if __name__ == "__main__": + print("Running Anthropic custom User-Agent proxy tests...") + test_anthropic_config_supports_custom_user_agent() + print("✓ Test 1 passed: custom_user_agent is supported") + + test_proxy_litellm_params_with_custom_user_agent() + print("✓ Test 2 passed: custom_user_agent from litellm_params works") + + test_proxy_env_var_anthropic_user_agent() + print("✓ Test 3 passed: ANTHROPIC_USER_AGENT env var works") + + test_proxy_extra_headers_user_agent() + print("✓ Test 4 passed: User-Agent via extra_headers works") + + test_proxy_priority_order() + print("✓ Test 5 passed: Priority order is correct") + + print("\n✅ All proxy tests passed!")