From 3599d6996ae0cf23b6f6939a24f143a1bbf22609 Mon Sep 17 00:00:00 2001 From: Zakaria Khchiche <45357915+Zakariakhchiche@users.noreply.github.com> Date: Fri, 2 Oct 2026 18:43:35 +0200 Subject: [PATCH 1/3] Python: Honor allow_multiple_tool_calls in AnthropicClient when tool_choice is unset --- .../anthropic/agent_framework_anthropic/_chat_client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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"} From 0a6e61e484ed84e948b65bf3a3b9d438f60a0491 Mon Sep 17 00:00:00 2001 From: Zakaria Khchiche <45357915+Zakariakhchiche@users.noreply.github.com> Date: Fri, 2 Oct 2026 18:44:07 +0200 Subject: [PATCH 2/3] Add regression tests for Anthropic parallel tool use without tool_choice --- .../anthropic/tests/test_anthropic_client.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index 0b330a469cb..6d5b317fb30 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -1417,6 +1417,77 @@ 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", "disable_parallel_tool_use": 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", "disable_parallel_tool_use": True} + for call in calls[1:]: + assert call.kwargs.get("tool_choice") == {"type": "auto", "disable_parallel_tool_use": True} + + async def test_prepare_options_with_tool_choice_required( mock_anthropic_client: MagicMock, ) -> None: From 634bc90bb3e11962a200fb8a6ade2b885cce00bc Mon Sep 17 00:00:00 2001 From: Zakaria Khchiche <45357915+Zakariakhchiche@users.noreply.github.com> Date: Fri, 2 Oct 2026 19:12:33 +0200 Subject: [PATCH 3/3] Assert tool_choice fields individually, like the neighbouring tests --- .../packages/anthropic/tests/test_anthropic_client.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index 6d5b317fb30..dee2c4bdcd6 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -1431,7 +1431,8 @@ def get_weather(city: str) -> str: 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", "disable_parallel_tool_use": True} + 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( @@ -1483,9 +1484,13 @@ def get_weather(city: str) -> str: calls = mock_anthropic_client.beta.messages.create.call_args_list assert len(calls) == 3 - assert calls[0].kwargs["tool_choice"] == {"type": "any", "disable_parallel_tool_use": True} + 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:]: - assert call.kwargs.get("tool_choice") == {"type": "auto", "disable_parallel_tool_use": True} + 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(