From 583bdd34a237c4995d1cce94eb63d7cd5a3d1a52 Mon Sep 17 00:00:00 2001 From: SwiftWinds <12981958+SwiftWinds@users.noreply.github.com> Date: Mon, 20 Apr 2026 20:20:47 -0700 Subject: [PATCH 1/2] fix(bedrock): allowlist Bedrock Invoke body fields and filter all anthropic-beta values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fail-safes for the /v1/messages → Bedrock Invoke pass-through so new Anthropic-only extensions Claude Code starts sending can't reach Bedrock and trigger a 400 "Extra inputs are not permitted": 1. Top-level body fields are filtered to a typed allowlist. New `BedrockInvokeAnthropicMessagesRequest` TypedDict (in `litellm/types/llms/bedrock.py`) captures the Bedrock Invoke Anthropic Messages body schema; the runtime allowlist is derived from its `__annotations__` so the type and the filter can't drift. Anchored to the AWS reference page in docstrings + transform comment. An exact-set test pins the resolved allowlist so any future edit forces conscious review. Drops context_management, output_config, speed, mcp_servers, container, inference_geo, internal litellm_metadata, and any future Anthropic addition. output_format stays as an active inline-schema conversion (not just a strip). 2. The anthropic-beta header list is filtered + transformed against the bedrock mapping for ALL betas, not just auto-injected ones. The previous code union'd user-provided betas back in unfiltered, so a client on a new Anthropic-direct beta (e.g. advisor-tool-…, context-management-…) could still pin the request to fail. In a proxy context the client can't know the backend is Bedrock; the provider mapping is authoritative. User-provided drops are logged at WARNING so intentional overrides leave a breadcrumb. Updates one existing test that happened to assert on the old buggy pass-through (it used output-128k-2025-02-19, which is null in the bedrock mapping and would 400 at runtime); rewrote it against a bedrock-supported beta. Scope: messages/invoke only. The same user-beta bypass exists in chat/invoke but that's a different code path with different user-expectation trade-offs — follow-up. --- .../anthropic_claude3_transformation.py | 46 +++++- litellm/types/llms/bedrock.py | 44 ++++++ .../test_anthropic_claude3_transformation.py | 149 ++++++++++++++++++ .../bedrock/test_anthropic_beta_support.py | 4 +- 4 files changed, 233 insertions(+), 10 deletions(-) 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 31e0e76fd9..96593b35d0 100644 --- a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py @@ -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( diff --git a/litellm/types/llms/bedrock.py b/litellm/types/llms/bedrock.py index 6830d95d36..9ffb52ef88 100644 --- a/litellm/types/llms/bedrock.py +++ b/litellm/types/llms/bedrock.py @@ -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 diff --git a/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py b/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py index d3a9c94ea5..7a2a6f56d6 100644 --- a/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py +++ b/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py @@ -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(): """ diff --git a/tests/test_litellm/llms/bedrock/test_anthropic_beta_support.py b/tests/test_litellm/llms/bedrock/test_anthropic_beta_support.py index 46fbd67902..a20ec94a99 100644 --- a/tests/test_litellm/llms/bedrock/test_anthropic_beta_support.py +++ b/tests/test_litellm/llms/bedrock/test_anthropic_beta_support.py @@ -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.""" From 11b776935d4878b513772ee5b0cde1105c943d0d Mon Sep 17 00:00:00 2001 From: SwiftWinds <12981958+SwiftWinds@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:39:11 -0700 Subject: [PATCH 2/2] chore: make `uv` newer than 0.10 allowable --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d5d238473b..ef06a628fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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]