Skip to content

fix(tool_parser): parse all parallel invokes in a minimax_m2 wrapper - #1824

Merged
key4ng merged 1 commit into
mainfrom
fix/minimax-parallel-tool-calls
Jun 23, 2026
Merged

key4ng merged 1 commit into
mainfrom
fix/minimax-parallel-tool-calls

Conversation

@key4ng

@key4ng key4ng commented Jun 23, 2026 •

Copy link
Copy Markdown
Member

Description

Problem

On the nightly BFCL A/B, MiniMax-M2.7 scored 0 on the SMG arm for every parallel category — parallel, parallel_multiple, live_parallel, live_parallel_multiple — while pure vLLM scored normally (e.g. live_parallel 75 → 0). Per-case dumps showed SMG returned 1 tool call where vLLM returned 2 (16/16 failed live_parallel cases returned ≤1 call), failing BFCL's wrong_count check.

Root cause: MiniMax M2 emits parallel calls as multiple <invoke> blocks inside one <minimax:tool_call> wrapper (Anthropic-style), but minimax_m2.rs only extracted the first:

  • Non-streaming: invoke_extractor.captures(block) returns only the first match.
  • Streaming: after the first </invoke> it jumped straight to the wrapper-end (waiting_for_tool_call_end), discarding any later <invoke> in the same wrapper.

