mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-05 08:07:05 +00:00
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:
parent
1b9631d260
commit
e91c0fd77c
2 changed files with 409 additions and 1 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue