Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions litellm/llms/anthropic/batches/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,19 @@ def validate_environment(
api_base: Optional[str] = None,
) -> dict:
"""Validate and prepare environment-specific headers and parameters."""
from ..common_utils import set_anthropic_headers

# 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:
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"
)
_headers = {
_headers = set_anthropic_headers(api_key, {
"accept": "application/json",
"anthropic-version": "2023-06-01",
"content-type": "application/json",
"x-api-key": api_key,
}
})
# Add beta header for message batches
if "anthropic-beta" not in headers:
headers["anthropic-beta"] = "message-batches-2024-09-24"
Expand Down
71 changes: 62 additions & 9 deletions litellm/llms/anthropic/common_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,59 @@
from litellm.types.llms.openai import AllMessageValues


def is_anthropic_oauth_key(value: Optional[str]) -> bool:
"""
Check if a value is an Anthropic OAuth token.

Handles both formats:
- "Bearer sk-ant-oat01-xxx" (Authorization header format)
- "sk-ant-oat01-xxx" (raw API key format)
"""
if not value:
return False

# Handle "Bearer sk-ant-oat01-xxx" format
scheme, _, token = value.partition(" ")
if scheme.lower() == "bearer" and token.startswith(ANTHROPIC_OAUTH_TOKEN_PREFIX):
return True

# Handle raw token "sk-ant-oat01-xxx" format
return value.startswith(ANTHROPIC_OAUTH_TOKEN_PREFIX)


def set_anthropic_headers(api_key: str, headers: Optional[dict] = None) -> dict:
"""
Create headers dict with appropriate auth header for Anthropic API requests.

OAuth tokens use the Authorization header, regular API keys use x-api-key.

Args:
api_key: The API key or OAuth token
headers: Optional base headers to merge with auth header

Returns:
New dict with auth header and any base headers merged
"""
result = dict(headers) if headers else {}
if is_anthropic_oauth_key(api_key):
if api_key.lower().startswith("bearer "):
result["authorization"] = api_key
else:
result["authorization"] = f"Bearer {api_key}"
else:
result["x-api-key"] = api_key

return result


def optionally_handle_anthropic_oauth(
headers: dict, api_key: Optional[str]
) -> tuple[dict, Optional[str]]:
"""
Handle Anthropic OAuth token detection and header setup.

If an OAuth token is detected in the Authorization header, extracts it
and sets the required OAuth headers.
Checks both the Authorization header and the api_key parameter for OAuth tokens.
If found, sets the required OAuth beta headers.

Args:
headers: Request headers dict
Expand All @@ -38,14 +83,22 @@ def optionally_handle_anthropic_oauth(
Returns:
Tuple of (updated headers, api_key)
"""
auth_header = headers.get("authorization", "")
if auth_header and auth_header.startswith(f"Bearer {ANTHROPIC_OAUTH_TOKEN_PREFIX}"):
# Check Authorization header first (case-insensitive lookup)
auth_header = ""
for key, value in headers.items():
if key.lower() == "authorization":
auth_header = value
break
if auth_header and is_anthropic_oauth_key(auth_header):
api_key = auth_header.replace("Bearer ", "")
headers["anthropic-beta"] = ANTHROPIC_OAUTH_BETA_HEADER
headers["anthropic-dangerous-direct-browser-access"] = "true"
# Also check if api_key is directly an OAuth token
elif api_key and is_anthropic_oauth_key(api_key):
headers["anthropic-beta"] = ANTHROPIC_OAUTH_BETA_HEADER
headers["anthropic-dangerous-direct-browser-access"] = "true"
return headers, api_key


