fix: omit toolConfig when tool_choice="none" in BedrockChatClient (#4535)

Bedrock's Converse API only accepts "auto", "any", or "tool" as valid
toolChoice keys. The previous code mapped tool_choice="none" to
{"none": {}}, which causes a botocore.exceptions.ParamValidationError.

When tool_choice="none" (set by FunctionInvocationLayer after exhausting
max iterations), the fix now omits toolConfig entirely so the model
won't attempt tool calls.

Added tests for tool_choice="none", "auto", and "required" modes.

Fixes #4529

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
This commit is contained in:
L. Elaine Dazzio
2026-03-12 14:49:08 -04:00
committed by GitHub
Unverified
parent ed2fb3b9dd
commit b6a1315386
2 changed files with 80 additions and 3 deletions
@@ -405,11 +405,16 @@ class BedrockChatClient(
tool_config = self._prepare_tools(options.get("tools"))
if tool_mode := validate_tool_mode(options.get("tool_choice")):
tool_config = tool_config or {}
match tool_mode.get("mode"):
case "auto" | "none":
tool_config["toolChoice"] = {tool_mode.get("mode"): {}}
case "none":
# Bedrock doesn't support toolChoice "none".
# Omit toolConfig entirely so the model won't attempt tool calls.
tool_config = None
case "auto":
tool_config = tool_config or {}
tool_config["toolChoice"] = {"auto": {}}
case "required":
tool_config = tool_config or {}
if required_name := tool_mode.get("required_function_name"):
tool_config["toolChoice"] = {"tool": {"name": required_name}}
else:
@@ -31,6 +31,15 @@ class _StubBedrockRuntime:
}
def _make_client() -> BedrockChatClient:
"""Create a BedrockChatClient with a stub runtime for unit tests."""
return BedrockChatClient(
model_id="amazon.titan-text",
region="us-west-2",
client=_StubBedrockRuntime(),
)
async def test_get_response_invokes_bedrock_runtime() -> None:
stub = _StubBedrockRuntime()
client = BedrockChatClient(
@@ -65,3 +74,66 @@ def test_build_request_requires_non_system_messages() -> None:
with pytest.raises(ValueError):
client._prepare_options(messages, {})
def test_prepare_options_tool_choice_none_omits_tool_config() -> None:
"""When tool_choice='none', toolConfig must be omitted entirely.
Bedrock's Converse API only accepts 'auto', 'any', or 'tool' as valid
toolChoice keys. Sending {"none": {}} causes a ParamValidationError.
The fix omits toolConfig so the model won't attempt tool calls.
Fixes #4529.
"""
client = _make_client()
messages = [Message(role="user", contents=[Content.from_text(text="hello")])]
# Even when tools are provided, tool_choice="none" should strip toolConfig
options: dict[str, Any] = {
"tool_choice": "none",
"tools": [
{"toolSpec": {"name": "get_weather", "description": "Get weather", "inputSchema": {"json": {}}}},
],
}
request = client._prepare_options(messages, options)
assert "toolConfig" not in request, (
f"toolConfig should be omitted when tool_choice='none', got: {request.get('toolConfig')}"
)
def test_prepare_options_tool_choice_auto_includes_tool_config() -> None:
"""When tool_choice='auto', toolConfig.toolChoice should be {'auto': {}}."""
client = _make_client()
messages = [Message(role="user", contents=[Content.from_text(text="hello")])]
options: dict[str, Any] = {
"tool_choice": "auto",
"tools": [
{"toolSpec": {"name": "get_weather", "description": "Get weather", "inputSchema": {"json": {}}}},
],
}
request = client._prepare_options(messages, options)
assert "toolConfig" in request
assert request["toolConfig"]["toolChoice"] == {"auto": {}}
def test_prepare_options_tool_choice_required_includes_any() -> None:
"""When tool_choice='required' (no specific function), toolChoice should be {'any': {}}."""
client = _make_client()
messages = [Message(role="user", contents=[Content.from_text(text="hello")])]
options: dict[str, Any] = {
"tool_choice": "required",
"tools": [
{"toolSpec": {"name": "get_weather", "description": "Get weather", "inputSchema": {"json": {}}}},
],
}
request = client._prepare_options(messages, options)
assert "toolConfig" in request
assert request["toolConfig"]["toolChoice"] == {"any": {}}