tool_parser/parsers/minimax_m3.rs
1use std::collections::{HashMap, HashSet};
2
3use async_trait::async_trait;
4use openai_protocol::common::Tool;
5use serde_json::{json, Map, Value};
6
7use crate::{
8 errors::ParserResult,
9 factory::GrammarStart,
10 parsers::helpers,
11 traits::ToolParser,
12 types::{FunctionCall, StreamingParseResult, ToolCall, ToolCallItem},
13};
14
15/// Namespace marker MiniMax M3 prepends before each structural tag.
16const NAMESPACE: &str = "]<]minimax[>[";
17/// Opening marker for a tool-call block.
18const TOOL_CALL_START: &str = "]<]minimax[>[<tool_call>";
19/// Closing marker for a tool-call block.
20const TOOL_CALL_END: &str = "]<]minimax[>[</tool_call>";
21/// Opening marker for an `<invoke ...>` element (attributes follow before `>`).
22const INVOKE_START: &str = "]<]minimax[>[<invoke";
23/// Closing marker for an `<invoke>` element.
24const INVOKE_END: &str = "]<]minimax[>[</invoke>";
25/// Opening marker prefix for a parameter element (`<name>`).
26const ELEMENT_START: &str = "]<]minimax[>[<";
27/// Opening marker prefix for a parameter closing tag (`</name>`).
28const ELEMENT_END_START: &str = "]<]minimax[>[</";
29/// Reserved field name used to preserve mixed text within an object element.
30const MIXED_TEXT_FIELD: &str = "$text";
31/// Opening marker of the model's thought.
32const THINK_START: &str = "<mm:think>";
33/// Closing marker of the model's thought.
34const THINK_END: &str = "</mm:think>";
35/// The element tag of a list's items, as the model writes an array.
36const ITEM: &str = "item";
37/// The markers that end a call wherever they stand; no argument value may hold one.
38const CALL_EXITS: [&str; 3] = ["</invoke>", "<invoke name=\"", "</tool_call>"];
39/// How deep the structural tag follows a schema's nesting; below that, a value is any text.
40const DEPTH: usize = 16;
41/// How many elements one invoke's grammar may hold; past that, the values still to write are
42/// free text. An acyclic schema unfolds as k^d elements for k properties over d levels, which the
43/// depth cap alone does not bound; ordinary tool schemas stay far below this. The budget is each
44/// invoke's own: a tag for many tools holds up to this many elements per tool. The budget is spent
45/// depth first, in the schema's order, but an object's later properties each keep one element
46/// while an earlier one is descended into, so what runs short loses its structure from the inside
47/// out: the nested objects first, the top-level arguments last. The budget counts elements, not
48/// bytes: a value's own spelling (an enum's words, a number's schema) is counted by
49/// [`LEAF_BYTES`], and a property typed `null`, written as nothing, costs none.
50const NODES: usize = 1024;
51/// How many bytes the leaves of one invoke may spell: an enum's words, a number's schema with the
52/// definitions its pointers reach. Past that, a string with an enum is text as it is and a number
53/// is free text, so a value that is large, or reused through references, cannot multiply the
54/// grammar: a 5,000-word enum reached by a thousand number leaves is written while it fits, not a
55/// thousand times. The budget is each invoke's own, as [`NODES`] is, and is spent in the schema's
56/// order.
57const LEAF_BYTES: usize = 256 * 1024;
58/// How many bytes the walks behind one invoke's number leaves may size, the refused walks counted
59/// too. A walk stops at what the leaves may still spell, but a leaf that is refused spends none of
60/// that, so a thousand distinct leaves at a definition just past the budget would each walk it to
61/// the end; past this allowance a number leaf with pointers is free text without a walk. A leaf
62/// without pointers walks nothing but itself, which the request already holds, and the allowance
63/// neither bounds nor charges it.
64const WALK_BYTES: usize = 4 * LEAF_BYTES;
65/// What an enum's word costs beyond its length: the alternative that spells it.
66const WORD_FRAMING: usize = 36;
67
68/// MiniMax M3 format parser for tool calls.
69///
70/// Handles the MiniMax M3 specific framing, where the namespace marker
71/// `]<]minimax[>[` is prepended before every structural tag:
72///
73/// ```text
74/// ]<]minimax[>[<tool_call>
75/// ]<]minimax[>[<invoke name="func">
76/// ]<]minimax[>[<key>value]<]minimax[>[</key>
77/// ]<]minimax[>[</invoke>
78/// ]<]minimax[>[</tool_call>
79/// ```
80///
81/// Differences from MiniMax M2:
82/// - Each structural tag is prefixed with the namespace marker `]<]minimax[>[`.
83/// - The start token is `]<]minimax[>[<tool_call>` (not `<minimax:tool_call>`).
84/// - A single tool-call block may contain multiple `<invoke>` tags.
85/// - Parameters are expressed with parameter-name XML tags and may nest
86/// recursively to form objects and arrays.
87///
88/// Reference: the serving engine's `MinimaxM3ToolParser` (`tool_call_start_token =
89/// "]<]minimax[>[<tool_call>"`).
90pub struct MinimaxM3Parser {
91 // Streaming state
92 buffer: String,
93 prev_tool_call_arr: Vec<Value>,
94 current_tool_id: i32,
95 streamed_args_for_tool: Vec<String>,
96 in_tool_call: bool,
97}
98
99/// A parsed parameter value: either leaf text or nested child elements.
100enum ParamValue {
101 Text(String),
102 Elements(Vec<(String, ParamValue)>),
103}
104
105impl MinimaxM3Parser {
106 /// Create a new MiniMax M3 parser.
107 pub fn new() -> Self {
108 Self {
109 buffer: String::new(),
110 prev_tool_call_arr: Vec::new(),
111 current_tool_id: -1,
112 streamed_args_for_tool: Vec::new(),
113 in_tool_call: false,
114 }
115 }
116
117 /// Parse a leaf value from text, coercing by declared schema type when known,
118 /// otherwise inferring the JSON type (number/bool/null) and defaulting to string.
119 fn coerce_leaf(text: &str, declared_type: Option<&str>) -> Value {
120 // An empty container renders as an element with no children and no
121 // text, which reaches the leaf path indistinguishable from an empty
122 // string. `coerce_by_schema_type` cannot parse "" as JSON, so without
123 // this the value would be inferred as `""` and fail the tool's schema.
124 if text.trim().is_empty() {
125 match declared_type {
126 Some("array") => return Value::Array(Vec::new()),
127 Some("object") => return Value::Object(Map::new()),
128 _ => {}
129 }
130 }
131
132 if let Some(value) = helpers::coerce_by_schema_type(text, declared_type) {
133 return value;
134 }
135 Self::infer_value(text)
136 }
137
138 /// Infer a JSON value from a raw text leaf (no schema available).
139 fn infer_value(text: &str) -> Value {
140 match text {
141 "true" | "True" => return Value::Bool(true),
142 "false" | "False" => return Value::Bool(false),
143 "null" | "None" => return Value::Null,
144 _ => {}
145 }
146
147 if let Ok(num) = text.parse::<i64>() {
148 return Value::Number(num.into());
149 }
150 if let Ok(num) = text.parse::<f64>() {
151 if let Some(n) = serde_json::Number::from_f64(num) {
152 return Value::Number(n);
153 }
154 }
155
156 Value::String(text.to_string())
157 }
158
159 /// Length of the longest suffix of `buffer` that is a proper prefix of `token`.
160 ///
161 /// Unlike [`helpers::ends_with_partial_token`] (which returns the shortest such
162 /// match), this prefers the longest match. The M3 start token contains repeated
163 /// `]` characters, so a shortest-match would mis-align and leak the marker's
164 /// leading bytes as normal text.
165 fn longest_partial_suffix(buffer: &str, token: &str) -> Option<usize> {
166 if buffer.is_empty() || token.is_empty() {
167 return None;
168 }
169 token
170 .char_indices()
171 .skip(1)
172 .map(|(i, _)| i)
173 .filter(|&i| buffer.ends_with(&token[..i]))
174 .max()
175 }
176
177 /// Whether a buffered tool-call wrapper can still become a valid invoke.
178 ///
179 /// Whitespace is allowed between the wrapper and the invoke marker. Once
180 /// the first non-whitespace bytes diverge from that marker, later input
181 /// cannot turn the candidate into a tool call.
182 fn could_start_invoke(buffer: &str) -> bool {
183 let Some(after_wrapper) = buffer.strip_prefix(TOOL_CALL_START) else {
184 return false;
185 };
186 let candidate = after_wrapper.trim_start();
187 if candidate.is_empty() || INVOKE_START.starts_with(candidate) {
188 return true;
189 }
190
191 let Some(after_invoke) = candidate.strip_prefix(INVOKE_START) else {
192 return false;
193 };
194 after_invoke.is_empty()
195 || after_invoke
196 .chars()
197 .next()
198 .is_some_and(|c| c.is_whitespace() || c == '>')
199 }
200
201 /// Decode common XML entities.
202 fn decode_xml_entities(text: &str) -> String {
203 text.replace("<", "<")
204 .replace(">", ">")
205 .replace(""", "\"")
206 .replace("'", "'")
207 .replace("&", "&")
208 }
209
210 /// Extract the `name="..."` attribute value from an invoke header (the text
211 /// between `]<]minimax[>[<invoke` and the closing `>`).
212 fn parse_invoke_name(header: &str) -> Option<String> {
213 let idx = header.find("name")?;
214 let after = &header[idx + "name".len()..];
215 let after = after.trim_start();
216 let after = after.strip_prefix('=')?.trim_start();
217 if let Some(rest) = after.strip_prefix('"') {
218 let end = rest.find('"')?;
219 Some(rest[..end].trim().to_string())
220 } else if let Some(rest) = after.strip_prefix('\'') {
221 let end = rest.find('\'')?;
222 Some(rest[..end].trim().to_string())
223 } else {
224 let end = after
225 .find(|c: char| c.is_whitespace())
226 .unwrap_or(after.len());
227 let name = after[..end].trim();
228 if name.is_empty() {
229 None
230 } else {
231 Some(name.to_string())
232 }
233 }
234 }
235
236 /// Parse the body of one element (recursively), returning its value and the
237 /// number of bytes consumed up to and including the matching close tag.
238 /// An empty schema-declared container may omit its close immediately before
239 /// its parent closes; in that case the parent marker remains unconsumed.
240 fn parse_element_body(
241 input: &str,
242 name: &str,
243 schema: Option<&Value>,
244 parent_name: Option<&str>,
245 ) -> Option<(ParamValue, usize)> {
246 let close_tag = format!("{ELEMENT_END_START}{name}>");
247 let mut pos = 0;
248 let mut text = String::new();
249 let mut children: Vec<(String, ParamValue)> = Vec::new();
250
251 loop {
252 // Accumulate any text up to the next namespace marker; a body
253 // with no further marker is an unterminated element.
254 let rest = &input[pos..];
255 let text_chunk = &rest[..rest.find(NAMESPACE)?];
256 text.push_str(text_chunk);
257 pos += text_chunk.len();
258
259 let rest = &input[pos..];
260 if rest.starts_with(&close_tag) {
261 pos += close_tag.len();
262 break;
263 }
264 if text.trim().is_empty()
265 && children.is_empty()
266 && matches!(Self::schema_type(schema), Some("array" | "object"))
267 && parent_name.is_some_and(|parent| {
268 rest.starts_with(&format!("{ELEMENT_END_START}{parent}>"))
269 })
270 {
271 // The parent consumes its own close; the empty child's close
272 // was omitted by the model.
273 break;
274 }
275 if rest.starts_with(ELEMENT_START) {
276 // Child element.
277 let (child_name, child_value, consumed) =
278 Self::parse_element(rest, schema, Some(name))?;
279 children.push((child_name, child_value));
280 pos += consumed;
281 continue;
282 }
283 // A namespace marker that is neither the expected close nor a child
284 // start: malformed body.
285 return None;
286 }
287
288 let value = if children.is_empty() {
289 ParamValue::Text(text)
290 } else {
291 if !text.trim().is_empty() {
292 Self::push_mixed_text(&mut children, text);
293 }
294 ParamValue::Elements(children)
295 };
296 Some((value, pos))
297 }
298
299 /// Parse a complete element starting at `input` (which must begin with
300 /// `]<]minimax[>[<name>`). Returns `(name, value, consumed_bytes)`.
301 fn parse_element(
302 input: &str,
303 parent_schema: Option<&Value>,
304 parent_name: Option<&str>,
305 ) -> Option<(String, ParamValue, usize)> {
306 let after_start = input.strip_prefix(ELEMENT_START)?;
307 let gt = after_start.find('>')?;
308 let name = after_start[..gt].trim().to_string();
309 if name.is_empty() || name.starts_with('/') {
310 return None;
311 }
312 let body_start = ELEMENT_START.len() + gt + 1;
313 let parent_schema = Self::container_schema(parent_schema).or(parent_schema);
314 let schema = if Self::schema_type(parent_schema) == Some("array") {
315 parent_schema.and_then(|schema| schema.get("items"))
316 } else {
317 Self::property_schema(parent_schema, &name)
318 };
319 let schema = Self::container_schema(schema).or(schema);
320 let (value, body_consumed) =
321 Self::parse_element_body(&input[body_start..], &name, schema, parent_name)?;
322 Some((name, value, body_start + body_consumed))
323 }
324
325 /// Preserve mixed text content under a reserved object field, avoiding a
326 /// collision with an existing child name by prefixing `$`.
327 fn push_mixed_text(children: &mut Vec<(String, ParamValue)>, text: String) {
328 let mut field = MIXED_TEXT_FIELD.to_string();
329 while children.iter().any(|(name, _)| *name == field) {
330 field.insert(0, '$');
331 }
332 children.push((field, ParamValue::Text(text)));
333 }
334
335 /// The scalar `type` declared by a schema node, when it has one.
336 fn schema_type(schema: Option<&Value>) -> Option<&str> {
337 schema?.get("type")?.as_str()
338 }
339
340 /// Resolve a direct container or a transparent composition wrapper.
341 /// Nullable unions must have exactly one container branch; scalar/unknown
342 /// alternatives are ambiguous. Only singleton allOf wrappers are unwrapped:
343 /// choosing one of several conjuncts would discard the others' constraints.
344 fn container_schema(schema: Option<&Value>) -> Option<&Value> {
345 let schema = schema?;
346 if schema.get("type").is_some() {
347 return matches!(Self::schema_type(Some(schema)), Some("array" | "object"))
348 .then_some(schema);
349 }
350
351 let mut compositions = ["oneOf", "anyOf", "allOf"]
352 .into_iter()
353 .filter_map(|key| schema.get(key).map(|branches| (key, branches)));
354 let (keyword, branches) = compositions.next()?;
355 if compositions.next().is_some() {
356 return None;
357 }
358 // Unwrapping must not drop sibling validation or structural keywords.
359 if schema.as_object()?.keys().any(|key| {
360 key != keyword
361 && !matches!(
362 key.as_str(),
363 "title"
364 | "description"
365 | "default"
366 | "examples"
367 | "deprecated"
368 | "readOnly"
369 | "writeOnly"
370 | "$comment"
371 )
372 }) {
373 return None;
374 }
375 let branches = branches.as_array()?;
376 if keyword == "allOf" && branches.len() != 1 {
377 return None;
378 }
379 let mut container = None;
380 for branch in branches {
381 if keyword != "allOf" && Self::schema_type(Some(branch)) == Some("null") {
382 continue;
383 }
384 let resolved = Self::container_schema(Some(branch))?;
385 if container.replace(resolved).is_some() {
386 return None;
387 }
388 }
389 container
390 }
391
392 /// Find a named property in this schema or a nested composition branch.
393 fn property_schema<'a>(schema: Option<&'a Value>, name: &str) -> Option<&'a Value> {
394 let schema = schema?;
395 if let Some(property) = schema.get("properties").and_then(|value| value.get(name)) {
396 return Some(property);
397 }
398
399 for keyword in ["oneOf", "anyOf", "allOf"] {
400 for branch in schema
401 .get(keyword)
402 .and_then(Value::as_array)
403 .into_iter()
404 .flatten()
405 {
406 if let Some(property) = Self::property_schema(Some(branch), name) {
407 return Some(property);
408 }
409 }
410 }
411
412 None
413 }
414
415 /// Convert a parsed parameter value into a JSON value, coercing leaves by
416 /// the schema node they sit under: array elements descend into `items`,
417 /// object members into their `properties` entry.
418 fn value_to_json(value: ParamValue, schema: Option<&Value>) -> Value {
419 let schema = Self::container_schema(schema).or(schema);
420 match value {
421 ParamValue::Text(text) => {
422 let decoded = Self::decode_xml_entities(&text);
423 Self::coerce_leaf(&decoded, Self::schema_type(schema))
424 }
425 ParamValue::Elements(children) => {
426 // Repeated child names under an `array` schema (or with duplicate
427 // keys) collapse to an array; otherwise build an object.
428 if Self::schema_type(schema) == Some("array") {
429 let items_schema = schema.and_then(|s| s.get("items"));
430 let items = children
431 .into_iter()
432 .map(|(_, v)| Self::value_to_json(v, items_schema))
433 .collect();
434 return Value::Array(items);
435 }
436
437 let mut map: Map<String, Value> = Map::new();
438 for (name, child) in children {
439 let child_schema = Self::property_schema(schema, &name);
440 let child_json = Self::value_to_json(child, child_schema);
441 match map.get_mut(&name) {
442 Some(Value::Array(arr)) => arr.push(child_json),
443 Some(existing) => {
444 let prev = existing.take();
445 *existing = Value::Array(vec![prev, child_json]);
446 }
447 None => {
448 map.insert(name, child_json);
449 }
450 }
451 }
452 Value::Object(map)
453 }
454 }
455 }
456
457 /// Parse all parameter elements inside a complete invoke body into a JSON
458 /// object. `params_schema` is the function's `parameters` JSON schema.
459 /// `None` when an element starts but fails to parse: emitting the
460 /// arguments collected so far would run the tool with silently missing
461 /// parameters.
462 fn parse_invoke_params(body: &str, params_schema: Option<&Value>) -> Option<Value> {
463 let mut map: Map<String, Value> = Map::new();
464 let mut pos = 0;
465
466 loop {
467 let rest = &body[pos..];
468 let trimmed = rest.trim_start();
469 let trim_len = rest.len() - trimmed.len();
470 if trimmed.is_empty() {
471 break;
472 }
473 if !trimmed.starts_with(ELEMENT_START) {
474 // Tolerant: ordinary text at a parameter boundary ends the params.
475 break;
476 }
477 pos += trim_len;
478 let (name, value, consumed) = Self::parse_element(&body[pos..], params_schema, None)?;
479 pos += consumed;
480 let json = Self::value_to_json(value, Self::property_schema(params_schema, &name));
481 match map.get_mut(&name) {
482 Some(Value::Array(arr)) => arr.push(json),
483 Some(existing) => {
484 let prev = existing.take();
485 *existing = Value::Array(vec![prev, json]);
486 }
487 None => {
488 map.insert(name, json);
489 }
490 }
491 }
492
493 Some(Value::Object(map))
494 }
495
496 /// Parse a single invoke block (the text between `]<]minimax[>[<invoke` and
497 /// `]<]minimax[>[</invoke>`) into a tool call.
498 fn parse_invoke(block: &str, tools: &[Tool]) -> Option<ToolCall> {
499 // `block` begins at the namespace marker for `<invoke`.
500 let after_invoke = block.strip_prefix(INVOKE_START)?;
501 let gt = after_invoke.find('>')?;
502 let header = &after_invoke[..gt];
503 let name = Self::parse_invoke_name(header)?;
504 let body = &after_invoke[gt + 1..];
505
506 let params_schema = tools
507 .iter()
508 .find(|t| t.function.name == name)
509 .map(|t| t.function.parameters_schema());
510 let arguments = Self::parse_invoke_params(body, params_schema)?;
511 let arguments_str = serde_json::to_string(&arguments).ok()?;
512
513 Some(ToolCall {
514 function: FunctionCall {
515 name,
516 arguments: arguments_str,
517 },
518 })
519 }
520
521 /// Parse all complete tool-call blocks in `text`.
522 /// Returns the tool calls and the byte position of the first tool-call block.
523 fn parse_tool_calls(text: &str, tools: &[Tool]) -> (Vec<ToolCall>, Option<usize>) {
524 let mut calls = Vec::new();
525 let mut first_pos = None;
526 let mut search_from = 0;
527
528 while let Some(rel_start) = text[search_from..].find(TOOL_CALL_START) {
529 let block_start = search_from + rel_start;
530 let inner_start = block_start + TOOL_CALL_START.len();
531 let Some(rel_end) = text[inner_start..].find(TOOL_CALL_END) else {
532 break;
533 };
534 let inner_end = inner_start + rel_end;
535 let inner = &text[inner_start..inner_end];
536
537 // Extract each invoke block within the tool-call wrapper.
538 let mut invoke_from = 0;
539 while let Some(rel_inv) = inner[invoke_from..].find(INVOKE_START) {
540 let inv_start = invoke_from + rel_inv;
541 let Some(rel_inv_end) = inner[inv_start..].find(INVOKE_END) else {
542 break;
543 };
544 let inv_end = inv_start + rel_inv_end;
545 let inv_block = &inner[inv_start..inv_end];
546 if let Some(call) = Self::parse_invoke(inv_block, tools) {
547 if first_pos.is_none() {
548 first_pos = Some(block_start);
549 }
550 calls.push(call);
551 }
552 invoke_from = inv_end + INVOKE_END.len();
553 }
554
555 search_from = inner_end + TOOL_CALL_END.len();
556 }
557
558 (calls, first_pos)
559 }
560
561 /// Shared non-streaming parse. `tools` empty means infer types from text.
562 fn parse_complete_inner(text: &str, tools: &[Tool]) -> (String, Vec<ToolCall>) {
563 if !text.contains(TOOL_CALL_START) {
564 return (text.to_string(), vec![]);
565 }
566 let (calls, first_pos) = Self::parse_tool_calls(text, tools);
567 if calls.is_empty() {
568 return (text.to_string(), vec![]);
569 }
570 let normal_text = match first_pos {
571 Some(pos) => text[..pos].to_string(),
572 None => text.to_string(),
573 };
574 (normal_text, calls)
575 }
576
577 /// Constrain the forced calls in the model's own syntax: one block,
578 /// `]<]minimax[>[<tool_call>` and a newline, one invoke or more, the
579 /// separator and `</tool_call>`; an invoke is the separator,
580 /// `<invoke name="NAME">`, one element per schema property in the
581 /// schema's order (optional unless the schema requires it), the
582 /// separator, `</invoke>` and a newline; an element is the separator and
583 /// `<key>`, the value, the separator and `</key>`. A value follows the
584 /// type its schema pins: a string as text free of the separator, a string
585 /// `enum` as one of its values, a number as the JSON its schema accepts, a
586 /// boolean as one of the words the parser reads, an object as nested
587 /// elements, an array as `<item>` elements, `null` as nothing (the
588 /// template leaves such an argument out), anything untyped as free text
589 /// up to the closing tag. A nameless tool is skipped; with no named tool
590 /// the block is empty, which parses to no calls. The separator stays in
591 /// the trigger: this parser reads the block only with it.
592 ///
593 /// The forced block is written at once, so the tag opens with what the
594 /// model writes before a call when the template leaves the thought to it
595 /// (its default): the thought block, or the bare `</mm:think>` the model
596 /// writes for an empty one, or nothing. A prompt that ends inside the
597 /// thought gets the registry's [`Self::reasoning_prefix`] in front as
598 /// well (see `ParserRegistry::generate_tool_constraint`).
599 pub fn build_structural_tag(tools: &[Tool], at_least_one: bool) -> Value {
600 let invokes: Vec<Value> = tools
601 .iter()
602 .filter(|tool| !tool.function.name.is_empty())
603 .map(|tool| Elements::of(tool.function.parameters_schema()).invoke(&tool.function.name))
604 .collect();
605 // An empty list of invokes is unsatisfiable; with no named tool, admit
606 // only an empty block.
607 let invokes = if invokes.is_empty() {
608 json!({ "type": "const_string", "value": "" })
609 } else {
610 json!({
611 "type": "tags_with_separator",
612 "tags": invokes,
613 "separator": "",
614 "at_least_one": true,
615 })
616 };
617 json!({
618 "format": {
619 "type": "sequence",
620 "elements": [
621 Self::before_the_block(),
622 {
623 "type": "triggered_tags",
624 "triggers": [TOOL_CALL_START],
625 "tags": [{
626 "type": "tag",
627 "begin": format!("{TOOL_CALL_START}\n"),
628 "content": invokes,
629 "end": TOOL_CALL_END,
630 }],
631 "at_least_one": at_least_one,
632 },
633 ],
634 }
635 })
636 }
637
638 /// What the model writes before the block when the template leaves the
639 /// thought to it: its thought between `<mm:think>` and `</mm:think>`,
640 /// free of the markers and the separator, or the bare close it writes
641 /// for an empty thought, or nothing at all.
642 fn before_the_block() -> Value {
643 json!({
644 "type": "optional",
645 "content": {
646 "type": "or",
647 "elements": [
648 {
649 "type": "tag",
650 "begin": THINK_START,
651 "content": {
652 "type": "any_text",
653 "excludes": [THINK_START, THINK_END, NAMESPACE],
654 },
655 "end": THINK_END,
656 },
657 { "type": "const_string", "value": THINK_END },
658 ],
659 },
660 })
661 }
662
663 /// What precedes a forced call on a prompt that ends inside the thought,
664 /// by where the engine starts the grammar; the registry emits
665 /// `sequence[prefix, calls]`. Where the engine applies the grammar only
666 /// after its reasoning parser has seen the model's own `</mm:think>`,
667 /// nothing precedes the calls. Where it applies the grammar from the
668 /// first token, the prefix is the thought, free of the think markers and
669 /// the separator, closed by `</mm:think>`, so the call follows the
670 /// thought. Where the worker does not say, the prefix is free text that
671 /// writes no separator, so no call marker, with nothing owed at its end:
672 /// the thought and its close on the one kind of engine, nothing on the
673 /// other.
674 pub fn reasoning_prefix(start: GrammarStart) -> Option<Value> {
675 match start {
676 GrammarStart::AfterReasoning => None,
677 GrammarStart::Unknown => Some(json!({
678 "type": "any_text",
679 "excludes": [NAMESPACE],
680 })),
681 GrammarStart::FirstToken => Some(json!({
682 "type": "tag",
683 "begin": "",
684 "content": {
685 "type": "any_text",
686 "excludes": [THINK_START, THINK_END, NAMESPACE],
687 },
688 "end": THINK_END,
689 })),
690 }
691 }
692}
693
694/// The elements of one invoke, written from the tool's `parameters` schema.
695///
696/// Local references (`#/$defs/Name`, `#/definitions/Name`) are followed, the pointer's escapes
697/// undone; a bare `#` is not, since it would name the fragment's own document once the engine
698/// compiles a fragment alone; a reference back into a schema already being written, directly or
699/// through another definition, is written as free text where it recurs, so a schema that refers
700/// to itself stays bounded, the depth cap the outer bound. The elements of one invoke are counted
701/// against [`NODES`]: once they are spent, the values still to write (the rest of an object, a
702/// list) are free text, so a wide fan-out stays bounded too; before a property is descended into,
703/// one element is held for each of the properties after it that writes one, so an early object
704/// cannot take the elements its siblings need for their own tags. The leaves' spellings are
705/// counted against [`LEAF_BYTES`]: an enum's words, and a number's schema with the definitions
706/// its pointers reach (those and no other, each written once per fragment); once they are spent,
707/// an enum is text as it is and a number free text. The walks that size a number's definitions,
708/// refused ones too, are counted against [`WALK_BYTES`]; once it is spent, a number with pointers
709/// is free text without a walk.
710///
711/// The `symphony` crate's `grammar/xml.rs` writes the same model's block from its table, and
712/// spells the same output for a schema within the budgets; it follows the references and the
713/// depth cap the same way and has the same element and byte budgets, and takes the same walk
714/// allowance in its follow-up.
715struct Elements<'a> {
716 root: &'a Value,
717 /// The pointers of the references on the path being written, outermost first.
718 path: Vec<&'a str>,
719 /// The elements this invoke may still write.
720 nodes: usize,
721 /// How many of them are held for the properties after the one being written, at every level
722 /// of the descent together.
723 reserved: usize,
724 /// The bytes the invoke's leaves may still spell.
725 leaf_bytes: usize,
726 /// The bytes the walks behind its number leaves may still size, refused walks counted too.
727 walk_bytes: usize,
728 /// The serialized size of each root definition a fragment reached, by the entry's address,
729 /// worked out once per invoke.
730 entry_bytes: HashMap<usize, usize>,
731 /// What each number leaf's fragment costs and reaches, by the leaf schema's address: `None`
732 /// for a leaf the budget or the walks' allowance refused, which they refuse again, both only
733 /// shrinking.
734 reached_by_leaf: HashMap<usize, Option<(usize, Vec<Reached<'a>>)>>,
735 /// What each enum costs, by its word list's address.
736 enum_bytes: HashMap<usize, usize>,
737}
738
739/// A root definition a fragment reaches: its block, its name and its entry.
740type Reached<'a> = (&'static str, &'a str, &'a Value);
741
742/// How a value's schema is written: as free text, at the depth cap or behind a reference that
743/// recurs; or by its type, its references followed and on the path, `pushed` of them, which the
744/// writer unwinds once the value is written. A schema typed `null` is written as nothing, and is
745/// neither ([`Elements::how`]).
746enum Writes<'a> {
747 FreeText,
748 Typed {
749 schema: &'a Value,
750 pushed: usize,
751 depth: usize,
752 },
753}
754
755/// What writing a property gives: nothing, for a property typed `null`; no element, once the
756/// invoke's budget is spent, when the caller writes the rest as free text; or the element.
757enum Element {
758 Nothing,
759 Spent,
760 Written(Value),
761}
762
763impl<'a> Elements<'a> {
764 fn of(root: &'a Value) -> Self {
765 Self {
766 root,
767 path: Vec::new(),
768 nodes: NODES,
769 reserved: 0,
770 leaf_bytes: LEAF_BYTES,
771 walk_bytes: WALK_BYTES,
772 entry_bytes: HashMap::new(),
773 reached_by_leaf: HashMap::new(),
774 enum_bytes: HashMap::new(),
775 }
776 }
777
778 /// `cost` bytes off the leaves' budget; `false` when they do not fit, when the caller writes
779 /// the value's fallback and the budget stays for a smaller leaf.
780 fn takes_bytes(&mut self, cost: usize) -> bool {
781 if cost > self.leaf_bytes {
782 return false;
783 }
784 self.leaf_bytes -= cost;
785 true
786 }
787
788 /// Whether the budget has an element left beyond the ones held for later siblings.
789 fn has_an_element(&self) -> bool {
790 self.nodes > self.reserved
791 }
792
793 /// One element off the budget, which [`Self::has_an_element`] said was there.
794 fn take_an_element(&mut self) {
795 self.nodes = self.nodes.saturating_sub(1);
796 }
797
798 /// How `schema` is written at `depth`, or `None` for a schema typed `null`, written as
799 /// nothing: the one place that decides, for the count of an object's properties that write
800 /// and for the writing itself.
801 fn how(&mut self, schema: &'a Value, depth: usize) -> Option<Writes<'a>> {
802 let Some(depth) = depth.checked_sub(1) else {
803 return Some(Writes::FreeText);
804 };
805 let Some((schema, pushed)) = self.follow(schema) else {
806 return Some(Writes::FreeText);
807 };
808 if MinimaxM3Parser::schema_type(Some(schema)) == Some("null") {
809 self.unwind(pushed);
810 return None;
811 }
812 Some(Writes::Typed {
813 schema,
814 pushed,
815 depth,
816 })
817 }
818
819 /// Whether a property's schema writes an element: everything but `null`, as [`Self::how`]
820 /// has it.
821 fn writes_an_element(&mut self, schema: &'a Value, depth: usize) -> bool {
822 match self.how(schema, depth) {
823 None => false,
824 Some(writes) => {
825 self.leaves(writes);
826 true
827 }
828 }
829 }
830
831 /// A value classified and not written: its references, if [`Self::how`] followed any, off
832 /// the path again.
833 fn leaves(&mut self, writes: Writes<'a>) {
834 if let Writes::Typed { pushed, .. } = writes {
835 self.unwind(pushed);
836 }
837 }
838
839 /// One invoke: the separator and the opener with the tool's name, the
840 /// elements the schema shapes, the separator, `</invoke>` and a newline.
841 fn invoke(mut self, name: &str) -> Value {
842 let content = match self.follow(self.root) {
843 Some((schema, pushed)) => {
844 let content = self.members(schema, "</invoke>", DEPTH);
845 self.unwind(pushed);
846 content
847 }
848 None => Self::free_text("</invoke>"),
849 };
850 json!({
851 "type": "tag",
852 "begin": format!("{INVOKE_START} name=\"{name}\">"),
853 "content": content,
854 "end": format!("{INVOKE_END}\n"),
855 })
856 }
857
858 /// The schema a local pointer names: an entry under `$defs` or `definitions`, the pointer's
859 /// escapes (`~1` for `/`, `~0` for `~`) undone; nothing for a bare `#`, a pointer elsewhere,
860 /// or a missing target.
861 fn definition(&self, pointer: &str) -> Option<&'a Value> {
862 let mut segments = pointer.strip_prefix("#/")?.split('/');
863 let block = match segments.next()? {
864 block @ ("$defs" | "definitions") => block,
865 _ => return None,
866 };
867 let mut node = self.root.get(block)?;
868 for segment in segments {
869 node = node.get(segment.replace("~1", "/").replace("~0", "~"))?;
870 }
871 Some(node)
872 }
873
874 /// `schema` with its references followed, and how many of them the path took on (the caller
875 /// unwinds them once the schema is written); `None` when a reference re-enters a schema on
876 /// the path, or the hops exceed the depth cap: that node is written as free text. A pointer
877 /// that names nothing stops the following, and the schema stays what it is: untyped.
878 fn follow(&mut self, schema: &'a Value) -> Option<(&'a Value, usize)> {
879 let mut schema = schema;
880 let mut pushed = 0;
881 while let Some(pointer) = schema.get("$ref").and_then(Value::as_str) {
882 let Some(target) = self.definition(pointer) else {
883 break;
884 };
885 if self.path.contains(&pointer) || pushed == DEPTH {
886 self.unwind(pushed);
887 return None;
888 }
889 self.path.push(pointer);
890 pushed += 1;
891 schema = target;
892 }
893 Some((schema, pushed))
894 }
895
896 /// The path without its last `pushed` references.
897 fn unwind(&mut self, pushed: usize) {
898 let keep = self.path.len().saturating_sub(pushed);
899 self.path.truncate(keep);
900 }
901
902 /// An object's elements: one per property in the schema's order, each
903 /// optional unless the schema requires it, a property typed `null` left
904 /// out at no cost to the budget; free text up to `close` for a schema
905 /// without properties, and for the properties left once the invoke's
906 /// element budget is spent. While a property's value is written, one
907 /// element is held for each property after it that writes one, so each
908 /// still gets its own tag.
909 fn members(&mut self, schema: &'a Value, close: &str, depth: usize) -> Value {
910 let properties = schema
911 .get("properties")
912 .and_then(Value::as_object)
913 .filter(|properties| !properties.is_empty());
914 let Some(properties) = properties else {
915 return Self::free_text(close);
916 };
917 let required: Vec<&str> = schema
918 .get("required")
919 .and_then(Value::as_array)
920 .map(|names| names.iter().filter_map(Value::as_str).collect())
921 .unwrap_or_default();
922 // The properties that write, counted first: while one is written, one element is held
923 // for each of them after it.
924 let mut later: usize = 0;
925 for schema in properties.values() {
926 if self.writes_an_element(schema, depth) {
927 later += 1;
928 }
929 }
930 let mut elements: Vec<Value> = Vec::new();
931 for (key, schema) in properties {
932 match self.element(key, schema, depth, later.saturating_sub(1)) {
933 Element::Nothing => {}
934 Element::Spent => {
935 elements.push(Self::free_text(close));
936 break;
937 }
938 Element::Written(element) => {
939 later = later.saturating_sub(1);
940 elements.push(if required.contains(&key.as_str()) {
941 element
942 } else {
943 json!({ "type": "optional", "content": element })
944 });
945 }
946 }
947 }
948 if elements.is_empty() {
949 return json!({ "type": "const_string", "value": "" });
950 }
951 json!({ "type": "sequence", "elements": elements })
952 }
953
954 /// One element: the separator and `<key>`, the value, the separator and
955 /// `</key>`, one element off the budget, with `later` held for the
956 /// properties after it while its value is written; nothing for a property
957 /// typed `null`, at no cost and whatever the budget; no element once the
958 /// budget is spent.
959 fn element(&mut self, key: &str, schema: &'a Value, depth: usize, later: usize) -> Element {
960 let Some(writes) = self.how(schema, depth) else {
961 return Element::Nothing;
962 };
963 if !self.has_an_element() {
964 self.leaves(writes);
965 return Element::Spent;
966 }
967 self.take_an_element();
968 self.reserved += later;
969 let value = self.value(&format!("</{key}>"), writes);
970 self.reserved -= later;
971 Element::Written(json!({
972 "type": "sequence",
973 "elements": [
974 { "type": "const_string", "value": format!("{ELEMENT_START}{key}>") },
975 value,
976 { "type": "const_string", "value": format!("{ELEMENT_END_START}{key}>") },
977 ],
978 }))
979 }
980
981 /// The value between an element's tags, as [`Self::how`] says it is
982 /// written: free text, or by the type its schema pins.
983 fn value(&mut self, close: &str, writes: Writes<'a>) -> Value {
984 let Writes::Typed {
985 schema,
986 pushed,
987 depth,
988 } = writes
989 else {
990 return Self::free_text(close);
991 };
992 let value = match MinimaxM3Parser::schema_type(Some(schema)) {
993 Some("boolean") => Self::one_of(["true", "false", "True", "False"]),
994 Some("object") => self.members(schema, close, depth),
995 Some("array") => match schema.get("items") {
996 Some(items) if self.has_an_element() => {
997 self.take_an_element();
998 self.items(items, depth)
999 }
1000 _ => Self::free_text(close),
1001 },
1002 Some("string") => match schema.get("enum").and_then(Value::as_array) {
1003 Some(values) if !values.is_empty() && values.iter().all(Value::is_string) => {
1004 let key = std::ptr::from_ref(values) as usize;
1005 let cost = *self.enum_bytes.entry(key).or_insert_with(|| {
1006 values
1007 .iter()
1008 .filter_map(Value::as_str)
1009 .map(|word| word.len() + WORD_FRAMING)
1010 .sum()
1011 });
1012 if self.takes_bytes(cost) {
1013 Self::one_of(values.iter().filter_map(Value::as_str))
1014 } else {
1015 Self::text(close)
1016 }
1017 }
1018 _ => Self::text(close),
1019 },
1020 Some("integer" | "number") => self.fragment(schema, close),
1021 _ => Self::free_text(close),
1022 };
1023 self.unwind(pushed);
1024 value
1025 }
1026
1027 /// A list's items: any number of `<item>` elements, each a value of the
1028 /// items' type, an item typed `null` an empty element.
1029 fn items(&mut self, items: &'a Value, depth: usize) -> Value {
1030 let open = format!("{ELEMENT_START}{ITEM}>");
1031 let close = format!("{ELEMENT_END_START}{ITEM}>");
1032 let item = match self.how(items, depth) {
1033 Some(writes) => {
1034 let value = self.value(&format!("</{ITEM}>"), writes);
1035 json!({
1036 "type": "sequence",
1037 "elements": [
1038 { "type": "const_string", "value": open },
1039 value,
1040 { "type": "const_string", "value": close },
1041 ],
1042 })
1043 }
1044 None => json!({ "type": "const_string", "value": format!("{open}{close}") }),
1045 };
1046 json!({ "type": "star", "content": item })
1047 }
1048
1049 /// A number's schema as the engine gets it: with the root's definitions its pointers reach
1050 /// beside it, those and no other (the entry a pointer names by its first segment, and the
1051 /// entries those reach), so each pointer resolves once the engine compiles the fragment
1052 /// alone; a definition the schema declares itself stays. Free text up to `close` once the
1053 /// leaves' byte budget cannot take the fragment, definitions included, or the walks' allowance
1054 /// ([`WALK_BYTES`]) is spent before the fragment's definitions are sized.
1055 fn fragment(&mut self, schema: &'a Value, close: &str) -> Value {
1056 let Some((cost, reached)) = self.reached_within(schema) else {
1057 return Self::free_text(close);
1058 };
1059 if !self.takes_bytes(cost) {
1060 return Self::free_text(close);
1061 }
1062 let mut fragment = schema.clone();
1063 if let Some(object) = fragment.as_object_mut() {
1064 for (block, name, entry) in reached {
1065 let slot = object.entry(block.to_string()).or_insert_with(|| json!({}));
1066 if let Some(entries) = slot.as_object_mut() {
1067 entries
1068 .entry(name.to_string())
1069 .or_insert_with(|| entry.clone());
1070 }
1071 }
1072 }
1073 json!({ "type": "json_schema", "json_schema": fragment })
1074 }
1075
1076 /// What a number leaf's fragment costs, with the root's definitions its pointers reach,
1077 /// directly or through one another (each as its block, `$defs` or `definitions`, its name,
1078 /// the first segment after the block, since a pointer deeper into an entry needs the whole
1079 /// entry, and its entry), when that fits what the leaves may still spell; `None` as soon as
1080 /// the running count passes it, so a leaf the budget refuses costs the walk up to the budget
1081 /// and no more. Worked out once per leaf schema per invoke: a leaf refused once is refused
1082 /// again, the budget only shrinking. The walks, refused ones too, spend [`WALK_BYTES`] at the
1083 /// pace of the bytes they size; past it a leaf with pointers is refused without a walk, while
1084 /// a leaf without pointers, which walks nothing but itself, is neither bound nor charged.
1085 fn reached_within(&mut self, schema: &'a Value) -> Option<(usize, Vec<Reached<'a>>)> {
1086 let key = std::ptr::from_ref(schema) as usize;
1087 if let Some(known) = self.reached_by_leaf.get(&key) {
1088 return match known {
1089 Some((cost, reached)) if *cost <= self.leaf_bytes => Some((*cost, reached.clone())),
1090 _ => None,
1091 };
1092 }
1093 // A leaf without pointers walks nothing but itself, which the request already holds: the
1094 // allowance is for the definitions a leaf reaches, and neither bounds nor charges one.
1095 let walks = !local_pointers(schema).is_empty();
1096 let limit = if walks {
1097 self.leaf_bytes.min(self.walk_bytes)
1098 } else {
1099 self.leaf_bytes
1100 };
1101 let found = self.walk_reached(schema, limit);
1102 if walks {
1103 self.walk_bytes = self
1104 .walk_bytes
1105 .saturating_sub(found.as_ref().map_or(limit, |(cost, _)| *cost));
1106 }
1107 self.reached_by_leaf.insert(key, found.clone());
1108 found
1109 }
1110
1111 /// The walk behind [`Self::reached_within`], within `limit`.
1112 fn walk_reached(
1113 &mut self,
1114 schema: &'a Value,
1115 limit: usize,
1116 ) -> Option<(usize, Vec<Reached<'a>>)> {
1117 let mut cost = json_bytes(schema);
1118 if cost > limit {
1119 return None;
1120 }
1121 let mut reached: Vec<Reached<'a>> = Vec::new();
1122 let mut seen: HashSet<(&'static str, &'a str)> = HashSet::new();
1123 let mut pending: Vec<&'a Value> = vec![schema];
1124 while let Some(node) = pending.pop() {
1125 for pointer in local_pointers(node) {
1126 let Some((block, name, entry)) = self.entry_of(pointer) else {
1127 continue;
1128 };
1129 if !seen.insert((block, name)) {
1130 continue;
1131 }
1132 cost += self.entry_bytes(entry);
1133 if cost > limit {
1134 return None;
1135 }
1136 reached.push((block, name, entry));
1137 pending.push(entry);
1138 }
1139 }
1140 Some((cost, reached))
1141 }
1142
1143 /// The block, the name and the entry a local pointer names, the name's escapes undone (`~1`
1144 /// for `/`, `~0` for `~`); nothing for a bare `#`, a pointer elsewhere or a missing entry.
1145 fn entry_of(&self, pointer: &str) -> Option<(&'static str, &'a str, &'a Value)> {
1146 let mut segments = pointer.strip_prefix("#/")?.split('/');
1147 let block = match segments.next()? {
1148 "$defs" => "$defs",
1149 "definitions" => "definitions",
1150 _ => return None,
1151 };
1152 let entries = self.root.get(block)?.as_object()?;
1153 let name = segments.next()?.replace("~1", "/").replace("~0", "~");
1154 let (key, entry) = entries.get_key_value(name.as_str())?;
1155 Some((block, key.as_str(), entry))
1156 }
1157
1158 /// The serialized size of a definition's entry, worked out once per invoke.
1159 fn entry_bytes(&mut self, entry: &'a Value) -> usize {
1160 let key = std::ptr::from_ref(entry) as usize;
1161 if let Some(bytes) = self.entry_bytes.get(&key) {
1162 return *bytes;
1163 }
1164 let bytes = json_bytes(entry);
1165 self.entry_bytes.insert(key, bytes);
1166 bytes
1167 }
1168
1169 /// One of the words.
1170 fn one_of(words: impl IntoIterator<Item = &'a str>) -> Value {
1171 let words: Vec<Value> = words
1172 .into_iter()
1173 .map(|word| json!({ "type": "const_string", "value": word }))
1174 .collect();
1175 json!({ "type": "or", "elements": words })
1176 }
1177
1178 /// A string as it is: text that holds neither the separator, nor `close`,
1179 /// nor a marker that ends the call.
1180 fn text(close: &str) -> Value {
1181 Self::excluding(&[NAMESPACE, close])
1182 }
1183
1184 /// A value the schema does not shape: text that holds neither `close` nor
1185 /// a marker that ends the call, separators and nested tags included,
1186 /// which the parser reads as it infers.
1187 fn free_text(close: &str) -> Value {
1188 Self::excluding(&[close])
1189 }
1190
1191 /// Any text that holds none of `first` and none of the markers that end
1192 /// the call, each listed once.
1193 fn excluding(first: &[&str]) -> Value {
1194 let mut excludes: Vec<&str> = Vec::new();
1195 for text in first.iter().chain(CALL_EXITS.iter()) {
1196 if !excludes.contains(text) {
1197 excludes.push(text);
1198 }
1199 }
1200 json!({ "type": "any_text", "excludes": excludes })
1201 }
1202}
1203
1204/// The local pointers in `schema`, wherever a `$ref`, `$dynamicRef` or `$recursiveRef` stands.
1205fn local_pointers(schema: &Value) -> Vec<&str> {
1206 fn collect<'v>(schema: &'v Value, pointers: &mut Vec<&'v str>) {
1207 match schema {
1208 Value::Object(object) => {
1209 for (key, value) in object {
1210 if matches!(key.as_str(), "$ref" | "$dynamicRef" | "$recursiveRef") {
1211 if let Some(pointer) =
1212 value.as_str().filter(|pointer| pointer.starts_with('#'))
1213 {
1214 pointers.push(pointer);
1215 }
1216 }
1217 collect(value, pointers);
1218 }
1219 }
1220 Value::Array(items) => items.iter().for_each(|item| collect(item, pointers)),
1221 _ => {}
1222 }
1223 }
1224 let mut pointers = Vec::new();
1225 collect(schema, &mut pointers);
1226 pointers
1227}
1228
1229/// About how many bytes `value` takes serialized, the escapes counted: what a leaf costs the
1230/// invoke's byte budget.
1231fn json_bytes(value: &Value) -> usize {
1232 match value {
1233 Value::Null => 4,
1234 Value::Bool(flag) => {
1235 if *flag {
1236 4
1237 } else {
1238 5
1239 }
1240 }
1241 Value::Number(number) => number.to_string().len(),
1242 Value::String(text) => {
1243 text.len()
1244 + 2
1245 + text
1246 .bytes()
1247 .filter(|byte| matches!(byte, b'"' | b'\\') || *byte < 0x20)
1248 .count()
1249 }
1250 Value::Array(items) => {
1251 2 + items.iter().map(json_bytes).sum::<usize>() + items.len().saturating_sub(1)
1252 }
1253 Value::Object(object) => {
1254 2 + object
1255 .iter()
1256 .map(|(key, value)| key.len() + 3 + json_bytes(value))
1257 .sum::<usize>()
1258 + object.len().saturating_sub(1)
1259 }
1260 }
1261}
1262
1263impl Default for MinimaxM3Parser {
1264 fn default() -> Self {
1265 Self::new()
1266 }
1267}
1268
1269#[async_trait]
1270impl ToolParser for MinimaxM3Parser {
1271 async fn parse_complete(&self, text: &str) -> ParserResult<(String, Vec<ToolCall>)> {
1272 Ok(Self::parse_complete_inner(text, &[]))
1273 }
1274
1275 async fn parse_complete_with_tools(
1276 &self,
1277 text: &str,
1278 tools: &[Tool],
1279 ) -> ParserResult<(String, Vec<ToolCall>)> {
1280 Ok(Self::parse_complete_inner(text, tools))
1281 }
1282
1283 async fn parse_incremental(
1284 &mut self,
1285 chunk: &str,
1286 tools: &[Tool],
1287 ) -> ParserResult<StreamingParseResult> {
1288 self.buffer.push_str(chunk);
1289 let mut normal_text = String::new();
1290 let mut calls = Vec::new();
1291
1292 loop {
1293 // Outside a tool call: emit normal text until a start token appears.
1294 if !self.in_tool_call {
1295 if let Some(start) = self.buffer.find(TOOL_CALL_START) {
1296 normal_text.push_str(&self.buffer[..start]);
1297 self.buffer = self.buffer[start..].to_string();
1298 self.in_tool_call = true;
1299 continue;
1300 }
1301
1302 // No start token: flush text, holding back a potential partial token.
1303 if let Some(partial_len) =
1304 Self::longest_partial_suffix(&self.buffer, TOOL_CALL_START)
1305 {
1306 let end = self.buffer.len() - partial_len;
1307 normal_text.push_str(&self.buffer[..end]);
1308 self.buffer = self.buffer[end..].to_string();
1309 } else {
1310 normal_text.push_str(&self.buffer);
1311 self.buffer.clear();
1312 }
1313 break;
1314 }
1315
1316 // Inside a tool call: wait for the complete end token before emitting.
1317 let Some(end_rel) = self.buffer.find(TOOL_CALL_END) else {
1318 if !Self::could_start_invoke(&self.buffer) {
1319 // Release the false wrapper, then resume the normal-text
1320 // scan so a later marker (including a partial one) is still
1321 // recognized rather than flushed as ordinary content.
1322 normal_text.push_str(TOOL_CALL_START);
1323 self.buffer.drain(..TOOL_CALL_START.len());
1324 self.in_tool_call = false;
1325 continue;
1326 }
1327 break;
1328 };
1329 let block_end = end_rel + TOOL_CALL_END.len();
1330 let block = self.buffer[..block_end].to_string();
1331 self.buffer = self.buffer[block_end..].to_string();
1332 self.in_tool_call = false;
1333
1334 let (block_calls, _) = Self::parse_tool_calls(&block, tools);
1335 if block_calls.is_empty() {
1336 // No invoke parsed: match the non-streaming path, which
1337 // returns malformed blocks as text rather than dropping them.
1338 normal_text.push_str(&block);
1339 continue;
1340 }
1341 for call in block_calls {
1342 if self.current_tool_id == -1 {
1343 self.current_tool_id = 0;
1344 } else {
1345 self.current_tool_id += 1;
1346 }
1347 let tool_id = self.current_tool_id as usize;
1348 helpers::ensure_capacity(
1349 self.current_tool_id,
1350 &mut self.prev_tool_call_arr,
1351 &mut self.streamed_args_for_tool,
1352 );
1353
1354 let args = call.function.arguments.clone();
1355 if tool_id < self.streamed_args_for_tool.len() {
1356 self.streamed_args_for_tool[tool_id].clone_from(&args);
1357 }
1358 let parsed_args: Value =
1359 serde_json::from_str(&args).unwrap_or_else(|_| Value::Object(Map::new()));
1360 if tool_id < self.prev_tool_call_arr.len() {
1361 self.prev_tool_call_arr[tool_id] = serde_json::json!({
1362 "name": call.function.name,
1363 "arguments": parsed_args,
1364 });
1365 }
1366
1367 // Emit name then full arguments for this completed invoke.
1368 calls.push(ToolCallItem {
1369 tool_index: tool_id,
1370 name: Some(call.function.name),
1371 parameters: String::new(),
1372 });
1373 calls.push(ToolCallItem {
1374 tool_index: tool_id,
1375 name: None,
1376 parameters: args,
1377 });
1378 }
1379 }
1380
1381 Ok(StreamingParseResult { normal_text, calls })
1382 }
1383
1384 fn has_tool_markers(&self, text: &str) -> bool {
1385 text.contains(TOOL_CALL_START)
1386 }
1387
1388 fn get_unstreamed_tool_args(&self) -> Option<Vec<ToolCallItem>> {
1389 helpers::get_unstreamed_args(&self.prev_tool_call_arr, &self.streamed_args_for_tool)
1390 }
1391
1392 fn take_unstreamed_normal_text(&mut self) -> String {
1393 // Completed blocks are removed from `buffer`, so anything left here is
1394 // an independent, incomplete candidate and must be returned verbatim.
1395 // Leave the parser ready to process ordinary text if it is reused.
1396 self.in_tool_call = false;
1397 std::mem::take(&mut self.buffer)
1398 }
1399
1400 fn reset(&mut self) {
1401 self.buffer.clear();
1402 self.prev_tool_call_arr.clear();
1403 self.current_tool_id = -1;
1404 self.streamed_args_for_tool.clear();
1405 self.in_tool_call = false;
1406 }
1407}