Local /v1/messages: invert disable_parallel_tool_use into parallel_tool_calls (PR #5711)

Anthropic Messages API nests `disable_parallel_tool_use` inside the
`tool_choice` object (per docs.claude.com/parallel-tool-use). The local
Anthropic-compat endpoint dropped that flag because the OpenAI shape it
translates into uses a different name and lives at the top level
instead. SDK clients (anthropic-python, anthropic-sdk-go, etc.) that
already speak this dialect therefore could not opt out of parallel
tool calls against the local GGUF model.

Extract `disable_parallel_tool_use` from the incoming tool_choice and
invert it to `parallel_tool_calls` on the agentic-loop call. Plain-chat
and existing tool_choice shapes are untouched. Added a focused unit
test that pins the dict/None/bool/string boundary cases.
This commit is contained in:
Daniel Han 2026-05-24 16:23:23 +00:00
commit ad36aaa71d
2 changed files with 36 additions and 0 deletions

View file

@ -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:

View file

@ -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