项目文件夹

文件
wehub-resource-sync 94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

795 行
31 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
import sys
import tempfile
import unittest
from types import ModuleType, SimpleNamespace
from unittest.mock import Mock, patch
from sglang.srt.parser.template_detection import (
REASONING_PARSER_RULES,
TOOL_CALL_PARSER_RULES,
ReasoningToggleConfig,
detect_reasoning_parser,
detect_reasoning_pattern,
detect_tool_call_parser,
resolve_auto_parsers,
)
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(2.0, "base-a-test-cpu")
class _DummyTokenizer:
def __init__(self, vocab, chat_template=None):
self._vocab = vocab
self.chat_template = chat_template
def get_vocab(self):
return {token: i for i, token in enumerate(self._vocab)}
def _patch_hf_transformers_utils(get_tokenizer, get_config=None):
module = ModuleType("sglang.srt.utils.hf_transformers_utils")
module.get_tokenizer = get_tokenizer
if get_config is not None:
module.get_config = get_config
return patch.dict(sys.modules, {module.__name__: module})
class TestTemplateManagerReasoningDetection(unittest.TestCase):
def _detect(self, template, vocab):
force, config = detect_reasoning_pattern(template)
parser = detect_reasoning_parser(
template, _DummyTokenizer(vocab), config, force
)
return force, config, parser
def test_qwen3_template_not_misclassified_as_glm45(self):
template = """
{% set enable_thinking = enable_thinking if enable_thinking is defined else true %}
{% if '</think>' in content %}
<tool_call>
"""
_, config, parser = self._detect(
template, ["<tool_call>", "<|endoftext|>", "</think>"]
)
self.assertEqual(
config,
ReasoningToggleConfig(toggle_param="enable_thinking", default_enabled=True),
)
self.assertEqual(parser, "qwen3")
def test_glm45_requires_glm_specific_template_markers(self):
template = """
[gMASK]<sop>
{% set enable_thinking = enable_thinking if enable_thinking is defined else true %}
/nothink
<tool_call>
"""
_, config, parser = self._detect(
template, ["<tool_call>", "<|endoftext|>", "<|user|>"]
)
self.assertEqual(
config,
ReasoningToggleConfig(toggle_param="enable_thinking", default_enabled=True),
)
self.assertEqual(parser, "glm45")
def test_interns1_detects_enable_thinking_default_true(self):
template = """
{% set default_thinking_sys %}...<think>...</think>{% endset %}
{% if enable_thinking is not defined or enable_thinking %}
"""
_, config, parser = self._detect(template, ["<|endoftext|>"])
self.assertEqual(
config,
ReasoningToggleConfig(toggle_param="enable_thinking", default_enabled=True),
)
self.assertEqual(parser, "interns1")
def test_nemotron_detects_uppercase_true_assignment(self):
template = """
{% set enable_thinking = enable_thinking if enable_thinking is defined else True %}
{% set truncate_history_thinking = truncate_history_thinking if truncate_history_thinking is defined else True %}
"""
_, config, parser = self._detect(template, ["<|endoftext|>"])
self.assertEqual(
config,
ReasoningToggleConfig(toggle_param="enable_thinking", default_enabled=True),
)
self.assertEqual(parser, "nemotron_3")
def test_nemotron_super_detects_low_effort_kwarg(self):
template = """
{% set enable_thinking = enable_thinking if enable_thinking is defined else True %}
{%- set low_effort = low_effort if low_effort is defined else False %}
{% set truncate_history_thinking = truncate_history_thinking if truncate_history_thinking is defined else True %}
"""
_, config, parser = self._detect(template, [""])
self.assertEqual(
config,
ReasoningToggleConfig(
toggle_param="enable_thinking",
default_enabled=True,
effort_kwarg="low_effort",
),
)
self.assertEqual(parser, "nemotron_3")
def test_minimax_uses_template_signature_without_toggle_config(self):
template = """
{%- set toolcall_begin_token = '<minimax:tool_call>' -%}
"""
_, config, parser = self._detect(template, ["<minimax:tool_call>"])
self.assertIsNone(config)
self.assertEqual(parser, "minimax")
MINIMAX_M3_TEMPLATE = (
"{%- set ns_token = ']<]minimax[>[' -%}\n"
"{%- set toolcall_begin_token = ns_token ~ '<tool_call>' -%}\n"
"<mm:think>\n"
)
def test_minimax_m3_detected_via_mm_think_signature(self):
_, config, parser = self._detect(
self.MINIMAX_M3_TEMPLATE, ["<mm:think>", "</mm:think>"]
)
self.assertIsNone(config)
self.assertEqual(parser, "minimax-m3")
def test_minimax_m2_not_misclassified_as_m3(self):
template = """
{%- set toolcall_begin_token = '<minimax:tool_call>' -%}
"""
_, _, parser = self._detect(template, ["<minimax:tool_call>"])
self.assertEqual(parser, "minimax")
class TestTemplateDetectionRuleMatrix(unittest.TestCase):
"""Table-driven tests for REASONING_PARSER_RULES and REASONING_MODE_RULES."""
def _detect(self, template, vocab=None):
if vocab is None:
vocab = []
force, config = detect_reasoning_pattern(template)
parser = detect_reasoning_parser(
template, _DummyTokenizer(vocab), config, force
)
return force, config, parser
PARSER_RULES_MATRIX = [
# (name, template_snippet, vocab, expected_parser, expected_toggle_param)
(
"apertus2509_via_unique_vocab_token",
"{% set enable_thinking = enable_thinking if enable_thinking is defined else true %}\n",
["<|inner_prefix|>"],
"apertus2509",
"enable_thinking",
),
(
"deepseek_r1_think_tags",
"<think>\nLet me reason about this\n</think>\nAnswer here",
[],
"deepseek-r1",
None, # matched by deepseek_r1_think_tags rule (has <think> text)
),
(
"deepseek_v3",
"{% if not thinking is defined %}{% set thinking = false %}{% endif %}\n"
"<think>",
[],
"deepseek-v3",
"thinking",
),
(
"deepseek_v4_dsml_tool_calls",
"{% if not thinking is defined %}{% set thinking = false %}{% endif %}\n"
'<DSMLtool_calls><DSMLinvoke name="tool"></DSMLinvoke>',
[],
"deepseek-v4",
"thinking",
),
(
"hunyuan_interleaved_thinking",
"{% set reasoning_effort = reasoning_effort | default('no_think', true) %}\n"
"{% set interleaved_thinking = interleaved_thinking | default(false, true) %}\n"
"<think>reasoning</think><tool_calls><tool_call>name<tool_sep>",
["<tool_calls>", "<tool_sep>", "<arg_key>", "<arg_value>"],
"hunyuan",
None,
),
(
"poolside_v1_enable_thinking_false",
"{% if not enable_thinking is defined %}{% set enable_thinking = false %}{% endif %}\n"
"<tool_call>{{ name }}\n<arg_key>{{ key }}</arg_key><arg_value>{{ value }}</arg_value>",
["<tool_call>", "<arg_key>", "<arg_value>"],
"poolside_v1",
"enable_thinking",
),
(
"poolside_v1_actual_template_shape",
"{% set enable_thinking = enable_thinking | default(false) %}\n"
"Wrap your thinking in '<think>', '</think>' tags, followed by a function call.\n"
"For each function call, return an unescaped XML-like object with function name "
"and arguments within '<tool_call>' and '</tool_call>' tags, like here:\n"
"<tool_call>function-name\n"
"<arg_key>argument-key</arg_key>\n"
"<arg_value>value-of-argument-key</arg_value>\n"
"</tool_call>",
["<tool_call>"],
"poolside_v1",
None,
),
(
"lfm2_not_deepseek_r1_from_history_cleanup",
"{% set keep_past_thinking = keep_past_thinking | default(false) %}\n"
"{% if not keep_past_thinking and '</think>' in content %}"
"{{ content.split('</think>')[-1] }}{% endif %}\n"
'<|tool_call_start|>[get_weather(city="Paris")]<|tool_call_end|>',
["<|tool_call_start|>", "<|tool_call_end|>"],
None,
None,
),
(
"qwen3_coder_actual_template_shape_has_no_reasoning",
"<tools>\n"
"<tool_call><function=get_weather>"
"<parameter=city>Paris</parameter></function></tool_call>",
["<tool_call>"],
None,
None,
),
(
"step3p5_actual_template_shape",
"{% if reasoning_effort is defined %}Reasoning: {{ reasoning_effort }}{% endif %}\n"
"<tool_call><function=get_weather>"
"<parameter=city>Paris</parameter></function></tool_call>\n"
"{% if '<think>' in content %}{{ content.split('</think>')[-1] }}{% endif %}",
["<tool_call>", "<tool_calls>"],
"step3p5",
None,
),
(
"step3p5_think_tags",
"Step3.5-Flash\n<function=tool><parameter=arg>value</parameter></function>\n<think>",
[],
"step3p5",
None,
),
(
"step3_steptml",
"<tool_calls_begin><tool_call_begin>function<tool_sep>"
'<steptml:invoke name="tool"><steptml:parameter name="arg">value</steptml:parameter>'
"</steptml:invoke><tool_call_end><think>",
[],
"step3",
None,
),
(
"qwen3_enable_thinking_true",
"{% set enable_thinking = enable_thinking if enable_thinking is defined else true %}\n",
[],
"qwen3",
"enable_thinking",
),
(
"kimi_unicode_markers",
"\u25c1think\u25b7some text\u25c1/think\u25b7",
[],
"kimi",
None,
),
(
"mistral_reasoning_effort",
"{% if reasoning_effort %}[THINK]{% endif %}",
[],
"mistral",
None, # special_case="mistral"
),
(
"gpt_oss_channel",
"<|channel|>analysis<|message|>",
[],
"gpt-oss",
None, # special_case="always"
),
(
"kimi_k2_with_tool_vocab",
"{% set thinking = thinking if thinking is defined else true %}\n<think>",
["<|tool_calls_section_begin|>", "<|tool_calls_section_end|>"],
"kimi_k2",
"thinking",
),
(
"mimo_enable_thinking_false",
"{% if not enable_thinking is defined %}{% set enable_thinking = false %}{% endif %}\n"
"enable_thinking",
[],
"mimo",
"enable_thinking",
),
]
def test_parser_rules_matrix(self):
for (
name,
template,
vocab,
expected_parser,
expected_toggle,
) in self.PARSER_RULES_MATRIX:
with self.subTest(name=name):
_, config, parser = self._detect(template, vocab)
self.assertEqual(
parser,
expected_parser,
f"Rule '{name}': expected parser '{expected_parser}', got '{parser}'",
)
if expected_toggle is not None:
self.assertIsNotNone(
config, f"Rule '{name}': expected config, got None"
)
self.assertEqual(
config.toggle_param,
expected_toggle,
f"Rule '{name}': expected toggle '{expected_toggle}', "
f"got '{config.toggle_param}'",
)
def test_unrecognized_template_returns_none(self):
template = "Hello {{ user_message }}, how can I help you?"
_, config, parser = self._detect(template)
self.assertIsNone(config)
self.assertIsNone(parser)
def test_empty_template_returns_none(self):
_, config, parser = self._detect("")
self.assertIsNone(config)
self.assertIsNone(parser)
def test_qwen3_precedence_over_deepseek_r1(self):
"""Template with enable_thinking=true but no <think> tag should be qwen3, not deepseek_r1."""
template = "{% set enable_thinking = enable_thinking if enable_thinking is defined else true %}"
_, config, parser = self._detect(template)
self.assertEqual(parser, "qwen3")
self.assertEqual(config.toggle_param, "enable_thinking")
self.assertTrue(config.default_enabled)
class TestToolCallParserDetection(unittest.TestCase):
"""Tests for detect_tool_call_parser() using real model tokenizers."""
def _detect_all(self, model_name):
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
template = tok.chat_template
force, config = detect_reasoning_pattern(template)
rp = detect_reasoning_parser(template, tok, config, force)
tcp = detect_tool_call_parser(template, tok, config, force)
return rp, tcp
def test_qwen3_detects_qwen_tool_call_parser(self):
rp, tcp = self._detect_all("Qwen/Qwen3-0.6B")
self.assertEqual(rp, "qwen3")
self.assertEqual(tcp, "qwen")
def test_tool_call_parser_rule_values_via_snippets(self):
"""Table-driven: verify tool-call rule values differ from reasoning where expected."""
cases = [
# (name, template, vocab, expected_tool_call)
(
"qwen_maps_from_qwen3_config",
"{% set enable_thinking = enable_thinking if enable_thinking is defined else true %}",
[],
"qwen",
),
("gpt_oss", "<|channel|>analysis<|message|>", [], "gpt-oss"),
("gemma4", "<|channel>content", [], "gemma4"),
("minimax_maps_to_m2", "<minimax:tool_call>", [], "minimax-m2"),
(
"minimax_m3_ns_token_only",
"{%- set ns_token = ']<]minimax[>[' -%}\n"
"{%- set toolcall_begin_token = ns_token ~ '<tool_call>' -%}",
[],
"minimax-m3",
),
(
"deepseekv3",
"{% if not thinking is defined %}{% set thinking = false %}{% endif %}",
[],
"deepseekv3",
),
(
"deepseekv31",
(
"{% if not thinking is defined %}{% set thinking = false %}{% endif %}\n"
"<tool▁calls▁begin><tool▁call▁begin>tool<tool▁sep>{}<tool▁call▁end>"
),
[],
"deepseekv31",
),
(
"deepseekv32",
'<DSMLfunction_calls><DSMLinvoke name="tool"></DSMLinvoke></DSMLfunction_calls>',
[],
"deepseekv32",
),
(
"deepseekv4",
'<DSMLtool_calls><DSMLinvoke name="tool"></DSMLinvoke></DSMLtool_calls>',
[],
"deepseekv4",
),
(
"kimi_k2",
"{% set thinking = thinking if thinking is defined else true %}\n<think>",
["<|tool_calls_section_begin|>"],
"kimi_k2",
),
(
"minicpm5",
(
"{% set enable_thinking = enable_thinking if enable_thinking is defined else true %}"
'\n<function name="{{ tool.name }}">'
'\n<param name="{{ param.name }}">{{ param.value }}</param>'
"\n</function>"
),
["<function", "<param"],
"minicpm5",
),
(
"lfm2",
'<|tool_call_start|>[get_weather(city="Paris")]<|tool_call_end|>',
["<|tool_call_start|>", "<|tool_call_end|>"],
"lfm2",
),
(
"hunyuan",
"<tool_calls><tool_call>get_weather<tool_sep><arg_key>city</arg_key><arg_value>Paris</arg_value></tool_call></tool_calls>",
["<tool_calls>", "<tool_sep>", "<arg_key>", "<arg_value>"],
"hunyuan",
),
(
"poolside_v1",
"{% if not enable_thinking is defined %}{% set enable_thinking = false %}{% endif %}\n"
"<tool_call>get_weather\n<arg_key>city</arg_key><arg_value>Paris</arg_value></tool_call>",
["<tool_call>", "<arg_key>", "<arg_value>"],
"poolside_v1",
),
(
"poolside_v1_actual_template_shape",
"{% set enable_thinking = enable_thinking | default(false) %}\n"
"return an unescaped XML-like object with function name and arguments "
"within '<tool_call>' and '</tool_call>' tags\n"
"<tool_call>function-name\n"
"<arg_key>argument-key</arg_key>\n"
"<arg_value>value-of-argument-key</arg_value>\n"
"</tool_call>",
["<tool_call>"],
"poolside_v1",
),
(
"qwen3_coder",
"{% set enable_thinking = enable_thinking if enable_thinking is defined else true %}\n"
"<tool_call><function=get_weather><parameter=city>Paris</parameter></function></tool_call>",
["<tool_call>"],
"qwen3_coder",
),
(
"step3p5",
"Step3.5-Flash\n<tool_call><function=get_weather><parameter=city>Paris</parameter></function></tool_call>",
["<tool_call>"],
"step3p5",
),
(
"step3p5_actual_template_shape",
"{% if reasoning_effort is defined %}Reasoning: {{ reasoning_effort }}{% endif %}\n"
"<tool_call><function=get_weather>"
"<parameter=city>Paris</parameter></function></tool_call>",
["<tool_call>", "<tool_calls>"],
"step3p5",
),
(
"step3",
"<tool_calls_begin><tool_call_begin>function<tool_sep>"
'<steptml:invoke name="get_weather"><steptml:parameter name="city">Paris</steptml:parameter>'
"</steptml:invoke><tool_call_end><tool_calls_end>",
[],
"step3",
),
(
"glm47_compact_tool_call",
(
"[gMASK]<sop>\n"
"{% set enable_thinking = enable_thinking if enable_thinking is defined else true %}\n"
"{% for tc in m.tool_calls %}\n"
"{{- '<tool_call>' + tc.name -}}\n"
"{% set _args = tc.arguments %}"
"{% for k, v in _args.items() %}"
"<arg_key>{{ k }}</arg_key><arg_value>{{ v }}</arg_value>"
"{% endfor %}</tool_call>\n"
"{% endfor %}"
),
["<tool_call>", "<arg_key>", "<arg_value>", "<|endoftext|>"],
"glm47",
),
(
"glm45_newline_tool_call",
(
"[gMASK]<sop>\n"
"{% set enable_thinking = enable_thinking if enable_thinking is defined else true %}\n"
"{% for tc in m.tool_calls %}\n"
"{{ '\\n<tool_call>' + tc.name }}\n"
"{% set _args = tc.arguments %}\n"
"{% for k, v in _args.items() %}\n"
"<arg_key>{{ k }}</arg_key>\n<arg_value>{{ v }}</arg_value>\n"
"{% endfor %}\n</tool_call>\n"
"{% endfor %}"
),
["<tool_call>", "<arg_key>", "<arg_value>", "<|endoftext|>"],
"glm45",
),
(
"xml_kv_tool_call_via_vocab",
"{% set reasoning_effort = reasoning_effort | default('high', true) %}\n<think>",
["<tool_call>", "<arg_key>", "<arg_value>", "<|endoftext|>"],
"glm45",
),
]
for name, template, vocab, expected in cases:
with self.subTest(name=name):
force, config = detect_reasoning_pattern(template)
result = detect_tool_call_parser(
template, _DummyTokenizer(vocab), config, force
)
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["glm47"], rule_index["glm45"])
self.assertLess(rule_index["glm45"], rule_index["xml_kv_tool_call"])
def test_specific_rules_precede_broad_fallbacks(self):
reasoning_index = {
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["hunyuan"], reasoning_index["deepseek_r1_think_tags"]
)
self.assertLess(reasoning_index["poolside_v1"], reasoning_index["mimo"])
self.assertLess(
reasoning_index["step3p5"], reasoning_index["deepseek_r1_think_tags"]
)
self.assertLess(
reasoning_index["step3"], reasoning_index["deepseek_r1_think_tags"]
)
tool_index = {rule.name: i for i, rule in enumerate(TOOL_CALL_PARSER_RULES)}
self.assertLess(tool_index["deepseek_v31"], tool_index["deepseek_v3"])
self.assertLess(tool_index["hunyuan"], tool_index["xml_kv_tool_call"])
self.assertLess(tool_index["poolside_v1"], tool_index["xml_kv_tool_call"])
self.assertLess(tool_index["step3p5"], tool_index["qwen3_coder"])
self.assertLess(tool_index["step3"], tool_index["deepseek_v3"])
self.assertLess(tool_index["qwen3_coder"], tool_index["qwen"])
def test_xml_kv_requires_both_arg_tokens(self):
template = "Hello {{ user }}"
force, config = detect_reasoning_pattern(template)
for vocab in (["<arg_key>"], ["<arg_value>"], []):
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))
def test_unrecognized_template_returns_none(self):
force, config = detect_reasoning_pattern("Hello {{ user }}")
result = detect_tool_call_parser("Hello {{ user }}", None, config, force)
self.assertIsNone(result)
def test_minicpm5_rule_precedes_broad_fallback_rules(self):
rule_names = [rule.name for rule in TOOL_CALL_PARSER_RULES]
minicpm5_idx = rule_names.index("minicpm5")
self.assertLess(minicpm5_idx, rule_names.index("mimo"))
self.assertLess(minicpm5_idx, rule_names.index("qwen"))
def test_minimax_m3_rule_precedes_m2_in_both_registries(self):
for rules in (REASONING_PARSER_RULES, TOOL_CALL_PARSER_RULES):
names = [rule.name for rule in rules]
self.assertLess(names.index("minimax_m3"), names.index("minimax"))
def test_minicpm5_not_misclassified_as_qwen(self):
template = (
"{% set enable_thinking = enable_thinking if enable_thinking is defined else true %}"
'\n<function name="{{ tool.name }}">'
'\n<param name="{{ param.name }}">{{ param.value }}</param>'
"\n</function>"
)
force, config = detect_reasoning_pattern(template)
result = detect_tool_call_parser(
template, _DummyTokenizer(["<function", "<param"]), config, force
)
self.assertEqual(result, "minicpm5")
class TestResolveAutoParsers(unittest.TestCase):
"""Tests for resolve_auto_parsers()."""
qwen3_template = "{% set enable_thinking = enable_thinking if enable_thinking is defined else true %}"
class _Args(SimpleNamespace):
# Write-through override, per the runtime-context testing idiom:
# production adjusts parsers through override(source, ...), so the
# stand-in needs the method (a bare SimpleNamespace would raise).
def override(self, source, **fields):
for key, value in fields.items():
setattr(self, key, value)
def _make_server_args(
self, reasoning_parser=None, tool_call_parser=None, chat_template=None
):
return self._Args(
reasoning_parser=reasoning_parser,
tool_call_parser=tool_call_parser,
model_path="Qwen/Qwen3-0.6B",
trust_remote_code=False,
chat_template=chat_template,
)
def test_resolves_both_parsers_with_tokenizer_template(self):
args = self._make_server_args(reasoning_parser="auto", tool_call_parser="auto")
tokenizer = _DummyTokenizer([], chat_template=self.qwen3_template)
with _patch_hf_transformers_utils(Mock(return_value=tokenizer)):
resolve_auto_parsers(args)
self.assertEqual(args.reasoning_parser, "qwen3")
self.assertEqual(args.tool_call_parser, "qwen")
def test_resolves_reasoning_parser_only(self):
args = self._make_server_args(reasoning_parser="auto", tool_call_parser=None)
tokenizer = _DummyTokenizer([], chat_template=self.qwen3_template)
with _patch_hf_transformers_utils(Mock(return_value=tokenizer)):
resolve_auto_parsers(args)
self.assertEqual(args.reasoning_parser, "qwen3")
self.assertIsNone(args.tool_call_parser)
def test_resolves_tool_call_parser_only(self):
args = self._make_server_args(reasoning_parser="qwen3", tool_call_parser="auto")
tokenizer = _DummyTokenizer([], chat_template=self.qwen3_template)
with _patch_hf_transformers_utils(Mock(return_value=tokenizer)):
resolve_auto_parsers(args)
self.assertEqual(args.reasoning_parser, "qwen3")
self.assertEqual(args.tool_call_parser, "qwen")
def test_neither_auto_is_noop(self):
args = self._make_server_args(reasoning_parser="qwen3", tool_call_parser="qwen")
resolve_auto_parsers(args)
self.assertEqual(args.reasoning_parser, "qwen3")
self.assertEqual(args.tool_call_parser, "qwen")
def test_nonexistent_model_disables_both_parsers(self):
args = self._make_server_args(reasoning_parser="auto", tool_call_parser="auto")
args.model_path = "nonexistent/model-does-not-exist-xyz"
with _patch_hf_transformers_utils(
Mock(side_effect=RuntimeError("tokenizer unavailable")),
Mock(side_effect=RuntimeError("config unavailable")),
):
resolve_auto_parsers(args)
self.assertIsNone(args.reasoning_parser)
self.assertIsNone(args.tool_call_parser)
def test_none_chat_template_disables_both_parsers(self):
args = self._make_server_args(reasoning_parser="auto", tool_call_parser="auto")
tokenizer = _DummyTokenizer([])
with _patch_hf_transformers_utils(Mock(return_value=tokenizer)):
resolve_auto_parsers(args)
self.assertIsNone(args.reasoning_parser)
self.assertIsNone(args.tool_call_parser)
def test_deepseek_v32_arch_without_chat_template_uses_custom_encoder(self):
args = self._make_server_args(reasoning_parser="auto", tool_call_parser="auto")
tokenizer = _DummyTokenizer([])
config = SimpleNamespace(architectures=["DeepseekV32ForCausalLM"])
with _patch_hf_transformers_utils(
Mock(return_value=tokenizer), Mock(return_value=config)
):
resolve_auto_parsers(args)
self.assertEqual(args.reasoning_parser, "deepseek-v3")
self.assertEqual(args.tool_call_parser, "deepseekv32")
def test_deepseek_v4_arch_without_chat_template_uses_custom_encoder(self):
args = self._make_server_args(reasoning_parser="auto", tool_call_parser="auto")
tokenizer = _DummyTokenizer([])
config = SimpleNamespace(architectures=["DeepseekV4ForCausalLM"])
with _patch_hf_transformers_utils(
Mock(return_value=tokenizer), Mock(return_value=config)
):
resolve_auto_parsers(args)
self.assertEqual(args.reasoning_parser, "deepseek-v4")
self.assertEqual(args.tool_call_parser, "deepseekv4")
def test_deepseek_arch_fallback_runs_when_tokenizer_load_fails(self):
args = self._make_server_args(reasoning_parser="auto", tool_call_parser="auto")
config = SimpleNamespace(architectures=["DeepseekV32ForCausalLM"])
with _patch_hf_transformers_utils(
Mock(side_effect=RuntimeError("tokenizer unavailable")),
Mock(return_value=config),
):
resolve_auto_parsers(args)
self.assertEqual(args.reasoning_parser, "deepseek-v3")
self.assertEqual(args.tool_call_parser, "deepseekv32")
def test_explicit_non_jinja_template_skips_architecture_fallback(self):
args = self._make_server_args(
reasoning_parser="auto",
tool_call_parser="auto",
chat_template="chatml",
)
args.model_path = "deepseek-ai/DeepSeek-V3.2"
tokenizer = _DummyTokenizer([])
get_config = Mock()
with _patch_hf_transformers_utils(Mock(return_value=tokenizer), get_config):
resolve_auto_parsers(args)
get_config.assert_not_called()
self.assertIsNone(args.reasoning_parser)
self.assertIsNone(args.tool_call_parser)
def test_explicit_jinja_template_takes_precedence(self):
tokenizer = _DummyTokenizer([], chat_template=None)
with tempfile.NamedTemporaryFile("w", suffix=".jinja") as f:
f.write(
"{% if not thinking is defined %}{% set thinking = false %}{% endif %}\n"
'<DSMLfunction_calls><DSMLinvoke name="tool"></DSMLinvoke>'
)
f.flush()
args = self._make_server_args(
reasoning_parser="auto",
tool_call_parser="auto",
chat_template=f.name,
)
with _patch_hf_transformers_utils(Mock(return_value=tokenizer)):
resolve_auto_parsers(args)
self.assertEqual(args.reasoning_parser, "deepseek-v3")
self.assertEqual(args.tool_call_parser, "deepseekv32")
if __name__ == "__main__":
unittest.main()