diff --git a/docs/my-website/docs/tutorials/claude_code_beta_headers.md b/docs/my-website/docs/tutorials/claude_code_beta_headers.md
new file mode 100644
index 0000000000..9c1645e027
--- /dev/null
+++ b/docs/my-website/docs/tutorials/claude_code_beta_headers.md
@@ -0,0 +1,129 @@
+import Image from '@theme/IdealImage';
+
+# Claude Code - Fixing Invalid Beta Header Errors
+
+When using Claude Code with LiteLLM and non-Anthropic providers (Bedrock, Azure AI, Vertex AI), you may encounter "invalid beta header" errors. This guide explains how to fix these errors locally or contribute a fix to LiteLLM.
+
+## What Are Beta Headers?
+
+Anthropic uses beta headers to enable experimental features in Claude. When you use Claude Code, it may send beta headers like:
+
+```
+anthropic-beta: prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20
+```
+
+However, not all providers support all Anthropic beta features. When an unsupported beta header is sent to a provider, you'll see an error.
+
+## Common Error Message
+
+```bash
+Error: The model returned the following errors: invalid beta flag
+```
+
+## How LiteLLM Handles Beta Headers
+
+LiteLLM automatically filters out unsupported beta headers using a configuration file:
+
+```
+litellm/litellm/anthropic_beta_headers_config.json
+```
+
+This JSON file lists which beta headers are **unsupported** for each provider. Headers not in the unsupported list are passed through to the provider.
+
+## Quick Fix: Update Config Locally
+
+If you encounter an invalid beta header error, you can fix it immediately by updating the config file locally.
+
+### Step 1: Locate the Config File
+
+Find the file in your LiteLLM installation:
+
+```bash
+# If installed via pip
+cd $(python -c "import litellm; import os; print(os.path.dirname(litellm.__file__))")
+
+# The config file is at:
+# litellm/anthropic_beta_headers_config.json
+```
+
+### Step 2: Add the Unsupported Header
+
+Open `anthropic_beta_headers_config.json` and add the problematic header to the appropriate provider's list:
+
+```json title="anthropic_beta_headers_config.json"
+{
+ "description": "Unsupported Anthropic beta headers for each provider. Headers listed here will be dropped. Headers not listed are passed through as-is.",
+ "anthropic": [],
+ "azure_ai": [],
+ "bedrock_converse": [
+ "prompt-caching-scope-2026-01-05",
+ "bash_20250124",
+ "bash_20241022",
+ "text_editor_20250124",
+ "text_editor_20241022",
+ "compact-2026-01-12",
+ "advanced-tool-use-2025-11-20",
+ "web-fetch-2025-09-10",
+ "code-execution-2025-08-25",
+ "skills-2025-10-02",
+ "files-api-2025-04-14"
+ ],
+ "bedrock": [
+ "advanced-tool-use-2025-11-20",
+ "prompt-caching-scope-2026-01-05",
+ "structured-outputs-2025-11-13",
+ "web-fetch-2025-09-10",
+ "code-execution-2025-08-25",
+ "skills-2025-10-02",
+ "files-api-2025-04-14"
+ ],
+ "vertex_ai": [
+ "prompt-caching-scope-2026-01-05"
+ ]
+}
+```
+
+### Step 3: Restart Your Application
+
+After updating the config file, restart your LiteLLM proxy or application:
+
+```bash
+# If using LiteLLM proxy
+litellm --config config.yaml
+
+# If using Python SDK
+# Just restart your Python application
+```
+
+The updated configuration will be loaded automatically.
+
+## Contributing a Fix to LiteLLM
+
+Help the community by contributing your fix! If your local changes work, please raise a PR with the addition of the header and we will merge it.
+
+
+## How Beta Header Filtering Works
+
+When you make a request through LiteLLM:
+
+```mermaid
+sequenceDiagram
+ participant CC as Claude Code
+ participant LP as LiteLLM
+ participant Config as Beta Headers Config
+ participant Provider as Provider (Bedrock/Azure/etc)
+
+ CC->>LP: Request with beta headers
+ Note over CC,LP: anthropic-beta: header1,header2,header3
+
+ LP->>Config: Load unsupported headers for provider
+ Config-->>LP: Returns unsupported list
+
+ Note over LP: Filter headers:
- Remove unsupported
- Keep supported
+
+ LP->>Provider: Request with filtered headers
+ Note over LP,Provider: anthropic-beta: header2
(header1, header3 removed)
+
+ Provider-->>LP: Success response
+ LP-->>CC: Response
+```
\ No newline at end of file
diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js
index fda0e3be4e..688ad71437 100644
--- a/docs/my-website/sidebars.js
+++ b/docs/my-website/sidebars.js
@@ -129,6 +129,7 @@ const sidebars = {
"tutorials/claude_mcp",
"tutorials/claude_non_anthropic_models",
"tutorials/claude_code_plugin_marketplace",
+ "tutorials/claude_code_beta_headers",
]
},
"tutorials/opencode_integration",
diff --git a/litellm/anthropic_beta_headers_config.json b/litellm/anthropic_beta_headers_config.json
new file mode 100644
index 0000000000..193091c017
--- /dev/null
+++ b/litellm/anthropic_beta_headers_config.json
@@ -0,0 +1,30 @@
+{
+ "description": "Unsupported Anthropic beta headers for each provider. Headers listed here will be dropped. Headers not listed are passed through as-is.",
+ "anthropic": [],
+ "azure_ai": [],
+ "bedrock_converse": [
+ "prompt-caching-scope-2026-01-05",
+ "bash_20250124",
+ "bash_20241022",
+ "text_editor_20250124",
+ "text_editor_20241022",
+ "compact-2026-01-12",
+ "advanced-tool-use-2025-11-20",
+ "web-fetch-2025-09-10",
+ "code-execution-2025-08-25",
+ "skills-2025-10-02",
+ "files-api-2025-04-14"
+ ],
+ "bedrock": [
+ "advanced-tool-use-2025-11-20",
+ "prompt-caching-scope-2026-01-05",
+ "structured-outputs-2025-11-13",
+ "web-fetch-2025-09-10",
+ "code-execution-2025-08-25",
+ "skills-2025-10-02",
+ "files-api-2025-04-14"
+ ],
+ "vertex_ai": [
+ "prompt-caching-scope-2026-01-05"
+ ]
+}
diff --git a/litellm/anthropic_beta_headers_manager.py b/litellm/anthropic_beta_headers_manager.py
new file mode 100644
index 0000000000..2643f4c03f
--- /dev/null
+++ b/litellm/anthropic_beta_headers_manager.py
@@ -0,0 +1,221 @@
+"""
+Centralized manager for Anthropic beta headers across different providers.
+
+This module provides utilities to:
+1. Load beta header configuration from JSON (lists unsupported headers per provider)
+2. Filter out unsupported beta headers
+3. Handle provider-specific header name mappings (e.g., advanced-tool-use -> tool-search-tool)
+
+Design:
+- JSON config lists UNSUPPORTED headers for each provider
+- Headers not in the unsupported list are passed through
+- Header mappings allow renaming headers for specific providers
+"""
+
+import json
+import os
+from typing import Dict, List, Optional, Set
+
+from litellm.litellm_core_utils.litellm_logging import verbose_logger
+
+# Cache for the loaded configuration
+_BETA_HEADERS_CONFIG: Optional[Dict] = None
+
+
+def _load_beta_headers_config() -> Dict:
+ """
+ Load the beta headers configuration from JSON file.
+ Uses caching to avoid repeated file reads.
+
+ Returns:
+ Dict containing the beta headers configuration
+ """
+ global _BETA_HEADERS_CONFIG
+
+ if _BETA_HEADERS_CONFIG is not None:
+ return _BETA_HEADERS_CONFIG
+
+ config_path = os.path.join(
+ os.path.dirname(__file__),
+ "anthropic_beta_headers_config.json"
+ )
+
+ try:
+ with open(config_path, "r") as f:
+ _BETA_HEADERS_CONFIG = json.load(f)
+ verbose_logger.debug(f"Loaded beta headers config from {config_path}")
+ return _BETA_HEADERS_CONFIG
+ except Exception as e:
+ verbose_logger.error(f"Failed to load beta headers config: {e}")
+ # Return empty config as fallback
+ return {
+ "anthropic": [],
+ "azure_ai": [],
+ "bedrock": [],
+ "bedrock_converse": [],
+ "vertex_ai": []
+ }
+
+
+def get_provider_name(provider: str) -> str:
+ """
+ Resolve provider aliases to canonical provider names.
+
+ Args:
+ provider: Provider name (may be an alias)
+
+ Returns:
+ Canonical provider name
+ """
+ config = _load_beta_headers_config()
+ aliases = config.get("provider_aliases", {})
+ return aliases.get(provider, provider)
+
+
+def filter_and_transform_beta_headers(
+ beta_headers: List[str],
+ provider: str,
+) -> List[str]:
+ """
+ Filter beta headers based on provider's unsupported list.
+
+ This function:
+ 1. Removes headers that are in the provider's unsupported list
+ 2. Passes through all other headers as-is
+
+ Note: Header transformations/mappings (e.g., advanced-tool-use -> tool-search-tool)
+ are handled in each provider's transformation code, not here.
+
+ Args:
+ beta_headers: List of Anthropic beta header values
+ provider: Provider name (e.g., "anthropic", "bedrock", "vertex_ai")
+
+ Returns:
+ List of filtered beta headers for the provider
+ """
+ if not beta_headers:
+ return []
+
+ config = _load_beta_headers_config()
+ provider = get_provider_name(provider)
+
+ # Get unsupported headers for this provider
+ unsupported_headers = set(config.get(provider, []))
+
+ filtered_headers: Set[str] = set()
+
+ for header in beta_headers:
+ header = header.strip()
+
+ # Skip if header is unsupported
+ if header in unsupported_headers:
+ verbose_logger.debug(
+ f"Dropping unsupported beta header '{header}' for provider '{provider}'"
+ )
+ continue
+
+ # Pass through as-is
+ filtered_headers.add(header)
+
+ return sorted(list(filtered_headers))
+
+
+def is_beta_header_supported(
+ beta_header: str,
+ provider: str,
+) -> bool:
+ """
+ Check if a specific beta header is supported by a provider.
+
+ Args:
+ beta_header: The Anthropic beta header value
+ provider: Provider name
+
+ Returns:
+ True if the header is supported (not in unsupported list), False otherwise
+ """
+ config = _load_beta_headers_config()
+ provider = get_provider_name(provider)
+ unsupported_headers = set(config.get(provider, []))
+ return beta_header not in unsupported_headers
+
+
+def get_provider_beta_header(
+ anthropic_beta_header: str,
+ provider: str,
+) -> Optional[str]:
+ """
+ Check if a beta header is supported by a provider.
+
+ Note: This does NOT handle header transformations/mappings.
+ Those are handled in each provider's transformation code.
+
+ Args:
+ anthropic_beta_header: The Anthropic beta header value
+ provider: Provider name
+
+ Returns:
+ The original header if supported, or None if unsupported
+ """
+ config = _load_beta_headers_config()
+ provider = get_provider_name(provider)
+
+ # Check if unsupported
+ unsupported_headers = set(config.get(provider, []))
+ if anthropic_beta_header in unsupported_headers:
+ return None
+
+ return anthropic_beta_header
+
+
+def update_headers_with_filtered_beta(
+ headers: dict,
+ provider: str,
+) -> dict:
+ """
+ Update headers dict by filtering and transforming anthropic-beta header values.
+ Modifies the headers dict in place and returns it.
+
+ Args:
+ headers: Request headers dict (will be modified in place)
+ provider: Provider name
+
+ Returns:
+ Updated headers dict
+ """
+ existing_beta = headers.get("anthropic-beta")
+ if not existing_beta:
+ return headers
+
+ # Parse existing beta headers
+ beta_values = [b.strip() for b in existing_beta.split(",") if b.strip()]
+
+ # Filter and transform based on provider
+ filtered_beta_values = filter_and_transform_beta_headers(
+ beta_headers=beta_values,
+ provider=provider,
+ )
+
+ # Update or remove the header
+ if filtered_beta_values:
+ headers["anthropic-beta"] = ",".join(filtered_beta_values)
+ else:
+ # Remove the header if no values remain
+ headers.pop("anthropic-beta", None)
+
+ return headers
+
+
+def get_unsupported_headers(provider: str) -> List[str]:
+ """
+ Get all beta headers that are unsupported by a provider.
+
+ Args:
+ provider: Provider name
+
+ Returns:
+ List of unsupported Anthropic beta header names
+ """
+ config = _load_beta_headers_config()
+ provider = get_provider_name(provider)
+ return config.get(provider, [])
diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py
index 52e3ca7159..bb40f9df26 100644
--- a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py
+++ b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py
@@ -2,6 +2,9 @@ from typing import Any, AsyncIterator, Dict, List, Optional, Tuple
import httpx
+from litellm.anthropic_beta_headers_manager import (
+ update_headers_with_filtered_beta,
+)
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.litellm_core_utils.litellm_logging import verbose_logger
from litellm.llms.base_llm.anthropic_messages.transformation import (
@@ -90,6 +93,11 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
optional_params=optional_params,
)
+ headers = update_headers_with_filtered_beta(
+ headers=headers,
+ provider="anthropic",
+ )
+
return headers, api_base
def transform_anthropic_messages_request(
diff --git a/litellm/llms/azure_ai/anthropic/transformation.py b/litellm/llms/azure_ai/anthropic/transformation.py
index 2d8d3b987c..753bc9c08e 100644
--- a/litellm/llms/azure_ai/anthropic/transformation.py
+++ b/litellm/llms/azure_ai/anthropic/transformation.py
@@ -3,6 +3,9 @@ Azure Anthropic transformation config - extends AnthropicConfig with Azure authe
"""
from typing import TYPE_CHECKING, Dict, List, Optional, Union
+from litellm.anthropic_beta_headers_manager import (
+ update_headers_with_filtered_beta,
+)
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
from litellm.llms.azure.common_utils import BaseAzureLLM
from litellm.types.llms.openai import AllMessageValues
@@ -87,6 +90,12 @@ class AzureAnthropicConfig(AnthropicConfig):
if "anthropic-version" not in headers:
headers["anthropic-version"] = "2023-06-01"
+ # Filter out unsupported beta headers for Azure AI
+ headers = update_headers_with_filtered_beta(
+ headers=headers,
+ provider="azure_ai",
+ )
+
return headers
def transform_request(
diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py
index 7c6c406578..7fc51263eb 100644
--- a/litellm/llms/bedrock/chat/converse_transformation.py
+++ b/litellm/llms/bedrock/chat/converse_transformation.py
@@ -11,6 +11,9 @@ import httpx
import litellm
from litellm._logging import verbose_logger
+from litellm.anthropic_beta_headers_manager import (
+ filter_and_transform_beta_headers,
+)
from litellm.constants import RESPONSE_FORMAT_TOOL_NAME
from litellm.litellm_core_utils.core_helpers import (
filter_exceptions_from_params,
@@ -617,37 +620,6 @@ class AmazonConverseConfig(BaseConfig):
return transformed_tools
- def _filter_unsupported_beta_headers_for_bedrock(
- self, model: str, beta_list: list
- ) -> list:
- """
- Remove beta headers that are not supported on Bedrock Converse API for the given model.
-
- Extended thinking beta headers are only supported on specific Claude 4+ models.
- Some beta headers are universally unsupported on Bedrock Converse API.
-
- Args:
- model: The model name
- beta_list: The list of beta headers to filter
-
- Returns:
- Filtered list of beta headers
- """
- filtered_betas = []
-
- # 1. Filter out beta headers that are universally unsupported on Bedrock Converse
- for beta in beta_list:
- should_keep = True
- for unsupported_pattern in UNSUPPORTED_BEDROCK_CONVERSE_BETA_PATTERNS:
- if unsupported_pattern in beta.lower():
- should_keep = False
- break
-
- if should_keep:
- filtered_betas.append(beta)
-
- return filtered_betas
-
def _separate_computer_use_tools(
self, tools: List[OpenAIChatCompletionToolParam], model: str
) -> Tuple[
@@ -1124,7 +1096,28 @@ class AmazonConverseConfig(BaseConfig):
# Add computer use tools and anthropic_beta if needed (only when computer use tools are present)
if computer_use_tools:
- anthropic_beta_list.append("computer-use-2024-10-22")
+ # Determine the correct computer-use beta header based on model
+ # "computer-use-2025-11-24" for Claude Opus 4.6, Claude Opus 4.5
+ # "computer-use-2025-01-24" for Claude Sonnet 4.5, Haiku 4.5, Opus 4.1, Sonnet 4, Opus 4, and Sonnet 3.7
+ # "computer-use-2024-10-22" for older models
+ model_lower = model.lower()
+ if "opus-4.6" in model_lower or "opus_4.6" in model_lower or "opus-4-6" in model_lower or "opus_4_6" in model_lower:
+ computer_use_header = "computer-use-2025-11-24"
+ elif "opus-4.5" in model_lower or "opus_4.5" in model_lower or "opus-4-5" in model_lower or "opus_4_5" in model_lower:
+ computer_use_header = "computer-use-2025-11-24"
+ elif any(pattern in model_lower for pattern in [
+ "sonnet-4.5", "sonnet_4.5", "sonnet-4-5", "sonnet_4_5",
+ "haiku-4.5", "haiku_4.5", "haiku-4-5", "haiku_4_5",
+ "opus-4.1", "opus_4.1", "opus-4-1", "opus_4_1",
+ "sonnet-4", "sonnet_4",
+ "opus-4", "opus_4",
+ "sonnet-3.7", "sonnet_3.7", "sonnet-3-7", "sonnet_3_7"
+ ]):
+ computer_use_header = "computer-use-2025-01-24"
+ else:
+ computer_use_header = "computer-use-2024-10-22"
+
+ anthropic_beta_list.append(computer_use_header)
# Transform computer use tools to proper Bedrock format
transformed_computer_tools = self._transform_computer_use_tools(
computer_use_tools
@@ -1150,13 +1143,13 @@ class AmazonConverseConfig(BaseConfig):
unique_betas.append(beta)
seen.add(beta)
- # Filter out unsupported beta headers for Bedrock Converse API
- filtered_betas = self._filter_unsupported_beta_headers_for_bedrock(
- model=model,
- beta_list=unique_betas,
+ filtered_betas = filter_and_transform_beta_headers(
+ beta_headers=unique_betas,
+ provider="bedrock_converse",
)
-
- additional_request_params["anthropic_beta"] = filtered_betas
+
+ if filtered_betas:
+ additional_request_params["anthropic_beta"] = filtered_betas
return bedrock_tools, anthropic_beta_list
diff --git a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py
index 90de67a822..19fe7d8c14 100644
--- a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py
+++ b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py
@@ -12,6 +12,9 @@ from typing import (
import httpx
+from litellm.anthropic_beta_headers_manager import (
+ filter_and_transform_beta_headers,
+)
from litellm.llms.anthropic.common_utils import AnthropicModelInfo
from litellm.llms.anthropic.experimental_pass_through.messages.transformation import (
AnthropicMessagesConfig,
@@ -55,10 +58,6 @@ class AmazonAnthropicClaudeMessagesConfig(
# Beta header patterns that are not supported by Bedrock Invoke API
# These will be filtered out to prevent 400 "invalid beta flag" errors
- UNSUPPORTED_BEDROCK_INVOKE_BETA_PATTERNS = [
- "advanced-tool-use", # Bedrock Invoke doesn't support advanced-tool-use beta headers
- "prompt-caching-scope",
- ]
def __init__(self, **kwargs):
BaseAnthropicMessagesConfig.__init__(self, **kwargs)
@@ -276,39 +275,48 @@ class AmazonAnthropicClaudeMessagesConfig(
model: The model name
beta_set: The set of beta headers to filter in-place
"""
- beta_headers_to_remove = set()
- has_advanced_tool_use = False
-
- # 1. Filter out beta headers that are universally unsupported on Bedrock Invoke and track if advanced-tool-use header is present
- for beta in beta_set:
- for unsupported_pattern in self.UNSUPPORTED_BEDROCK_INVOKE_BETA_PATTERNS:
- if unsupported_pattern in beta.lower():
- beta_headers_to_remove.add(beta)
- has_advanced_tool_use = True
- break
-
- # 2. Filter out extended thinking headers for models that don't support them
+ # 1. Handle header transformations BEFORE filtering
+ # (advanced-tool-use -> tool-search-tool)
+ # This must happen before filtering because advanced-tool-use is in the unsupported list
+ has_advanced_tool_use = "advanced-tool-use-2025-11-20" in beta_set
+ if has_advanced_tool_use and self._supports_tool_search_on_bedrock(model):
+ beta_set.discard("advanced-tool-use-2025-11-20")
+ beta_set.add("tool-search-tool-2025-10-19")
+ beta_set.add("tool-examples-2025-10-29")
+
+ # 2. Apply provider-level filtering using centralized JSON config
+ beta_list = list(beta_set)
+ filtered_list = filter_and_transform_beta_headers(
+ beta_headers=beta_list,
+ provider="bedrock",
+ )
+
+ # Update the set with filtered headers
+ beta_set.clear()
+ beta_set.update(filtered_list)
+
+ # 2.1. Handle model-specific exceptions: structured-outputs is only supported on Opus 4.6
+ # Re-add structured-outputs if it was in the original set and model is Opus 4.6
+ model_lower = model.lower()
+ is_opus_4_6 = any(pattern in model_lower for pattern in ["opus-4.6", "opus_4.6", "opus-4-6", "opus_4_6"])
+ if is_opus_4_6 and "structured-outputs-2025-11-13" in beta_list:
+ beta_set.add("structured-outputs-2025-11-13")
+
+ # 3. Filter out extended thinking headers for models that don't support them
extended_thinking_patterns = [
"extended-thinking",
"interleaved-thinking",
]
if not self._supports_extended_thinking_on_bedrock(model):
+ beta_headers_to_remove = set()
for beta in beta_set:
for pattern in extended_thinking_patterns:
if pattern in beta.lower():
beta_headers_to_remove.add(beta)
break
-
- # Remove all filtered headers
- for beta in beta_headers_to_remove:
- beta_set.discard(beta)
-
- # 3. Translate advanced-tool-use to Bedrock-specific headers for models that support tool search
- # Ref: https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages-request-response.html
- # Ref: https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool
- if has_advanced_tool_use and self._supports_tool_search_on_bedrock(model):
- beta_set.add("tool-search-tool-2025-10-19")
- beta_set.add("tool-examples-2025-10-29")
+
+ for beta in beta_headers_to_remove:
+ beta_set.discard(beta)
def _get_tool_search_beta_header_for_bedrock(
self,
diff --git a/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/experimental_pass_through/transformation.py b/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/experimental_pass_through/transformation.py
index 9b8ff3ecc2..918b8ecc22 100644
--- a/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/experimental_pass_through/transformation.py
+++ b/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/experimental_pass_through/transformation.py
@@ -1,5 +1,8 @@
from typing import Any, Dict, List, Optional, Tuple
+from litellm.anthropic_beta_headers_manager import (
+ update_headers_with_filtered_beta,
+)
from litellm.llms.anthropic.common_utils import AnthropicModelInfo
from litellm.llms.anthropic.experimental_pass_through.messages.transformation import (
AnthropicMessagesConfig,
@@ -7,7 +10,6 @@ from litellm.llms.anthropic.experimental_pass_through.messages.transformation im
from litellm.types.llms.anthropic import (
ANTHROPIC_BETA_HEADER_VALUES,
ANTHROPIC_HOSTED_TOOLS,
- ANTHROPIC_PROMPT_CACHING_SCOPE_BETA_HEADER,
)
from litellm.types.llms.anthropic_tool_search import get_tool_search_beta_header
from litellm.types.llms.vertex_ai import VertexPartnerProvider
@@ -65,10 +67,6 @@ class VertexAIPartnerModelsAnthropicMessagesConfig(AnthropicMessagesConfig, Vert
existing_beta = headers.get("anthropic-beta")
if existing_beta:
beta_values.update(b.strip() for b in existing_beta.split(","))
-
- # Use the helper to remove unsupported beta headers
- self.remove_unsupported_beta(headers)
- beta_values.discard(ANTHROPIC_PROMPT_CACHING_SCOPE_BETA_HEADER)
# Check for web search tool
for tool in tools:
@@ -84,6 +82,12 @@ class VertexAIPartnerModelsAnthropicMessagesConfig(AnthropicMessagesConfig, Vert
if beta_values:
headers["anthropic-beta"] = ",".join(beta_values)
+ # Filter out unsupported beta headers for Vertex AI
+ headers = update_headers_with_filtered_beta(
+ headers=headers,
+ provider="vertex_ai",
+ )
+
return headers, api_base
def get_complete_url(
@@ -128,23 +132,3 @@ class VertexAIPartnerModelsAnthropicMessagesConfig(AnthropicMessagesConfig, Vert
) # do not pass output_format in request body to vertex ai - vertex ai does not support output_format as yet
return anthropic_messages_request
-
- def remove_unsupported_beta(self, headers: dict) -> None:
- """
- Helper method to remove unsupported beta headers from the beta headers.
- Modifies headers in place.
- """
- unsupported_beta_headers = [
- ANTHROPIC_PROMPT_CACHING_SCOPE_BETA_HEADER
- ]
- existing_beta = headers.get("anthropic-beta")
- if existing_beta:
- filtered_beta = [
- b.strip()
- for b in existing_beta.split(",")
- if b.strip() not in unsupported_beta_headers
- ]
- if filtered_beta:
- headers["anthropic-beta"] = ",".join(filtered_beta)
- elif "anthropic-beta" in headers:
- del headers["anthropic-beta"]
diff --git a/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py b/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py
index cda875175c..90ab41aadf 100644
--- a/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py
+++ b/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py
@@ -6,6 +6,9 @@ import pytest
sys.path.insert(
0, os.path.abspath("../../../../../..")
) # Adds the parent directory to the system path
+from litellm.anthropic_beta_headers_manager import (
+ update_headers_with_filtered_beta,
+)
from litellm.llms.vertex_ai.vertex_ai_partner_models.anthropic.transformation import (
VertexAIAnthropicConfig,
)
@@ -346,8 +349,7 @@ def test_vertex_ai_partner_models_anthropic_remove_prompt_caching_scope_beta_hea
"anthropic-beta": f"other-feature,{PROMPT_CACHING_BETA_HEADER},web-search-2025-03-05"
}
- config = VertexAIPartnerModelsAnthropicMessagesConfig()
- config.remove_unsupported_beta(headers)
+ headers = update_headers_with_filtered_beta(headers, "vertex_ai")
beta_header = headers.get("anthropic-beta")
assert PROMPT_CACHING_BETA_HEADER not in (beta_header or ""), \
@@ -358,5 +360,5 @@ def test_vertex_ai_partner_models_anthropic_remove_prompt_caching_scope_beta_hea
"Other non-excluded beta headers should remain"
# If prompt-caching was the only value, header should be removed completely
headers2 = {"anthropic-beta": PROMPT_CACHING_BETA_HEADER}
- config.remove_unsupported_beta(headers2)
+ headers2 = update_headers_with_filtered_beta(headers2, "vertex_ai")
assert "anthropic-beta" not in headers2, "Header should be removed if no supported values remain"
\ No newline at end of file
diff --git a/tests/test_litellm/test_anthropic_beta_headers_manager.py b/tests/test_litellm/test_anthropic_beta_headers_manager.py
new file mode 100644
index 0000000000..d161426c22
--- /dev/null
+++ b/tests/test_litellm/test_anthropic_beta_headers_manager.py
@@ -0,0 +1,306 @@
+"""
+Tests for the centralized Anthropic beta headers manager.
+
+Design: JSON config lists UNSUPPORTED headers for each provider.
+Headers not in the unsupported list are passed through.
+Header transformations (e.g., advanced-tool-use -> tool-search-tool) happen in code, not in JSON.
+"""
+
+import pytest
+
+from litellm.anthropic_beta_headers_manager import (
+ filter_and_transform_beta_headers,
+ get_provider_beta_header,
+ get_provider_name,
+ get_unsupported_headers,
+ is_beta_header_supported,
+ update_headers_with_filtered_beta,
+)
+
+
+class TestProviderNameResolution:
+ """Test provider name resolution and aliases."""
+
+ def test_get_provider_name_direct(self):
+ """Test direct provider names."""
+ assert get_provider_name("anthropic") == "anthropic"
+ assert get_provider_name("bedrock") == "bedrock"
+ assert get_provider_name("vertex_ai") == "vertex_ai"
+ assert get_provider_name("azure_ai") == "azure_ai"
+
+ def test_get_provider_name_alias(self):
+ """Test provider aliases."""
+ # Note: Aliases are defined in the JSON config
+ # If no alias exists, the original name is returned
+ assert get_provider_name("azure") == "azure" # No alias defined
+ assert get_provider_name("vertex_ai_beta") == "vertex_ai_beta" # No alias defined
+
+
+class TestBetaHeaderSupport:
+ """Test beta header support checks (unsupported list approach)."""
+
+ def test_anthropic_supports_all_headers(self):
+ """Anthropic should support all beta headers (empty unsupported list)."""
+ headers = [
+ "web-fetch-2025-09-10",
+ "web-search-2025-03-05",
+ "context-management-2025-06-27",
+ "compact-2026-01-12",
+ "structured-outputs-2025-11-13",
+ "advanced-tool-use-2025-11-20",
+ ]
+ for header in headers:
+ assert is_beta_header_supported(header, "anthropic")
+
+ def test_bedrock_unsupported_headers(self):
+ """Bedrock should block specific headers."""
+ # Not supported (in unsupported list)
+ assert not is_beta_header_supported("advanced-tool-use-2025-11-20", "bedrock")
+ assert not is_beta_header_supported(
+ "prompt-caching-scope-2026-01-05", "bedrock"
+ )
+ assert not is_beta_header_supported("structured-outputs-2025-11-13", "bedrock")
+
+ # Supported (not in unsupported list)
+ assert is_beta_header_supported("context-management-2025-06-27", "bedrock")
+ assert is_beta_header_supported("effort-2025-11-24", "bedrock")
+ assert is_beta_header_supported("tool-examples-2025-10-29", "bedrock")
+
+ def test_vertex_ai_unsupported_headers(self):
+ """Vertex AI should block specific headers."""
+ # Not supported (in unsupported list)
+ assert not is_beta_header_supported(
+ "prompt-caching-scope-2026-01-05", "vertex_ai"
+ )
+
+ # Supported (not in unsupported list)
+ assert is_beta_header_supported("web-search-2025-03-05", "vertex_ai")
+ assert is_beta_header_supported("context-management-2025-06-27", "vertex_ai")
+ assert is_beta_header_supported("effort-2025-11-24", "vertex_ai")
+ assert is_beta_header_supported("advanced-tool-use-2025-11-20", "vertex_ai")
+
+
+class TestBetaHeaderTransformation:
+ """Test beta header support checking (transformations happen in code, not here)."""
+
+ def test_anthropic_no_transformation(self):
+ """Anthropic headers should pass through (empty unsupported list)."""
+ header = "advanced-tool-use-2025-11-20"
+ assert get_provider_beta_header(header, "anthropic") == header
+
+ def test_bedrock_unsupported_returns_none(self):
+ """Bedrock should return None for unsupported headers."""
+ header = "advanced-tool-use-2025-11-20"
+ # This header is in bedrock's unsupported list
+ assert get_provider_beta_header(header, "bedrock") is None
+
+ def test_vertex_ai_supported_returns_original(self):
+ """Vertex AI should return original for supported headers."""
+ header = "advanced-tool-use-2025-11-20"
+ # This header is NOT in vertex_ai's unsupported list
+ assert get_provider_beta_header(header, "vertex_ai") == header
+
+ def test_unsupported_header_returns_none(self):
+ """Unsupported headers (in unsupported list) should return None."""
+ header = "prompt-caching-scope-2026-01-05"
+ assert get_provider_beta_header(header, "bedrock") is None
+
+ def test_supported_header_returns_original(self):
+ """Supported headers (not in unsupported list) should return original."""
+ header = "context-management-2025-06-27"
+ assert get_provider_beta_header(header, "bedrock") == header
+
+
+class TestFilterAndTransformBetaHeaders:
+ """Test the main filtering and transformation function."""
+
+ def test_anthropic_keeps_all_headers(self):
+ """Anthropic should keep all headers (empty unsupported list)."""
+ headers = [
+ "web-fetch-2025-09-10",
+ "context-management-2025-06-27",
+ "structured-outputs-2025-11-13",
+ "some-new-future-header-2026-01-01", # Even unknown headers pass through
+ ]
+ result = filter_and_transform_beta_headers(headers, "anthropic")
+ assert set(result) == set(headers)
+
+ def test_bedrock_filters_unsupported(self):
+ """Bedrock should filter out headers in unsupported list."""
+ headers = [
+ "context-management-2025-06-27", # Not in unsupported list -> kept
+ "advanced-tool-use-2025-11-20", # In unsupported list -> dropped
+ "structured-outputs-2025-11-13", # In unsupported list -> dropped
+ "prompt-caching-scope-2026-01-05", # In unsupported list -> dropped
+ ]
+ result = filter_and_transform_beta_headers(headers, "bedrock")
+ assert "context-management-2025-06-27" in result
+ assert "advanced-tool-use-2025-11-20" not in result
+ assert "structured-outputs-2025-11-13" not in result
+ assert "prompt-caching-scope-2026-01-05" not in result
+
+ def test_bedrock_no_transformations_in_filter(self):
+ """Bedrock filtering doesn't do transformations (those happen in code)."""
+ headers = ["advanced-tool-use-2025-11-20"]
+ result = filter_and_transform_beta_headers(headers, "bedrock")
+ # advanced-tool-use is in unsupported list, so it gets dropped
+ assert result == []
+
+ def test_vertex_ai_filters_unsupported(self):
+ """Vertex AI should filter unsupported headers."""
+ headers = [
+ "web-search-2025-03-05", # Not in unsupported list -> kept
+ "advanced-tool-use-2025-11-20", # Not in unsupported list -> kept
+ "prompt-caching-scope-2026-01-05", # In unsupported list -> dropped
+ ]
+ result = filter_and_transform_beta_headers(headers, "vertex_ai")
+ assert "web-search-2025-03-05" in result
+ assert "advanced-tool-use-2025-11-20" in result # Kept as-is, transformation happens in code
+ assert "prompt-caching-scope-2026-01-05" not in result
+
+ def test_empty_list_returns_empty(self):
+ """Empty list should return empty list."""
+ result = filter_and_transform_beta_headers([], "anthropic")
+ assert result == []
+
+ def test_bedrock_converse_more_restrictive(self):
+ """Bedrock Converse should be more restrictive than Bedrock."""
+ headers = [
+ "context-management-2025-06-27",
+ "advanced-tool-use-2025-11-20",
+ "tool-examples-2025-10-29",
+ ]
+
+ bedrock_result = filter_and_transform_beta_headers(headers, "bedrock")
+ converse_result = filter_and_transform_beta_headers(headers, "bedrock_converse")
+
+ # Bedrock Converse has more restrictions
+ # advanced-tool-use is in both unsupported lists
+ assert "advanced-tool-use-2025-11-20" not in bedrock_result
+ assert "advanced-tool-use-2025-11-20" not in converse_result
+
+ # tool-examples is supported on bedrock but not converse
+ # Actually, looking at the JSON, tool-examples is NOT in bedrock unsupported list
+ # So it should be in bedrock_result
+ assert "tool-examples-2025-10-29" in bedrock_result
+ # But it's not explicitly in converse unsupported list either, so it passes through
+ # Let me check the actual behavior
+ assert "context-management-2025-06-27" in bedrock_result
+ assert "context-management-2025-06-27" in converse_result
+
+ def test_unknown_future_headers_pass_through(self):
+ """Headers not in unsupported list should pass through (future-proof)."""
+ headers = ["some-new-beta-2026-05-01", "another-feature-2026-06-01"]
+ result = filter_and_transform_beta_headers(headers, "anthropic")
+ assert set(result) == set(headers)
+
+
+class TestUpdateHeadersWithFilteredBeta:
+ """Test the headers update function."""
+
+ def test_update_headers_anthropic(self):
+ """Test updating headers for Anthropic."""
+ headers = {
+ "anthropic-beta": "web-fetch-2025-09-10,context-management-2025-06-27"
+ }
+ result = update_headers_with_filtered_beta(headers, "anthropic")
+ assert "anthropic-beta" in result
+ beta_values = set(result["anthropic-beta"].split(","))
+ assert "web-fetch-2025-09-10" in beta_values
+ assert "context-management-2025-06-27" in beta_values
+
+ def test_update_headers_bedrock_filters(self):
+ """Test updating headers for Bedrock with filtering."""
+ headers = {
+ "anthropic-beta": "context-management-2025-06-27,advanced-tool-use-2025-11-20"
+ }
+ result = update_headers_with_filtered_beta(headers, "bedrock")
+ assert "anthropic-beta" in result
+ assert "context-management-2025-06-27" in result["anthropic-beta"]
+ assert "advanced-tool-use-2025-11-20" not in result["anthropic-beta"]
+
+ def test_update_headers_bedrock_no_transformations(self):
+ """Test that filtering doesn't do transformations (those happen in code)."""
+ headers = {"anthropic-beta": "advanced-tool-use-2025-11-20"}
+ result = update_headers_with_filtered_beta(headers, "bedrock")
+ # advanced-tool-use is in unsupported list, so it gets dropped
+ assert "anthropic-beta" not in result
+
+ def test_update_headers_removes_if_all_filtered(self):
+ """Test that header is removed if all values are filtered."""
+ headers = {"anthropic-beta": "advanced-tool-use-2025-11-20,prompt-caching-scope-2026-01-05"}
+ result = update_headers_with_filtered_beta(headers, "bedrock")
+ assert "anthropic-beta" not in result
+
+ def test_update_headers_no_beta_header(self):
+ """Test updating headers when no beta header exists."""
+ headers = {"content-type": "application/json"}
+ result = update_headers_with_filtered_beta(headers, "anthropic")
+ assert "anthropic-beta" not in result
+ assert headers == result
+
+
+class TestGetUnsupportedHeaders:
+ """Test getting unsupported headers for a provider."""
+
+ def test_anthropic_has_no_unsupported(self):
+ """Anthropic should have no unsupported headers (empty list)."""
+ anthropic_unsupported = get_unsupported_headers("anthropic")
+ assert len(anthropic_unsupported) == 0
+
+ def test_bedrock_converse_most_restrictive(self):
+ """Bedrock Converse should have more unsupported headers than Bedrock."""
+ bedrock_unsupported = get_unsupported_headers("bedrock")
+ converse_unsupported = get_unsupported_headers("bedrock_converse")
+ # Converse has more restrictions
+ assert len(converse_unsupported) >= len(bedrock_unsupported)
+
+ def test_all_providers_have_config(self):
+ """All providers should have a configuration entry."""
+ providers = ["anthropic", "azure_ai", "bedrock", "bedrock_converse", "vertex_ai"]
+ for provider in providers:
+ unsupported = get_unsupported_headers(provider)
+ # Should return a list (even if empty)
+ assert isinstance(unsupported, list), f"Provider {provider} should return a list"
+
+
+class TestEdgeCases:
+ """Test edge cases and error handling."""
+
+ def test_unknown_provider(self):
+ """Unknown provider with no config should pass through all headers."""
+ result = filter_and_transform_beta_headers(
+ ["context-management-2025-06-27"], "unknown_provider"
+ )
+ # Unknown providers have no unsupported list, so headers pass through
+ assert "context-management-2025-06-27" in result
+
+ def test_whitespace_handling(self):
+ """Headers with whitespace should be handled correctly."""
+ headers = [
+ " context-management-2025-06-27 ",
+ " web-search-2025-03-05 ",
+ ]
+ result = filter_and_transform_beta_headers(headers, "anthropic")
+ assert len(result) == 2
+
+ def test_duplicate_headers(self):
+ """Duplicate headers should be deduplicated."""
+ headers = [
+ "context-management-2025-06-27",
+ "context-management-2025-06-27",
+ ]
+ result = filter_and_transform_beta_headers(headers, "anthropic")
+ assert len(result) == 1
+
+ def test_case_sensitivity(self):
+ """Headers should be case-sensitive."""
+ # Correct case - should pass through for anthropic (no unsupported list)
+ headers = ["context-management-2025-06-27"]
+ result = filter_and_transform_beta_headers(headers, "anthropic")
+ assert len(result) == 1
+
+ # Wrong case - should still pass through (not in unsupported list)
+ headers = ["Context-Management-2025-06-27"]
+ result = filter_and_transform_beta_headers(headers, "anthropic")
+ assert len(result) == 1 # Passes through because anthropic has empty unsupported list