Skip to main content

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("&lt;", "<")
204            .replace("&gt;", ">")
205            .replace("&quot;", "\"")
206            .replace("&apos;", "'")
207            .replace("&amp;", "&")
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);
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).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}