diff --git a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py index 5cc91b1efed..5a966416328 100644 --- a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py +++ b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py @@ -1144,14 +1144,18 @@ def _prepare_tools_for_anthropic( request_state.tool_name_aliases = {} # Process tool choice + allow_multiple = options.get("allow_multiple_tool_calls") if options.get("tool_choice") is None: + # Anthropic only exposes parallel tool use through tool_choice, so carry the setting + # on its default "auto" mode (e.g. after the function loop resets tool_choice="required"). + if allow_multiple is not None and result.get("tools"): + result["tool_choice"] = {"type": "auto", "disable_parallel_tool_use": not allow_multiple} return result or None tool_mode = validate_tool_mode(options.get("tool_choice")) if tool_mode is None: return result or None if "allowed_tools" in tool_mode: logger.warning("allowed_tools is not supported by Anthropic; the setting will be ignored") - allow_multiple = options.get("allow_multiple_tool_calls") match tool_mode.get("mode"): case "auto": tool_choice: dict[str, Any] = {"type": "auto"} diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index 0b330a469cb..dee2c4bdcd6 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -1417,6 +1417,82 @@ async def test_prepare_options_with_tool_choice_auto( assert "allow_multiple_tool_calls" not in run_options +async def test_prepare_options_allow_multiple_tool_calls_without_tool_choice( + mock_anthropic_client: MagicMock, +) -> None: + """allow_multiple_tool_calls=False must disable parallel tool use even when tool_choice is unset.""" + client = create_test_anthropic_client(mock_anthropic_client) + + @tool(approval_mode="never_require") + def get_weather(city: str) -> str: + """Get the weather.""" + return f"sunny in {city}" + + messages = [Message(role="user", contents=["Weather in Paris and Rome?"])] + run_options = client._prepare_options(messages, {"tools": [get_weather], "allow_multiple_tool_calls": False}) + + assert run_options["tool_choice"]["type"] == "auto" + assert run_options["tool_choice"]["disable_parallel_tool_use"] is True + + +async def test_function_loop_keeps_parallel_tool_use_disabled_after_required_tool_choice( + mock_anthropic_client: MagicMock, +) -> None: + """After the loop resets tool_choice='required', later requests must still disable parallel tool use.""" + client = create_test_anthropic_client(mock_anthropic_client) + + @tool(approval_mode="never_require") + def get_weather(city: str) -> str: + """Get the weather.""" + return f"sunny in {city}" + + usage = BetaUsage(input_tokens=10, output_tokens=5) + mock_anthropic_client.beta.messages.create.side_effect = [ + BetaMessage( + id="msg_1", + type="message", + role="assistant", + model="claude-test", + content=[BetaToolUseBlock(type="tool_use", id="toolu_1", name="get_weather", input={"city": "Paris"})], + stop_reason="tool_use", + usage=usage, + ), + BetaMessage( + id="msg_2", + type="message", + role="assistant", + model="claude-test", + content=[BetaToolUseBlock(type="tool_use", id="toolu_2", name="get_weather", input={"city": "Rome"})], + stop_reason="tool_use", + usage=usage, + ), + BetaMessage( + id="msg_3", + type="message", + role="assistant", + model="claude-test", + content=[BetaTextBlock(type="text", text="Sunny in both.")], + stop_reason="end_turn", + usage=usage, + ), + ] + + await client.get_response( + [Message(role="user", contents=["Weather in Paris and Rome?"])], + options={"tools": [get_weather], "tool_choice": "required", "allow_multiple_tool_calls": False}, + ) + + calls = mock_anthropic_client.beta.messages.create.call_args_list + assert len(calls) == 3 + assert calls[0].kwargs["tool_choice"]["type"] == "any" + assert calls[0].kwargs["tool_choice"]["disable_parallel_tool_use"] is True + for call in calls[1:]: + tool_choice = call.kwargs.get("tool_choice") + assert tool_choice is not None + assert tool_choice["type"] == "auto" + assert tool_choice["disable_parallel_tool_use"] is True + + async def test_prepare_options_with_tool_choice_required( mock_anthropic_client: MagicMock, ) -> None: