From 2b77493e32dffb9243f2ed99c26bc330e14a31b6 Mon Sep 17 00:00:00 2001 From: mateo Date: Sat, 31 Oct 2026 22:37:02 +0000 Subject: [PATCH] fix(anthropic-messages): map tool_choice.disable_parallel_tool_use to parallel_tool_calls=false on the chat and responses adapters Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../messages/adapters/transformation.py | 7 ++--- .../responses_adapters/transformation.py | 7 ++--- ...hropic_messages_adapters_transformation.py | 27 +++++++++++++++++++ .../test_responses_adapters_transformation.py | 16 +++++++++++ 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/litellm/llms/anthropic/messages/adapters/transformation.py b/litellm/llms/anthropic/messages/adapters/transformation.py index 4b1b5a8a11a..ace202fe057 100644 --- a/litellm/llms/anthropic/messages/adapters/transformation.py +++ b/litellm/llms/anthropic/messages/adapters/transformation.py @@ -1101,9 +1101,10 @@ class LiteLLMAnthropicMessagesAdapter: tool_choice: Final = anthropic_message_request["tool_choice"] if not tool_choice: return - new_kwargs["tool_choice"] = self.translate_anthropic_tool_choice_to_openai( - tool_choice=cast(AnthropicMessagesToolChoice, tool_choice) - ) + anthropic_tool_choice: Final = cast(AnthropicMessagesToolChoice, tool_choice) + new_kwargs["tool_choice"] = self.translate_anthropic_tool_choice_to_openai(tool_choice=anthropic_tool_choice) + if anthropic_tool_choice.get("disable_parallel_tool_use") is True: + new_kwargs["parallel_tool_calls"] = False # rebind-ok: out-param store like tool_choice above def _translate_stop_sequences_to_openai( self, diff --git a/litellm/llms/anthropic/messages/responses_adapters/transformation.py b/litellm/llms/anthropic/messages/responses_adapters/transformation.py index 0630bef0311..964189ca7da 100644 --- a/litellm/llms/anthropic/messages/responses_adapters/transformation.py +++ b/litellm/llms/anthropic/messages/responses_adapters/transformation.py @@ -656,9 +656,10 @@ class LiteLLMAnthropicToResponsesAPIAdapter: # tool_choice tool_choice: Final = anthropic_request.get("tool_choice") if tool_choice: - responses_kwargs["tool_choice"] = self.translate_tool_choice_to_responses_api( - cast(AnthropicMessagesToolChoice, tool_choice) - ) + anthropic_tool_choice: Final = cast(AnthropicMessagesToolChoice, tool_choice) + responses_kwargs["tool_choice"] = self.translate_tool_choice_to_responses_api(anthropic_tool_choice) + if anthropic_tool_choice.get("disable_parallel_tool_use") is True: + responses_kwargs["parallel_tool_calls"] = False # thinking -> reasoning thinking: Final = anthropic_request.get("thinking") diff --git a/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py b/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py index cc366dcde4c..a3670a2d7ba 100644 --- a/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py +++ b/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py @@ -5527,3 +5527,30 @@ async def test_chat_bridge_tool_result_text_block_without_text_reaches_the_model ], } ] + + +@pytest.mark.parametrize( + ("tool_choice", "expected_parallel_tool_calls"), + [ + ({"type": "auto", "disable_parallel_tool_use": True}, False), + ({"type": "any", "disable_parallel_tool_use": True}, False), + ({"type": "auto", "disable_parallel_tool_use": False}, None), + ({"type": "auto"}, None), + ], +) +def test_translate_anthropic_to_openai_disable_parallel_tool_use_sets_parallel_tool_calls( + tool_choice: dict[str, object], expected_parallel_tool_calls: bool | None +) -> None: + from litellm.types.llms.anthropic import AnthropicMessagesRequest + + request: Final = AnthropicMessagesRequest( + model="gpt-5.4", + max_tokens=64, + messages=[{"role": "user", "content": "What is the weather in Paris?"}], + tools=[{"name": "lookup_weather", "input_schema": {"type": "object", "properties": {}}}], + tool_choice=tool_choice, + ) + + translated, _ = LiteLLMAnthropicMessagesAdapter().translate_anthropic_to_openai(request) + + assert translated.get("parallel_tool_calls") == expected_parallel_tool_calls diff --git a/tests/unit/llms/anthropic/messages/responses_adapters/test_responses_adapters_transformation.py b/tests/unit/llms/anthropic/messages/responses_adapters/test_responses_adapters_transformation.py index ece0cf5d423..ba6410fd970 100644 --- a/tests/unit/llms/anthropic/messages/responses_adapters/test_responses_adapters_transformation.py +++ b/tests/unit/llms/anthropic/messages/responses_adapters/test_responses_adapters_transformation.py @@ -1178,6 +1178,22 @@ class TestTranslateRequestBroaderCoverage: kwargs = _ADAPTER.translate_request(req) assert kwargs["tool_choice"] == {"type": "function", "name": "do_thing"} + @pytest.mark.parametrize( + ("tool_choice", "expected_parallel_tool_calls"), + [ + ({"type": "auto", "disable_parallel_tool_use": True}, False), + ({"type": "any", "disable_parallel_tool_use": True}, False), + ({"type": "auto", "disable_parallel_tool_use": False}, None), + ({"type": "auto"}, None), + ], + ) + def test_disable_parallel_tool_use_sets_parallel_tool_calls( + self, tool_choice: dict[str, object], expected_parallel_tool_calls: bool | None + ): + req = _make_request(tools=[{"name": "do_thing"}], tool_choice=tool_choice) + kwargs = _ADAPTER.translate_request(req) + assert kwargs.get("parallel_tool_calls") == expected_parallel_tool_calls + def test_thinking_translated_to_reasoning(self): req = _make_request(thinking={"type": "enabled", "budget_tokens": 12000}) kwargs = _ADAPTER.translate_request(req)