use std::collections::{HashMap, HashSet};
use async_trait::async_trait;
use openai_protocol::common::Tool;
use serde_json::{json, Map, Value};
use crate::{
errors::ParserResult,
factory::GrammarStart,
parsers::helpers,
traits::ToolParser,
types::{FunctionCall, StreamingParseResult, ToolCall, ToolCallItem},
};
const NAMESPACE: &str = "]<]minimax[>[";
const TOOL_CALL_START: &str = "]<]minimax[>[<tool_call>";
const TOOL_CALL_END: &str = "]<]minimax[>[</tool_call>";
const INVOKE_START: &str = "]<]minimax[>[<invoke";
const INVOKE_END: &str = "]<]minimax[>[</invoke>";
const ELEMENT_START: &str = "]<]minimax[>[<";
const ELEMENT_END_START: &str = "]<]minimax[>[</";
const MIXED_TEXT_FIELD: &str = "$text";
const THINK_START: &str = "<mm:think>";
const THINK_END: &str = "</mm:think>";
const ITEM: &str = "item";
const CALL_EXITS: [&str; 3] = ["</invoke>", "<invoke name=\"", "</tool_call>"];
const DEPTH: usize = 16;
const NODES: usize = 1024;
const LEAF_BYTES: usize = 256 * 1024;
const WALK_BYTES: usize = 4 * LEAF_BYTES;
const WORD_FRAMING: usize = 36;
pub struct MinimaxM3Parser {
buffer: String,
prev_tool_call_arr: Vec<Value>,
current_tool_id: i32,
streamed_args_for_tool: Vec<String>,
in_tool_call: bool,
}
enum ParamValue {
Text(String),
Elements(Vec<(String, ParamValue)>),
}
impl MinimaxM3Parser {
pub fn new() -> Self {
Self {
buffer: String::new(),
prev_tool_call_arr: Vec::new(),
current_tool_id: -1,
streamed_args_for_tool: Vec::new(),
in_tool_call: false,
}
}
fn coerce_leaf(text: &str, declared_type: Option<&str>) -> Value {
if text.trim().is_empty() {
match declared_type {
Some("array") => return Value::Array(Vec::new()),
Some("object") => return Value::Object(Map::new()),
_ => {}
}
}
if let Some(value) = helpers::coerce_by_schema_type(text, declared_type) {
return value;
}
Self::infer_value(text)
}
fn infer_value(text: &str) -> Value {
match text {
"true" | "True" => return Value::Bool(true),
"false" | "False" => return Value::Bool(false),
"null" | "None" => return Value::Null,
_ => {}
}
if let Ok(num) = text.parse::<i64>() {
return Value::Number(num.into());
}
if let Ok(num) = text.parse::<f64>() {
if let Some(n) = serde_json::Number::from_f64(num) {
return Value::Number(n);
}
}
Value::String(text.to_string())
}
fn longest_partial_suffix(buffer: &str, token: &str) -> Option<usize> {
if buffer.is_empty() || token.is_empty() {
return None;
}
token
.char_indices()
.skip(1)
.map(|(i, _)| i)
.filter(|&i| buffer.ends_with(&token[..i]))
.max()
}
fn could_start_invoke(buffer: &str) -> bool {
let Some(after_wrapper) = buffer.strip_prefix(TOOL_CALL_START) else {
return false;
};
let candidate = after_wrapper.trim_start();
if candidate.is_empty() || INVOKE_START.starts_with(candidate) {
return true;
}
let Some(after_invoke) = candidate.strip_prefix(INVOKE_START) else {
return false;
};
after_invoke.is_empty()
|| after_invoke
.chars()
.next()
.is_some_and(|c| c.is_whitespace() || c == '>')
}
fn decode_xml_entities(text: &str) -> String {
text.replace("<", "<")
.replace(">", ">")
.replace(""", "\"")
.replace("'", "'")
.replace("&", "&")
}
fn parse_invoke_name(header: &str) -> Option<String> {
let idx = header.find("name")?;
let after = &header[idx + "name".len()..];
let after = after.trim_start();
let after = after.strip_prefix('=')?.trim_start();
if let Some(rest) = after.strip_prefix('"') {
let end = rest.find('"')?;
Some(rest[..end].trim().to_string())
} else if let Some(rest) = after.strip_prefix('\'') {
let end = rest.find('\'')?;
Some(rest[..end].trim().to_string())
} else {
let end = after
.find(|c: char| c.is_whitespace())
.unwrap_or(after.len());
let name = after[..end].trim();
if name.is_empty() {
None
} else {
Some(name.to_string())
}
}
}
fn parse_element_body(
input: &str,
name: &str,
schema: Option<&Value>,
parent_name: Option<&str>,
) -> Option<(ParamValue, usize)> {
let close_tag = format!("{ELEMENT_END_START}{name}>");
let mut pos = 0;
let mut text = String::new();
let mut children: Vec<(String, ParamValue)> = Vec::new();
loop {
let rest = &input[pos..];
let text_chunk = &rest[..rest.find(NAMESPACE)?];
text.push_str(text_chunk);
pos += text_chunk.len();
let rest = &input[pos..];
if rest.starts_with(&close_tag) {
pos += close_tag.len();
break;
}
if text.trim().is_empty()
&& children.is_empty()
&& matches!(Self::schema_type(schema), Some("array" | "object"))
&& parent_name.is_some_and(|parent| {
rest.starts_with(&format!("{ELEMENT_END_START}{parent}>"))
})
{
break;
}
if rest.starts_with(ELEMENT_START) {
let (child_name, child_value, consumed) =
Self::parse_element(rest, schema, Some(name))?;
children.push((child_name, child_value));
pos += consumed;
continue;
}
return None;
}
let value = if children.is_empty() {
ParamValue::Text(text)
} else {
if !text.trim().is_empty() {
Self::push_mixed_text(&mut children, text);
}
ParamValue::Elements(children)
};
Some((value, pos))
}
fn parse_element(
input: &str,
parent_schema: Option<&Value>,
parent_name: Option<&str>,
) -> Option<(String, ParamValue, usize)> {
let after_start = input.strip_prefix(ELEMENT_START)?;
let gt = after_start.find('>')?;
let name = after_start[..gt].trim().to_string();
if name.is_empty() || name.starts_with('/') {
return None;
}
let body_start = ELEMENT_START.len() + gt + 1;
let parent_schema = Self::container_schema(parent_schema).or(parent_schema);
let schema = if Self::schema_type(parent_schema) == Some("array") {
parent_schema.and_then(|schema| schema.get("items"))
} else {
Self::property_schema(parent_schema, &name)
};
let schema = Self::container_schema(schema).or(schema);
let (value, body_consumed) =
Self::parse_element_body(&input[body_start..], &name, schema, parent_name)?;
Some((name, value, body_start + body_consumed))
}
fn push_mixed_text(children: &mut Vec<(String, ParamValue)>, text: String) {
let mut field = MIXED_TEXT_FIELD.to_string();
while children.iter().any(|(name, _)| *name == field) {
field.insert(0, '$');
}
children.push((field, ParamValue::Text(text)));
}
fn schema_type(schema: Option<&Value>) -> Option<&str> {
schema?.get("type")?.as_str()
}
fn container_schema(schema: Option<&Value>) -> Option<&Value> {
let schema = schema?;
if schema.get("type").is_some() {
return matches!(Self::schema_type(Some(schema)), Some("array" | "object"))
.then_some(schema);
}
let mut compositions = ["oneOf", "anyOf", "allOf"]
.into_iter()
.filter_map(|key| schema.get(key).map(|branches| (key, branches)));
let (keyword, branches) = compositions.next()?;
if compositions.next().is_some() {
return None;
}
if schema.as_object()?.keys().any(|key| {
key != keyword
&& !matches!(
key.as_str(),
"title"
| "description"
| "default"
| "examples"
| "deprecated"
| "readOnly"
| "writeOnly"
| "$comment"
)
}) {
return None;
}
let branches = branches.as_array()?;
if keyword == "allOf" && branches.len() != 1 {
return None;
}
let mut container = None;
for branch in branches {
if keyword != "allOf" && Self::schema_type(Some(branch)) == Some("null") {
continue;
}
let resolved = Self::container_schema(Some(branch))?;
if container.replace(resolved).is_some() {
return None;
}
}
container
}
fn property_schema<'a>(schema: Option<&'a Value>, name: &str) -> Option<&'a Value> {
let schema = schema?;
if let Some(property) = schema.get("properties").and_then(|value| value.get(name)) {
return Some(property);
}
for keyword in ["oneOf", "anyOf", "allOf"] {
for branch in schema
.get(keyword)
.and_then(Value::as_array)
.into_iter()
.flatten()
{
if let Some(property) = Self::property_schema(Some(branch), name) {
return Some(property);
}
}
}
None
}
fn value_to_json(value: ParamValue, schema: Option<&Value>) -> Value {
let schema = Self::container_schema(schema).or(schema);
match value {
ParamValue::Text(text) => {
let decoded = Self::decode_xml_entities(&text);
Self::coerce_leaf(&decoded, Self::schema_type(schema))
}
ParamValue::Elements(children) => {
if Self::schema_type(schema) == Some("array") {
let items_schema = schema.and_then(|s| s.get("items"));
let items = children
.into_iter()
.map(|(_, v)| Self::value_to_json(v, items_schema))
.collect();
return Value::Array(items);
}
let mut map: Map<String, Value> = Map::new();
for (name, child) in children {
let child_schema = Self::property_schema(schema, &name);
let child_json = Self::value_to_json(child, child_schema);
match map.get_mut(&name) {
Some(Value::Array(arr)) => arr.push(child_json),
Some(existing) => {
let prev = existing.take();
*existing = Value::Array(vec![prev, child_json]);
}
None => {
map.insert(name, child_json);
}
}
}
Value::Object(map)
}
}
}
fn parse_invoke_params(body: &str, params_schema: Option<&Value>) -> Option<Value> {
let mut map: Map<String, Value> = Map::new();
let mut pos = 0;
loop {
let rest = &body[pos..];
let trimmed = rest.trim_start();
let trim_len = rest.len() - trimmed.len();
if trimmed.is_empty() {
break;
}
if !trimmed.starts_with(ELEMENT_START) {
break;
}
pos += trim_len;
let (name, value, consumed) = Self::parse_element(&body[pos..], params_schema, None)?;
pos += consumed;
let json = Self::value_to_json(value, Self::property_schema(params_schema, &name));
match map.get_mut(&name) {
Some(Value::Array(arr)) => arr.push(json),
Some(existing) => {
let prev = existing.take();
*existing = Value::Array(vec![prev, json]);
}
None => {
map.insert(name, json);
}
}
}
Some(Value::Object(map))
}
fn parse_invoke(block: &str, tools: &[Tool]) -> Option<ToolCall> {
let after_invoke = block.strip_prefix(INVOKE_START)?;
let gt = after_invoke.find('>')?;
let header = &after_invoke[..gt];
let name = Self::parse_invoke_name(header)?;
let body = &after_invoke[gt + 1..];
let params_schema = tools
.iter()
.find(|t| t.function.name == name)
.map(|t| &t.function.parameters);
let arguments = Self::parse_invoke_params(body, params_schema)?;
let arguments_str = serde_json::to_string(&arguments).ok()?;
Some(ToolCall {
function: FunctionCall {
name,
arguments: arguments_str,
},
})
}
fn parse_tool_calls(text: &str, tools: &[Tool]) -> (Vec<ToolCall>, Option<usize>) {
let mut calls = Vec::new();
let mut first_pos = None;
let mut search_from = 0;
while let Some(rel_start) = text[search_from..].find(TOOL_CALL_START) {
let block_start = search_from + rel_start;
let inner_start = block_start + TOOL_CALL_START.len();
let Some(rel_end) = text[inner_start..].find(TOOL_CALL_END) else {
break;
};
let inner_end = inner_start + rel_end;
let inner = &text[inner_start..inner_end];
let mut invoke_from = 0;
while let Some(rel_inv) = inner[invoke_from..].find(INVOKE_START) {
let inv_start = invoke_from + rel_inv;
let Some(rel_inv_end) = inner[inv_start..].find(INVOKE_END) else {
break;
};
let inv_end = inv_start + rel_inv_end;
let inv_block = &inner[inv_start..inv_end];
if let Some(call) = Self::parse_invoke(inv_block, tools) {
if first_pos.is_none() {
first_pos = Some(block_start);
}
calls.push(call);
}
invoke_from = inv_end + INVOKE_END.len();
}
search_from = inner_end + TOOL_CALL_END.len();
}
(calls, first_pos)
}
fn parse_complete_inner(text: &str, tools: &[Tool]) -> (String, Vec<ToolCall>) {
if !text.contains(TOOL_CALL_START) {
return (text.to_string(), vec![]);
}
let (calls, first_pos) = Self::parse_tool_calls(text, tools);
if calls.is_empty() {
return (text.to_string(), vec![]);
}
let normal_text = match first_pos {
Some(pos) => text[..pos].to_string(),
None => text.to_string(),
};
(normal_text, calls)
}
pub fn build_structural_tag(tools: &[Tool], at_least_one: bool) -> Value {
let invokes: Vec<Value> = tools
.iter()
.filter(|tool| !tool.function.name.is_empty())
.map(|tool| Elements::of(&tool.function.parameters).invoke(&tool.function.name))
.collect();
let invokes = if invokes.is_empty() {
json!({ "type": "const_string", "value": "" })
} else {
json!({
"type": "tags_with_separator",
"tags": invokes,
"separator": "",
"at_least_one": true,
})
};
json!({
"format": {
"type": "sequence",
"elements": [
Self::before_the_block(),
{
"type": "triggered_tags",
"triggers": [TOOL_CALL_START],
"tags": [{
"type": "tag",
"begin": format!("{TOOL_CALL_START}\n"),
"content": invokes,
"end": TOOL_CALL_END,
}],
"at_least_one": at_least_one,
},
],
}
})
}
fn before_the_block() -> Value {
json!({
"type": "optional",
"content": {
"type": "or",
"elements": [
{
"type": "tag",
"begin": THINK_START,
"content": {
"type": "any_text",
"excludes": [THINK_START, THINK_END, NAMESPACE],
},
"end": THINK_END,
},
{ "type": "const_string", "value": THINK_END },
],
},
})
}
pub fn reasoning_prefix(start: GrammarStart) -> Option<Value> {
match start {
GrammarStart::AfterReasoning => None,
GrammarStart::Unknown => Some(json!({
"type": "any_text",
"excludes": [NAMESPACE],
})),
GrammarStart::FirstToken => Some(json!({
"type": "tag",
"begin": "",
"content": {
"type": "any_text",
"excludes": [THINK_START, THINK_END, NAMESPACE],
},
"end": THINK_END,
})),
}
}
}
struct Elements<'a> {
root: &'a Value,
path: Vec<&'a str>,
nodes: usize,
reserved: usize,
leaf_bytes: usize,
walk_bytes: usize,
entry_bytes: HashMap<usize, usize>,
reached_by_leaf: HashMap<usize, Option<(usize, Vec<Reached<'a>>)>>,
enum_bytes: HashMap<usize, usize>,
}
type Reached<'a> = (&'static str, &'a str, &'a Value);
enum Writes<'a> {
FreeText,
Typed {
schema: &'a Value,
pushed: usize,
depth: usize,
},
}
enum Element {
Nothing,
Spent,
Written(Value),
}
impl<'a> Elements<'a> {
fn of(root: &'a Value) -> Self {
Self {
root,
path: Vec::new(),
nodes: NODES,
reserved: 0,
leaf_bytes: LEAF_BYTES,
walk_bytes: WALK_BYTES,
entry_bytes: HashMap::new(),
reached_by_leaf: HashMap::new(),
enum_bytes: HashMap::new(),
}
}
fn takes_bytes(&mut self, cost: usize) -> bool {
if cost > self.leaf_bytes {
return false;
}
self.leaf_bytes -= cost;
true
}
fn has_an_element(&self) -> bool {
self.nodes > self.reserved
}
fn take_an_element(&mut self) {
self.nodes = self.nodes.saturating_sub(1);
}
fn how(&mut self, schema: &'a Value, depth: usize) -> Option<Writes<'a>> {
let Some(depth) = depth.checked_sub(1) else {
return Some(Writes::FreeText);
};
let Some((schema, pushed)) = self.follow(schema) else {
return Some(Writes::FreeText);
};
if MinimaxM3Parser::schema_type(Some(schema)) == Some("null") {
self.unwind(pushed);
return None;
}
Some(Writes::Typed {
schema,
pushed,
depth,
})
}
fn writes_an_element(&mut self, schema: &'a Value, depth: usize) -> bool {
match self.how(schema, depth) {
None => false,
Some(writes) => {
self.leaves(writes);
true
}
}
}
fn leaves(&mut self, writes: Writes<'a>) {
if let Writes::Typed { pushed, .. } = writes {
self.unwind(pushed);
}
}
fn invoke(mut self, name: &str) -> Value {
let content = match self.follow(self.root) {
Some((schema, pushed)) => {
let content = self.members(schema, "</invoke>", DEPTH);
self.unwind(pushed);
content
}
None => Self::free_text("</invoke>"),
};
json!({
"type": "tag",
"begin": format!("{INVOKE_START} name=\"{name}\">"),
"content": content,
"end": format!("{INVOKE_END}\n"),
})
}
fn definition(&self, pointer: &str) -> Option<&'a Value> {
let mut segments = pointer.strip_prefix("#/")?.split('/');
let block = match segments.next()? {
block @ ("$defs" | "definitions") => block,
_ => return None,
};
let mut node = self.root.get(block)?;
for segment in segments {
node = node.get(segment.replace("~1", "/").replace("~0", "~"))?;
}
Some(node)
}
fn follow(&mut self, schema: &'a Value) -> Option<(&'a Value, usize)> {
let mut schema = schema;
let mut pushed = 0;
while let Some(pointer) = schema.get("$ref").and_then(Value::as_str) {
let Some(target) = self.definition(pointer) else {
break;
};
if self.path.contains(&pointer) || pushed == DEPTH {
self.unwind(pushed);
return None;
}
self.path.push(pointer);
pushed += 1;
schema = target;
}
Some((schema, pushed))
}
fn unwind(&mut self, pushed: usize) {
let keep = self.path.len().saturating_sub(pushed);
self.path.truncate(keep);
}
fn members(&mut self, schema: &'a Value, close: &str, depth: usize) -> Value {
let properties = schema
.get("properties")
.and_then(Value::as_object)
.filter(|properties| !properties.is_empty());
let Some(properties) = properties else {
return Self::free_text(close);
};
let required: Vec<&str> = schema
.get("required")
.and_then(Value::as_array)
.map(|names| names.iter().filter_map(Value::as_str).collect())
.unwrap_or_default();
let mut later: usize = 0;
for schema in properties.values() {
if self.writes_an_element(schema, depth) {
later += 1;
}
}
let mut elements: Vec<Value> = Vec::new();
for (key, schema) in properties {
match self.element(key, schema, depth, later.saturating_sub(1)) {
Element::Nothing => {}
Element::Spent => {
elements.push(Self::free_text(close));
break;
}
Element::Written(element) => {
later = later.saturating_sub(1);
elements.push(if required.contains(&key.as_str()) {
element
} else {
json!({ "type": "optional", "content": element })
});
}
}
}
if elements.is_empty() {
return json!({ "type": "const_string", "value": "" });
}
json!({ "type": "sequence", "elements": elements })
}
fn element(&mut self, key: &str, schema: &'a Value, depth: usize, later: usize) -> Element {
let Some(writes) = self.how(schema, depth) else {
return Element::Nothing;
};
if !self.has_an_element() {
self.leaves(writes);
return Element::Spent;
}
self.take_an_element();
self.reserved += later;
let value = self.value(&format!("</{key}>"), writes);
self.reserved -= later;
Element::Written(json!({
"type": "sequence",
"elements": [
{ "type": "const_string", "value": format!("{ELEMENT_START}{key}>") },
value,
{ "type": "const_string", "value": format!("{ELEMENT_END_START}{key}>") },
],
}))
}
fn value(&mut self, close: &str, writes: Writes<'a>) -> Value {
let Writes::Typed {
schema,
pushed,
depth,
} = writes
else {
return Self::free_text(close);
};
let value = match MinimaxM3Parser::schema_type(Some(schema)) {
Some("boolean") => Self::one_of(["true", "false", "True", "False"]),
Some("object") => self.members(schema, close, depth),
Some("array") => match schema.get("items") {
Some(items) if self.has_an_element() => {
self.take_an_element();
self.items(items, depth)
}
_ => Self::free_text(close),
},
Some("string") => match schema.get("enum").and_then(Value::as_array) {
Some(values) if !values.is_empty() && values.iter().all(Value::is_string) => {
let key = std::ptr::from_ref(values) as usize;
let cost = *self.enum_bytes.entry(key).or_insert_with(|| {
values
.iter()
.filter_map(Value::as_str)
.map(|word| word.len() + WORD_FRAMING)
.sum()
});
if self.takes_bytes(cost) {
Self::one_of(values.iter().filter_map(Value::as_str))
} else {
Self::text(close)
}
}
_ => Self::text(close),
},
Some("integer" | "number") => self.fragment(schema, close),
_ => Self::free_text(close),
};
self.unwind(pushed);
value
}
fn items(&mut self, items: &'a Value, depth: usize) -> Value {
let open = format!("{ELEMENT_START}{ITEM}>");
let close = format!("{ELEMENT_END_START}{ITEM}>");
let item = match self.how(items, depth) {
Some(writes) => {
let value = self.value(&format!("</{ITEM}>"), writes);
json!({
"type": "sequence",
"elements": [
{ "type": "const_string", "value": open },
value,
{ "type": "const_string", "value": close },
],
})
}
None => json!({ "type": "const_string", "value": format!("{open}{close}") }),
};
json!({ "type": "star", "content": item })
}
fn fragment(&mut self, schema: &'a Value, close: &str) -> Value {
let Some((cost, reached)) = self.reached_within(schema) else {
return Self::free_text(close);
};
if !self.takes_bytes(cost) {
return Self::free_text(close);
}
let mut fragment = schema.clone();
if let Some(object) = fragment.as_object_mut() {
for (block, name, entry) in reached {
let slot = object.entry(block.to_string()).or_insert_with(|| json!({}));
if let Some(entries) = slot.as_object_mut() {
entries
.entry(name.to_string())
.or_insert_with(|| entry.clone());
}
}
}
json!({ "type": "json_schema", "json_schema": fragment })
}
fn reached_within(&mut self, schema: &'a Value) -> Option<(usize, Vec<Reached<'a>>)> {
let key = std::ptr::from_ref(schema) as usize;
if let Some(known) = self.reached_by_leaf.get(&key) {
return match known {
Some((cost, reached)) if *cost <= self.leaf_bytes => Some((*cost, reached.clone())),
_ => None,
};
}
let walks = !local_pointers(schema).is_empty();
let limit = if walks {
self.leaf_bytes.min(self.walk_bytes)
} else {
self.leaf_bytes
};
let found = self.walk_reached(schema, limit);
if walks {
self.walk_bytes = self
.walk_bytes
.saturating_sub(found.as_ref().map_or(limit, |(cost, _)| *cost));
}
self.reached_by_leaf.insert(key, found.clone());
found
}
fn walk_reached(
&mut self,
schema: &'a Value,
limit: usize,
) -> Option<(usize, Vec<Reached<'a>>)> {
let mut cost = json_bytes(schema);
if cost > limit {
return None;
}
let mut reached: Vec<Reached<'a>> = Vec::new();
let mut seen: HashSet<(&'static str, &'a str)> = HashSet::new();
let mut pending: Vec<&'a Value> = vec![schema];
while let Some(node) = pending.pop() {
for pointer in local_pointers(node) {
let Some((block, name, entry)) = self.entry_of(pointer) else {
continue;
};
if !seen.insert((block, name)) {
continue;
}
cost += self.entry_bytes(entry);
if cost > limit {
return None;
}
reached.push((block, name, entry));
pending.push(entry);
}
}
Some((cost, reached))
}
fn entry_of(&self, pointer: &str) -> Option<(&'static str, &'a str, &'a Value)> {
let mut segments = pointer.strip_prefix("#/")?.split('/');
let block = match segments.next()? {
"$defs" => "$defs",
"definitions" => "definitions",
_ => return None,
};
let entries = self.root.get(block)?.as_object()?;
let name = segments.next()?.replace("~1", "/").replace("~0", "~");
let (key, entry) = entries.get_key_value(name.as_str())?;
Some((block, key.as_str(), entry))
}
fn entry_bytes(&mut self, entry: &'a Value) -> usize {
let key = std::ptr::from_ref(entry) as usize;
if let Some(bytes) = self.entry_bytes.get(&key) {
return *bytes;
}
let bytes = json_bytes(entry);
self.entry_bytes.insert(key, bytes);
bytes
}
fn one_of(words: impl IntoIterator<Item = &'a str>) -> Value {
let words: Vec<Value> = words
.into_iter()
.map(|word| json!({ "type": "const_string", "value": word }))
.collect();
json!({ "type": "or", "elements": words })
}
fn text(close: &str) -> Value {
Self::excluding(&[NAMESPACE, close])
}
fn free_text(close: &str) -> Value {
Self::excluding(&[close])
}
fn excluding(first: &[&str]) -> Value {
let mut excludes: Vec<&str> = Vec::new();
for text in first.iter().chain(CALL_EXITS.iter()) {
if !excludes.contains(text) {
excludes.push(text);
}
}
json!({ "type": "any_text", "excludes": excludes })
}
}
fn local_pointers(schema: &Value) -> Vec<&str> {
fn collect<'v>(schema: &'v Value, pointers: &mut Vec<&'v str>) {
match schema {
Value::Object(object) => {
for (key, value) in object {
if matches!(key.as_str(), "$ref" | "$dynamicRef" | "$recursiveRef") {
if let Some(pointer) =
value.as_str().filter(|pointer| pointer.starts_with('#'))
{
pointers.push(pointer);
}
}
collect(value, pointers);
}
}
Value::Array(items) => items.iter().for_each(|item| collect(item, pointers)),
_ => {}
}
}
let mut pointers = Vec::new();
collect(schema, &mut pointers);
pointers
}
fn json_bytes(value: &Value) -> usize {
match value {
Value::Null => 4,
Value::Bool(flag) => {
if *flag {
4
} else {
5
}
}
Value::Number(number) => number.to_string().len(),
Value::String(text) => {
text.len()
+ 2
+ text
.bytes()
.filter(|byte| matches!(byte, b'"' | b'\\') || *byte < 0x20)
.count()
}
Value::Array(items) => {
2 + items.iter().map(json_bytes).sum::<usize>() + items.len().saturating_sub(1)
}
Value::Object(object) => {
2 + object
.iter()
.map(|(key, value)| key.len() + 3 + json_bytes(value))
.sum::<usize>()
+ object.len().saturating_sub(1)
}
}
}
impl Default for MinimaxM3Parser {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl ToolParser for MinimaxM3Parser {
async fn parse_complete(&self, text: &str) -> ParserResult<(String, Vec<ToolCall>)> {
Ok(Self::parse_complete_inner(text, &[]))
}
async fn parse_complete_with_tools(
&self,
text: &str,
tools: &[Tool],
) -> ParserResult<(String, Vec<ToolCall>)> {
Ok(Self::parse_complete_inner(text, tools))
}
async fn parse_incremental(
&mut self,
chunk: &str,
tools: &[Tool],
) -> ParserResult<StreamingParseResult> {
self.buffer.push_str(chunk);
let mut normal_text = String::new();
let mut calls = Vec::new();
loop {
if !self.in_tool_call {
if let Some(start) = self.buffer.find(TOOL_CALL_START) {
normal_text.push_str(&self.buffer[..start]);
self.buffer = self.buffer[start..].to_string();
self.in_tool_call = true;
continue;
}
if let Some(partial_len) =
Self::longest_partial_suffix(&self.buffer, TOOL_CALL_START)
{
let end = self.buffer.len() - partial_len;
normal_text.push_str(&self.buffer[..end]);
self.buffer = self.buffer[end..].to_string();
} else {
normal_text.push_str(&self.buffer);
self.buffer.clear();
}
break;
}
let Some(end_rel) = self.buffer.find(TOOL_CALL_END) else {
if !Self::could_start_invoke(&self.buffer) {
normal_text.push_str(TOOL_CALL_START);
self.buffer.drain(..TOOL_CALL_START.len());
self.in_tool_call = false;
continue;
}
break;
};
let block_end = end_rel + TOOL_CALL_END.len();
let block = self.buffer[..block_end].to_string();
self.buffer = self.buffer[block_end..].to_string();
self.in_tool_call = false;
let (block_calls, _) = Self::parse_tool_calls(&block, tools);
if block_calls.is_empty() {
normal_text.push_str(&block);
continue;
}
for call in block_calls {
if self.current_tool_id == -1 {
self.current_tool_id = 0;
} else {
self.current_tool_id += 1;
}
let tool_id = self.current_tool_id as usize;
helpers::ensure_capacity(
self.current_tool_id,
&mut self.prev_tool_call_arr,
&mut self.streamed_args_for_tool,
);
let args = call.function.arguments.clone();
if tool_id < self.streamed_args_for_tool.len() {
self.streamed_args_for_tool[tool_id].clone_from(&args);
}
let parsed_args: Value =
serde_json::from_str(&args).unwrap_or_else(|_| Value::Object(Map::new()));
if tool_id < self.prev_tool_call_arr.len() {
self.prev_tool_call_arr[tool_id] = serde_json::json!({
"name": call.function.name,
"arguments": parsed_args,
});
}
calls.push(ToolCallItem {
tool_index: tool_id,
name: Some(call.function.name),
parameters: String::new(),
});
calls.push(ToolCallItem {
tool_index: tool_id,
name: None,
parameters: args,
});
}
}
Ok(StreamingParseResult { normal_text, calls })
}
fn has_tool_markers(&self, text: &str) -> bool {
text.contains(TOOL_CALL_START)
}
fn get_unstreamed_tool_args(&self) -> Option<Vec<ToolCallItem>> {
helpers::get_unstreamed_args(&self.prev_tool_call_arr, &self.streamed_args_for_tool)
}
fn take_unstreamed_normal_text(&mut self) -> String {
self.in_tool_call = false;
std::mem::take(&mut self.buffer)
}
fn reset(&mut self) {
self.buffer.clear();
self.prev_tool_call_arr.clear();
self.current_tool_id = -1;
self.streamed_args_for_tool.clear();
self.in_tool_call = false;
}
}