1use std::{collections::HashMap, fmt::Write as FmtWrite};
2
3use async_trait::async_trait;
4use openai_protocol::common::Tool;
5use regex::Regex;
6use serde_json::Value;
7
8use crate::{
9 errors::ParserResult,
10 parsers::helpers,
11 traits::ToolParser,
12 types::{FunctionCall, StreamingParseResult, ToolCall, ToolCallItem},
13};
14
15pub struct MinimaxM2Parser {
28 tool_call_extractor: Regex,
30 invoke_extractor: Regex,
31 param_extractor: Regex,
32
33 buffer: String,
35 prev_tool_call_arr: Vec<Value>,
36 current_tool_id: i32,
37 streamed_args_for_tool: Vec<String>,
38 current_function_name: String,
39 current_parameters: HashMap<String, Value>,
40 in_tool_call: bool,
41 function_name_sent: bool,
42
43 tool_call_start_token: &'static str,
45 tool_call_end_token: &'static str,
46 invoke_end_token: &'static str,
47}
48
49impl MinimaxM2Parser {
50 #[inline]
52 fn parse_value(text: &str) -> Value {
53 match text {
55 "true" | "True" => return Value::Bool(true),
56 "false" | "False" => return Value::Bool(false),
57 "null" | "None" => return Value::Null,
58 _ => {}
59 }
60
61 if let Ok(num) = text.parse::<i64>() {
63 return Value::Number(num.into());
64 }
65
66 if let Ok(num) = text.parse::<f64>() {
67 if let Some(n) = serde_json::Number::from_f64(num) {
68 return Value::Number(n);
69 }
70 }
71
72 Value::String(text.to_string())
74 }
75
76 #[expect(
78 clippy::expect_used,
79 reason = "regex patterns are compile-time string literals"
80 )]
81 pub fn new() -> Self {
82 let tool_call_pattern = r"(?s)<minimax:tool_call>.*?</minimax:tool_call>";
84 let tool_call_extractor = Regex::new(tool_call_pattern).expect("Valid regex pattern");
85
86 let invoke_pattern = r#"(?s)<invoke\s+name="([^"]+)">(.*?)</invoke>"#;
87 let invoke_extractor = Regex::new(invoke_pattern).expect("Valid regex pattern");
88
89 let param_pattern = r#"(?s)<parameter\s+name="([^"]+)">(.*?)</parameter>"#;
90 let param_extractor = Regex::new(param_pattern).expect("Valid regex pattern");
91
92 Self {
93 tool_call_extractor,
94 invoke_extractor,
95 param_extractor,
96 buffer: String::new(),
97 prev_tool_call_arr: Vec::new(),
98 current_tool_id: -1,
99 streamed_args_for_tool: Vec::new(),
100 current_function_name: String::new(),
101 current_parameters: HashMap::new(),
102 in_tool_call: false,
103 function_name_sent: false,
104 tool_call_start_token: "<minimax:tool_call>",
105 tool_call_end_token: "</minimax:tool_call>",
106 invoke_end_token: "</invoke>",
107 }
108 }
109
110 fn parse_parameters(
113 &self,
114 params_text: &str,
115 param_types: &HashMap<String, String>,
116 ) -> serde_json::Map<String, Value> {
117 let mut parameters = serde_json::Map::new();
118
119 for capture in self.param_extractor.captures_iter(params_text) {
120 let key = capture.get(1).map_or("", |m| m.as_str()).trim();
121 let value_str = capture.get(2).map_or("", |m| m.as_str());
122
123 let decoded_value = Self::decode_xml_entities(value_str);
124 let value = helpers::coerce_by_schema_type(
125 &decoded_value,
126 param_types.get(key).map(String::as_str),
127 )
128 .unwrap_or_else(|| Self::parse_value(&decoded_value));
129
130 parameters.insert(key.to_string(), value);
131 }
132
133 parameters
134 }
135
136 fn decode_xml_entities(text: &str) -> String {
138 text.replace("<", "<")
139 .replace(">", ">")
140 .replace("&", "&")
141 .replace(""", "\"")
142 .replace("'", "'")
143 }
144
145 fn parse_tool_call(&self, block: &str, tools: &[Tool]) -> Vec<ToolCall> {
151 let mut calls = Vec::new();
152 for captures in self.invoke_extractor.captures_iter(block) {
153 let func_name = captures.get(1).map_or("", |m| m.as_str()).trim();
155
156 let params_text = captures.get(2).map_or("", |m| m.as_str());
158
159 let param_types = helpers::param_types_for_function(tools, func_name);
161 let parameters = self.parse_parameters(params_text, ¶m_types);
162
163 match serde_json::to_string(¶meters) {
164 Ok(arguments_str) => calls.push(ToolCall {
165 function: FunctionCall {
166 name: func_name.to_string(),
167 arguments: arguments_str,
168 },
169 }),
170 Err(e) => tracing::debug!("Failed to serialize tool call arguments: {}", e),
171 }
172 }
173 calls
174 }
175
176 fn parse_tool_calls_from_text(
178 &self,
179 text: &str,
180 tools: &[Tool],
181 ) -> (Vec<ToolCall>, Option<usize>) {
182 let mut tool_calls = Vec::new();
183 let mut first_valid_pos = None;
184
185 for mat in self.tool_call_extractor.find_iter(text) {
186 let calls = self.parse_tool_call(mat.as_str(), tools);
187 if !calls.is_empty() && first_valid_pos.is_none() {
188 first_valid_pos = Some(mat.start());
189 }
190 tool_calls.extend(calls);
191 }
192
193 (tool_calls, first_valid_pos)
194 }
195
196 fn parse_complete_inner(&self, text: &str, tools: &[Tool]) -> (String, Vec<ToolCall>) {
198 if !self.has_tool_markers(text) {
199 return (text.to_string(), vec![]);
200 }
201 let (tool_calls, first_valid_tool_pos) = self.parse_tool_calls_from_text(text, tools);
202 if tool_calls.is_empty() {
203 return (text.to_string(), vec![]);
204 }
205 let normal_text = match first_valid_tool_pos {
206 Some(pos) => text[..pos].to_string(),
207 None => text.to_string(),
208 };
209 (normal_text, tool_calls)
210 }
211
212 fn parse_and_stream_parameters(&mut self, text: &str, tools: &[Tool]) -> Vec<ToolCallItem> {
214 let mut calls = Vec::new();
215 let param_types = helpers::param_types_for_function(tools, &self.current_function_name);
216
217 let param_matches: Vec<_> = self
219 .param_extractor
220 .captures_iter(text)
221 .map(|cap| {
222 let name = cap.get(1).map_or("", |m| m.as_str()).trim().to_string();
223 let value_str = cap.get(2).map_or("", |m| m.as_str());
224 let decoded = Self::decode_xml_entities(value_str);
225
226 let value = helpers::coerce_by_schema_type(
229 &decoded,
230 param_types.get(&name).map(String::as_str),
231 )
232 .unwrap_or_else(|| {
233 if decoded.starts_with('{') || decoded.starts_with('[') {
234 serde_json::from_str::<Value>(&decoded)
235 .unwrap_or_else(|_| Self::parse_value(&decoded))
236 } else {
237 Self::parse_value(&decoded)
238 }
239 });
240
241 (name, value)
242 })
243 .collect();
244
245 let mut new_params = HashMap::new();
247 for (name, value) in param_matches {
248 new_params.insert(name, value);
249 }
250
251 if !new_params.is_empty() && new_params != self.current_parameters {
253 let tool_id = self.current_tool_id as usize;
254
255 while self.streamed_args_for_tool.len() <= tool_id {
257 self.streamed_args_for_tool.push(String::new());
258 }
259
260 if self.current_parameters.is_empty() {
262 let mut json_fragment = String::with_capacity(256);
264 json_fragment.push('{');
265
266 let mut first = true;
267 for (key, value) in &new_params {
268 if !first {
269 json_fragment.push_str(", ");
270 }
271 let key_json = serde_json::to_string(key).unwrap_or_default();
273 let value_json = serde_json::to_string(value).unwrap_or_default();
274 let _ = write!(&mut json_fragment, "{key_json}: {value_json}");
275 first = false;
276 }
277
278 calls.push(ToolCallItem {
279 tool_index: tool_id,
280 name: None,
281 parameters: json_fragment.clone(),
282 });
283
284 self.streamed_args_for_tool[tool_id] = json_fragment;
285 } else {
286 let new_keys: Vec<_> = new_params
288 .keys()
289 .filter(|k| !self.current_parameters.contains_key(*k))
290 .collect();
291
292 if !new_keys.is_empty() {
293 let mut json_fragment = String::with_capacity(128);
294
295 for key in new_keys {
296 let value = &new_params[key];
297 let key_json = serde_json::to_string(key).unwrap_or_default();
299 let value_json = serde_json::to_string(value).unwrap_or_default();
300 let _ = write!(&mut json_fragment, ", {key_json}: {value_json}");
301 }
302
303 calls.push(ToolCallItem {
304 tool_index: tool_id,
305 name: None,
306 parameters: json_fragment.clone(),
307 });
308
309 self.streamed_args_for_tool[tool_id].push_str(&json_fragment);
310 }
311 }
312
313 self.current_parameters = new_params;
315
316 while self.prev_tool_call_arr.len() <= tool_id {
318 self.prev_tool_call_arr.push(Value::Null);
319 }
320 self.prev_tool_call_arr[tool_id] = serde_json::json!({
321 "name": self.current_function_name,
322 "arguments": self.current_parameters,
323 });
324 }
325
326 calls
327 }
328}
329
330impl Default for MinimaxM2Parser {
331 fn default() -> Self {
332 Self::new()
333 }
334}
335
336#[async_trait]
337impl ToolParser for MinimaxM2Parser {
338 async fn parse_complete(&self, text: &str) -> ParserResult<(String, Vec<ToolCall>)> {
339 Ok(self.parse_complete_inner(text, &[]))
340 }
341
342 async fn parse_complete_with_tools(
343 &self,
344 text: &str,
345 tools: &[Tool],
346 ) -> ParserResult<(String, Vec<ToolCall>)> {
347 Ok(self.parse_complete_inner(text, tools))
348 }
349
350 async fn parse_incremental(
351 &mut self,
352 chunk: &str,
353 tools: &[Tool],
354 ) -> ParserResult<StreamingParseResult> {
355 self.buffer.push_str(chunk);
356 let mut normal_text = String::new();
357 let mut calls = Vec::new();
358
359 loop {
360 if !self.in_tool_call && !self.buffer.contains(self.tool_call_start_token) {
362 if let Some(partial_len) =
364 helpers::ends_with_partial_token(&self.buffer, self.tool_call_start_token)
365 {
366 let end = self.buffer.len() - partial_len;
368 normal_text = self.buffer[..end].to_string();
369 self.buffer = self.buffer[end..].to_string();
370 } else {
371 normal_text.clone_from(&self.buffer);
373 self.buffer.clear();
374 }
375 break;
376 }
377
378 if !self.in_tool_call {
380 if let Some(start) = self.buffer.find(self.tool_call_start_token) {
381 normal_text = self.buffer[..start].to_string();
382 self.buffer =
383 self.buffer[start + self.tool_call_start_token.len()..].to_string();
384
385 self.in_tool_call = true;
386 self.function_name_sent = false;
387 self.current_function_name.clear();
388 self.current_parameters.clear();
389
390 continue;
391 } else {
392 break;
394 }
395 }
396
397 if !self.function_name_sent {
399 if let Some(end_pos) = self.buffer.find(self.tool_call_end_token) {
402 let next_invoke = self.buffer.find("<invoke");
403 if next_invoke.is_none_or(|i| end_pos < i) {
404 self.buffer =
405 self.buffer[end_pos + self.tool_call_end_token.len()..].to_string();
406 self.in_tool_call = false;
407 self.current_function_name.clear();
408 self.current_parameters.clear();
409 continue;
410 }
411 }
412
413 if let Some(captures) = self.invoke_extractor.captures(&self.buffer) {
416 let function_name = captures
417 .get(1)
418 .map_or("", |m| m.as_str())
419 .trim()
420 .to_string();
421
422 self.current_function_name.clone_from(&function_name);
425 self.function_name_sent = true;
426
427 if self.current_tool_id == -1 {
429 self.current_tool_id = 0;
430 }
431
432 helpers::ensure_capacity(
434 self.current_tool_id,
435 &mut self.prev_tool_call_arr,
436 &mut self.streamed_args_for_tool,
437 );
438
439 calls.push(ToolCallItem {
441 tool_index: self.current_tool_id as usize,
442 name: Some(function_name),
443 parameters: String::new(),
444 });
445
446 if let Some(pos) = self.buffer.find('>') {
449 self.buffer = self.buffer[pos + 1..].to_string();
450 }
451 continue;
452 }
453 break;
455 }
456
457 if self.function_name_sent {
459 let buffer_copy = self.buffer.clone(); let parameter_calls = self.parse_and_stream_parameters(&buffer_copy, tools);
462 calls.extend(parameter_calls);
463
464 if let Some(invoke_end) = self.buffer.find(self.invoke_end_token) {
466 let tool_id = self.current_tool_id as usize;
468 if tool_id < self.streamed_args_for_tool.len() {
469 let current_streamed = &self.streamed_args_for_tool[tool_id];
470 if !current_streamed.is_empty() && !current_streamed.ends_with('}') {
471 let open_braces = current_streamed.matches('{').count();
473 let close_braces = current_streamed.matches('}').count();
474 if open_braces > close_braces {
475 calls.push(ToolCallItem {
476 tool_index: tool_id,
477 name: None,
478 parameters: "}".to_string(),
479 });
480 self.streamed_args_for_tool[tool_id].push('}');
481 }
482 }
483 }
484
485 self.buffer =
487 self.buffer[invoke_end + self.invoke_end_token.len()..].to_string();
488
489 self.function_name_sent = false;
495 self.current_function_name.clear();
496 self.current_parameters.clear();
497 self.current_tool_id += 1;
498 continue;
499 }
500 break;
502 }
503 }
504
505 Ok(StreamingParseResult { normal_text, calls })
506 }
507
508 fn has_tool_markers(&self, text: &str) -> bool {
509 text.contains(self.tool_call_start_token)
510 }
511
512 fn get_unstreamed_tool_args(&self) -> Option<Vec<ToolCallItem>> {
513 helpers::get_unstreamed_args(&self.prev_tool_call_arr, &self.streamed_args_for_tool)
514 }
515
516 fn reset(&mut self) {
517 self.buffer.clear();
518 self.prev_tool_call_arr.clear();
519 self.current_tool_id = -1;
520 self.streamed_args_for_tool.clear();
521 self.current_function_name.clear();
522 self.current_parameters.clear();
523 self.in_tool_call = false;
524 self.function_name_sent = false;
525 }
526}