From ce02ebfbd03e95900f9c9ff1cffc3f16f350e7e4 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 3 Nov 2026 23:03:53 -0800 Subject: [PATCH] fix(bedrock): send only the effort from output_config to Converse Claude without thinking Other output_config keys such as task_budget never reached Converse without a thinking block, and Bedrock answers 400 to them, so they stay behind there --- .../messages/adapters/transformation.py | 11 +++++--- ...hropic_messages_adapters_transformation.py | 27 +++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/litellm/llms/anthropic/messages/adapters/transformation.py b/litellm/llms/anthropic/messages/adapters/transformation.py index 6743c4fd329..33e2abb4075 100644 --- a/litellm/llms/anthropic/messages/adapters/transformation.py +++ b/litellm/llms/anthropic/messages/adapters/transformation.py @@ -1222,7 +1222,9 @@ class LiteLLMAnthropicMessagesAdapter: Bedrock Converse takes the tier in ``output_config`` with or without a ``thinking`` block, so it travels on its own instead of only alongside ``thinking``. ``format`` stays behind, since - ``_translate_output_format_to_openai`` carries it as ``response_format``. + ``_translate_output_format_to_openai`` carries it as ``response_format``. Without ``thinking`` + only ``effort`` goes, since the other keys (``task_budget``) never reached Converse there and + it rejects them. """ if "output_config" not in anthropic_message_request or not self._is_bedrock_claude_target(new_kwargs["model"]): return @@ -1231,13 +1233,16 @@ class LiteLLMAnthropicMessagesAdapter: if not isinstance(output_config, dict): return - effort_config: Final = {k: v for k, v in output_config.items() if k != "format"} + has_thinking: Final = bool(anthropic_message_request.get("thinking")) + effort_config: Final = { + k: v for k, v in output_config.items() if k == "effort" or (has_thinking and k != "format") + } if not effort_config: return model: Final = new_kwargs["model"] if ( drop_params - and not anthropic_message_request.get("thinking") + and not has_thinking and is_bedrock_application_inference_profile_arn(bedrock_capability_model(model, base_model)) ): litellm.verbose_logger.warning(UNRESOLVED_ARN_EFFORT_WITHOUT_THINKING_WARNING, model) diff --git a/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py b/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py index 05f6e61db0c..1e2dc5e44a2 100644 --- a/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py +++ b/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py @@ -5526,6 +5526,33 @@ async def test_bedrock_converse_claude_gets_the_effort_without_a_thinking_block( assert "thinking" not in sent +_TASK_BUDGET: Final = {"type": "tokens", "total": 20000} + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("output_config", "sent_output_config"), + [ + pytest.param({"effort": "low", "task_budget": _TASK_BUDGET}, {"effort": "low"}, id="effort_and_task_budget"), + pytest.param({"task_budget": _TASK_BUDGET}, None, id="task_budget_only"), + ], +) +async def test_bedrock_converse_claude_without_a_thinking_block_gets_only_the_effort_from_output_config( + _converse_route: respx.Route, + output_config: Mapping[str, object], + sent_output_config: Mapping[str, object] | None, +) -> None: + await litellm.anthropic.messages.acreate( + model=f"bedrock/converse/{_CONVERSE_EFFORT_MODEL}", + max_tokens=64, + messages=[{"role": "user", "content": "Name the capital of France."}], + output_config=output_config, + drop_params=True, + ) + + assert _sent_converse_fields(_converse_route).get("output_config") == sent_output_config + + @pytest.mark.asyncio @pytest.mark.parametrize( ("drop_params_source", "forwards_effort"),