[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
@@ -0,0 +1,539 @@
import json
import unittest
from sglang.srt.entrypoints.openai.protocol import Function, Tool
from sglang.srt.function_call.minimax_m3 import MINIMAX_NS_TOKEN, MinimaxM3Detector
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=7, suite="base-a-test-cpu")
NS = MINIMAX_NS_TOKEN
def _make_tools():
return [
Tool(
type="function",
function=Function(
name="get_current_date",
description="Get the current date",
parameters={},
),
),
Tool(
type="function",
function=Function(
name="get_weather",
description="Get weather information",
parameters={
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"},
},
"required": ["city"],
},
),
),
Tool(
type="function",
function=Function(
name="search",
description="Search the web",
parameters={
"type": "object",
"properties": {
"query": {"type": "string"},
"count": {"type": "integer"},
"ratio": {"type": "number"},
"verbose": {"type": "boolean"},
},
"required": ["query"],
},
),
),
Tool(
type="function",
function=Function(
name="create_event",
description="Create a calendar event",
parameters={
"type": "object",
"properties": {
"title": {"type": "string"},
"location": {
"type": "object",
"properties": {
"city": {"type": "string"},
"zip": {"type": "integer"},
},
},
"tags": {
"type": "array",
"items": {"type": "string"},
},
},
},
),
),
Tool(
type="function",
function=Function(
name="add_note",
description="Add a free-form note",
parameters={
"type": "object",
"properties": {"note": {"type": "string"}},
},
),
),
Tool(
type="function",
function=Function(
name="configure",
description="Configure runtime options",
parameters={
"type": "object",
"properties": {
"mode": {
"type": "string",
"enum": ["none", "low", "high"],
},
"optional": {"type": ["string", "null"]},
},
},
),
),
]
def _wire(*lines):
return "".join(NS + line for line in lines)
def _segments(*lines):
return [NS + line for line in lines]
def _collect_streamed_tool_calls(all_calls):
tools = {}
for c in all_calls:
idx = c.tool_index
if idx not in tools:
tools[idx] = {"name": c.name or "", "parameters": c.parameters or ""}
else:
if c.name:
tools[idx]["name"] += c.name
if c.parameters:
tools[idx]["parameters"] += c.parameters
return [tools[i] for i in sorted(tools.keys())]
def _stream_segments(segments, tools):
detector = MinimaxM3Detector()
all_calls = []
for seg in _segments(*segments):
all_calls.extend(detector.parse_streaming_increment(seg, tools).calls)
collected = _collect_streamed_tool_calls(all_calls)
return [{"name": c["name"], "args": json.loads(c["parameters"])} for c in collected]
def _parse_segments(segments, tools):
detector = MinimaxM3Detector()
result = detector.detect_and_parse(_wire(*segments), tools)
return [
{"name": c.name, "args": json.loads(c.parameters)} for c in result.calls
], result.normal_text
class TestMinimaxM3HasToolCall(CustomTestCase):
def setUp(self):
self.detector = MinimaxM3Detector()
def test_has_tool_call_true(self):
text = _wire(
"<tool_call>",
'<invoke name="get_current_date">',
"</invoke>",
"</tool_call>",
)
self.assertTrue(self.detector.has_tool_call(text))
def test_has_tool_call_false(self):
self.assertFalse(
self.detector.has_tool_call("The weather in Beijing is sunny.")
)
class TestMinimaxM3DetectAndParse(CustomTestCase):
def setUp(self):
self.tools = _make_tools()
def test_no_tool_call(self):
text = "This is a plain response with no tool call."
calls, normal = _parse_segments_text(text, self.tools)
self.assertEqual(len(calls), 0)
self.assertEqual(normal, text)
def test_single_tool_call(self):
segments = (
"<tool_call>",
'<invoke name="get_weather">',
"<city>Beijing",
"</city>",
"</invoke>",
"</tool_call>",
)
calls, _ = _parse_segments(segments, self.tools)
self.assertEqual(len(calls), 1)
self.assertEqual(calls[0]["name"], "get_weather")
self.assertEqual(calls[0]["args"], {"city": "Beijing"})
def test_zero_arg_tool_call(self):
segments = (
"<tool_call>",
'<invoke name="get_current_date">',
"</invoke>",
"</tool_call>",
)
calls, _ = _parse_segments(segments, self.tools)
self.assertEqual(len(calls), 1)
self.assertEqual(calls[0]["name"], "get_current_date")
self.assertEqual(calls[0]["args"], {})
def test_multiple_tool_calls_separate_blocks(self):
segments = (
"<tool_call>",
'<invoke name="get_weather">',
"<city>Beijing",
"</city>",
"</invoke>",
"</tool_call>",
"<tool_call>",
'<invoke name="get_weather">',
"<city>Tokyo",
"</city>",
"</invoke>",
"</tool_call>",
)
calls, _ = _parse_segments(segments, self.tools)
self.assertEqual(len(calls), 2)
self.assertEqual(calls[0]["args"]["city"], "Beijing")
self.assertEqual(calls[1]["args"]["city"], "Tokyo")
def test_multiple_invokes_one_block(self):
segments = (
"<tool_call>",
'<invoke name="get_weather">',
"<city>Beijing",
"</city>",
"</invoke>",
'<invoke name="get_weather">',
"<city>Tokyo",
"</city>",
"</invoke>",
"</tool_call>",
)
calls, _ = _parse_segments(segments, self.tools)
self.assertEqual(len(calls), 2)
self.assertEqual(calls[0]["args"]["city"], "Beijing")
self.assertEqual(calls[1]["args"]["city"], "Tokyo")
def test_content_before_tool_call(self):
segments = (
"<tool_call>",
'<invoke name="get_weather">',
"<city>Paris",
"</city>",
"</invoke>",
"</tool_call>",
)
detector = MinimaxM3Detector()
text = "Let me check the weather." + _wire(*segments)
result = detector.detect_and_parse(text, self.tools)
self.assertEqual(len(result.calls), 1)
self.assertEqual(result.normal_text, "Let me check the weather.")
def test_typed_scalars(self):
segments = (
"<tool_call>",
'<invoke name="search">',
"<query>pizza",
"</query>",
"<count>7",
"</count>",
"<ratio>0.5",
"</ratio>",
"<verbose>true",
"</verbose>",
"</invoke>",
"</tool_call>",
)
calls, _ = _parse_segments(segments, self.tools)
args = calls[0]["args"]
self.assertEqual(args["query"], "pizza")
self.assertEqual(args["count"], 7)
self.assertIsInstance(args["count"], int)
self.assertAlmostEqual(args["ratio"], 0.5)
self.assertIs(args["verbose"], True)
def test_nested_object_and_array(self):
segments = (
"<tool_call>",
'<invoke name="create_event">',
"<title>Standup",
"</title>",
"<location>",
"<city>NYC",
"</city>",
"<zip>10001",
"</zip>",
"</location>",
"<tags>",
"<item>red",
"</item>",
"<item>blue",
"</item>",
"</tags>",
"</invoke>",
"</tool_call>",
)
calls, _ = _parse_segments(segments, self.tools)
self.assertEqual(len(calls), 1)
self.assertEqual(
calls[0]["args"],
{
"title": "Standup",
"location": {"city": "NYC", "zip": 10001},
"tags": ["red", "blue"],
},
)
def test_string_with_special_characters(self):
value = 'a "quoted" word with \\ backslash and\nnewline'
segments = (
"<tool_call>",
'<invoke name="add_note">',
"<note>" + value,
"</note>",
"</invoke>",
"</tool_call>",
)
calls, _ = _parse_segments(segments, self.tools)
self.assertEqual(calls[0]["args"], {"note": value})
class TestMinimaxM3NoneNullRegression(CustomTestCase):
def setUp(self):
self.tools = _make_tools()
def _configure_segments(self, param, value):
return (
"<tool_call>",
'<invoke name="configure">',
"<{}>{}".format(param, value),
"</{}>".format(param),
"</invoke>",
"</tool_call>",
)
def test_plain_string_none_not_coerced(self):
for value in ("none", "nil", "null"):
with self.subTest(value=value):
segments = self._configure_segments("mode", value)
calls, _ = _parse_segments(segments, self.tools)
self.assertEqual(calls[0]["args"], {"mode": value})
self.assertIsInstance(calls[0]["args"]["mode"], str)
def test_plain_string_none_not_coerced_streaming(self):
for value in ("none", "nil", "null"):
with self.subTest(value=value):
segments = self._configure_segments("mode", value)
calls = _stream_segments(segments, self.tools)
self.assertEqual(calls[0]["args"], {"mode": value})
self.assertIsInstance(calls[0]["args"]["mode"], str)
def test_streaming_and_non_streaming_agree_for_string(self):
for value in ("none", "nil", "null"):
with self.subTest(value=value):
segments = self._configure_segments("mode", value)
non_stream, _ = _parse_segments(segments, self.tools)
stream = _stream_segments(segments, self.tools)
self.assertEqual(non_stream, stream)
def test_nullable_param_null_becomes_none(self):
segments = self._configure_segments("optional", "null")
calls, _ = _parse_segments(segments, self.tools)
self.assertEqual(calls[0]["args"], {"optional": None})
def test_nullable_param_none_stays_string(self):
segments = self._configure_segments("optional", "none")
calls, _ = _parse_segments(segments, self.tools)
self.assertEqual(calls[0]["args"], {"optional": "none"})
class TestMinimaxM3Streaming(CustomTestCase):
def setUp(self):
self.tools = _make_tools()
def test_normal_text_only(self):
detector = MinimaxM3Detector()
result = detector.parse_streaming_increment("Hello there.", self.tools)
self.assertEqual(result.normal_text, "Hello there.")
self.assertEqual(len(result.calls), 0)
def test_single_tool_call_chunked(self):
segments = (
"<tool_call>",
'<invoke name="get_weather">',
"<city>Beijing",
"</city>",
"</invoke>",
"</tool_call>",
)
calls = _stream_segments(segments, self.tools)
self.assertEqual(len(calls), 1)
self.assertEqual(calls[0]["name"], "get_weather")
self.assertEqual(calls[0]["args"], {"city": "Beijing"})
def test_streaming_matches_non_streaming(self):
cases = {
"weather": (
"<tool_call>",
'<invoke name="get_weather">',
"<city>Beijing",
"</city>",
"</invoke>",
"</tool_call>",
),
"typed": (
"<tool_call>",
'<invoke name="search">',
"<query>pizza",
"</query>",
"<count>7",
"</count>",
"<ratio>0.5",
"</ratio>",
"<verbose>true",
"</verbose>",
"</invoke>",
"</tool_call>",
),
"nested": (
"<tool_call>",
'<invoke name="create_event">',
"<title>Standup",
"</title>",
"<location>",
"<city>NYC",
"</city>",
"<zip>10001",
"</zip>",
"</location>",
"<tags>",
"<item>red",
"</item>",
"<item>blue",
"</item>",
"</tags>",
"</invoke>",
"</tool_call>",
),
"special": (
"<tool_call>",
'<invoke name="add_note">',
"<note>" + 'a "q" and \\ back\nslash',
"</note>",
"</invoke>",
"</tool_call>",
),
"multi_invoke": (
"<tool_call>",
'<invoke name="get_weather">',
"<city>Beijing",
"</city>",
"</invoke>",
'<invoke name="get_weather">',
"<city>Tokyo",
"</city>",
"</invoke>",
"</tool_call>",
),
}
for name, segments in cases.items():
with self.subTest(case=name):
non_stream, _ = _parse_segments(segments, self.tools)
stream = _stream_segments(segments, self.tools)
self.assertEqual(non_stream, stream)
def test_streaming_sequential_tool_index(self):
segments = (
"<tool_call>",
'<invoke name="get_weather">',
"<city>Beijing",
"</city>",
"</invoke>",
'<invoke name="get_weather">',
"<city>Tokyo",
"</city>",
"</invoke>",
"</tool_call>",
)
detector = MinimaxM3Detector()
all_calls = []
for seg in _segments(*segments):
all_calls.extend(detector.parse_streaming_increment(seg, self.tools).calls)
self.assertEqual(sorted({c.tool_index for c in all_calls}), [0, 1])
class TestMinimaxM3Malformed(CustomTestCase):
def setUp(self):
self.tools = _make_tools()
def test_truncated_no_closing_tags(self):
text = _wire(
"<tool_call>",
'<invoke name="get_weather">',
"<city>Beijing",
)
detector = MinimaxM3Detector()
result = detector.detect_and_parse(text, self.tools)
self.assertEqual(len(result.calls), 0)
self.assertEqual(result.normal_text, text)
def test_mismatched_closing_tag(self):
text = _wire(
"<tool_call>",
'<invoke name="get_weather">',
"<city>Beijing",
"</wrong>",
"</invoke>",
"</tool_call>",
)
detector = MinimaxM3Detector()
result = detector.detect_and_parse(text, self.tools)
self.assertEqual(len(result.calls), 0)
self.assertEqual(result.normal_text, text)
def test_truncated_streaming_does_not_crash(self):
segments = (
"<tool_call>",
'<invoke name="get_weather">',
"<city>Beijing",
)
detector = MinimaxM3Detector()
for seg in _segments(*segments):
detector.parse_streaming_increment(seg, self.tools)
def _parse_segments_text(text, tools):
detector = MinimaxM3Detector()
result = detector.detect_and_parse(text, tools)
return [
{"name": c.name, "args": json.loads(c.parameters)} for c in result.calls
], result.normal_text
if __name__ == "__main__":
unittest.main()