From 68a8f6115c0621e32a1aae323aecf648e76d74a4 Mon Sep 17 00:00:00 2001 From: LeizhenZhang Date: Sat, 24 Oct 2026 22:59:08 -0500 Subject: [PATCH 1/2] [#20955][fix] MiniMax-M2 tool parser: stop name patterns at the first > The invoke and parameter patterns captured the name with a lazy (.*?). While an invoke or parameter is still open, which is the normal state in the middle of a stream, every > inside the value became a candidate end for the name, and from each one the body group scanned to the end of the buffer. Code arguments are full of > (a > b, p->x), so each streaming increment cost about (number of >) x (value length) and a 4k-character argument took about 18 s of event-loop time. A name never contains >, so the name group is now [^>]*? in the invoke, parameter and partial-invoke patterns. The same 4k-character argument streams in about 0.4 s. Parsed names and values are unchanged. Signed-off-by: LeizhenZhang --- .../serve/tool_parser/minimax_m2_parser.py | 6 +-- .../unittest/llmapi/apps/test_tool_parsers.py | 48 +++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/tensorrt_llm/serve/tool_parser/minimax_m2_parser.py b/tensorrt_llm/serve/tool_parser/minimax_m2_parser.py index 8201138f09e3..1479dc9a0286 100644 --- a/tensorrt_llm/serve/tool_parser/minimax_m2_parser.py +++ b/tensorrt_llm/serve/tool_parser/minimax_m2_parser.py @@ -93,10 +93,10 @@ def __init__(self): r"(.*?)", re.DOTALL ) self.invoke_regex = re.compile( - r'(.*?)', re.DOTALL + r']*?)["\']?\s*>(.*?)', re.DOTALL ) self.param_regex = re.compile( - r'(.*?)', re.DOTALL + r']*?)["\']?\s*>(.*?)', re.DOTALL ) # Streaming state @@ -191,7 +191,7 @@ def parse_streaming_increment(self, new_text: str, tools: List[Tool]) -> Streami complete_invocations = list(self.invoke_regex.finditer(inner_text)) # Check for partial invoke (started but not closed) partial_invoke = re.search( - r'((?:(?!).)*?)$', + r']*?)["\']?\s*>((?:(?!).)*?)$', inner_text, re.DOTALL, ) diff --git a/tests/unittest/llmapi/apps/test_tool_parsers.py b/tests/unittest/llmapi/apps/test_tool_parsers.py index 24b8e77cd0a8..b7c0627e20c8 100644 --- a/tests/unittest/llmapi/apps/test_tool_parsers.py +++ b/tests/unittest/llmapi/apps/test_tool_parsers.py @@ -4298,6 +4298,54 @@ def test_streaming_preserves_prefix_in_same_chunk(self, sample_tools): names = [c.name for c in result.calls if c.name] assert "get_weather" in names + def test_parameter_value_with_angle_brackets_streams_in_linear_time( + self, sample_tools): + """A long parameter value full of `>` characters, four per increment. + + Code values such as `if (a > b) { p->x = q; }` hold many `>`. A + linear parse takes about a second here. A name pattern that may run + past `>` backtracks over every one of them on every increment and + takes tens of seconds, well past the bound. + """ + value = "if (a > b) { p->x = q; }\n" * 160 + "return;" + text = ('' + '' + f'{value}' + '' + '') + parser = MiniMaxM2ToolParser() + calls = [] + start = time.monotonic() + + for i in range(0, len(text), 4): + calls.extend( + parser.parse_streaming_increment(text[i:i + 4], + sample_tools).calls) + calls.extend(parser.finish(sample_tools).calls) + + assert time.monotonic() - start < 10 + first_call = [c for c in calls if c.tool_index == 0] + assert first_call[0].name == "get_weather" + arguments = "".join(c.parameters for c in first_call) + assert json.loads(arguments) == {"location": value} + + def test_detect_and_parse_parameter_value_with_angle_brackets( + self, sample_tools, parser): + """Names stop at the first `>`, values keep every `>` they hold.""" + text = ('' + '' + 'a > b -> c' + '' + '') + + result = parser.detect_and_parse(text, sample_tools) + + assert len(result.calls) == 1 + assert result.calls[0].name == "get_weather" + assert json.loads(result.calls[0].parameters) == { + "location": "a > b -> c" + } + # ============================================================================ # Reasoning Parser Tests for Interleaved Thinking From 104decc08901c9b47bcd85b2c40cc0fc5ad0781b Mon Sep 17 00:00:00 2001 From: LeizhenZhang Date: Sun, 25 Oct 2026 13:17:46 -0500 Subject: [PATCH 2/2] [#20955][fix] MiniMax-M2 tool parser: name the angle-bracket test by its time bound The test asserts a fixed time bound, so drop the linear-time claim from its name and docstring. The bound and the value checks are unchanged. Signed-off-by: LeizhenZhang --- tests/unittest/llmapi/apps/test_tool_parsers.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/unittest/llmapi/apps/test_tool_parsers.py b/tests/unittest/llmapi/apps/test_tool_parsers.py index b7c0627e20c8..72a0c9b91ca4 100644 --- a/tests/unittest/llmapi/apps/test_tool_parsers.py +++ b/tests/unittest/llmapi/apps/test_tool_parsers.py @@ -4298,14 +4298,13 @@ def test_streaming_preserves_prefix_in_same_chunk(self, sample_tools): names = [c.name for c in result.calls if c.name] assert "get_weather" in names - def test_parameter_value_with_angle_brackets_streams_in_linear_time( + def test_parameter_value_with_angle_brackets_streams_within_time_bound( self, sample_tools): - """A long parameter value full of `>` characters, four per increment. + """Stream a long parameter value full of `>` within the time bound. - Code values such as `if (a > b) { p->x = q; }` hold many `>`. A - linear parse takes about a second here. A name pattern that may run - past `>` backtracks over every one of them on every increment and - takes tens of seconds, well past the bound. + Code values such as `if (a > b) { p->x = q; }` hold many `>`, four per + increment here. The bound guards against regex work that grows with + every `>` seen on each increment. """ value = "if (a > b) { p->x = q; }\n" * 160 + "return;" text = (''