1use async_trait::async_trait;
2use openai_protocol::common::Tool;
3use serde_json::{Map, Value};
4
5use crate::{
6 errors::ParserResult,
7 parsers::helpers,
8 traits::ToolParser,
9 types::{FunctionCall, StreamingParseResult, ToolCall, ToolCallItem},
10};
11
12const NAMESPACE: &str = "]<]minimax[>[";
14const TOOL_CALL_START: &str = "]<]minimax[>[<tool_call>";
16const TOOL_CALL_END: &str = "]<]minimax[>[</tool_call>";
18const INVOKE_START: &str = "]<]minimax[>[<invoke";
20const INVOKE_END: &str = "]<]minimax[>[</invoke>";
22const ELEMENT_START: &str = "]<]minimax[>[<";
24const ELEMENT_END_START: &str = "]<]minimax[>[</";
26const MIXED_TEXT_FIELD: &str = "$text";
28
29pub struct MinimaxM3Parser {
52 buffer: String,
54 prev_tool_call_arr: Vec<Value>,
55 current_tool_id: i32,
56 streamed_args_for_tool: Vec<String>,
57 in_tool_call: bool,
58}
59
60enum ParamValue {
62 Text(String),
63 Elements(Vec<(String, ParamValue)>),
64}
65
66impl MinimaxM3Parser {
67 pub fn new() -> Self {
69 Self {
70 buffer: String::new(),
71 prev_tool_call_arr: Vec::new(),
72 current_tool_id: -1,
73 streamed_args_for_tool: Vec::new(),
74 in_tool_call: false,
75 }
76 }
77
78 fn coerce_leaf(text: &str, declared_type: Option<&str>) -> Value {
81 if text.trim().is_empty() {
86 match declared_type {
87 Some("array") => return Value::Array(Vec::new()),
88 Some("object") => return Value::Object(Map::new()),
89 _ => {}
90 }
91 }
92
93 if let Some(value) = helpers::coerce_by_schema_type(text, declared_type) {
94 return value;
95 }
96 Self::infer_value(text)
97 }
98
99 fn infer_value(text: &str) -> Value {
101 match text {
102 "true" | "True" => return Value::Bool(true),
103 "false" | "False" => return Value::Bool(false),
104 "null" | "None" => return Value::Null,
105 _ => {}
106 }
107
108 if let Ok(num) = text.parse::<i64>() {
109 return Value::Number(num.into());
110 }
111 if let Ok(num) = text.parse::<f64>() {
112 if let Some(n) = serde_json::Number::from_f64(num) {
113 return Value::Number(n);
114 }
115 }
116
117 Value::String(text.to_string())
118 }
119
120 fn longest_partial_suffix(buffer: &str, token: &str) -> Option<usize> {
127 if buffer.is_empty() || token.is_empty() {
128 return None;
129 }
130 token
131 .char_indices()
132 .skip(1)
133 .map(|(i, _)| i)
134 .filter(|&i| buffer.ends_with(&token[..i]))
135 .max()
136 }
137
138 fn could_start_invoke(buffer: &str) -> bool {
144 let Some(after_wrapper) = buffer.strip_prefix(TOOL_CALL_START) else {
145 return false;
146 };
147 let candidate = after_wrapper.trim_start();
148 if candidate.is_empty() || INVOKE_START.starts_with(candidate) {
149 return true;
150 }
151
152 let Some(after_invoke) = candidate.strip_prefix(INVOKE_START) else {
153 return false;
154 };
155 after_invoke.is_empty()
156 || after_invoke
157 .chars()
158 .next()
159 .is_some_and(|c| c.is_whitespace() || c == '>')
160 }
161
162 fn decode_xml_entities(text: &str) -> String {
164 text.replace("<", "<")
165 .replace(">", ">")
166 .replace(""", "\"")
167 .replace("'", "'")
168 .replace("&", "&")
169 }
170
171 fn parse_invoke_name(header: &str) -> Option<String> {
174 let idx = header.find("name")?;
175 let after = &header[idx + "name".len()..];
176 let after = after.trim_start();
177 let after = after.strip_prefix('=')?.trim_start();
178 if let Some(rest) = after.strip_prefix('"') {
179 let end = rest.find('"')?;
180 Some(rest[..end].trim().to_string())
181 } else if let Some(rest) = after.strip_prefix('\'') {
182 let end = rest.find('\'')?;
183 Some(rest[..end].trim().to_string())
184 } else {
185 let end = after
186 .find(|c: char| c.is_whitespace())
187 .unwrap_or(after.len());
188 let name = after[..end].trim();
189 if name.is_empty() {
190 None
191 } else {
192 Some(name.to_string())
193 }
194 }
195 }
196
197 fn parse_element_body(
202 input: &str,
203 name: &str,
204 schema: Option<&Value>,
205 parent_name: Option<&str>,
206 ) -> Option<(ParamValue, usize)> {
207 let close_tag = format!("{ELEMENT_END_START}{name}>");
208 let mut pos = 0;
209 let mut text = String::new();
210 let mut children: Vec<(String, ParamValue)> = Vec::new();
211
212 loop {
213 let rest = &input[pos..];
216 let text_chunk = &rest[..rest.find(NAMESPACE)?];
217 text.push_str(text_chunk);
218 pos += text_chunk.len();
219
220 let rest = &input[pos..];
221 if rest.starts_with(&close_tag) {
222 pos += close_tag.len();
223 break;
224 }
225 if text.trim().is_empty()
226 && children.is_empty()
227 && matches!(Self::schema_type(schema), Some("array" | "object"))
228 && parent_name.is_some_and(|parent| {
229 rest.starts_with(&format!("{ELEMENT_END_START}{parent}>"))
230 })
231 {
232 break;
235 }
236 if rest.starts_with(ELEMENT_START) {
237 let (child_name, child_value, consumed) =
239 Self::parse_element(rest, schema, Some(name))?;
240 children.push((child_name, child_value));
241 pos += consumed;
242 continue;
243 }
244 return None;
247 }
248
249 let value = if children.is_empty() {
250 ParamValue::Text(text)
251 } else {
252 if !text.trim().is_empty() {
253 Self::push_mixed_text(&mut children, text);
254 }
255 ParamValue::Elements(children)
256 };
257 Some((value, pos))
258 }
259
260 fn parse_element(
263 input: &str,
264 parent_schema: Option<&Value>,
265 parent_name: Option<&str>,
266 ) -> Option<(String, ParamValue, usize)> {
267 let after_start = input.strip_prefix(ELEMENT_START)?;
268 let gt = after_start.find('>')?;
269 let name = after_start[..gt].trim().to_string();
270 if name.is_empty() || name.starts_with('/') {
271 return None;
272 }
273 let body_start = ELEMENT_START.len() + gt + 1;
274 let parent_schema = Self::container_schema(parent_schema).or(parent_schema);
275 let schema = if Self::schema_type(parent_schema) == Some("array") {
276 parent_schema.and_then(|schema| schema.get("items"))
277 } else {
278 Self::property_schema(parent_schema, &name)
279 };
280 let schema = Self::container_schema(schema).or(schema);
281 let (value, body_consumed) =
282 Self::parse_element_body(&input[body_start..], &name, schema, parent_name)?;
283 Some((name, value, body_start + body_consumed))
284 }
285
286 fn push_mixed_text(children: &mut Vec<(String, ParamValue)>, text: String) {
289 let mut field = MIXED_TEXT_FIELD.to_string();
290 while children.iter().any(|(name, _)| *name == field) {
291 field.insert(0, '$');
292 }
293 children.push((field, ParamValue::Text(text)));
294 }
295
296 fn schema_type(schema: Option<&Value>) -> Option<&str> {
298 schema?.get("type")?.as_str()
299 }
300
301 fn container_schema(schema: Option<&Value>) -> Option<&Value> {
306 let schema = schema?;
307 if schema.get("type").is_some() {
308 return matches!(Self::schema_type(Some(schema)), Some("array" | "object"))
309 .then_some(schema);
310 }
311
312 let mut compositions = ["oneOf", "anyOf", "allOf"]
313 .into_iter()
314 .filter_map(|key| schema.get(key).map(|branches| (key, branches)));
315 let (keyword, branches) = compositions.next()?;
316 if compositions.next().is_some() {
317 return None;
318 }
319 if schema.as_object()?.keys().any(|key| {
321 key != keyword
322 && !matches!(
323 key.as_str(),
324 "title"
325 | "description"
326 | "default"
327 | "examples"
328 | "deprecated"
329 | "readOnly"
330 | "writeOnly"
331 | "$comment"
332 )
333 }) {
334 return None;
335 }
336 let branches = branches.as_array()?;
337 if keyword == "allOf" && branches.len() != 1 {
338 return None;
339 }
340 let mut container = None;
341 for branch in branches {
342 if keyword != "allOf" && Self::schema_type(Some(branch)) == Some("null") {
343 continue;
344 }
345 let resolved = Self::container_schema(Some(branch))?;
346 if container.replace(resolved).is_some() {
347 return None;
348 }
349 }
350 container
351 }
352
353 fn property_schema<'a>(schema: Option<&'a Value>, name: &str) -> Option<&'a Value> {
355 let schema = schema?;
356 if let Some(property) = schema.get("properties").and_then(|value| value.get(name)) {
357 return Some(property);
358 }
359
360 for keyword in ["oneOf", "anyOf", "allOf"] {
361 for branch in schema
362 .get(keyword)
363 .and_then(Value::as_array)
364 .into_iter()
365 .flatten()
366 {
367 if let Some(property) = Self::property_schema(Some(branch), name) {
368 return Some(property);
369 }
370 }
371 }
372
373 None
374 }
375
376 fn value_to_json(value: ParamValue, schema: Option<&Value>) -> Value {
380 let schema = Self::container_schema(schema).or(schema);
381 match value {
382 ParamValue::Text(text) => {
383 let decoded = Self::decode_xml_entities(&text);
384 Self::coerce_leaf(&decoded, Self::schema_type(schema))
385 }
386 ParamValue::Elements(children) => {
387 if Self::schema_type(schema) == Some("array") {
390 let items_schema = schema.and_then(|s| s.get("items"));
391 let items = children
392 .into_iter()
393 .map(|(_, v)| Self::value_to_json(v, items_schema))
394 .collect();
395 return Value::Array(items);
396 }
397
398 let mut map: Map<String, Value> = Map::new();
399 for (name, child) in children {
400 let child_schema = Self::property_schema(schema, &name);
401 let child_json = Self::value_to_json(child, child_schema);
402 match map.get_mut(&name) {
403 Some(Value::Array(arr)) => arr.push(child_json),
404 Some(existing) => {
405 let prev = existing.take();
406 *existing = Value::Array(vec![prev, child_json]);
407 }
408 None => {
409 map.insert(name, child_json);
410 }
411 }
412 }
413 Value::Object(map)
414 }
415 }
416 }
417
418 fn parse_invoke_params(body: &str, params_schema: Option<&Value>) -> Option<Value> {
424 let mut map: Map<String, Value> = Map::new();
425 let mut pos = 0;
426
427 loop {
428 let rest = &body[pos..];
429 let trimmed = rest.trim_start();
430 let trim_len = rest.len() - trimmed.len();
431 if trimmed.is_empty() {
432 break;
433 }
434 if !trimmed.starts_with(ELEMENT_START) {
435 break;
437 }
438 pos += trim_len;
439 let (name, value, consumed) = Self::parse_element(&body[pos..], params_schema, None)?;
440 pos += consumed;
441 let json = Self::value_to_json(value, Self::property_schema(params_schema, &name));
442 match map.get_mut(&name) {
443 Some(Value::Array(arr)) => arr.push(json),
444 Some(existing) => {
445 let prev = existing.take();
446 *existing = Value::Array(vec![prev, json]);
447 }
448 None => {
449 map.insert(name, json);
450 }
451 }
452 }
453
454 Some(Value::Object(map))
455 }
456
457 fn parse_invoke(block: &str, tools: &[Tool]) -> Option<ToolCall> {
460 let after_invoke = block.strip_prefix(INVOKE_START)?;
462 let gt = after_invoke.find('>')?;
463 let header = &after_invoke[..gt];
464 let name = Self::parse_invoke_name(header)?;
465 let body = &after_invoke[gt + 1..];
466
467 let params_schema = tools
468 .iter()
469 .find(|t| t.function.name == name)
470 .map(|t| &t.function.parameters);
471 let arguments = Self::parse_invoke_params(body, params_schema)?;
472 let arguments_str = serde_json::to_string(&arguments).ok()?;
473
474 Some(ToolCall {
475 function: FunctionCall {
476 name,
477 arguments: arguments_str,
478 },
479 })
480 }
481
482 fn parse_tool_calls(text: &str, tools: &[Tool]) -> (Vec<ToolCall>, Option<usize>) {
485 let mut calls = Vec::new();
486 let mut first_pos = None;
487 let mut search_from = 0;
488
489 while let Some(rel_start) = text[search_from..].find(TOOL_CALL_START) {
490 let block_start = search_from + rel_start;
491 let inner_start = block_start + TOOL_CALL_START.len();
492 let Some(rel_end) = text[inner_start..].find(TOOL_CALL_END) else {
493 break;
494 };
495 let inner_end = inner_start + rel_end;
496 let inner = &text[inner_start..inner_end];
497
498 let mut invoke_from = 0;
500 while let Some(rel_inv) = inner[invoke_from..].find(INVOKE_START) {
501 let inv_start = invoke_from + rel_inv;
502 let Some(rel_inv_end) = inner[inv_start..].find(INVOKE_END) else {
503 break;
504 };
505 let inv_end = inv_start + rel_inv_end;
506 let inv_block = &inner[inv_start..inv_end];
507 if let Some(call) = Self::parse_invoke(inv_block, tools) {
508 if first_pos.is_none() {
509 first_pos = Some(block_start);
510 }
511 calls.push(call);
512 }
513 invoke_from = inv_end + INVOKE_END.len();
514 }
515
516 search_from = inner_end + TOOL_CALL_END.len();
517 }
518
519 (calls, first_pos)
520 }
521
522 fn parse_complete_inner(text: &str, tools: &[Tool]) -> (String, Vec<ToolCall>) {
524 if !text.contains(TOOL_CALL_START) {
525 return (text.to_string(), vec![]);
526 }
527 let (calls, first_pos) = Self::parse_tool_calls(text, tools);
528 if calls.is_empty() {
529 return (text.to_string(), vec![]);
530 }
531 let normal_text = match first_pos {
532 Some(pos) => text[..pos].to_string(),
533 None => text.to_string(),
534 };
535 (normal_text, calls)
536 }
537}
538
539impl Default for MinimaxM3Parser {
540 fn default() -> Self {
541 Self::new()
542 }
543}
544
545#[async_trait]
546impl ToolParser for MinimaxM3Parser {
547 async fn parse_complete(&self, text: &str) -> ParserResult<(String, Vec<ToolCall>)> {
548 Ok(Self::parse_complete_inner(text, &[]))
549 }
550
551 async fn parse_complete_with_tools(
552 &self,
553 text: &str,
554 tools: &[Tool],
555 ) -> ParserResult<(String, Vec<ToolCall>)> {
556 Ok(Self::parse_complete_inner(text, tools))
557 }
558
559 async fn parse_incremental(
560 &mut self,
561 chunk: &str,
562 tools: &[Tool],
563 ) -> ParserResult<StreamingParseResult> {
564 self.buffer.push_str(chunk);
565 let mut normal_text = String::new();
566 let mut calls = Vec::new();
567
568 loop {
569 if !self.in_tool_call {
571 if let Some(start) = self.buffer.find(TOOL_CALL_START) {
572 normal_text.push_str(&self.buffer[..start]);
573 self.buffer = self.buffer[start..].to_string();
574 self.in_tool_call = true;
575 continue;
576 }
577
578 if let Some(partial_len) =
580 Self::longest_partial_suffix(&self.buffer, TOOL_CALL_START)
581 {
582 let end = self.buffer.len() - partial_len;
583 normal_text.push_str(&self.buffer[..end]);
584 self.buffer = self.buffer[end..].to_string();
585 } else {
586 normal_text.push_str(&self.buffer);
587 self.buffer.clear();
588 }
589 break;
590 }
591
592 let Some(end_rel) = self.buffer.find(TOOL_CALL_END) else {
594 if !Self::could_start_invoke(&self.buffer) {
595 normal_text.push_str(TOOL_CALL_START);
599 self.buffer.drain(..TOOL_CALL_START.len());
600 self.in_tool_call = false;
601 continue;
602 }
603 break;
604 };
605 let block_end = end_rel + TOOL_CALL_END.len();
606 let block = self.buffer[..block_end].to_string();
607 self.buffer = self.buffer[block_end..].to_string();
608 self.in_tool_call = false;
609
610 let (block_calls, _) = Self::parse_tool_calls(&block, tools);
611 if block_calls.is_empty() {
612 normal_text.push_str(&block);
615 continue;
616 }
617 for call in block_calls {
618 if self.current_tool_id == -1 {
619 self.current_tool_id = 0;
620 } else {
621 self.current_tool_id += 1;
622 }
623 let tool_id = self.current_tool_id as usize;
624 helpers::ensure_capacity(
625 self.current_tool_id,
626 &mut self.prev_tool_call_arr,
627 &mut self.streamed_args_for_tool,
628 );
629
630 let args = call.function.arguments.clone();
631 if tool_id < self.streamed_args_for_tool.len() {
632 self.streamed_args_for_tool[tool_id].clone_from(&args);
633 }
634 let parsed_args: Value =
635 serde_json::from_str(&args).unwrap_or_else(|_| Value::Object(Map::new()));
636 if tool_id < self.prev_tool_call_arr.len() {
637 self.prev_tool_call_arr[tool_id] = serde_json::json!({
638 "name": call.function.name,
639 "arguments": parsed_args,
640 });
641 }
642
643 calls.push(ToolCallItem {
645 tool_index: tool_id,
646 name: Some(call.function.name),
647 parameters: String::new(),
648 });
649 calls.push(ToolCallItem {
650 tool_index: tool_id,
651 name: None,
652 parameters: args,
653 });
654 }
655 }
656
657 Ok(StreamingParseResult { normal_text, calls })
658 }
659
660 fn has_tool_markers(&self, text: &str) -> bool {
661 text.contains(TOOL_CALL_START)
662 }
663
664 fn get_unstreamed_tool_args(&self) -> Option<Vec<ToolCallItem>> {
665 helpers::get_unstreamed_args(&self.prev_tool_call_arr, &self.streamed_args_for_tool)
666 }
667
668 fn take_unstreamed_normal_text(&mut self) -> String {
669 self.in_tool_call = false;
673 std::mem::take(&mut self.buffer)
674 }
675
676 fn reset(&mut self) {
677 self.buffer.clear();
678 self.prev_tool_call_arr.clear();
679 self.current_tool_id = -1;
680 self.streamed_args_for_tool.clear();
681 self.in_tool_call = false;
682 }
683}