Skip to content

fix(tool-parser): close MiniMax M2 streamed argument objects - #2745

Merged
slin1237 merged 1 commit into
mainfrom
ai-jz/minimax-m2-json-closure
Oct 3, 2026
Merged

slin1237 merged 1 commit into
mainfrom
ai-jz/minimax-m2-json-closure

Conversation

@ai-jz

@ai-jz ai-jz commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Problem

SMG's MiniMax M2 tool parser can return a valid tool call in non-streaming mode but malformed JSON in streaming mode. For example, a tool expects {"data":{"host":"db.example"}}, but the assembled stream is {"data": {"host":"db.example"}: the outer arguments object never closes, so the client cannot decode it or execute the tool. This was found while checking SMG's native Rust parser; it does not depend on a verifier's expected answer.

This fixes minimax_m2; MiniMax M3 uses the separate minimax_m3 parser, which this PR does not change.

The completion check treats a value's final } as the end of the outer object. Counting braces also treats braces inside strings as JSON structure.

Solution

When </invoke> arrives, close the outer object that the parser opened. Parameter fragments never close that object themselves. Preserve the {} output for empty calls from #2718.

This follows the existing Qwen XML fix in #2490: close the outer object at the tool-call boundary rather than counting braces inside values.

Changes

  • Replace the suffix and brace-count checks with the parser's existing invoke-completion boundary; no new state or API.
  • Add two regressions covering nested objects, braces in string values, empty consecutive calls, and reuse after reset.

Test Plan

Minimal reproducible example — CPU only, no model required

From an SMG checkout, create the example directory with mkdir -p crates/tool_parser/examples. Save the following as crates/tool_parser/examples/repro_minimax_closure.rs, then run cargo run -p tool-parser --example repro_minimax_closure.

use openai_protocol::common::{Function, Tool};
use serde_json::{json, Value};
use tool_parser::{MinimaxM2Parser, ToolParser};

#[tokio::main]
async fn main() {
    let tools = vec![Tool {
        tool_type: "function".into(),
        function: Function {
            name: "process".into(),
            description: None,
            parameters: json!({
                "type": "object",
                "properties": {"data": {"type": "object"}}
            }),
            strict: None,
        },
    }];
    let input = r#"<minimax:tool_call><invoke name="process"><parameter name="data">{"host":"db.example"}</parameter></invoke></minimax:tool_call>"#;
    let (_, complete) = MinimaxM2Parser::new()
        .parse_complete_with_tools(input, &tools)
        .await
        .unwrap();
    println!("complete: {}", complete[0].function.arguments);

    let mut parser = MinimaxM2Parser::new();
    let mut streamed = String::new();
    for call in parser.parse_incremental(input, &tools).await.unwrap().calls {
        streamed.push_str(&call.parameters);
    }
    if let Some(calls) = parser.get_unstreamed_tool_args() {
        for call in calls {
            streamed.push_str(&call.parameters);
        }
    }
    println!("streamed: {streamed}");
    let parsed: Value = serde_json::from_str(&streamed).unwrap();
    assert_eq!(parsed, json!({"data": {"host": "db.example"}}));
}

Remove the temporary example before running repository lint checks.

Input Before After
Complete typed object Valid {"data":{"host":"db.example"}} Same
Same text through streaming, including EOF recovery Missing the outer }; JSON decoding fails Valid JSON equal to complete parsing

Tested on an independent patch against c0d3efa4ff829694164d901bbe7a837923604fc1, with Rust 1.98.0: the MRE fails before and passes after the fix, and all 552 tool-parser tests pass with no skips. Parser Clippy and workspace formatting pass.

Focused regression command:

cargo test -p tool-parser --test tool_parser_minimax_m2

The nested-object test uses both a whole input and seven-byte chunks. The second regression checks a string containing }, followed by an empty invoke in the same wrapper, and repeats after reset(). The existing scalar, empty-call, parallel-call, and partial-tag tests remain applicable. Non-streaming parsing and parameter conversion are unchanged. A separate, pre-existing issue can mix arguments when multiple invokes arrive in one chunk; invocation scoping is outside this JSON-closure fix.

Selected command output from the independent patch
Finished `dev` profile [unoptimized] target(s) in 22.93s
test result: ok. 117 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s

The test line is the library test binary; the total across all tool-parser test binaries is reported above.

Repository checks

Workspace checks also passed with this patch and the two companion parser fixes (#2743 and #2744) applied together. The parser checks above tested this PR independently.

Command Result
cargo +nightly fmt --all -- --check PASS, exit 0
cargo clippy --locked --all-targets --all-features -- -D warnings PASS, exit 0
cargo test --locked PASS, exit 0; 44 existing ignored tests

WASM test fixtures were built. Fixture-dependent vision cases were not exercised because the CI vision-golden generation step was not run. Ignored external-service/campaign tests and GPU serving were not run; this is not a full hosted-CI result.

Hosted CI

Both workflows completed successfully for PR head 19f33a5495e51e89618bfbb13ce6f8570c393fbd:

Workflow Result
PR Test (SMG) PASS, including Rust unit tests and the repository's serving E2E matrix
Benchmark - Tool Parser PASS

Claude Respond and go-bindings-benchmark were skipped by the workflow; they are not counted as passing tests.

Checklist
  • cargo +nightly fmt --all -- --check passes
  • Workspace Clippy passes with all three fixes applied
  • Focused tests and MRE pass on the final change
  • Workspace gate results and coverage limits recorded

Signed-off-by: ai-jz <ai-jz@users.noreply.github.com>
@github-actions github-actions Bot added tests Test changes tool-parser Tool/function call parser changes labels Oct 3, 2026
@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4075469c-d653-4c35-a7e6-26be9f3f1e89
📥 Commits

Reviewing files that changed from the base of the PR and between c0d3efa and 19f33a5.

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

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 1 remain after this review.


📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • Improved MiniMax M2 streaming so nested object parameters are completed consistently when an invocation ends, including when values contain closing braces inside strings.
    • Improved consistency between complete and streamed parsing across different input chunk sizes.
    • Added coverage for successive invocations, including empty parameters and parser resets.

Walkthrough

When a MiniMax M2 invocation ends after streaming parameters, the parser now emits and stores a closing brace unconditionally. New tests cover nested object parameters, chunked input, and successive parser runs.

Changes

MiniMax M2 streaming

Layer / File(s) Summary
Close streamed parameter objects
crates/tool_parser/src/parsers/minimax_m2.rs, crates/tool_parser/tests/tool_parser_minimax_m2.rs
The parser now emits and stores } when an invocation ends after parameters were streamed. Tests compare complete parsing with whole-input and 7-byte streaming, and check successive runs, including an empty invocation and a brace inside a string.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 19f33

The MiniMax M2 streaming fix is mergeable after normal checks; no remaining defect is established.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 2 files.
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.
Title check ✅ Passed The title clearly identifies the MiniMax M2 streaming argument-object closure fix.
Description check ✅ Passed The description explains the streaming JSON closure issue, the fix, and the regression tests. It is directly related to the changeset.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

Comment thread crates/tool_parser/src/parsers/minimax_m2.rs
@slin1237
slin1237 merged commit 8a1f298 into main Oct 3, 2026
67 checks passed
@slin1237
slin1237 deleted the ai-jz/minimax-m2-json-closure branch October 3, 2026 12:44
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