@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 20fda43151
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 11, 08:45 UTC

[Bugfix][Frontend] Prevent IndexError in MiniMax M2 tool parser during streaming extraction (#30555)

Signed-off-by: WangErXiao <863579016@qq.com>

Robin committed Dec 17, 2025 at 16:37 UTC 20fda431515d19a883cc962d3a1fa727f225e82d
2 files changed +137 -4
tests/tool_use/test_minimax_m2_tool_parser.py new
+119
@@ -0,0 +1,119 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +import json
5 +
6 +import pytest
7 +
8 +from vllm.tool_parsers.minimax_m2_tool_parser import (
9 + MinimaxM2ToolParser,
10 +)
11 +
12 +pytestmark = pytest.mark.cpu_test
13 +
14 +
15 +class FakeTokenizer:
16 + """Minimal fake tokenizer that exposes the attributes used by the
17 + parser: a truthy model_tokenizer marker and a vocab mapping for the
18 + special tokens.
19 + """
20 +
21 + def __init__(self):
22 + self.model_tokenizer = True
23 + # The parser will look up start/end tokens by their literal strings
24 + self.vocab = {
25 + "<minimax:tool_call>": 1,
26 + "</minimax:tool_call>": 2,
27 + }
28 +
29 + def get_vocab(self):
30 + return self.vocab
31 +
32 +
33 +@pytest.fixture
34 +def minimax_m2_tool_parser():
35 + return MinimaxM2ToolParser(FakeTokenizer())
36 +
37 +
38 +def test_extract_tool_calls_streaming_incremental(minimax_m2_tool_parser):
39 + parser = minimax_m2_tool_parser
40 + parser._reset_streaming_state()
41 + chunks = [
42 + "<minimax:tool_call>",
43 + '<invoke name="get_weather">',
44 + '<parameter name="city">',
45 + "Seattle</parameter>",
46 + "</invoke></minimax:tool_call>",
47 + ]
48 + previous = ""
49 + for chunk in chunks:
50 + current = previous + chunk
51 + delta = chunk
52 + parser.extract_tool_calls_streaming(
53 + previous_text=previous,
54 + current_text=current,
55 + delta_text=delta,
56 + previous_token_ids=[],
57 + current_token_ids=[],
58 + delta_token_ids=[],
59 + request=None,
60 + )
61 + previous = current
62 +
63 + assert len(parser.prev_tool_call_arr) == 1
64 + entry = parser.prev_tool_call_arr[0]
65 +
66 + assert entry["name"] == "get_weather"
67 + args = entry["arguments"]
68 + assert args["city"] == "Seattle"
69 +
70 +
71 +def test_streaming_minimax_m2_multiple_invokes(minimax_m2_tool_parser):
72 + parser = minimax_m2_tool_parser
73 + parser._reset_streaming_state()
74 +
75 + chunks = [
76 + "<minimax:tool_call>",
77 + '<invoke name="search_web">',
78 + '<parameter name="query_tag">',
79 + '["technology", "events"]</parameter>',
80 + '<parameter name="query_list">',
81 + '["OpenAI", "latest", "release"]</parameter>',
82 + "</invoke>",
83 + '<invoke name="search_web">',
84 + '<parameter name="query_tag">',
85 + '["technology", "events"]</parameter>',
86 + '<parameter name="query_list">',
87 + '["Gemini", "latest", "release"]</parameter>',
88 + "</invoke>",
89 + "</minimax:tool_call>",
90 + ]
91 + previous = ""
92 + for chunk in chunks:
93 + current = previous + chunk
94 + delta = chunk
95 + parser.extract_tool_calls_streaming(
96 + previous_text=previous,
97 + current_text=current,
98 + delta_text=delta,
99 + previous_token_ids=[],
100 + current_token_ids=[],
101 + delta_token_ids=[],
102 + request=None,
103 + )
104 + previous = current
105 +
106 + assert len(parser.prev_tool_call_arr) == 2
107 +
108 + for entry, expect_model in zip(parser.prev_tool_call_arr, ["OpenAI", "Gemini"]):
109 + assert entry["name"] == "search_web"
110 + args = json.dumps(entry["arguments"])
111 + assert "technology" in args and "events" in args
112 + assert expect_model in args
113 +
114 + # check streamed_args_for_tool for serving_chat.py
115 + for index in range(2):
116 + expected_call = parser.prev_tool_call_arr[index].get("arguments", {})
117 + expected_call = json.dumps(expected_call)
118 + actual_call = parser.streamed_args_for_tool[index]
119 + assert expected_call == actual_call
vllm/tool_parsers/minimax_m2_tool_parser.py
+18 -4
@@ -122,6 +122,8 @@ class MinimaxM2ToolParser(ToolParser):
122 self.streaming_request = None
123 # Clear previous tool call history to avoid state pollution
124 self.prev_tool_call_arr.clear()
125 + # Reset streamed args tracking
126 + self.streamed_args_for_tool.clear()
127
128 def _extract_name(self, name_str: str) -> str:
129 """Extract name from quoted string."""
@@ -421,9 +423,12 @@ class MinimaxM2ToolParser(ToolParser):
423 self.prev_tool_call_arr.append(
424 {
425 "name": self.current_function_name,
424 - "arguments": "{}", # Placeholder, will be updated later
426 + "arguments": {}, # Placeholder, will be updated later
427 }
428 )
429 + # Initialize streamed_args_for_tool for this tool call
430 + if len(self.streamed_args_for_tool) <= self.current_tool_index:
431 + self.streamed_args_for_tool.append("")
432
433 # Send header with function info
434 return DeltaMessage(
@@ -445,6 +450,9 @@ class MinimaxM2ToolParser(ToolParser):
450 # Send opening brace if not sent yet
451 if self.in_function and not self.json_started:
452 self.json_started = True
453 + # Update streamed_args_for_tool for opening brace
454 + if self.current_tool_index < len(self.streamed_args_for_tool):
455 + self.streamed_args_for_tool[self.current_tool_index] += "{"
456 return DeltaMessage(
457 tool_calls=[
458 DeltaToolCall(
@@ -493,7 +501,7 @@ class MinimaxM2ToolParser(ToolParser):
501 args = parsed_tool.function.arguments
502 self.prev_tool_call_arr[self.current_tool_index][
503 "arguments"
496 - ] = args
504 + ] = json.loads(args)
505 except Exception:
506 pass # Ignore parsing errors during streaming
507
@@ -505,7 +513,9 @@ class MinimaxM2ToolParser(ToolParser):
513 )
514 ]
515 )
508 -
516 + # Update streamed_args_for_tool for closing brace
517 + if self.current_tool_index < len(self.streamed_args_for_tool):
518 + self.streamed_args_for_tool[self.current_tool_index] += "}"
519 # Reset state for next tool
520 self.json_closed = True
521 self.in_function = False
@@ -630,7 +640,11 @@ class MinimaxM2ToolParser(ToolParser):
640 )
641
642 self.param_count += 1
633 -
643 + # Update streamed_args_for_tool for this tool call
644 + if self.current_tool_index < len(self.streamed_args_for_tool):
645 + self.streamed_args_for_tool[self.current_tool_index] += (
646 + json_fragment
647 + )
648 return DeltaMessage(
649 tool_calls=[
650 DeltaToolCall(