diff --git a/crates/tool_parser/src/parsers/minimax_m2.rs b/crates/tool_parser/src/parsers/minimax_m2.rs
index 8097889f15..fed31346fc 100644
--- a/crates/tool_parser/src/parsers/minimax_m2.rs
+++ b/crates/tool_parser/src/parsers/minimax_m2.rs
@@ -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.
+ // 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('}');
}
}
diff --git a/crates/tool_parser/tests/tool_parser_minimax_m2.rs b/crates/tool_parser/tests/tool_parser_minimax_m2.rs
index 7ed608795d..8c3f94452f 100644
--- a/crates/tool_parser/tests/tool_parser_minimax_m2.rs
+++ b/crates/tool_parser/tests/tool_parser_minimax_m2.rs
@@ -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#"{"host":"db.example"}"#;
+ 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::(&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 [
+ "",
+ r#"}"#,
+ r#""#,
+ "",
+ ] {
+ 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::(&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();