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..72a0c9b91ca4 100644 --- a/tests/unittest/llmapi/apps/test_tool_parsers.py +++ b/tests/unittest/llmapi/apps/test_tool_parsers.py @@ -4298,6 +4298,53 @@ 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_within_time_bound( + self, sample_tools): + """Stream a long parameter value full of `>` within the time 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 = ('' + '' + 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