[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:
co-authored by
Xinyuan Tong
zijiexia
parent
e3ceccf781
commit
0663ebc783
@@ -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()."""
|
||||
|
||||
Reference in New Issue
Block a user