mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
ed2fb3b9dd
commit
b6a1315386
@@ -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": {}}
|
||||
|
||||
Reference in New Issue
Block a user