diff --git a/litellm/anthropic_beta_headers_config.json b/litellm/anthropic_beta_headers_config.json index 332d9b3ad9d..3ba927f1f96 100644 --- a/litellm/anthropic_beta_headers_config.json +++ b/litellm/anthropic_beta_headers_config.json @@ -129,7 +129,7 @@ "mcp-servers-2025-12-04": null, "output-128k-2025-02-19": null, "structured-output-2024-03-01": null, - "per-turn-control-2026-07-01": null, + "per-turn-control-2026-07-01": "per-turn-control-2026-07-01", "prompt-caching-scope-2026-01-05": null, "skills-2025-10-02": null, "structured-outputs-2025-11-13": null, diff --git a/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_messages_per_turn_control.py b/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_messages_per_turn_control.py index 05c12c6a285..e01c9f4a501 100644 --- a/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_messages_per_turn_control.py +++ b/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_messages_per_turn_control.py @@ -95,14 +95,14 @@ def test_added_per_turn_control_beta_survives_the_anthropic_allowlist(): assert PER_TURN_CONTROL in _betas(filtered) -@pytest.mark.parametrize("provider", ["bedrock", "bedrock_converse", "databricks"]) +@pytest.mark.parametrize("provider", ["bedrock_converse", "databricks"]) def test_per_turn_control_beta_is_dropped_for_providers_without_it(provider): filtered = update_headers_with_filtered_beta(headers={"anthropic-beta": PER_TURN_CONTROL}, provider=provider) assert "anthropic-beta" not in filtered -@pytest.mark.parametrize("provider", ["azure_ai", "vertex_ai"]) +@pytest.mark.parametrize("provider", ["azure_ai", "bedrock", "vertex_ai"]) def test_per_turn_control_beta_is_forwarded_for_providers_with_it(provider): filtered = update_headers_with_filtered_beta(headers={"anthropic-beta": PER_TURN_CONTROL}, provider=provider) diff --git a/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py b/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py index a269d556262..a532f1c8dfe 100644 --- a/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py +++ b/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py @@ -2302,6 +2302,37 @@ def test_bedrock_invoke_transform_hoists_only_leading_system_run(local_model_cos ] +def test_bedrock_invoke_transform_forwards_per_turn_control_beta_with_message_output_config( + local_model_cost_map, local_beta_headers_config +): + """Claude Code's per-turn control sends a mid-conversation ``role: "system"`` + message carrying a message-level ``output_config`` together with the + per-turn-control-2026-07-01 beta. Bedrock Invoke rejects that message with + "messages.N.output_config: Extra inputs are not permitted" unless the beta + is present, so the beta must reach ``anthropic_beta`` and the message must + stay in place with its ``output_config``.""" + from litellm.types.router import GenericLiteLLMParams + + cfg = AmazonAnthropicClaudeMessagesConfig() + messages = [ + {"role": "user", "content": "hi"}, + {"role": "assistant", "content": "hello"}, + {"role": "system", "content": [], "output_config": {"effort": "low"}}, + {"role": "user", "content": "continue"}, + ] + + result = cfg.transform_anthropic_messages_request( + model="us.anthropic.claude-opus-5-5", + messages=copy.deepcopy(messages), + anthropic_messages_optional_request_params={"max_tokens": 256, "stream": False}, + litellm_params=GenericLiteLLMParams(), + headers={"anthropic-beta": "per-turn-control-2026-07-01"}, + ) + + assert result["messages"] == messages + assert result["anthropic_beta"].count("per-turn-control-2026-07-01") == 1 + + def test_bedrock_invoke_transform_converts_mid_conversation_system_for_older_claude(local_model_cost_map): """Invoke rejects ``role: "system"`` in every position on Opus 4.7, Sonnet 4.6, Haiku 4.5, etc. ("role 'system' is not supported on this model"), but diff --git a/tests/unit/test_anthropic_beta_headers_filtering.py b/tests/unit/test_anthropic_beta_headers_filtering.py index 1a6899f16ba..cd50387fa22 100644 --- a/tests/unit/test_anthropic_beta_headers_filtering.py +++ b/tests/unit/test_anthropic_beta_headers_filtering.py @@ -458,6 +458,21 @@ class TestAnthropicBetaHeadersFiltering: assert filtered == ["dangerous-tool-use-2026-09-03"] + @pytest.mark.parametrize("provider", ["anthropic", "bedrock", "bedrock_mantle"]) + def test_per_turn_control_forwarded(self, provider): + """Claude Code sends per-turn-control-2026-07-01 together with a + mid-conversation `role: system` message that carries a message-level + `output_config` (e.g. an effort change). Bedrock Invoke accepts that + message only when the beta is present and otherwise answers + "messages.N.output_config: Extra inputs are not permitted", so dropping + the header (previously mapped to null) turned every such turn into a 400.""" + filtered = filter_and_transform_beta_headers( + beta_headers=["per-turn-control-2026-07-01"], + provider=provider, + ) + + assert filtered == ["per-turn-control-2026-07-01"] + def test_null_value_headers_filtered(self): """Test that headers with null values are always filtered out.""" for provider in [