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>
This commit is contained in:
mateo 2026-09-01 19:05:03 +00:00
parent 6513f5c539
commit d816b75dd4
3 changed files with 85 additions and 12 deletions

View file

@ -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")

View file

@ -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", ""))

View file

@ -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": {}}