class AnthropicError(BaseLLMException):
def __init__(
self,
Expand Down Expand Up @@ -366,12 +419,11 @@ def get_anthropic_headers(
if container_with_skills_used:
betas.add("skills-2025-10-02")

headers = {
headers = set_anthropic_headers(api_key, {
"anthropic-version": anthropic_version or "2023-06-01",
"x-api-key": api_key,
"accept": "application/json",
"content-type": "application/json",
}
})

if user_anthropic_beta_headers is not None:
betas.update(user_anthropic_beta_headers)
Expand Down Expand Up @@ -475,9 +527,10 @@ def get_models(
raise ValueError(
"ANTHROPIC_API_BASE or ANTHROPIC_API_KEY is not set. Please set the environment variable, to query Anthropic's `/models` endpoint."
)
headers = set_anthropic_headers(api_key, {"anthropic-version": "2023-06-01"})
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:
Expand Down
7 changes: 4 additions & 3 deletions litellm/llms/anthropic/completion/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,17 @@ def validate_environment(
api_key: Optional[str] = None,
api_base: Optional[str] = None,
) -> dict:
from litellm.llms.anthropic.common_utils import set_anthropic_headers

if api_key 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"
)
_headers = {
_headers = set_anthropic_headers(api_key, {
"accept": "application/json",
"anthropic-version": "2023-06-01",
"content-type": "application/json",
"x-api-key": api_key,
}
})
headers.update(_headers)
return headers

Expand Down
6 changes: 3 additions & 3 deletions litellm/llms/anthropic/count_tokens/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import Any, Dict, List

from litellm.constants import ANTHROPIC_TOKEN_COUNTING_BETA_VERSION
from litellm.llms.anthropic.common_utils import set_anthropic_headers


class AnthropicCountTokensConfig:
Expand Down Expand Up @@ -63,12 +64,11 @@ def get_required_headers(self, api_key: str) -> Dict[str, str]:
Returns:
Dictionary of required headers
"""
return {
return set_anthropic_headers(api_key, {
"Content-Type": "application/json",
"x-api-key": api_key,
"anthropic-version": "2023-06-01",
"anthropic-beta": ANTHROPIC_TOKEN_COUNTING_BETA_VERSION,
}
})

def validate_request(
self, model: str, messages: List[Dict[str, Any]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
AnthropicError,
AnthropicModelInfo,
optionally_handle_anthropic_oauth,
set_anthropic_headers,
)

DEFAULT_ANTHROPIC_API_BASE = "https://api.anthropic.com"
Expand Down Expand Up @@ -78,8 +79,10 @@ def validate_anthropic_messages_environment(
if api_key is None:
api_key = os.getenv("ANTHROPIC_API_KEY")

if "x-api-key" not in headers and api_key:
headers["x-api-key"] = api_key
# Case-insensitive check for existing auth headers
headers_lower = {k.lower(): v for k, v in headers.items()}
if "x-api-key" not in headers_lower and "authorization" not in headers_lower and api_key:
headers.update(set_anthropic_headers(api_key))
if "anthropic-version" not in headers:
headers["anthropic-version"] = DEFAULT_ANTHROPIC_API_VERSION
if "content-type" not in headers:
Expand Down
7 changes: 3 additions & 4 deletions litellm/llms/anthropic/files/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from litellm.types.utils import CallTypes, LlmProviders, ModelResponse

from ..chat.transformation import AnthropicConfig
from ..common_utils import AnthropicModelInfo
from ..common_utils import AnthropicModelInfo, set_anthropic_headers

# Map Anthropic error types to HTTP status codes
ANTHROPIC_ERROR_STATUS_CODE_MAP = {
Expand Down Expand Up @@ -94,11 +94,10 @@ async def afile_content(
results_url = f"{api_base.rstrip('/')}/v1/messages/batches/{batch_id}/results"

# Prepare headers
headers = {
headers = set_anthropic_headers(api_key, {
"accept": "application/json",
"anthropic-version": "2023-06-01",
"x-api-key": api_key,
}
})

# Make the request to Anthropic
async_client = get_async_httpx_client(llm_provider=LlmProviders.ANTHROPIC)
Expand Down
4 changes: 2 additions & 2 deletions litellm/llms/anthropic/skills/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def validate_environment(
self, headers: dict, litellm_params: Optional[GenericLiteLLMParams]
) -> dict:
"""Add Anthropic-specific headers"""
from litellm.llms.anthropic.common_utils import AnthropicModelInfo
from litellm.llms.anthropic.common_utils import AnthropicModelInfo, set_anthropic_headers

# Get API key
api_key = None
Expand All @@ -45,7 +45,7 @@ def validate_environment(
raise ValueError("ANTHROPIC_API_KEY is required for Skills API")

# Add required headers
headers["x-api-key"] = api_key
headers.update(set_anthropic_headers(api_key))
headers["anthropic-version"] = "2023-06-01"

# Add beta header for skills API
Expand Down
5 changes: 5 additions & 0 deletions litellm/proxy/litellm_pre_call_utils.py
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clean_headers function should be modified too, when you want to use the router for your anthropic llm calls (non-passthrough) - the code flow goes through clean_headers which should be modified to pass the auth header through

def clean_headers(
    headers: Headers, litellm_key_header_name: Optional[str] = None
) -> dict:
    """
    Removes litellm api key from headers
    """
    clean_headers = {}
    litellm_key_lower = (
        litellm_key_header_name.lower() if litellm_key_header_name is not None else None
    )

    for header, value in headers.items():
        header_lower = header.lower()
        # Preserve Authorization header if it contains Anthropic OAuth token (sk-ant-oat*)
        # This allows OAuth tokens to be forwarded to Anthropic-compatible providers
        # via add_provider_specific_headers_to_request()
        if header_lower == "authorization" and is_anthropic_oauth_key(value):
            clean_headers[header] = value
        # Check if header should be excluded: either in special headers cache or matches custom litellm key
        elif header_lower not in _SPECIAL_HEADERS_CACHE and (
            litellm_key_lower is None or header_lower != litellm_key_lower
        ):
            clean_headers[header] = value
    return clean_headers

Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
)
from litellm.proxy.auth.route_checks import RouteChecks
from litellm.router import Router
from litellm.llms.anthropic.common_utils import is_anthropic_oauth_key
from litellm.types.llms.anthropic import ANTHROPIC_API_HEADERS
from litellm.types.services import ServiceTypes
from litellm.types.utils import (
Expand Down Expand Up @@ -320,6 +321,7 @@ def _get_forwardable_headers(
Looks for any `x-` headers and sends them to the LLM Provider.

[07/09/2025] - Support 'anthropic-beta' header as well.
[01/26/2026] - Support 'authorization' header for Anthropic OAuth tokens.
"""
forwarded_headers = {}
for header, value in headers.items():
Expand All @@ -329,6 +331,9 @@ def _get_forwardable_headers(
forwarded_headers[header] = value
elif header.lower().startswith("anthropic-beta"):
forwarded_headers[header] = value
elif header.lower() == "authorization" and is_anthropic_oauth_key(value):
# Forward Authorization header for Anthropic OAuth tokens (sk-ant-oat*)
forwarded_headers[header] = value
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be the wrong place to add the header, if you have multiple providers listed in your router, it will forward the authentication header to all the providers listed in your router.

Authorization headers with Anthropic OAuth tokens (sk-ant-oat*) should be
        handled separately via add_provider_specific_headers_to_request() to ensure
        they only go to Anthropic-compatible providers.

function that should be modified is add_provider_specific_headers_to_request and it should be like:

def add_provider_specific_headers_to_request(
    data: dict,
    headers: dict,
):
    anthropic_headers = {}
    # boolean to indicate if a header was added
    added_header = False
    for header in ANTHROPIC_API_HEADERS:
        if header in headers:
            header_value = headers[header]
            anthropic_headers[header] = header_value
            added_header = True

    # Check for Authorization header with Anthropic OAuth token (sk-ant-oat*)
    # This needs to be handled via provider-specific headers to ensure it only
    # goes to Anthropic-compatible providers, not all providers
    for header, value in headers.items():
        if header.lower() == "authorization" and is_anthropic_oauth_key(value):
            anthropic_headers[header] = value
            added_header = True
            break

    if added_header is True:
        # Anthropic headers work across multiple providers
        # Store as comma-separated list so retrieval can match any of them
        data["provider_specific_header"] = ProviderSpecificHeader(
            custom_llm_provider=f"{LlmProviders.ANTHROPIC.value},{LlmProviders.BEDROCK.value},{LlmProviders.VERTEX_AI.value}",
            extra_headers=anthropic_headers,
        )

    return


return forwarded_headers

Expand Down
29 changes: 24 additions & 5 deletions litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,20 +594,39 @@ async def anthropic_proxy_route(
base_url = httpx.URL(base_target_url)
updated_url = base_url.copy_with(path=encoded_endpoint)

# Add or update query parameters
anthropic_api_key = passthrough_endpoint_router.get_credentials(
custom_llm_provider="anthropic",
region_name=None,
# Check for OAuth token in incoming request, otherwise use stored credentials
from litellm.llms.anthropic.common_utils import (
optionally_handle_anthropic_oauth,
set_anthropic_headers,
)

incoming_headers = dict(request.headers)
oauth_headers, oauth_api_key = optionally_handle_anthropic_oauth(
headers=incoming_headers, api_key=None
)

if oauth_api_key:
# OAuth token found - use Authorization header with OAuth beta headers
custom_headers = set_anthropic_headers(oauth_api_key)
custom_headers["anthropic-dangerous-direct-browser-access"] = oauth_headers.get(
"anthropic-dangerous-direct-browser-access", "true"
)
else:
# No OAuth token - use stored API key with x-api-key
anthropic_api_key = passthrough_endpoint_router.get_credentials(
custom_llm_provider="anthropic",
region_name=None,
)
custom_headers = {"x-api-key": "{}".format(anthropic_api_key)}

## check for streaming
is_streaming_request = await is_streaming_request_fn(request)

## CREATE PASS-THROUGH
endpoint_func = create_pass_through_route(
endpoint=endpoint,
target=str(updated_url),
custom_headers={"x-api-key": "{}".format(anthropic_api_key)},
custom_headers=custom_headers,
_forward_headers=True,
is_streaming_request=is_streaming_request,
) # dynamically construct pass-through endpoint based on incoming path
Expand Down
Loading
Loading