[minimax-m3] Split 4/4: model + VL + glue + function-call + fp8 quant + generic infra (#28715)

Co-authored-by: Xinyuan Tong <xinyuan-tong@users.noreply.github.com>
Co-authored-by: zijiexia <37504505+zijiexia@users.noreply.github.com>
This commit is contained in:
Xinyuan Tong
2026-07-11 11:11:06 +08:00
committed by GitHub
co-authored by Xinyuan Tong zijiexia
parent e3ceccf781
commit 0663ebc783
45 changed files with 7477 additions and 475 deletions
@@ -762,6 +762,96 @@ class TestMiniMaxAppendThinkDetector(CustomTestCase):
self.assertEqual(result.normal_text, "Second")
class TestMiniMaxM3Detector(CustomTestCase):
"""Test cases for MiniMaxM3Detector multi-turn stray-closer handling."""
def _detector(self, force_reasoning=False):
from sglang.srt.parser.reasoning_parser import MiniMaxM3Detector
return MiniMaxM3Detector(force_reasoning=force_reasoning)
def test_drops_leading_stray_close_non_stream(self):
"""Non-thinking multi-turn reply opening with a stray </mm:think>."""
result = self._detector().detect_and_parse("</mm:think>The answer is 42.")
self.assertEqual(result.normal_text, "The answer is 42.")
self.assertEqual(result.reasoning_text or "", "")
def test_drops_leading_stray_close_with_whitespace(self):
result = self._detector().detect_and_parse("\n</mm:think>Hello")
self.assertEqual(result.normal_text, "Hello")
def test_plain_reply_untouched(self):
result = self._detector().detect_and_parse("Just a normal answer.")
self.assertEqual(result.normal_text, "Just a normal answer.")
def test_real_reasoning_block_non_stream(self):
result = self._detector().detect_and_parse(
"<mm:think>reasoning here</mm:think>final"
)
self.assertEqual(result.reasoning_text, "reasoning here")
self.assertEqual(result.normal_text, "final")
def test_thinking_mode_close_not_dropped(self):
"""force_reasoning=True means the closer ends reasoning, not a stray drop."""
result = self._detector(force_reasoning=True).detect_and_parse(
"reasoning</mm:think>answer"
)
self.assertEqual(result.reasoning_text, "reasoning")
self.assertEqual(result.normal_text, "answer")
def test_drops_leading_stray_close_stream_single_token(self):
detector = self._detector()
first = detector.parse_streaming_increment("</mm:think>")
self.assertEqual(first.normal_text or "", "")
second = detector.parse_streaming_increment("The answer.")
self.assertEqual(second.normal_text, "The answer.")
def test_drops_leading_stray_close_stream_after_whitespace(self):
"""A leading whitespace token before the atomic </mm:think> is buffered."""
detector = self._detector()
self.assertEqual(detector.parse_streaming_increment(" ").normal_text or "", "")
self.assertEqual(
detector.parse_streaming_increment("</mm:think>").normal_text or "", ""
)
self.assertEqual(
detector.parse_streaming_increment("Hello").normal_text, "Hello"
)
def test_plain_reply_stream_untouched(self):
detector = self._detector()
out = detector.parse_streaming_increment("Hello")
self.assertEqual(out.normal_text, "Hello")
def test_minimax_m3_model_type(self):
from sglang.srt.parser.reasoning_parser import MiniMaxM3Detector
parser = ReasoningParser("minimax-m3")
self.assertIsInstance(parser.detector, MiniMaxM3Detector)
def test_force_nonempty_content_via_chat_template_kwargs(self):
"""force_nonempty_content must reach the M3 detector without a TypeError."""
from sglang.srt.entrypoints.openai.protocol import (
ChatCompletionMessageUserParam,
ChatCompletionRequest,
)
request = ChatCompletionRequest(
model="test",
messages=[ChatCompletionMessageUserParam(role="user", content="Hi")],
chat_template_kwargs={"force_nonempty_content": True},
)
parser = ReasoningParser("minimax-m3", request=request)
self.assertTrue(parser.detector._force_nonempty_content)
def test_force_nonempty_content_swaps_when_no_content(self):
from sglang.srt.parser.reasoning_parser import MiniMaxM3Detector
detector = MiniMaxM3Detector(force_reasoning=True, force_nonempty_content=True)
result = detector.detect_and_parse("only reasoning, no closer")
self.assertEqual(result.normal_text, "only reasoning, no closer")
self.assertEqual(result.reasoning_text or "", "")
class TestReasoningParserAdvanced(CustomTestCase):
"""Additional tests for ReasoningParser init edge cases."""
@@ -130,6 +130,27 @@ class TestTemplateManagerReasoningDetection(unittest.TestCase):
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."""
@@ -376,6 +397,13 @@ class TestToolCallParserDetection(unittest.TestCase):
("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 %}",
@@ -583,6 +611,24 @@ class TestToolCallParserDetection(unittest.TestCase):
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()."""