Skip to main content

tool_parser/parsers/
minimax_m3.rs

1use async_trait::async_trait;
2use openai_protocol::common::Tool;
3use serde_json::{Map, Value};
4
5use crate::{
6    errors::ParserResult,
7    parsers::helpers,
8    traits::ToolParser,
9    types::{FunctionCall, StreamingParseResult, ToolCall, ToolCallItem},
10};
11
12/// Namespace marker MiniMax M3 prepends before each structural tag.
13const NAMESPACE: &str = "]<]minimax[>[";
14/// Opening marker for a tool-call block.
15const TOOL_CALL_START: &str = "]<]minimax[>[<tool_call>";
16/// Closing marker for a tool-call block.
17const TOOL_CALL_END: &str = "]<]minimax[>[</tool_call>";
18/// Opening marker for an `<invoke ...>` element (attributes follow before `>`).
19const INVOKE_START: &str = "]<]minimax[>[<invoke";
20/// Closing marker for an `<invoke>` element.
21const INVOKE_END: &str = "]<]minimax[>[</invoke>";
22/// Opening marker prefix for a parameter element (`<name>`).
23const ELEMENT_START: &str = "]<]minimax[>[<";
24/// Opening marker prefix for a parameter closing tag (`</name>`).
25const ELEMENT_END_START: &str = "]<]minimax[>[</";
26/// Reserved field name used to preserve mixed text within an object element.
27const MIXED_TEXT_FIELD: &str = "$text";
28
29/// MiniMax M3 format parser for tool calls.
30///
31/// Handles the MiniMax M3 specific framing, where the namespace marker
32/// `]<]minimax[>[` is prepended before every structural tag:
33///
34/// ```text
35/// ]<]minimax[>[<tool_call>
36/// ]<]minimax[>[<invoke name="func">
37/// ]<]minimax[>[<key>value]<]minimax[>[</key>
38/// ]<]minimax[>[</invoke>
39/// ]<]minimax[>[</tool_call>
40/// ```
41///
42/// Differences from MiniMax M2:
43/// - Each structural tag is prefixed with the namespace marker `]<]minimax[>[`.
44/// - The start token is `]<]minimax[>[<tool_call>` (not `<minimax:tool_call>`).
45/// - A single tool-call block may contain multiple `<invoke>` tags.
46/// - Parameters are expressed with parameter-name XML tags and may nest
47///   recursively to form objects and arrays.
48///
49/// Reference: vLLM `MinimaxM3ToolParser` (`tool_call_start_token =
50/// "]<]minimax[>[<tool_call>"`).
51pub struct MinimaxM3Parser {
52    // Streaming state
53    buffer: String,
54    prev_tool_call_arr: Vec<Value>,
55    current_tool_id: i32,
56    streamed_args_for_tool: Vec<String>,
57    in_tool_call: bool,
58}
59
60/// A parsed parameter value: either leaf text or nested child elements.
61enum ParamValue {
62    Text(String),
63    Elements(Vec<(String, ParamValue)>),
64}
65
66impl MinimaxM3Parser {
67    /// Create a new MiniMax M3 parser.
68    pub fn new() -> Self {
69        Self {
70            buffer: String::new(),
71            prev_tool_call_arr: Vec::new(),
72            current_tool_id: -1,
73            streamed_args_for_tool: Vec::new(),
74            in_tool_call: false,
75        }
76    }
77
78    /// Parse a leaf value from text, coercing by declared schema type when known,
79    /// otherwise inferring the JSON type (number/bool/null) and defaulting to string.
80    fn coerce_leaf(text: &str, declared_type: Option<&str>) -> Value {
81        // An empty container renders as an element with no children and no
82        // text, which reaches the leaf path indistinguishable from an empty
83        // string. `coerce_by_schema_type` cannot parse "" as JSON, so without
84        // this the value would be inferred as `""` and fail the tool's schema.
85        if text.trim().is_empty() {
86            match declared_type {
87                Some("array") => return Value::Array(Vec::new()),
88                Some("object") => return Value::Object(Map::new()),
89                _ => {}
90            }
91        }
92
93        if let Some(value) = helpers::coerce_by_schema_type(text, declared_type) {
94            return value;
95        }
96        Self::infer_value(text)
97    }
98
99    /// Infer a JSON value from a raw text leaf (no schema available).
100    fn infer_value(text: &str) -> Value {
101        match text {
102            "true" | "True" => return Value::Bool(true),
103            "false" | "False" => return Value::Bool(false),
104            "null" | "None" => return Value::Null,
105            _ => {}
106        }
107
108        if let Ok(num) = text.parse::<i64>() {
109            return Value::Number(num.into());
110        }
111        if let Ok(num) = text.parse::<f64>() {
112            if let Some(n) = serde_json::Number::from_f64(num) {
113                return Value::Number(n);
114            }
115        }
116
117        Value::String(text.to_string())
118    }
119
120    /// Length of the longest suffix of `buffer` that is a proper prefix of `token`.
121    ///
122    /// Unlike [`helpers::ends_with_partial_token`] (which returns the shortest such
123    /// match), this prefers the longest match. The M3 start token contains repeated
124    /// `]` characters, so a shortest-match would mis-align and leak the marker's
125    /// leading bytes as normal text.
126    fn longest_partial_suffix(buffer: &str, token: &str) -> Option<usize> {
127        if buffer.is_empty() || token.is_empty() {
128            return None;
129        }
130        token
131            .char_indices()
132            .skip(1)
133            .map(|(i, _)| i)
134            .filter(|&i| buffer.ends_with(&token[..i]))
135            .max()
136    }
137
138    /// Whether a buffered tool-call wrapper can still become a valid invoke.
139    ///
140    /// Whitespace is allowed between the wrapper and the invoke marker. Once
141    /// the first non-whitespace bytes diverge from that marker, later input
142    /// cannot turn the candidate into a tool call.
143    fn could_start_invoke(buffer: &str) -> bool {
144        let Some(after_wrapper) = buffer.strip_prefix(TOOL_CALL_START) else {
145            return false;
146        };
147        let candidate = after_wrapper.trim_start();
148        if candidate.is_empty() || INVOKE_START.starts_with(candidate) {
149            return true;
150        }
151
152        let Some(after_invoke) = candidate.strip_prefix(INVOKE_START) else {
153            return false;
154        };
155        after_invoke.is_empty()
156            || after_invoke
157                .chars()
158                .next()
159                .is_some_and(|c| c.is_whitespace() || c == '>')
160    }
161
162    /// Decode common XML entities.
163    fn decode_xml_entities(text: &str) -> String {
164        text.replace("&lt;", "<")
165            .replace("&gt;", ">")
166            .replace("&quot;", "\"")
167            .replace("&apos;", "'")
168            .replace("&amp;", "&")
169    }
170
171    /// Extract the `name="..."` attribute value from an invoke header (the text
172    /// between `]<]minimax[>[<invoke` and the closing `>`).
173    fn parse_invoke_name(header: &str) -> Option<String> {
174        let idx = header.find("name")?;
175        let after = &header[idx + "name".len()..];
176        let after = after.trim_start();
177        let after = after.strip_prefix('=')?.trim_start();
178        if let Some(rest) = after.strip_prefix('"') {
179            let end = rest.find('"')?;
180            Some(rest[..end].trim().to_string())
181        } else if let Some(rest) = after.strip_prefix('\'') {
182            let end = rest.find('\'')?;
183            Some(rest[..end].trim().to_string())
184        } else {
185            let end = after
186                .find(|c: char| c.is_whitespace())
187                .unwrap_or(after.len());
188            let name = after[..end].trim();
189            if name.is_empty() {
190                None
191            } else {
192                Some(name.to_string())
193            }
194        }
195    }
196
197    /// Parse the body of one element (recursively), returning its value and the
198    /// number of bytes consumed up to and including the matching close tag.
199    /// An empty schema-declared container may omit its close immediately before
200    /// its parent closes; in that case the parent marker remains unconsumed.
201    fn parse_element_body(
202        input: &str,
203        name: &str,
204        schema: Option<&Value>,
205        parent_name: Option<&str>,
206    ) -> Option<(ParamValue, usize)> {
207        let close_tag = format!("{ELEMENT_END_START}{name}>");
208        let mut pos = 0;
209        let mut text = String::new();
210        let mut children: Vec<(String, ParamValue)> = Vec::new();
211
212        loop {
213            // Accumulate any text up to the next namespace marker; a body
214            // with no further marker is an unterminated element.
215            let rest = &input[pos..];
216            let text_chunk = &rest[..rest.find(NAMESPACE)?];
217            text.push_str(text_chunk);
218            pos += text_chunk.len();
219
220            let rest = &input[pos..];
221            if rest.starts_with(&close_tag) {
222                pos += close_tag.len();
223                break;
224            }
225            if text.trim().is_empty()
226                && children.is_empty()
227                && matches!(Self::schema_type(schema), Some("array" | "object"))
228                && parent_name.is_some_and(|parent| {
229                    rest.starts_with(&format!("{ELEMENT_END_START}{parent}>"))
230                })
231            {
232                // The parent consumes its own close; the empty child's close
233                // was omitted by the model.
234                break;
235            }
236            if rest.starts_with(ELEMENT_START) {
237                // Child element.
238                let (child_name, child_value, consumed) =
239                    Self::parse_element(rest, schema, Some(name))?;
240                children.push((child_name, child_value));
241                pos += consumed;
242                continue;
243            }
244            // A namespace marker that is neither the expected close nor a child
245            // start: malformed body.
246            return None;
247        }
248
249        let value = if children.is_empty() {
250            ParamValue::Text(text)
251        } else {
252            if !text.trim().is_empty() {
253                Self::push_mixed_text(&mut children, text);
254            }
255            ParamValue::Elements(children)
256        };
257        Some((value, pos))
258    }
259
260    /// Parse a complete element starting at `input` (which must begin with
261    /// `]<]minimax[>[<name>`). Returns `(name, value, consumed_bytes)`.
262    fn parse_element(
263        input: &str,
264        parent_schema: Option<&Value>,
265        parent_name: Option<&str>,
266    ) -> Option<(String, ParamValue, usize)> {
267        let after_start = input.strip_prefix(ELEMENT_START)?;
268        let gt = after_start.find('>')?;
269        let name = after_start[..gt].trim().to_string();
270        if name.is_empty() || name.starts_with('/') {
271            return None;
272        }
273        let body_start = ELEMENT_START.len() + gt + 1;
274        let parent_schema = Self::container_schema(parent_schema).or(parent_schema);
275        let schema = if Self::schema_type(parent_schema) == Some("array") {
276            parent_schema.and_then(|schema| schema.get("items"))
277        } else {
278            Self::property_schema(parent_schema, &name)
279        };
280        let schema = Self::container_schema(schema).or(schema);
281        let (value, body_consumed) =
282            Self::parse_element_body(&input[body_start..], &name, schema, parent_name)?;
283        Some((name, value, body_start + body_consumed))
284    }
285
286    /// Preserve mixed text content under a reserved object field, avoiding a
287    /// collision with an existing child name by prefixing `$`.
288    fn push_mixed_text(children: &mut Vec<(String, ParamValue)>, text: String) {
289        let mut field = MIXED_TEXT_FIELD.to_string();
290        while children.iter().any(|(name, _)| *name == field) {
291            field.insert(0, '$');
292        }
293        children.push((field, ParamValue::Text(text)));
294    }
295
296    /// The scalar `type` declared by a schema node, when it has one.
297    fn schema_type(schema: Option<&Value>) -> Option<&str> {
298        schema?.get("type")?.as_str()
299    }
300
301    /// Resolve a direct container or a transparent composition wrapper.
302    /// Nullable unions must have exactly one container branch; scalar/unknown
303    /// alternatives are ambiguous. Only singleton allOf wrappers are unwrapped:
304    /// choosing one of several conjuncts would discard the others' constraints.
305    fn container_schema(schema: Option<&Value>) -> Option<&Value> {
306        let schema = schema?;
307        if schema.get("type").is_some() {
308            return matches!(Self::schema_type(Some(schema)), Some("array" | "object"))
309                .then_some(schema);
310        }
311
312        let mut compositions = ["oneOf", "anyOf", "allOf"]
313            .into_iter()
314            .filter_map(|key| schema.get(key).map(|branches| (key, branches)));
315        let (keyword, branches) = compositions.next()?;
316        if compositions.next().is_some() {
317            return None;
318        }
319        // Unwrapping must not drop sibling validation or structural keywords.
320        if schema.as_object()?.keys().any(|key| {
321            key != keyword
322                && !matches!(
323                    key.as_str(),
324                    "title"
325                        | "description"
326                        | "default"
327                        | "examples"
328                        | "deprecated"
329                        | "readOnly"
330                        | "writeOnly"
331                        | "$comment"
332                )
333        }) {
334            return None;
335        }
336        let branches = branches.as_array()?;
337        if keyword == "allOf" && branches.len() != 1 {
338            return None;
339        }
340        let mut container = None;
341        for branch in branches {
342            if keyword != "allOf" && Self::schema_type(Some(branch)) == Some("null") {
343                continue;
344            }
345            let resolved = Self::container_schema(Some(branch))?;
346            if container.replace(resolved).is_some() {
347                return None;
348            }
349        }
350        container
351    }
352
353    /// Find a named property in this schema or a nested composition branch.
354    fn property_schema<'a>(schema: Option<&'a Value>, name: &str) -> Option<&'a Value> {
355        let schema = schema?;
356        if let Some(property) = schema.get("properties").and_then(|value| value.get(name)) {
357            return Some(property);
358        }
359
360        for keyword in ["oneOf", "anyOf", "allOf"] {
361            for branch in schema
362                .get(keyword)
363                .and_then(Value::as_array)
364                .into_iter()
365                .flatten()
366            {
367                if let Some(property) = Self::property_schema(Some(branch), name) {
368                    return Some(property);
369                }
370            }
371        }
372
373        None
374    }
375
376    /// Convert a parsed parameter value into a JSON value, coercing leaves by
377    /// the schema node they sit under: array elements descend into `items`,
378    /// object members into their `properties` entry.
379    fn value_to_json(value: ParamValue, schema: Option<&Value>) -> Value {
380        let schema = Self::container_schema(schema).or(schema);
381        match value {
382            ParamValue::Text(text) => {
383                let decoded = Self::decode_xml_entities(&text);
384                Self::coerce_leaf(&decoded, Self::schema_type(schema))
385            }
386            ParamValue::Elements(children) => {
387                // Repeated child names under an `array` schema (or with duplicate
388                // keys) collapse to an array; otherwise build an object.
389                if Self::schema_type(schema) == Some("array") {
390                    let items_schema = schema.and_then(|s| s.get("items"));
391                    let items = children
392                        .into_iter()
393                        .map(|(_, v)| Self::value_to_json(v, items_schema))
394                        .collect();
395                    return Value::Array(items);
396                }
397
398                let mut map: Map<String, Value> = Map::new();
399                for (name, child) in children {
400                    let child_schema = Self::property_schema(schema, &name);
401                    let child_json = Self::value_to_json(child, child_schema);
402                    match map.get_mut(&name) {
403                        Some(Value::Array(arr)) => arr.push(child_json),
404                        Some(existing) => {
405                            let prev = existing.take();
406                            *existing = Value::Array(vec![prev, child_json]);
407                        }
408                        None => {
409                            map.insert(name, child_json);
410                        }
411                    }
412                }
413                Value::Object(map)
414            }
415        }
416    }
417
418    /// Parse all parameter elements inside a complete invoke body into a JSON
419    /// object. `params_schema` is the function's `parameters` JSON schema.
420    /// `None` when an element starts but fails to parse: emitting the
421    /// arguments collected so far would run the tool with silently missing
422    /// parameters.
423    fn parse_invoke_params(body: &str, params_schema: Option<&Value>) -> Option<Value> {
424        let mut map: Map<String, Value> = Map::new();
425        let mut pos = 0;
426
427        loop {
428            let rest = &body[pos..];
429            let trimmed = rest.trim_start();
430            let trim_len = rest.len() - trimmed.len();
431            if trimmed.is_empty() {
432                break;
433            }
434            if !trimmed.starts_with(ELEMENT_START) {
435                // Tolerant: ordinary text at a parameter boundary ends the params.
436                break;
437            }
438            pos += trim_len;
439            let (name, value, consumed) = Self::parse_element(&body[pos..], params_schema, None)?;
440            pos += consumed;
441            let json = Self::value_to_json(value, Self::property_schema(params_schema, &name));
442            match map.get_mut(&name) {
443                Some(Value::Array(arr)) => arr.push(json),
444                Some(existing) => {
445                    let prev = existing.take();
446                    *existing = Value::Array(vec![prev, json]);
447                }
448                None => {
449                    map.insert(name, json);
450                }
451            }
452        }
453
454        Some(Value::Object(map))
455    }
456
457    /// Parse a single invoke block (the text between `]<]minimax[>[<invoke` and
458    /// `]<]minimax[>[</invoke>`) into a tool call.
459    fn parse_invoke(block: &str, tools: &[Tool]) -> Option<ToolCall> {
460        // `block` begins at the namespace marker for `<invoke`.
461        let after_invoke = block.strip_prefix(INVOKE_START)?;
462        let gt = after_invoke.find('>')?;
463        let header = &after_invoke[..gt];
464        let name = Self::parse_invoke_name(header)?;
465        let body = &after_invoke[gt + 1..];
466
467        let params_schema = tools
468            .iter()
469            .find(|t| t.function.name == name)
470            .map(|t| &t.function.parameters);
471        let arguments = Self::parse_invoke_params(body, params_schema)?;
472        let arguments_str = serde_json::to_string(&arguments).ok()?;
473
474        Some(ToolCall {
475            function: FunctionCall {
476                name,
477                arguments: arguments_str,
478            },
479        })
480    }
481
482    /// Parse all complete tool-call blocks in `text`.
483    /// Returns the tool calls and the byte position of the first tool-call block.
484    fn parse_tool_calls(text: &str, tools: &[Tool]) -> (Vec<ToolCall>, Option<usize>) {
485        let mut calls = Vec::new();
486        let mut first_pos = None;
487        let mut search_from = 0;
488
489        while let Some(rel_start) = text[search_from..].find(TOOL_CALL_START) {
490            let block_start = search_from + rel_start;
491            let inner_start = block_start + TOOL_CALL_START.len();
492            let Some(rel_end) = text[inner_start..].find(TOOL_CALL_END) else {
493                break;
494            };
495            let inner_end = inner_start + rel_end;
496            let inner = &text[inner_start..inner_end];
497
498            // Extract each invoke block within the tool-call wrapper.
499            let mut invoke_from = 0;
500            while let Some(rel_inv) = inner[invoke_from..].find(INVOKE_START) {
501                let inv_start = invoke_from + rel_inv;
502                let Some(rel_inv_end) = inner[inv_start..].find(INVOKE_END) else {
503                    break;
504                };
505                let inv_end = inv_start + rel_inv_end;
506                let inv_block = &inner[inv_start..inv_end];
507                if let Some(call) = Self::parse_invoke(inv_block, tools) {
508                    if first_pos.is_none() {
509                        first_pos = Some(block_start);
510                    }
511                    calls.push(call);
512                }
513                invoke_from = inv_end + INVOKE_END.len();
514            }
515
516            search_from = inner_end + TOOL_CALL_END.len();
517        }
518
519        (calls, first_pos)
520    }
521
522    /// Shared non-streaming parse. `tools` empty means infer types from text.
523    fn parse_complete_inner(text: &str, tools: &[Tool]) -> (String, Vec<ToolCall>) {
524        if !text.contains(TOOL_CALL_START) {
525            return (text.to_string(), vec![]);
526        }
527        let (calls, first_pos) = Self::parse_tool_calls(text, tools);
528        if calls.is_empty() {
529            return (text.to_string(), vec![]);
530        }
531        let normal_text = match first_pos {
532            Some(pos) => text[..pos].to_string(),
533            None => text.to_string(),
534        };
535        (normal_text, calls)
536    }
537}
538
539impl Default for MinimaxM3Parser {
540    fn default() -> Self {
541        Self::new()
542    }
543}
544
545#[async_trait]
546impl ToolParser for MinimaxM3Parser {
547    async fn parse_complete(&self, text: &str) -> ParserResult<(String, Vec<ToolCall>)> {
548        Ok(Self::parse_complete_inner(text, &[]))
549    }
550
551    async fn parse_complete_with_tools(
552        &self,
553        text: &str,
554        tools: &[Tool],
555    ) -> ParserResult<(String, Vec<ToolCall>)> {
556        Ok(Self::parse_complete_inner(text, tools))
557    }
558
559    async fn parse_incremental(
560        &mut self,
561        chunk: &str,
562        tools: &[Tool],
563    ) -> ParserResult<StreamingParseResult> {
564        self.buffer.push_str(chunk);
565        let mut normal_text = String::new();
566        let mut calls = Vec::new();
567
568        loop {
569            // Outside a tool call: emit normal text until a start token appears.
570            if !self.in_tool_call {
571                if let Some(start) = self.buffer.find(TOOL_CALL_START) {
572                    normal_text.push_str(&self.buffer[..start]);
573                    self.buffer = self.buffer[start..].to_string();
574                    self.in_tool_call = true;
575                    continue;
576                }
577
578                // No start token: flush text, holding back a potential partial token.
579                if let Some(partial_len) =
580                    Self::longest_partial_suffix(&self.buffer, TOOL_CALL_START)
581                {
582                    let end = self.buffer.len() - partial_len;
583                    normal_text.push_str(&self.buffer[..end]);
584                    self.buffer = self.buffer[end..].to_string();
585                } else {
586                    normal_text.push_str(&self.buffer);
587                    self.buffer.clear();
588                }
589                break;
590            }
591
592            // Inside a tool call: wait for the complete end token before emitting.
593            let Some(end_rel) = self.buffer.find(TOOL_CALL_END) else {
594                if !Self::could_start_invoke(&self.buffer) {
595                    // Release the false wrapper, then resume the normal-text
596                    // scan so a later marker (including a partial one) is still
597                    // recognized rather than flushed as ordinary content.
598                    normal_text.push_str(TOOL_CALL_START);
599                    self.buffer.drain(..TOOL_CALL_START.len());
600                    self.in_tool_call = false;
601                    continue;
602                }
603                break;
604            };
605            let block_end = end_rel + TOOL_CALL_END.len();
606            let block = self.buffer[..block_end].to_string();
607            self.buffer = self.buffer[block_end..].to_string();
608            self.in_tool_call = false;
609
610            let (block_calls, _) = Self::parse_tool_calls(&block, tools);
611            if block_calls.is_empty() {
612                // No invoke parsed: match the non-streaming path, which
613                // returns malformed blocks as text rather than dropping them.
614                normal_text.push_str(&block);
615                continue;
616            }
617            for call in block_calls {
618                if self.current_tool_id == -1 {
619                    self.current_tool_id = 0;
620                } else {
621                    self.current_tool_id += 1;
622                }
623                let tool_id = self.current_tool_id as usize;
624                helpers::ensure_capacity(
625                    self.current_tool_id,
626                    &mut self.prev_tool_call_arr,
627                    &mut self.streamed_args_for_tool,
628                );
629
630                let args = call.function.arguments.clone();
631                if tool_id < self.streamed_args_for_tool.len() {
632                    self.streamed_args_for_tool[tool_id].clone_from(&args);
633                }
634                let parsed_args: Value =
635                    serde_json::from_str(&args).unwrap_or_else(|_| Value::Object(Map::new()));
636                if tool_id < self.prev_tool_call_arr.len() {
637                    self.prev_tool_call_arr[tool_id] = serde_json::json!({
638                        "name": call.function.name,
639                        "arguments": parsed_args,
640                    });
641                }
642
643                // Emit name then full arguments for this completed invoke.
644                calls.push(ToolCallItem {
645                    tool_index: tool_id,
646                    name: Some(call.function.name),
647                    parameters: String::new(),
648                });
649                calls.push(ToolCallItem {
650                    tool_index: tool_id,
651                    name: None,
652                    parameters: args,
653                });
654            }
655        }
656
657        Ok(StreamingParseResult { normal_text, calls })
658    }
659
660    fn has_tool_markers(&self, text: &str) -> bool {
661        text.contains(TOOL_CALL_START)
662    }
663
664    fn get_unstreamed_tool_args(&self) -> Option<Vec<ToolCallItem>> {
665        helpers::get_unstreamed_args(&self.prev_tool_call_arr, &self.streamed_args_for_tool)
666    }
667
668    fn take_unstreamed_normal_text(&mut self) -> String {
669        // Completed blocks are removed from `buffer`, so anything left here is
670        // an independent, incomplete candidate and must be returned verbatim.
671        // Leave the parser ready to process ordinary text if it is reused.
672        self.in_tool_call = false;
673        std::mem::take(&mut self.buffer)
674    }
675
676    fn reset(&mut self) {
677        self.buffer.clear();
678        self.prev_tool_call_arr.clear();
679        self.current_tool_id = -1;
680        self.streamed_args_for_tool.clear();
681        self.in_tool_call = false;
682    }
683}