From b6a1315386a0c82331ae05b03bf4c542b1c200a6 Mon Sep 17 00:00:00 2001 From: "L. Elaine Dazzio" Date: Thu, 12 Mar 2026 14:49:08 -0400 Subject: [PATCH] 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 --- .../agent_framework_bedrock/_chat_client.py | 11 ++- .../bedrock/tests/test_bedrock_client.py | 72 +++++++++++++++++++ 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py index 5bc9735846..40b15fb6ba 100644 --- a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py +++ b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py @@ -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: diff --git a/python/packages/bedrock/tests/test_bedrock_client.py b/python/packages/bedrock/tests/test_bedrock_client.py index e2a2f71750..1566bff234 100644 --- a/python/packages/bedrock/tests/test_bedrock_client.py +++ b/python/packages/bedrock/tests/test_bedrock_client.py @@ -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": {}}