(vLLM's reference parser does invoke_complete_regex.findall(text) — all invokes.)

Solution

  • Non-streaming: parse_tool_call now iterates captures_iter over every <invoke> in the wrapper and returns a Vec<ToolCall>; parse_tool_calls_from_text extends with all of them.
  • Streaming: after each </invoke>, reset per-invoke state and advance the tool index, then either parse the next <invoke> in the wrapper or close the wrapper when </minimax:tool_call> precedes the next <invoke>. Removed the now-unused waiting_for_tool_call_end state.

Changes

  • crates/tool_parser/src/parsers/minimax_m2.rs — multi-invoke extraction (non-streaming + streaming).
  • crates/tool_parser/tests/tool_parser_minimax_m2.rs — add test_minimax_parallel_invokes_in_one_wrapper (non-streaming) and test_minimax_streaming_parallel_invokes_in_one_wrapper (streaming).

Test Plan

  • cargo test -p tool-parser — all pass (33 in the minimax suite incl. the 2 new; full crate green).
  • cargo +nightly fmt / cargo clippy -p tool-parser --all-targets — clean.
  • Behavioral validation: the next nightly BFCL A/B should bring MiniMax's parallel/live_parallel categories back to vLLM parity.
Checklist
  • cargo +nightly fmt passes
  • cargo clippy --all-targets --all-features -- -D warnings passes
  • (Optional) Documentation updated
  • (Optional) Please join us on Slack #sig-smg to discuss, review, and merge PRs

Summary by CodeRabbit

  • New Features

    • MiniMax M2 parser now supports extraction of multiple tool calls from a single request.
  • Bug Fixes

    • Improved error handling—JSON serialization failures are now logged instead of halting the parser.
  • Tests

    • Added test coverage for parallel tool invocation parsing scenarios.

…l wrapper

MiniMax M2 emits parallel tool calls as multiple <invoke> blocks inside a
single <minimax:tool_call> wrapper (Anthropic-style). The parser extracted
only the first: non-streaming used invoke_extractor.captures() (first match),
and streaming jumped to the wrapper-end after one </invoke>, discarding the
rest. This zeroed the BFCL parallel/parallel_multiple/live_parallel categories
on the SMG arm (it returned 1 call where vLLM returned N).

Non-streaming now iterates captures_iter over every <invoke> in the wrapper;
streaming resets per-invoke state after each </invoke> and advances the tool
index, closing the wrapper only when </minimax:tool_call> precedes the next
<invoke>. Adds non-streaming + streaming parallel-in-one-wrapper tests.

Signed-off-by: key4ng <rukeyang@gmail.com>
@coderabbitai

coderabbitai Bot commented Jun 23, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The MiniMax M2 tool-call parser is refactored to support multiple parallel <invoke> blocks inside a single <minimax:tool_call> wrapper. parse_tool_call now returns Vec<ToolCall>, the waiting_for_tool_call_end state field is removed, the incremental streaming state machine is updated to chain invokes, and two integration tests cover complete and streaming paths.

Changes

MiniMax M2 parallel invoke parsing

Layer / File(s) Summary
parse_tool_call signature and complete parsing path
crates/tool_parser/src/parsers/minimax_m2.rs
parse_tool_call changed to iterate all <invoke> captures and return Vec<ToolCall>; serialization errors now log at debug level and skip rather than propagate; parse_tool_calls_from_text extends results from the Vec and sets first_valid_pos conditionally; ParserError import, waiting_for_tool_call_end field, constructor init, and reset() entry all removed.
Incremental streaming state machine
crates/tool_parser/src/parsers/minimax_m2.rs
Removes waiting_for_tool_call_end branch; adds wrapper-end-tag detection before the next <invoke> to exit tool-call mode; after </invoke> resets per-invoke state, increments tool_index, and immediately continues for the next invoke slot.
Integration tests for parallel invokes
crates/tool_parser/tests/tool_parser_minimax_m2.rs
Two new async tests: parse_complete asserts two invokes in one wrapper yield two ToolCalls with correct names/args and empty normal_text; parse_incremental asserts both tool names are streamed and the second invoke uses tool_index == 1.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • lightseekorg/smg#1728: Modifies the same minimax_m2.rs incremental parse_incremental state logic and MiniMax M2 streaming tests, overlapping directly with this PR's changes.

Suggested labels

tests, tool-parser

Suggested reviewers

  • slin1237
  • CatherineSue

Poem

🐇 Hop, hop, two invokes in one wrapper neat,
The parser now loops where before it would retreat.
waiting_for_tool_call_end? Gone without a trace!
Each </invoke> found just bumps the index place.
Two tool calls emerge, both correct and fleet —
A multi-invoke bunny dance, oh what a treat! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(tool_parser): parse all parallel invokes in a minimax_m2 wrapper' accurately and specifically describes the main change—fixing the parser to handle multiple parallel tool calls instead of just the first one, which directly addresses the root cause documented in the PR objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/minimax-parallel-tool-calls

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added tests Test changes tool-parser Tool/function call parser changes labels Jun 23, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean fix. The → change for non-streaming and the state-machine rework for streaming both look correct. Traced through the parallel-invoke streaming path (single chunk, split chunks, wrapper-close edge cases) and the tool_index accounting is sound. Tests cover the key cases well.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the MinimaxM2Parser to support parsing and streaming multiple parallel <invoke> blocks within a single <minimax:tool_call> wrapper. It refactors parse_tool_call to return a list of tool calls using captures_iter, simplifies the parser's state machine by removing the waiting_for_tool_call_end state, and adds unit tests to verify both complete and incremental parsing of parallel tool calls. There are no review comments, so no feedback is provided.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c2b49b218b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +494 to +498
self.function_name_sent = false;
self.current_function_name.clear();
self.current_parameters.clear();
self.current_tool_id += 1;
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Limit streaming parameter parsing to the current invoke

When a streaming delta contains two <invoke> blocks in the same <minimax:tool_call> wrapper, this new path advances to the next invoke only after parse_and_stream_parameters(&buffer_copy, tools) has already scanned every <parameter> tag still in self.buffer. That means parameters from the second invoke are emitted on the first tool call (overwriting same-named args like location), and then the second tool is parsed again with its own args. Slice parameter parsing to the text before the first </invoke> before resetting state and continuing to the next invoke.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/tool_parser/tests/tool_parser_minimax_m2.rs`:
- Around line 276-294: The streaming test in the chunk parsing loop should
accumulate not just tool names but also the streamed parameters from each
invoke. Modify the test to track the parameters field from each call in
result.calls alongside the names vector, then add assertions after the loop to
verify that the first invoke's parameters contain Beijing and the second
invoke's parameters contain Shanghai, which will catch any cross-contamination
of arguments between the two invoke slots.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f8a7b262-eede-4ad8-8f52-e06114b14e59

📥 Commits

Reviewing files that changed from the base of the PR and between d00eb82 and c2b49b2.

📒 Files selected for processing (2)
  • crates/tool_parser/src/parsers/minimax_m2.rs
  • crates/tool_parser/tests/tool_parser_minimax_m2.rs

Comment on lines +276 to +294
let mut names = Vec::new();
let mut max_index = 0usize;
for chunk in chunks {
let result = parser.parse_incremental(chunk, &tools).await.unwrap();
for call in result.calls {
if let Some(name) = call.name {
names.push(name);
}
max_index = max_index.max(call.tool_index);
}
}

assert_eq!(
names.len(),
2,
"both invokes must stream a tool name, got {names:?}"
);
assert_eq!(max_index, 1, "second invoke must use tool_index 1");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert streamed argument content per invoke.

The streaming test only checks tool names and max_index. Since the regression being fixed is about chained invokes within one wrapper, accumulating each invoke's streamed parameters and asserting the first carries Beijing and the second Shanghai would catch argument cross-contamination between the two <invoke> slots, not just the count.

💚 Proposed test strengthening
     let mut names = Vec::new();
+    let mut args = vec![String::new(), String::new()];
     let mut max_index = 0usize;
     for chunk in chunks {
         let result = parser.parse_incremental(chunk, &tools).await.unwrap();
         for call in result.calls {
             if let Some(name) = call.name {
                 names.push(name);
             }
+            args[call.tool_index].push_str(&call.parameters);
             max_index = max_index.max(call.tool_index);
         }
     }

     assert_eq!(
         names.len(),
         2,
         "both invokes must stream a tool name, got {names:?}"
     );
     assert_eq!(max_index, 1, "second invoke must use tool_index 1");
+    assert!(args[0].contains("Beijing"), "first invoke args: {}", args[0]);
+    assert!(args[1].contains("Shanghai"), "second invoke args: {}", args[1]);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let mut names = Vec::new();
let mut max_index = 0usize;
for chunk in chunks {
let result = parser.parse_incremental(chunk, &tools).await.unwrap();
for call in result.calls {
if let Some(name) = call.name {
names.push(name);
}
max_index = max_index.max(call.tool_index);
}
}
assert_eq!(
names.len(),
2,
"both invokes must stream a tool name, got {names:?}"
);
assert_eq!(max_index, 1, "second invoke must use tool_index 1");
}
let mut names = Vec::new();
let mut args = vec![String::new(), String::new()];
let mut max_index = 0usize;
for chunk in chunks {
let result = parser.parse_incremental(chunk, &tools).await.unwrap();
for call in result.calls {
if let Some(name) = call.name {
names.push(name);
}
args[call.tool_index].push_str(&call.parameters);
max_index = max_index.max(call.tool_index);
}
}
assert_eq!(
names.len(),
2,
"both invokes must stream a tool name, got {names:?}"
);
assert_eq!(max_index, 1, "second invoke must use tool_index 1");
assert!(args[0].contains("Beijing"), "first invoke args: {}", args[0]);
assert!(args[1].contains("Shanghai"), "second invoke args: {}", args[1]);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tool_parser/tests/tool_parser_minimax_m2.rs` around lines 276 - 294,
The streaming test in the chunk parsing loop should accumulate not just tool
names but also the streamed parameters from each invoke. Modify the test to
track the parameters field from each call in result.calls alongside the names
vector, then add assertions after the loop to verify that the first invoke's
parameters contain Beijing and the second invoke's parameters contain Shanghai,
which will catch any cross-contamination of arguments between the two invoke
slots.

@key4ng
key4ng merged commit 5949be8 into main Jun 23, 2026
83 of 85 checks passed
@key4ng
key4ng deleted the fix/minimax-parallel-tool-calls branch June 23, 2026 21:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tests Test changes tool-parser Tool/function call parser changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants