Skip to content

Commit b6a1315

Browse files
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>
1 parent ed2fb3b commit b6a1315

2 files changed

Lines changed: 80 additions & 3 deletions

File tree

python/packages/bedrock/agent_framework_bedrock/_chat_client.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -405,11 +405,16 @@ def _prepare_options(
405405

406406
tool_config = self._prepare_tools(options.get("tools"))
407407
if tool_mode := validate_tool_mode(options.get("tool_choice")):
408-
tool_config = tool_config or {}
409408
match tool_mode.get("mode"):
410-
case "auto" | "none":
411-
tool_config["toolChoice"] = {tool_mode.get("mode"): {}}
409+
case "none":
410+
# Bedrock doesn't support toolChoice "none".
411+
# Omit toolConfig entirely so the model won't attempt tool calls.
412+
tool_config = None
413+
case "auto":
414+
tool_config = tool_config or {}
415+
tool_config["toolChoice"] = {"auto": {}}
412416
case "required":
417+
tool_config = tool_config or {}
413418
if required_name := tool_mode.get("required_function_name"):
414419
tool_config["toolChoice"] = {"tool": {"name": required_name}}
415420
else:

python/packages/bedrock/tests/test_bedrock_client.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ def converse(self, **kwargs: Any) -> dict[str, Any]:
3131
}
3232

3333

34+
def _make_client() -> BedrockChatClient:
35+
"""Create a BedrockChatClient with a stub runtime for unit tests."""
36+
return BedrockChatClient(
37+
model_id="amazon.titan-text",
38+
region="us-west-2",
39+
client=_StubBedrockRuntime(),
40+
)
41+
42+
3443
async def test_get_response_invokes_bedrock_runtime() -> None:
3544
stub = _StubBedrockRuntime()
3645
client = BedrockChatClient(
@@ -65,3 +74,66 @@ def test_build_request_requires_non_system_messages() -> None:
6574

6675
with pytest.raises(ValueError):
6776
client._prepare_options(messages, {})
77+
78+
79+
def test_prepare_options_tool_choice_none_omits_tool_config() -> None:
80+
"""When tool_choice='none', toolConfig must be omitted entirely.
81+
82+
Bedrock's Converse API only accepts 'auto', 'any', or 'tool' as valid
83+
toolChoice keys. Sending {"none": {}} causes a ParamValidationError.
84+
The fix omits toolConfig so the model won't attempt tool calls.
85+
86+
Fixes #4529.
87+
"""
88+
client = _make_client()
89+
messages = [Message(role="user", contents=[Content.from_text(text="hello")])]
90+
91+
# Even when tools are provided, tool_choice="none" should strip toolConfig
92+
options: dict[str, Any] = {
93+
"tool_choice": "none",
94+
"tools": [
95+
{"toolSpec": {"name": "get_weather", "description": "Get weather", "inputSchema": {"json": {}}}},
96+
],
97+
}
98+
99+
request = client._prepare_options(messages, options)
100+
101+
assert "toolConfig" not in request, (
102+
f"toolConfig should be omitted when tool_choice='none', got: {request.get('toolConfig')}"
103+
)
104+
105+
106+
def test_prepare_options_tool_choice_auto_includes_tool_config() -> None:
107+
"""When tool_choice='auto', toolConfig.toolChoice should be {'auto': {}}."""
108+
client = _make_client()
109+
messages = [Message(role="user", contents=[Content.from_text(text="hello")])]
110+
111+
options: dict[str, Any] = {
112+
"tool_choice": "auto",
113+
"tools": [
114+
{"toolSpec": {"name": "get_weather", "description": "Get weather", "inputSchema": {"json": {}}}},
115+
],
116+
}
117+
118+
request = client._prepare_options(messages, options)
119+
120+
assert "toolConfig" in request
121+
assert request["toolConfig"]["toolChoice"] == {"auto": {}}
122+
123+
124+
def test_prepare_options_tool_choice_required_includes_any() -> None:
125+
"""When tool_choice='required' (no specific function), toolChoice should be {'any': {}}."""
126+
client = _make_client()
127+
messages = [Message(role="user", contents=[Content.from_text(text="hello")])]
128+
129+
options: dict[str, Any] = {
130+
"tool_choice": "required",
131+
"tools": [
132+
{"toolSpec": {"name": "get_weather", "description": "Get weather", "inputSchema": {"json": {}}}},
133+
],
134+
}
135+
136+
request = client._prepare_options(messages, options)
137+
138+
assert "toolConfig" in request
139+
assert request["toolConfig"]["toolChoice"] == {"any": {}}

0 commit comments

Comments
 (0)