fix(bedrock): Filter unsupported beta headers for Bedrock passthrough

AWS Bedrock only supports a specific whitelist of beta flags and rejects
requests with "invalid beta flag" error for unsupported ones (e.g.,
mcp-servers-2025-12-04, oauth-2025-04-20 from Claude Code).

This implements whitelist-based filtering in BedrockPassthroughConfig
to filter the anthropic-beta header before signing and forwarding
requests, consistent with the Invoke API approach in PR #19877.

Supported beta flags (per AWS docs):
- computer-use-2024-10-22, computer-use-2025-01-24
- token-efficient-tools-2025-02-19
- interleaved-thinking-2025-05-14
- output-128k-2025-02-19
- dev-full-thinking-2025-05-14
- context-1m-2025-08-07
- context-management-2025-06-27
- effort-2025-11-24
- tool-search-tool-2025-10-19
- tool-examples-2025-10-29

Fixes: https://github.com/BerriAI/litellm/issues/16726

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Brian Lai 2026-01-29 15:16:11 -05:00
parent 1b9631d260
commit e91c0fd77c
No known key found for this signature in database
GPG key ID: CD68B450A43757AA
2 changed files with 409 additions and 1 deletions

View file

@ -93,6 +93,51 @@ class BedrockPassthroughConfig(
endpoint = re.sub(r'model/[^/]+/', f'model/{encoded_model_id}/', endpoint)
return self.format_url(endpoint, endpoint_url, request_query_params or {}), endpoint_url
# AWS Bedrock only supports a specific whitelist of beta flags
# Reference: https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages-request-response.html
BEDROCK_SUPPORTED_BETAS = {
"computer-use-2024-10-22", # Legacy computer use
"computer-use-2025-01-24", # Current computer use (Claude 3.7 Sonnet)
"token-efficient-tools-2025-02-19", # Tool use (Claude 3.7+ and Claude 4+)
"interleaved-thinking-2025-05-14", # Interleaved thinking (Claude 4+)
"output-128k-2025-02-19", # 128K output tokens (Claude 3.7 Sonnet)
"dev-full-thinking-2025-05-14", # Developer mode for raw thinking (Claude 4+)
"context-1m-2025-08-07", # 1 million tokens (Claude Sonnet 4)
"context-management-2025-06-27", # Context management (Claude Sonnet/Haiku 4.5)
"effort-2025-11-24", # Effort parameter (Claude Opus 4.5)
"tool-search-tool-2025-10-19", # Tool search (Claude Opus 4.5)
"tool-examples-2025-10-29", # Tool use examples (Claude Opus 4.5)
}
def _filter_anthropic_beta_header(self, headers: dict) -> dict:
"""
Filter anthropic-beta header to only include Bedrock-supported beta flags.
AWS Bedrock doesn't support all Anthropic beta flags and will reject
requests with "invalid beta flag" error for unsupported ones
(e.g., mcp-servers-2025-12-04, oauth-2025-04-20 from Claude Code).
Args:
headers: Original request headers
Returns:
Headers with anthropic-beta filtered to only supported values
"""
filtered_headers = {}
for key, value in headers.items():
if key.lower() == "anthropic-beta":
# Parse comma-separated beta values and filter to supported ones
beta_values = [b.strip() for b in value.split(",")]
supported_betas = [
b for b in beta_values if b in self.BEDROCK_SUPPORTED_BETAS
]
if supported_betas:
filtered_headers[key] = ",".join(supported_betas)
# If no supported betas remain, omit the header entirely
else:
filtered_headers[key] = value
return filtered_headers
def sign_request(
self,
headers: dict,
@ -101,10 +146,13 @@ class BedrockPassthroughConfig(
api_base: str,
model: Optional[str] = None,
) -> Tuple[dict, Optional[bytes]]:
# Filter anthropic-beta header to only include Bedrock-supported beta flags
filtered_headers = self._filter_anthropic_beta_header(headers)
optional_params = litellm_params.copy()
return self._sign_request(
service_name="bedrock",
headers=headers,
headers=filtered_headers,
optional_params=optional_params,
request_data=request_data or {},
api_base=api_base,

View file

@ -0,0 +1,360 @@
"""
Test header filtering for Bedrock passthrough endpoints.
Tests that unsupported anthropic-beta headers are filtered out before forwarding
requests to AWS Bedrock to prevent "invalid beta flag" errors.
Uses whitelist-based filtering consistent with the Invoke API approach in PR #19877.
"""
import pytest
from unittest.mock import Mock, patch
from litellm.llms.bedrock.passthrough.transformation import BedrockPassthroughConfig
class TestBedrockHeaderFiltering:
"""Test suite for anthropic-beta header filtering in Bedrock passthrough."""
def test_unsupported_beta_header_filtered(self):
"""Test that unsupported anthropic-beta headers are filtered out."""
config = BedrockPassthroughConfig()
headers = {
"content-type": "application/json",
"anthropic-beta": "oauth-2025-04-20", # Unsupported - should be filtered
"x-api-key": "test-key",
"user-agent": "claude-code/1.0",
}
litellm_params = {
"aws_region_name": "us-east-1",
"aws_access_key_id": "test-access-key",
"aws_secret_access_key": "test-secret-key",
}
with patch.object(config, "_sign_request") as mock_sign:
mock_sign.return_value = ({}, None)
config.sign_request(
headers=headers,
litellm_params=litellm_params,
request_data={"messages": []},
api_base="https://bedrock-runtime.us-east-1.amazonaws.com",
model="anthropic.claude-v2",
)
# Verify _sign_request was called
assert mock_sign.called
# Get the headers that were passed to _sign_request
call_args = mock_sign.call_args
signed_headers = call_args[1]["headers"]
# anthropic-beta should be filtered out (unsupported value)
assert "anthropic-beta" not in signed_headers
# Other headers should remain
assert "content-type" in signed_headers
assert "x-api-key" in signed_headers
assert "user-agent" in signed_headers
def test_supported_beta_header_preserved(self):
"""Test that supported anthropic-beta headers are preserved."""
config = BedrockPassthroughConfig()
# Test each supported beta flag
supported_betas = [
"computer-use-2024-10-22",
"computer-use-2025-01-24",
"token-efficient-tools-2025-02-19",
"interleaved-thinking-2025-05-14",
"output-128k-2025-02-19",
"dev-full-thinking-2025-05-14",
"context-1m-2025-08-07",
"context-management-2025-06-27",
"effort-2025-11-24",
"tool-search-tool-2025-10-19",
"tool-examples-2025-10-29",
]
for beta_value in supported_betas:
headers = {
"anthropic-beta": beta_value,
"content-type": "application/json",
}
litellm_params = {
"aws_region_name": "us-east-1",
"aws_access_key_id": "test-access-key",
"aws_secret_access_key": "test-secret-key",
}
with patch.object(config, "_sign_request") as mock_sign:
mock_sign.return_value = ({}, None)
config.sign_request(
headers=headers,
litellm_params=litellm_params,
request_data={},
api_base="https://bedrock-runtime.us-east-1.amazonaws.com",
model="anthropic.claude-v2",
)
call_args = mock_sign.call_args
signed_headers = call_args[1]["headers"]
# Supported beta should be preserved
assert "anthropic-beta" in signed_headers
assert signed_headers["anthropic-beta"] == beta_value
def test_mixed_beta_values_filtered(self):
"""Test filtering with mix of supported and unsupported beta values."""
config = BedrockPassthroughConfig()
# Mix of supported and unsupported betas (comma-separated)
headers = {
"anthropic-beta": "computer-use-2024-10-22,mcp-servers-2025-12-04,oauth-2025-04-20,token-efficient-tools-2025-02-19",
"content-type": "application/json",
}
litellm_params = {
"aws_region_name": "us-east-1",
"aws_access_key_id": "test-access-key",
"aws_secret_access_key": "test-secret-key",
}
with patch.object(config, "_sign_request") as mock_sign:
mock_sign.return_value = ({}, None)
config.sign_request(
headers=headers,
litellm_params=litellm_params,
request_data={},
api_base="https://bedrock-runtime.us-east-1.amazonaws.com",
model="anthropic.claude-v2",
)
call_args = mock_sign.call_args
signed_headers = call_args[1]["headers"]
# anthropic-beta should be present with only supported values
assert "anthropic-beta" in signed_headers
beta_values = signed_headers["anthropic-beta"].split(",")
# Supported betas should be kept
assert "computer-use-2024-10-22" in beta_values
assert "token-efficient-tools-2025-02-19" in beta_values
# Unsupported betas should be filtered
assert "mcp-servers-2025-12-04" not in beta_values
assert "oauth-2025-04-20" not in beta_values
def test_mcp_servers_beta_filtered(self):
"""
Test that mcp-servers beta header is filtered out.
This is the main issue from Claude Code - when MCP servers are configured,
Claude Code sends mcp-servers-2025-12-04 which Bedrock doesn't support.
Fixes: https://github.com/BerriAI/litellm/issues/16726
"""
config = BedrockPassthroughConfig()
headers = {
"anthropic-beta": "mcp-servers-2025-12-04",
"content-type": "application/json",
}
litellm_params = {
"aws_region_name": "us-east-1",
"aws_access_key_id": "test-access-key",
"aws_secret_access_key": "test-secret-key",
}
with patch.object(config, "_sign_request") as mock_sign:
mock_sign.return_value = ({}, None)
config.sign_request(
headers=headers,
litellm_params=litellm_params,
request_data={},
api_base="https://bedrock-runtime.us-east-1.amazonaws.com",
model="anthropic.claude-v2",
)
call_args = mock_sign.call_args
signed_headers = call_args[1]["headers"]
# mcp-servers beta should be filtered out entirely
assert "anthropic-beta" not in signed_headers
def test_case_insensitive_header_matching(self):
"""Test that header matching is case-insensitive."""
config = BedrockPassthroughConfig()
test_cases = [
"anthropic-beta",
"Anthropic-Beta",
"ANTHROPIC-BETA",
"Anthropic-beta",
"AnThRoPiC-bEtA",
]
for header_name in test_cases:
headers = {
header_name: "oauth-2025-04-20", # Unsupported
"content-type": "application/json",
}
litellm_params = {
"aws_region_name": "us-east-1",
"aws_access_key_id": "test-access-key",
"aws_secret_access_key": "test-secret-key",
}
with patch.object(config, "_sign_request") as mock_sign:
mock_sign.return_value = ({}, None)
config.sign_request(
headers=headers,
litellm_params=litellm_params,
request_data={},
api_base="https://bedrock-runtime.us-east-1.amazonaws.com",
model="anthropic.claude-v2",
)
call_args = mock_sign.call_args
signed_headers = call_args[1]["headers"]
# Verify header was processed (unsupported value filtered)
assert not any(
k.lower() == "anthropic-beta" for k in signed_headers.keys()
)
def test_other_headers_preserved(self):
"""Test that non-beta headers are preserved correctly."""
config = BedrockPassthroughConfig()
headers = {
"content-type": "application/json",
"anthropic-beta": "oauth-2025-04-20", # Should be filtered (unsupported)
"anthropic-version": "2023-06-01", # Should be preserved
"x-api-key": "test-key", # Should be preserved
"user-agent": "claude-code/2.0", # Should be preserved
"authorization": "Bearer token", # Should be preserved
}
litellm_params = {
"aws_region_name": "us-east-1",
"aws_access_key_id": "test-access-key",
"aws_secret_access_key": "test-secret-key",
}
with patch.object(config, "_sign_request") as mock_sign:
mock_sign.return_value = ({}, None)
config.sign_request(
headers=headers,
litellm_params=litellm_params,
request_data={},
api_base="https://bedrock-runtime.us-east-1.amazonaws.com",
model="anthropic.claude-v2",
)
call_args = mock_sign.call_args
signed_headers = call_args[1]["headers"]
# Verify anthropic-beta is filtered (unsupported value)
assert "anthropic-beta" not in signed_headers
# Verify all other headers are preserved
assert signed_headers["content-type"] == "application/json"
assert signed_headers["anthropic-version"] == "2023-06-01"
assert signed_headers["x-api-key"] == "test-key"
assert signed_headers["user-agent"] == "claude-code/2.0"
assert signed_headers["authorization"] == "Bearer token"
def test_empty_headers(self):
"""Test that empty headers dict doesn't cause errors."""
config = BedrockPassthroughConfig()
headers = {}
litellm_params = {
"aws_region_name": "us-east-1",
"aws_access_key_id": "test-access-key",
"aws_secret_access_key": "test-secret-key",
}
with patch.object(config, "_sign_request") as mock_sign:
mock_sign.return_value = ({}, None)
# Should not raise any errors
config.sign_request(
headers=headers,
litellm_params=litellm_params,
request_data={},
api_base="https://bedrock-runtime.us-east-1.amazonaws.com",
model="anthropic.claude-v2",
)
call_args = mock_sign.call_args
signed_headers = call_args[1]["headers"]
# Should be an empty dict
assert signed_headers == {}
def test_only_unsupported_beta_results_in_no_header(self):
"""Test that only unsupported beta values results in header being omitted."""
config = BedrockPassthroughConfig()
headers = {"anthropic-beta": "oauth-2025-04-20,mcp-servers-2025-12-04"}
litellm_params = {
"aws_region_name": "us-east-1",
"aws_access_key_id": "test-access-key",
"aws_secret_access_key": "test-secret-key",
}
with patch.object(config, "_sign_request") as mock_sign:
mock_sign.return_value = ({}, None)
config.sign_request(
headers=headers,
litellm_params=litellm_params,
request_data={},
api_base="https://bedrock-runtime.us-east-1.amazonaws.com",
model="anthropic.claude-v2",
)
call_args = mock_sign.call_args
signed_headers = call_args[1]["headers"]
# Should result in no anthropic-beta header
assert "anthropic-beta" not in signed_headers
class TestBedrockSupportedBetasConstant:
"""Test the BEDROCK_SUPPORTED_BETAS constant is properly defined."""
def test_supported_betas_is_set(self):
"""Verify BEDROCK_SUPPORTED_BETAS is a set."""
assert isinstance(BedrockPassthroughConfig.BEDROCK_SUPPORTED_BETAS, set)
def test_supported_betas_contains_expected_values(self):
"""Verify all expected supported betas are in the set."""
expected_betas = {
"computer-use-2024-10-22",
"computer-use-2025-01-24",
"token-efficient-tools-2025-02-19",
"interleaved-thinking-2025-05-14",
"output-128k-2025-02-19",
"dev-full-thinking-2025-05-14",
"context-1m-2025-08-07",
"context-management-2025-06-27",
"effort-2025-11-24",
"tool-search-tool-2025-10-19",
"tool-examples-2025-10-29",
}
assert BedrockPassthroughConfig.BEDROCK_SUPPORTED_BETAS == expected_betas