[Feature] Xiaomi MiMo-V2-Flash day0 support (#15207)

Co-authored-by: 谢学扬 <xiexueyang@xiaomi.com>
Co-authored-by: tz <tangzhen3@xiaomi.com>
Co-authored-by: 李家乐 <lijiale10@xiaomi.com>
Co-authored-by: 张晨 <zhangchen50@xiaomi.com>
Co-authored-by: Shaohui Liu <liushaohui3@xiaomi.com>
Co-authored-by: 王晨 <wangchen77@xiaomi.com>
Co-authored-by: jiangzihan <jiangzihan@xiaomi.com>
Co-authored-by: xiexueyang <xyxie_wangyi@163.com>
Co-authored-by: Linghao Zhang <zhanglinghao@xiaomi.com>
Co-authored-by: ispobock <ispobaoke@gmail.com>
Co-authored-by: Liangsheng Yin <lsyincs@gmail.com>
Co-authored-by: JoyFuture <35593546+JoyFuture@users.noreply.github.com>
Co-authored-by: Liangsheng Yin <hnyls2002@gmail.com>
Co-authored-by: Qiaolin Yu <liin1211@outlook.com>
Co-authored-by: root <root@bj9-ml-g8h20e-k8s-slave106-20251106.alicn.idc.xiaomi.com>
This commit is contained in:
Yingchun Lai
2025-12-19 11:40:07 +08:00
committed by GitHub
co-authored by 谢学扬 tz 李家乐 张晨 Shaohui Liu 王晨 jiangzihan xiexueyang Linghao Zhang ispobock Liangsheng Yin JoyFuture Liangsheng Yin Qiaolin Yu root
parent a0985dd5e5
commit 160a06cab2
38 changed files with 5396 additions and 169 deletions
@@ -10,6 +10,7 @@ from sglang.srt.function_call.glm4_moe_detector import Glm4MoeDetector
from sglang.srt.function_call.json_array_parser import JsonArrayParser
from sglang.srt.function_call.kimik2_detector import KimiK2Detector
from sglang.srt.function_call.llama32_detector import Llama32Detector
from sglang.srt.function_call.mimo_detector import MiMoDetector
from sglang.srt.function_call.mistral_detector import MistralDetector
from sglang.srt.function_call.pythonic_detector import PythonicDetector
from sglang.srt.function_call.qwen3_coder_detector import Qwen3CoderDetector
@@ -2246,6 +2247,446 @@ class TestGlm4MoeDetector(unittest.TestCase):
check_single_todos(result, expected_output)
class TestMiMoDetector(unittest.TestCase):
def setUp(self):
# Create sample tools for testing
self.tools = [
Tool(
type="function",
function=Function(
name="get_current_weather",
description="Get the current weather",
parameters={
"properties": {
"city": {"type": "string", "description": "The city name"},
"state": {
"type": "string",
"description": "The state code",
},
"unit": {
"type": "string",
"enum": ["fahrenheit", "celsius"],
},
},
"required": ["city", "state"],
},
),
),
Tool(
type="function",
function=Function(
name="calculate_area",
description="Calculate area of a shape",
parameters={
"properties": {
"shape": {"type": "string"},
"dimensions": {"type": "object"},
"precision": {"type": "integer"},
}
},
),
),
]
self.detector = MiMoDetector()
def test_has_tool_call(self):
"""Test detection of tool call markers."""
self.assertTrue(self.detector.has_tool_call("<tool_call>test</tool_call>"))
self.assertFalse(self.detector.has_tool_call("No tool call here"))
def test_detect_and_parse_no_tools(self):
"""Test parsing text without tool calls."""
model_output = "This is a test response without any tool calls"
result = self.detector.detect_and_parse(model_output, tools=[])
self.assertEqual(result.normal_text, model_output)
self.assertEqual(result.calls, [])
def test_detect_and_parse_single_tool(self):
"""Test parsing a single tool call."""
model_output = """<tool_call>
<function=get_current_weather>
<parameter=city>Dallas</parameter>
<parameter=state>TX</parameter>
<parameter=unit>fahrenheit</parameter>
</function>
</tool_call>"""
result = self.detector.detect_and_parse(model_output, tools=self.tools)
self.assertEqual(result.normal_text, "")
self.assertEqual(len(result.calls), 1)
self.assertEqual(result.calls[0].name, "get_current_weather")
params = json.loads(result.calls[0].parameters)
self.assertEqual(params["city"], "Dallas")
self.assertEqual(params["state"], "TX")
self.assertEqual(params["unit"], "fahrenheit")
def test_detect_and_parse_with_content(self):
"""Test parsing tool call with surrounding text."""
model_output = """Sure! Let me check the weather for you.<tool_call>
<function=get_current_weather>
<parameter=city>Dallas</parameter>
<parameter=state>TX</parameter>
<parameter=unit>fahrenheit</parameter>
</function>
</tool_call>"""
result = self.detector.detect_and_parse(model_output, tools=self.tools)
self.assertEqual(result.normal_text, "Sure! Let me check the weather for you.")
self.assertEqual(len(result.calls), 1)
self.assertEqual(result.calls[0].name, "get_current_weather")
def test_detect_and_parse_multiline_param(self):
"""Test parsing tool call with multiline parameter values."""
model_output = """<tool_call>
<function=calculate_area>
<parameter=shape>rectangle</parameter>
<parameter=dimensions>{"width": 10, "height": 20}</parameter>
<parameter=precision>2</parameter>
</function>
</tool_call>"""
result = self.detector.detect_and_parse(model_output, tools=self.tools)
self.assertEqual(len(result.calls), 1)
self.assertEqual(result.calls[0].name, "calculate_area")
params = json.loads(result.calls[0].parameters)
self.assertEqual(params["shape"], "rectangle")
self.assertEqual(params["dimensions"], {"width": 10, "height": 20})
self.assertEqual(params["precision"], 2)
def test_detect_and_parse_parallel_tools(self):
"""Test parsing multiple tool calls."""
model_output = """<tool_call>
<function=get_current_weather>
<parameter=city>Dallas</parameter>
<parameter=state>TX</parameter>
<parameter=unit>fahrenheit</parameter>
</function>
</tool_call>
<tool_call>
<function=get_current_weather>
<parameter=city>Orlando</parameter>
<parameter=state>FL</parameter>
<parameter=unit>fahrenheit</parameter>
</function>
</tool_call>"""
result = self.detector.detect_and_parse(model_output, tools=self.tools)
self.assertEqual(result.normal_text, "")
self.assertEqual(len(result.calls), 2)
# First call
self.assertEqual(result.calls[0].name, "get_current_weather")
params1 = json.loads(result.calls[0].parameters)
self.assertEqual(params1["city"], "Dallas")
self.assertEqual(params1["state"], "TX")
# Second call
self.assertEqual(result.calls[1].name, "get_current_weather")
params2 = json.loads(result.calls[1].parameters)
self.assertEqual(params2["city"], "Orlando")
self.assertEqual(params2["state"], "FL")
def test_parse_streaming_simple(self):
"""Test basic streaming parsing."""
chunks = [
"Sure! ",
"Let me check ",
"the weather.",
"<tool_call>",
"\n<function=get_current_weather>",
"\n<parameter=city>Dallas</parameter>",
"\n<parameter=state>TX</parameter>",
"\n</function>",
"\n</tool_call>",
]
accumulated_text = ""
accumulated_calls = []
tool_calls_by_index = {}
for chunk in chunks:
result = self.detector.parse_streaming_increment(chunk, tools=self.tools)
accumulated_text += result.normal_text
# Track calls by tool_index to handle streaming properly
for call in result.calls:
if call.tool_index is not None:
if call.tool_index not in tool_calls_by_index:
tool_calls_by_index[call.tool_index] = {
"name": "",
"parameters": "",
}
if call.name:
tool_calls_by_index[call.tool_index]["name"] = call.name
if call.parameters:
tool_calls_by_index[call.tool_index][
"parameters"
] += call.parameters
self.assertEqual(accumulated_text, "Sure! Let me check the weather.")
self.assertEqual(len(tool_calls_by_index), 1)
# Get the complete tool call
tool_call = tool_calls_by_index[0]
self.assertEqual(tool_call["name"], "get_current_weather")
# Parse the accumulated parameters
params = json.loads(tool_call["parameters"])
self.assertEqual(params["city"], "Dallas")
self.assertEqual(params["state"], "TX")
def test_parse_streaming_incomplete(self):
"""Test streaming with incomplete tool call."""
# Send incomplete tool call
chunks = [
"<tool_call>",
"\n<function=get_current_weather>",
"\n<parameter=city>Dallas</parameter>",
"\n<parameter=state>",
# Missing </parameter>, </function>, </tool_call>
]
tool_calls_by_index = {}
for chunk in chunks:
result = self.detector.parse_streaming_increment(chunk, tools=self.tools)
# Track calls by tool_index to handle streaming properly
for call in result.calls:
if call.tool_index is not None:
if call.tool_index not in tool_calls_by_index:
tool_calls_by_index[call.tool_index] = {
"name": "",
"parameters": "",
}
if call.name:
tool_calls_by_index[call.tool_index]["name"] = call.name
if call.parameters:
tool_calls_by_index[call.tool_index][
"parameters"
] += call.parameters
# Should have no complete tool calls yet (buffered)
self.assertEqual(len(tool_calls_by_index), 0)
# Now complete it
result = self.detector.parse_streaming_increment(
"TX</parameter>\n</function>\n</tool_call>", tools=self.tools
)
# Update the accumulated parameters
for call in result.calls:
if call.tool_index is not None:
if call.tool_index not in tool_calls_by_index:
tool_calls_by_index[call.tool_index] = {
"name": "",
"parameters": "",
}
if call.name:
tool_calls_by_index[call.tool_index]["name"] = call.name
if call.parameters:
tool_calls_by_index[call.tool_index][
"parameters"
] += call.parameters
# Now should have complete tool call
self.assertEqual(len(tool_calls_by_index), 1)
final_params = json.loads(tool_calls_by_index[0]["parameters"])
self.assertEqual(final_params["city"], "Dallas")
self.assertEqual(final_params["state"], "TX")
def test_edge_case_no_parameters(self):
"""Test tool call without parameters."""
model_output = """<tool_call>
<function=get_current_weather>
</function>
</tool_call>"""
result = self.detector.detect_and_parse(model_output, tools=self.tools)
self.assertEqual(len(result.calls), 1)
self.assertEqual(result.calls[0].name, "get_current_weather")
self.assertEqual(json.loads(result.calls[0].parameters), {})
def test_edge_case_special_chars_in_value(self):
"""Test parameter with special characters in value."""
model_output = """<tool_call>
<function=get_current_weather>
<parameter=city>Dallas->TX</parameter>
</function>
</tool_call>"""
result = self.detector.detect_and_parse(model_output, tools=self.tools)
self.assertEqual(len(result.calls), 1)
params = json.loads(result.calls[0].parameters)
self.assertEqual(params["city"], "Dallas->TX")
def test_extract_tool_calls_type_conversion(self):
"""Test parameter type conversion based on tool schema."""
test_tool = Tool(
type="function",
function=Function(
name="test_types",
parameters={
"type": "object",
"properties": {
"int_param": {"type": "integer"},
"float_param": {"type": "float"},
"bool_param": {"type": "boolean"},
"str_param": {"type": "string"},
"obj_param": {"type": "object"},
},
},
),
)
model_output = """<tool_call>
<function=test_types>
<parameter=int_param>42</parameter>
<parameter=float_param>3.14</parameter>
<parameter=bool_param>true</parameter>
<parameter=str_param>hello world</parameter>
<parameter=obj_param>{"key": "value"}</parameter>
</function>
</tool_call>"""
result = self.detector.detect_and_parse(model_output, tools=[test_tool])
self.assertEqual(len(result.calls), 1)
params = json.loads(result.calls[0].parameters)
self.assertEqual(params["int_param"], 42)
self.assertEqual(params["float_param"], 3.14)
self.assertEqual(params["bool_param"], True)
self.assertEqual(params["str_param"], "hello world")
self.assertEqual(params["obj_param"], {"key": "value"})
def test_parse_streaming_incremental(self):
"""Test that streaming is truly incremental with very small chunks."""
# Simulate more realistic token-based chunks where <tool_call> is a single token
chunks = [
"I'll check the weather.",
"<tool_call>",
"\n<function=get_current_weather>\n",
"<parameter=city>",
"Dallas",
"</parameter>\n",
"<parameter=state>",
"TX",
"</parameter>\n",
"</function>\n",
"</tool_call>",
]
accumulated_text = ""
tool_calls = []
chunks_count = 0
for chunk in chunks:
result = self.detector.parse_streaming_increment(chunk, self.tools)
accumulated_text += result.normal_text
chunks_count += 1
for tool_call_chunk in result.calls:
if (
hasattr(tool_call_chunk, "tool_index")
and tool_call_chunk.tool_index is not None
):
while len(tool_calls) <= tool_call_chunk.tool_index:
tool_calls.append({"name": "", "parameters": ""})
tc = tool_calls[tool_call_chunk.tool_index]
if tool_call_chunk.name:
tc["name"] = tool_call_chunk.name
if tool_call_chunk.parameters:
tc["parameters"] += tool_call_chunk.parameters
self.assertGreater(chunks_count, 3)
# Verify the accumulated results
self.assertIn("I'll check the weather.", accumulated_text)
self.assertEqual(len(tool_calls), 1)
self.assertEqual(tool_calls[0]["name"], "get_current_weather")
params = json.loads(tool_calls[0]["parameters"])
self.assertEqual(params, {"city": "Dallas", "state": "TX"})
def test_parse_streaming_multiple_tools(self):
"""Test streaming with multiple tool calls."""
model_output = """<tool_call>
<function=get_current_weather>
<parameter=city>Dallas</parameter>
<parameter=state>TX</parameter>
</function>
</tool_call>
Some text in between.
<tool_call>
<function=calculate_area>
<parameter=shape>circle</parameter>
<parameter=dimensions>{"radius": 5}</parameter>
</function>
</tool_call>"""
# Simulate streaming by chunks
chunk_size = 20
chunks = [
model_output[i : i + chunk_size]
for i in range(0, len(model_output), chunk_size)
]
accumulated_text = ""
tool_calls = []
chunks_count = 0
for chunk in chunks:
result = self.detector.parse_streaming_increment(chunk, self.tools)
accumulated_text += result.normal_text
chunks_count += 1
for tool_call_chunk in result.calls:
if (
hasattr(tool_call_chunk, "tool_index")
and tool_call_chunk.tool_index is not None
):
while len(tool_calls) <= tool_call_chunk.tool_index:
tool_calls.append({"name": "", "parameters": ""})
tc = tool_calls[tool_call_chunk.tool_index]
if tool_call_chunk.name:
tc["name"] = tool_call_chunk.name
if tool_call_chunk.parameters:
tc["parameters"] += tool_call_chunk.parameters
self.assertIn("Some text in between.", accumulated_text)
self.assertEqual(len(tool_calls), 2)
self.assertEqual(tool_calls[0]["name"], "get_current_weather")
self.assertEqual(tool_calls[1]["name"], "calculate_area")
# Verify parameters
params1 = json.loads(tool_calls[0]["parameters"])
self.assertEqual(params1, {"city": "Dallas", "state": "TX"})
params2 = json.loads(tool_calls[1]["parameters"])
self.assertEqual(params2, {"shape": "circle", "dimensions": {"radius": 5}})
def test_html_entity_decoding(self):
"""Test that HTML entities in parameter values are decoded."""
model_output = """<tool_call>
<function=get_current_weather>
<parameter=city>Dallas &amp; Fort Worth</parameter>
<parameter=state>TX</parameter>
</function>
</tool_call>"""
result = self.detector.detect_and_parse(model_output, tools=self.tools)
self.assertEqual(len(result.calls), 1)
params = json.loads(result.calls[0].parameters)
self.assertEqual(params["city"], "Dallas & Fort Worth")
class TestJsonArrayParser(unittest.TestCase):
def setUp(self):
# Create sample tools for testing