diff --git a/litellm/anthropic_beta_headers_config.json b/litellm/anthropic_beta_headers_config.json index 1b659770181..a1359d98c4a 100644 --- a/litellm/anthropic_beta_headers_config.json +++ b/litellm/anthropic_beta_headers_config.json @@ -201,6 +201,7 @@ "bash_20250124": null, "code-execution-2025-08-25": null, "compact-2026-01-12": "compact-2026-01-12", + "compact-2026-09-04": "compact-2026-09-04", "computer-use-2025-01-24": "computer-use-2025-01-24", "computer-use-2025-11-24": "computer-use-2025-11-24", "context-1m-2025-08-07": "context-1m-2025-08-07", 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 cf48fcf5c2f..814da685b8c 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 ( @@ -82,6 +83,7 @@ else: _MODEL_INFO: Final[TypeAdapter[Mapping[str, object] | None]] = TypeAdapter(Mapping[str, object] | None) +_MESSAGE_LIST: Final[TypeAdapter[tuple[object, ...]]] = TypeAdapter(tuple[object, ...]) class AmazonAnthropicClaudeMessagesConfig( @@ -571,6 +573,19 @@ class AmazonAnthropicClaudeMessagesConfig( "edits": retained_edits, } + @staticmethod + def _add_compaction_beta_for_bedrock_invoke( + anthropic_messages_request: Mapping[str, object], + beta_set: set[str], # mutable-ok: the caller's beta set is grown in place, like the sibling helpers + ) -> None: + """``compaction`` and an adopted signed block both need the compact-2026-09-04 beta on Bedrock. + + Ref: https://docs.aws.amazon.com/bedrock/latest/userguide/claude-messages-compaction.html + """ + messages: Final = _MESSAGE_LIST.validate_python(anthropic_messages_request.get("messages") or ()) + if isinstance(anthropic_messages_request.get("compaction"), dict) or carries_signed_compaction_block(messages): + beta_set.add("compact-2026-09-04") + def strips_beta_gated_fields_from_final_headers(self) -> bool: return False @@ -639,6 +654,7 @@ class AmazonAnthropicClaudeMessagesConfig( anthropic_messages_request=anthropic_messages_request, beta_set=beta_set, ) + self._add_compaction_beta_for_bedrock_invoke(anthropic_messages_request, beta_set) # pyright: ignore[reportUnknownArgumentType] # built from the base signature's bare dict self._get_tool_search_beta_header_for_bedrock( model=model, @@ -858,6 +874,14 @@ class AmazonAnthropicClaudeMessagesConfig( if filtered_betas: anthropic_messages_request["anthropic_beta"] = filtered_betas + 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" + ) + del anthropic_messages_request["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/litellm/types/llms/bedrock.py b/litellm/types/llms/bedrock.py index ff4f2c4fd1e..e42a1a8f057 100644 --- a/litellm/types/llms/bedrock.py +++ b/litellm/types/llms/bedrock.py @@ -1257,6 +1257,13 @@ class BedrockInvokeAnthropicMessagesRequest(TypedDict, total=False): # other edit types (e.g. `clear_thinking_20251015`) never reach Bedrock. context_management: dict + # `compaction` ({"type": "summarize", "instructions"?: str}) is the + # `compact-2026-09-04` beta: one summarization call that returns a single + # signed compaction block. Bedrock rejects it together with + # `context_management`. Ref: + # https://docs.aws.amazon.com/bedrock/latest/userguide/claude-messages-compaction.html + compaction: ReadOnly[Mapping[str, object]] + class BedrockBatchRecordKind(Enum): """ 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 c4614dd636f..991a0c36f03 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,112 @@ 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 + ``context_management`` edit. Bedrock InvokeModel accepts it with that beta and + returns one signed compaction block. Before this fix the top-level allowlist + dropped ``compaction``, so the request reached Bedrock as an ordinary turn. + + Ref: https://docs.aws.amazon.com/bedrock/latest/userguide/claude-messages-compaction.html + """ + from litellm.types.router import GenericLiteLLMParams + + cfg = AmazonAnthropicClaudeMessagesConfig() + messages = [ + {"role": "user", "content": [{"type": "text", "text": "Is CHG-TR-0042 Type IB in TR-TITCK?"}]}, + {"role": "assistant", "content": [{"type": "text", "text": "Type IB."}]}, + {"role": "user", "content": [{"type": "text", "text": "Now KR-MFDS."}]}, + ] + optional_params = {"max_tokens": 1024, "compaction": {"type": "summarize", "instructions": "Keep rule ids."}} + + result = cfg.transform_anthropic_messages_request( + model="anthropic.claude-opus-5", + messages=messages, + anthropic_messages_optional_request_params=optional_params, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert result["compaction"] == {"type": "summarize", "instructions": "Keep rule ids."} + assert "compact-2026-09-04" in result["anthropic_beta"] + assert "context_management" not in result + 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 + carry the ``compact-2026-09-04`` beta again. The bedrock beta map has to keep it. + """ + 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={"anthropic-beta": "compact-2026-09-04"}, + ) + + assert "compact-2026-09-04" in result["anthropic_beta"] + 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: object): + """ + 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..908e0bbbced 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[str, str] = {} + 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", "") diff --git a/tests/unit/test_anthropic_beta_headers_filtering.py b/tests/unit/test_anthropic_beta_headers_filtering.py index a9bc741f800..d0fb6fe0f6e 100644 --- a/tests/unit/test_anthropic_beta_headers_filtering.py +++ b/tests/unit/test_anthropic_beta_headers_filtering.py @@ -99,7 +99,9 @@ class TestAnthropicBetaHeadersFiltering: filtered = filter_and_transform_beta_headers( beta_headers=all_headers, provider=provider ) - assert ("compact-2026-09-04" in filtered) is (provider in ("anthropic", "azure_ai", "vertex_ai")) + # Bedrock InvokeModel serves the compact-2026-09-04 `compaction` parameter; Converse does not. + # https://docs.aws.amazon.com/bedrock/latest/userguide/claude-messages-compaction.html + assert ("compact-2026-09-04" in filtered) is (provider in ("anthropic", "azure_ai", "vertex_ai", "bedrock")) assert ("mid-conversation-tool-changes-2026-07-01" in filtered) is (provider != "bedrock_converse") for header in unsupported_headers: