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