diff --git a/python/sglang/srt/parser/template_detection.py b/python/sglang/srt/parser/template_detection.py index c87629ca2..0d9faa454 100644 --- a/python/sglang/srt/parser/template_detection.py +++ b/python/sglang/srt/parser/template_detection.py @@ -336,9 +336,26 @@ def _is_glm45(ctx): ) +def _is_glm53(ctx): + # GLM-5.3 keeps the GLM-4.5 prompt and tool-call format but replaces the + # enable_thinking toggle with an always-on "Reasoning Effort:" header. + return ( + ctx.has_text("[gMASK]") + and ctx.has_text("Reasoning Effort:") + and not ctx.has_text("enable_thinking") + and ctx.has_text("") + and ctx.has_text("") + and ctx.has_text("") + ) + + +def _is_glm_family(ctx): + return _is_glm45(ctx) or _is_glm53(ctx) + + def _is_glm47(ctx): - return _is_glm45(ctx) and ctx.has_pattern( - r"\{\{[-\s]*['\"]['\"]\s*\+\s*tc\.name" + return _is_glm_family(ctx) and ctx.has_pattern( + r"\{\{[-\s]*['\"]['\"]\s*[+~]\s*tc\.name" ) @@ -486,7 +503,7 @@ REASONING_PARSER_RULES = ( DetectionRule(name="gpt_oss", value="gpt-oss", predicate=_is_gpt_oss), DetectionRule(name="kimi_k2", value="kimi_k2", predicate=_is_kimi_k2), DetectionRule(name="nemotron_3", value="nemotron_3", predicate=_is_nemotron_3), - DetectionRule(name="glm45", value="glm45", predicate=_is_glm45), + DetectionRule(name="glm45", value="glm45", predicate=_is_glm_family), DetectionRule(name="hunyuan", value="hunyuan", predicate=_is_hunyuan), DetectionRule(name="poolside_v1", value="poolside_v1", predicate=_is_poolside_v1), DetectionRule(name="mimo", value="mimo", predicate=_is_mimo), @@ -526,7 +543,7 @@ TOOL_CALL_PARSER_RULES = ( DetectionRule(name="deepseek_v31", value="deepseekv31", predicate=_is_deepseek_v31), DetectionRule(name="lfm2", value="lfm2", predicate=_is_lfm2), DetectionRule(name="glm47", value="glm47", predicate=_is_glm47), - DetectionRule(name="glm45", value="glm45", predicate=_is_glm45), + DetectionRule(name="glm45", value="glm45", predicate=_is_glm_family), DetectionRule(name="minicpm5", value="minicpm5", predicate=_is_minicpm5), DetectionRule(name="hunyuan", value="hunyuan", predicate=_is_hunyuan), DetectionRule(name="poolside_v1", value="poolside_v1", predicate=_is_poolside_v1), diff --git a/test/registered/unit/parser/test_template_manager.py b/test/registered/unit/parser/test_template_manager.py index 33ebdab73..5bd6fc59d 100644 --- a/test/registered/unit/parser/test_template_manager.py +++ b/test/registered/unit/parser/test_template_manager.py @@ -38,6 +38,26 @@ def _patch_hf_transformers_utils(get_tokenizer, get_config=None): return patch.dict(sys.modules, {module.__name__: module}) +def _glm53_template(concat): + """GLM-5.3 shape: always-on thinking behind a ``Reasoning Effort:`` header + (no ``enable_thinking`` toggle) and the compact GLM-4.7 tool-call format; + HF revisions differ only in the ``+`` / ``~`` concat operator.""" + return ( + "[gMASK]\n" + "{%- set effective_reasoning_effort = reasoning_effort if reasoning_effort is defined " + "and reasoning_effort in ['low', 'high'] else 'max' -%}\n" + "<|system|>Reasoning Effort: {{ effective_reasoning_effort | capitalize }}\n" + "{% for tc in m.tool_calls %}\n" + f"{{{{- '' {concat} tc.name -}}}}\n" + "{% set _args = tc.arguments %}" + "{% for k, v in _args.items() %}" + "{{ k }}{{ v }}" + "{% endfor %}\n" + "{% endfor %}\n" + "<|assistant|>{{- '' -}}" + ) + + class TestTemplateManagerReasoningDetection(unittest.TestCase): def _detect(self, template, vocab): force, config = detect_reasoning_pattern(template) @@ -79,6 +99,23 @@ class TestTemplateManagerReasoningDetection(unittest.TestCase): ) self.assertEqual(parser, "glm45") + def test_glm53_effort_template_resolves_glm_parsers(self): + # Without an enable_thinking toggle the GLM-4.5 rule misses, and the + # template used to fall through to deepseek-r1 + the xml_kv fallback + # (glm45 tool parser), which cannot read the compact tool-call format. + vocab = ["", "", "", "<|user|>", "<|endoftext|>"] + for concat in ("+", "~"): + template = _glm53_template(concat) + with self.subTest(concat=concat): + force, config, parser = self._detect(template, vocab) + self.assertEqual(parser, "glm45") + self.assertEqual( + detect_tool_call_parser( + template, _DummyTokenizer(vocab), config, force + ), + "glm47", + ) + def test_interns1_detects_enable_thinking_default_true(self): template = """ {% set default_thinking_sys %}......{% endset %} @@ -716,6 +753,22 @@ class TestToolCallParserDetection(unittest.TestCase): ["", "", "", "<|endoftext|>"], "glm47", ), + ( + "glm47_tilde_concat_tool_call", + ( + "[gMASK]\n" + "{% set enable_thinking = enable_thinking if enable_thinking is defined else true %}\n" + "{% for tc in m.tool_calls %}\n" + "{{- '' ~ tc.name -}}\n" + "{% set _args = tc.arguments %}" + "{% for k, v in _args.items() %}" + "{{ k }}{{ v }}" + "{% endfor %}\n" + "{% endfor %}" + ), + ["", "", "", "<|endoftext|>"], + "glm47", + ), ( "glm45_newline_tool_call", ( @@ -760,6 +813,9 @@ class TestToolCallParserDetection(unittest.TestCase): rule.name: i for i, rule in enumerate(REASONING_PARSER_RULES) } self.assertLess(reasoning_index["deepseek_v4"], reasoning_index["deepseek_v3"]) + self.assertLess( + reasoning_index["glm45"], reasoning_index["deepseek_r1_think_tags"] + ) self.assertLess( reasoning_index["hunyuan"], reasoning_index["deepseek_r1_think_tags"] )