Skip to content
Merged
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
21 changes: 9 additions & 12 deletions crates/tool_parser/src/parsers/minimax_m2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -475,18 +475,15 @@ impl ToolParser for MinimaxM2Parser {
parameters: "{}".to_string(),
});
self.streamed_args_for_tool[tool_id].push_str("{}");
} else if !current_streamed.ends_with('}') {
// Count opening and closing braces to check if JSON is complete
let open_braces = current_streamed.matches('{').count();
let close_braces = current_streamed.matches('}').count();
if open_braces > close_braces {
calls.push(ToolCallItem {
tool_index: tool_id,
name: None,
parameters: "}".to_string(),
});
self.streamed_args_for_tool[tool_id].push('}');
}
} else {
// Parameter fragments leave the outer object open.
Comment thread
ai-jz marked this conversation as resolved.
// Braces inside values do not close that object.
calls.push(ToolCallItem {
tool_index: tool_id,
name: None,
parameters: "}".to_string(),
});
self.streamed_args_for_tool[tool_id].push('}');
}
}

Expand Down
73 changes: 73 additions & 0 deletions crates/tool_parser/tests/tool_parser_minimax_m2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,79 @@ async fn test_minimax_streaming_empty_parameters() {
}
}

#[tokio::test]
async fn test_minimax_streaming_closes_outer_object_after_nested_value() {
let tools = vec![Tool {
tool_type: "function".to_string(),
function: Function {
name: "process".to_string(),
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 (_normal, complete) = MinimaxM2Parser::new()
.parse_complete_with_tools(input, &tools)
.await
.unwrap();
let expected: serde_json::Value =
serde_json::from_str(&complete[0].function.arguments).unwrap();
assert_eq!(expected, json!({"data": {"host": "db.example"}}));

for chunk_size in [input.len(), 7] {
let mut parser = MinimaxM2Parser::new();
let mut arguments = String::new();
for chunk in input.as_bytes().chunks(chunk_size) {
let result = parser
.parse_incremental(std::str::from_utf8(chunk).unwrap(), &tools)
.await
.unwrap();
for call in result.calls {
arguments.push_str(&call.parameters);
}
}
if let Some(calls) = parser.get_unstreamed_tool_args() {
for call in calls {
arguments.push_str(&call.parameters);
}
}
assert_eq!(
serde_json::from_str::<serde_json::Value>(&arguments).unwrap(),
expected
);
}
}

#[tokio::test]
async fn test_minimax_streaming_closure_ignores_braces_in_strings_and_resets() {
let mut parser = MinimaxM2Parser::new();
for _ in 0..2 {
let mut arguments = [String::new(), String::new()];
for chunk in [
"<minimax:tool_call>",
r#"<invoke name="process"><parameter name="text">}</parameter></invoke>"#,
r#"<invoke name="process"></invoke>"#,
"</minimax:tool_call>",
] {
let result = parser.parse_incremental(chunk, &[]).await.unwrap();
for call in result.calls {
arguments[call.tool_index].push_str(&call.parameters);
}
}
assert_eq!(
serde_json::from_str::<serde_json::Value>(&arguments[0]).unwrap(),
json!({"text": "}"})
);
assert_eq!(arguments[1], "{}");
assert!(parser.get_unstreamed_tool_args().is_none());
parser.reset();
}
}

#[tokio::test]
async fn test_minimax_multiline_parameter_values() {
let parser = MinimaxM2Parser::new();
Expand Down
Loading