[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
parent a0985dd5e5
commit 160a06cab2
38 changed files with 5396 additions and 169 deletions

View File

@@ -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