From 518d0a5570d12e3afde8bc02084f6814dd8360f0 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 27 May 2026 14:15:04 +0000 Subject: [PATCH] Studio: correct tool-call parser docstrings for PR #5811 Three label corrections surfaced while cross-checking the parser against official HF chat templates. Comment-only, no behaviour change; 104/104 tests still pass. 1. ```` XML was labelled "Qwen3.5 xml". The canonical emitter is Qwen3-Coder and it always wraps the block inside an outer ..., never bare. Updated labels in the module docstring and the three inline comments. 2. Mistral grouping was wrong on two counts: Ministral-8B-2410 uses Tekken V3 and emits the ``[TOOL_CALLS] [...]`` array form, not ``[ARGS]``. Mistral-Large-2411 ships tokenizer.model.v7 (no ``[ARGS]`` token) and also emits the array form. ``[ARGS]`` only enters with Tekken V13 (Devstral, Magistral-Small-2509). Regrouped the doc lines accordingly. 3. Gemma 4 ``<|tool_call>`` is forward-looking. Neither gemma-3-12b-it nor gemma-3n-E4B-it emit this shape in their chat templates today. Noted that the capability gate correctly suppresses the tools pill on real Gemma 3 templates, so the parser stays as ready-when-Google-ships scaffolding. --- .../core/inference/tool_call_parser.py | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/studio/backend/core/inference/tool_call_parser.py b/studio/backend/core/inference/tool_call_parser.py index 9f603b7839..13156ab520 100644 --- a/studio/backend/core/inference/tool_call_parser.py +++ b/studio/backend/core/inference/tool_call_parser.py @@ -8,14 +8,14 @@ Covers the emission formats so the safetensors + MLX agentic loop sees the same call shape llama-server normalises for GGUF: - ``{json}`` (Qwen / Hermes) - - ``v`` (Qwen3.5 xml) + - ``v`` (Qwen3-Coder XML; nested in ) - ``<|python_tag|>NAME.call(k="v", ...)`` (Llama-3 built-in tools) - ``<|python_tag|>{"name":..., "parameters":...}`` (Llama-3 custom) - ``{"name":..., "parameters":...}`` (Llama-3.2 bare JSON) - - ``[TOOL_CALLS] [{...}, ...]`` (Mistral v0.3 / Nemo / Small) - - ``[TOOL_CALLS]name{json}`` (Mistral v11+ / Magistral) - - ``[TOOL_CALLS]name[ARGS]{json}`` (Ministral / Mistral Large 3) - - ``<|tool_call>call:NAME{k:<|"|>v<|"|>}`` (Gemma 4) + - ``[TOOL_CALLS] [{...}, ...]`` (Mistral V3 tokenizer: v0.3, Nemo, Small, Ministral-8B-2410, Large-2411) + - ``[TOOL_CALLS]name{json}`` (Mistral V11+ Tekken: Magistral) + - ``[TOOL_CALLS]name[ARGS]{json}`` (Mistral V13 Tekken: Devstral, Magistral-Small-2509) + - ``<|tool_call>call:NAME{k:<|"|>v<|"|>}`` (Gemma 4; forward-looking, no shipping Gemma model emits this yet) Closing tags / brackets are tolerated when missing because models frequently truncate them mid-stream. @@ -85,7 +85,7 @@ BUDGET_EXHAUSTED_NUDGE = ( # Qwen / Hermes ``{json}``. _TC_JSON_START_RE = re.compile(r"\s*\{") -# Qwen3.5 / Hermes XML ``v``. +# Qwen3-Coder XML ``v``, nested inside . _TC_FUNC_START_RE = re.compile(r"\s*") _TC_END_TAG_RE = re.compile(r"") _TC_FUNC_CLOSE_RE = re.compile(r"\s*\s*$") @@ -102,14 +102,17 @@ _LLAMA3_KV_RE = re.compile( re.VERBOSE, ) -# Mistral ``[TOOL_CALLS]`` trigger. v11+ chains them, each followed by -# a bare name plus ``{json}`` (Magistral) or ``[ARGS]{json}`` (Ministral -# / Large 3). +# Mistral ``[TOOL_CALLS]`` trigger. V11+ Tekken chains them, each +# followed by a bare name plus ``{json}`` (Magistral) or +# ``[ARGS]{json}`` (V13 Tekken: Devstral, Magistral-Small-2509). +# V3 tokenizer models (v0.3, Nemo, Small, Ministral-8B-2410, +# Large-2411) emit the ``[TOOL_CALLS] [...]`` array form instead. _MISTRAL_TRIGGER = "[TOOL_CALLS]" _MISTRAL_ARGS_MARKER = "[ARGS]" _MISTRAL_V11_NAME_RE = re.compile(r"\s*([\w\.\-]+)\s*") -# Gemma 4: ``<|tool_call>call:NAME{...}``, ``<|"|>`` wraps strings. +# Gemma 4 (forward-looking; Gemma 3 does not emit this shape yet): +# ``<|tool_call>call:NAME{...}``, ``<|"|>`` wraps strings. _GEMMA_TC_RE = re.compile(r"<\|tool_call>\s*call\s*:\s*([\w\.\-]+)\s*\{") _GEMMA_STR_BEGIN = '<|"|>' _GEMMA_STR_END = '<|"|>' @@ -223,10 +226,10 @@ def parse_tool_calls_from_text(content: str, *, id_offset: int = 0) -> list[dict as soon as one matches so we never double-count.""" for parser in ( _parse_tool_call_json, # Qwen / Hermes - _parse_function_xml, # Qwen3.5 / Hermes XML + _parse_function_xml, # Qwen3-Coder XML _parse_llama3_python_tag, # Llama-3 _parse_mistral_tool_calls, # Mistral - _parse_gemma_tool_calls, # Gemma 4 + _parse_gemma_tool_calls, # Gemma 4 (forward-looking) ): calls = parser(content, id_offset = id_offset) if calls: @@ -642,7 +645,9 @@ def _consume_mistral_call(obj_text: str, out: list[dict], id_offset: int) -> Non def _parse_gemma_tool_calls(content: str, *, id_offset: int) -> list[dict]: - """Gemma 4: ``<|tool_call>call:NAME{k:<|"|>v<|"|>, ...}``.""" + """Gemma 4 (forward-looking; no shipping Gemma 3 model emits this): + ``<|tool_call>call:NAME{k:<|"|>v<|"|>, ...}``. + Capability gate suppresses the tools pill on real Gemma 3 templates.""" out: list[dict] = [] for m in _GEMMA_TC_RE.finditer(content): name = m.group(1)