From 99ae5699fb0aab3f324e171744e6d1b91f61243b Mon Sep 17 00:00:00 2001 From: Sina Azizi Date: Sun, 16 Aug 2026 13:34:13 -0700 Subject: [PATCH] fix(anthropic): default tool_choice to auto when only parallel_tool_calls is set --- src/any_llm/providers/anthropic/utils.py | 4 ++-- .../unit/providers/test_anthropic_provider.py | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/any_llm/providers/anthropic/utils.py b/src/any_llm/providers/anthropic/utils.py index cb9cc87f3..3210f89bc 100644 --- a/src/any_llm/providers/anthropic/utils.py +++ b/src/any_llm/providers/anthropic/utils.py @@ -430,7 +430,7 @@ def _convert_tool_choice(params: CompletionParams) -> dict[str, Any]: parallel_tool_calls = params.parallel_tool_calls if parallel_tool_calls is None: parallel_tool_calls = True - tool_choice = params.tool_choice or "any" + tool_choice = params.tool_choice or "auto" if tool_choice == "required": tool_choice = "any" elif isinstance(tool_choice, dict): @@ -480,7 +480,7 @@ def _convert_params(params: CompletionParams, **kwargs: Any) -> dict[str, Any]: if params.tools: params.tools = _convert_tool_spec(params.tools) - if params.tool_choice or params.parallel_tool_calls: + if params.tool_choice is not None or params.parallel_tool_calls is not None: params.tool_choice = _convert_tool_choice(params) if params.reasoning_effort is None or params.reasoning_effort == "none": diff --git a/tests/unit/providers/test_anthropic_provider.py b/tests/unit/providers/test_anthropic_provider.py index 33eef8531..0dacb06be 100644 --- a/tests/unit/providers/test_anthropic_provider.py +++ b/tests/unit/providers/test_anthropic_provider.py @@ -309,6 +309,30 @@ async def test_completion_with_tool_choice_and_parallel_tool_calls(parallel_tool ) +@pytest.mark.parametrize("parallel_tool_calls", [True, False]) +@pytest.mark.asyncio +async def test_completion_with_parallel_tool_calls_only(parallel_tool_calls: bool) -> None: + """Test that parallel_tool_calls without tool_choice maps to "auto" instead of forcing tool use.""" + api_key = "test-api-key" + model = "model-id" + messages = [{"role": "user", "content": "Hello"}] + + with mock_anthropic_provider() as mock_anthropic: + provider = AnthropicProvider(api_key=api_key) + await provider._acompletion( + CompletionParams(model_id=model, messages=messages, parallel_tool_calls=parallel_tool_calls), + ) + + expected_kwargs = {"tool_choice": {"type": "auto", "disable_parallel_tool_use": not parallel_tool_calls}} + + mock_anthropic.return_value.messages.create.assert_called_once_with( + model=model, + messages=[{"role": "user", "content": "Hello"}], + **expected_kwargs, + max_tokens=DEFAULT_MAX_TOKENS, + ) + + @pytest.mark.asyncio async def test_completion_inside_agent_loop(agent_loop_messages: list[dict[str, Any]]) -> None: api_key = "test-api-key"