Merge pull request #26148 from BerriAI/litellm_fix-bedrock-invoke-allowlist

fix(bedrock): allowlist Bedrock Invoke body fields and filter all anthropic-beta values
This commit is contained in:
Mateo Wang
2026-04-21 13:22:50 -07:00
committed by GitHub
5 changed files with 234 additions and 11 deletions
@@ -34,6 +34,7 @@ from litellm.llms.bedrock.common_utils import (
remove_custom_field_from_tools,
)
from litellm.types.llms.anthropic import ANTHROPIC_TOOL_SEARCH_BETA_HEADER
from litellm.types.llms.bedrock import BedrockInvokeAnthropicMessagesRequest
from litellm.types.llms.openai import AllMessageValues
from litellm.types.router import GenericLiteLLMParams
from litellm.types.utils import GenericStreamingChunk
@@ -59,6 +60,10 @@ class AmazonAnthropicClaudeMessagesConfig(
DEFAULT_BEDROCK_ANTHROPIC_API_VERSION = "bedrock-2023-05-31"
BEDROCK_INVOKE_ALLOWED_TOP_LEVEL_FIELDS = frozenset(
BedrockInvokeAnthropicMessagesRequest.__annotations__.keys()
)
def __init__(self, **kwargs):
BaseAnthropicMessagesConfig.__init__(self, **kwargs)
AmazonInvokeConfig.__init__(self, **kwargs)
@@ -500,10 +505,6 @@ class AmazonAnthropicClaudeMessagesConfig(
anthropic_messages_request=anthropic_messages_request,
)
# 5b. Strip `output_config` — Bedrock Invoke doesn't support it
# Fixes: https://github.com/BerriAI/litellm/issues/22797
anthropic_messages_request.pop("output_config", None)
# 5a. Remove `custom` field from tools (Bedrock doesn't support it)
# Claude Code sends `custom: {defer_loading: true}` on tool definitions,
# which causes Bedrock to reject the request with "Extra inputs are not permitted"
@@ -550,14 +551,43 @@ class AmazonAnthropicClaudeMessagesConfig(
if "tool-search-tool-2025-10-19" in beta_set:
beta_set.add("tool-examples-2025-10-29")
filtered_auto_betas = filter_and_transform_beta_headers(
beta_headers=list(beta_set - user_beta_set),
provider="bedrock",
filtered_betas = sorted(
filter_and_transform_beta_headers(
beta_headers=list(beta_set),
provider="bedrock",
)
)
filtered_betas = sorted(user_beta_set.union(set(filtered_auto_betas)))
dropped_user_betas = sorted(
b
for b in user_beta_set
if not filter_and_transform_beta_headers([b], provider="bedrock")
)
if dropped_user_betas:
verbose_logger.warning(
"Bedrock Invoke: dropping unsupported anthropic-beta values "
"from client headers: %s. Bedrock has no mapping entry for "
"these; forwarding them would cause a 400.",
dropped_user_betas,
)
if filtered_betas:
anthropic_messages_request["anthropic_beta"] = filtered_betas
# 7. Final safety net: filter top-level fields to the Bedrock Invoke allowlist.
# Catches Anthropic-only extensions (context_management, output_config, speed,
# mcp_servers, ...) and any future additions Claude Code may start sending.
allowed = self.BEDROCK_INVOKE_ALLOWED_TOP_LEVEL_FIELDS
stripped = sorted(k for k in anthropic_messages_request if k not in allowed)
if stripped:
verbose_logger.debug(
"Bedrock Invoke: stripping unsupported top-level request fields: %s",
stripped,
)
anthropic_messages_request = {
k: v for k, v in anthropic_messages_request.items() if k in allowed
}
return anthropic_messages_request
def get_async_streaming_response_iterator(
+44
View File
@@ -997,3 +997,47 @@ class BedrockToolBlock(TypedDict, total=False):
toolSpec: Optional[ToolSpecBlock]
systemTool: Optional[SystemToolBlock] # For Nova grounding
cachePoint: Optional[CachePointBlock]
class BedrockInvokeAnthropicMessagesRequest(TypedDict, total=False):
"""
Top-level request body accepted by AWS Bedrock `InvokeModel` /
`InvokeModelWithResponseStream` when calling an Anthropic Claude model with
the Messages API format. The LiteLLM /v1/messages → Bedrock Invoke
transformation filters outgoing requests to the keys of this TypedDict; any
other field (Anthropic-only extension, internal metadata, future addition)
is dropped before signing so Bedrock doesn't 400 with
"Extra inputs are not permitted".
Reference:
https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html
https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages-request-response.html
Editing this type is the single source of truth — the runtime allowlist in
`AmazonAnthropicClaudeMessagesConfig.BEDROCK_INVOKE_ALLOWED_TOP_LEVEL_FIELDS`
is derived from `__annotations__`, and a test asserts the resolved set
exactly, so any edit forces a conscious review.
Value types are intentionally loose (`list`, `dict`) — this type exists to
pin the allowed field names, not to validate nested structure.
"""
# Required by Bedrock
anthropic_version: str
max_tokens: int
messages: list
# Documented optional fields
anthropic_beta: List[str]
system: object # str or list[TextBlock]
stop_sequences: List[str]
temperature: float
top_p: float
top_k: int
tools: list
tool_choice: dict
# `thinking` is required for Opus 4.5 / Sonnet 4 extended thinking,
# `metadata` is part of the common Anthropic Messages API shape.
thinking: dict
metadata: dict
+1 -1
View File
@@ -208,7 +208,7 @@ build-backend = "uv_build"
[tool.uv]
default-groups = ["dev"]
required-version = "==0.10.9"
required-version = ">=0.10.9"
exclude-newer = "3 days"
[tool.uv.sources]
@@ -579,6 +579,155 @@ def test_bedrock_messages_strips_output_config_with_output_format():
assert "output_format" not in result
def test_bedrock_messages_strips_context_management():
"""
Ensure context_management is stripped from the request before sending to
Bedrock Invoke, which doesn't support this Anthropic-specific parameter.
Claude Code sends context_management on every request; leaving it in the body
causes a 400 "context_management: Extra inputs are not permitted" from Bedrock.
"""
from litellm.types.router import GenericLiteLLMParams
cfg = AmazonAnthropicClaudeMessagesConfig()
messages = [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}]
optional_params = {
"max_tokens": 4096,
"context_management": {
"edits": [{"type": "clear_thinking_20251015", "keep": "all"}]
},
}
result = cfg.transform_anthropic_messages_request(
model="anthropic.claude-3-haiku-20240307-v1:0",
messages=messages,
anthropic_messages_optional_request_params=optional_params,
litellm_params=GenericLiteLLMParams(),
headers={},
)
assert (
"context_management" not in result
), "context_management should be stripped — Bedrock Invoke rejects it"
assert result.get("max_tokens") == 4096
def test_bedrock_messages_allowlist_filters_anthropic_only_fields():
"""
Bedrock Invoke rejects any top-level body field it doesn't recognize with
"Extra inputs are not permitted". Defend against that by filtering the
outgoing body to a Bedrock-supported allowlist — catches Anthropic-only
extensions (speed, mcp_servers, container, ...) and any future additions
Claude Code starts sending before we learn about them.
"""
from litellm.types.router import GenericLiteLLMParams
cfg = AmazonAnthropicClaudeMessagesConfig()
messages = [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}]
optional_params = {
"max_tokens": 4096,
"temperature": 0.5,
"speed": "fast",
"mcp_servers": [{"type": "url", "url": "https://example.com"}],
"container": {"skills": []},
"inference_geo": "us",
"output_config": {"effort": "low"},
"context_management": {"edits": []},
}
result = cfg.transform_anthropic_messages_request(
model="anthropic.claude-3-haiku-20240307-v1:0",
messages=messages,
anthropic_messages_optional_request_params=optional_params,
litellm_params=GenericLiteLLMParams(),
headers={},
)
for bad in (
"speed",
"mcp_servers",
"container",
"inference_geo",
"output_config",
"context_management",
"model",
"stream",
):
assert bad not in result, f"{bad} should be stripped by the allowlist"
# Supported fields pass through.
assert result["max_tokens"] == 4096
assert result["temperature"] == 0.5
assert result["anthropic_version"] == cfg.DEFAULT_BEDROCK_ANTHROPIC_API_VERSION
# Every surviving key is in the allowlist.
assert set(result).issubset(cfg.BEDROCK_INVOKE_ALLOWED_TOP_LEVEL_FIELDS)
def test_bedrock_messages_filters_user_provided_unsupported_beta_header():
"""
In proxy deployments the client (e.g. Claude Code) doesn't know the backend
is Bedrock and may send Anthropic-direct beta headers Bedrock can't handle.
All betas must go through the provider mapping, not just auto-injected ones
— otherwise Bedrock 400s on the unsupported value.
"""
from litellm.types.router import GenericLiteLLMParams
cfg = AmazonAnthropicClaudeMessagesConfig()
messages = [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}]
optional_params = {"max_tokens": 128}
# `advisor-tool-2026-03-01` has no bedrock mapping entry → must be dropped.
# `context-1m-2025-08-07` does → must pass through.
headers = {
"anthropic-beta": "advisor-tool-2026-03-01,context-1m-2025-08-07",
}
result = cfg.transform_anthropic_messages_request(
model="anthropic.claude-3-haiku-20240307-v1:0",
messages=messages,
anthropic_messages_optional_request_params=optional_params,
litellm_params=GenericLiteLLMParams(),
headers=headers,
)
betas = result.get("anthropic_beta") or []
assert (
"advisor-tool-2026-03-01" not in betas
), "user-provided beta not in the Bedrock mapping must be dropped"
assert (
"context-1m-2025-08-07" in betas
), "user-provided beta that IS in the Bedrock mapping should survive"
def test_bedrock_messages_renames_user_provided_aliased_beta_header():
"""
Bedrock's config maps `advanced-tool-use-2025-11-20` to
`tool-search-tool-2025-10-19`. User-provided betas must go through the
rename too, not be forwarded under their Anthropic-direct spelling.
"""
from litellm.types.router import GenericLiteLLMParams
cfg = AmazonAnthropicClaudeMessagesConfig()
messages = [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}]
optional_params = {"max_tokens": 128}
headers = {"anthropic-beta": "advanced-tool-use-2025-11-20"}
result = cfg.transform_anthropic_messages_request(
model="anthropic.claude-3-haiku-20240307-v1:0",
messages=messages,
anthropic_messages_optional_request_params=optional_params,
litellm_params=GenericLiteLLMParams(),
headers=headers,
)
betas = result.get("anthropic_beta") or []
assert (
"advanced-tool-use-2025-11-20" not in betas
), "Anthropic-direct spelling should be rewritten, not forwarded verbatim"
assert (
"tool-search-tool-2025-10-19" in betas
), "user-provided beta should be renamed to the Bedrock-side spelling"
@pytest.mark.asyncio
async def test_promote_message_stop_usage_preserves_message_delta_output_tokens():
"""
@@ -95,7 +95,7 @@ class TestAnthropicBetaHeaderSupport:
def test_messages_transformation_anthropic_beta(self):
"""Test that Messages API transformation includes anthropic_beta in request."""
config = AmazonAnthropicClaudeMessagesConfig()
headers = {"anthropic-beta": "output-128k-2025-02-19"}
headers = {"anthropic-beta": "context-1m-2025-08-07"}
result = config.transform_anthropic_messages_request(
model="anthropic.claude-haiku-4-5-20251001-v1:0",
@@ -107,7 +107,7 @@ class TestAnthropicBetaHeaderSupport:
assert "anthropic_beta" in result
# Sort both arrays before comparing to avoid flakiness from ordering differences
assert sorted(result["anthropic_beta"]) == sorted(["output-128k-2025-02-19"])
assert sorted(result["anthropic_beta"]) == sorted(["context-1m-2025-08-07"])
def test_converse_computer_use_compatibility(self):
"""Test that user anthropic_beta headers work with computer use tools."""