Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion src/strands/models/bedrock.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@
"anthropic.claude",
]

# Models that support toolConfig.toolChoice.any in the Bedrock Converse API.
# Other model families (Meta Llama, Amazon Titan/Nova, Mistral, Cohere, etc.) reject
# toolChoice.any with a ValidationException.
_MODELS_SUPPORT_TOOL_CHOICE_ANY = [
"anthropic.claude",
]

T = TypeVar("T", bound=BaseModel)

DEFAULT_READ_TIMEOUT = 120
Expand Down Expand Up @@ -193,6 +200,39 @@ def _cache_strategy(self) -> str | None:
return "anthropic"
return None

@property
def _supports_tool_choice_any(self) -> bool:
"""Whether this model supports toolConfig.toolChoice.any in the Bedrock Converse API.

Only Anthropic Claude models support toolChoice.any. Other families (Meta Llama, Amazon
Titan/Nova, Mistral, Cohere, etc.) reject it with a ValidationException.

Returns:
True if the model supports toolChoice.any, False otherwise.
"""
model_id = self.config.get("model_id", "").lower()
return any(prefix in model_id for prefix in _MODELS_SUPPORT_TOOL_CHOICE_ANY)

def _resolve_tool_choice(self, tool_choice: ToolChoice | None) -> dict[str, Any]:
"""Resolve the effective toolChoice for a Bedrock Converse request.

Falls back from toolChoice.any to toolChoice.auto for models that do not
support toolChoice.any (e.g. Meta Llama, Amazon Titan/Nova, Mistral).

Args:
tool_choice: Requested tool choice, or None for the default.

Returns:
A toolChoice dict safe to include in the Bedrock Converse request.
"""
if tool_choice and "any" in tool_choice and not self._supports_tool_choice_any:
logger.warning(
"model_id=<%s> | toolChoice.any is not supported by this model; falling back to toolChoice.auto",
self.config.get("model_id"),
)
return {"auto": {}}
return tool_choice if tool_choice else {"auto": {}}

@override
def update_config(self, **model_config: Unpack[BedrockConfig]) -> None: # type: ignore
"""Update the Bedrock Model configuration with the provided arguments.
Expand Down Expand Up @@ -271,7 +311,11 @@ def _format_request(
else []
),
],
**({"toolChoice": tool_choice if tool_choice else {"auto": {}}}),
**(
{
"toolChoice": self._resolve_tool_choice(tool_choice),
}
),
}
}
if tool_specs
Expand Down
31 changes: 28 additions & 3 deletions tests/strands/models/test_bedrock.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,12 +502,15 @@ def test_format_request_tool_choice_auto(model, messages, model_id, tool_spec):
assert tru_request == exp_request


def test_format_request_tool_choice_any(model, messages, model_id, tool_spec):
def test_format_request_tool_choice_any(bedrock_client, messages, tool_spec):
"""toolChoice.any passes through unchanged for Anthropic Claude models."""
claude_model_id = "us.anthropic.claude-sonnet-4-20250514-v1:0"
claude_model = BedrockModel(model_id=claude_model_id)
tool_choice = {"any": {}}
tru_request = model._format_request(messages, [tool_spec], tool_choice=tool_choice)
tru_request = claude_model._format_request(messages, [tool_spec], tool_choice=tool_choice)
exp_request = {
"inferenceConfig": {},
"modelId": model_id,
"modelId": claude_model_id,
"messages": messages,
"system": [],
"toolConfig": {
Expand All @@ -519,6 +522,28 @@ def test_format_request_tool_choice_any(model, messages, model_id, tool_spec):
assert tru_request == exp_request


def test_format_request_tool_choice_any_falls_back_for_unsupported_models(
bedrock_client, messages, model_id, tool_spec
):
"""toolChoice.any falls back to toolChoice.auto for models that do not support it."""
# model_id fixture is "m1" (not a Claude model)
non_claude_model = BedrockModel(model_id=model_id)
tool_choice = {"any": {}}
tru_request = non_claude_model._format_request(messages, [tool_spec], tool_choice=tool_choice)
exp_request = {
"inferenceConfig": {},
"modelId": model_id,
"messages": messages,
"system": [],
"toolConfig": {
"tools": [{"toolSpec": tool_spec}],
"toolChoice": {"auto": {}},
},
}

assert tru_request == exp_request


def test_format_request_tool_choice_tool(model, messages, model_id, tool_spec):
tool_choice = {"tool": {"name": "test_tool"}}
tru_request = model._format_request(messages, [tool_spec], tool_choice=tool_choice)
Expand Down