diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index 9f5b07317b..3342bc2d78 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -4461,6 +4461,16 @@ async def anthropic_messages( if openai_tool_choice is None: openai_tool_choice = "auto" + # Anthropic nests `disable_parallel_tool_use` inside `tool_choice` + # (https://docs.claude.com/en/docs/agents-and-tools/tool-use/implement-tool-use). + # Flip it into the OpenAI-shaped `parallel_tool_calls` toggle so the + # local GGUF tool loop respects clients that opt out of parallel calls. + anthropic_parallel_tool_calls: Optional[bool] = None + if isinstance(payload.tool_choice, dict): + _disable = payload.tool_choice.get("disable_parallel_tool_use") + if isinstance(_disable, bool): + anthropic_parallel_tool_calls = not _disable + cancel_event = threading.Event() # ── Tool routing ────────────────────────────────────────── @@ -4666,6 +4676,7 @@ async def anthropic_messages( auto_heal_tool_calls = True, tool_call_timeout = 300, session_id = payload.session_id, + parallel_tool_calls = anthropic_parallel_tool_calls, ) if payload.stream: diff --git a/studio/backend/tests/test_sampling_params_routing.py b/studio/backend/tests/test_sampling_params_routing.py index 889fe2621e..3aad1e2d41 100644 --- a/studio/backend/tests/test_sampling_params_routing.py +++ b/studio/backend/tests/test_sampling_params_routing.py @@ -819,3 +819,28 @@ def test_chat_settings_payload_accepts_new_sampling_keys(): assert ip.stop == ["END"] assert ip.serviceTier == "standard_only" assert ip.parallelToolCalls is False + + +# ── Local /v1/messages: disable_parallel_tool_use translation ────────── + + +def test_local_anthropic_disable_parallel_tool_use_translation(): + """Anthropic nests `disable_parallel_tool_use` under `tool_choice` + (per docs.claude.com). The local /v1/messages GGUF tool path must + invert it into OpenAI-shaped `parallel_tool_calls` so third-party + clients (Claude SDK, LiteLLM in passthrough mode) opt out of + parallel calls successfully even on the local model.""" + # Mirror the extraction logic in routes/inference.py:anthropic_messages. + def _extract(tc): + if isinstance(tc, dict): + v = tc.get("disable_parallel_tool_use") + if isinstance(v, bool): + return not v + return None + + assert _extract({"type": "auto", "disable_parallel_tool_use": True}) is False + assert _extract({"type": "any", "disable_parallel_tool_use": False}) is True + assert _extract({"type": "auto"}) is None + assert _extract(None) is None + assert _extract("auto") is None # string form (non-dict) → no opinion + assert _extract({"type": "auto", "disable_parallel_tool_use": "yes"}) is None