From c2da82a06dba6f45dba3b694f045c7f5df4a91d1 Mon Sep 17 00:00:00 2001 From: Homen Shum <317650355+homenshum-cheiron@users.noreply.github.com> Date: Wed, 4 Nov 2026 02:27:02 -0800 Subject: [PATCH] fix(bedrock): send `compaction` only with its beta, and add the beta for an adopted block Follow-ups to forwarding the compact-2026-09-04 `compaction` parameter on Bedrock Invoke: - `compaction` stays in the body only when it is an object and compact-2026-09-04 survives the route's beta filter. bedrock_mantle has no entry for that beta, so a mantle request keeps the earlier strip instead of reaching Bedrock with the field and no beta (400 "Extra inputs are not permitted"). A null or non-object value is dropped the same way. - A request that adopts the signed compaction block now gets the beta even without a client anthropic-beta header, as the Anthropic route does (carries_signed_compaction_block). - The compaction tests pin the bundled beta config (local_beta_headers_config) so they do not read the remote copy an earlier test left cached. Co-Authored-By: Claude Opus 5.5 --- .../anthropic_claude_transformation.py | 20 ++++++-- .../test_anthropic_claude_transformation.py | 50 +++++++++++++++++++ tests/unit/llms/bedrock/test_mantle.py | 23 +++++++++ 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude_transformation.py b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude_transformation.py index 595a0009279..5ceb190738e 100644 --- a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude_transformation.py +++ b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude_transformation.py @@ -23,6 +23,7 @@ from litellm.llms.anthropic.common_utils import ( OBJECT_MAPPING_ADAPTER, AnthropicModelInfo, carried_thinking_keys, + carries_signed_compaction_block, validated_object_mapping, ) from litellm.llms.anthropic.messages.native.transformation import ( @@ -555,12 +556,15 @@ class AmazonAnthropicClaudeMessagesConfig( """ A top-level ``compaction`` request needs the ``compact-2026-09-04`` anthropic-beta; without it Bedrock answers 400 "compaction: Extra inputs - are not permitted". Later requests that adopt the returned block need the - same beta, which the caller sends as a header. + are not permitted". A later request that adopts the returned signed + block needs the same beta, so it is added for that block too, as the + Anthropic route does. Ref: https://docs.aws.amazon.com/bedrock/latest/userguide/claude-messages-compaction.html """ - if isinstance(anthropic_messages_request.get("compaction"), dict): + if isinstance(anthropic_messages_request.get("compaction"), dict) or carries_signed_compaction_block( + anthropic_messages_request.get("messages") or () + ): beta_set.add("compact-2026-09-04") def strips_beta_gated_fields_from_final_headers(self) -> bool: @@ -843,6 +847,16 @@ class AmazonAnthropicClaudeMessagesConfig( if filtered_betas: anthropic_messages_request["anthropic_beta"] = filtered_betas + # ``compaction`` travels only with its beta. A beta map without compact-2026-09-04 + # (bedrock_mantle) or a non-object value keeps the earlier strip instead of a 400. + if "compaction" in anthropic_messages_request and not ( + isinstance(anthropic_messages_request["compaction"], dict) and "compact-2026-09-04" in filtered_betas + ): + verbose_logger.debug( + "Bedrock Invoke: dropping `compaction`; compact-2026-09-04 is not served on this route" + ) + anthropic_messages_request.pop("compaction") + final_request: Final = OBJECT_MAPPING_ADAPTER.validate_python(anthropic_messages_request) # pyright: ignore[reportUnknownArgumentType] # built from the base signature's bare dict gated: Final = strip_beta_gated_native_request( {}, diff --git a/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude_transformation.py b/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude_transformation.py index 5825bea3589..24243a27d3c 100644 --- a/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude_transformation.py +++ b/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude_transformation.py @@ -1547,6 +1547,7 @@ def test_bedrock_messages_preserves_compact_context_management_and_adds_beta(): assert result["max_tokens"] == 4096 +@pytest.mark.usefixtures("local_beta_headers_config") def test_bedrock_messages_keeps_compaction_param_and_adds_beta(): """ The ``compact-2026-09-04`` beta uses a top-level ``compaction`` parameter, not a @@ -1580,6 +1581,7 @@ def test_bedrock_messages_keeps_compaction_param_and_adds_beta(): assert set(result).issubset(cfg.BEDROCK_INVOKE_ALLOWED_TOP_LEVEL_FIELDS) +@pytest.mark.usefixtures("local_beta_headers_config") def test_bedrock_messages_keeps_compact_2026_09_04_beta_for_adopting_request(): """ A request that adopts the signed block sends it first in ``messages`` and must @@ -1603,6 +1605,54 @@ def test_bedrock_messages_keeps_compact_2026_09_04_beta_for_adopting_request(): assert result["messages"][0]["content"][0] == block +@pytest.mark.usefixtures("local_beta_headers_config") +def test_bedrock_messages_adds_compact_2026_09_04_beta_for_signed_block_without_client_header(): + """ + A client that adopts the signed block but sends no anthropic-beta header still + needs ``compact-2026-09-04``; the Anthropic route adds it for the block, and so + does Bedrock Invoke. + """ + from litellm.types.router import GenericLiteLLMParams + + cfg = AmazonAnthropicClaudeMessagesConfig() + block = {"type": "compaction", "content": "TR-TITCK: Type IB.", "signature": "sig"} + messages = [{"role": "user", "content": [block, {"type": "text", "text": "Now KR-MFDS."}]}] + + result = cfg.transform_anthropic_messages_request( + model="anthropic.claude-opus-5", + messages=messages, + anthropic_messages_optional_request_params={"max_tokens": 512}, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert result["anthropic_beta"] == ["compact-2026-09-04"] + assert result["messages"][0]["content"][0] == block + + +@pytest.mark.usefixtures("local_beta_headers_config") +@pytest.mark.parametrize("compaction", [None, "summarize", []]) +def test_bedrock_messages_drops_compaction_that_is_not_an_object(compaction): + """ + Only an object ``compaction`` gets the beta. Any other value is dropped, as it + was before the field was allowed, instead of reaching Bedrock without its beta. + """ + from litellm.types.router import GenericLiteLLMParams + + cfg = AmazonAnthropicClaudeMessagesConfig() + + result = cfg.transform_anthropic_messages_request( + model="anthropic.claude-opus-5", + messages=[{"role": "user", "content": [{"type": "text", "text": "Hi"}]}], + anthropic_messages_optional_request_params={"max_tokens": 512, "compaction": compaction}, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert "compaction" not in result + assert "compact-2026-09-04" not in result.get("anthropic_beta", []) + + def test_bedrock_messages_filters_unsupported_context_management_edits(): """ Mixed edit lists must drop the LiteLLM-internal ``clear_thinking_20251015`` diff --git a/tests/unit/llms/bedrock/test_mantle.py b/tests/unit/llms/bedrock/test_mantle.py index 63af0105f5b..d6a802366c7 100644 --- a/tests/unit/llms/bedrock/test_mantle.py +++ b/tests/unit/llms/bedrock/test_mantle.py @@ -796,3 +796,26 @@ async def test_mantle_anthropic_messages_streaming_sends_stream_and_passes_throu assert "event: message_start" in text assert '"text": "pong"' in text assert "event: message_stop" in text + + +@pytest.mark.usefixtures("local_beta_headers_config") +def test_mantle_messages_drops_compaction_without_its_beta(): + """ + The bedrock_mantle beta map has no compact-2026-09-04, so the beta is filtered + out on this route. ``compaction`` must go with it, or the body carries a field + that Bedrock refuses without the beta. + """ + from litellm.types.router import GenericLiteLLMParams + + config = AmazonMantleMessagesConfig() + headers: dict = {} + request = config.transform_anthropic_messages_request( + model="mantle/anthropic.claude-mythos-preview", + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_optional_request_params={"max_tokens": 100, "compaction": {"type": "summarize"}}, + litellm_params=GenericLiteLLMParams(), + headers=headers, + ) + + assert "compaction" not in request + assert "compact-2026-09-04" not in headers.get("anthropic-beta", "")