From 40faf44f7a1891dc84e5e798e8403e8ce5ee82c2 Mon Sep 17 00:00:00 2001 From: Xinyuan Tong <115166877+JustinTong0323@users.noreply.github.com> Date: Thu, 21 May 2026 07:34:52 +0100 Subject: [PATCH] [auto-detect] match Ring-2.6/Ling XML kv tool-call format via vocab signature (#25366) --- .../sglang/srt/managers/template_detection.py | 13 ++++++++++ .../unit/managers/test_template_manager.py | 24 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/python/sglang/srt/managers/template_detection.py b/python/sglang/srt/managers/template_detection.py index 7efe8b6ff..0198e75f0 100644 --- a/python/sglang/srt/managers/template_detection.py +++ b/python/sglang/srt/managers/template_detection.py @@ -199,6 +199,16 @@ def _is_glm45(ctx): ) +def _is_xml_kv_tool_call(ctx): + # Structural signature for the GLM-4.5 / GLM-4.6 style tool-call format + # (`namek\nv...`). + # Matches any model whose tokenizer carries `` and `` as + # added tokens — e.g., inclusionAI/Ring-2.6, which borrows GLM's tool-call + # format but doesn't share the `[gMASK]` / `enable_thinking` family + # signature checked by `_is_glm45`. + return ctx.has_vocab("") and ctx.has_vocab("") + + def _is_mimo(ctx): return ctx.reasoning_config == ReasoningToggleConfig( toggle_param="enable_thinking", default_enabled=False @@ -268,6 +278,9 @@ TOOL_CALL_PARSER_RULES = ( DetectionRule(name="interns1", value="interns1", predicate=_is_interns1), DetectionRule(name="mistral", value="mistral", predicate=_is_mistral), DetectionRule(name="glm45", value="glm45", predicate=_is_glm45), + DetectionRule( + name="xml_kv_tool_call", value="glm45", predicate=_is_xml_kv_tool_call + ), DetectionRule(name="mimo", value="mimo", predicate=_is_mimo), DetectionRule(name="qwen", value="qwen", predicate=_is_qwen3), DetectionRule(name="deepseek_v3", value="deepseekv3", predicate=_is_deepseek_v3), diff --git a/test/registered/unit/managers/test_template_manager.py b/test/registered/unit/managers/test_template_manager.py index dd4db0c41..fe7d40c00 100644 --- a/test/registered/unit/managers/test_template_manager.py +++ b/test/registered/unit/managers/test_template_manager.py @@ -2,6 +2,7 @@ import unittest from types import SimpleNamespace from sglang.srt.managers.template_detection import ( + TOOL_CALL_PARSER_RULES, ReasoningToggleConfig, detect_reasoning_parser, detect_reasoning_pattern, @@ -265,6 +266,12 @@ class TestToolCallParserDetection(unittest.TestCase): ["<|tool_calls_section_begin|>"], "kimi_k2", ), + ( + "xml_kv_tool_call_via_vocab", + "{% set reasoning_effort = reasoning_effort | default('high', true) %}\n", + ["", "", "", "<|endoftext|>"], + "glm45", + ), ] for name, template, vocab, expected in cases: with self.subTest(name=name): @@ -274,6 +281,23 @@ class TestToolCallParserDetection(unittest.TestCase): ) self.assertEqual(result, expected) + def test_glm45_rule_precedes_xml_kv_fallback(self): + # The specific GLM-4.5 family check must run before the generic + # xml_kv_tool_call fallback. Both currently map to "glm45", so the + # value-based test above can't catch a swap — assert positions directly. + rule_index = {rule.name: i for i, rule in enumerate(TOOL_CALL_PARSER_RULES)} + self.assertLess(rule_index["glm45"], rule_index["xml_kv_tool_call"]) + + def test_xml_kv_requires_both_arg_tokens(self): + template = "Hello {{ user }}" + force, config = detect_reasoning_pattern(template) + for vocab in ([""], [""], []): + with self.subTest(vocab=vocab): + result = detect_tool_call_parser( + template, _DummyTokenizer(vocab), config, force + ) + self.assertIsNone(result) + def test_none_template_returns_none(self): self.assertIsNone(detect_tool_call_parser(None, None))