From 00c4a401be39cc101ec62ef11111566366ef9e48 Mon Sep 17 00:00:00 2001 From: bahtya Date: Thu, 9 Apr 2026 01:35:15 +0800 Subject: [PATCH 1/3] fix(bedrock): don't send toolChoice when no tools are configured BedrockChatClient was sending toolConfig.toolChoice even when no tools were configured (tools=None). AWS Bedrock requires toolConfig.tools to be present whenever toolChoice is specified, causing a 400 validation error. Only set toolChoice when tool_config has a 'tools' key present. Fixes #5165 Signed-off-by: bahtya --- .../agent_framework_bedrock/_chat_client.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py index 3606cdf26b..bcd04de0f7 100644 --- a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py +++ b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py @@ -411,14 +411,14 @@ def _prepare_options( # 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": {}} + if tool_config and "tools" in tool_config: + 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: - tool_config["toolChoice"] = {"any": {}} + if tool_config and "tools" in tool_config: + if required_name := tool_mode.get("required_function_name"): + tool_config["toolChoice"] = {"tool": {"name": required_name}} + else: + tool_config["toolChoice"] = {"any": {}} case _: raise ValueError(f"Unsupported tool mode for Bedrock: {tool_mode.get('mode')}") if tool_config: From a46c28b73eb8606a812ad3f5a0b710bc2db4df51 Mon Sep 17 00:00:00 2001 From: bahtya Date: Mon, 13 Apr 2026 20:19:50 +0800 Subject: [PATCH 2/3] test: add tests for toolChoice without tools - test_prepare_options_tool_choice_auto_without_tools_omits_tool_config - test_prepare_options_tool_choice_required_without_tools_omits_tool_config Verifies that toolConfig is omitted when tool_choice is set but no tools are provided, preventing ParamValidationError from Bedrock. --- .../bedrock/tests/test_bedrock_client.py | 36 +++++++++++++++++++ test_addition.py | 36 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 test_addition.py diff --git a/python/packages/bedrock/tests/test_bedrock_client.py b/python/packages/bedrock/tests/test_bedrock_client.py index fbc241b24c..959f5bf5c5 100644 --- a/python/packages/bedrock/tests/test_bedrock_client.py +++ b/python/packages/bedrock/tests/test_bedrock_client.py @@ -137,3 +137,39 @@ def test_prepare_options_tool_choice_required_includes_any() -> None: assert "toolConfig" in request assert request["toolConfig"]["toolChoice"] == {"any": {}} + + + +def test_prepare_options_tool_choice_auto_without_tools_omits_tool_config() -> None: + """When tool_choice='auto' but no tools are provided, toolConfig must be omitted. + + Without tools, setting toolChoice would cause a ParamValidationError from Bedrock. + """ + client = _make_client() + messages = [Message(role="user", contents=[Content.from_text(text="hello")])] + + options: dict[str, Any] = { + "tool_choice": "auto", + } + + request = client._prepare_options(messages, options) + + assert "toolConfig" not in request, ( + f"toolConfig should be omitted when no tools are provided, got: {request.get('toolConfig')}" + ) + + +def test_prepare_options_tool_choice_required_without_tools_omits_tool_config() -> None: + """When tool_choice='required' but no tools are provided, toolConfig must be omitted.""" + client = _make_client() + messages = [Message(role="user", contents=[Content.from_text(text="hello")])] + + options: dict[str, Any] = { + "tool_choice": "required", + } + + request = client._prepare_options(messages, options) + + assert "toolConfig" not in request, ( + f"toolConfig should be omitted when no tools are provided, got: {request.get('toolConfig')}" + ) diff --git a/test_addition.py b/test_addition.py new file mode 100644 index 0000000000..ae493672bc --- /dev/null +++ b/test_addition.py @@ -0,0 +1,36 @@ + + + +def test_prepare_options_tool_choice_auto_without_tools_omits_tool_config() -> None: + """When tool_choice='auto' but no tools are provided, toolConfig must be omitted. + + Without tools, setting toolChoice would cause a ParamValidationError from Bedrock. + """ + client = _make_client() + messages = [Message(role="user", contents=[Content.from_text(text="hello")])] + + options: dict[str, Any] = { + "tool_choice": "auto", + } + + request = client._prepare_options(messages, options) + + assert "toolConfig" not in request, ( + f"toolConfig should be omitted when no tools are provided, got: {request.get('toolConfig')}" + ) + + +def test_prepare_options_tool_choice_required_without_tools_omits_tool_config() -> None: + """When tool_choice='required' but no tools are provided, toolConfig must be omitted.""" + client = _make_client() + messages = [Message(role="user", contents=[Content.from_text(text="hello")])] + + options: dict[str, Any] = { + "tool_choice": "required", + } + + request = client._prepare_options(messages, options) + + assert "toolConfig" not in request, ( + f"toolConfig should be omitted when no tools are provided, got: {request.get('toolConfig')}" + ) From 5f08beeb84308521de8e802266d4faa124c2220d Mon Sep 17 00:00:00 2001 From: bahtya Date: Fri, 17 Apr 2026 14:12:51 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20address=20maintainer=20feedback=20?= =?UTF-8?q?=E2=80=94=20remove=20stray=20test=20file,=20raise=20ValueError?= =?UTF-8?q?=20for=20required=20without=20tools?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Remove test_addition.py — stray duplicate of tests already in python/packages/bedrock/tests/test_bedrock_client.py, missing all necessary imports and would fail with NameError. 2. Change tool_choice='required' handling to raise ValueError when no tools are configured instead of silently falling through. Using 'required' without tools is a logical contradiction — the model must invoke a tool but none exist — so surfacing this as a ValueError helps callers catch the misconfiguration early. 3. Update the corresponding test to expect ValueError instead of silently omitted toolConfig. --- .../agent_framework_bedrock/_chat_client.py | 14 +++++--- .../bedrock/tests/test_bedrock_client.py | 11 +++--- test_addition.py | 36 ------------------- 3 files changed, 13 insertions(+), 48 deletions(-) delete mode 100644 test_addition.py diff --git a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py index bcd04de0f7..0bc941f0c6 100644 --- a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py +++ b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py @@ -414,11 +414,15 @@ def _prepare_options( if tool_config and "tools" in tool_config: tool_config["toolChoice"] = {"auto": {}} case "required": - if tool_config and "tools" in tool_config: - if required_name := tool_mode.get("required_function_name"): - tool_config["toolChoice"] = {"tool": {"name": required_name}} - else: - tool_config["toolChoice"] = {"any": {}} + if not (tool_config and "tools" in tool_config): + raise ValueError( + "tool_choice='required' requires at least one tool to be configured, " + "but no tools were provided." + ) + if required_name := tool_mode.get("required_function_name"): + tool_config["toolChoice"] = {"tool": {"name": required_name}} + else: + tool_config["toolChoice"] = {"any": {}} case _: raise ValueError(f"Unsupported tool mode for Bedrock: {tool_mode.get('mode')}") if tool_config: diff --git a/python/packages/bedrock/tests/test_bedrock_client.py b/python/packages/bedrock/tests/test_bedrock_client.py index 959f5bf5c5..48d1847655 100644 --- a/python/packages/bedrock/tests/test_bedrock_client.py +++ b/python/packages/bedrock/tests/test_bedrock_client.py @@ -159,8 +159,8 @@ def test_prepare_options_tool_choice_auto_without_tools_omits_tool_config() -> N ) -def test_prepare_options_tool_choice_required_without_tools_omits_tool_config() -> None: - """When tool_choice='required' but no tools are provided, toolConfig must be omitted.""" +def test_prepare_options_tool_choice_required_without_tools_raises() -> None: + """When tool_choice='required' but no tools are provided, a ValueError must be raised.""" client = _make_client() messages = [Message(role="user", contents=[Content.from_text(text="hello")])] @@ -168,8 +168,5 @@ def test_prepare_options_tool_choice_required_without_tools_omits_tool_config() "tool_choice": "required", } - request = client._prepare_options(messages, options) - - assert "toolConfig" not in request, ( - f"toolConfig should be omitted when no tools are provided, got: {request.get('toolConfig')}" - ) + with pytest.raises(ValueError, match="tool_choice='required' requires at least one tool"): + client._prepare_options(messages, options) diff --git a/test_addition.py b/test_addition.py deleted file mode 100644 index ae493672bc..0000000000 --- a/test_addition.py +++ /dev/null @@ -1,36 +0,0 @@ - - - -def test_prepare_options_tool_choice_auto_without_tools_omits_tool_config() -> None: - """When tool_choice='auto' but no tools are provided, toolConfig must be omitted. - - Without tools, setting toolChoice would cause a ParamValidationError from Bedrock. - """ - client = _make_client() - messages = [Message(role="user", contents=[Content.from_text(text="hello")])] - - options: dict[str, Any] = { - "tool_choice": "auto", - } - - request = client._prepare_options(messages, options) - - assert "toolConfig" not in request, ( - f"toolConfig should be omitted when no tools are provided, got: {request.get('toolConfig')}" - ) - - -def test_prepare_options_tool_choice_required_without_tools_omits_tool_config() -> None: - """When tool_choice='required' but no tools are provided, toolConfig must be omitted.""" - client = _make_client() - messages = [Message(role="user", contents=[Content.from_text(text="hello")])] - - options: dict[str, Any] = { - "tool_choice": "required", - } - - request = client._prepare_options(messages, options) - - assert "toolConfig" not in request, ( - f"toolConfig should be omitted when no tools are provided, got: {request.get('toolConfig')}" - )