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,