Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions tensorrt_llm/serve/tool_parser/minimax_m2_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,10 @@ def __init__(self):
r"<minimax:tool_call>(.*?)</minimax:tool_call>", re.DOTALL
)
self.invoke_regex = re.compile(
r'<invoke\s+name=["\']?(.*?)["\']?\s*>(.*?)</invoke>', re.DOTALL
r'<invoke\s+name=["\']?([^>]*?)["\']?\s*>(.*?)</invoke>', re.DOTALL
)
self.param_regex = re.compile(
r'<parameter\s+name=["\']?(.*?)["\']?\s*>(.*?)</parameter>', re.DOTALL
r'<parameter\s+name=["\']?([^>]*?)["\']?\s*>(.*?)</parameter>', re.DOTALL
)

# Streaming state
Expand Down Expand Up @@ -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'<invoke\s+name=["\']?(.*?)["\']?\s*>((?:(?!</invoke>).)*?)$',
r'<invoke\s+name=["\']?([^>]*?)["\']?\s*>((?:(?!</invoke>).)*?)$',
inner_text,
re.DOTALL,
)
Expand Down
47 changes: 47 additions & 0 deletions tests/unittest/llmapi/apps/test_tool_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ('<minimax:tool_call>'
'<invoke name="get_weather">'
f'<parameter name="location">{value}</parameter>'
'</invoke>'
'</minimax:tool_call>')
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 = ('<minimax:tool_call>'
'<invoke name="get_weather">'
'<parameter name="location">a > b -> c</parameter>'
'</invoke>'
'</minimax:tool_call>')

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
Expand Down