From d816b75dd4978a776f27612f80565615122dfefe Mon Sep 17 00:00:00 2001 From: mateo Date: Tue, 1 Sep 2026 19:05:03 +0000 Subject: [PATCH] feat(bedrock): gate forced tool_choice on supports_forced_tool_use in converse Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/llms/anthropic/common_utils.py | 28 +++++---- .../bedrock/chat/converse_transformation.py | 10 +++- .../chat/test_converse_transformation.py | 59 +++++++++++++++++++ 3 files changed, 85 insertions(+), 12 deletions(-) diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 0ca0e07d4d0..b1f927fd8f9 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -326,19 +326,13 @@ class AnthropicModelInfo(BaseLLMModelInfo): ) @staticmethod - def _apply_forced_tool_choice( - model: str, - tool_choice: AnthropicMessagesToolChoice, - drop_params: bool, - ) -> AnthropicMessagesToolChoice: - """Forward ``tool_choice`` unless the model map flags the model with + def forced_tool_use_downgraded(model: str, drop_params: bool) -> bool: + """True when the model map flags the model with ``supports_forced_tool_use: false`` (Fable 5.1 / Mythos 5.1 400 on - ``any``/``tool``), in which case downgrade to ``auto`` (with - drop_params) or raise a clean client-side 400.""" - if tool_choice["type"] not in ("any", "tool"): - return tool_choice + ``any``/``tool``) and ``drop_params`` asks for the ``auto`` downgrade; + raises a clean client-side 400 for such models without ``drop_params``.""" if AnthropicModelInfo._get_model_capability(model, "supports_forced_tool_use") is not False: - return tool_choice + return False if not (litellm.drop_params or drop_params): raise litellm.utils.UnsupportedParamsError( message=( @@ -349,6 +343,18 @@ class AnthropicModelInfo(BaseLLMModelInfo): status_code=400, ) litellm.verbose_logger.warning(DROP_FORCED_TOOL_CHOICE_WARNING, model) + return True + + @staticmethod + def _apply_forced_tool_choice( + model: str, + tool_choice: AnthropicMessagesToolChoice, + drop_params: bool, + ) -> AnthropicMessagesToolChoice: + if tool_choice["type"] not in ("any", "tool"): + return tool_choice + if not AnthropicModelInfo.forced_tool_use_downgraded(model, drop_params): + return tool_choice disable_parallel: Final = tool_choice.get("disable_parallel_tool_use") if disable_parallel is None: return AnthropicMessagesToolChoice(type="auto") diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index 395d99a4caa..6f99f572686 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -588,6 +588,10 @@ class AmazonConverseConfig(BaseConfig): supported_params.append("context_management") return supported_params + @staticmethod + def _auto_tool_choice() -> ToolChoiceValuesBlock: + return ToolChoiceValuesBlock(auto={}) + def map_tool_choice_values( self, model: str, tool_choice: str | dict, drop_params: bool ) -> ToolChoiceValuesBlock | None: @@ -600,10 +604,14 @@ class AmazonConverseConfig(BaseConfig): status_code=400, ) elif tool_choice == "required": + if AnthropicModelInfo.forced_tool_use_downgraded(model, drop_params): + return self._auto_tool_choice() return ToolChoiceValuesBlock(any={}) elif tool_choice == "auto": - return ToolChoiceValuesBlock(auto={}) + return self._auto_tool_choice() elif isinstance(tool_choice, dict): + if AnthropicModelInfo.forced_tool_use_downgraded(model, drop_params): + return self._auto_tool_choice() # only supported for anthropic + mistral models - https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ToolChoice.html specific_tool: Final = SpecificToolChoiceBlock( name=make_valid_bedrock_tool_name(tool_choice.get("function", {}).get("name", "")) diff --git a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py index 63f895e1819..37ca801a7a7 100644 --- a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py @@ -6449,3 +6449,62 @@ def test_disabled_thinking_omitted_for_always_on_models_converse( assert "thinking" not in additional else: assert additional.get("thinking") == {"type": "disabled"} + +@pytest.mark.parametrize( + "model", + ["anthropic.claude-fable-5-1", "us.anthropic.claude-fable-5-1"], +) +@pytest.mark.parametrize( + "tool_choice", + ["required", {"type": "function", "function": {"name": "get_weather"}}], +) +def test_forced_tool_choice_downgraded_to_auto_on_fable_5_1_converse( + local_model_cost_map, model, tool_choice +): + config = AmazonConverseConfig() + + result = config.map_tool_choice_values( + model=model, tool_choice=tool_choice, drop_params=True + ) + + assert result == {"auto": {}} + + +@pytest.mark.parametrize( + "tool_choice", + ["required", {"type": "function", "function": {"name": "get_weather"}}], +) +def test_forced_tool_choice_raises_clean_error_on_fable_5_1_converse( + local_model_cost_map, tool_choice, monkeypatch +): + monkeypatch.setattr(litellm, "drop_params", False) + config = AmazonConverseConfig() + + with pytest.raises(litellm.utils.UnsupportedParamsError, match="forced tool use"): + config.map_tool_choice_values( + model="anthropic.claude-fable-5-1", tool_choice=tool_choice, drop_params=False + ) + + +@pytest.mark.parametrize("tool_choice", ["auto", "none"]) +def test_unforced_tool_choice_unaffected_on_fable_5_1_converse(local_model_cost_map, tool_choice): + config = AmazonConverseConfig() + + result = config.map_tool_choice_values( + model="anthropic.claude-fable-5-1", tool_choice=tool_choice, drop_params=True + ) + + assert result == ({"auto": {}} if tool_choice == "auto" else None) + + +def test_forced_tool_choice_forwarded_on_converse_models_that_support_it( + local_model_cost_map, monkeypatch +): + monkeypatch.setattr(litellm, "drop_params", False) + config = AmazonConverseConfig() + + result = config.map_tool_choice_values( + model="anthropic.claude-fable-5", tool_choice="required", drop_params=False + ) + + assert result == {"any": {}}