@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 396c8fee50
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 2, 18:15 UTC

[Rust Frontend] Align tool parser fallback behavior between streaming & non-streaming paths (#43662)

Signed-off-by: Bugen Zhao <i@bugenzhao.com>

Bugen Zhao committed May 27, 2026 at 18:13 UTC 396c8fee5071117914104244cc962db805ab0af0
26 files changed +1187 -899
rust/Cargo.lock
+1
@@ -5850,6 +5850,7 @@ name = "vllm-tool-parser"
5850 version = "0.1.0"
5851 dependencies = [
5852 "criterion",
5853 + "easy-ext",
5854 "expect-test",
5855 "futures",
5856 "openai-protocol",
rust/src/chat/src/output/default/tool.rs
+289 -55
@@ -15,7 +15,7 @@ use crate::Result;
15 use crate::error::Error;
16 use crate::event::AssistantBlockKind;
17 use crate::output::generate_tool_call_id;
18 -use crate::parser::tool::{ToolCallDelta, ToolParseResult, ToolParser};
18 +use crate::parser::tool::{ToolCallDelta, ToolParser, ToolParserOutput};
19
20 /// Per-stream tool parsing state.
21 struct ToolState {
@@ -57,46 +57,52 @@ impl ToolState {
57 return Ok(events);
58 }
59
60 - let parse_result = self.parser.push(&delta);
60 + let mut output = ToolParserOutput::default();
61 + let parse_result = self.parser.parse_into(&delta, &mut output);
62
63 match parse_result {
63 - Ok(result) => self.process_parse_result(kind, result, &mut events)?,
64 + Ok(()) => self.process_parser_output(kind, output, &mut events)?,
65 Err(error) => {
65 - if !self.parser_failed {
66 - warn!(
67 - error = %error.as_report(),
68 - "tool parser failed; falling back to plain text deltas"
69 - );
70 - self.parser_failed = true;
71 - }
66 + warn!(
67 + error = %error.as_report(),
68 + "tool parser failed; falling back to plain text deltas"
69 + );
70 + // Permanently mark this parser as failed.
71 + // TODO: we may consider recovering from parsing errors in the future.
72 + self.parser_failed = true;
73 +
74 + // On parsing failure, we still apply the partial parser output if any, but we close
75 + // any open tool calls and emit the remaining buffered text as a plain-text delta to
76 + // preserve as much of the output as possible.
77 + self.process_parser_output(kind, output, &mut events)?;
78 self.open_call_index = None;
73 - events.push(AssistantEvent::TextDelta { kind, delta });
79 + push_text_delta(&mut events, kind, self.parser.reset());
80 }
81 }
82
83 Ok(events)
84 }
85
80 - /// Apply one parsed tool result to the current stream state.
81 - fn process_parse_result(
86 + /// Apply one parsed tool output to the current stream state.
87 + fn process_parser_output(
88 &mut self,
89 kind: AssistantBlockKind,
84 - result: ToolParseResult,
90 + output: ToolParserOutput,
91 events: &mut Vec<AssistantEvent>,
92 ) -> Result<()> {
93 // When we are not currently streaming a tool call, preserve plain
94 // text first and then surface any new tool call items.
95 if self.open_call_index.is_none() {
90 - push_text_delta(events, kind, result.normal_text);
91 - self.process_tool_items(result.calls, events)?;
96 + push_text_delta(events, kind, output.normal_text);
97 + self.process_tool_items(output.calls, events)?;
98 } else {
99 // Once a tool call is open, prioritize tool deltas first. If the
100 // parser emits normal text again, close the tool call and resume
101 // plain text output.
96 - self.process_tool_items(result.calls, events)?;
97 - if !result.normal_text.is_empty() {
102 + self.process_tool_items(output.calls, events)?;
103 + if !output.normal_text.is_empty() {
104 self.open_call_index = None;
99 - push_text_delta(events, kind, result.normal_text);
105 + push_text_delta(events, kind, output.normal_text);
106 }
107 }
108 Ok(())
@@ -158,8 +164,8 @@ impl ToolState {
164 }
165
166 match self.parser.finish() {
161 - Ok(result) => {
162 - self.process_parse_result(AssistantBlockKind::Text, result, &mut events)?
167 + Ok(output) => {
168 + self.process_parser_output(AssistantBlockKind::Text, output, &mut events)?
169 }
170 Err(error) => {
171 warn!(
@@ -265,17 +271,24 @@ mod tests {
271 use crate::error::Error;
272 use crate::event::{AssistantBlockKind, AssistantMessageExt as _};
273 use crate::output::structured::structured_chat_event_stream;
268 - use crate::parser::tool::{ToolParseResult, ToolParser, ToolParserError};
274 + use crate::parser::tool::{
275 + DeepSeekV4ToolParser, ToolParser, ToolParserError, ToolParserOutput,
276 + };
277 use crate::request::ChatTool;
270 - use crate::stream::ChatEventStream;
278 + use crate::stream::{ChatEventStream, CollectedAssistantMessage};
279
280 struct FailingParser {
281 fail_next: bool,
282 + buffered: String,
283 }
284
285 struct ScriptedParser {
277 - push_results: Vec<ToolParseResult>,
278 - finish_result: ToolParseResult,
286 + push_outputs: Vec<ToolParserOutput>,
287 + finish_output: ToolParserOutput,
288 + }
289 +
290 + struct PartialThenFailParser {
291 + buffered: String,
292 }
293
294 impl ToolParser for FailingParser {
@@ -283,10 +296,14 @@ mod tests {
296 where
297 Self: Sized + 'static,
298 {
286 - Ok(Box::new(Self { fail_next: false }))
299 + Ok(Box::new(Self {
300 + fail_next: false,
301 + buffered: String::new(),
302 + }))
303 }
304
289 - fn push(&mut self, _chunk: &str) -> Result<ToolParseResult> {
305 + fn parse_into(&mut self, chunk: &str, _output: &mut ToolParserOutput) -> Result<()> {
306 + self.buffered.push_str(chunk);
307 if self.fail_next {
308 self.fail_next = false;
309 return Err(ToolParserError::ParsingFailed {
@@ -294,7 +311,16 @@ mod tests {
311 });
312 }
313
297 - Ok(ToolParseResult::default())
314 + self.buffered.clear();
315 + Ok(())
316 + }
317 +
318 + fn finish(&mut self) -> Result<ToolParserOutput> {
319 + Ok(ToolParserOutput::default())
320 + }
321 +
322 + fn reset(&mut self) -> String {
323 + std::mem::take(&mut self.buffered)
324 }
325 }
326
@@ -304,20 +330,217 @@ mod tests {
330 Self: Sized + 'static,
331 {
332 Ok(Box::new(Self {
307 - push_results: Vec::new(),
308 - finish_result: ToolParseResult::default(),
333 + push_outputs: Vec::new(),
334 + finish_output: ToolParserOutput::default(),
335 }))
336 }
337
312 - fn push(&mut self, _chunk: &str) -> Result<ToolParseResult> {
313 - Ok(self.push_results.pop().unwrap_or_default())
338 + fn parse_into(&mut self, _chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
339 + let mut next = self.push_outputs.pop().unwrap_or_default();
340 + output.normal_text.push_str(&next.normal_text);
341 + output.calls.append(&mut next.calls);
342 + Ok(())
343 + }
344 +
345 + fn finish(&mut self) -> Result<ToolParserOutput> {
346 + Ok(std::mem::take(&mut self.finish_output))
347 }
348
316 - fn finish(&mut self) -> Result<ToolParseResult> {
317 - Ok(std::mem::take(&mut self.finish_result))
349 + fn reset(&mut self) -> String {
350 + String::new()
351 }
352 }
353
354 + impl ToolParser for PartialThenFailParser {
355 + fn create(_tools: &[ChatTool]) -> vllm_tool_parser::Result<Box<dyn ToolParser>>
356 + where
357 + Self: Sized + 'static,
358 + {
359 + Ok(Box::new(Self {
360 + buffered: String::new(),
361 + }))
362 + }
363 +
364 + fn parse_into(&mut self, _chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
365 + output.calls.extend([
366 + crate::parser::tool::ToolCallDelta {
367 + tool_index: 0,
368 + name: Some("get_weather".to_string()),
369 + arguments: String::new(),
370 + },
371 + crate::parser::tool::ToolCallDelta {
372 + tool_index: 0,
373 + name: None,
374 + arguments: r#"{"location":"SF"}"#.to_string(),
375 + },
376 + ]);
377 + self.buffered.push_str(" trailing text");
378 + Err(ToolParserError::ParsingFailed {
379 + message: "boom".to_string(),
380 + })
381 + }
382 +
383 + fn finish(&mut self) -> Result<ToolParserOutput> {
384 + Ok(ToolParserOutput::default())
385 + }
386 +
387 + fn reset(&mut self) -> String {
388 + std::mem::take(&mut self.buffered)
389 + }
390 + }
391 +
392 + fn deepseek_v4_test_tools() -> Vec<ChatTool> {
393 + vec![
394 + ChatTool {
395 + name: "get_weather".to_string(),
396 + description: None,
397 + parameters: serde_json::json!({
398 + "type": "object",
399 + "properties": {
400 + "location": { "type": "string" }
401 + }
402 + }),
403 + strict: None,
404 + },
405 + ChatTool {
406 + name: "add".to_string(),
407 + description: None,
408 + parameters: serde_json::json!({
409 + "type": "object",
410 + "properties": {
411 + "x": { "type": "integer" },
412 + "y": { "type": "integer" }
413 + }
414 + }),
415 + strict: None,
416 + },
417 + ]
418 + }
419 +
420 + async fn collect_deepseek_v4_message(chunks: Vec<String>) -> CollectedAssistantMessage {
421 + let events = chunks
422 + .into_iter()
423 + .map(|delta| {
424 + Ok(ContentEvent::TextDelta {
425 + kind: AssistantBlockKind::Text,
426 + delta,
427 + })
428 + })
429 + .chain(std::iter::once(Ok(ContentEvent::Done {
430 + prompt_token_count: 1,
431 + output_token_count: 1,
432 + finish_reason: FinishReason::stop_eos(),
433 + kv_transfer_params: None,
434 + })));
435 + let parser = DeepSeekV4ToolParser::create(&deepseek_v4_test_tools()).unwrap();
436 + let assistant_events = tool_event_stream(stream::iter(events), Some(parser));
437 + let chat_events = structured_chat_event_stream(assistant_events);
438 +
439 + ChatEventStream::new("req_deepseek_v4".to_string(), Box::pin(chat_events))
440 + .collect_message()
441 + .await
442 + .unwrap()
443 + }
444 +
445 + fn message_tool_projection(
446 + message: &CollectedAssistantMessage,
447 + ) -> (String, Vec<(String, serde_json::Value)>) {
448 + (
449 + message.message.text(),
450 + message
451 + .message
452 + .tool_calls()
453 + .map(|call| {
454 + (
455 + call.name.clone(),
456 + serde_json::from_str(&call.arguments).unwrap(),
457 + )
458 + })
459 + .collect(),
460 + )
461 + }
462 +
463 + #[tokio::test]
464 + async fn tool_parser_error_preserves_partial_output_and_flushes_buffer() {
465 + let events = stream::iter(vec![
466 + Ok(ContentEvent::TextDelta {
467 + kind: AssistantBlockKind::Text,
468 + delta: "ignored".to_string(),
469 + }),
470 + Ok(ContentEvent::Done {
471 + prompt_token_count: 1,
472 + output_token_count: 1,
473 + finish_reason: FinishReason::stop_eos(),
474 + kv_transfer_params: None,
475 + }),
476 + ]);
477 +
478 + let events = tool_event_stream(
479 + events,
480 + Some(Box::new(PartialThenFailParser {
481 + buffered: String::new(),
482 + })),
483 + )
484 + .collect::<Vec<_>>()
485 + .await
486 + .into_iter()
487 + .collect::<crate::Result<Vec<_>>>()
488 + .unwrap();
489 +
490 + assert!(matches!(
491 + &events[0],
492 + AssistantEvent::ToolCallStart { name, .. } if name == "get_weather"
493 + ));
494 + assert!(matches!(
495 + &events[1],
496 + AssistantEvent::ToolCallArgumentsDelta { delta } if delta == r#"{"location":"SF"}"#
497 + ));
498 + assert_eq!(
499 + events[2],
500 + AssistantEvent::TextDelta {
501 + kind: AssistantBlockKind::Text,
502 + delta: " trailing text".to_string(),
503 + }
504 + );
505 + assert!(matches!(events[3], AssistantEvent::Done { .. }));
506 + }
507 +
508 + #[tokio::test]
509 + async fn real_buffered_parser_error_matches_streaming_and_non_streaming() {
510 + let prefix = "I will check both.\n";
511 + let first_tool_call = concat!(
512 + "<|DSML|tool_calls>\n",
513 + "<|DSML|invoke name=\"get_weather\">\n",
514 + "<|DSML|parameter name=\"location\" string=\"true\">Tokyo</|DSML|parameter>\n",
515 + "</|DSML|invoke>",
516 + );
517 + let malformed_second_tool_call = concat!(
518 + "\n<|DSML|invoke name=\"add\">\n",
519 + "not a parameter\n",
520 + "</|DSML|invoke>\n",
521 + "</|DSML|tool_calls>",
522 + );
523 + let streaming_chunks = vec![
524 + prefix.to_string(),
525 + first_tool_call.to_string(),
526 + malformed_second_tool_call.to_string(),
527 + ];
528 + let full_output = streaming_chunks.concat();
529 +
530 + let streaming = collect_deepseek_v4_message(streaming_chunks).await;
531 + let non_streaming = collect_deepseek_v4_message(vec![full_output]).await;
532 +
533 + let expected = (
534 + format!("{prefix}{malformed_second_tool_call}"),
535 + vec![(
536 + "get_weather".to_string(),
537 + serde_json::json!({ "location": "Tokyo" }),
538 + )],
539 + );
540 + assert_eq!(message_tool_projection(&streaming), expected);
541 + assert_eq!(message_tool_projection(&non_streaming), expected);
542 + }
543 +
544 #[tokio::test]
545 async fn tool_parser_failure_falls_back_to_plain_text() {
546 let events = stream::iter(vec![
@@ -341,10 +564,15 @@ mod tests {
564 }),
565 ]);
566
344 - let collected =
345 - tool_event_stream(events, Some(Box::new(FailingParser { fail_next: true })))
346 - .collect::<Vec<_>>()
347 - .await;
567 + let collected = tool_event_stream(
568 + events,
569 + Some(Box::new(FailingParser {
570 + fail_next: true,
571 + buffered: String::new(),
572 + })),
573 + )
574 + .collect::<Vec<_>>()
575 + .await;
576
577 let events = collected
578 .into_iter()
@@ -415,12 +643,18 @@ mod tests {
643 kv_transfer_params: None,
644 }),
645 ]);
418 - let events = tool_event_stream(events, Some(Box::new(FailingParser { fail_next: false })))
419 - .collect::<Vec<_>>()
420 - .await
421 - .into_iter()
422 - .collect::<crate::Result<Vec<_>>>()
423 - .unwrap();
646 + let events = tool_event_stream(
647 + events,
648 + Some(Box::new(FailingParser {
649 + fail_next: false,
650 + buffered: String::new(),
651 + })),
652 + )
653 + .collect::<Vec<_>>()
654 + .await
655 + .into_iter()
656 + .collect::<crate::Result<Vec<_>>>()
657 + .unwrap();
658
659 assert_eq!(
660 events,
@@ -468,7 +702,7 @@ mod tests {
702 ]);
703
704 let parser = ScriptedParser {
471 - push_results: vec![ToolParseResult {
705 + push_outputs: vec![ToolParserOutput {
706 normal_text: String::new(),
707 calls: vec![
708 crate::parser::tool::ToolCallDelta {
@@ -483,14 +717,14 @@ mod tests {
717 },
718 ],
719 }],
486 - finish_result: ToolParseResult::default(),
720 + finish_output: ToolParserOutput::default(),
721 };
722
723 let err = tool_event_stream(events, Some(Box::new(parser)))
724 .collect::<Vec<_>>()
725 .await
726 .into_iter()
493 - .find_map(|result| result.err())
727 + .find_map(|output| output.err())
728 .expect("expected invariant error");
729
730 assert!(matches!(err, Error::ToolCallStreamInvariant { .. }));
@@ -514,8 +748,8 @@ mod tests {
748 ]);
749
750 let parser = ScriptedParser {
517 - push_results: vec![
518 - ToolParseResult {
751 + push_outputs: vec![
752 + ToolParserOutput {
753 normal_text: String::new(),
754 calls: vec![crate::parser::tool::ToolCallDelta {
755 tool_index: 0,
@@ -523,11 +757,11 @@ mod tests {
757 arguments: "}".to_string(),
758 }],
759 },
526 - ToolParseResult {
760 + ToolParserOutput {
761 normal_text: "plain text".to_string(),
762 calls: Vec::new(),
763 },
530 - ToolParseResult {
764 + ToolParserOutput {
765 normal_text: String::new(),
766 calls: vec![crate::parser::tool::ToolCallDelta {
767 tool_index: 0,
@@ -536,14 +770,14 @@ mod tests {
770 }],
771 },
772 ],
539 - finish_result: ToolParseResult::default(),
773 + finish_output: ToolParserOutput::default(),
774 };
775
776 let err = tool_event_stream(events, Some(Box::new(parser)))
777 .collect::<Vec<_>>()
778 .await
779 .into_iter()
546 - .find_map(|result| result.err())
780 + .find_map(|output| output.err())
781 .expect("expected invariant error");
782
783 assert!(matches!(
@@ -573,7 +807,7 @@ mod tests {
807 ]);
808
809 let parser = ScriptedParser {
576 - push_results: vec![ToolParseResult {
810 + push_outputs: vec![ToolParserOutput {
811 normal_text: String::new(),
812 calls: vec![
813 crate::parser::tool::ToolCallDelta {
@@ -588,7 +822,7 @@ mod tests {
822 },
823 ],
824 }],
591 - finish_result: ToolParseResult::default(),
825 + finish_output: ToolParserOutput::default(),
826 };
827
828 let events = tool_event_stream(events, Some(Box::new(parser)))
rust/src/chat/src/parser/tool/mod.rs
+1 -1
@@ -6,7 +6,7 @@ pub use vllm_tool_parser::{
6 DeepSeekV3ToolParser, DeepSeekV4ToolParser, DeepSeekV31ToolParser, DeepSeekV32ToolParser,
7 Gemma4ToolParser, Glm45MoeToolParser, Glm47MoeToolParser, HermesToolParser, KimiK2ToolParser,
8 Llama3JsonToolParser, MinimaxM2ToolParser, MistralToolParser, Qwen3CoderToolParser,
9 - Qwen3XmlToolParser, ToolCallDelta, ToolParseResult, ToolParser, ToolParserError,
9 + Qwen3XmlToolParser, ToolCallDelta, ToolParser, ToolParserError, ToolParserOutput,
10 };
11
12 use crate::parser::ParserFactory;
rust/src/chat/src/parser/tool/tests.rs
+11 -3
@@ -1,6 +1,6 @@
1 use vllm_tool_parser::Result;
2
3 -use super::{ToolParseResult, ToolParser, ToolParserFactory, names};
3 +use super::{ToolParser, ToolParserFactory, ToolParserOutput, names};
4 use crate::Error;
5 use crate::request::ChatTool;
6
@@ -18,8 +18,16 @@ impl ToolParser for FakeToolParser {
18 true
19 }
20
21 - fn push(&mut self, _chunk: &str) -> Result<ToolParseResult> {
22 - Ok(ToolParseResult::default())
21 + fn parse_into(&mut self, _chunk: &str, _output: &mut ToolParserOutput) -> Result<()> {
22 + Ok(())
23 + }
24 +
25 + fn finish(&mut self) -> Result<ToolParserOutput> {
26 + Ok(ToolParserOutput::default())
27 + }
28 +
29 + fn reset(&mut self) -> String {
30 + String::new()
31 }
32 }
33
rust/src/tool-parser/Cargo.toml
+1
@@ -8,6 +8,7 @@ license.workspace = true
8 test-util = []
9
10 [dependencies]
11 +easy-ext.workspace = true
12 serde.workspace = true
13 serde_json.workspace = true
14 thiserror.workspace = true
rust/src/tool-parser/src/deepseek_dsml/deepseek_v32.rs
+71 -69
@@ -1,5 +1,5 @@
1 use super::{DeepSeekDsmlToolParser, DsmlTokens};
2 -use crate::{Result, Tool, ToolParseResult, ToolParser};
2 +use crate::{Result, Tool, ToolParser, ToolParserOutput};
3
4 /// Tool parser for DeepSeek V3.2 models.
5 ///
@@ -33,7 +33,6 @@ impl DeepSeekV32ToolParser {
33 }
34
35 impl ToolParser for DeepSeekV32ToolParser {
36 - /// Create a boxed DeepSeek V3.2 tool parser.
36 fn create(tools: &[Tool]) -> Result<Box<dyn ToolParser>>
37 where
38 Self: Sized + 'static,
@@ -41,20 +40,21 @@ impl ToolParser for DeepSeekV32ToolParser {
40 Ok(Box::new(Self::new(tools)))
41 }
42
44 - /// Preserve DSML special tokens while decoding.
43 fn preserve_special_tokens(&self) -> bool {
44 true
45 }
46
49 - /// Push one decoded text chunk through the DSML parser.
50 - fn push(&mut self, chunk: &str) -> Result<ToolParseResult> {
51 - self.0.push(chunk)
47 + fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
48 + self.0.parse_into(chunk, output)
49 }
50
54 - /// Flush buffered text and reset parser state.
55 - fn finish(&mut self) -> Result<ToolParseResult> {
51 + fn finish(&mut self) -> Result<ToolParserOutput> {
52 self.0.finish()
53 }
54 +
55 + fn reset(&mut self) -> String {
56 + self.0.reset()
57 + }
58 }
59
60 #[cfg(test)]
@@ -63,8 +63,8 @@ mod tests {
63 use thiserror_ext::AsReport;
64
65 use super::DeepSeekV32ToolParser;
66 - use crate::ToolParser;
66 use crate::test_utils::{collect_stream, split_by_chars, test_tools};
67 + use crate::{ToolParser, ToolParserTestExt as _};
68
69 fn build_tool_call(function_name: &str, params: &[(&str, &str)]) -> String {
70 let params = params
@@ -84,27 +84,27 @@ mod tests {
84 #[test]
85 fn deepseek_v32_parse_complete_without_tool_call_keeps_text() {
86 let mut parser = DeepSeekV32ToolParser::new(&test_tools());
87 - let result = parser.parse_complete("Hello, world!").unwrap();
87 + let output = parser.parse_complete("Hello, world!").unwrap();
88
89 - assert_eq!(result.normal_text, "Hello, world!");
90 - assert!(result.calls.is_empty());
89 + assert_eq!(output.normal_text, "Hello, world!");
90 + assert!(output.calls.is_empty());
91 }
92
93 #[test]
94 fn deepseek_v32_parse_complete_extracts_single_tool_call() {
95 let mut parser = DeepSeekV32ToolParser::new(&test_tools());
96 - let result = parser
96 + let output = parser
97 .parse_complete(&build_tool_call(
98 "get_weather",
99 &[("location", "SF"), ("date", "2024-01-16")],
100 ))
101 .unwrap();
102
103 - assert!(result.normal_text.is_empty());
104 - assert_eq!(result.calls.len(), 1);
105 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
103 + assert!(output.normal_text.is_empty());
104 + assert_eq!(output.calls.len(), 1);
105 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
106 assert_eq!(
107 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
107 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
108 json!({
109 "location": "SF",
110 "date": "2024-01-16"
@@ -119,16 +119,16 @@ mod tests {
119 "Thinking... {}",
120 build_tool_call("get_weather", &[("location", "NYC")])
121 );
122 - let result = parser.parse_complete(&output).unwrap();
122 + let output = parser.parse_complete(&output).unwrap();
123
124 - assert_eq!(result.normal_text, "Thinking... ");
125 - assert_eq!(result.calls.len(), 1);
124 + assert_eq!(output.normal_text, "Thinking... ");
125 + assert_eq!(output.calls.len(), 1);
126 }
127
128 #[test]
129 fn deepseek_v32_parse_complete_converts_schema_types() {
130 let mut parser = DeepSeekV32ToolParser::new(&test_tools());
131 - let result = parser
131 + let output = parser
132 .parse_complete(
133 "<|DSML|function_calls>\n\
134 <|DSML|invoke name=\"convert\">\n\
@@ -142,9 +142,9 @@ mod tests {
142 )
143 .unwrap();
144
145 - assert_eq!(result.calls.len(), 1);
145 + assert_eq!(output.calls.len(), 1);
146 assert_eq!(
147 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
147 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
148 json!({
149 "whole": 5.0,
150 "flag": true,
@@ -158,7 +158,7 @@ mod tests {
158 #[test]
159 fn deepseek_v32_parse_complete_string_attr_overrides_schema_types() {
160 let mut parser = DeepSeekV32ToolParser::new(&test_tools());
161 - let result = parser
161 + let output = parser
162 .parse_complete(
163 "<|DSML|function_calls>\n\
164 <|DSML|invoke name=\"convert\">\n\
@@ -172,9 +172,9 @@ mod tests {
172 )
173 .unwrap();
174
175 - assert_eq!(result.calls.len(), 1);
175 + assert_eq!(output.calls.len(), 1);
176 assert_eq!(
177 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
177 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
178 json!({
179 "whole": "5.0",
180 "flag": "true",
@@ -188,7 +188,7 @@ mod tests {
188 #[test]
189 fn deepseek_v32_parse_complete_unescapes_literal_closing_tags_in_parameter_value() {
190 let mut parser = DeepSeekV32ToolParser::new(&test_tools());
191 - let result = parser
191 + let output = parser
192 .parse_complete(&build_tool_call(
193 "get_weather",
194 &[
@@ -202,7 +202,7 @@ mod tests {
202 .unwrap();
203
204 assert_eq!(
205 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
205 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
206 json!({
207 "location": "Hangzhou </|DSML|parameter></|DSML|invoke></|DSML|function_calls>",
208 "date": "2026-05-08",
@@ -213,7 +213,7 @@ mod tests {
213 #[test]
214 fn deepseek_v32_streaming_extracts_single_tool_call() {
215 let mut parser = DeepSeekV32ToolParser::new(&test_tools());
216 - let result = collect_stream(
216 + let output = collect_stream(
217 &mut parser,
218 &[
219 "<|DSML|function_calls>\n",
@@ -224,11 +224,11 @@ mod tests {
224 ],
225 );
226
227 - assert!(result.normal_text.is_empty());
228 - assert_eq!(result.calls.len(), 1);
229 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
227 + assert!(output.normal_text.is_empty());
228 + assert_eq!(output.calls.len(), 1);
229 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
230 assert_eq!(
231 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
231 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
232 json!({ "location": "SF" })
233 );
234 }
@@ -236,7 +236,7 @@ mod tests {
236 #[test]
237 fn deepseek_v32_streaming_preserves_prefix_text() {
238 let mut parser = DeepSeekV32ToolParser::new(&test_tools());
239 - let result = collect_stream(
239 + let output = collect_stream(
240 &mut parser,
241 &[
242 "Thinking... ",
@@ -248,23 +248,23 @@ mod tests {
248 ],
249 );
250
251 - assert_eq!(result.normal_text, "Thinking... ");
252 - assert_eq!(result.calls.len(), 1);
251 + assert_eq!(output.normal_text, "Thinking... ");
252 + assert_eq!(output.calls.len(), 1);
253 }
254
255 #[test]
256 fn deepseek_v32_streaming_without_tool_call_emits_text_incrementally() {
257 let mut parser = DeepSeekV32ToolParser::new(&test_tools());
258 - let result = collect_stream(&mut parser, &["Hello, ", "world!"]);
258 + let output = collect_stream(&mut parser, &["Hello, ", "world!"]);
259
260 - assert_eq!(result.normal_text, "Hello, world!");
261 - assert!(result.calls.is_empty());
260 + assert_eq!(output.normal_text, "Hello, world!");
261 + assert!(output.calls.is_empty());
262 }
263
264 #[test]
265 fn deepseek_v32_streaming_extracts_multiple_tool_calls_in_order() {
266 let mut parser = DeepSeekV32ToolParser::new(&test_tools());
267 - let result = collect_stream(
267 + let output = collect_stream(
268 &mut parser,
269 &[&format!(
270 "{}\n{}",
@@ -274,17 +274,17 @@ mod tests {
274 )],
275 );
276
277 - assert_eq!(result.calls.len(), 2);
278 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
279 - assert_eq!(result.calls[1].name.as_deref(), Some("get_weather"));
280 - assert_eq!(result.calls[0].tool_index, 0);
281 - assert_eq!(result.calls[1].tool_index, 1);
277 + assert_eq!(output.calls.len(), 2);
278 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
279 + assert_eq!(output.calls[1].name.as_deref(), Some("get_weather"));
280 + assert_eq!(output.calls[0].tool_index, 0);
281 + assert_eq!(output.calls[1].tool_index, 1);
282 assert_eq!(
283 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
283 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
284 json!({ "location": "SF" })
285 );
286 assert_eq!(
287 - serde_json::from_str::<Value>(&result.calls[1].arguments).unwrap(),
287 + serde_json::from_str::<Value>(&output.calls[1].arguments).unwrap(),
288 json!({ "location": "NYC" })
289 );
290 }
@@ -294,11 +294,11 @@ mod tests {
294 let text = build_tool_call("get_weather", &[("location", "SF")]);
295 let chunks = split_by_chars(&text, 5);
296 let mut parser = DeepSeekV32ToolParser::new(&test_tools());
297 - let result = collect_stream(&mut parser, &chunks);
297 + let output = collect_stream(&mut parser, &chunks);
298
299 - assert_eq!(result.calls.len(), 1);
299 + assert_eq!(output.calls.len(), 1);
300 assert_eq!(
301 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
301 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
302 json!({ "location": "SF" })
303 );
304 }
@@ -306,7 +306,7 @@ mod tests {
306 #[test]
307 fn deepseek_v32_streaming_handles_bpe_chunked_dsml_opener() {
308 let mut parser = DeepSeekV32ToolParser::new(&test_tools());
309 - let result = collect_stream(
309 + let output = collect_stream(
310 &mut parser,
311 &[
312 "<|DSML|",
@@ -333,11 +333,11 @@ mod tests {
333 ],
334 );
335
336 - assert!(result.normal_text.is_empty());
337 - assert_eq!(result.calls.len(), 1);
338 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
336 + assert!(output.normal_text.is_empty());
337 + assert_eq!(output.calls.len(), 1);
338 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
339 assert_eq!(
340 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
340 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
341 json!({ "location": "Beijing" })
342 );
343 }
@@ -345,12 +345,12 @@ mod tests {
345 #[test]
346 fn deepseek_v32_streaming_truncated_parameter_does_not_leak_eos() {
347 let mut parser = DeepSeekV32ToolParser::new(&test_tools());
348 - parser.push("<|DSML|function_calls>\n").unwrap();
349 - parser.push("<|DSML|invoke name=\"get_weather\">\n").unwrap();
348 + parser.parse_chunk("<|DSML|function_calls>\n").unwrap();
349 + parser.parse_chunk("<|DSML|invoke name=\"get_weather\">\n").unwrap();
350 parser
351 - .push("<|DSML|parameter name=\"location\" string=\"true\">Tokyo")
351 + .parse_chunk("<|DSML|parameter name=\"location\" string=\"true\">Tokyo")
352 .unwrap();
353 - parser.push("<|end▁of▁sentence|>").unwrap();
353 + parser.parse_chunk("<|end▁of▁sentence|>").unwrap();
354
355 let error = parser.finish().unwrap_err();
356 assert!(error.to_report_string().contains("incomplete DeepSeek DSML tool call"));
@@ -358,7 +358,7 @@ mod tests {
358 #[test]
359 fn deepseek_v32_streaming_drops_eos_after_complete_tool_calls() {
360 let mut parser = DeepSeekV32ToolParser::new(&test_tools());
361 - let result = collect_stream(
361 + let output = collect_stream(
362 &mut parser,
363 &[
364 "<|DSML|function_calls>\n",
@@ -369,15 +369,15 @@ mod tests {
369 ],
370 );
371
372 - assert!(result.normal_text.is_empty());
373 - assert_eq!(result.calls.len(), 1);
374 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
372 + assert!(output.normal_text.is_empty());
373 + assert_eq!(output.calls.len(), 1);
374 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
375 }
376
377 #[test]
378 fn deepseek_v32_streaming_ignores_text_after_complete_tool_calls() {
379 let mut parser = DeepSeekV32ToolParser::new(&test_tools());
380 - let result = collect_stream(
380 + let output = collect_stream(
381 &mut parser,
382 &[
383 "<|DSML|function_calls>\n",
@@ -389,17 +389,19 @@ mod tests {
389 ],
390 );
391
392 - assert!(result.normal_text.is_empty());
393 - assert_eq!(result.calls.len(), 1);
392 + assert!(output.normal_text.is_empty());
393 + assert_eq!(output.calls.len(), 1);
394 }
395
396 #[test]
397 fn deepseek_v32_streaming_does_not_emit_incomplete_invoke() {
398 let mut parser = DeepSeekV32ToolParser::new(&test_tools());
399 - parser.push("<|DSML|function_calls>\n").unwrap();
400 - parser.push("<|DSML|invoke name=\"get_weather\">\n").unwrap();
399 + parser.parse_chunk("<|DSML|function_calls>\n").unwrap();
400 + parser.parse_chunk("<|DSML|invoke name=\"get_weather\">\n").unwrap();
401 parser
402 - .push("<|DSML|parameter name=\"location\" string=\"true\">SF</|DSML|parameter>\n")
402 + .parse_chunk(
403 + "<|DSML|parameter name=\"location\" string=\"true\">SF</|DSML|parameter>\n",
404 + )
405 .unwrap();
406
407 let error = parser.finish().unwrap_err();
rust/src/tool-parser/src/deepseek_dsml/deepseek_v4.rs
+20 -19
@@ -1,5 +1,5 @@
1 use super::{DeepSeekDsmlToolParser, DsmlTokens};
2 -use crate::{Result, Tool, ToolParseResult, ToolParser};
2 +use crate::{Result, Tool, ToolParser, ToolParserOutput};
3
4 /// Tool parser for DeepSeek V4 models.
5 ///
@@ -36,7 +36,6 @@ impl DeepSeekV4ToolParser {
36 }
37
38 impl ToolParser for DeepSeekV4ToolParser {
39 - /// Create a boxed DeepSeek V4 tool parser.
39 fn create(tools: &[Tool]) -> Result<Box<dyn ToolParser>>
40 where
41 Self: Sized + 'static,
@@ -44,27 +43,29 @@ impl ToolParser for DeepSeekV4ToolParser {
43 Ok(Box::new(Self::new(tools)))
44 }
45
47 - /// Preserve DSML special tokens while decoding.
46 fn preserve_special_tokens(&self) -> bool {
47 true
48 }
49
52 - /// Push one decoded text chunk through the DSML parser.
53 - fn push(&mut self, chunk: &str) -> Result<ToolParseResult> {
54 - self.0.push(chunk)
50 + fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
51 + self.0.parse_into(chunk, output)
52 }
53
57 - /// Flush buffered text and reset parser state.
58 - fn finish(&mut self) -> Result<ToolParseResult> {
54 + fn finish(&mut self) -> Result<ToolParserOutput> {
55 self.0.finish()
56 }
57 +
58 + fn reset(&mut self) -> String {
59 + self.0.reset()
60 + }
61 }
62
63 #[cfg(test)]
64 mod tests {
65 use serde_json::{Value, json};
66
67 - use super::{DeepSeekV4ToolParser, ToolParser};
67 + use super::DeepSeekV4ToolParser;
68 + use crate::ToolParserTestExt as _;
69 use crate::test_utils::{collect_stream, test_tools};
70
71 fn build_tool_call(function_name: &str, params: &[(&str, &str)]) -> String {
@@ -85,18 +86,18 @@ mod tests {
86 #[test]
87 fn deepseek_v4_parse_complete_reuses_dsml_parser_with_tool_calls_token() {
88 let mut parser = DeepSeekV4ToolParser::new(&test_tools());
88 - let result = parser
89 + let output = parser
90 .parse_complete(&build_tool_call(
91 "get_weather",
92 &[("location", "SF"), ("date", "2024-01-16")],
93 ))
94 .unwrap();
95
95 - assert!(result.normal_text.is_empty());
96 - assert_eq!(result.calls.len(), 1);
97 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
96 + assert!(output.normal_text.is_empty());
97 + assert_eq!(output.calls.len(), 1);
98 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
99 assert_eq!(
99 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
100 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
101 json!({
102 "location": "SF",
103 "date": "2024-01-16"
@@ -107,7 +108,7 @@ mod tests {
108 #[test]
109 fn deepseek_v4_streaming_handles_tool_calls_token_split_across_chunks() {
110 let mut parser = DeepSeekV4ToolParser::new(&test_tools());
110 - let result = collect_stream(
111 + let output = collect_stream(
112 &mut parser,
113 &[
114 "Thinking... ",
@@ -122,11 +123,11 @@ mod tests {
123 ],
124 );
125
125 - assert_eq!(result.normal_text, "Thinking... ");
126 - assert_eq!(result.calls.len(), 1);
127 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
126 + assert_eq!(output.normal_text, "Thinking... ");
127 + assert_eq!(output.calls.len(), 1);
128 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
129 assert_eq!(
129 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
130 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
131 json!({ "location": "Beijing" })
132 );
133 }
rust/src/tool-parser/src/deepseek_dsml/mod.rs
+14 -19
@@ -6,7 +6,7 @@ use winnow::token::{literal, rest, take_until};
6
7 use super::parameters::ToolSchemas;
8 use super::utils::{parse_buffered_event, safe_text_len, xml_unescape};
9 -use super::{Result, ToolCallDelta, ToolParseResult};
9 +use super::{Result, ToolCallDelta, ToolParserOutput};
10 use crate::Tool;
11
12 mod deepseek_v32;
@@ -89,10 +89,10 @@ impl DeepSeekDsmlToolParser {
89 }
90
91 /// Apply one parsed DSML event to parser state and output.
92 - fn apply_event(&mut self, event: DsmlEvent, result: &mut ToolParseResult) -> Result<()> {
92 + fn apply_event(&mut self, event: DsmlEvent, output: &mut ToolParserOutput) -> Result<()> {
93 match event {
94 DsmlEvent::Text { len: consumed_len } => {
95 - result.normal_text.push_str(&self.buffer[..consumed_len]);
95 + output.normal_text.push_str(&self.buffer[..consumed_len]);
96 }
97 DsmlEvent::ToolCallsStart => self.mode = DsmlMode::ToolBlock,
98 DsmlEvent::Invoke { name, raw_params } => {
@@ -112,7 +112,7 @@ impl DeepSeekDsmlToolParser {
112 let arguments = serde_json::to_string(&arguments)
113 .map_err(|error| parsing_failed!("failed to serialize arguments: {}", error))?;
114
115 - result.calls.push(ToolCallDelta {
115 + output.calls.push(ToolCallDelta {
116 tool_index: self.emitted_invoke_count,
117 name: Some(name),
118 arguments,
@@ -125,46 +125,41 @@ impl DeepSeekDsmlToolParser {
125 Ok(())
126 }
127
128 - /// Reset all streaming state.
129 - fn reset(&mut self) {
130 - self.buffer.clear();
128 + fn reset(&mut self) -> String {
129 self.mode = DsmlMode::Text;
130 self.emitted_invoke_count = 0;
131 + std::mem::take(&mut self.buffer)
132 }
133
135 - /// Push one decoded text chunk through the DSML parser.
136 - fn push(&mut self, chunk: &str) -> Result<ToolParseResult> {
134 + fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
135 // Extract tool calls from streaming model output.
136 //
137 // Uses a buffer-until-complete-invoke strategy: text is buffered until
138 // a complete invoke block is available, then parsed and emitted in one
139 // shot.
140 self.buffer.push_str(chunk);
143 - let mut result = ToolParseResult::default();
141
142 while let Some((event, consumed_len)) = parse_buffered_event(&self.buffer, |input| {
143 parse_next_dsml_event(input, self.mode, self.tokens)
144 })? {
148 - self.apply_event(event, &mut result)?;
145 + self.apply_event(event, output)?;
146 self.buffer.drain(..consumed_len);
147 }
148
152 - Ok(result)
149 + Ok(())
150 }
151
155 - /// Flush buffered text and reset parser state.
156 - fn finish(&mut self) -> Result<ToolParseResult> {
157 - let mut result = ToolParseResult::default();
152 + fn finish(&mut self) -> Result<ToolParserOutput> {
153 + let mut output = ToolParserOutput::default();
154 match self.mode {
159 - DsmlMode::Text => result.normal_text.push_str(&self.buffer),
155 + DsmlMode::Text => output.normal_text.push_str(&self.buffer),
156 DsmlMode::Done => {}
157 DsmlMode::ToolBlock => {
162 - self.reset();
158 return Err(parsing_failed!("incomplete DeepSeek DSML tool call"));
159 }
160 }
166 - self.reset();
167 - Ok(result)
161 + let _ = self.reset();
162 + Ok(output)
163 }
164 }
165
rust/src/tool-parser/src/deepseek_json/deepseek_v3.rs
+37 -36
@@ -1,5 +1,5 @@
1 use super::{DeepSeekJsonFormat, DeepSeekJsonToolParser};
2 -use crate::{Result, Tool, ToolParseResult, ToolParser};
2 +use crate::{Result, Tool, ToolParser, ToolParserOutput};
3
4 /// Tool parser for DeepSeek V3 JSON-fenced tool calls.
5 ///
@@ -25,7 +25,6 @@ impl DeepSeekV3ToolParser {
25 }
26
27 impl ToolParser for DeepSeekV3ToolParser {
28 - /// Create a boxed DeepSeek V3 tool parser.
28 fn create(tools: &[Tool]) -> Result<Box<dyn ToolParser>>
29 where
30 Self: Sized + 'static,
@@ -33,15 +32,17 @@ impl ToolParser for DeepSeekV3ToolParser {
32 Ok(Box::new(Self::new(tools)))
33 }
34
36 - /// Push one decoded text chunk through the DeepSeek V3 parser.
37 - fn push(&mut self, chunk: &str) -> Result<ToolParseResult> {
38 - self.0.push(chunk)
35 + fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
36 + self.0.parse_into(chunk, output)
37 }
38
41 - /// Flush buffered text and reset parser state.
42 - fn finish(&mut self) -> Result<ToolParseResult> {
39 + fn finish(&mut self) -> Result<ToolParserOutput> {
40 self.0.finish()
41 }
42 +
43 + fn reset(&mut self) -> String {
44 + self.0.reset()
45 + }
46 }
47
48 #[cfg(test)]
@@ -55,7 +56,7 @@ mod tests {
56 V3_JSON_START,
57 };
58 use crate::test_utils::{collect_stream, split_by_chars, test_tools};
58 - use crate::{ToolParseResult, ToolParser};
59 + use crate::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
60
61 fn v3_tool_call(function_name: &str, arguments: &str) -> String {
62 format!(
@@ -70,39 +71,39 @@ mod tests {
71 #[test]
72 fn deepseek_v3_parse_complete_without_tool_call_keeps_text() {
73 let mut parser = DeepSeekV3ToolParser::new(&test_tools());
73 - let result = parser.parse_complete("Hello, world!").unwrap();
74 + let output = parser.parse_complete("Hello, world!").unwrap();
75
75 - assert_eq!(result.normal_text, "Hello, world!");
76 - assert!(result.calls.is_empty());
76 + assert_eq!(output.normal_text, "Hello, world!");
77 + assert!(output.calls.is_empty());
78 }
79
80 #[test]
81 fn deepseek_v3_parse_complete_extracts_raw_json_arguments() {
82 let mut parser = DeepSeekV3ToolParser::new(&test_tools());
83 let arguments = r#"{ "location": "Tokyo", "days": "3" }"#;
83 - let result = parser
84 + let output = parser
85 .parse_complete(&format!(
86 "Let me check.\n{} trailing text",
87 tool_section(&[v3_tool_call("get_weather", arguments)])
88 ))
89 .unwrap();
90
90 - assert_eq!(result.normal_text, "Let me check.\n");
91 - assert_eq!(result.calls.len(), 1);
92 - assert_eq!(result.calls[0].tool_index, 0);
93 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
94 - assert_eq!(result.calls[0].arguments, arguments);
91 + assert_eq!(output.normal_text, "Let me check.\n");
92 + assert_eq!(output.calls.len(), 1);
93 + assert_eq!(output.calls[0].tool_index, 0);
94 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
95 + assert_eq!(output.calls[0].arguments, arguments);
96 }
97
98 #[test]
99 fn deepseek_v3_does_not_validate_or_normalize_arguments() {
100 let mut parser = DeepSeekV3ToolParser::new(&test_tools());
101 let arguments = r#"{"location":"Tokyo",}"#;
101 - let result = parser
102 + let output = parser
103 .parse_complete(&tool_section(&[v3_tool_call("get_weather", arguments)]))
104 .unwrap();
105
105 - assert_eq!(result.calls[0].arguments, arguments);
106 + assert_eq!(output.calls[0].arguments, arguments);
107 }
108
109 #[test]
@@ -122,23 +123,23 @@ mod tests {
123 TOOL_CALLS_END,
124 ];
125
125 - let mut result = ToolParseResult::default();
126 + let mut output = ToolParserOutput::default();
127 let mut observed_arguments = Vec::new();
128 for chunk in chunks {
128 - let next = parser.push(chunk).unwrap();
129 + let next = parser.parse_chunk(chunk).unwrap();
130 observed_arguments.extend(
131 next.calls
132 .iter()
133 .filter(|call| call.name.is_none())
134 .map(|call| call.arguments.clone()),
135 );
135 - result.append(next);
136 + output.append(next);
137 }
137 - result.append(parser.finish().unwrap());
138 + output.append(parser.finish().unwrap());
139
140 assert_eq!(observed_arguments, ["{\"location\":", "\"Beijing\"", "}"]);
141 assert_eq!(
141 - result.coalesce_calls().calls[0].arguments,
142 + output.coalesce_calls().calls[0].arguments,
143 r#"{"location":"Beijing"}"#
144 );
145 }
@@ -152,11 +153,11 @@ mod tests {
153 let chunks = split_by_chars(&input, 5);
154 let mut parser = DeepSeekV3ToolParser::new(&test_tools());
155
155 - let result = collect_stream(&mut parser, &chunks);
156 + let output = collect_stream(&mut parser, &chunks);
157
157 - assert_eq!(result.normal_text, "hello ");
158 - assert_eq!(result.calls.len(), 1);
159 - assert_eq!(result.calls[0].arguments, r#"{"location":"Tokyo"}"#);
158 + assert_eq!(output.normal_text, "hello ");
159 + assert_eq!(output.calls.len(), 1);
160 + assert_eq!(output.calls[0].arguments, r#"{"location":"Tokyo"}"#);
161 }
162
163 #[test]
@@ -165,10 +166,10 @@ mod tests {
166 let arguments = format!("{{\"text\":\"literal {V3_ARGUMENT_END} inside\"}}");
167 let input = tool_section(&[v3_tool_call("echo", &arguments)]);
168
168 - let result = parser.parse_complete(&input).unwrap();
169 + let output = parser.parse_complete(&input).unwrap();
170
170 - assert_eq!(result.calls.len(), 1);
171 - assert_eq!(result.calls[0].arguments, arguments);
171 + assert_eq!(output.calls.len(), 1);
172 + assert_eq!(output.calls[0].arguments, arguments);
173 }
174
175 #[test]
@@ -180,10 +181,10 @@ mod tests {
181 let chunks = split_by_chars(&input, 7);
182 let mut parser = DeepSeekV3ToolParser::new(&test_tools());
183
183 - let result = collect_stream(&mut parser, &chunks);
184 + let output = collect_stream(&mut parser, &chunks);
185
186 expect![[r#"
186 - ToolParseResult {
187 + ToolParserOutput {
188 normal_text: "",
189 calls: [
190 ToolCallDelta {
@@ -203,14 +204,14 @@ mod tests {
204 ],
205 }
206 "#]]
206 - .assert_debug_eq(&result);
207 + .assert_debug_eq(&output);
208 }
209
210 #[test]
211 fn deepseek_v3_finish_fails_incomplete_tool_call() {
212 let mut parser = DeepSeekV3ToolParser::new(&test_tools());
213 parser
213 - .push(&format!(
214 + .parse_chunk(&format!(
215 "{TOOL_CALLS_START}{TOOL_CALL_START}function{TOOL_CALL_SEPARATOR}get_weather{V3_JSON_START}{{\"location\""
216 ))
217 .unwrap();
@@ -228,7 +229,7 @@ mod tests {
229 "{TOOL_CALLS_START}{TOOL_CALL_START}tool{TOOL_CALL_SEPARATOR}get_weather{V3_JSON_START}{{}}"
230 );
231
231 - let error = parser.push(&input).unwrap_err();
232 + let error = parser.parse_chunk(&input).unwrap_err();
233
234 expect!["tool parser parsing failed: "].assert_eq(&error.to_report_string());
235 }
rust/src/tool-parser/src/deepseek_json/deepseek_v31.rs
+41 -40
@@ -1,5 +1,5 @@
1 use super::{DeepSeekJsonFormat, DeepSeekJsonToolParser};
2 -use crate::{Result, Tool, ToolParseResult, ToolParser};
2 +use crate::{Result, Tool, ToolParser, ToolParserOutput};
3
4 /// Tool parser for DeepSeek V3.1 raw JSON tool calls.
5 ///
@@ -21,7 +21,6 @@ impl DeepSeekV31ToolParser {
21 }
22
23 impl ToolParser for DeepSeekV31ToolParser {
24 - /// Create a boxed DeepSeek V3.1 tool parser.
24 fn create(tools: &[Tool]) -> Result<Box<dyn ToolParser>>
25 where
26 Self: Sized + 'static,
@@ -29,15 +28,17 @@ impl ToolParser for DeepSeekV31ToolParser {
28 Ok(Box::new(Self::new(tools)))
29 }
30
32 - /// Push one decoded text chunk through the DeepSeek V3.1 parser.
33 - fn push(&mut self, chunk: &str) -> Result<ToolParseResult> {
34 - self.0.push(chunk)
31 + fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
32 + self.0.parse_into(chunk, output)
33 }
34
37 - /// Flush buffered text and reset parser state.
38 - fn finish(&mut self) -> Result<ToolParseResult> {
35 + fn finish(&mut self) -> Result<ToolParserOutput> {
36 self.0.finish()
37 }
38 +
39 + fn reset(&mut self) -> String {
40 + self.0.reset()
41 + }
42 }
43
44 #[cfg(test)]
@@ -50,7 +51,7 @@ mod tests {
51 TOOL_CALL_END, TOOL_CALL_SEPARATOR, TOOL_CALL_START, TOOL_CALLS_END, TOOL_CALLS_START,
52 };
53 use crate::test_utils::{collect_stream, split_by_chars, test_tools};
53 - use crate::{ToolParseResult, ToolParser};
54 + use crate::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
55
56 fn v31_tool_call(function_name: &str, arguments: &str) -> String {
57 format!("{TOOL_CALL_START}{function_name}{TOOL_CALL_SEPARATOR}{arguments}{TOOL_CALL_END}")
@@ -63,39 +64,39 @@ mod tests {
64 #[test]
65 fn deepseek_v31_parse_complete_without_tool_call_keeps_text() {
66 let mut parser = DeepSeekV31ToolParser::new(&test_tools());
66 - let result = parser.parse_complete("Hello, world!").unwrap();
67 + let output = parser.parse_complete("Hello, world!").unwrap();
68
68 - assert_eq!(result.normal_text, "Hello, world!");
69 - assert!(result.calls.is_empty());
69 + assert_eq!(output.normal_text, "Hello, world!");
70 + assert!(output.calls.is_empty());
71 }
72
73 #[test]
74 fn deepseek_v31_parse_complete_extracts_raw_json_arguments() {
75 let mut parser = DeepSeekV31ToolParser::new(&test_tools());
76 let arguments = r#"{ "location": "Tokyo", "days": "3" }"#;
76 - let result = parser
77 + let output = parser
78 .parse_complete(&format!(
79 "Let me check.{} trailing text",
80 tool_section(&[v31_tool_call("get_weather", arguments)])
81 ))
82 .unwrap();
83
83 - assert_eq!(result.normal_text, "Let me check.");
84 - assert_eq!(result.calls.len(), 1);
85 - assert_eq!(result.calls[0].tool_index, 0);
86 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
87 - assert_eq!(result.calls[0].arguments, arguments);
84 + assert_eq!(output.normal_text, "Let me check.");
85 + assert_eq!(output.calls.len(), 1);
86 + assert_eq!(output.calls[0].tool_index, 0);
87 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
88 + assert_eq!(output.calls[0].arguments, arguments);
89 }
90
91 #[test]
92 fn deepseek_v31_does_not_validate_or_normalize_arguments() {
93 let mut parser = DeepSeekV31ToolParser::new(&test_tools());
94 let arguments = r#"{"location":"Tokyo",}"#;
94 - let result = parser
95 + let output = parser
96 .parse_complete(&tool_section(&[v31_tool_call("get_weather", arguments)]))
97 .unwrap();
98
98 - assert_eq!(result.calls[0].arguments, arguments);
99 + assert_eq!(output.calls[0].arguments, arguments);
100 }
101
102 #[test]
@@ -113,23 +114,23 @@ mod tests {
114 TOOL_CALLS_END,
115 ];
116
116 - let mut result = ToolParseResult::default();
117 + let mut output = ToolParserOutput::default();
118 let mut observed_arguments = Vec::new();
119 for chunk in chunks {
119 - let next = parser.push(chunk).unwrap();
120 + let next = parser.parse_chunk(chunk).unwrap();
121 observed_arguments.extend(
122 next.calls
123 .iter()
124 .filter(|call| call.name.is_none())
125 .map(|call| call.arguments.clone()),
126 );
126 - result.append(next);
127 + output.append(next);
128 }
128 - result.append(parser.finish().unwrap());
129 + output.append(parser.finish().unwrap());
130
131 assert_eq!(observed_arguments, ["{\"location\":", "\"Beijing\"", "}"]);
132 assert_eq!(
132 - result.coalesce_calls().calls[0].arguments,
133 + output.coalesce_calls().calls[0].arguments,
134 r#"{"location":"Beijing"}"#
135 );
136 }
@@ -143,11 +144,11 @@ mod tests {
144 let chunks = split_by_chars(&input, 5);
145 let mut parser = DeepSeekV31ToolParser::new(&test_tools());
146
146 - let result = collect_stream(&mut parser, &chunks);
147 + let output = collect_stream(&mut parser, &chunks);
148
148 - assert_eq!(result.normal_text, "hello ");
149 - assert_eq!(result.calls.len(), 1);
150 - assert_eq!(result.calls[0].arguments, r#"{"location":"Tokyo"}"#);
149 + assert_eq!(output.normal_text, "hello ");
150 + assert_eq!(output.calls.len(), 1);
151 + assert_eq!(output.calls[0].arguments, r#"{"location":"Tokyo"}"#);
152 }
153
154 #[test]
@@ -156,10 +157,10 @@ mod tests {
157 let arguments = format!(r#"{{"text":"literal {TOOL_CALL_END} inside"}}"#);
158 let input = tool_section(&[v31_tool_call("echo", &arguments)]);
159
159 - let result = parser.parse_complete(&input).unwrap();
160 + let output = parser.parse_complete(&input).unwrap();
161
161 - assert_eq!(result.calls.len(), 1);
162 - assert_eq!(result.calls[0].arguments, arguments);
162 + assert_eq!(output.calls.len(), 1);
163 + assert_eq!(output.calls[0].arguments, arguments);
164 }
165
166 #[test]
@@ -171,10 +172,10 @@ mod tests {
172 let chunks = split_by_chars(&input, 7);
173 let mut parser = DeepSeekV31ToolParser::new(&test_tools());
174
174 - let result = collect_stream(&mut parser, &chunks);
175 + let output = collect_stream(&mut parser, &chunks);
176
177 expect![[r#"
177 - ToolParseResult {
178 + ToolParserOutput {
179 normal_text: "",
180 calls: [
181 ToolCallDelta {
@@ -194,7 +195,7 @@ mod tests {
195 ],
196 }
197 "#]]
197 - .assert_debug_eq(&result);
198 + .assert_debug_eq(&output);
199 }
200
201 #[test]
@@ -205,18 +206,18 @@ mod tests {
206 );
207 let mut parser = DeepSeekV31ToolParser::new(&test_tools());
208
208 - let result = collect_stream(&mut parser, &[&input]);
209 + let output = collect_stream(&mut parser, &[&input]);
210
210 - assert!(result.normal_text.is_empty());
211 - assert_eq!(result.calls.len(), 1);
212 - assert_eq!(result.calls[0].arguments, r#"{"location":"Tokyo"}"#);
211 + assert!(output.normal_text.is_empty());
212 + assert_eq!(output.calls.len(), 1);
213 + assert_eq!(output.calls[0].arguments, r#"{"location":"Tokyo"}"#);
214 }
215
216 #[test]
217 fn deepseek_v31_finish_fails_incomplete_tool_call() {
218 let mut parser = DeepSeekV31ToolParser::new(&test_tools());
219 parser
219 - .push(&format!(
220 + .parse_chunk(&format!(
221 "{TOOL_CALLS_START}{TOOL_CALL_START}get_weather{TOOL_CALL_SEPARATOR}{{\"location\""
222 ))
223 .unwrap();
@@ -232,7 +233,7 @@ mod tests {
233 let mut parser = DeepSeekV31ToolParser::new(&test_tools());
234 let input = format!("{TOOL_CALLS_START}{TOOL_CALL_START}{TOOL_CALL_SEPARATOR}{{}}");
235
235 - let error = parser.push(&input).unwrap_err();
236 + let error = parser.parse_chunk(&input).unwrap_err();
237
238 expect!["tool parser parsing failed: "].assert_eq(&error.to_report_string());
239 }
rust/src/tool-parser/src/deepseek_json/mod.rs
+15 -19
@@ -10,7 +10,7 @@ use winnow::stream::Partial;
10 use winnow::token::{literal, rest, take_until};
11
12 use super::utils::{JsonObjectScanState, parse_buffered_event, safe_text_len, take_json_object};
13 -use super::{Result, ToolCallDelta, ToolParseResult};
13 +use super::{Result, ToolCallDelta, ToolParserOutput};
14
15 pub(super) const TOOL_CALLS_START: &str = "<|tool▁calls▁begin|>";
16 pub(super) const TOOL_CALLS_END: &str = "<|tool▁calls▁end|>";
@@ -92,11 +92,11 @@ impl DeepSeekJsonToolParser {
92 fn apply_event(
93 &mut self,
94 event: DeepSeekJsonEvent,
95 - result: &mut ToolParseResult,
95 + output: &mut ToolParserOutput,
96 ) -> Result<()> {
97 match event {
98 DeepSeekJsonEvent::Text { len: consumed_len } => {
99 - result.normal_text.push_str(&self.buffer[..consumed_len]);
99 + output.normal_text.push_str(&self.buffer[..consumed_len]);
100 }
101 DeepSeekJsonEvent::ToolCallsStart => self.mode = DeepSeekJsonMode::ToolBlock,
102 DeepSeekJsonEvent::ToolCallStart => self.mode = DeepSeekJsonMode::Header,
@@ -107,7 +107,7 @@ impl DeepSeekJsonToolParser {
107 self.mode = DeepSeekJsonMode::Arguments {
108 json_scan: JsonObjectScanState::default(),
109 };
110 - result.calls.push(ToolCallDelta {
110 + output.calls.push(ToolCallDelta {
111 tool_index,
112 name: Some(function_name),
113 arguments: String::new(),
@@ -120,7 +120,7 @@ impl DeepSeekJsonToolParser {
120 self.format.parser_name()
121 ));
122 };
123 - result.calls.push(ToolCallDelta {
123 + output.calls.push(ToolCallDelta {
124 tool_index,
125 name: None,
126 arguments: self.buffer[..consumed_len].to_string(),
@@ -139,26 +139,23 @@ impl DeepSeekJsonToolParser {
139 Ok(())
140 }
141
142 - /// Push one decoded text chunk through the DeepSeek JSON parser.
143 - fn push(&mut self, chunk: &str) -> Result<ToolParseResult> {
142 + fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
143 self.buffer.push_str(chunk);
145 - let mut result = ToolParseResult::default();
144
145 while let Some((event, consumed_len)) = parse_buffered_event(&self.buffer, |input| {
146 parse_next_deepseek_json_event(input, &mut self.mode, self.format)
147 })? {
150 - self.apply_event(event, &mut result)?;
148 + self.apply_event(event, output)?;
149 self.buffer.drain(..consumed_len);
150 }
151
154 - Ok(result)
152 + Ok(())
153 }
154
157 - /// Flush buffered text and reset parser state.
158 - fn finish(&mut self) -> Result<ToolParseResult> {
159 - let mut result = ToolParseResult::default();
155 + fn finish(&mut self) -> Result<ToolParserOutput> {
156 + let mut output = ToolParserOutput::default();
157 match &self.mode {
161 - DeepSeekJsonMode::Text => result.normal_text.push_str(&self.buffer),
158 + DeepSeekJsonMode::Text => output.normal_text.push_str(&self.buffer),
159 DeepSeekJsonMode::ToolBlock | DeepSeekJsonMode::Done => {}
160 DeepSeekJsonMode::Header | DeepSeekJsonMode::Arguments { .. } => {
161 return Err(parsing_failed!(
@@ -167,16 +164,15 @@ impl DeepSeekJsonToolParser {
164 ));
165 }
166 }
170 - self.reset();
171 - Ok(result)
167 + let _ = self.reset();
168 + Ok(output)
169 }
170
174 - /// Reset all streaming state.
175 - fn reset(&mut self) {
176 - self.buffer.clear();
171 + fn reset(&mut self) -> String {
172 self.mode = DeepSeekJsonMode::Text;
173 self.active_tool_index = None;
174 self.emitted_tool_count = 0;
175 + std::mem::take(&mut self.buffer)
176 }
177 }
178
rust/src/tool-parser/src/gemma4.rs
+75 -73
@@ -7,7 +7,7 @@ use winnow::stream::Partial;
7 use winnow::token::{literal, take_till, take_until};
8
9 use super::utils::{parse_buffered_event, safe_text_len};
10 -use super::{Result, ToolCallDelta, ToolParseResult, ToolParser};
10 +use super::{Result, ToolCallDelta, ToolParser, ToolParserOutput};
11 use crate::Tool;
12
13 const TOOL_CALL_START: &str = "<|tool_call>";
@@ -51,16 +51,16 @@ impl Gemma4ToolParser {
51 }
52 }
53
54 - fn apply_event(&mut self, event: Gemma4Event, result: &mut ToolParseResult) -> Result<()> {
54 + fn apply_event(&mut self, event: Gemma4Event, output: &mut ToolParserOutput) -> Result<()> {
55 match event {
56 Gemma4Event::Text { len: consumed_len } => {
57 - result.normal_text.push_str(&self.buffer[..consumed_len]);
57 + output.normal_text.push_str(&self.buffer[..consumed_len]);
58 }
59 Gemma4Event::ToolCall { name, args } => {
60 let arguments = serde_json::to_string(&args)
61 .map_err(|error| parsing_failed!("failed to serialize arguments: {}", error))?;
62
63 - result.calls.push(ToolCallDelta {
63 + output.calls.push(ToolCallDelta {
64 tool_index: self.emitted_tool_count,
65 name: Some(name),
66 arguments,
@@ -71,9 +71,9 @@ impl Gemma4ToolParser {
71 Ok(())
72 }
73
74 - fn reset(&mut self) {
75 - self.buffer.clear();
74 + fn reset(&mut self) -> String {
75 self.emitted_tool_count = 0;
76 + std::mem::take(&mut self.buffer)
77 }
78 }
79
@@ -89,33 +89,35 @@ impl ToolParser for Gemma4ToolParser {
89 true
90 }
91
92 - fn push(&mut self, chunk: &str) -> Result<ToolParseResult> {
92 + fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
93 self.buffer.push_str(chunk);
94 - let mut result = ToolParseResult::default();
94
95 while let Some((event, consumed_len)) =
96 parse_buffered_event(&self.buffer, parse_next_gemma4_event)?
97 {
99 - self.apply_event(event, &mut result)?;
98 + self.apply_event(event, output)?;
99 self.buffer.drain(..consumed_len);
100 }
101
103 - Ok(result)
102 + Ok(())
103 }
104
106 - fn finish(&mut self) -> Result<ToolParseResult> {
107 - let mut result = ToolParseResult::default();
105 + fn finish(&mut self) -> Result<ToolParserOutput> {
106 + let mut output = ToolParserOutput::default();
107
108 if !self.buffer.is_empty() {
109 if self.buffer.starts_with(TOOL_CALL_START) {
111 - self.reset();
110 return Err(parsing_failed!("incomplete Gemma4 tool call"));
111 }
114 - result.normal_text.push_str(&self.buffer);
112 + output.normal_text.push_str(&self.buffer);
113 }
114
117 - self.reset();
118 - Ok(result)
115 + let _ = self.reset();
116 + Ok(output)
117 + }
118 +
119 + fn reset(&mut self) -> String {
120 + Gemma4ToolParser::reset(self)
121 }
122 }
123
@@ -285,10 +287,10 @@ mod tests {
287 use winnow::stream::Partial;
288
289 use super::{
288 - Gemma4ToolParser, ToolCallDelta, ToolParseResult, ToolParser, gemma4_args,
290 + Gemma4ToolParser, ToolCallDelta, ToolParser, ToolParserOutput, gemma4_args,
291 gemma4_array_content,
292 };
291 - use crate::Tool;
293 + use crate::{Tool, ToolParserTestExt as _};
294
295 fn parse_gemma4_args(args: &str) -> super::Result<serde_json::Map<String, Value>> {
296 let mut input = Partial::new(args);
@@ -367,18 +369,18 @@ mod tests {
369 ]
370 }
371
370 - fn collect_stream(chunks: &[&str]) -> ToolParseResult {
372 + fn collect_stream(chunks: &[&str]) -> ToolParserOutput {
373 let mut parser = Gemma4ToolParser::new(&test_tools());
372 - let mut result = ToolParseResult::default();
374 + let mut output = ToolParserOutput::default();
375 for chunk in chunks {
374 - result.append(parser.push(chunk).unwrap());
376 + output.append(parser.parse_chunk(chunk).unwrap());
377 }
376 - result.append(parser.finish().unwrap());
377 - result.coalesce_calls()
378 + output.append(parser.finish().unwrap());
379 + output.coalesce_calls()
380 }
381
380 - fn first_call(result: &ToolParseResult) -> &ToolCallDelta {
381 - result.calls.first().expect("expected one tool call")
382 + fn first_call(output: &ToolParserOutput) -> &ToolCallDelta {
383 + output.calls.first().expect("expected one tool call")
384 }
385
386 #[test]
@@ -416,15 +418,15 @@ mod tests {
418 #[test]
419 fn gemma4_parse_complete_extracts_single_tool_call() {
420 let mut parser = Gemma4ToolParser::new(&test_tools());
419 - let result = parser
421 + let output = parser
422 .parse_complete("<|tool_call>call:get_weather{location:<|\"|>London<|\"|>}<tool_call|>")
423 .unwrap();
424
423 - assert!(result.normal_text.is_empty());
424 - assert_eq!(result.calls.len(), 1);
425 - assert_eq!(first_call(&result).name.as_deref(), Some("get_weather"));
425 + assert!(output.normal_text.is_empty());
426 + assert_eq!(output.calls.len(), 1);
427 + assert_eq!(first_call(&output).name.as_deref(), Some("get_weather"));
428 assert_eq!(
427 - serde_json::from_str::<Value>(&first_call(&result).arguments).unwrap(),
429 + serde_json::from_str::<Value>(&first_call(&output).arguments).unwrap(),
430 json!({ "location": "London" })
431 );
432 }
@@ -441,7 +443,7 @@ mod tests {
443
444 #[test]
445 fn gemma4_streaming_basic_single_tool_call() {
444 - let result = collect_stream(&[
446 + let output = collect_stream(&[
447 "<|tool_call>",
448 "call:get_weather{",
449 "location:<|\"|>Paris",
@@ -450,17 +452,17 @@ mod tests {
452 "<tool_call|>",
453 ]);
454
453 - assert!(result.normal_text.is_empty());
454 - assert_eq!(first_call(&result).name.as_deref(), Some("get_weather"));
455 + assert!(output.normal_text.is_empty());
456 + assert_eq!(first_call(&output).name.as_deref(), Some("get_weather"));
457 assert_eq!(
456 - serde_json::from_str::<Value>(&first_call(&result).arguments).unwrap(),
458 + serde_json::from_str::<Value>(&first_call(&output).arguments).unwrap(),
459 json!({ "location": "Paris, France" })
460 );
461 }
462
463 #[test]
464 fn gemma4_streaming_text_before_and_after_tool_call() {
463 - let result = collect_stream(&[
465 + let output = collect_stream(&[
466 "Let me check ",
467 "the weather. ",
468 "<|tool_call>",
@@ -470,10 +472,10 @@ mod tests {
472 "div>",
473 ]);
474
473 - assert_eq!(result.normal_text, "Let me check the weather. <div>");
474 - assert_eq!(first_call(&result).name.as_deref(), Some("get_weather"));
475 + assert_eq!(output.normal_text, "Let me check the weather. <div>");
476 + assert_eq!(first_call(&output).name.as_deref(), Some("get_weather"));
477 assert_eq!(
476 - serde_json::from_str::<Value>(&first_call(&result).arguments).unwrap(),
478 + serde_json::from_str::<Value>(&first_call(&output).arguments).unwrap(),
479 json!({ "location": "London" })
480 );
481 }
@@ -481,68 +483,68 @@ mod tests {
483 #[test]
484 fn gemma4_streaming_waits_for_complete_tool_call() {
485 let mut parser = Gemma4ToolParser::new(&test_tools());
484 - let mut result = ToolParseResult::default();
486 + let mut output = ToolParserOutput::default();
487
488 for chunk in [
489 "<|tool_call>",
490 "call:get_weather{",
491 "location:<|\"|>Paris<|\"|>}",
492 ] {
491 - result.append(parser.push(chunk).unwrap());
492 - assert!(result.calls.is_empty());
493 + output.append(parser.parse_chunk(chunk).unwrap());
494 + assert!(output.calls.is_empty());
495 }
496
495 - result.append(parser.push("<tool_call|>").unwrap());
496 - let result = result.coalesce_calls();
497 + output.append(parser.parse_chunk("<tool_call|>").unwrap());
498 + let output = output.coalesce_calls();
499
498 - assert_eq!(first_call(&result).name.as_deref(), Some("get_weather"));
500 + assert_eq!(first_call(&output).name.as_deref(), Some("get_weather"));
501 assert_eq!(
500 - serde_json::from_str::<Value>(&first_call(&result).arguments).unwrap(),
502 + serde_json::from_str::<Value>(&first_call(&output).arguments).unwrap(),
503 json!({ "location": "Paris" })
504 );
505 }
506
507 #[test]
508 fn gemma4_streaming_handles_boolean_split_across_chunks() {
507 - let result = collect_stream(&[
509 + let output = collect_stream(&[
510 "<|tool_call>",
511 "call:search{input:{all:tru",
512 "e}}",
513 "<tool_call|>",
514 ]);
515
514 - assert_eq!(first_call(&result).name.as_deref(), Some("search"));
516 + assert_eq!(first_call(&output).name.as_deref(), Some("search"));
517 assert_eq!(
516 - serde_json::from_str::<Value>(&first_call(&result).arguments).unwrap(),
518 + serde_json::from_str::<Value>(&first_call(&output).arguments).unwrap(),
519 json!({ "input": { "all": true } })
520 );
521 }
522
523 #[test]
524 fn gemma4_streaming_handles_false_split_across_chunks() {
523 - let result = collect_stream(&["<|tool_call>", "call:set{flag:fals", "e}", "<tool_call|>"]);
525 + let output = collect_stream(&["<|tool_call>", "call:set{flag:fals", "e}", "<tool_call|>"]);
526
525 - assert_eq!(first_call(&result).name.as_deref(), Some("set"));
527 + assert_eq!(first_call(&output).name.as_deref(), Some("set"));
528 assert_eq!(
527 - serde_json::from_str::<Value>(&first_call(&result).arguments).unwrap(),
529 + serde_json::from_str::<Value>(&first_call(&output).arguments).unwrap(),
530 json!({ "flag": false })
531 );
532 }
533
534 #[test]
535 fn gemma4_streaming_handles_number_split_across_chunks() {
534 - let result = collect_stream(&["<|tool_call>", "call:set{count:4", "2}", "<tool_call|>"]);
536 + let output = collect_stream(&["<|tool_call>", "call:set{count:4", "2}", "<tool_call|>"]);
537
536 - assert_eq!(first_call(&result).name.as_deref(), Some("set"));
538 + assert_eq!(first_call(&output).name.as_deref(), Some("set"));
539 assert_eq!(
538 - serde_json::from_str::<Value>(&first_call(&result).arguments).unwrap(),
540 + serde_json::from_str::<Value>(&first_call(&output).arguments).unwrap(),
541 json!({ "count": 42 })
542 );
543 }
544
545 #[test]
546 fn gemma4_streaming_handles_split_string_delimiter() {
545 - let result = collect_stream(&[
547 + let output = collect_stream(&[
548 "<|tool_call>",
549 "call:todowrite{",
550 "content:<|\"|>Buy milk<|",
@@ -550,17 +552,17 @@ mod tests {
552 "<tool_call|>",
553 ]);
554
553 - assert_eq!(first_call(&result).name.as_deref(), Some("todowrite"));
555 + assert_eq!(first_call(&output).name.as_deref(), Some("todowrite"));
556 assert_eq!(
555 - serde_json::from_str::<Value>(&first_call(&result).arguments).unwrap(),
557 + serde_json::from_str::<Value>(&first_call(&output).arguments).unwrap(),
558 json!({ "content": "Buy milk" })
559 );
558 - assert!(!first_call(&result).arguments.contains("<|"));
560 + assert!(!first_call(&output).arguments.contains("<|"));
561 }
562
563 #[test]
564 fn gemma4_streaming_handles_end_marker_literal_inside_string() {
563 - let result = collect_stream(&[
565 + let output = collect_stream(&[
566 "<|tool_call>",
567 "call:todowrite{",
568 "content:<|\"|>literal }<tool_call|> inside",
@@ -568,16 +570,16 @@ mod tests {
570 "<tool_call|>",
571 ]);
572
571 - assert_eq!(first_call(&result).name.as_deref(), Some("todowrite"));
573 + assert_eq!(first_call(&output).name.as_deref(), Some("todowrite"));
574 assert_eq!(
573 - serde_json::from_str::<Value>(&first_call(&result).arguments).unwrap(),
575 + serde_json::from_str::<Value>(&first_call(&output).arguments).unwrap(),
576 json!({ "content": "literal }<tool_call|> inside" })
577 );
578 }
579
580 #[test]
581 fn gemma4_streaming_handles_html_argument_without_duplication() {
580 - let result = collect_stream(&[
582 + let output = collect_stream(&[
583 "<|tool_call>",
584 "call:write_file{",
585 "path:<|\"|>index.html<|\"|>,",
@@ -590,9 +592,9 @@ mod tests {
592 "<tool_call|>",
593 ]);
594
593 - assert_eq!(first_call(&result).name.as_deref(), Some("write_file"));
595 + assert_eq!(first_call(&output).name.as_deref(), Some("write_file"));
596 assert_eq!(
595 - serde_json::from_str::<Value>(&first_call(&result).arguments).unwrap(),
597 + serde_json::from_str::<Value>(&first_call(&output).arguments).unwrap(),
598 json!({
599 "path": "index.html",
600 "content": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width\">\n",
@@ -602,7 +604,7 @@ mod tests {
604
605 #[test]
606 fn gemma4_streaming_trailing_bare_bool_is_not_duplicated() {
605 - let result = collect_stream(&[
607 + let output = collect_stream(&[
608 "<|tool_call>",
609 "call:Edit{",
610 "file_path:<|\"|>src/env.py<|\"|>,",
@@ -613,9 +615,9 @@ mod tests {
615 "<tool_call|>",
616 ]);
617
616 - assert_eq!(first_call(&result).name.as_deref(), Some("Edit"));
618 + assert_eq!(first_call(&output).name.as_deref(), Some("Edit"));
619 assert_eq!(
618 - serde_json::from_str::<Value>(&first_call(&result).arguments).unwrap(),
620 + serde_json::from_str::<Value>(&first_call(&output).arguments).unwrap(),
621 json!({
622 "file_path": "src/env.py",
623 "old_string": "old_val",
@@ -624,7 +626,7 @@ mod tests {
626 })
627 );
628 assert_eq!(
627 - first_call(&result).arguments.matches("replace_all").count(),
629 + first_call(&output).arguments.matches("replace_all").count(),
630 1
631 );
632 }
@@ -632,18 +634,18 @@ mod tests {
634 #[test]
635 fn gemma4_finish_flushes_partial_start_marker_as_text() {
636 let mut parser = Gemma4ToolParser::new(&test_tools());
635 - let mut result = parser.push("<").unwrap();
636 - result.append(parser.finish().unwrap());
637 + let mut output = parser.parse_chunk("<").unwrap();
638 + output.append(parser.finish().unwrap());
639
638 - assert_eq!(result.normal_text, "<");
639 - assert!(result.calls.is_empty());
640 + assert_eq!(output.normal_text, "<");
641 + assert!(output.calls.is_empty());
642 }
643
644 #[test]
645 fn gemma4_finish_rejects_complete_args_without_end_marker() {
646 let mut parser = Gemma4ToolParser::new(&test_tools());
647 for chunk in ["<|tool_call>", "call:get_status{}"] {
646 - parser.push(chunk).unwrap();
648 + parser.parse_chunk(chunk).unwrap();
649 }
650
651 let error = parser.finish().unwrap_err();
rust/src/tool-parser/src/glm_xml/glm45_moe.rs
+8 -7
@@ -1,5 +1,5 @@
1 use super::{GlmXmlToolParser, Separator};
2 -use crate::{Result, Tool, ToolParseResult, ToolParser};
2 +use crate::{Result, Tool, ToolParser, ToolParserOutput};
3
4 /// Tool parser for GLM-4.5/4.6 MoE XML-style tool calls.
5 ///
@@ -23,7 +23,6 @@ impl Glm45MoeToolParser {
23 }
24
25 impl ToolParser for Glm45MoeToolParser {
26 - /// Create a boxed GLM-4.5/4.6 MoE tool parser.
26 fn create(tools: &[Tool]) -> Result<Box<dyn ToolParser>>
27 where
28 Self: Sized + 'static,
@@ -31,13 +30,15 @@ impl ToolParser for Glm45MoeToolParser {
30 Ok(Box::new(Self::new(tools)))
31 }
32
34 - /// Push one decoded text chunk through the GLM MoE parser.
35 - fn push(&mut self, chunk: &str) -> Result<ToolParseResult> {
36 - self.0.push(chunk)
33 + fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
34 + self.0.parse_into(chunk, output)
35 }
36
39 - /// Flush buffered text and reset parser state.
40 - fn finish(&mut self) -> Result<ToolParseResult> {
37 + fn finish(&mut self) -> Result<ToolParserOutput> {
38 self.0.finish()
39 }
40 +
41 + fn reset(&mut self) -> String {
42 + self.0.reset()
43 + }
44 }
rust/src/tool-parser/src/glm_xml/glm47_moe.rs
+27 -22
@@ -1,5 +1,5 @@
1 use super::{GlmXmlToolParser, Separator};
2 -use crate::{Result, Tool, ToolParseResult, ToolParser};
2 +use crate::{Result, Tool, ToolParser, ToolParserOutput};
3
4 /// Tool parser for GLM-4.7 MoE XML-style tool calls.
5 ///
@@ -22,20 +22,25 @@ impl ToolParser for Glm47MoeToolParser {
22 Ok(Box::new(Self::new(tools)))
23 }
24
25 - fn push(&mut self, chunk: &str) -> Result<ToolParseResult> {
26 - self.0.push(chunk)
25 + fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
26 + self.0.parse_into(chunk, output)
27 }
28
29 - fn finish(&mut self) -> Result<ToolParseResult> {
29 + fn finish(&mut self) -> Result<ToolParserOutput> {
30 self.0.finish()
31 }
32 +
33 + fn reset(&mut self) -> String {
34 + self.0.reset()
35 + }
36 }
37
38 #[cfg(test)]
39 mod tests {
40 use serde_json::{Value, json};
41
38 - use super::{Glm47MoeToolParser, ToolParser};
42 + use super::Glm47MoeToolParser;
43 + use crate::ToolParserTestExt as _;
44 use crate::test_utils::{collect_stream, split_by_chars, test_tools};
45
46 fn glm47_tool_call(function_name: &str, params: &[(&str, &str)]) -> String {
@@ -58,13 +63,13 @@ mod tests {
63 )
64 );
65
61 - let result = parser.parse_complete(&output).unwrap();
66 + let output = parser.parse_complete(&output).unwrap();
67
63 - assert_eq!(result.normal_text, "Let me search for that.\n");
64 - assert_eq!(result.calls.len(), 1);
65 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
68 + assert_eq!(output.normal_text, "Let me search for that.\n");
69 + assert_eq!(output.calls.len(), 1);
70 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
71 assert_eq!(
67 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
72 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
73 json!({"city": "Beijing", "date": "2024-12-25"})
74 );
75 }
@@ -79,14 +84,14 @@ mod tests {
84 );
85
86 let chunks = split_by_chars(&output, 7);
82 - let result = collect_stream(&mut parser, &chunks);
87 + let output = collect_stream(&mut parser, &chunks);
88
84 - assert_eq!(result.normal_text, "");
85 - assert_eq!(result.calls.len(), 2);
86 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
87 - assert_eq!(result.calls[1].name.as_deref(), Some("add"));
89 + assert_eq!(output.normal_text, "");
90 + assert_eq!(output.calls.len(), 2);
91 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
92 + assert_eq!(output.calls[1].name.as_deref(), Some("add"));
93 assert_eq!(
89 - serde_json::from_str::<Value>(&result.calls[1].arguments).unwrap(),
94 + serde_json::from_str::<Value>(&output.calls[1].arguments).unwrap(),
95 json!({"x": 1, "y": 2})
96 );
97 }
@@ -94,7 +99,7 @@ mod tests {
99 #[test]
100 fn glm47_parse_complete_converts_schema_types() {
101 let mut parser = Glm47MoeToolParser::new(&test_tools());
97 - let result = parser
102 + let output = parser
103 .parse_complete(&glm47_tool_call(
104 "convert",
105 &[
@@ -108,7 +113,7 @@ mod tests {
113 .unwrap();
114
115 assert_eq!(
111 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
116 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
117 json!({
118 "whole": 42,
119 "flag": true,
@@ -123,12 +128,12 @@ mod tests {
128 fn glm47_parse_complete_extracts_zero_argument_call() {
129 let mut parser = Glm47MoeToolParser::new(&test_tools());
130
126 - let result = parser.parse_complete("<tool_call>add</tool_call>").unwrap();
131 + let output = parser.parse_complete("<tool_call>add</tool_call>").unwrap();
132
128 - assert_eq!(result.calls.len(), 1);
129 - assert_eq!(result.calls[0].name.as_deref(), Some("add"));
133 + assert_eq!(output.calls.len(), 1);
134 + assert_eq!(output.calls[0].name.as_deref(), Some("add"));
135 assert_eq!(
131 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
136 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
137 json!({})
138 );
139 }
rust/src/tool-parser/src/glm_xml/mod.rs
+49 -53
@@ -6,7 +6,7 @@ use winnow::token::{literal, rest, take_until, take_while};
6
7 use super::parameters::ToolSchemas;
8 use super::utils::{parse_buffered_event, safe_text_len, xml_unescape};
9 -use super::{Result, ToolCallDelta, ToolParseResult};
9 +use super::{Result, ToolCallDelta, ToolParserOutput};
10 use crate::Tool;
11
12 mod glm45_moe;
@@ -76,10 +76,10 @@ impl GlmXmlToolParser {
76 }
77
78 /// Apply one parsed GLM event to parser state and output.
79 - fn apply_event(&mut self, event: GlmEvent, result: &mut ToolParseResult) -> Result<()> {
79 + fn apply_event(&mut self, event: GlmEvent, output: &mut ToolParserOutput) -> Result<()> {
80 match event {
81 GlmEvent::Text { len: consumed_len } => {
82 - result.normal_text.push_str(&self.buffer[..consumed_len]);
82 + output.normal_text.push_str(&self.buffer[..consumed_len]);
83 }
84 GlmEvent::ToolCallStart => self.mode = GlmMode::ToolCall,
85 GlmEvent::ToolCall { name, raw_params } => {
@@ -88,7 +88,7 @@ impl GlmXmlToolParser {
88 let arguments = serde_json::to_string(&arguments)
89 .map_err(|error| parsing_failed!("failed to serialize arguments: {}", error))?;
90
91 - result.calls.push(ToolCallDelta {
91 + output.calls.push(ToolCallDelta {
92 tool_index: self.emitted_tool_count,
93 name: Some(name),
94 arguments,
@@ -100,40 +100,36 @@ impl GlmXmlToolParser {
100 Ok(())
101 }
102
103 - /// Reset all streaming state.
104 - fn reset(&mut self) {
105 - self.buffer.clear();
103 + fn reset(&mut self) -> String {
104 self.mode = GlmMode::Text;
105 self.emitted_tool_count = 0;
106 + std::mem::take(&mut self.buffer)
107 }
108
110 - /// Push one decoded text chunk through the GLM MoE parser.
111 - fn push(&mut self, chunk: &str) -> Result<ToolParseResult> {
109 + fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
110 self.buffer.push_str(chunk);
113 - let mut result = ToolParseResult::default();
111
112 while let Some((event, consumed_len)) = parse_buffered_event(&self.buffer, |input| {
113 parse_next_glm_event(input, self.mode, self.separator)
114 })? {
118 - self.apply_event(event, &mut result)?;
115 + self.apply_event(event, output)?;
116 self.buffer.drain(..consumed_len);
117 }
118
122 - Ok(result)
119 + Ok(())
120 }
121
125 - /// Flush buffered text and reset parser state.
126 - fn finish(&mut self) -> Result<ToolParseResult> {
127 - let mut result = ToolParseResult::default();
122 + fn finish(&mut self) -> Result<ToolParserOutput> {
123 + let mut output = ToolParserOutput::default();
124 if !self.buffer.is_empty() {
125 match self.mode {
130 - GlmMode::Text => result.normal_text.push_str(&self.buffer),
126 + GlmMode::Text => output.normal_text.push_str(&self.buffer),
127 GlmMode::ToolCall => return Err(parsing_failed!("incomplete GLM MoE tool call")),
128 GlmMode::AfterToolCall => {}
129 }
130 }
135 - self.reset();
136 - Ok(result)
131 + let _ = self.reset();
132 + Ok(output)
133 }
134 }
135
@@ -256,8 +252,8 @@ mod tests {
252 use thiserror_ext::AsReport;
253
254 use super::Glm45MoeToolParser;
259 - use crate::ToolParser;
255 use crate::test_utils::{collect_stream, split_by_chars, test_tools};
256 + use crate::{ToolParser, ToolParserTestExt as _};
257
258 fn glm45_tool_call(function_name: &str, params: &[(&str, &str)]) -> String {
259 let params = params
@@ -273,10 +269,10 @@ mod tests {
269 #[test]
270 fn glm45_parse_complete_without_tool_call_keeps_text() {
271 let mut parser = Glm45MoeToolParser::new(&test_tools());
276 - let result = parser.parse_complete("Hello, world!").unwrap();
272 + let output = parser.parse_complete("Hello, world!").unwrap();
273
278 - assert_eq!(result.normal_text, "Hello, world!");
279 - assert!(result.calls.is_empty());
274 + assert_eq!(output.normal_text, "Hello, world!");
275 + assert!(output.calls.is_empty());
276 }
277
278 #[test]
@@ -290,13 +286,13 @@ mod tests {
286 )
287 );
288
293 - let result = parser.parse_complete(&output).unwrap();
289 + let output = parser.parse_complete(&output).unwrap();
290
295 - assert_eq!(result.normal_text, "Let me search for that.\n");
296 - assert_eq!(result.calls.len(), 1);
297 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
291 + assert_eq!(output.normal_text, "Let me search for that.\n");
292 + assert_eq!(output.calls.len(), 1);
293 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
294 assert_eq!(
299 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
295 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
296 json!({"city": "Beijing", "date": "2024-12-25"})
297 );
298 }
@@ -311,14 +307,14 @@ mod tests {
307 );
308
309 let chunks = split_by_chars(&output, 11);
314 - let result = collect_stream(&mut parser, &chunks);
310 + let output = collect_stream(&mut parser, &chunks);
311
316 - assert_eq!(result.normal_text, "");
317 - assert_eq!(result.calls.len(), 2);
318 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
319 - assert_eq!(result.calls[1].name.as_deref(), Some("add"));
312 + assert_eq!(output.normal_text, "");
313 + assert_eq!(output.calls.len(), 2);
314 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
315 + assert_eq!(output.calls[1].name.as_deref(), Some("add"));
316 assert_eq!(
321 - serde_json::from_str::<Value>(&result.calls[1].arguments).unwrap(),
317 + serde_json::from_str::<Value>(&output.calls[1].arguments).unwrap(),
318 json!({"x": 1, "y": 2})
319 );
320 }
@@ -326,7 +322,7 @@ mod tests {
322 #[test]
323 fn glm45_parse_complete_unescapes_literal_closing_tags_in_arg_value() {
324 let mut parser = Glm45MoeToolParser::new(&test_tools());
329 - let result = parser
325 + let output = parser
326 .parse_complete(&glm45_tool_call(
327 "get_weather",
328 &[
@@ -337,7 +333,7 @@ mod tests {
333 .unwrap();
334
335 assert_eq!(
340 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
336 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
337 json!({
338 "city": "Paris </arg_value></tool_call>",
339 "date": "2026-05-08",
@@ -349,17 +345,17 @@ mod tests {
345 fn glm45_streaming_without_tool_call_emits_text_incrementally() {
346 let mut parser = Glm45MoeToolParser::new(&test_tools());
347
352 - let result = collect_stream(&mut parser, &["hello ", "world"]);
348 + let output = collect_stream(&mut parser, &["hello ", "world"]);
349
354 - assert_eq!(result.normal_text, "hello world");
355 - assert!(result.calls.is_empty());
350 + assert_eq!(output.normal_text, "hello world");
351 + assert!(output.calls.is_empty());
352 }
353
354 #[test]
355 fn glm45_streaming_preserves_prefix_text() {
356 let mut parser = Glm45MoeToolParser::new(&test_tools());
357
362 - let result = collect_stream(
358 + let output = collect_stream(
359 &mut parser,
360 &[
361 "Prefix ",
@@ -367,14 +363,14 @@ mod tests {
363 ],
364 );
365
370 - assert_eq!(result.normal_text, "Prefix ");
371 - assert_eq!(result.calls.len(), 1);
366 + assert_eq!(output.normal_text, "Prefix ");
367 + assert_eq!(output.calls.len(), 1);
368 }
369
370 #[test]
371 fn glm45_streaming_handles_start_token_split_across_chunks() {
372 let mut parser = Glm45MoeToolParser::new(&test_tools());
377 - let result = collect_stream(
373 + let output = collect_stream(
374 &mut parser,
375 &[
376 "hello <tool",
@@ -383,26 +379,26 @@ mod tests {
379 ],
380 );
381
386 - assert_eq!(result.normal_text, "hello ");
387 - assert_eq!(result.calls.len(), 1);
388 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
382 + assert_eq!(output.normal_text, "hello ");
383 + assert_eq!(output.calls.len(), 1);
384 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
385 }
386
387 #[test]
388 fn glm45_streaming_does_not_emit_incomplete_tool_call() {
389 let mut parser = Glm45MoeToolParser::new(&test_tools());
390
395 - let result = parser.push("<tool_call>get_weather\n<arg_key>city</arg_key>").unwrap();
391 + let output = parser.parse_chunk("<tool_call>get_weather\n<arg_key>city</arg_key>").unwrap();
392
397 - assert_eq!(result.normal_text, "");
398 - assert!(result.calls.is_empty());
393 + assert_eq!(output.normal_text, "");
394 + assert!(output.calls.is_empty());
395 }
396
397 #[test]
398 fn glm45_finish_fails_incomplete_tool_call() {
399 let mut parser = Glm45MoeToolParser::new(&test_tools());
400
405 - parser.push("<tool_call>get_weather\n<arg_key>city</arg_key>").unwrap();
401 + parser.parse_chunk("<tool_call>get_weather\n<arg_key>city</arg_key>").unwrap();
402 let error = parser.finish().unwrap_err();
403
404 assert!(error.as_report().to_string().contains("incomplete GLM MoE tool call"));
@@ -412,7 +408,7 @@ mod tests {
408 fn glm45_malformed_tool_call_fails_fast() {
409 let mut parser = Glm45MoeToolParser::new(&test_tools());
410
415 - let error = parser.push("<tool_call>get_weather<arg_key>city</arg_key><arg_value>Paris</arg_value></tool_call>").unwrap_err();
411 + let error = parser.parse_chunk("<tool_call>get_weather<arg_key>city</arg_key><arg_value>Paris</arg_value></tool_call>").unwrap_err();
412
413 assert!(error.as_report().to_string().contains("tool parser parsing failed"));
414 }
@@ -421,7 +417,7 @@ mod tests {
417 fn glm45_streaming_ignores_trailing_text_after_tool_calls() {
418 let mut parser = Glm45MoeToolParser::new(&test_tools());
419
424 - let result = collect_stream(
420 + let output = collect_stream(
421 &mut parser,
422 &[&format!(
423 "{}<|endoftext|>",
@@ -429,7 +425,7 @@ mod tests {
425 )],
426 );
427
432 - assert_eq!(result.normal_text, "");
433 - assert_eq!(result.calls.len(), 1);
428 + assert_eq!(output.normal_text, "");
429 + assert_eq!(output.calls.len(), 1);
430 }
431 }
rust/src/tool-parser/src/json/hermes.rs
+37 -36
@@ -1,5 +1,5 @@
1 use super::{JsonToolCallConfig, JsonToolCallParser, JsonToolCallWhitespace};
2 -use crate::{Result, Tool, ToolParseResult, ToolParser};
2 +use crate::{Result, Tool, ToolParser, ToolParserOutput};
3
4 const HERMES_CONFIG: JsonToolCallConfig = JsonToolCallConfig {
5 parser_name: "Hermes",
@@ -38,7 +38,6 @@ impl HermesToolParser {
38 }
39
40 impl ToolParser for HermesToolParser {
41 - /// Create a boxed Hermes tool parser.
41 fn create(tools: &[Tool]) -> Result<Box<dyn ToolParser>>
42 where
43 Self: Sized + 'static,
@@ -46,15 +45,17 @@ impl ToolParser for HermesToolParser {
45 Ok(Box::new(Self::new(tools)))
46 }
47
49 - /// Push one decoded text chunk through the Hermes parser.
50 - fn push(&mut self, chunk: &str) -> Result<ToolParseResult> {
51 - self.inner.push(chunk)
48 + fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
49 + self.inner.parse_into(chunk, output)
50 }
51
54 - /// Flush buffered text and reset parser state.
55 - fn finish(&mut self) -> Result<ToolParseResult> {
52 + fn finish(&mut self) -> Result<ToolParserOutput> {
53 self.inner.finish()
54 }
55 +
56 + fn reset(&mut self) -> String {
57 + self.inner.reset()
58 + }
59 }
60
61 #[cfg(test)]
@@ -64,7 +65,7 @@ mod tests {
65
66 use super::HermesToolParser;
67 use crate::test_utils::{collect_stream, split_by_chars, test_tools};
67 - use crate::{ToolParseResult, ToolParser};
68 + use crate::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
69
70 fn build_tool_call(function_name: &str, arguments: &str) -> String {
71 format!(r#"<tool_call>{{"name":"{function_name}","arguments":{arguments}}}</tool_call>"#)
@@ -73,51 +74,51 @@ mod tests {
74 #[test]
75 fn hermes_parse_complete_without_tool_call_keeps_text() {
76 let mut parser = HermesToolParser::new(&test_tools());
76 - let result = parser.parse_complete("Hello, world!").unwrap();
77 + let output = parser.parse_complete("Hello, world!").unwrap();
78
78 - assert_eq!(result.normal_text, "Hello, world!");
79 - assert!(result.calls.is_empty());
79 + assert_eq!(output.normal_text, "Hello, world!");
80 + assert!(output.calls.is_empty());
81 }
82
83 #[test]
84 fn hermes_parse_complete_extracts_raw_json_arguments() {
85 let mut parser = HermesToolParser::new(&test_tools());
86 let arguments = r#"{ "location": "Tokyo", "days": "3" }"#;
86 - let result = parser
87 + let output = parser
88 .parse_complete(&format!(
89 "Let me check.\n{}",
90 build_tool_call("get_weather", arguments)
91 ))
92 .unwrap();
93
93 - assert_eq!(result.normal_text, "Let me check.\n");
94 - assert_eq!(result.calls.len(), 1);
95 - assert_eq!(result.calls[0].tool_index, 0);
96 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
97 - assert_eq!(result.calls[0].arguments, arguments);
94 + assert_eq!(output.normal_text, "Let me check.\n");
95 + assert_eq!(output.calls.len(), 1);
96 + assert_eq!(output.calls[0].tool_index, 0);
97 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
98 + assert_eq!(output.calls[0].arguments, arguments);
99 }
100
101 #[test]
102 fn hermes_accepts_newline_after_tool_call_start() {
103 let mut parser = HermesToolParser::new(&test_tools());
103 - let result = parser
104 + let output = parser
105 .parse_complete(
106 r#"<tool_call>
107 {"name":"get_weather","arguments":{}}</tool_call>"#,
108 )
109 .unwrap();
110
110 - assert_eq!(result.calls.len(), 1);
111 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
111 + assert_eq!(output.calls.len(), 1);
112 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
113 }
114
115 #[test]
116 fn hermes_does_not_validate_or_normalize_arguments() {
117 let mut parser = HermesToolParser::new(&test_tools());
118 let arguments = r#"{"location":"Tokyo",}"#;
118 - let result = parser.parse_complete(&build_tool_call("get_weather", arguments)).unwrap();
119 + let output = parser.parse_complete(&build_tool_call("get_weather", arguments)).unwrap();
120
120 - assert_eq!(result.calls[0].arguments, arguments);
121 + assert_eq!(output.calls[0].arguments, arguments);
122 }
123
124 #[test]
@@ -132,24 +133,24 @@ mod tests {
133 "}</tool_call> suffix",
134 ];
135
135 - let mut result = ToolParseResult::default();
136 + let mut output = ToolParserOutput::default();
137 let mut observed_arguments = Vec::new();
138 for chunk in chunks {
138 - let next = parser.push(chunk).unwrap();
139 + let next = parser.parse_chunk(chunk).unwrap();
140 observed_arguments.extend(
141 next.calls
142 .iter()
143 .filter(|call| call.name.is_none())
144 .map(|call| call.arguments.clone()),
145 );
145 - result.append(next);
146 + output.append(next);
147 }
147 - result.append(parser.finish().unwrap());
148 + output.append(parser.finish().unwrap());
149
150 assert_eq!(observed_arguments, ["{\"location\":", "\"Beijing\"", "}"]);
150 - assert_eq!(result.normal_text, "preface suffix");
151 + assert_eq!(output.normal_text, "preface suffix");
152 assert_eq!(
152 - result.coalesce_calls().calls[0].arguments,
153 + output.coalesce_calls().calls[0].arguments,
154 r#"{"location":"Beijing"}"#
155 );
156 }
@@ -163,11 +164,11 @@ mod tests {
164 let chunks = split_by_chars(&input, 5);
165 let mut parser = HermesToolParser::new(&test_tools());
166
166 - let result = collect_stream(&mut parser, &chunks);
167 + let output = collect_stream(&mut parser, &chunks);
168
168 - assert_eq!(result.normal_text, "hello ");
169 - assert_eq!(result.calls.len(), 1);
170 - assert_eq!(result.calls[0].arguments, r#"{"location":"Tokyo"}"#);
169 + assert_eq!(output.normal_text, "hello ");
170 + assert_eq!(output.calls.len(), 1);
171 + assert_eq!(output.calls[0].arguments, r#"{"location":"Tokyo"}"#);
172 }
173
174 #[test]
@@ -180,10 +181,10 @@ mod tests {
181 let chunks = split_by_chars(&input, 7);
182 let mut parser = HermesToolParser::new(&test_tools());
183
183 - let result = collect_stream(&mut parser, &chunks);
184 + let output = collect_stream(&mut parser, &chunks);
185
186 expect![[r#"
186 - ToolParseResult {
187 + ToolParserOutput {
188 normal_text: "",
189 calls: [
190 ToolCallDelta {
@@ -203,14 +204,14 @@ mod tests {
204 ],
205 }
206 "#]]
206 - .assert_debug_eq(&result);
207 + .assert_debug_eq(&output);
208 }
209
210 #[test]
211 fn hermes_finish_fails_incomplete_tool_call() {
212 let mut parser = HermesToolParser::new(&test_tools());
213 parser
213 - .push(r#"<tool_call>{"name":"get_weather","arguments":{"location""#)
214 + .parse_chunk(r#"<tool_call>{"name":"get_weather","arguments":{"location""#)
215 .unwrap();
216
217 let error = parser.finish().unwrap_err();
rust/src/tool-parser/src/json/llama.rs
+66 -65
@@ -9,7 +9,7 @@ use super::{
9 argument_delta_event, tool_call_header_event,
10 };
11 use crate::utils::{JsonObjectScanState, parse_buffered_event};
12 -use crate::{Result, Tool, ToolCallDelta, ToolParseResult, ToolParser};
12 +use crate::{Result, Tool, ToolCallDelta, ToolParser, ToolParserOutput};
13
14 #[derive(Debug, Clone, PartialEq, Eq)]
15 enum LlamaJsonMode {
@@ -78,7 +78,7 @@ impl Llama3JsonToolParser {
78 }
79
80 /// Apply one parsed Llama JSON event to parser state and output.
81 - fn apply_event(&mut self, event: LlamaJsonEvent, result: &mut ToolParseResult) -> Result<()> {
81 + fn apply_event(&mut self, event: LlamaJsonEvent, output: &mut ToolParserOutput) -> Result<()> {
82 match event {
83 LlamaJsonEvent::ToolCallHeader { function_name } => {
84 let tool_index = self.emitted_tool_count;
@@ -87,7 +87,7 @@ impl Llama3JsonToolParser {
87 self.mode = LlamaJsonMode::Arguments {
88 json_scan: JsonObjectScanState::default(),
89 };
90 - result.calls.push(ToolCallDelta {
90 + output.calls.push(ToolCallDelta {
91 tool_index,
92 name: Some(function_name),
93 arguments: String::new(),
@@ -99,7 +99,7 @@ impl Llama3JsonToolParser {
99 "Llama JSON arguments without an active tool call"
100 ));
101 };
102 - result.calls.push(ToolCallDelta {
102 + output.calls.push(ToolCallDelta {
103 tool_index,
104 name: None,
105 arguments: self.buffer[..consumed_len].to_string(),
@@ -117,17 +117,15 @@ impl Llama3JsonToolParser {
117 Ok(())
118 }
119
120 - /// Reset all streaming state.
121 - fn reset(&mut self) {
122 - self.buffer.clear();
120 + fn reset(&mut self) -> String {
121 self.mode = LlamaJsonMode::Start;
122 self.active_tool_index = None;
123 self.emitted_tool_count = 0;
124 + std::mem::take(&mut self.buffer)
125 }
126 }
127
128 impl ToolParser for Llama3JsonToolParser {
130 - /// Create a boxed Llama JSON tool parser.
129 fn create(tools: &[Tool]) -> Result<Box<dyn ToolParser>>
130 where
131 Self: Sized + 'static,
@@ -135,37 +133,34 @@ impl ToolParser for Llama3JsonToolParser {
133 Ok(Box::new(Self::new(tools)))
134 }
135
138 - /// Push one decoded text chunk through the Llama JSON parser.
139 - fn push(&mut self, chunk: &str) -> Result<ToolParseResult> {
136 + fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
137 self.buffer.push_str(chunk);
141 - let mut result = ToolParseResult::default();
138
139 if !self.commit_start() {
144 - return Ok(result);
140 + return Ok(());
141 }
142
143 if matches!(self.mode, LlamaJsonMode::Passthrough) {
148 - result.normal_text.push_str(&self.buffer);
144 + output.normal_text.push_str(&self.buffer);
145 self.buffer.clear();
150 - return Ok(result);
146 + return Ok(());
147 }
148
149 while let Some((event, consumed_len)) = parse_buffered_event(&self.buffer, |input| {
150 parse_next_llama_json_event(input, &mut self.mode)
151 })? {
156 - self.apply_event(event, &mut result)?;
152 + self.apply_event(event, output)?;
153 self.buffer.drain(..consumed_len);
154 }
155
160 - Ok(result)
156 + Ok(())
157 }
158
163 - /// Flush buffered text and reset parser state.
164 - fn finish(&mut self) -> Result<ToolParseResult> {
165 - let mut result = ToolParseResult::default();
159 + fn finish(&mut self) -> Result<ToolParserOutput> {
160 + let mut output = ToolParserOutput::default();
161 match &self.mode {
162 LlamaJsonMode::Start | LlamaJsonMode::Passthrough => {
168 - result.normal_text.push_str(&self.buffer);
163 + output.normal_text.push_str(&self.buffer);
164 }
165 LlamaJsonMode::AfterCall if self.buffer.trim().is_empty() => {}
166 LlamaJsonMode::Header | LlamaJsonMode::Arguments { .. } => {
@@ -175,8 +170,12 @@ impl ToolParser for Llama3JsonToolParser {
170 return Err(parsing_failed!("invalid Llama JSON"));
171 }
172 }
178 - self.reset();
179 - Ok(result)
173 + let _ = self.reset();
174 + Ok(output)
175 + }
176 +
177 + fn reset(&mut self) -> String {
178 + Llama3JsonToolParser::reset(self)
179 }
180 }
181
@@ -254,7 +253,7 @@ mod tests {
253
254 use super::Llama3JsonToolParser;
255 use crate::test_utils::{collect_stream, split_by_chars, test_tools};
257 - use crate::{ToolParseResult, ToolParser};
256 + use crate::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
257
258 fn build_tool_call(function_name: &str, parameters: &str) -> String {
259 format!(r#"{{"name":"{function_name}","parameters":{parameters}}}"#)
@@ -263,26 +262,28 @@ mod tests {
262 #[test]
263 fn llama_json_parse_complete_without_tool_call_keeps_text() {
264 let mut parser = Llama3JsonToolParser::new(&test_tools());
266 - let result = parser.parse_complete("Hello, world!").unwrap();
265 + let output = parser.parse_complete("Hello, world!").unwrap();
266
268 - assert_eq!(result.normal_text, "Hello, world!");
269 - assert!(result.calls.is_empty());
267 + assert_eq!(output.normal_text, "Hello, world!");
268 + assert!(output.calls.is_empty());
269 }
270
271 #[test]
272 fn llama_json_passthrough_never_reenters_tool_parsing() {
273 let mut parser = Llama3JsonToolParser::new(&test_tools());
275 - let mut result = parser.push("plain text first ").unwrap();
276 - result.append(
277 - parser.push(&build_tool_call("get_weather", r#"{"location":"Tokyo"}"#)).unwrap(),
274 + let mut output = parser.parse_chunk("plain text first ").unwrap();
275 + output.append(
276 + parser
277 + .parse_chunk(&build_tool_call("get_weather", r#"{"location":"Tokyo"}"#))
278 + .unwrap(),
279 );
279 - result.append(parser.finish().unwrap());
280 + output.append(parser.finish().unwrap());
281
282 assert_eq!(
282 - result.normal_text,
283 + output.normal_text,
284 r#"plain text first {"name":"get_weather","parameters":{"location":"Tokyo"}}"#
285 );
285 - assert!(result.calls.is_empty());
286 + assert!(output.calls.is_empty());
287 }
288
289 #[test]
@@ -292,10 +293,10 @@ mod tests {
293 "<|python_tag|>{}",
294 build_tool_call("get_weather", r#"{"location":"Tokyo"}"#)
295 );
295 - let result = parser.parse_complete(&input).unwrap();
296 + let output = parser.parse_complete(&input).unwrap();
297
297 - assert_eq!(result.normal_text, input);
298 - assert!(result.calls.is_empty());
298 + assert_eq!(output.normal_text, input);
299 + assert!(output.calls.is_empty());
300 }
301
302 #[test]
@@ -305,22 +306,22 @@ mod tests {
306 "\n {}",
307 build_tool_call("get_weather", r#"{"location":"Tokyo"}"#)
308 );
308 - let result = parser.parse_complete(&input).unwrap();
309 + let output = parser.parse_complete(&input).unwrap();
310
310 - assert_eq!(result.normal_text, input);
311 - assert!(result.calls.is_empty());
311 + assert_eq!(output.normal_text, input);
312 + assert!(output.calls.is_empty());
313 }
314
315 #[test]
316 fn llama_json_extracts_raw_parameters_object() {
317 let mut parser = Llama3JsonToolParser::new(&test_tools());
318 let arguments = r#"{ "location": "Tokyo", "days": 3 }"#;
318 - let result = parser.parse_complete(&build_tool_call("get_weather", arguments)).unwrap();
319 + let output = parser.parse_complete(&build_tool_call("get_weather", arguments)).unwrap();
320
320 - assert_eq!(result.calls.len(), 1);
321 - assert_eq!(result.calls[0].tool_index, 0);
322 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
323 - assert_eq!(result.calls[0].arguments, arguments);
321 + assert_eq!(output.calls.len(), 1);
322 + assert_eq!(output.calls[0].tool_index, 0);
323 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
324 + assert_eq!(output.calls[0].arguments, arguments);
325 }
326
327 #[test]
@@ -344,10 +345,10 @@ mod tests {
345 build_tool_call("get_weather", r#"{"location":"Shanghai"}"#),
346 build_tool_call("add", r#"{"x":1,"y":2}"#),
347 );
347 - let result = parser.parse_complete(&input).unwrap();
348 + let output = parser.parse_complete(&input).unwrap();
349
350 expect![[r#"
350 - ToolParseResult {
351 + ToolParserOutput {
352 normal_text: "",
353 calls: [
354 ToolCallDelta {
@@ -367,7 +368,7 @@ mod tests {
368 ],
369 }
370 "#]]
370 - .assert_debug_eq(&result);
371 + .assert_debug_eq(&output);
372 }
373
374 #[test]
@@ -380,23 +381,23 @@ mod tests {
381 "}}",
382 ];
383
383 - let mut result = ToolParseResult::default();
384 + let mut output = ToolParserOutput::default();
385 let mut observed_arguments = Vec::new();
386 for chunk in chunks {
386 - let next = parser.push(chunk).unwrap();
387 + let next = parser.parse_chunk(chunk).unwrap();
388 observed_arguments.extend(
389 next.calls
390 .iter()
391 .filter(|call| call.name.is_none())
392 .map(|call| call.arguments.clone()),
393 );
393 - result.append(next);
394 + output.append(next);
395 }
395 - result.append(parser.finish().unwrap());
396 + output.append(parser.finish().unwrap());
397
398 assert_eq!(observed_arguments, ["{\"location\":", "\"Beijing\"", "}"]);
399 assert_eq!(
399 - result.coalesce_calls().calls[0].arguments,
400 + output.coalesce_calls().calls[0].arguments,
401 r#"{"location":"Beijing"}"#
402 );
403 }
@@ -411,16 +412,16 @@ mod tests {
412 let chunks = split_by_chars(&input, 6);
413 let mut parser = Llama3JsonToolParser::new(&test_tools());
414
414 - let result = collect_stream(&mut parser, &chunks);
415 + let output = collect_stream(&mut parser, &chunks);
416
416 - assert_eq!(result.normal_text, "");
417 - assert_eq!(result.calls.len(), 2);
417 + assert_eq!(output.normal_text, "");
418 + assert_eq!(output.calls.len(), 2);
419 assert_eq!(
419 - result.calls[0].arguments,
420 + output.calls[0].arguments,
421 r#"{"location":"Dallas","state":"TX"}"#
422 );
422 - assert_eq!(result.calls[1].name.as_deref(), Some("add"));
423 - assert_eq!(result.calls[1].arguments, r#"{"x":4,"y":5}"#);
423 + assert_eq!(output.calls[1].name.as_deref(), Some("add"));
424 + assert_eq!(output.calls[1].arguments, r#"{"x":4,"y":5}"#);
425 }
426
427 #[test]
@@ -430,29 +431,29 @@ mod tests {
431 "payload": {"items": [1, {"value": "literal { brace } and \"quote\""}]},
432 "flag": true
433 }"#;
433 - let result = parser.parse_complete(&build_tool_call("convert", arguments)).unwrap();
434 + let output = parser.parse_complete(&build_tool_call("convert", arguments)).unwrap();
435
435 - assert_eq!(result.calls[0].arguments, arguments);
436 + assert_eq!(output.calls[0].arguments, arguments);
437 }
438
439 #[test]
440 fn llama_json_keeps_trailing_whitespace_after_tool_call() {
441 let mut parser = Llama3JsonToolParser::new(&test_tools());
441 - let result = parser
442 + let output = parser
443 .parse_complete(&format!(
444 "{}\n\t ",
445 build_tool_call("get_weather", r#"{"location":"Tokyo"}"#)
446 ))
447 .unwrap();
448
448 - assert_eq!(result.normal_text, "");
449 - assert_eq!(result.calls.len(), 1);
449 + assert_eq!(output.normal_text, "");
450 + assert_eq!(output.calls.len(), 1);
451 }
452
453 #[test]
454 fn llama_json_finish_fails_incomplete_tool_call() {
455 let mut parser = Llama3JsonToolParser::new(&test_tools());
455 - parser.push(r#"{"name":"get_weather","parameters":{"location""#).unwrap();
456 + parser.parse_chunk(r#"{"name":"get_weather","parameters":{"location""#).unwrap();
457
458 let error = parser.finish().unwrap_err();
459
@@ -463,7 +464,7 @@ mod tests {
464 #[test]
465 fn llama_json_malformed_field_order_fails_fast() {
466 let mut parser = Llama3JsonToolParser::new(&test_tools());
466 - let error = parser.push(r#"{"parameters":{},"name":"get_weather"}"#).unwrap_err();
467 + let error = parser.parse_chunk(r#"{"parameters":{},"name":"get_weather"}"#).unwrap_err();
468
469 expect![[r#"
470 tool parser parsing failed: invalid Llama JSON
@@ -475,7 +476,7 @@ mod tests {
476 fn llama_json_trailing_non_separator_content_errors() {
477 let mut parser = Llama3JsonToolParser::new(&test_tools());
478 let error = parser
478 - .push(&format!(
479 + .parse_chunk(&format!(
480 "{} trailing",
481 build_tool_call("get_weather", r#"{"location":"Tokyo"}"#)
482 ))
rust/src/tool-parser/src/json/mistral.rs
+38 -37
@@ -1,5 +1,5 @@
1 use super::{JsonToolCallConfig, JsonToolCallParser, JsonToolCallWhitespace};
2 -use crate::{Result, Tool, ToolParseResult, ToolParser};
2 +use crate::{Result, Tool, ToolParser, ToolParserOutput};
3
4 const MISTRAL_CONFIG: JsonToolCallConfig = JsonToolCallConfig {
5 parser_name: "Mistral",
@@ -35,7 +35,6 @@ impl MistralToolParser {
35 }
36
37 impl ToolParser for MistralToolParser {
38 - /// Create a boxed Mistral tool parser.
38 fn create(tools: &[Tool]) -> Result<Box<dyn ToolParser>>
39 where
40 Self: Sized + 'static,
@@ -43,15 +42,17 @@ impl ToolParser for MistralToolParser {
42 Ok(Box::new(Self::new(tools)))
43 }
44
46 - /// Push one decoded text chunk through the Mistral parser.
47 - fn push(&mut self, chunk: &str) -> Result<ToolParseResult> {
48 - self.inner.push(chunk)
45 + fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
46 + self.inner.parse_into(chunk, output)
47 }
48
51 - /// Flush buffered text and reset parser state.
52 - fn finish(&mut self) -> Result<ToolParseResult> {
49 + fn finish(&mut self) -> Result<ToolParserOutput> {
50 self.inner.finish()
51 }
52 +
53 + fn reset(&mut self) -> String {
54 + self.inner.reset()
55 + }
56 }
57
58 #[cfg(test)]
@@ -61,7 +62,7 @@ mod tests {
62
63 use super::MistralToolParser;
64 use crate::test_utils::{collect_stream, split_by_chars, test_tools};
64 - use crate::{ToolParseResult, ToolParser};
65 + use crate::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
66
67 fn build_tool_call(function_name: &str, arguments: &str) -> String {
68 format!(r#"{{"name":"{function_name}","arguments":{arguments}}}"#)
@@ -74,34 +75,34 @@ mod tests {
75 #[test]
76 fn mistral_parse_complete_without_tool_call_keeps_text() {
77 let mut parser = MistralToolParser::new(&test_tools());
77 - let result = parser.parse_complete("Hello, world!").unwrap();
78 + let output = parser.parse_complete("Hello, world!").unwrap();
79
79 - assert_eq!(result.normal_text, "Hello, world!");
80 - assert!(result.calls.is_empty());
80 + assert_eq!(output.normal_text, "Hello, world!");
81 + assert!(output.calls.is_empty());
82 }
83
84 #[test]
85 fn mistral_parse_complete_extracts_raw_json_arguments() {
86 let mut parser = MistralToolParser::new(&test_tools());
87 let arguments = r#"{ "location": "Tokyo", "days": "3" }"#;
87 - let result = parser
88 + let output = parser
89 .parse_complete(&format!(
90 "Let me check.\n{}",
91 build_tool_calls(&[build_tool_call("get_weather", arguments)])
92 ))
93 .unwrap();
94
94 - assert_eq!(result.normal_text, "Let me check.\n");
95 - assert_eq!(result.calls.len(), 1);
96 - assert_eq!(result.calls[0].tool_index, 0);
97 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
98 - assert_eq!(result.calls[0].arguments, arguments);
95 + assert_eq!(output.normal_text, "Let me check.\n");
96 + assert_eq!(output.calls.len(), 1);
97 + assert_eq!(output.calls[0].tool_index, 0);
98 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
99 + assert_eq!(output.calls[0].arguments, arguments);
100 }
101
102 #[test]
103 fn mistral_parse_complete_extracts_pretty_multiple_tool_calls() {
104 let mut parser = MistralToolParser::new(&test_tools());
104 - let result = parser
105 + let output = parser
106 .parse_complete(
107 r#"I'll help.
108 [TOOL_CALLS] [
@@ -113,7 +114,7 @@ mod tests {
114 .unwrap();
115
116 expect![[r#"
116 - ToolParseResult {
117 + ToolParserOutput {
118 normal_text: "I'll help.\n",
119 calls: [
120 ToolCallDelta {
@@ -133,21 +134,21 @@ mod tests {
134 ],
135 }
136 "#]]
136 - .assert_debug_eq(&result);
137 + .assert_debug_eq(&output);
138 }
139
140 #[test]
141 fn mistral_does_not_validate_or_normalize_arguments() {
142 let mut parser = MistralToolParser::new(&test_tools());
143 let arguments = r#"{"location":"Tokyo",}"#;
143 - let result = parser
144 + let output = parser
145 .parse_complete(&build_tool_calls(&[build_tool_call(
146 "get_weather",
147 arguments,
148 )]))
149 .unwrap();
150
150 - assert_eq!(result.calls[0].arguments, arguments);
151 + assert_eq!(output.calls[0].arguments, arguments);
152 }
153
154 #[test]
@@ -162,24 +163,24 @@ mod tests {
163 "}] suffix",
164 ];
165
165 - let mut result = ToolParseResult::default();
166 + let mut output = ToolParserOutput::default();
167 let mut observed_arguments = Vec::new();
168 for chunk in chunks {
168 - let next = parser.push(chunk).unwrap();
169 + let next = parser.parse_chunk(chunk).unwrap();
170 observed_arguments.extend(
171 next.calls
172 .iter()
173 .filter(|call| call.name.is_none())
174 .map(|call| call.arguments.clone()),
175 );
175 - result.append(next);
176 + output.append(next);
177 }
177 - result.append(parser.finish().unwrap());
178 + output.append(parser.finish().unwrap());
179
180 assert_eq!(observed_arguments, ["{\"location\":", "\"Beijing\"", "}"]);
180 - assert_eq!(result.normal_text, "preface suffix");
181 + assert_eq!(output.normal_text, "preface suffix");
182 assert_eq!(
182 - result.coalesce_calls().calls[0].arguments,
183 + output.coalesce_calls().calls[0].arguments,
184 r#"{"location":"Beijing"}"#
185 );
186 }
@@ -193,30 +194,30 @@ mod tests {
194 let chunks = split_by_chars(&input, 5);
195 let mut parser = MistralToolParser::new(&test_tools());
196
196 - let result = collect_stream(&mut parser, &chunks);
197 + let output = collect_stream(&mut parser, &chunks);
198
198 - assert_eq!(result.normal_text, "hello ");
199 - assert_eq!(result.calls.len(), 1);
200 - assert_eq!(result.calls[0].arguments, r#"{"location":"Tokyo"}"#);
199 + assert_eq!(output.normal_text, "hello ");
200 + assert_eq!(output.calls.len(), 1);
201 + assert_eq!(output.calls[0].arguments, r#"{"location":"Tokyo"}"#);
202 }
203
204 #[test]
205 fn mistral_keeps_array_bracket_literal_inside_json_string() {
206 let mut parser = MistralToolParser::new(&test_tools());
207 let arguments = r#"{"text":"Array notation: arr[0] = value[1]"}"#;
207 - let result = parser
208 + let output = parser
209 .parse_complete(&build_tool_calls(&[build_tool_call("echo", arguments)]))
210 .unwrap();
211
211 - assert_eq!(result.calls.len(), 1);
212 - assert_eq!(result.calls[0].arguments, arguments);
212 + assert_eq!(output.calls.len(), 1);
213 + assert_eq!(output.calls[0].arguments, arguments);
214 }
215
216 #[test]
217 fn mistral_finish_fails_incomplete_tool_call() {
218 let mut parser = MistralToolParser::new(&test_tools());
219 parser
219 - .push(r#"[TOOL_CALLS] [{"name":"get_weather","arguments":{"location""#)
220 + .parse_chunk(r#"[TOOL_CALLS] [{"name":"get_weather","arguments":{"location""#)
221 .unwrap();
222
223 let error = parser.finish().unwrap_err();
@@ -229,7 +230,7 @@ mod tests {
230 fn mistral_malformed_field_order_fails_fast() {
231 let mut parser = MistralToolParser::new(&test_tools());
232 let error = parser
232 - .push(r#"[TOOL_CALLS] [{"arguments":{},"name":"get_weather"}]"#)
233 + .parse_chunk(r#"[TOOL_CALLS] [{"arguments":{},"name":"get_weather"}]"#)
234 .unwrap_err();
235
236 expect![[r#"
rust/src/tool-parser/src/json/mod.rs
+30 -34
@@ -20,7 +20,7 @@ use winnow::token::literal;
20 use super::utils::{
21 JsonObjectScanState, json_str, parse_buffered_event, safe_text_len, take_json_object,
22 };
23 -use super::{Result, ToolCallDelta, ToolParseResult};
23 +use super::{Result, ToolCallDelta, ToolParserOutput};
24
25 type JsonToolInput<'i> = Partial<&'i str>;
26
@@ -80,27 +80,24 @@ impl JsonToolCallParser {
80 }
81 }
82
83 - /// Push one decoded text chunk through the JSON tool-call parser.
84 - fn push(&mut self, chunk: &str) -> Result<ToolParseResult> {
83 + fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
84 self.buffer.push_str(chunk);
86 - let mut result = ToolParseResult::default();
85 let config = self.config;
86
87 while let Some((event, consumed_len)) = parse_buffered_event(&self.buffer, |input| {
88 parse_next_json_tool_call_event(input, &mut self.mode, config)
89 })? {
92 - self.apply_event(event, &mut result)?;
90 + self.apply_event(event, output)?;
91 self.buffer.drain(..consumed_len);
92 }
93
96 - Ok(result)
94 + Ok(())
95 }
96
99 - /// Flush buffered text and reset parser state.
100 - fn finish(&mut self) -> Result<ToolParseResult> {
101 - let mut result = ToolParseResult::default();
97 + fn finish(&mut self) -> Result<ToolParserOutput> {
98 + let mut output = ToolParserOutput::default();
99 match &self.mode {
103 - JsonToolCallMode::Text => result.normal_text.push_str(&self.buffer),
100 + JsonToolCallMode::Text => output.normal_text.push_str(&self.buffer),
101 JsonToolCallMode::Header | JsonToolCallMode::Arguments { .. } => {
102 return Err(parsing_failed!(
103 "incomplete {} tool call",
@@ -108,19 +105,19 @@ impl JsonToolCallParser {
105 ));
106 }
107 }
111 - self.reset();
112 - Ok(result)
108 + let _ = self.reset();
109 + Ok(output)
110 }
111
112 /// Apply one parsed JSON tool-call event to parser state and output.
113 fn apply_event(
114 &mut self,
115 event: JsonToolCallEvent,
119 - result: &mut ToolParseResult,
116 + output: &mut ToolParserOutput,
117 ) -> Result<()> {
118 match event {
119 JsonToolCallEvent::Text { len: consumed_len } => {
123 - result.normal_text.push_str(&self.buffer[..consumed_len]);
120 + output.normal_text.push_str(&self.buffer[..consumed_len]);
121 }
122 JsonToolCallEvent::ToolCallStart => self.mode = JsonToolCallMode::Header,
123 JsonToolCallEvent::ToolCallHeader { function_name } => {
@@ -130,7 +127,7 @@ impl JsonToolCallParser {
127 self.mode = JsonToolCallMode::Arguments {
128 json_scan: JsonObjectScanState::default(),
129 };
133 - result.calls.push(ToolCallDelta {
130 + output.calls.push(ToolCallDelta {
131 tool_index,
132 name: Some(function_name),
133 arguments: String::new(),
@@ -143,7 +140,7 @@ impl JsonToolCallParser {
140 self.config.parser_name
141 ));
142 };
146 - result.calls.push(ToolCallDelta {
143 + output.calls.push(ToolCallDelta {
144 tool_index,
145 name: None,
146 arguments: self.buffer[..consumed_len].to_string(),
@@ -161,12 +158,11 @@ impl JsonToolCallParser {
158 Ok(())
159 }
160
164 - /// Reset all streaming state.
165 - fn reset(&mut self) {
166 - self.buffer.clear();
161 + fn reset(&mut self) -> String {
162 self.mode = JsonToolCallMode::Text;
163 self.active_tool_index = None;
164 self.emitted_tool_count = 0;
165 + std::mem::take(&mut self.buffer)
166 }
167 }
168
@@ -336,7 +332,7 @@ mod tests {
332 use expect_test::expect;
333
334 use super::{JsonToolCallConfig, JsonToolCallParser, JsonToolCallWhitespace};
339 - use crate::ToolParseResult;
335 + use crate::ToolParserOutput;
336
337 const DELIMITED_CONFIG: JsonToolCallConfig = JsonToolCallConfig {
338 parser_name: "Delimited JSON",
@@ -356,13 +352,13 @@ mod tests {
352 format!("<tool_calls>{}</tool_calls>", tool_calls.join(" <\n"))
353 }
354
359 - fn collect_chunks(parser: &mut JsonToolCallParser, chunks: &[&str]) -> ToolParseResult {
360 - let mut result = ToolParseResult::default();
355 + fn collect_chunks(parser: &mut JsonToolCallParser, chunks: &[&str]) -> ToolParserOutput {
356 + let mut output = ToolParserOutput::default();
357 for chunk in chunks {
362 - result.append(parser.push(chunk).unwrap());
358 + parser.parse_into(chunk, &mut output).unwrap();
359 }
364 - result.append(parser.finish().unwrap());
365 - result.coalesce_calls()
360 + output.append(parser.finish().unwrap());
361 + output.coalesce_calls()
362 }
363
364 #[test]
@@ -373,10 +369,10 @@ mod tests {
369 ]);
370 let mut parser = JsonToolCallParser::new(DELIMITED_CONFIG);
371
376 - let result = collect_chunks(&mut parser, &[&input]);
372 + let output = collect_chunks(&mut parser, &[&input]);
373
374 expect![[r#"
379 - ToolParseResult {
375 + ToolParserOutput {
376 normal_text: "",
377 calls: [
378 ToolCallDelta {
@@ -396,7 +392,7 @@ mod tests {
392 ],
393 }
394 "#]]
399 - .assert_debug_eq(&result);
395 + .assert_debug_eq(&output);
396 }
397
398 #[test]
@@ -409,10 +405,10 @@ mod tests {
405 "</tool_calls>",
406 ];
407
412 - let result = collect_chunks(&mut parser, &chunks);
408 + let output = collect_chunks(&mut parser, &chunks);
409
410 expect![[r#"
415 - ToolParseResult {
411 + ToolParserOutput {
412 normal_text: "",
413 calls: [
414 ToolCallDelta {
@@ -432,7 +428,7 @@ mod tests {
428 ],
429 }
430 "#]]
435 - .assert_debug_eq(&result);
431 + .assert_debug_eq(&output);
432 }
433
434 #[test]
@@ -444,10 +440,10 @@ mod tests {
440 "</tool_calls> trailing text",
441 ];
442
447 - let result = collect_chunks(&mut parser, &chunks);
443 + let output = collect_chunks(&mut parser, &chunks);
444
445 expect![[r#"
450 - ToolParseResult {
446 + ToolParserOutput {
447 normal_text: " trailing text",
448 calls: [
449 ToolCallDelta {
@@ -460,6 +456,6 @@ mod tests {
456 ],
457 }
458 "#]]
463 - .assert_debug_eq(&result);
459 + .assert_debug_eq(&output);
460 }
461 }
rust/src/tool-parser/src/json/qwen.rs
+42 -41
@@ -1,5 +1,5 @@
1 use super::{JsonToolCallConfig, JsonToolCallParser, JsonToolCallWhitespace};
2 -use crate::{Result, Tool, ToolParseResult, ToolParser};
2 +use crate::{Result, Tool, ToolParser, ToolParserOutput};
3
4 const QWEN_XML_CONFIG: JsonToolCallConfig = JsonToolCallConfig {
5 parser_name: "Qwen XML",
@@ -40,7 +40,6 @@ impl Qwen3XmlToolParser {
40 }
41
42 impl ToolParser for Qwen3XmlToolParser {
43 - /// Create a boxed Qwen XML tool parser.
43 fn create(tools: &[Tool]) -> Result<Box<dyn ToolParser>>
44 where
45 Self: Sized + 'static,
@@ -48,15 +47,17 @@ impl ToolParser for Qwen3XmlToolParser {
47 Ok(Box::new(Self::new(tools)))
48 }
49
51 - /// Push one decoded text chunk through the Qwen XML parser.
52 - fn push(&mut self, chunk: &str) -> Result<ToolParseResult> {
53 - self.inner.push(chunk)
50 + fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
51 + self.inner.parse_into(chunk, output)
52 }
53
56 - /// Flush buffered text and reset parser state.
57 - fn finish(&mut self) -> Result<ToolParseResult> {
54 + fn finish(&mut self) -> Result<ToolParserOutput> {
55 self.inner.finish()
56 }
57 +
58 + fn reset(&mut self) -> String {
59 + self.inner.reset()
60 + }
61 }
62
63 #[cfg(test)]
@@ -66,7 +67,7 @@ mod tests {
67
68 use super::Qwen3XmlToolParser;
69 use crate::test_utils::{collect_stream, split_by_chars, test_tools};
69 - use crate::{ToolParseResult, ToolParser};
70 + use crate::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
71
72 fn build_tool_call(function_name: &str, arguments: &str) -> String {
73 format!(
@@ -77,37 +78,37 @@ mod tests {
78 #[test]
79 fn qwen_xml_parse_complete_without_tool_call_keeps_text() {
80 let mut parser = Qwen3XmlToolParser::new(&test_tools());
80 - let result = parser.parse_complete("Hello, world!").unwrap();
81 + let output = parser.parse_complete("Hello, world!").unwrap();
82
82 - assert_eq!(result.normal_text, "Hello, world!");
83 - assert!(result.calls.is_empty());
83 + assert_eq!(output.normal_text, "Hello, world!");
84 + assert!(output.calls.is_empty());
85 }
86
87 #[test]
88 fn qwen_xml_parse_complete_extracts_raw_json_arguments() {
89 let mut parser = Qwen3XmlToolParser::new(&test_tools());
90 let arguments = r#"{ "location": "Tokyo", "days": "3" }"#;
90 - let result = parser
91 + let output = parser
92 .parse_complete(&format!(
93 "Let me check.\n{}",
94 build_tool_call("get_weather", arguments)
95 ))
96 .unwrap();
97
97 - assert_eq!(result.normal_text, "Let me check.\n");
98 - assert_eq!(result.calls.len(), 1);
99 - assert_eq!(result.calls[0].tool_index, 0);
100 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
101 - assert_eq!(result.calls[0].arguments, arguments);
98 + assert_eq!(output.normal_text, "Let me check.\n");
99 + assert_eq!(output.calls.len(), 1);
100 + assert_eq!(output.calls[0].tool_index, 0);
101 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
102 + assert_eq!(output.calls[0].arguments, arguments);
103 }
104
105 #[test]
106 fn qwen_xml_does_not_validate_or_normalize_arguments() {
107 let mut parser = Qwen3XmlToolParser::new(&test_tools());
108 let arguments = r#"{"location":"Tokyo",}"#;
108 - let result = parser.parse_complete(&build_tool_call("get_weather", arguments)).unwrap();
109 + let output = parser.parse_complete(&build_tool_call("get_weather", arguments)).unwrap();
110
110 - assert_eq!(result.calls[0].arguments, arguments);
111 + assert_eq!(output.calls[0].arguments, arguments);
112 }
113
114 #[test]
@@ -122,23 +123,23 @@ mod tests {
123 "}\n</tool_call>",
124 ];
125
125 - let mut result = ToolParseResult::default();
126 + let mut output = ToolParserOutput::default();
127 let mut observed_arguments = Vec::new();
128 for chunk in chunks {
128 - let next = parser.push(chunk).unwrap();
129 + let next = parser.parse_chunk(chunk).unwrap();
130 observed_arguments.extend(
131 next.calls
132 .iter()
133 .filter(|call| call.name.is_none())
134 .map(|call| call.arguments.clone()),
135 );
135 - result.append(next);
136 + output.append(next);
137 }
137 - result.append(parser.finish().unwrap());
138 + output.append(parser.finish().unwrap());
139
140 assert_eq!(observed_arguments, ["{\"location\":", "\"Beijing\"", "}"]);
141 assert_eq!(
141 - result.coalesce_calls().calls[0].arguments,
142 + output.coalesce_calls().calls[0].arguments,
143 r#"{"location":"Beijing"}"#
144 );
145 }
@@ -152,27 +153,27 @@ mod tests {
153 let chunks = split_by_chars(&input, 5);
154 let mut parser = Qwen3XmlToolParser::new(&test_tools());
155
155 - let result = collect_stream(&mut parser, &chunks);
156 + let output = collect_stream(&mut parser, &chunks);
157
157 - assert_eq!(result.normal_text, "hello ");
158 - assert_eq!(result.calls.len(), 1);
159 - assert_eq!(result.calls[0].arguments, r#"{"location":"Tokyo"}"#);
158 + assert_eq!(output.normal_text, "hello ");
159 + assert_eq!(output.calls.len(), 1);
160 + assert_eq!(output.calls[0].arguments, r#"{"location":"Tokyo"}"#);
161 }
162
163 #[test]
164 fn qwen_xml_keeps_end_marker_literal_inside_json_string() {
165 let mut parser = Qwen3XmlToolParser::new(&test_tools());
166 let arguments = r#"{"text":"literal </tool_call> inside"}"#;
166 - let result = parser.parse_complete(&build_tool_call("echo", arguments)).unwrap();
167 + let output = parser.parse_complete(&build_tool_call("echo", arguments)).unwrap();
168
168 - assert_eq!(result.calls.len(), 1);
169 - assert_eq!(result.calls[0].arguments, arguments);
169 + assert_eq!(output.calls.len(), 1);
170 + assert_eq!(output.calls[0].arguments, arguments);
171 }
172
173 #[test]
174 fn qwen_xml_decodes_escaped_function_name() {
175 let mut parser = Qwen3XmlToolParser::new(&test_tools());
175 - let result = parser
176 + let output = parser
177 .parse_complete(
178 r#"<tool_call>
179 {"name":"say_\"hi","arguments":{}}
@@ -180,7 +181,7 @@ mod tests {
181 )
182 .unwrap();
183
183 - assert_eq!(result.calls[0].name.as_deref(), Some("say_\"hi"));
184 + assert_eq!(output.calls[0].name.as_deref(), Some("say_\"hi"));
185 }
186
187 #[test]
@@ -189,10 +190,10 @@ mod tests {
190 let input = r#"<tool_call>{"name":"get_weather","arguments":{}}
191 </tool_call>"#;
192
192 - let result = parser.parse_complete(input).unwrap();
193 + let output = parser.parse_complete(input).unwrap();
194
194 - assert_eq!(result.normal_text, input);
195 - assert!(result.calls.is_empty());
195 + assert_eq!(output.normal_text, input);
196 + assert!(output.calls.is_empty());
197 }
198
199 #[test]
@@ -218,10 +219,10 @@ mod tests {
219 let chunks = split_by_chars(&input, 7);
220 let mut parser = Qwen3XmlToolParser::new(&test_tools());
221
221 - let result = collect_stream(&mut parser, &chunks);
222 + let output = collect_stream(&mut parser, &chunks);
223
224 expect![[r#"
224 - ToolParseResult {
225 + ToolParserOutput {
226 normal_text: "",
227 calls: [
228 ToolCallDelta {
@@ -241,14 +242,14 @@ mod tests {
242 ],
243 }
244 "#]]
244 - .assert_debug_eq(&result);
245 + .assert_debug_eq(&output);
246 }
247
248 #[test]
249 fn qwen_xml_finish_fails_incomplete_tool_call() {
250 let mut parser = Qwen3XmlToolParser::new(&test_tools());
251 parser
251 - .push(
252 + .parse_chunk(
253 r#"<tool_call>
254 {"name":"get_weather","arguments":{"location""#,
255 )
@@ -264,7 +265,7 @@ mod tests {
265 fn qwen_xml_malformed_field_order_fails_fast() {
266 let mut parser = Qwen3XmlToolParser::new(&test_tools());
267 let error = parser
267 - .push(
268 + .parse_chunk(
269 r#"<tool_call>
270 {"arguments":{},"name":"get_weather"}
271 </tool_call>"#,
rust/src/tool-parser/src/kimi_k2.rs
+56 -58
@@ -5,7 +5,7 @@ use winnow::stream::Partial;
5 use winnow::token::{literal, rest, take_until, take_while};
6
7 use super::utils::{JsonObjectScanState, parse_buffered_event, safe_text_len, take_json_object};
8 -use super::{Result, ToolCallDelta, ToolParseResult, ToolParser};
8 +use super::{Result, ToolCallDelta, ToolParser, ToolParserOutput};
9 use crate::Tool;
10
11 const TOOL_CALLS_START: &str = "<|tool_calls_section_begin|>";
@@ -73,10 +73,10 @@ impl KimiK2ToolParser {
73 }
74
75 /// Apply one parsed Kimi K2 event to parser state and output.
76 - fn apply_event(&mut self, event: KimiK2Event, result: &mut ToolParseResult) -> Result<()> {
76 + fn apply_event(&mut self, event: KimiK2Event, output: &mut ToolParserOutput) -> Result<()> {
77 match event {
78 KimiK2Event::Text { len: consumed_len } => {
79 - result.normal_text.push_str(&self.buffer[..consumed_len]);
79 + output.normal_text.push_str(&self.buffer[..consumed_len]);
80 }
81 KimiK2Event::ToolCallsStart => self.mode = KimiK2Mode::ToolBlock,
82 KimiK2Event::ToolCallStart => self.mode = KimiK2Mode::Header,
@@ -89,7 +89,7 @@ impl KimiK2ToolParser {
89 self.mode = KimiK2Mode::Arguments {
90 json_scan: JsonObjectScanState::default(),
91 };
92 - result.calls.push(ToolCallDelta {
92 + output.calls.push(ToolCallDelta {
93 tool_index,
94 name: Some(function_name),
95 arguments: String::new(),
@@ -101,7 +101,7 @@ impl KimiK2ToolParser {
101 "Kimi K2 arguments without an active tool call"
102 ));
103 };
104 - result.calls.push(ToolCallDelta {
104 + output.calls.push(ToolCallDelta {
105 tool_index,
106 name: None,
107 arguments: self.buffer[..consumed_len].to_string(),
@@ -120,16 +120,14 @@ impl KimiK2ToolParser {
120 Ok(())
121 }
122
123 - /// Reset all streaming state.
124 - fn reset(&mut self) {
125 - self.buffer.clear();
123 + fn reset(&mut self) -> String {
124 self.mode = KimiK2Mode::Text;
125 self.active_tool_index = None;
126 + std::mem::take(&mut self.buffer)
127 }
128 }
129
130 impl ToolParser for KimiK2ToolParser {
132 - /// Create a boxed Kimi K2 tool parser.
131 fn create(tools: &[Tool]) -> Result<Box<dyn ToolParser>>
132 where
133 Self: Sized + 'static,
@@ -137,38 +135,38 @@ impl ToolParser for KimiK2ToolParser {
135 Ok(Box::new(Self::new(tools)))
136 }
137
140 - /// Preserve Kimi K2 special-token markers while decoding.
138 fn preserve_special_tokens(&self) -> bool {
139 true
140 }
141
145 - /// Push one decoded text chunk through the Kimi K2 parser.
146 - fn push(&mut self, chunk: &str) -> Result<ToolParseResult> {
142 + fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
143 self.buffer.push_str(chunk);
148 - let mut result = ToolParseResult::default();
144
145 while let Some((event, consumed_len)) = parse_buffered_event(&self.buffer, |input| {
146 parse_next_kimi_k2_event(input, &mut self.mode)
147 })? {
153 - self.apply_event(event, &mut result)?;
148 + self.apply_event(event, output)?;
149 self.buffer.drain(..consumed_len);
150 }
151
157 - Ok(result)
152 + Ok(())
153 }
154
160 - /// Flush buffered text and reset parser state.
161 - fn finish(&mut self) -> Result<ToolParseResult> {
162 - let mut result = ToolParseResult::default();
155 + fn finish(&mut self) -> Result<ToolParserOutput> {
156 + let mut output = ToolParserOutput::default();
157 match &self.mode {
164 - KimiK2Mode::Text => result.normal_text.push_str(&self.buffer),
158 + KimiK2Mode::Text => output.normal_text.push_str(&self.buffer),
159 KimiK2Mode::ToolBlock | KimiK2Mode::Done => {}
160 KimiK2Mode::Header | KimiK2Mode::Arguments { .. } => {
161 return Err(parsing_failed!("incomplete Kimi K2 tool call"));
162 }
163 }
170 - self.reset();
171 - Ok(result)
164 + let _ = self.reset();
165 + Ok(output)
166 + }
167 +
168 + fn reset(&mut self) -> String {
169 + KimiK2ToolParser::reset(self)
170 }
171 }
172
@@ -323,8 +321,8 @@ mod tests {
321 KimiK2ToolParser, TOOL_CALL_ARGUMENT_START, TOOL_CALL_END, TOOL_CALL_START, TOOL_CALLS_END,
322 TOOL_CALLS_START, ToolParser, tool_header,
323 };
326 - use crate::ToolParseResult;
324 use crate::test_utils::{collect_stream, split_by_chars, test_tools};
325 + use crate::{ToolParserOutput, ToolParserTestExt as _};
326
327 fn build_tool_call(function_name: &str, index: usize, arguments: &str) -> String {
328 format!(
@@ -339,35 +337,35 @@ mod tests {
337 #[test]
338 fn kimi_k2_parse_complete_without_tool_call_keeps_text() {
339 let mut parser = KimiK2ToolParser::new(&test_tools());
342 - let result = parser.parse_complete("Hello, world!").unwrap();
340 + let output = parser.parse_complete("Hello, world!").unwrap();
341
344 - assert_eq!(result.normal_text, "Hello, world!");
345 - assert!(result.calls.is_empty());
342 + assert_eq!(output.normal_text, "Hello, world!");
343 + assert!(output.calls.is_empty());
344 }
345
346 #[test]
347 fn kimi_k2_parse_complete_extracts_raw_json_arguments() {
348 let mut parser = KimiK2ToolParser::new(&test_tools());
349 let arguments = r#"{ "location": "NYC", "days": "3" }"#;
352 - let result = parser
350 + let output = parser
351 .parse_complete(&format!(
352 "Checking. {} trailing text",
353 build_tool_section(&[build_tool_call("get_weather", 0, arguments)])
354 ))
355 .unwrap();
356
359 - assert_eq!(result.normal_text, "Checking. ");
360 - assert_eq!(result.calls.len(), 1);
361 - assert_eq!(result.calls[0].tool_index, 0);
362 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
363 - assert_eq!(result.calls[0].arguments, arguments);
357 + assert_eq!(output.normal_text, "Checking. ");
358 + assert_eq!(output.calls.len(), 1);
359 + assert_eq!(output.calls[0].tool_index, 0);
360 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
361 + assert_eq!(output.calls[0].arguments, arguments);
362 }
363
364 #[test]
365 fn kimi_k2_does_not_validate_or_normalize_arguments() {
366 let mut parser = KimiK2ToolParser::new(&test_tools());
367 let arguments = r#"{"location":"NYC",}"#;
370 - let result = parser
368 + let output = parser
369 .parse_complete(&build_tool_section(&[build_tool_call(
370 "get_weather",
371 0,
@@ -375,7 +373,7 @@ mod tests {
373 )]))
374 .unwrap();
375
378 - assert_eq!(result.calls[0].arguments, arguments);
376 + assert_eq!(output.calls[0].arguments, arguments);
377 }
378
379 #[test]
@@ -393,23 +391,23 @@ mod tests {
391 TOOL_CALLS_END,
392 ];
393
396 - let mut result = ToolParseResult::default();
394 + let mut output = ToolParserOutput::default();
395 let mut observed_arguments = Vec::new();
396 for chunk in chunks {
399 - let next = parser.push(chunk).unwrap();
397 + let next = parser.parse_chunk(chunk).unwrap();
398 observed_arguments.extend(
399 next.calls
400 .iter()
401 .filter(|call| call.name.is_none())
402 .map(|call| call.arguments.clone()),
403 );
406 - result.append(next);
404 + output.append(next);
405 }
408 - result.append(parser.finish().unwrap());
406 + output.append(parser.finish().unwrap());
407
408 assert_eq!(observed_arguments, ["{\"location\":", "\"Paris\"", "}"]);
411 - let result = result.coalesce_calls();
412 - assert_eq!(result.calls[0].arguments, r#"{"location":"Paris"}"#);
409 + let output = output.coalesce_calls();
410 + assert_eq!(output.calls[0].arguments, r#"{"location":"Paris"}"#);
411 }
412
413 #[test]
@@ -427,11 +425,11 @@ mod tests {
425 TOOL_CALLS_END,
426 ];
427
430 - let result = collect_stream(&mut parser, &chunks);
428 + let output = collect_stream(&mut parser, &chunks);
429
432 - assert_eq!(result.normal_text, "hello ");
433 - assert_eq!(result.calls.len(), 1);
434 - assert_eq!(result.calls[0].arguments, r#"{"location":"NYC"}"#);
430 + assert_eq!(output.normal_text, "hello ");
431 + assert_eq!(output.calls.len(), 1);
432 + assert_eq!(output.calls[0].arguments, r#"{"location":"NYC"}"#);
433 }
434
435 #[test]
@@ -440,10 +438,10 @@ mod tests {
438 let arguments = format!(r#"{{"text":"literal {TOOL_CALL_END} inside"}}"#);
439 let input = build_tool_section(&[build_tool_call("echo", 0, &arguments)]);
440
443 - let result = parser.parse_complete(&input).unwrap();
441 + let output = parser.parse_complete(&input).unwrap();
442
445 - assert_eq!(result.calls.len(), 1);
446 - assert_eq!(result.calls[0].arguments, arguments);
443 + assert_eq!(output.calls.len(), 1);
444 + assert_eq!(output.calls[0].arguments, arguments);
445 }
446
447 #[test]
@@ -460,11 +458,11 @@ mod tests {
458 TOOL_CALLS_END,
459 ];
460
463 - let result = collect_stream(&mut parser, &chunks);
461 + let output = collect_stream(&mut parser, &chunks);
462
465 - assert_eq!(result.calls.len(), 1);
463 + assert_eq!(output.calls.len(), 1);
464 assert_eq!(
467 - result.calls[0].arguments,
465 + output.calls[0].arguments,
466 r#"{"text":"literal <|tool_call_end|> inside"}"#
467 );
468 }
@@ -478,10 +476,10 @@ mod tests {
476 ]);
477
478 let chunks = split_by_chars(&input, 7);
481 - let result = collect_stream(&mut parser, &chunks);
479 + let output = collect_stream(&mut parser, &chunks);
480
481 expect![[r#"
484 - ToolParseResult {
482 + ToolParserOutput {
483 normal_text: "",
484 calls: [
485 ToolCallDelta {
@@ -501,7 +499,7 @@ mod tests {
499 ],
500 }
501 "#]]
504 - .assert_debug_eq(&result);
502 + .assert_debug_eq(&output);
503 }
504
505 #[test]
@@ -511,11 +509,11 @@ mod tests {
509 "{TOOL_CALLS_START}{TOOL_CALL_START}api.tools.search:42{TOOL_CALL_ARGUMENT_START}{{}}{TOOL_CALL_END}{TOOL_CALLS_END}"
510 );
511
514 - let result = parser.parse_complete(&input).unwrap();
512 + let output = parser.parse_complete(&input).unwrap();
513
516 - assert_eq!(result.calls[0].tool_index, 42);
517 - assert_eq!(result.calls[0].name.as_deref(), Some("search"));
518 - assert_eq!(result.calls[0].arguments, "{}");
514 + assert_eq!(output.calls[0].tool_index, 42);
515 + assert_eq!(output.calls[0].name.as_deref(), Some("search"));
516 + assert_eq!(output.calls[0].arguments, "{}");
517 }
518
519 #[test]
@@ -536,7 +534,7 @@ mod tests {
534 fn kimi_k2_finish_fails_incomplete_tool_call() {
535 let mut parser = KimiK2ToolParser::new(&test_tools());
536 parser
539 - .push(&format!(
537 + .parse_chunk(&format!(
538 "{TOOL_CALLS_START}{TOOL_CALL_START}functions.get_weather:0{TOOL_CALL_ARGUMENT_START}{{\"location\""
539 ))
540 .unwrap();
@@ -553,7 +551,7 @@ mod tests {
551 let input =
552 format!("{TOOL_CALLS_START}{TOOL_CALL_START}get_weather{TOOL_CALL_ARGUMENT_START}{{}}");
553
556 - let error = parser.push(&input).unwrap_err();
554 + let error = parser.parse_chunk(&input).unwrap_err();
555
556 expect!["tool parser parsing failed: "].assert_eq(&error.to_report_string());
557 }
rust/src/tool-parser/src/lib.rs
+48 -17
@@ -51,20 +51,20 @@ pub struct ToolCallDelta {
51
52 /// Result of advancing tool parsing with one assistant-text input.
53 #[derive(Debug, Clone, Default, PartialEq, Eq)]
54 -pub struct ToolParseResult {
54 +pub struct ToolParserOutput {
55 /// Plain assistant text that is not part of any tool call.
56 pub normal_text: String,
57 /// Tool-call updates extracted from this input.
58 pub calls: Vec<ToolCallDelta>,
59 }
60
61 -impl ToolParseResult {
62 - /// Append another parser result onto this one.
61 +impl ToolParserOutput {
62 + /// Append another parser output onto this one.
63 ///
64 /// Note that this does not attempt to merge multiple deltas for the same
65 /// tool call into one complete item. Call `coalesce_calls()` after if
66 /// that behavior is desired.
67 - pub(crate) fn append(&mut self, mut other: Self) {
67 + pub fn append(&mut self, mut other: Self) {
68 self.normal_text.push_str(&other.normal_text);
69 self.calls.append(&mut other.calls);
70 }
@@ -75,7 +75,7 @@ impl ToolParseResult {
75 /// which delegates through the incremental parser lifecycle and then
76 /// needs to collapse streaming-style argument fragments into one final
77 /// tool call.
78 - pub(crate) fn coalesce_calls(mut self) -> Self {
78 + pub fn coalesce_calls(mut self) -> Self {
79 let mut merged = BTreeMap::<usize, ToolCallDelta>::new();
80 let mut order = Vec::new();
81
@@ -116,24 +116,55 @@ pub trait ToolParser: Send {
116 false
117 }
118
119 - /// Feed one decoded text delta into the parser.
120 - fn push(&mut self, chunk: &str) -> Result<ToolParseResult>;
119 + /// Feed one decoded text delta into the parser, appending committed output
120 + /// into `output`.
121 + ///
122 + /// If this returns an error, any output already appended to `output`
123 + /// remains committed parser output. The parser must keep its uncommitted
124 + /// buffer intact so callers may recover it with `reset()`.
125 + fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()>;
126
127 /// Flush any buffered partial state at end of stream.
123 - fn finish(&mut self) -> Result<ToolParseResult> {
124 - Ok(ToolParseResult::default())
128 + ///
129 + /// This operation is atomic: on error no partial output is returned and the
130 + /// parser's buffered state is left intact.
131 + fn finish(&mut self) -> Result<ToolParserOutput>;
132 +
133 + /// Clear parser state and return currently uncommitted buffered text.
134 + ///
135 + /// Callers may use this to recover any text that failed to parse after an error
136 + /// and output it as normal text.
137 + fn reset(&mut self) -> String;
138 +}
139 +
140 +/// Extension methods for easily testing `ToolParser` implementations.
141 +///
142 +/// These helpers do not handle partial parsing or error recovery, so they are
143 +/// not intended for use in production code paths.
144 +#[cfg(any(test, feature = "test-util"))]
145 +#[easy_ext::ext(ToolParserTestExt)]
146 +impl<T: ToolParser + ?Sized> T {
147 + /// Feed one decoded text delta and return only if the whole chunk parses.
148 + ///
149 + /// If parsing fails, partial committed output is discarded by this helper.
150 + /// Prefer `parse_into` for more fine-grained control in error recovery.
151 + pub fn parse_chunk(&mut self, chunk: &str) -> Result<ToolParserOutput> {
152 + let mut output = ToolParserOutput::default();
153 + self.parse_into(chunk, &mut output)?;
154 + Ok(output)
155 }
156
157 /// Parse complete tool calls from final output.
158 ///
129 - /// The default implementation reuses the incremental parser lifecycle by
130 - /// feeding the full output through `push()` and then calling `finish()`.
131 - /// This keeps one source of truth for robust parsers whose incremental
132 - /// state machine is equivalent across arbitrary chunking.
133 - fn parse_complete(&mut self, output: &str) -> Result<ToolParseResult> {
134 - let mut result = self.push(output)?;
135 - result.append(self.finish()?);
136 - Ok(result.coalesce_calls())
159 + /// This default implementation reuses the incremental parser lifecycle by
160 + /// feeding the full output through `parse_chunk()` and then calling `finish()`.
161 + ///
162 + /// If parsing fails, partial committed output is discarded by this helper.
163 + /// Prefer `parse_into` for more fine-grained control in error recovery.
164 + pub fn parse_complete(&mut self, text: &str) -> Result<ToolParserOutput> {
165 + let mut output = self.parse_chunk(text)?;
166 + output.append(self.finish()?);
167 + Ok(output.coalesce_calls())
168 }
169 }
170
rust/src/tool-parser/src/minimax_m2.rs
+70 -69
@@ -6,7 +6,7 @@ use winnow::token::{literal, rest, take_until};
6
7 use super::parameters::ToolSchemas;
8 use super::utils::{parse_buffered_event, safe_text_len, xml_unescape};
9 -use super::{Result, ToolCallDelta, ToolParseResult, ToolParser};
9 +use super::{Result, ToolCallDelta, ToolParser, ToolParserOutput};
10 use crate::Tool;
11
12 const TOOL_CALL_START: &str = "<minimax:tool_call>";
@@ -69,10 +69,10 @@ impl MinimaxM2ToolParser {
69 }
70
71 /// Apply one parsed MiniMax M2 event to parser state and output.
72 - fn apply_event(&mut self, event: MinimaxM2Event, result: &mut ToolParseResult) -> Result<()> {
72 + fn apply_event(&mut self, event: MinimaxM2Event, output: &mut ToolParserOutput) -> Result<()> {
73 match event {
74 MinimaxM2Event::Text { len: consumed_len } => {
75 - result.normal_text.push_str(&self.buffer[..consumed_len]);
75 + output.normal_text.push_str(&self.buffer[..consumed_len]);
76 }
77 MinimaxM2Event::ToolBlockStart => self.mode = MinimaxM2Mode::ToolBlock,
78 MinimaxM2Event::Invoke { name, raw_params } => {
@@ -80,7 +80,7 @@ impl MinimaxM2ToolParser {
80 let arguments = serde_json::to_string(&arguments)
81 .map_err(|error| parsing_failed!("failed to serialize arguments: {}", error))?;
82
83 - result.calls.push(ToolCallDelta {
83 + output.calls.push(ToolCallDelta {
84 tool_index: self.emitted_tool_count,
85 name: Some(name),
86 arguments,
@@ -93,16 +93,14 @@ impl MinimaxM2ToolParser {
93 Ok(())
94 }
95
96 - /// Reset all streaming state.
97 - fn reset(&mut self) {
98 - self.buffer.clear();
96 + fn reset(&mut self) -> String {
97 self.mode = MinimaxM2Mode::Text;
98 self.emitted_tool_count = 0;
99 + std::mem::take(&mut self.buffer)
100 }
101 }
102
103 impl ToolParser for MinimaxM2ToolParser {
105 - /// Create a boxed MiniMax M2 tool parser.
104 fn create(tools: &[Tool]) -> Result<Box<dyn ToolParser>>
105 where
106 Self: Sized + 'static,
@@ -110,35 +108,36 @@ impl ToolParser for MinimaxM2ToolParser {
108 Ok(Box::new(Self::new(tools)))
109 }
110
113 - /// Push one decoded text chunk through the MiniMax M2 parser.
114 - fn push(&mut self, chunk: &str) -> Result<ToolParseResult> {
111 + fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
112 self.buffer.push_str(chunk);
116 - let mut result = ToolParseResult::default();
113
114 while let Some((event, consumed_len)) = parse_buffered_event(&self.buffer, |input| {
115 parse_next_minimax_m2_event(input, self.mode)
116 })? {
121 - self.apply_event(event, &mut result)?;
117 + self.apply_event(event, output)?;
118 self.buffer.drain(..consumed_len);
119 }
120
125 - Ok(result)
121 + Ok(())
122 }
123
128 - /// Flush buffered text and reset parser state.
129 - fn finish(&mut self) -> Result<ToolParseResult> {
130 - let mut result = ToolParseResult::default();
124 + fn finish(&mut self) -> Result<ToolParserOutput> {
125 + let mut output = ToolParserOutput::default();
126 match self.mode {
127 MinimaxM2Mode::Text => {
133 - result.normal_text.push_str(&self.buffer);
128 + output.normal_text.push_str(&self.buffer);
129 }
130 MinimaxM2Mode::ToolBlock => {
131 return Err(parsing_failed!("incomplete MiniMax M2 tool call"));
132 }
133 MinimaxM2Mode::Done => {}
134 }
140 - self.reset();
141 - Ok(result)
135 + let _ = self.reset();
136 + Ok(output)
137 + }
138 +
139 + fn reset(&mut self) -> String {
140 + MinimaxM2ToolParser::reset(self)
141 }
142 }
143
@@ -254,6 +253,7 @@ mod tests {
253 use thiserror_ext::AsReport;
254
255 use super::{MinimaxM2ToolParser, TOOL_CALL_END, TOOL_CALL_START, ToolParser};
256 + use crate::ToolParserTestExt as _;
257 use crate::test_utils::{collect_stream, split_by_chars, test_tools};
258
259 fn build_tool_block(invokes: &[(&str, Vec<(&str, &str)>)]) -> String {
@@ -274,27 +274,27 @@ mod tests {
274 #[test]
275 fn minimax_m2_parse_complete_without_tool_call_keeps_text() {
276 let mut parser = MinimaxM2ToolParser::new(&test_tools());
277 - let result = parser.parse_complete("Hello, world!").unwrap();
277 + let output = parser.parse_complete("Hello, world!").unwrap();
278
279 - assert_eq!(result.normal_text, "Hello, world!");
280 - assert!(result.calls.is_empty());
279 + assert_eq!(output.normal_text, "Hello, world!");
280 + assert!(output.calls.is_empty());
281 }
282
283 #[test]
284 fn minimax_m2_parse_complete_extracts_single_tool_call() {
285 let mut parser = MinimaxM2ToolParser::new(&test_tools());
286 - let result = parser
286 + let output = parser
287 .parse_complete(&build_tool_block(&[(
288 "get_weather",
289 vec![("city", "Seattle"), ("days", "5")],
290 )]))
291 .unwrap();
292
293 - assert!(result.normal_text.is_empty());
294 - assert_eq!(result.calls.len(), 1);
295 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
293 + assert!(output.normal_text.is_empty());
294 + assert_eq!(output.calls.len(), 1);
295 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
296 assert_eq!(
297 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
297 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
298 json!({ "city": "Seattle", "days": 5 })
299 );
300 }
@@ -306,31 +306,31 @@ mod tests {
306 "Let me check. {} This trailing text is ignored.",
307 build_tool_block(&[("get_weather", vec![("city", "Seattle")])])
308 );
309 - let result = parser.parse_complete(&output).unwrap();
309 + let output = parser.parse_complete(&output).unwrap();
310
311 - assert_eq!(result.normal_text, "Let me check. ");
312 - assert_eq!(result.calls.len(), 1);
311 + assert_eq!(output.normal_text, "Let me check. ");
312 + assert_eq!(output.calls.len(), 1);
313 }
314
315 #[test]
316 fn minimax_m2_parse_complete_extracts_multiple_invokes() {
317 let mut parser = MinimaxM2ToolParser::new(&test_tools());
318 - let result = parser
318 + let output = parser
319 .parse_complete(&build_tool_block(&[
320 ("get_weather", vec![("city", "Seattle")]),
321 ("get_weather", vec![("city", "NYC")]),
322 ]))
323 .unwrap();
324
325 - assert_eq!(result.calls.len(), 2);
326 - assert_eq!(result.calls[0].tool_index, 0);
327 - assert_eq!(result.calls[1].tool_index, 1);
325 + assert_eq!(output.calls.len(), 2);
326 + assert_eq!(output.calls[0].tool_index, 0);
327 + assert_eq!(output.calls[1].tool_index, 1);
328 assert_eq!(
329 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
329 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
330 json!({ "city": "Seattle" })
331 );
332 assert_eq!(
333 - serde_json::from_str::<Value>(&result.calls[1].arguments).unwrap(),
333 + serde_json::from_str::<Value>(&output.calls[1].arguments).unwrap(),
334 json!({ "city": "NYC" })
335 );
336 }
@@ -338,7 +338,7 @@ mod tests {
338 #[test]
339 fn minimax_m2_parse_complete_converts_schema_types() {
340 let mut parser = MinimaxM2ToolParser::new(&test_tools());
341 - let result = parser
341 + let output = parser
342 .parse_complete(&build_tool_block(&[(
343 "convert",
344 vec![
@@ -352,7 +352,7 @@ mod tests {
352 .unwrap();
353
354 assert_eq!(
355 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
355 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
356 json!({
357 "whole": 5.0,
358 "flag": true,
@@ -366,7 +366,7 @@ mod tests {
366 #[test]
367 fn minimax_m2_parse_complete_unescapes_literal_closing_tags_in_parameter_value() {
368 let mut parser = MinimaxM2ToolParser::new(&test_tools());
369 - let result = parser
369 + let output = parser
370 .parse_complete(&build_tool_block(&[(
371 "get_weather",
372 vec![
@@ -380,7 +380,7 @@ mod tests {
380 .unwrap();
381
382 assert_eq!(
383 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
383 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
384 json!({
385 "city": "Seattle </parameter></invoke></minimax:tool_call>",
386 "days": 5,
@@ -391,7 +391,7 @@ mod tests {
391 #[test]
392 fn minimax_m2_parse_complete_handles_multiline_parameters() {
393 let mut parser = MinimaxM2ToolParser::new(&test_tools());
394 - let result = parser
394 + let output = parser
395 .parse_complete(
396 "<minimax:tool_call>\
397 <invoke name=\"calculate_area\">\
@@ -404,7 +404,7 @@ mod tests {
404 .unwrap();
405
406 assert_eq!(
407 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
407 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
408 json!({
409 "shape": "\nrectangle\n",
410 "dimensions": { "width": 10, "height": 20 },
@@ -416,7 +416,7 @@ mod tests {
416 #[test]
417 fn minimax_m2_streaming_extracts_single_tool_call() {
418 let mut parser = MinimaxM2ToolParser::new(&test_tools());
419 - let result = collect_stream(
419 + let output = collect_stream(
420 &mut parser,
421 &[
422 "<minimax:tool_call>",
@@ -426,11 +426,11 @@ mod tests {
426 ],
427 );
428
429 - assert!(result.normal_text.is_empty());
430 - assert_eq!(result.calls.len(), 1);
431 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
429 + assert!(output.normal_text.is_empty());
430 + assert_eq!(output.calls.len(), 1);
431 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
432 assert_eq!(
433 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
433 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
434 json!({ "city": "Seattle" })
435 );
436 }
@@ -438,7 +438,7 @@ mod tests {
438 #[test]
439 fn minimax_m2_streaming_preserves_prefix_text() {
440 let mut parser = MinimaxM2ToolParser::new(&test_tools());
441 - let result = collect_stream(
441 + let output = collect_stream(
442 &mut parser,
443 &[
444 "Let me check. ",
@@ -448,17 +448,17 @@ mod tests {
448 ],
449 );
450
451 - assert_eq!(result.normal_text, "Let me check. ");
452 - assert_eq!(result.calls.len(), 1);
451 + assert_eq!(output.normal_text, "Let me check. ");
452 + assert_eq!(output.calls.len(), 1);
453 }
454
455 #[test]
456 fn minimax_m2_streaming_without_tool_call_emits_text_incrementally() {
457 let mut parser = MinimaxM2ToolParser::new(&test_tools());
458 - let result = collect_stream(&mut parser, &["Hello, ", "world!"]);
458 + let output = collect_stream(&mut parser, &["Hello, ", "world!"]);
459
460 - assert_eq!(result.normal_text, "Hello, world!");
461 - assert!(result.calls.is_empty());
460 + assert_eq!(output.normal_text, "Hello, world!");
461 + assert!(output.calls.is_empty());
462 }
463
464 #[test]
@@ -466,10 +466,10 @@ mod tests {
466 let text = build_tool_block(&[("get_weather", vec![("city", "Seattle")])]);
467 let chunks = split_by_chars(&text, 3);
468 let mut parser = MinimaxM2ToolParser::new(&test_tools());
469 - let result = collect_stream(&mut parser, &chunks);
469 + let output = collect_stream(&mut parser, &chunks);
470
471 - assert_eq!(result.calls.len(), 1);
472 - assert!(result.normal_text.is_empty());
471 + assert_eq!(output.calls.len(), 1);
472 + assert!(output.normal_text.is_empty());
473 }
474
475 #[test]
@@ -480,11 +480,11 @@ mod tests {
480 ]);
481 let chunks = split_by_chars(&text, 7);
482 let mut parser = MinimaxM2ToolParser::new(&test_tools());
483 - let result = collect_stream(&mut parser, &chunks);
483 + let output = collect_stream(&mut parser, &chunks);
484
485 - assert_eq!(result.calls.len(), 2);
486 - assert_eq!(result.calls[0].tool_index, 0);
487 - assert_eq!(result.calls[1].tool_index, 1);
485 + assert_eq!(output.calls.len(), 2);
486 + assert_eq!(output.calls[0].tool_index, 0);
487 + assert_eq!(output.calls[1].tool_index, 1);
488 }
489
490 #[test]
@@ -520,25 +520,26 @@ mod tests {
520 );
521 let chunks = split_by_chars(&text, 5);
522 let mut parser = MinimaxM2ToolParser::new(&test_tools());
523 - let result = collect_stream(&mut parser, &chunks);
523 + let output = collect_stream(&mut parser, &chunks);
524
525 - assert!(result.normal_text.is_empty());
526 - assert_eq!(result.calls.len(), 1);
525 + assert!(output.normal_text.is_empty());
526 + assert_eq!(output.calls.len(), 1);
527 }
528
529 #[test]
530 fn minimax_m2_streaming_does_not_emit_incomplete_tool_call() {
531 let mut parser = MinimaxM2ToolParser::new(&test_tools());
532 - let result = parser.push(r#"<minimax:tool_call><invoke name="get_weather">"#).unwrap();
532 + let output =
533 + parser.parse_chunk(r#"<minimax:tool_call><invoke name="get_weather">"#).unwrap();
534
534 - assert!(result.normal_text.is_empty());
535 - assert!(result.calls.is_empty());
535 + assert!(output.normal_text.is_empty());
536 + assert!(output.calls.is_empty());
537 }
538
539 #[test]
540 fn minimax_m2_finish_fails_incomplete_tool_call() {
541 let mut parser = MinimaxM2ToolParser::new(&test_tools());
541 - parser.push(r#"<minimax:tool_call><invoke name="get_weather">"#).unwrap();
542 + parser.parse_chunk(r#"<minimax:tool_call><invoke name="get_weather">"#).unwrap();
543
544 assert!(parser.finish().is_err());
545 }
@@ -546,7 +547,7 @@ mod tests {
547 #[test]
548 fn minimax_m2_finish_fails_after_bare_tool_block_start() {
549 let mut parser = MinimaxM2ToolParser::new(&test_tools());
549 - parser.push("<minimax:tool_call>").unwrap();
550 + parser.parse_chunk("<minimax:tool_call>").unwrap();
551
552 assert!(parser.finish().is_err());
553 }
@@ -554,7 +555,7 @@ mod tests {
555 #[test]
556 fn minimax_m2_malformed_tool_call_fails_fast() {
557 let mut parser = MinimaxM2ToolParser::new(&test_tools());
557 - let error = parser.push("<minimax:tool_call><bad></minimax:tool_call>").unwrap_err();
558 + let error = parser.parse_chunk("<minimax:tool_call><bad></minimax:tool_call>").unwrap_err();
559
560 expect!["tool parser parsing failed: "].assert_eq(&error.to_report_string());
561 }
rust/src/tool-parser/src/qwen_coder.rs
+89 -89
@@ -6,7 +6,7 @@ use winnow::token::{literal, take_until};
6
7 use super::parameters::ToolSchemas;
8 use super::utils::{parse_buffered_event, safe_text_len, xml_unescape};
9 -use super::{Result, ToolCallDelta, ToolParseResult, ToolParser};
9 +use super::{Result, ToolCallDelta, ToolParser, ToolParserOutput};
10 use crate::Tool;
11
12 const TOOL_CALL_START: &str = "<tool_call>";
@@ -71,10 +71,10 @@ impl Qwen3CoderToolParser {
71 }
72
73 /// Apply one parsed Qwen Coder event to parser state and output.
74 - fn apply_event(&mut self, event: QwenCoderEvent, result: &mut ToolParseResult) -> Result<()> {
74 + fn apply_event(&mut self, event: QwenCoderEvent, output: &mut ToolParserOutput) -> Result<()> {
75 match event {
76 QwenCoderEvent::Text { len: consumed_len } => {
77 - result.normal_text.push_str(&self.buffer[..consumed_len]);
77 + output.normal_text.push_str(&self.buffer[..consumed_len]);
78 }
79 QwenCoderEvent::ToolCallStart => self.mode = QwenCoderMode::ToolCall,
80 QwenCoderEvent::ToolCall { name, raw_params } => {
@@ -83,7 +83,7 @@ impl Qwen3CoderToolParser {
83 let arguments = serde_json::to_string(&arguments)
84 .map_err(|error| parsing_failed!("failed to serialize arguments: {}", error))?;
85
86 - result.calls.push(ToolCallDelta {
86 + output.calls.push(ToolCallDelta {
87 tool_index: self.emitted_tool_count,
88 name: Some(name),
89 arguments,
@@ -94,16 +94,14 @@ impl Qwen3CoderToolParser {
94 Ok(())
95 }
96
97 - /// Reset all streaming state.
98 - fn reset(&mut self) {
99 - self.buffer.clear();
97 + fn reset(&mut self) -> String {
98 self.mode = QwenCoderMode::Text;
99 self.emitted_tool_count = 0;
100 + std::mem::take(&mut self.buffer)
101 }
102 }
103
104 impl ToolParser for Qwen3CoderToolParser {
106 - /// Create a boxed Qwen Coder tool parser.
105 fn create(tools: &[Tool]) -> Result<Box<dyn ToolParser>>
106 where
107 Self: Sized + 'static,
@@ -111,32 +109,33 @@ impl ToolParser for Qwen3CoderToolParser {
109 Ok(Box::new(Self::new(tools)))
110 }
111
114 - /// Push one decoded text chunk through the Qwen Coder parser.
115 - fn push(&mut self, chunk: &str) -> Result<ToolParseResult> {
112 + fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
113 self.buffer.push_str(chunk);
117 - let mut result = ToolParseResult::default();
114
115 while let Some((event, consumed_len)) = parse_buffered_event(&self.buffer, |input| {
116 parse_next_qwen_coder_event(input, self.mode)
117 })? {
122 - self.apply_event(event, &mut result)?;
118 + self.apply_event(event, output)?;
119 self.buffer.drain(..consumed_len);
120 }
121
126 - Ok(result)
122 + Ok(())
123 }
124
129 - /// Flush buffered text and reset parser state.
130 - fn finish(&mut self) -> Result<ToolParseResult> {
131 - let mut result = ToolParseResult::default();
125 + fn finish(&mut self) -> Result<ToolParserOutput> {
126 + let mut output = ToolParserOutput::default();
127 if !self.buffer.is_empty() {
128 if self.mode == QwenCoderMode::ToolCall || self.buffer.starts_with(TOOL_CALL_START) {
129 return Err(parsing_failed!("incomplete Qwen Coder tool call"));
130 }
136 - result.normal_text.push_str(&self.buffer);
131 + output.normal_text.push_str(&self.buffer);
132 }
138 - self.reset();
139 - Ok(result)
133 + let _ = self.reset();
134 + Ok(output)
135 + }
136 +
137 + fn reset(&mut self) -> String {
138 + Qwen3CoderToolParser::reset(self)
139 }
140 }
141
@@ -229,6 +228,7 @@ mod tests {
228 use thiserror_ext::AsReport;
229
230 use super::{Qwen3CoderToolParser, ToolParser};
231 + use crate::ToolParserTestExt as _;
232 use crate::test_utils::{collect_stream, split_by_chars, test_tools};
233
234 fn build_tool_call(function_name: &str, params: &[(&str, &str)]) -> String {
@@ -243,27 +243,27 @@ mod tests {
243 #[test]
244 fn qwen_coder_parse_complete_without_tool_call_keeps_text() {
245 let mut parser = Qwen3CoderToolParser::new(&test_tools());
246 - let result = parser.parse_complete("Hello, world!").unwrap();
246 + let output = parser.parse_complete("Hello, world!").unwrap();
247
248 - assert_eq!(result.normal_text, "Hello, world!");
249 - assert!(result.calls.is_empty());
248 + assert_eq!(output.normal_text, "Hello, world!");
249 + assert!(output.calls.is_empty());
250 }
251
252 #[test]
253 fn qwen_coder_parse_complete_extracts_single_tool_call() {
254 let mut parser = Qwen3CoderToolParser::new(&test_tools());
255 - let result = parser
255 + let output = parser
256 .parse_complete(&build_tool_call(
257 "get_weather",
258 &[("location", "SF"), ("date", "2026-04-29")],
259 ))
260 .unwrap();
261
262 - assert!(result.normal_text.is_empty());
263 - assert_eq!(result.calls.len(), 1);
264 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
262 + assert!(output.normal_text.is_empty());
263 + assert_eq!(output.calls.len(), 1);
264 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
265 assert_eq!(
266 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
266 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
267 json!({
268 "location": "SF",
269 "date": "2026-04-29"
@@ -278,16 +278,16 @@ mod tests {
278 "Thinking... {}",
279 build_tool_call("get_weather", &[("location", "NYC")])
280 );
281 - let result = parser.parse_complete(&output).unwrap();
281 + let output = parser.parse_complete(&output).unwrap();
282
283 - assert_eq!(result.normal_text, "Thinking... ");
284 - assert_eq!(result.calls.len(), 1);
283 + assert_eq!(output.normal_text, "Thinking... ");
284 + assert_eq!(output.calls.len(), 1);
285 }
286
287 #[test]
288 fn qwen_coder_parse_complete_converts_schema_types() {
289 let mut parser = Qwen3CoderToolParser::new(&test_tools());
290 - let result = parser
290 + let output = parser
291 .parse_complete(&build_tool_call(
292 "convert",
293 &[
@@ -300,9 +300,9 @@ mod tests {
300 ))
301 .unwrap();
302
303 - assert_eq!(result.calls.len(), 1);
303 + assert_eq!(output.calls.len(), 1);
304 assert_eq!(
305 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
305 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
306 json!({
307 "whole": 5.0,
308 "flag": true,
@@ -316,12 +316,12 @@ mod tests {
316 #[test]
317 fn qwen_coder_parse_complete_extracts_empty_arguments() {
318 let mut parser = Qwen3CoderToolParser::new(&test_tools());
319 - let result = parser.parse_complete(&build_tool_call("get_weather", &[])).unwrap();
319 + let output = parser.parse_complete(&build_tool_call("get_weather", &[])).unwrap();
320
321 - assert_eq!(result.calls.len(), 1);
322 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
321 + assert_eq!(output.calls.len(), 1);
322 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
323 assert_eq!(
324 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
324 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
325 json!({})
326 );
327 }
@@ -329,7 +329,7 @@ mod tests {
329 #[test]
330 fn qwen_coder_parse_complete_handles_upstream_multiline_typed_params() {
331 let mut parser = Qwen3CoderToolParser::new(&test_tools());
332 - let result = parser
332 + let output = parser
333 .parse_complete(
334 "<tool_call>\n\
335 <function=calculate_area>\n\
@@ -348,10 +348,10 @@ mod tests {
348 )
349 .unwrap();
350
351 - assert_eq!(result.calls.len(), 1);
352 - assert_eq!(result.calls[0].name.as_deref(), Some("calculate_area"));
351 + assert_eq!(output.calls.len(), 1);
352 + assert_eq!(output.calls[0].name.as_deref(), Some("calculate_area"));
353 assert_eq!(
354 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
354 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
355 json!({
356 "shape": "rectangle",
357 "dimensions": { "width": 10, "height": 20 },
@@ -363,7 +363,7 @@ mod tests {
363 #[test]
364 fn qwen_coder_parse_complete_handles_nested_json_parameter() {
365 let mut parser = Qwen3CoderToolParser::new(&test_tools());
366 - let result = parser
366 + let output = parser
367 .parse_complete(&build_tool_call(
368 "convert",
369 &[(
@@ -373,9 +373,9 @@ mod tests {
373 ))
374 .unwrap();
375
376 - assert_eq!(result.calls.len(), 1);
376 + assert_eq!(output.calls.len(), 1);
377 assert_eq!(
378 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
378 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
379 json!({
380 "payload": {
381 "nested": {
@@ -390,7 +390,7 @@ mod tests {
390 #[test]
391 fn qwen_coder_parse_complete_preserves_xml_like_parameter_values() {
392 let mut parser = Qwen3CoderToolParser::new(&test_tools());
393 - let result = parser
393 + let output = parser
394 .parse_complete(&build_tool_call(
395 "process",
396 &[
@@ -403,9 +403,9 @@ mod tests {
403 ))
404 .unwrap();
405
406 - assert_eq!(result.calls.len(), 1);
406 + assert_eq!(output.calls.len(), 1);
407 assert_eq!(
408 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
408 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
409 json!({
410 "html_content": r#"<div class="test"><span>Hello</span></div>"#,
411 "xml_snippet": r#"<root><child attr="value"/></root>"#,
@@ -416,7 +416,7 @@ mod tests {
416 #[test]
417 fn qwen_coder_parse_complete_unescapes_literal_closing_tags_in_parameter_value() {
418 let mut parser = Qwen3CoderToolParser::new(&test_tools());
419 - let result = parser
419 + let output = parser
420 .parse_complete(&build_tool_call(
421 "get_weather",
422 &[
@@ -429,9 +429,9 @@ mod tests {
429 ))
430 .unwrap();
431
432 - assert_eq!(result.calls.len(), 1);
432 + assert_eq!(output.calls.len(), 1);
433 assert_eq!(
434 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
434 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
435 json!({
436 "location": "杭州 </parameter></function></tool_call>",
437 "date": "2026-05-08",
@@ -442,16 +442,16 @@ mod tests {
442 #[test]
443 fn qwen_coder_parse_complete_does_not_double_encode_anyof_object() {
444 let mut parser = Qwen3CoderToolParser::new(&test_tools());
445 - let result = parser
445 + let output = parser
446 .parse_complete(&build_tool_call(
447 "update_record",
448 &[("data", r#"{"key":"value","count":42}"#)],
449 ))
450 .unwrap();
451
452 - assert_eq!(result.calls.len(), 1);
452 + assert_eq!(output.calls.len(), 1);
453 assert_eq!(
454 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
454 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
455 json!({
456 "data": { "key": "value", "count": 42 },
457 })
@@ -461,7 +461,7 @@ mod tests {
461 #[test]
462 fn qwen_coder_streaming_extracts_single_tool_call() {
463 let mut parser = Qwen3CoderToolParser::new(&test_tools());
464 - let result = collect_stream(
464 + let output = collect_stream(
465 &mut parser,
466 &[
467 "<tool_call>\n",
@@ -472,11 +472,11 @@ mod tests {
472 ],
473 );
474
475 - assert!(result.normal_text.is_empty());
476 - assert_eq!(result.calls.len(), 1);
477 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
475 + assert!(output.normal_text.is_empty());
476 + assert_eq!(output.calls.len(), 1);
477 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
478 assert_eq!(
479 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
479 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
480 json!({ "location": "SF" })
481 );
482 }
@@ -484,7 +484,7 @@ mod tests {
484 #[test]
485 fn qwen_coder_streaming_preserves_prefix_text() {
486 let mut parser = Qwen3CoderToolParser::new(&test_tools());
487 - let result = collect_stream(
487 + let output = collect_stream(
488 &mut parser,
489 &[
490 "Thinking... ",
@@ -496,17 +496,17 @@ mod tests {
496 ],
497 );
498
499 - assert_eq!(result.normal_text, "Thinking... ");
500 - assert_eq!(result.calls.len(), 1);
499 + assert_eq!(output.normal_text, "Thinking... ");
500 + assert_eq!(output.calls.len(), 1);
501 }
502
503 #[test]
504 fn qwen_coder_streaming_without_tool_call_emits_text_incrementally() {
505 let mut parser = Qwen3CoderToolParser::new(&test_tools());
506 - let result = collect_stream(&mut parser, &["Hello, ", "world!"]);
506 + let output = collect_stream(&mut parser, &["Hello, ", "world!"]);
507
508 - assert_eq!(result.normal_text, "Hello, world!");
509 - assert!(result.calls.is_empty());
508 + assert_eq!(output.normal_text, "Hello, world!");
509 + assert!(output.calls.is_empty());
510 }
511
512 #[test]
@@ -517,19 +517,19 @@ mod tests {
517 build_tool_call("get_weather", &[("location", "NYC")])
518 );
519 let mut parser = Qwen3CoderToolParser::new(&test_tools());
520 - let result = collect_stream(&mut parser, &[&text]);
520 + let output = collect_stream(&mut parser, &[&text]);
521
522 - assert_eq!(result.calls.len(), 2);
523 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
524 - assert_eq!(result.calls[1].name.as_deref(), Some("get_weather"));
525 - assert_eq!(result.calls[0].tool_index, 0);
526 - assert_eq!(result.calls[1].tool_index, 1);
522 + assert_eq!(output.calls.len(), 2);
523 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
524 + assert_eq!(output.calls[1].name.as_deref(), Some("get_weather"));
525 + assert_eq!(output.calls[0].tool_index, 0);
526 + assert_eq!(output.calls[1].tool_index, 1);
527 assert_eq!(
528 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
528 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
529 json!({ "location": "SF" })
530 );
531 assert_eq!(
532 - serde_json::from_str::<Value>(&result.calls[1].arguments).unwrap(),
532 + serde_json::from_str::<Value>(&output.calls[1].arguments).unwrap(),
533 json!({ "location": "NYC" })
534 );
535 }
@@ -543,19 +543,19 @@ mod tests {
543 );
544 let chunks = split_by_chars(&text, 5);
545 let mut parser = Qwen3CoderToolParser::new(&test_tools());
546 - let result = collect_stream(&mut parser, &chunks);
546 + let output = collect_stream(&mut parser, &chunks);
547
548 assert_eq!(
549 - result.normal_text,
549 + output.normal_text,
550 "I'll check two cities.Between calls.Done."
551 );
552 - assert_eq!(result.calls.len(), 2);
552 + assert_eq!(output.calls.len(), 2);
553 assert_eq!(
554 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
554 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
555 json!({ "city": "Dallas", "state": "TX" })
556 );
557 assert_eq!(
558 - serde_json::from_str::<Value>(&result.calls[1].arguments).unwrap(),
558 + serde_json::from_str::<Value>(&output.calls[1].arguments).unwrap(),
559 json!({ "city": "Orlando", "state": "FL" })
560 );
561 }
@@ -565,11 +565,11 @@ mod tests {
565 let text = build_tool_call("get_weather", &[("location", "SF")]);
566 let chunks = split_by_chars(&text, 3);
567 let mut parser = Qwen3CoderToolParser::new(&test_tools());
568 - let result = collect_stream(&mut parser, &chunks);
568 + let output = collect_stream(&mut parser, &chunks);
569
570 - assert_eq!(result.calls.len(), 1);
570 + assert_eq!(output.calls.len(), 1);
571 assert_eq!(
572 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
572 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
573 json!({ "location": "SF" })
574 );
575 }
@@ -577,19 +577,19 @@ mod tests {
577 #[test]
578 fn qwen_coder_streaming_does_not_emit_incomplete_tool_call() {
579 let mut parser = Qwen3CoderToolParser::new(&test_tools());
580 - let result = parser
581 - .push("<tool_call>\n<function=get_weather>\n<parameter=location>SF</parameter>")
580 + let output = parser
581 + .parse_chunk("<tool_call>\n<function=get_weather>\n<parameter=location>SF</parameter>")
582 .unwrap();
583
584 - assert!(result.normal_text.is_empty());
585 - assert!(result.calls.is_empty());
584 + assert!(output.normal_text.is_empty());
585 + assert!(output.calls.is_empty());
586 }
587
588 #[test]
589 fn qwen_coder_finish_fails_incomplete_tool_call() {
590 let mut parser = Qwen3CoderToolParser::new(&test_tools());
591 parser
592 - .push("<tool_call>\n<function=get_weather>\n<parameter=location>SF</parameter>")
592 + .parse_chunk("<tool_call>\n<function=get_weather>\n<parameter=location>SF</parameter>")
593 .unwrap();
594
595 assert!(parser.finish().is_err());
@@ -598,7 +598,7 @@ mod tests {
598 #[test]
599 fn qwen_coder_malformed_tool_call_fails_fast() {
600 let mut parser = Qwen3CoderToolParser::new(&test_tools());
601 - let error = parser.push("<tool_call>\n<bad>\n</tool_call>").unwrap_err();
601 + let error = parser.parse_chunk("<tool_call>\n<bad>\n</tool_call>").unwrap_err();
602
603 expect!["tool parser parsing failed: "].assert_eq(&error.to_report_string());
604 }
@@ -607,7 +607,7 @@ mod tests {
607 fn qwen_coder_missing_parameter_end_fails_fast_after_function_end() {
608 let mut parser = Qwen3CoderToolParser::new(&test_tools());
609 let error = parser
610 - .push(
610 + .parse_chunk(
611 "<tool_call>\n<function=get_weather>\n<parameter=location>SF</function>\n</tool_call>",
612 )
613 .unwrap_err();
@@ -618,14 +618,14 @@ mod tests {
618 #[test]
619 fn qwen_coder_parse_function_body_trims_one_wrapping_newline() {
620 let mut parser = Qwen3CoderToolParser::new(&test_tools());
621 - let result = parser
621 + let output = parser
622 .parse_complete(
623 "<tool_call>\n<function=get_weather>\n<parameter=location>\nHangzhou\n</parameter>\n</function>\n</tool_call>",
624 )
625 .unwrap();
626
627 assert_eq!(
628 - serde_json::from_str::<Value>(&result.calls[0].arguments).unwrap(),
628 + serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
629 json!({ "location": "Hangzhou" })
630 );
631 }
rust/src/tool-parser/src/test_utils.rs
+9 -7
@@ -1,7 +1,7 @@
1 use serde_json::json;
2
3 -use super::{ToolParseResult, ToolParser};
4 -use crate::Tool;
3 +use super::{ToolParser, ToolParserOutput};
4 +use crate::{Tool, ToolParserTestExt as _};
5
6 /// Build a reusable set of function tools for parser unit tests.
7 pub fn test_tools() -> Vec<Tool> {
@@ -82,13 +82,15 @@ pub fn test_tools() -> Vec<Tool> {
82 }
83
84 /// Push chunks through a streaming parser and coalesce its tool-call deltas.
85 -pub fn collect_stream<T: ToolParser + ?Sized>(parser: &mut T, chunks: &[&str]) -> ToolParseResult {
86 - let mut result = ToolParseResult::default();
85 +///
86 +/// Panics if there are any parsing errors along the way.
87 +pub fn collect_stream<T: ToolParser + ?Sized>(parser: &mut T, chunks: &[&str]) -> ToolParserOutput {
88 + let mut output = ToolParserOutput::default();
89 for chunk in chunks {
88 - result.append(parser.push(chunk).unwrap());
90 + output.append(parser.parse_chunk(chunk).unwrap());
91 }
90 - result.append(parser.finish().unwrap());
91 - result.coalesce_calls()
92 + output.append(parser.finish().unwrap());
93 + output.coalesce_calls()
94 }
95
96 /// Split text into chunks containing at most `chunk_chars` Unicode scalar
rust/src/tool-parser/src/tests.rs
+42 -30
@@ -1,4 +1,5 @@
1 -use super::{Result, Tool, ToolCallDelta, ToolParseResult, ToolParser};
1 +use super::{Result, Tool, ToolCallDelta, ToolParser, ToolParserOutput};
2 +use crate::ToolParserTestExt as _;
3
4 struct DefaultParser;
5
@@ -10,8 +11,16 @@ impl ToolParser for DefaultParser {
11 Ok(Box::new(Self))
12 }
13
13 - fn push(&mut self, _chunk: &str) -> Result<ToolParseResult> {
14 - Ok(ToolParseResult::default())
14 + fn parse_into(&mut self, _chunk: &str, _output: &mut ToolParserOutput) -> Result<()> {
15 + Ok(())
16 + }
17 +
18 + fn finish(&mut self) -> Result<ToolParserOutput> {
19 + Ok(ToolParserOutput::default())
20 + }
21 +
22 + fn reset(&mut self) -> String {
23 + String::new()
24 }
25 }
26
@@ -23,7 +32,7 @@ fn tool_parser_does_not_preserve_special_tokens_by_default() {
32 }
33
34 #[test]
26 -fn default_parse_complete_delegates_through_push_and_finish() {
35 +fn default_parse_complete_delegates_through_parse_chunk_and_finish() {
36 struct StreamingParser;
37
38 impl ToolParser for StreamingParser {
@@ -34,31 +43,30 @@ fn default_parse_complete_delegates_through_push_and_finish() {
43 Ok(Box::new(Self))
44 }
45
37 - fn push(&mut self, _chunk: &str) -> Result<ToolParseResult> {
38 - Ok(ToolParseResult {
39 - normal_text: "prefix ".to_string(),
40 - calls: vec![
41 - ToolCallDelta {
42 - tool_index: 0,
43 - name: Some("weather".to_string()),
44 - arguments: "{\"location\":".to_string(),
45 - },
46 - ToolCallDelta {
47 - tool_index: 0,
48 - name: None,
49 - arguments: "\"Paris\"".to_string(),
50 - },
51 - ToolCallDelta {
52 - tool_index: 1,
53 - name: Some("time".to_string()),
54 - arguments: "{\"timezone\":".to_string(),
55 - },
56 - ],
57 - })
46 + fn parse_into(&mut self, _chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
47 + output.normal_text.push_str("prefix ");
48 + output.calls.extend([
49 + ToolCallDelta {
50 + tool_index: 0,
51 + name: Some("weather".to_string()),
52 + arguments: "{\"location\":".to_string(),
53 + },
54 + ToolCallDelta {
55 + tool_index: 0,
56 + name: None,
57 + arguments: "\"Paris\"".to_string(),
58 + },
59 + ToolCallDelta {
60 + tool_index: 1,
61 + name: Some("time".to_string()),
62 + arguments: "{\"timezone\":".to_string(),
63 + },
64 + ]);
65 + Ok(())
66 }
67
60 - fn finish(&mut self) -> Result<ToolParseResult> {
61 - Ok(ToolParseResult {
68 + fn finish(&mut self) -> Result<ToolParserOutput> {
69 + Ok(ToolParserOutput {
70 normal_text: "suffix".to_string(),
71 calls: vec![
72 ToolCallDelta {
@@ -74,13 +82,17 @@ fn default_parse_complete_delegates_through_push_and_finish() {
82 ],
83 })
84 }
85 +
86 + fn reset(&mut self) -> String {
87 + String::new()
88 + }
89 }
90
91 let mut parser = StreamingParser;
80 - let result = parser.parse_complete("ignored").unwrap();
81 - assert_eq!(result.normal_text, "prefix suffix");
92 + let output = parser.parse_complete("ignored").unwrap();
93 + assert_eq!(output.normal_text, "prefix suffix");
94 assert_eq!(
83 - result.calls,
95 + output.calls,
96 vec![
97 ToolCallDelta {
98 tool_index: 0,