[Rust Frontend] Make `ToolParserOutput` a seq of `ToolParserEvent` to preserve order (#46584)

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

Bugen Zhao committed Jun 25, 2026 at 14:27 UTC 4d3b4b9b01efbca77872e3d4a568b273c7a245a7
32 files changed +1153 -818
rust/src/chat/src/output/default/unified.rs
+44
@@ -425,6 +425,18 @@ mod tests {
425 }
426 }
427
428 + fn tool_call_arguments(arguments: &str) -> UnifiedParserOutput {
429 + UnifiedParserOutput {
430 + events: vec![vllm_parser::unified::UnifiedParserEvent::ToolCall(
431 + ToolCallDelta {
432 + tool_index: 0,
433 + name: None,
434 + arguments: arguments.to_string(),
435 + },
436 + )],
437 + }
438 + }
439 +
440 fn combined(first: UnifiedParserOutput, second: UnifiedParserOutput) -> UnifiedParserOutput {
441 let mut output = first;
442 output.append(second);
@@ -531,6 +543,38 @@ mod tests {
543 );
544 }
545
546 + #[tokio::test]
547 + async fn unified_stream_emits_tool_arguments_before_trailing_text() {
548 + let events = collect(
549 + ScriptedParser::new([
550 + ScriptedStep::Output(tool_call("get_weather", "")),
551 + ScriptedStep::Output(combined(
552 + tool_call_arguments(r#"{"location":"Paris"}"#),
553 + text(" done"),
554 + )),
555 + ]),
556 + vec![decoded_delta("start"), decoded_delta("finish")],
557 + )
558 + .await;
559 +
560 + assert_eq!(
561 + events,
562 + vec![
563 + AssistantEvent::ToolCallStart {
564 + id: "call_test".to_string(),
565 + name: "get_weather".to_string(),
566 + },
567 + AssistantEvent::ToolCallArgumentsDelta {
568 + delta: r#"{"location":"Paris"}"#.to_string(),
569 + },
570 + AssistantEvent::TextDelta {
571 + kind: AssistantBlockKind::Text,
572 + delta: " done".to_string(),
573 + },
574 + ]
575 + );
576 + }
577 +
578 #[tokio::test]
579 async fn unified_stream_fallback_keeps_committed_output_and_disables_later_parsing() {
580 let events = collect(
rust/src/parser/benches/utils/mod.rs
+1 -1
@@ -23,7 +23,7 @@ pub(super) fn openai_tools(tools: &[Tool]) -> Vec<OpenAiTool> {
23
24 pub(super) fn feed_parser(parser: &mut dyn ToolParser, chunks: &[&str]) -> (String, usize) {
25 let result = collect_stream(parser, chunks);
26 - (result.normal_text, result.calls.len())
26 + (result.normal_text(), result.calls().len())
27 }
28
29 pub(super) fn feed_external_parser(
rust/src/parser/python/src/lib.rs
+14 -14
@@ -146,30 +146,30 @@ impl PyToolParserOutput {
146 #[new]
147 #[pyo3(signature = (normal_text="", calls=None))]
148 fn new(py: Python<'_>, normal_text: &str, calls: Option<Vec<Py<PyToolCallDelta>>>) -> Self {
149 - let calls =
150 - calls.unwrap_or_default().iter().map(|call| call.borrow(py).0.clone()).collect();
151 - Self(ToolParserOutput {
152 - normal_text: normal_text.to_owned(),
153 - calls,
154 - })
149 + let mut output = ToolParserOutput::default();
150 + output.push_text(normal_text);
151 + for call in calls.unwrap_or_default() {
152 + output.push_call(call.borrow(py).0.clone());
153 + }
154 + Self(output)
155 }
156
157 #[getter]
158 - fn normal_text(&self) -> &str {
159 - &self.0.normal_text
158 + fn normal_text(&self) -> String {
159 + self.0.normal_text()
160 }
161
162 #[getter]
163 fn calls(&self) -> Vec<PyToolCallDelta> {
164 - self.0.calls.iter().cloned().map(PyToolCallDelta).collect()
164 + self.0.calls().into_iter().cloned().map(PyToolCallDelta).collect()
165 }
166
167 fn append(&mut self, other: PyRef<'_, PyToolParserOutput>) {
168 self.0.append(other.0.clone());
169 }
170
171 - fn coalesce_calls(&self) -> Self {
172 - Self(self.0.clone().coalesce_calls())
171 + fn coalesce(&self) -> Self {
172 + Self(self.0.clone().coalesce())
173 }
174 }
175
@@ -300,7 +300,7 @@ mod tests {
300 }
301
302 #[test]
303 - fn output_append_and_coalesce_calls() {
303 + fn output_append_and_coalesce() {
304 with_python(|py| {
305 let first = Py::new(
306 py,
@@ -311,7 +311,7 @@ mod tests {
311 let other = Py::new(py, PyToolParserOutput::new(py, "", Some(vec![second])))?;
312 output.append(other.borrow(py));
313
314 - let coalesced = output.coalesce_calls();
314 + let coalesced = output.coalesce();
315 assert_eq!(coalesced.normal_text(), "text");
316 let calls = coalesced.calls();
317 assert_eq!(calls.len(), 1);
@@ -334,7 +334,7 @@ mod tests {
334 parser.parse_into_output(&build_call(), &mut output)?;
335 let finish = Py::new(py, parser.finish()?)?;
336 output.append(finish.borrow(py));
337 - let output = output.coalesce_calls();
337 + let output = output.coalesce();
338
339 assert_eq!(output.normal_text(), "");
340 let calls = output.calls();
rust/src/parser/src/tool/deepseek_dsml/deepseek_v32.rs
+44 -44
@@ -90,8 +90,8 @@ mod tests {
90 let mut parser = DeepSeekV32ToolParser::new(&test_tools());
91 let output = parser.parse_complete("Hello, world!").unwrap();
92
93 - assert_eq!(output.normal_text, "Hello, world!");
94 - assert!(output.calls.is_empty());
93 + assert_eq!(output.normal_text(), "Hello, world!");
94 + assert!(output.calls().is_empty());
95 }
96
97 #[test]
@@ -104,11 +104,11 @@ mod tests {
104 ))
105 .unwrap();
106
107 - assert!(output.normal_text.is_empty());
108 - assert_eq!(output.calls.len(), 1);
109 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
107 + assert!(output.normal_text().is_empty());
108 + assert_eq!(output.calls().len(), 1);
109 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
110 assert_eq!(
111 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
111 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
112 json!({
113 "location": "SF",
114 "date": "2024-01-16"
@@ -125,8 +125,8 @@ mod tests {
125 );
126 let output = parser.parse_complete(&output).unwrap();
127
128 - assert_eq!(output.normal_text, "Thinking... ");
129 - assert_eq!(output.calls.len(), 1);
128 + assert_eq!(output.normal_text(), "Thinking... ");
129 + assert_eq!(output.calls().len(), 1);
130 }
131
132 #[test]
@@ -146,9 +146,9 @@ mod tests {
146 )
147 .unwrap();
148
149 - assert_eq!(output.calls.len(), 1);
149 + assert_eq!(output.calls().len(), 1);
150 assert_eq!(
151 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
151 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
152 json!({
153 "whole": 5.0,
154 "flag": true,
@@ -176,9 +176,9 @@ mod tests {
176 )
177 .unwrap();
178
179 - assert_eq!(output.calls.len(), 1);
179 + assert_eq!(output.calls().len(), 1);
180 assert_eq!(
181 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
181 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
182 json!({
183 "whole": "5.0",
184 "flag": "true",
@@ -206,7 +206,7 @@ mod tests {
206 .unwrap();
207
208 assert_eq!(
209 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
209 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
210 json!({
211 "location": "Hangzhou &lt;/|DSML|parameter&gt;&lt;/|DSML|invoke&gt;&lt;/|DSML|function_calls&gt;",
212 "date": "2026-05-08",
@@ -228,11 +228,11 @@ mod tests {
228 ],
229 );
230
231 - assert!(output.normal_text.is_empty());
232 - assert_eq!(output.calls.len(), 1);
233 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
231 + assert!(output.normal_text().is_empty());
232 + assert_eq!(output.calls().len(), 1);
233 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
234 assert_eq!(
235 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
235 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
236 json!({ "location": "SF" })
237 );
238 }
@@ -252,8 +252,8 @@ mod tests {
252 ],
253 );
254
255 - assert_eq!(output.normal_text, "Thinking... ");
256 - assert_eq!(output.calls.len(), 1);
255 + assert_eq!(output.normal_text(), "Thinking... ");
256 + assert_eq!(output.calls().len(), 1);
257 }
258
259 #[test]
@@ -261,8 +261,8 @@ mod tests {
261 let mut parser = DeepSeekV32ToolParser::new(&test_tools());
262 let output = collect_stream(&mut parser, &["Hello, ", "world!"]);
263
264 - assert_eq!(output.normal_text, "Hello, world!");
265 - assert!(output.calls.is_empty());
264 + assert_eq!(output.normal_text(), "Hello, world!");
265 + assert!(output.calls().is_empty());
266 }
267
268 #[test]
@@ -278,17 +278,17 @@ mod tests {
278 )],
279 );
280
281 - assert_eq!(output.calls.len(), 2);
282 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
283 - assert_eq!(output.calls[1].name.as_deref(), Some("get_weather"));
284 - assert_eq!(output.calls[0].tool_index, 0);
285 - assert_eq!(output.calls[1].tool_index, 1);
281 + assert_eq!(output.calls().len(), 2);
282 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
283 + assert_eq!(output.calls()[1].name.as_deref(), Some("get_weather"));
284 + assert_eq!(output.calls()[0].tool_index, 0);
285 + assert_eq!(output.calls()[1].tool_index, 1);
286 assert_eq!(
287 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
287 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
288 json!({ "location": "SF" })
289 );
290 assert_eq!(
291 - serde_json::from_str::<Value>(&output.calls[1].arguments).unwrap(),
291 + serde_json::from_str::<Value>(&output.calls()[1].arguments).unwrap(),
292 json!({ "location": "NYC" })
293 );
294 }
@@ -300,9 +300,9 @@ mod tests {
300 let mut parser = DeepSeekV32ToolParser::new(&test_tools());
301 let output = collect_stream(&mut parser, &chunks);
302
303 - assert_eq!(output.calls.len(), 1);
303 + assert_eq!(output.calls().len(), 1);
304 assert_eq!(
305 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
305 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
306 json!({ "location": "SF" })
307 );
308 }
@@ -337,11 +337,11 @@ mod tests {
337 ],
338 );
339
340 - assert!(output.normal_text.is_empty());
341 - assert_eq!(output.calls.len(), 1);
342 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
340 + assert!(output.normal_text().is_empty());
341 + assert_eq!(output.calls().len(), 1);
342 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
343 assert_eq!(
344 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
344 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
345 json!({ "location": "Beijing" })
346 );
347 }
@@ -373,9 +373,9 @@ mod tests {
373 ],
374 );
375
376 - assert!(output.normal_text.is_empty());
377 - assert_eq!(output.calls.len(), 1);
378 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
376 + assert!(output.normal_text().is_empty());
377 + assert_eq!(output.calls().len(), 1);
378 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
379 }
380
381 #[test]
@@ -393,8 +393,8 @@ mod tests {
393 ],
394 );
395
396 - assert!(output.normal_text.is_empty());
397 - assert_eq!(output.calls.len(), 1);
396 + assert!(output.normal_text().is_empty());
397 + assert_eq!(output.calls().len(), 1);
398 }
399
400 #[test]
@@ -421,10 +421,10 @@ mod tests {
421 .parse_complete(&build_tool_call("get_weather", &[("location", "NYC")]))
422 .unwrap();
423
424 - assert_eq!(first.calls.len(), 1);
425 - assert_eq!(second.calls.len(), 1);
424 + assert_eq!(first.calls().len(), 1);
425 + assert_eq!(second.calls().len(), 1);
426 assert_eq!(
427 - serde_json::from_str::<Value>(&second.calls[0].arguments).unwrap(),
427 + serde_json::from_str::<Value>(&second.calls()[0].arguments).unwrap(),
428 json!({ "location": "NYC" })
429 );
430 }
@@ -439,7 +439,7 @@ mod tests {
439 let mut parser = DeepSeekV32ToolParser::new(&test_tools());
440 let complete = parser.parse_complete(&full_text).unwrap();
441
442 - assert_eq!(streamed.normal_text, complete.normal_text);
443 - assert_eq!(streamed.calls, complete.calls);
442 + assert_eq!(streamed.normal_text(), complete.normal_text());
443 + assert_eq!(streamed.calls(), complete.calls());
444 }
445 }
rust/src/parser/src/tool/deepseek_dsml/deepseek_v4.rs
+8 -8
@@ -107,11 +107,11 @@ mod tests {
107 ))
108 .unwrap();
109
110 - assert!(output.normal_text.is_empty());
111 - assert_eq!(output.calls.len(), 1);
112 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
110 + assert!(output.normal_text().is_empty());
111 + assert_eq!(output.calls().len(), 1);
112 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
113 assert_eq!(
114 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
114 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
115 json!({
116 "location": "SF",
117 "date": "2024-01-16"
@@ -137,11 +137,11 @@ mod tests {
137 ],
138 );
139
140 - assert_eq!(output.normal_text, "Thinking... ");
141 - assert_eq!(output.calls.len(), 1);
142 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
140 + assert_eq!(output.normal_text(), "Thinking... ");
141 + assert_eq!(output.calls().len(), 1);
142 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
143 assert_eq!(
144 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
144 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
145 json!({ "location": "Beijing" })
146 );
147 }
rust/src/parser/src/tool/deepseek_dsml/mod.rs
+3 -3
@@ -92,7 +92,7 @@ impl DeepSeekDsmlToolParser {
92 fn apply_event(&mut self, event: DsmlEvent, output: &mut ToolParserOutput) -> Result<()> {
93 match event {
94 DsmlEvent::Text { len: consumed_len } => {
95 - output.normal_text.push_str(&self.buffer[..consumed_len]);
95 + output.push_text(&self.buffer[..consumed_len]);
96 }
97 DsmlEvent::ToolCallsStart => {
98 self.mode = DsmlMode::ToolBlock {
@@ -116,7 +116,7 @@ impl DeepSeekDsmlToolParser {
116 let arguments = serde_json::to_string(&arguments)
117 .map_err(|error| parsing_failed!("failed to serialize arguments: {}", error))?;
118
119 - output.calls.push(ToolCallDelta {
119 + output.push_call(ToolCallDelta {
120 tool_index: self.emitted_invoke_count,
121 name: Some(name),
122 arguments,
@@ -156,7 +156,7 @@ impl DeepSeekDsmlToolParser {
156 fn finish(&mut self) -> Result<ToolParserOutput> {
157 let mut output = ToolParserOutput::default();
158 match self.mode {
159 - DsmlMode::Text => output.normal_text.push_str(&self.buffer),
159 + DsmlMode::Text => output.push_text(&self.buffer),
160 DsmlMode::Done => {}
161 DsmlMode::ToolBlock { .. } => {
162 return Err(parsing_failed!("incomplete DeepSeek DSML tool call"));
rust/src/parser/src/tool/deepseek_json/deepseek_v3.rs
+34 -31
@@ -77,8 +77,8 @@ mod tests {
77 let mut parser = DeepSeekV3ToolParser::new(&test_tools());
78 let output = parser.parse_complete("Hello, world!").unwrap();
79
80 - assert_eq!(output.normal_text, "Hello, world!");
81 - assert!(output.calls.is_empty());
80 + assert_eq!(output.normal_text(), "Hello, world!");
81 + assert!(output.calls().is_empty());
82 }
83
84 #[test]
@@ -92,11 +92,11 @@ mod tests {
92 ))
93 .unwrap();
94
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);
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]
@@ -107,7 +107,7 @@ mod tests {
107 .parse_complete(&tool_section(&[v3_tool_call("get_weather", arguments)]))
108 .unwrap();
109
110 - assert_eq!(output.calls[0].arguments, arguments);
110 + assert_eq!(output.calls()[0].arguments, arguments);
111 }
112
113 #[test]
@@ -132,7 +132,7 @@ mod tests {
132 for chunk in chunks {
133 let next = parser.parse_chunk(chunk).unwrap();
134 observed_arguments.extend(
135 - next.calls
135 + next.calls()
136 .iter()
137 .filter(|call| call.name.is_none())
138 .map(|call| call.arguments.clone()),
@@ -143,7 +143,7 @@ mod tests {
143
144 assert_eq!(observed_arguments, ["{\"location\":", "\"Beijing\"", "}"]);
145 assert_eq!(
146 - output.coalesce_calls().calls[0].arguments,
146 + output.coalesce().calls()[0].arguments,
147 r#"{"location":"Beijing"}"#
148 );
149 }
@@ -159,9 +159,9 @@ mod tests {
159
160 let output = collect_stream(&mut parser, &chunks);
161
162 - assert_eq!(output.normal_text, "hello ");
163 - assert_eq!(output.calls.len(), 1);
164 - assert_eq!(output.calls[0].arguments, r#"{"location":"Tokyo"}"#);
162 + assert_eq!(output.normal_text(), "hello ");
163 + assert_eq!(output.calls().len(), 1);
164 + assert_eq!(output.calls()[0].arguments, r#"{"location":"Tokyo"}"#);
165 }
166
167 #[test]
@@ -172,8 +172,8 @@ mod tests {
172
173 let output = parser.parse_complete(&input).unwrap();
174
175 - assert_eq!(output.calls.len(), 1);
176 - assert_eq!(output.calls[0].arguments, arguments);
175 + assert_eq!(output.calls().len(), 1);
176 + assert_eq!(output.calls()[0].arguments, arguments);
177 }
178
179 #[test]
@@ -189,22 +189,25 @@ mod tests {
189
190 expect![[r#"
191 ToolParserOutput {
192 - normal_text: "",
193 - calls: [
194 - ToolCallDelta {
195 - tool_index: 0,
196 - name: Some(
197 - "get_weather",
198 - ),
199 - arguments: "{\"location\":\"Shanghai\"}",
200 - },
201 - ToolCallDelta {
202 - tool_index: 1,
203 - name: Some(
204 - "add",
205 - ),
206 - arguments: "{\"x\":1,\"y\":2}",
207 - },
192 + events: [
193 + ToolCall(
194 + ToolCallDelta {
195 + tool_index: 0,
196 + name: Some(
197 + "get_weather",
198 + ),
199 + arguments: "{\"location\":\"Shanghai\"}",
200 + },
201 + ),
202 + ToolCall(
203 + ToolCallDelta {
204 + tool_index: 1,
205 + name: Some(
206 + "add",
207 + ),
208 + arguments: "{\"x\":1,\"y\":2}",
209 + },
210 + ),
211 ],
212 }
213 "#]]
rust/src/parser/src/tool/deepseek_json/deepseek_v31.rs
+37 -34
@@ -70,8 +70,8 @@ mod tests {
70 let mut parser = DeepSeekV31ToolParser::new(&test_tools());
71 let output = parser.parse_complete("Hello, world!").unwrap();
72
73 - assert_eq!(output.normal_text, "Hello, world!");
74 - assert!(output.calls.is_empty());
73 + assert_eq!(output.normal_text(), "Hello, world!");
74 + assert!(output.calls().is_empty());
75 }
76
77 #[test]
@@ -85,11 +85,11 @@ mod tests {
85 ))
86 .unwrap();
87
88 - assert_eq!(output.normal_text, "Let me check.");
89 - assert_eq!(output.calls.len(), 1);
90 - assert_eq!(output.calls[0].tool_index, 0);
91 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
92 - assert_eq!(output.calls[0].arguments, arguments);
88 + assert_eq!(output.normal_text(), "Let me check.");
89 + assert_eq!(output.calls().len(), 1);
90 + assert_eq!(output.calls()[0].tool_index, 0);
91 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
92 + assert_eq!(output.calls()[0].arguments, arguments);
93 }
94
95 #[test]
@@ -100,7 +100,7 @@ mod tests {
100 .parse_complete(&tool_section(&[v31_tool_call("get_weather", arguments)]))
101 .unwrap();
102
103 - assert_eq!(output.calls[0].arguments, arguments);
103 + assert_eq!(output.calls()[0].arguments, arguments);
104 }
105
106 #[test]
@@ -123,7 +123,7 @@ mod tests {
123 for chunk in chunks {
124 let next = parser.parse_chunk(chunk).unwrap();
125 observed_arguments.extend(
126 - next.calls
126 + next.calls()
127 .iter()
128 .filter(|call| call.name.is_none())
129 .map(|call| call.arguments.clone()),
@@ -134,7 +134,7 @@ mod tests {
134
135 assert_eq!(observed_arguments, ["{\"location\":", "\"Beijing\"", "}"]);
136 assert_eq!(
137 - output.coalesce_calls().calls[0].arguments,
137 + output.coalesce().calls()[0].arguments,
138 r#"{"location":"Beijing"}"#
139 );
140 }
@@ -150,9 +150,9 @@ mod tests {
150
151 let output = collect_stream(&mut parser, &chunks);
152
153 - assert_eq!(output.normal_text, "hello ");
154 - assert_eq!(output.calls.len(), 1);
155 - assert_eq!(output.calls[0].arguments, r#"{"location":"Tokyo"}"#);
153 + assert_eq!(output.normal_text(), "hello ");
154 + assert_eq!(output.calls().len(), 1);
155 + assert_eq!(output.calls()[0].arguments, r#"{"location":"Tokyo"}"#);
156 }
157
158 #[test]
@@ -163,8 +163,8 @@ mod tests {
163
164 let output = parser.parse_complete(&input).unwrap();
165
166 - assert_eq!(output.calls.len(), 1);
167 - assert_eq!(output.calls[0].arguments, arguments);
166 + assert_eq!(output.calls().len(), 1);
167 + assert_eq!(output.calls()[0].arguments, arguments);
168 }
169
170 #[test]
@@ -180,22 +180,25 @@ mod tests {
180
181 expect![[r#"
182 ToolParserOutput {
183 - normal_text: "",
184 - calls: [
185 - ToolCallDelta {
186 - tool_index: 0,
187 - name: Some(
188 - "get_weather",
189 - ),
190 - arguments: "{\"location\":\"Shanghai\"}",
191 - },
192 - ToolCallDelta {
193 - tool_index: 1,
194 - name: Some(
195 - "add",
196 - ),
197 - arguments: "{\"x\":1,\"y\":2}",
198 - },
183 + events: [
184 + ToolCall(
185 + ToolCallDelta {
186 + tool_index: 0,
187 + name: Some(
188 + "get_weather",
189 + ),
190 + arguments: "{\"location\":\"Shanghai\"}",
191 + },
192 + ),
193 + ToolCall(
194 + ToolCallDelta {
195 + tool_index: 1,
196 + name: Some(
197 + "add",
198 + ),
199 + arguments: "{\"x\":1,\"y\":2}",
200 + },
201 + ),
202 ],
203 }
204 "#]]
@@ -212,9 +215,9 @@ mod tests {
215
216 let output = collect_stream(&mut parser, &[&input]);
217
215 - assert!(output.normal_text.is_empty());
216 - assert_eq!(output.calls.len(), 1);
217 - assert_eq!(output.calls[0].arguments, r#"{"location":"Tokyo"}"#);
218 + assert!(output.normal_text().is_empty());
219 + assert_eq!(output.calls().len(), 1);
220 + assert_eq!(output.calls()[0].arguments, r#"{"location":"Tokyo"}"#);
221 }
222
223 #[test]
rust/src/parser/src/tool/deepseek_json/mod.rs
+4 -4
@@ -96,7 +96,7 @@ impl DeepSeekJsonToolParser {
96 ) -> Result<()> {
97 match event {
98 DeepSeekJsonEvent::Text { len: consumed_len } => {
99 - output.normal_text.push_str(&self.buffer[..consumed_len]);
99 + output.push_text(&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 - output.calls.push(ToolCallDelta {
110 + output.push_call(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 - output.calls.push(ToolCallDelta {
123 + output.push_call(ToolCallDelta {
124 tool_index,
125 name: None,
126 arguments: self.buffer[..consumed_len].to_string(),
@@ -155,7 +155,7 @@ impl DeepSeekJsonToolParser {
155 fn finish(&mut self) -> Result<ToolParserOutput> {
156 let mut output = ToolParserOutput::default();
157 match &self.mode {
158 - DeepSeekJsonMode::Text => output.normal_text.push_str(&self.buffer),
158 + DeepSeekJsonMode::Text => output.push_text(&self.buffer),
159 DeepSeekJsonMode::ToolBlock | DeepSeekJsonMode::Done => {}
160 DeepSeekJsonMode::Header | DeepSeekJsonMode::Arguments { .. } => {
161 return Err(parsing_failed!(
rust/src/parser/src/tool/gemma4.rs
+14 -14
@@ -70,7 +70,7 @@ impl Gemma4ToolParser {
70 fn apply_event(&mut self, event: Gemma4Event, output: &mut ToolParserOutput) -> Result<()> {
71 match event {
72 Gemma4Event::Text { len: consumed_len } => {
73 - output.normal_text.push_str(&self.buffer[..consumed_len]);
73 + output.push_text(&self.buffer[..consumed_len]);
74 }
75 Gemma4Event::ToolCallStart => self.mode = Gemma4Mode::Header,
76 Gemma4Event::ToolCallHeader { name } => {
@@ -89,7 +89,7 @@ impl Gemma4ToolParser {
89 let arguments = serde_json::to_string(&args)
90 .map_err(|error| parsing_failed!("failed to serialize arguments: {}", error))?;
91
92 - output.calls.push(ToolCallDelta {
92 + output.push_call(ToolCallDelta {
93 tool_index: self.emitted_tool_count,
94 name: Some(name),
95 arguments,
@@ -153,7 +153,7 @@ impl ToolParser for Gemma4ToolParser {
153 let mut output = ToolParserOutput::default();
154
155 match &self.mode {
156 - Gemma4Mode::Text => output.normal_text.push_str(&self.buffer),
156 + Gemma4Mode::Text => output.push_text(&self.buffer),
157 Gemma4Mode::Header | Gemma4Mode::ToolCall { .. } => {
158 return Err(parsing_failed!("incomplete Gemma4 tool call"));
159 }
@@ -501,11 +501,11 @@ mod tests {
501 output.append(parser.parse_chunk(chunk).unwrap());
502 }
503 output.append(parser.finish().unwrap());
504 - output.coalesce_calls()
504 + output.coalesce()
505 }
506
507 - fn first_call(output: &ToolParserOutput) -> &ToolCallDelta {
508 - output.calls.first().expect("expected one tool call")
507 + fn first_call(output: &ToolParserOutput) -> ToolCallDelta {
508 + (*output.calls().first().expect("expected one tool call")).clone()
509 }
510
511 #[test]
@@ -547,8 +547,8 @@ mod tests {
547 .parse_complete("<|tool_call>call:get_weather{location:<|\"|>London<|\"|>}<tool_call|>")
548 .unwrap();
549
550 - assert!(output.normal_text.is_empty());
551 - assert_eq!(output.calls.len(), 1);
550 + assert!(output.normal_text().is_empty());
551 + assert_eq!(output.calls().len(), 1);
552 assert_eq!(first_call(&output).name.as_deref(), Some("get_weather"));
553 assert_eq!(
554 serde_json::from_str::<Value>(&first_call(&output).arguments).unwrap(),
@@ -577,7 +577,7 @@ mod tests {
577 "<tool_call|>",
578 ]);
579
580 - assert!(output.normal_text.is_empty());
580 + assert!(output.normal_text().is_empty());
581 assert_eq!(first_call(&output).name.as_deref(), Some("get_weather"));
582 assert_eq!(
583 serde_json::from_str::<Value>(&first_call(&output).arguments).unwrap(),
@@ -597,7 +597,7 @@ mod tests {
597 "div>",
598 ]);
599
600 - assert_eq!(output.normal_text, "Let me check the weather. <div>");
600 + assert_eq!(output.normal_text(), "Let me check the weather. <div>");
601 assert_eq!(first_call(&output).name.as_deref(), Some("get_weather"));
602 assert_eq!(
603 serde_json::from_str::<Value>(&first_call(&output).arguments).unwrap(),
@@ -616,11 +616,11 @@ mod tests {
616 "location:<|\"|>Paris<|\"|>}",
617 ] {
618 output.append(parser.parse_chunk(chunk).unwrap());
619 - assert!(output.calls.is_empty());
619 + assert!(output.calls().is_empty());
620 }
621
622 output.append(parser.parse_chunk("<tool_call|>").unwrap());
623 - let output = output.coalesce_calls();
623 + let output = output.coalesce();
624
625 assert_eq!(first_call(&output).name.as_deref(), Some("get_weather"));
626 assert_eq!(
@@ -777,8 +777,8 @@ mod tests {
777 let mut output = parser.parse_chunk("<").unwrap();
778 output.append(parser.finish().unwrap());
779
780 - assert_eq!(output.normal_text, "<");
781 - assert!(output.calls.is_empty());
780 + assert_eq!(output.normal_text(), "<");
781 + assert!(output.calls().is_empty());
782 }
783
784 #[test]
rust/src/parser/src/tool/glm_xml/glm47_moe.rs
+13 -13
@@ -69,11 +69,11 @@ mod tests {
69
70 let output = parser.parse_complete(&output).unwrap();
71
72 - assert_eq!(output.normal_text, "Let me search for that.\n");
73 - assert_eq!(output.calls.len(), 1);
74 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
72 + assert_eq!(output.normal_text(), "Let me search for that.\n");
73 + assert_eq!(output.calls().len(), 1);
74 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
75 assert_eq!(
76 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
76 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
77 json!({"city": "Beijing", "date": "2024-12-25"})
78 );
79 }
@@ -90,12 +90,12 @@ mod tests {
90 let chunks = split_by_chars(&output, 7);
91 let output = collect_stream(&mut parser, &chunks);
92
93 - assert_eq!(output.normal_text, "");
94 - assert_eq!(output.calls.len(), 2);
95 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
96 - assert_eq!(output.calls[1].name.as_deref(), Some("add"));
93 + assert_eq!(output.normal_text(), "");
94 + assert_eq!(output.calls().len(), 2);
95 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
96 + assert_eq!(output.calls()[1].name.as_deref(), Some("add"));
97 assert_eq!(
98 - serde_json::from_str::<Value>(&output.calls[1].arguments).unwrap(),
98 + serde_json::from_str::<Value>(&output.calls()[1].arguments).unwrap(),
99 json!({"x": 1, "y": 2})
100 );
101 }
@@ -117,7 +117,7 @@ mod tests {
117 .unwrap();
118
119 assert_eq!(
120 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
120 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
121 json!({
122 "whole": 42,
123 "flag": true,
@@ -134,10 +134,10 @@ mod tests {
134
135 let output = parser.parse_complete("<tool_call>add</tool_call>").unwrap();
136
137 - assert_eq!(output.calls.len(), 1);
138 - assert_eq!(output.calls[0].name.as_deref(), Some("add"));
137 + assert_eq!(output.calls().len(), 1);
138 + assert_eq!(output.calls()[0].name.as_deref(), Some("add"));
139 assert_eq!(
140 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
140 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
141 json!({})
142 );
143 }
rust/src/parser/src/tool/glm_xml/mod.rs
+26 -26
@@ -79,7 +79,7 @@ impl GlmXmlToolParser {
79 fn apply_event(&mut self, event: GlmEvent, output: &mut ToolParserOutput) -> Result<()> {
80 match event {
81 GlmEvent::Text { len: consumed_len } => {
82 - output.normal_text.push_str(&self.buffer[..consumed_len]);
82 + output.push_text(&self.buffer[..consumed_len]);
83 }
84 GlmEvent::ToolCallStart => {
85 self.mode = GlmMode::ToolCall {
@@ -92,7 +92,7 @@ impl GlmXmlToolParser {
92 let arguments = serde_json::to_string(&arguments)
93 .map_err(|error| parsing_failed!("failed to serialize arguments: {}", error))?;
94
95 - output.calls.push(ToolCallDelta {
95 + output.push_call(ToolCallDelta {
96 tool_index: self.emitted_tool_count,
97 name: Some(name),
98 arguments,
@@ -127,7 +127,7 @@ impl GlmXmlToolParser {
127 let mut output = ToolParserOutput::default();
128 if !self.buffer.is_empty() {
129 match self.mode {
130 - GlmMode::Text => output.normal_text.push_str(&self.buffer),
130 + GlmMode::Text => output.push_text(&self.buffer),
131 GlmMode::ToolCall { .. } => {
132 return Err(parsing_failed!("incomplete GLM MoE tool call"));
133 }
@@ -283,8 +283,8 @@ mod tests {
283 let mut parser = Glm45MoeToolParser::new(&test_tools());
284 let output = parser.parse_complete("Hello, world!").unwrap();
285
286 - assert_eq!(output.normal_text, "Hello, world!");
287 - assert!(output.calls.is_empty());
286 + assert_eq!(output.normal_text(), "Hello, world!");
287 + assert!(output.calls().is_empty());
288 }
289
290 #[test]
@@ -300,11 +300,11 @@ mod tests {
300
301 let output = parser.parse_complete(&output).unwrap();
302
303 - assert_eq!(output.normal_text, "Let me search for that.\n");
304 - assert_eq!(output.calls.len(), 1);
305 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
303 + assert_eq!(output.normal_text(), "Let me search for that.\n");
304 + assert_eq!(output.calls().len(), 1);
305 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
306 assert_eq!(
307 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
307 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
308 json!({"city": "Beijing", "date": "2024-12-25"})
309 );
310 }
@@ -321,12 +321,12 @@ mod tests {
321 let chunks = split_by_chars(&output, 11);
322 let output = collect_stream(&mut parser, &chunks);
323
324 - assert_eq!(output.normal_text, "");
325 - assert_eq!(output.calls.len(), 2);
326 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
327 - assert_eq!(output.calls[1].name.as_deref(), Some("add"));
324 + assert_eq!(output.normal_text(), "");
325 + assert_eq!(output.calls().len(), 2);
326 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
327 + assert_eq!(output.calls()[1].name.as_deref(), Some("add"));
328 assert_eq!(
329 - serde_json::from_str::<Value>(&output.calls[1].arguments).unwrap(),
329 + serde_json::from_str::<Value>(&output.calls()[1].arguments).unwrap(),
330 json!({"x": 1, "y": 2})
331 );
332 }
@@ -345,7 +345,7 @@ mod tests {
345 .unwrap();
346
347 assert_eq!(
348 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
348 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
349 json!({
350 "city": "Paris &lt;/arg_value&gt;&lt;/tool_call&gt;",
351 "date": "2026-05-08",
@@ -359,8 +359,8 @@ mod tests {
359
360 let output = collect_stream(&mut parser, &["hello ", "world"]);
361
362 - assert_eq!(output.normal_text, "hello world");
363 - assert!(output.calls.is_empty());
362 + assert_eq!(output.normal_text(), "hello world");
363 + assert!(output.calls().is_empty());
364 }
365
366 #[test]
@@ -375,8 +375,8 @@ mod tests {
375 ],
376 );
377
378 - assert_eq!(output.normal_text, "Prefix ");
379 - assert_eq!(output.calls.len(), 1);
378 + assert_eq!(output.normal_text(), "Prefix ");
379 + assert_eq!(output.calls().len(), 1);
380 }
381
382 #[test]
@@ -391,9 +391,9 @@ mod tests {
391 ],
392 );
393
394 - assert_eq!(output.normal_text, "hello ");
395 - assert_eq!(output.calls.len(), 1);
396 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
394 + assert_eq!(output.normal_text(), "hello ");
395 + assert_eq!(output.calls().len(), 1);
396 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
397 }
398
399 #[test]
@@ -402,8 +402,8 @@ mod tests {
402
403 let output = parser.parse_chunk("<tool_call>get_weather\n<arg_key>city</arg_key>").unwrap();
404
405 - assert_eq!(output.normal_text, "");
406 - assert!(output.calls.is_empty());
405 + assert_eq!(output.normal_text(), "");
406 + assert!(output.calls().is_empty());
407 }
408
409 #[test]
@@ -437,7 +437,7 @@ mod tests {
437 )],
438 );
439
440 - assert_eq!(output.normal_text, "");
441 - assert_eq!(output.calls.len(), 1);
440 + assert_eq!(output.normal_text(), "");
441 + assert_eq!(output.calls().len(), 1);
442 }
443 }
rust/src/parser/src/tool/hy_v3.rs
+44 -41
@@ -79,7 +79,7 @@ impl HyV3ToolParser {
79 fn apply_event(&mut self, event: HyV3Event, output: &mut ToolParserOutput) -> Result<()> {
80 match event {
81 HyV3Event::Text { len: consumed_len } => {
82 - output.normal_text.push_str(&self.buffer[..consumed_len]);
82 + output.push_text(&self.buffer[..consumed_len]);
83 }
84 HyV3Event::ToolBlockStart => {
85 self.mode = HyV3Mode::ToolBlock {
@@ -91,7 +91,7 @@ impl HyV3ToolParser {
91 let arguments = serde_json::to_string(&arguments)
92 .map_err(|error| parsing_failed!("failed to serialize arguments: {}", error))?;
93
94 - output.calls.push(ToolCallDelta {
94 + output.push_call(ToolCallDelta {
95 tool_index: self.emitted_tool_count,
96 name: Some(name),
97 arguments,
@@ -133,7 +133,7 @@ impl ToolParser for HyV3ToolParser {
133 fn finish(&mut self) -> Result<ToolParserOutput> {
134 let mut output = ToolParserOutput::default();
135 match self.mode {
136 - HyV3Mode::Text => output.normal_text.push_str(&self.buffer),
136 + HyV3Mode::Text => output.push_text(&self.buffer),
137 HyV3Mode::ToolBlock { .. } => return Err(parsing_failed!("incomplete HY3 tool call")),
138 HyV3Mode::Done => {}
139 }
@@ -266,7 +266,7 @@ mod tests {
266 }
267
268 fn parsed_arguments(output: &ToolParserOutput, index: usize) -> Value {
269 - serde_json::from_str(&output.calls[index].arguments).unwrap()
269 + serde_json::from_str(&output.calls()[index].arguments).unwrap()
270 }
271
272 #[test]
@@ -281,8 +281,8 @@ mod tests {
281 let mut parser = HyV3ToolParser::new(&test_tools());
282 let output = parser.parse_complete("This is a plain response.").unwrap();
283
284 - assert_eq!(output.normal_text, "This is a plain response.");
285 - assert!(output.calls.is_empty());
284 + assert_eq!(output.normal_text(), "This is a plain response.");
285 + assert!(output.calls().is_empty());
286 }
287
288 #[test]
@@ -294,9 +294,9 @@ mod tests {
294 )
295 .unwrap();
296
297 - assert_eq!(output.normal_text, "");
298 - assert_eq!(output.calls.len(), 1);
299 - assert_eq!(output.calls[0].name.as_deref(), Some("get_current_date"));
297 + assert_eq!(output.normal_text(), "");
298 + assert_eq!(output.calls().len(), 1);
299 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_current_date"));
300 assert_eq!(parsed_arguments(&output, 0), json!({}));
301 }
302
@@ -309,7 +309,7 @@ mod tests {
309 )
310 .unwrap();
311
312 - assert_eq!(output.calls[0].name.as_deref(), Some("get_current_date"));
312 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_current_date"));
313 assert_eq!(parsed_arguments(&output, 0), json!({}));
314 }
315
@@ -354,8 +354,8 @@ mod tests {
354 ))
355 .unwrap();
356
357 - assert_eq!(output.normal_text, "Checking.");
358 - assert_eq!(output.calls[0].name.as_deref(), Some("get_current_date"));
357 + assert_eq!(output.normal_text(), "Checking.");
358 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_current_date"));
359 }
360
361 #[test]
@@ -376,22 +376,25 @@ mod tests {
376
377 expect![[r#"
378 ToolParserOutput {
379 - normal_text: "",
380 - calls: [
381 - ToolCallDelta {
382 - tool_index: 0,
383 - name: Some(
384 - "get_weather",
385 - ),
386 - arguments: "{\"city\":\"Beijing\",\"date\":\"2026-03-30\"}",
387 - },
388 - ToolCallDelta {
389 - tool_index: 1,
390 - name: Some(
391 - "get_weather",
392 - ),
393 - arguments: "{\"city\":\"Hangzhou\",\"date\":\"2026-03-30\"}",
394 - },
379 + events: [
380 + ToolCall(
381 + ToolCallDelta {
382 + tool_index: 0,
383 + name: Some(
384 + "get_weather",
385 + ),
386 + arguments: "{\"city\":\"Beijing\",\"date\":\"2026-03-30\"}",
387 + },
388 + ),
389 + ToolCall(
390 + ToolCallDelta {
391 + tool_index: 1,
392 + name: Some(
393 + "get_weather",
394 + ),
395 + arguments: "{\"city\":\"Hangzhou\",\"date\":\"2026-03-30\"}",
396 + },
397 + ),
398 ],
399 }
400 "#]]
@@ -434,8 +437,8 @@ mod tests {
437 output.append(parser.parse_chunk("response.").unwrap());
438 output.append(parser.finish().unwrap());
439
437 - assert_eq!(output.normal_text, "This is a plain response.");
438 - assert!(output.calls.is_empty());
440 + assert_eq!(output.normal_text(), "This is a plain response.");
441 + assert!(output.calls().is_empty());
442 }
443
444 #[test]
@@ -452,8 +455,8 @@ mod tests {
455
456 let output = collect_stream(&mut parser, &chunks);
457
455 - assert_eq!(output.calls.len(), 1);
456 - assert_eq!(output.calls[0].name.as_deref(), Some("get_current_date"));
458 + assert_eq!(output.calls().len(), 1);
459 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_current_date"));
460 assert_eq!(parsed_arguments(&output, 0), json!({}));
461 }
462
@@ -475,8 +478,8 @@ mod tests {
478
479 let output = collect_stream(&mut parser, &chunks);
480
478 - assert_eq!(output.calls.len(), 1);
479 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
481 + assert_eq!(output.calls().len(), 1);
482 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
483 assert_eq!(
484 parsed_arguments(&output, 0),
485 json!({ "city": "Beijing", "date": "2026-03-30" })
@@ -498,8 +501,8 @@ mod tests {
501
502 let output = collect_stream(&mut parser, &chunks);
503
501 - assert_eq!(output.normal_text, "Checking.");
502 - assert_eq!(output.calls[0].name.as_deref(), Some("get_current_date"));
504 + assert_eq!(output.normal_text(), "Checking.");
505 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_current_date"));
506 }
507
508 #[test]
@@ -519,7 +522,7 @@ mod tests {
522
523 let output = collect_stream(&mut parser, &chunks);
524
522 - assert_eq!(output.calls.len(), 2);
525 + assert_eq!(output.calls().len(), 2);
526 assert_eq!(parsed_arguments(&output, 0)["city"], json!("Beijing"));
527 assert_eq!(parsed_arguments(&output, 1)["city"], json!("Hangzhou"));
528 }
@@ -535,8 +538,8 @@ mod tests {
538
539 let output = collect_stream(&mut parser, &chunks);
540
538 - assert_eq!(output.normal_text, "hello ");
539 - assert_eq!(output.calls.len(), 1);
541 + assert_eq!(output.normal_text(), "hello ");
542 + assert_eq!(output.calls().len(), 1);
543 assert_eq!(parsed_arguments(&output, 0), json!({ "city": "Beijing" }));
544 }
545
@@ -552,8 +555,8 @@ mod tests {
555 )
556 .unwrap();
557
555 - assert_eq!(output.normal_text, "");
556 - assert!(output.calls.is_empty());
558 + assert_eq!(output.normal_text(), "");
559 + assert!(output.calls().is_empty());
560 }
561
562 #[test]
rust/src/parser/src/tool/json/granite4.rs
+75 -61
@@ -86,14 +86,14 @@ impl Granite4ToolParser {
86 /// Apply one parsed Granite 4 event to parser state and output.
87 fn apply_event(&mut self, event: Granite4Event, output: &mut ToolParserOutput) -> Result<()> {
88 match event {
89 - Granite4Event::Text { len } => output.normal_text.push_str(&self.buffer[..len]),
89 + Granite4Event::Text { len } => output.push_text(&self.buffer[..len]),
90 Granite4Event::ToolCallStart => self.mode = Granite4Mode::Header,
91 Granite4Event::ToolCallHeader { function_name } => {
92 let tool_index = self.emitted_tool_count;
93 self.emitted_tool_count += 1;
94 self.active_tool_index = Some(tool_index);
95 self.mode = Granite4Mode::Args { json_scan: None };
96 - output.calls.push(ToolCallDelta {
96 + output.push_call(ToolCallDelta {
97 tool_index,
98 name: Some(function_name),
99 arguments: String::new(),
@@ -125,7 +125,7 @@ impl Granite4ToolParser {
125 "Granite4 arguments without an active tool call"
126 ));
127 };
128 - output.calls.push(ToolCallDelta {
128 + output.push_call(ToolCallDelta {
129 tool_index,
130 name: None,
131 arguments,
@@ -165,7 +165,7 @@ impl ToolParser for Granite4ToolParser {
165 fn finish(&mut self) -> Result<ToolParserOutput> {
166 let mut output = ToolParserOutput::default();
167 match &self.mode {
168 - Granite4Mode::Text => output.normal_text.push_str(&self.buffer),
168 + Granite4Mode::Text => output.push_text(&self.buffer),
169 Granite4Mode::Header | Granite4Mode::Args { .. } | Granite4Mode::Close => {
170 return Err(parsing_failed!("incomplete Granite4 tool call"));
171 }
@@ -287,8 +287,8 @@ mod tests {
287 let mut parser = Granite4ToolParser::new(&test_tools());
288 let output = parser.parse_complete("Hello, world!").unwrap();
289
290 - assert_eq!(output.normal_text, "Hello, world!");
291 - assert!(output.calls.is_empty());
290 + assert_eq!(output.normal_text(), "Hello, world!");
291 + assert!(output.calls().is_empty());
292 }
293
294 #[test]
@@ -300,10 +300,10 @@ mod tests {
300 )
301 .unwrap();
302
303 - assert_eq!(output.normal_text, "");
304 - assert_eq!(output.calls.len(), 1);
305 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
306 - assert_eq!(output.calls[0].arguments, r#"{"city":"Boston"}"#);
303 + assert_eq!(output.normal_text(), "");
304 + assert_eq!(output.calls().len(), 1);
305 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
306 + assert_eq!(output.calls()[0].arguments, r#"{"city":"Boston"}"#);
307 }
308
309 #[test]
@@ -317,9 +317,9 @@ mod tests {
317 )
318 .unwrap();
319
320 - assert_eq!(output.calls.len(), 1);
321 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
322 - assert_eq!(output.calls[0].arguments, r#"{"city":"Boston"}"#);
320 + assert_eq!(output.calls().len(), 1);
321 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
322 + assert_eq!(output.calls()[0].arguments, r#"{"city":"Boston"}"#);
323 }
324
325 #[test]
@@ -333,22 +333,28 @@ mod tests {
333
334 expect![[r#"
335 ToolParserOutput {
336 - normal_text: "before middle after",
337 - calls: [
338 - ToolCallDelta {
339 - tool_index: 0,
340 - name: Some(
341 - "find_bbox",
342 - ),
343 - arguments: "{\"x\":1}",
344 - },
345 - ToolCallDelta {
346 - tool_index: 1,
347 - name: Some(
348 - "get_weather",
349 - ),
350 - arguments: "{\"city\":\"Boston\"}",
351 - },
336 + events: [
337 + Text(
338 + "before middle after",
339 + ),
340 + ToolCall(
341 + ToolCallDelta {
342 + tool_index: 0,
343 + name: Some(
344 + "find_bbox",
345 + ),
346 + arguments: "{\"x\":1}",
347 + },
348 + ),
349 + ToolCall(
350 + ToolCallDelta {
351 + tool_index: 1,
352 + name: Some(
353 + "get_weather",
354 + ),
355 + arguments: "{\"city\":\"Boston\"}",
356 + },
357 + ),
358 ],
359 }
360 "#]]
@@ -363,10 +369,10 @@ mod tests {
369
370 let output = collect_stream(&mut parser, &chunks);
371
366 - assert_eq!(output.normal_text, "hello bye");
367 - assert_eq!(output.calls.len(), 1);
368 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
369 - assert_eq!(output.calls[0].arguments, r#"{"city":"Tokyo"}"#);
372 + assert_eq!(output.normal_text(), "hello bye");
373 + assert_eq!(output.calls().len(), 1);
374 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
375 + assert_eq!(output.calls()[0].arguments, r#"{"city":"Tokyo"}"#);
376 }
377
378 #[test]
@@ -385,7 +391,7 @@ mod tests {
391 for chunk in chunks {
392 let next = parser.parse_chunk(chunk).unwrap();
393 observed_arguments.extend(
388 - next.calls
394 + next.calls()
395 .iter()
396 .filter(|call| call.name.is_none())
397 .map(|call| call.arguments.clone()),
@@ -396,7 +402,7 @@ mod tests {
402
403 assert_eq!(observed_arguments, [r#"{"city":"#, r#""Beijing""#, r#"}"#]);
404 assert_eq!(
399 - output.coalesce_calls().calls[0].arguments,
405 + output.coalesce().calls()[0].arguments,
406 r#"{"city":"Beijing"}"#
407 );
408 }
@@ -409,9 +415,9 @@ mod tests {
415
416 let output = collect_stream(&mut parser, &chunks);
417
412 - assert_eq!(output.calls.len(), 1);
413 - assert_eq!(output.calls[0].name.as_deref(), Some("f"));
414 - assert_eq!(output.calls[0].arguments, r#"{"a":1}"#);
418 + assert_eq!(output.calls().len(), 1);
419 + assert_eq!(output.calls()[0].name.as_deref(), Some("f"));
420 + assert_eq!(output.calls()[0].arguments, r#"{"a":1}"#);
421 }
422
423 #[test]
@@ -435,29 +441,37 @@ mod tests {
441
442 expect![[r#"
443 ToolParserOutput {
438 - normal_text: "Here goes the bbox call: \n Now the stock price call: \n Now another bbox call: \n See? I'm a helpful assistant.",
439 - calls: [
440 - ToolCallDelta {
441 - tool_index: 0,
442 - name: Some(
443 - "find_bbox",
444 - ),
445 - arguments: "{\"coordinates\": [[23.54, 43.1], [-12.2, 54.3], [4, 5]], \"coordinate_type\": \"latlong\"}",
446 - },
447 - ToolCallDelta {
448 - tool_index: 1,
449 - name: Some(
450 - "get_stock_price",
451 - ),
452 - arguments: "{\"symbol\": \"AAPL\", \"start_date\": \"2021-01-01\", \"end_date\": \"2021-12-31\"}",
453 - },
454 - ToolCallDelta {
455 - tool_index: 2,
456 - name: Some(
457 - "find_bbox",
458 - ),
459 - arguments: "{\"coordinates\": [[23.54, 43.1], [-12.2, 54.3], [4, 5]], \"coordinate_type\": \"latlong\"}",
460 - },
444 + events: [
445 + Text(
446 + "Here goes the bbox call: \n Now the stock price call: \n Now another bbox call: \n See? I'm a helpful assistant.",
447 + ),
448 + ToolCall(
449 + ToolCallDelta {
450 + tool_index: 0,
451 + name: Some(
452 + "find_bbox",
453 + ),
454 + arguments: "{\"coordinates\": [[23.54, 43.1], [-12.2, 54.3], [4, 5]], \"coordinate_type\": \"latlong\"}",
455 + },
456 + ),
457 + ToolCall(
458 + ToolCallDelta {
459 + tool_index: 1,
460 + name: Some(
461 + "get_stock_price",
462 + ),
463 + arguments: "{\"symbol\": \"AAPL\", \"start_date\": \"2021-01-01\", \"end_date\": \"2021-12-31\"}",
464 + },
465 + ),
466 + ToolCall(
467 + ToolCallDelta {
468 + tool_index: 2,
469 + name: Some(
470 + "find_bbox",
471 + ),
472 + arguments: "{\"coordinates\": [[23.54, 43.1], [-12.2, 54.3], [4, 5]], \"coordinate_type\": \"latlong\"}",
473 + },
474 + ),
475 ],
476 }
477 "#]].assert_debug_eq(&output);
rust/src/parser/src/tool/json/hermes.rs
+35 -32
@@ -80,8 +80,8 @@ mod tests {
80 let mut parser = HermesToolParser::new(&test_tools());
81 let output = parser.parse_complete("Hello, world!").unwrap();
82
83 - assert_eq!(output.normal_text, "Hello, world!");
84 - assert!(output.calls.is_empty());
83 + assert_eq!(output.normal_text(), "Hello, world!");
84 + assert!(output.calls().is_empty());
85 }
86
87 #[test]
@@ -95,11 +95,11 @@ mod tests {
95 ))
96 .unwrap();
97
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);
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]
@@ -112,8 +112,8 @@ mod tests {
112 )
113 .unwrap();
114
115 - assert_eq!(output.calls.len(), 1);
116 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
115 + assert_eq!(output.calls().len(), 1);
116 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
117 }
118
119 #[test]
@@ -122,7 +122,7 @@ mod tests {
122 let arguments = r#"{"location":"Tokyo",}"#;
123 let output = parser.parse_complete(&build_tool_call("get_weather", arguments)).unwrap();
124
125 - assert_eq!(output.calls[0].arguments, arguments);
125 + assert_eq!(output.calls()[0].arguments, arguments);
126 }
127
128 #[test]
@@ -142,7 +142,7 @@ mod tests {
142 for chunk in chunks {
143 let next = parser.parse_chunk(chunk).unwrap();
144 observed_arguments.extend(
145 - next.calls
145 + next.calls()
146 .iter()
147 .filter(|call| call.name.is_none())
148 .map(|call| call.arguments.clone()),
@@ -152,9 +152,9 @@ mod tests {
152 output.append(parser.finish().unwrap());
153
154 assert_eq!(observed_arguments, ["{\"location\":", "\"Beijing\"", "}"]);
155 - assert_eq!(output.normal_text, "preface suffix");
155 + assert_eq!(output.normal_text(), "preface suffix");
156 assert_eq!(
157 - output.coalesce_calls().calls[0].arguments,
157 + output.coalesce().calls()[0].arguments,
158 r#"{"location":"Beijing"}"#
159 );
160 }
@@ -170,9 +170,9 @@ mod tests {
170
171 let output = collect_stream(&mut parser, &chunks);
172
173 - assert_eq!(output.normal_text, "hello ");
174 - assert_eq!(output.calls.len(), 1);
175 - assert_eq!(output.calls[0].arguments, r#"{"location":"Tokyo"}"#);
173 + assert_eq!(output.normal_text(), "hello ");
174 + assert_eq!(output.calls().len(), 1);
175 + assert_eq!(output.calls()[0].arguments, r#"{"location":"Tokyo"}"#);
176 }
177
178 #[test]
@@ -189,22 +189,25 @@ mod tests {
189
190 expect![[r#"
191 ToolParserOutput {
192 - normal_text: "",
193 - calls: [
194 - ToolCallDelta {
195 - tool_index: 0,
196 - name: Some(
197 - "get_weather",
198 - ),
199 - arguments: "{\"location\":\"Shanghai\"}",
200 - },
201 - ToolCallDelta {
202 - tool_index: 1,
203 - name: Some(
204 - "add",
205 - ),
206 - arguments: "{\"x\":1,\"y\":2}",
207 - },
192 + events: [
193 + ToolCall(
194 + ToolCallDelta {
195 + tool_index: 0,
196 + name: Some(
197 + "get_weather",
198 + ),
199 + arguments: "{\"location\":\"Shanghai\"}",
200 + },
201 + ),
202 + ToolCall(
203 + ToolCallDelta {
204 + tool_index: 1,
205 + name: Some(
206 + "add",
207 + ),
208 + arguments: "{\"x\":1,\"y\":2}",
209 + },
210 + ),
211 ],
212 }
213 "#]]
rust/src/parser/src/tool/json/internlm2.rs
+41 -38
@@ -140,8 +140,8 @@ mod tests {
140 let mut parser = Internlm2ToolParser::new(&test_tools());
141 let result = parser.parse_complete("Hello, world!").unwrap();
142
143 - assert_eq!(result.normal_text, "Hello, world!");
144 - assert!(result.calls.is_empty());
143 + assert_eq!(result.normal_text(), "Hello, world!");
144 + assert!(result.calls().is_empty());
145 }
146
147 #[test]
@@ -155,11 +155,11 @@ mod tests {
155 ))
156 .unwrap();
157
158 - assert_eq!(result.normal_text, "Let me check.\n");
159 - assert_eq!(result.calls.len(), 1);
160 - assert_eq!(result.calls[0].tool_index, 0);
161 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
162 - assert_eq!(result.calls[0].arguments, arguments);
158 + assert_eq!(result.normal_text(), "Let me check.\n");
159 + assert_eq!(result.calls().len(), 1);
160 + assert_eq!(result.calls()[0].tool_index, 0);
161 + assert_eq!(result.calls()[0].name.as_deref(), Some("get_weather"));
162 + assert_eq!(result.calls()[0].arguments, arguments);
163 }
164
165 #[test]
@@ -170,9 +170,9 @@ mod tests {
170 .parse_complete(&build_tool_call("get_weather", "arguments", arguments))
171 .unwrap();
172
173 - assert_eq!(result.calls.len(), 1);
174 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
175 - assert_eq!(result.calls[0].arguments, arguments);
173 + assert_eq!(result.calls().len(), 1);
174 + assert_eq!(result.calls()[0].name.as_deref(), Some("get_weather"));
175 + assert_eq!(result.calls()[0].arguments, arguments);
176 }
177
178 #[test]
@@ -185,8 +185,8 @@ mod tests {
185 ))
186 .unwrap();
187
188 - assert_eq!(result.calls.len(), 1);
189 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
188 + assert_eq!(result.calls().len(), 1);
189 + assert_eq!(result.calls()[0].name.as_deref(), Some("get_weather"));
190 }
191
192 #[test]
@@ -197,7 +197,7 @@ mod tests {
197 .parse_complete(&build_tool_call("get_weather", "parameters", arguments))
198 .unwrap();
199
200 - assert_eq!(result.calls[0].arguments, arguments);
200 + assert_eq!(result.calls()[0].arguments, arguments);
201 }
202
203 #[test]
@@ -218,7 +218,7 @@ mod tests {
218 for chunk in chunks {
219 let next = parser.parse_chunk(chunk).unwrap();
220 observed_arguments.extend(
221 - next.calls
221 + next.calls()
222 .iter()
223 .filter(|call| call.name.is_none())
224 .map(|call| call.arguments.clone()),
@@ -231,9 +231,9 @@ mod tests {
231 observed_arguments,
232 [r#"{"location":"#, r#""Beijing""#, r#"}"#]
233 );
234 - assert_eq!(result.normal_text, "preface suffix");
234 + assert_eq!(result.normal_text(), "preface suffix");
235 assert_eq!(
236 - result.coalesce_calls().calls[0].arguments,
236 + result.coalesce().calls()[0].arguments,
237 r#"{"location":"Beijing"}"#
238 );
239 }
@@ -249,9 +249,9 @@ mod tests {
249
250 let result = collect_stream(&mut parser, &chunks);
251
252 - assert_eq!(result.normal_text, "hello ");
253 - assert_eq!(result.calls.len(), 1);
254 - assert_eq!(result.calls[0].arguments, r#"{"location":"Tokyo"}"#);
252 + assert_eq!(result.normal_text(), "hello ");
253 + assert_eq!(result.calls().len(), 1);
254 + assert_eq!(result.calls()[0].arguments, r#"{"location":"Tokyo"}"#);
255 }
256
257 #[test]
@@ -268,22 +268,25 @@ mod tests {
268
269 expect![[r#"
270 ToolParserOutput {
271 - normal_text: "",
272 - calls: [
273 - ToolCallDelta {
274 - tool_index: 0,
275 - name: Some(
276 - "get_weather",
277 - ),
278 - arguments: "{\"location\":\"Shanghai\"}",
279 - },
280 - ToolCallDelta {
281 - tool_index: 1,
282 - name: Some(
283 - "add",
284 - ),
285 - arguments: "{\"x\":1,\"y\":2}",
286 - },
271 + events: [
272 + ToolCall(
273 + ToolCallDelta {
274 + tool_index: 0,
275 + name: Some(
276 + "get_weather",
277 + ),
278 + arguments: "{\"location\":\"Shanghai\"}",
279 + },
280 + ),
281 + ToolCall(
282 + ToolCallDelta {
283 + tool_index: 1,
284 + name: Some(
285 + "add",
286 + ),
287 + arguments: "{\"x\":1,\"y\":2}",
288 + },
289 + ),
290 ],
291 }
292 "#]]
@@ -298,8 +301,8 @@ mod tests {
301
302 let result = parser.parse_complete(&input).unwrap();
303
301 - assert_eq!(result.calls.len(), 1);
302 - assert_eq!(result.calls[0].arguments, arguments);
304 + assert_eq!(result.calls().len(), 1);
305 + assert_eq!(result.calls()[0].arguments, arguments);
306 }
307
308 #[test]
@@ -313,7 +316,7 @@ mod tests {
316 let error = parser.finish().unwrap_err();
317
318 assert_eq!(
316 - pre_finish.calls[0].name.as_deref(),
319 + pre_finish.calls()[0].name.as_deref(),
320 Some("get_weather"),
321 "name delta is still emitted from parse_chunk() before truncation",
322 );
rust/src/parser/src/tool/json/llama.rs
+45 -42
@@ -87,7 +87,7 @@ impl Llama3JsonToolParser {
87 self.mode = LlamaJsonMode::Arguments {
88 json_scan: JsonObjectScanState::default(),
89 };
90 - output.calls.push(ToolCallDelta {
90 + output.push_call(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 - output.calls.push(ToolCallDelta {
102 + output.push_call(ToolCallDelta {
103 tool_index,
104 name: None,
105 arguments: self.buffer[..consumed_len].to_string(),
@@ -145,7 +145,7 @@ impl ToolParser for Llama3JsonToolParser {
145 }
146
147 if matches!(self.mode, LlamaJsonMode::Passthrough) {
148 - output.normal_text.push_str(&self.buffer);
148 + output.push_text(&self.buffer);
149 self.buffer.clear();
150 return Ok(());
151 }
@@ -164,7 +164,7 @@ impl ToolParser for Llama3JsonToolParser {
164 let mut output = ToolParserOutput::default();
165 match &self.mode {
166 LlamaJsonMode::Start | LlamaJsonMode::Passthrough => {
167 - output.normal_text.push_str(&self.buffer);
167 + output.push_text(&self.buffer);
168 }
169 LlamaJsonMode::AfterCall if self.buffer.trim().is_empty() => {}
170 LlamaJsonMode::Header | LlamaJsonMode::Arguments { .. } => {
@@ -268,8 +268,8 @@ mod tests {
268 let mut parser = Llama3JsonToolParser::new(&test_tools());
269 let output = parser.parse_complete("Hello, world!").unwrap();
270
271 - assert_eq!(output.normal_text, "Hello, world!");
272 - assert!(output.calls.is_empty());
271 + assert_eq!(output.normal_text(), "Hello, world!");
272 + assert!(output.calls().is_empty());
273 }
274
275 #[test]
@@ -284,10 +284,10 @@ mod tests {
284 output.append(parser.finish().unwrap());
285
286 assert_eq!(
287 - output.normal_text,
287 + output.normal_text(),
288 r#"plain text first {"name":"get_weather","parameters":{"location":"Tokyo"}}"#
289 );
290 - assert!(output.calls.is_empty());
290 + assert!(output.calls().is_empty());
291 }
292
293 #[test]
@@ -299,8 +299,8 @@ mod tests {
299 );
300 let output = parser.parse_complete(&input).unwrap();
301
302 - assert_eq!(output.normal_text, input);
303 - assert!(output.calls.is_empty());
302 + assert_eq!(output.normal_text(), input);
303 + assert!(output.calls().is_empty());
304 }
305
306 #[test]
@@ -312,8 +312,8 @@ mod tests {
312 );
313 let output = parser.parse_complete(&input).unwrap();
314
315 - assert_eq!(output.normal_text, input);
316 - assert!(output.calls.is_empty());
315 + assert_eq!(output.normal_text(), input);
316 + assert!(output.calls().is_empty());
317 }
318
319 #[test]
@@ -322,10 +322,10 @@ mod tests {
322 let arguments = r#"{ "location": "Tokyo", "days": 3 }"#;
323 let output = parser.parse_complete(&build_tool_call("get_weather", arguments)).unwrap();
324
325 - assert_eq!(output.calls.len(), 1);
326 - assert_eq!(output.calls[0].tool_index, 0);
327 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
328 - assert_eq!(output.calls[0].arguments, arguments);
325 + assert_eq!(output.calls().len(), 1);
326 + assert_eq!(output.calls()[0].tool_index, 0);
327 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
328 + assert_eq!(output.calls()[0].arguments, arguments);
329 }
330
331 #[test]
@@ -353,22 +353,25 @@ mod tests {
353
354 expect![[r#"
355 ToolParserOutput {
356 - normal_text: "",
357 - calls: [
358 - ToolCallDelta {
359 - tool_index: 0,
360 - name: Some(
361 - "get_weather",
362 - ),
363 - arguments: "{\"location\":\"Shanghai\"}",
364 - },
365 - ToolCallDelta {
366 - tool_index: 1,
367 - name: Some(
368 - "add",
369 - ),
370 - arguments: "{\"x\":1,\"y\":2}",
371 - },
356 + events: [
357 + ToolCall(
358 + ToolCallDelta {
359 + tool_index: 0,
360 + name: Some(
361 + "get_weather",
362 + ),
363 + arguments: "{\"location\":\"Shanghai\"}",
364 + },
365 + ),
366 + ToolCall(
367 + ToolCallDelta {
368 + tool_index: 1,
369 + name: Some(
370 + "add",
371 + ),
372 + arguments: "{\"x\":1,\"y\":2}",
373 + },
374 + ),
375 ],
376 }
377 "#]]
@@ -390,7 +393,7 @@ mod tests {
393 for chunk in chunks {
394 let next = parser.parse_chunk(chunk).unwrap();
395 observed_arguments.extend(
393 - next.calls
396 + next.calls()
397 .iter()
398 .filter(|call| call.name.is_none())
399 .map(|call| call.arguments.clone()),
@@ -401,7 +404,7 @@ mod tests {
404
405 assert_eq!(observed_arguments, ["{\"location\":", "\"Beijing\"", "}"]);
406 assert_eq!(
404 - output.coalesce_calls().calls[0].arguments,
407 + output.coalesce().calls()[0].arguments,
408 r#"{"location":"Beijing"}"#
409 );
410 }
@@ -418,14 +421,14 @@ mod tests {
421
422 let output = collect_stream(&mut parser, &chunks);
423
421 - assert_eq!(output.normal_text, "");
422 - assert_eq!(output.calls.len(), 2);
424 + assert_eq!(output.normal_text(), "");
425 + assert_eq!(output.calls().len(), 2);
426 assert_eq!(
424 - output.calls[0].arguments,
427 + output.calls()[0].arguments,
428 r#"{"location":"Dallas","state":"TX"}"#
429 );
427 - assert_eq!(output.calls[1].name.as_deref(), Some("add"));
428 - assert_eq!(output.calls[1].arguments, r#"{"x":4,"y":5}"#);
430 + assert_eq!(output.calls()[1].name.as_deref(), Some("add"));
431 + assert_eq!(output.calls()[1].arguments, r#"{"x":4,"y":5}"#);
432 }
433
434 #[test]
@@ -437,7 +440,7 @@ mod tests {
440 }"#;
441 let output = parser.parse_complete(&build_tool_call("convert", arguments)).unwrap();
442
440 - assert_eq!(output.calls[0].arguments, arguments);
443 + assert_eq!(output.calls()[0].arguments, arguments);
444 }
445
446 #[test]
@@ -450,8 +453,8 @@ mod tests {
453 ))
454 .unwrap();
455
453 - assert_eq!(output.normal_text, "");
454 - assert_eq!(output.calls.len(), 1);
456 + assert_eq!(output.normal_text(), "");
457 + assert_eq!(output.calls().len(), 1);
458 }
459
460 #[test]
rust/src/parser/src/tool/json/mistral.rs
+38 -32
@@ -77,8 +77,8 @@ mod tests {
77 let mut parser = MistralToolParser::new(&test_tools());
78 let output = parser.parse_complete("Hello, world!").unwrap();
79
80 - assert_eq!(output.normal_text, "Hello, world!");
81 - assert!(output.calls.is_empty());
80 + assert_eq!(output.normal_text(), "Hello, world!");
81 + assert!(output.calls().is_empty());
82 }
83
84 #[test]
@@ -92,11 +92,11 @@ mod tests {
92 ))
93 .unwrap();
94
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);
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]
@@ -115,22 +115,28 @@ mod tests {
115
116 expect![[r#"
117 ToolParserOutput {
118 - normal_text: "I'll help.\n",
119 - calls: [
120 - ToolCallDelta {
121 - tool_index: 0,
122 - name: Some(
123 - "get_weather",
124 - ),
125 - arguments: "{\"city\": \"Tokyo\", \"units\": \"celsius\"}",
126 - },
127 - ToolCallDelta {
128 - tool_index: 1,
129 - name: Some(
130 - "add",
131 - ),
132 - arguments: "{\"x\": 1, \"y\": 2}",
133 - },
118 + events: [
119 + Text(
120 + "I'll help.\n",
121 + ),
122 + ToolCall(
123 + ToolCallDelta {
124 + tool_index: 0,
125 + name: Some(
126 + "get_weather",
127 + ),
128 + arguments: "{\"city\": \"Tokyo\", \"units\": \"celsius\"}",
129 + },
130 + ),
131 + ToolCall(
132 + ToolCallDelta {
133 + tool_index: 1,
134 + name: Some(
135 + "add",
136 + ),
137 + arguments: "{\"x\": 1, \"y\": 2}",
138 + },
139 + ),
140 ],
141 }
142 "#]]
@@ -148,7 +154,7 @@ mod tests {
154 )]))
155 .unwrap();
156
151 - assert_eq!(output.calls[0].arguments, arguments);
157 + assert_eq!(output.calls()[0].arguments, arguments);
158 }
159
160 #[test]
@@ -168,7 +174,7 @@ mod tests {
174 for chunk in chunks {
175 let next = parser.parse_chunk(chunk).unwrap();
176 observed_arguments.extend(
171 - next.calls
177 + next.calls()
178 .iter()
179 .filter(|call| call.name.is_none())
180 .map(|call| call.arguments.clone()),
@@ -178,9 +184,9 @@ mod tests {
184 output.append(parser.finish().unwrap());
185
186 assert_eq!(observed_arguments, ["{\"location\":", "\"Beijing\"", "}"]);
181 - assert_eq!(output.normal_text, "preface suffix");
187 + assert_eq!(output.normal_text(), "preface suffix");
188 assert_eq!(
183 - output.coalesce_calls().calls[0].arguments,
189 + output.coalesce().calls()[0].arguments,
190 r#"{"location":"Beijing"}"#
191 );
192 }
@@ -196,9 +202,9 @@ mod tests {
202
203 let output = collect_stream(&mut parser, &chunks);
204
199 - assert_eq!(output.normal_text, "hello ");
200 - assert_eq!(output.calls.len(), 1);
201 - assert_eq!(output.calls[0].arguments, r#"{"location":"Tokyo"}"#);
205 + assert_eq!(output.normal_text(), "hello ");
206 + assert_eq!(output.calls().len(), 1);
207 + assert_eq!(output.calls()[0].arguments, r#"{"location":"Tokyo"}"#);
208 }
209
210 #[test]
@@ -209,8 +215,8 @@ mod tests {
215 .parse_complete(&build_tool_calls(&[build_tool_call("echo", arguments)]))
216 .unwrap();
217
212 - assert_eq!(output.calls.len(), 1);
213 - assert_eq!(output.calls[0].arguments, arguments);
218 + assert_eq!(output.calls().len(), 1);
219 + assert_eq!(output.calls()[0].arguments, arguments);
220 }
221
222 #[test]
rust/src/parser/src/tool/json/mod.rs
+56 -46
@@ -106,7 +106,7 @@ impl JsonToolCallParser {
106 fn finish(&mut self) -> Result<ToolParserOutput> {
107 let mut output = ToolParserOutput::default();
108 match &self.mode {
109 - JsonToolCallMode::Text => output.normal_text.push_str(&self.buffer),
109 + JsonToolCallMode::Text => output.push_text(&self.buffer),
110 JsonToolCallMode::Header | JsonToolCallMode::Arguments { .. } => {
111 return Err(parsing_failed!(
112 "incomplete {} tool call",
@@ -126,7 +126,7 @@ impl JsonToolCallParser {
126 ) -> Result<()> {
127 match event {
128 JsonToolCallEvent::Text { len: consumed_len } => {
129 - output.normal_text.push_str(&self.buffer[..consumed_len]);
129 + output.push_text(&self.buffer[..consumed_len]);
130 }
131 JsonToolCallEvent::ToolCallStart => self.mode = JsonToolCallMode::Header,
132 JsonToolCallEvent::ToolCallHeader { function_name } => {
@@ -136,7 +136,7 @@ impl JsonToolCallParser {
136 self.mode = JsonToolCallMode::Arguments {
137 json_scan: JsonObjectScanState::default(),
138 };
139 - output.calls.push(ToolCallDelta {
139 + output.push_call(ToolCallDelta {
140 tool_index,
141 name: Some(function_name),
142 arguments: String::new(),
@@ -149,7 +149,7 @@ impl JsonToolCallParser {
149 self.config.parser_name
150 ));
151 };
152 - output.calls.push(ToolCallDelta {
152 + output.push_call(ToolCallDelta {
153 tool_index,
154 name: None,
155 arguments: self.buffer[..consumed_len].to_string(),
@@ -400,7 +400,7 @@ mod tests {
400 parser.parse_into(chunk, &mut output).unwrap();
401 }
402 output.append(parser.finish().unwrap());
403 - output.coalesce_calls()
403 + output.coalesce()
404 }
405
406 #[test]
@@ -415,22 +415,25 @@ mod tests {
415
416 expect![[r#"
417 ToolParserOutput {
418 - normal_text: "",
419 - calls: [
420 - ToolCallDelta {
421 - tool_index: 0,
422 - name: Some(
423 - "get_weather",
424 - ),
425 - arguments: "{\"location\":\"Shanghai\"}",
426 - },
427 - ToolCallDelta {
428 - tool_index: 1,
429 - name: Some(
430 - "add",
431 - ),
432 - arguments: "{\"x\":1,\"y\":2}",
433 - },
418 + events: [
419 + ToolCall(
420 + ToolCallDelta {
421 + tool_index: 0,
422 + name: Some(
423 + "get_weather",
424 + ),
425 + arguments: "{\"location\":\"Shanghai\"}",
426 + },
427 + ),
428 + ToolCall(
429 + ToolCallDelta {
430 + tool_index: 1,
431 + name: Some(
432 + "add",
433 + ),
434 + arguments: "{\"x\":1,\"y\":2}",
435 + },
436 + ),
437 ],
438 }
439 "#]]
@@ -451,22 +454,25 @@ mod tests {
454
455 expect![[r#"
456 ToolParserOutput {
454 - normal_text: "",
455 - calls: [
456 - ToolCallDelta {
457 - tool_index: 0,
458 - name: Some(
459 - "get_weather",
460 - ),
461 - arguments: "{\"location\":\"Shanghai\"}",
462 - },
463 - ToolCallDelta {
464 - tool_index: 1,
465 - name: Some(
466 - "add",
467 - ),
468 - arguments: "{\"x\":1,\"y\":2}",
469 - },
457 + events: [
458 + ToolCall(
459 + ToolCallDelta {
460 + tool_index: 0,
461 + name: Some(
462 + "get_weather",
463 + ),
464 + arguments: "{\"location\":\"Shanghai\"}",
465 + },
466 + ),
467 + ToolCall(
468 + ToolCallDelta {
469 + tool_index: 1,
470 + name: Some(
471 + "add",
472 + ),
473 + arguments: "{\"x\":1,\"y\":2}",
474 + },
475 + ),
476 ],
477 }
478 "#]]
@@ -486,15 +492,19 @@ mod tests {
492
493 expect![[r#"
494 ToolParserOutput {
489 - normal_text: " trailing text",
490 - calls: [
491 - ToolCallDelta {
492 - tool_index: 0,
493 - name: Some(
494 - "get_weather",
495 - ),
496 - arguments: "{\"location\":\"Shanghai\"}",
497 - },
495 + events: [
496 + Text(
497 + " trailing text",
498 + ),
499 + ToolCall(
500 + ToolCallDelta {
501 + tool_index: 0,
502 + name: Some(
503 + "get_weather",
504 + ),
505 + arguments: "{\"location\":\"Shanghai\"}",
506 + },
507 + ),
508 ],
509 }
510 "#]]
rust/src/parser/src/tool/json/phi4mini.rs
+49 -46
@@ -87,8 +87,8 @@ mod tests {
87 let mut parser = Phi4MiniJsonToolParser::new(&test_tools());
88 let result = parser.parse_complete("Hello, world!").unwrap();
89
90 - assert_eq!(result.normal_text, "Hello, world!");
91 - assert!(result.calls.is_empty());
90 + assert_eq!(result.normal_text(), "Hello, world!");
91 + assert!(result.calls().is_empty());
92 }
93
94 #[test]
@@ -99,10 +99,10 @@ mod tests {
99 .parse_complete(&wrap(&[build_call("get_weather", "arguments", arguments)]))
100 .unwrap();
101
102 - assert_eq!(result.calls.len(), 1);
103 - assert_eq!(result.calls[0].tool_index, 0);
104 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
105 - assert_eq!(result.calls[0].arguments, arguments);
102 + assert_eq!(result.calls().len(), 1);
103 + assert_eq!(result.calls()[0].tool_index, 0);
104 + assert_eq!(result.calls()[0].name.as_deref(), Some("get_weather"));
105 + assert_eq!(result.calls()[0].arguments, arguments);
106 }
107
108 #[test]
@@ -113,9 +113,9 @@ mod tests {
113 .parse_complete(&wrap(&[build_call("get_weather", "parameters", arguments)]))
114 .unwrap();
115
116 - assert_eq!(result.calls.len(), 1);
117 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
118 - assert_eq!(result.calls[0].arguments, arguments);
116 + assert_eq!(result.calls().len(), 1);
117 + assert_eq!(result.calls()[0].name.as_deref(), Some("get_weather"));
118 + assert_eq!(result.calls()[0].arguments, arguments);
119 }
120
121 #[test]
@@ -130,22 +130,25 @@ mod tests {
130
131 expect![[r#"
132 ToolParserOutput {
133 - normal_text: "",
134 - calls: [
135 - ToolCallDelta {
136 - tool_index: 0,
137 - name: Some(
138 - "get_weather",
139 - ),
140 - arguments: "{\"location\":\"Shanghai\"}",
141 - },
142 - ToolCallDelta {
143 - tool_index: 1,
144 - name: Some(
145 - "add",
146 - ),
147 - arguments: "{\"x\":1,\"y\":2}",
148 - },
133 + events: [
134 + ToolCall(
135 + ToolCallDelta {
136 + tool_index: 0,
137 + name: Some(
138 + "get_weather",
139 + ),
140 + arguments: "{\"location\":\"Shanghai\"}",
141 + },
142 + ),
143 + ToolCall(
144 + ToolCallDelta {
145 + tool_index: 1,
146 + name: Some(
147 + "add",
148 + ),
149 + arguments: "{\"x\":1,\"y\":2}",
150 + },
151 + ),
152 ],
153 }
154 "#]]
@@ -162,8 +165,8 @@ mod tests {
165 .parse_complete(&wrap(&[build_call("convert", "arguments", arguments)]))
166 .unwrap();
167
165 - assert_eq!(result.calls.len(), 1);
166 - assert_eq!(result.calls[0].arguments, arguments);
168 + assert_eq!(result.calls().len(), 1);
169 + assert_eq!(result.calls()[0].arguments, arguments);
170 }
171
172 /// Preface text before a tool call is preserved as normal_text, consistent
@@ -182,8 +185,8 @@ mod tests {
185
186 let result = parser.parse_complete(&input).unwrap();
187
185 - assert_eq!(result.normal_text, "Let me check.\n");
186 - assert_eq!(result.calls.len(), 1);
188 + assert_eq!(result.normal_text(), "Let me check.\n");
189 + assert_eq!(result.calls().len(), 1);
190 }
191
192 #[test]
@@ -194,7 +197,7 @@ mod tests {
197 .parse_complete(&wrap(&[build_call("get_weather", "arguments", arguments)]))
198 .unwrap();
199
197 - assert_eq!(result.calls[0].arguments, arguments);
200 + assert_eq!(result.calls()[0].arguments, arguments);
201 }
202
203 /// The bundled `tool_chat_template_phi4_mini.jinja` emits objects with
@@ -208,9 +211,9 @@ mod tests {
211
212 let result = parser.parse_complete(input).unwrap();
213
211 - assert_eq!(result.calls.len(), 1);
212 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
213 - assert_eq!(result.calls[0].arguments, r#"{"location": "Tokyo"}"#);
214 + assert_eq!(result.calls().len(), 1);
215 + assert_eq!(result.calls()[0].name.as_deref(), Some("get_weather"));
216 + assert_eq!(result.calls()[0].arguments, r#"{"location": "Tokyo"}"#);
217 }
218
219 /// Argument deltas are streamed through the shared JSON core.
@@ -230,10 +233,10 @@ mod tests {
233
234 let result = collect_stream(&mut parser, &chunks);
235
233 - assert_eq!(result.normal_text, "preface suffix");
234 - assert_eq!(result.calls.len(), 1);
235 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
236 - assert_eq!(result.calls[0].arguments, r#"{"location":"Beijing"}"#);
236 + assert_eq!(result.normal_text(), "preface suffix");
237 + assert_eq!(result.calls().len(), 1);
238 + assert_eq!(result.calls()[0].name.as_deref(), Some("get_weather"));
239 + assert_eq!(result.calls()[0].arguments, r#"{"location":"Beijing"}"#);
240 }
241
242 #[test]
@@ -251,9 +254,9 @@ mod tests {
254
255 let result = collect_stream(&mut parser, &chunks);
256
254 - assert_eq!(result.normal_text, "hello ");
255 - assert_eq!(result.calls.len(), 1);
256 - assert_eq!(result.calls[0].arguments, r#"{"location":"Tokyo"}"#);
257 + assert_eq!(result.normal_text(), "hello ");
258 + assert_eq!(result.calls().len(), 1);
259 + assert_eq!(result.calls()[0].arguments, r#"{"location":"Tokyo"}"#);
260 }
261
262 #[test]
@@ -287,9 +290,9 @@ mod tests {
290 .parse_complete(&wrap(&[build_call("convert", "arguments", arguments)]))
291 .unwrap();
292
290 - assert_eq!(result.calls.len(), 1);
291 - assert_eq!(result.calls[0].name.as_deref(), Some("convert"));
292 - assert_eq!(result.calls[0].arguments, arguments);
293 + assert_eq!(result.calls().len(), 1);
294 + assert_eq!(result.calls()[0].name.as_deref(), Some("convert"));
295 + assert_eq!(result.calls()[0].arguments, arguments);
296 }
297
298 /// The chat template emits parallel calls as `},\n {` (comma + newline +
@@ -307,9 +310,9 @@ mod tests {
310
311 let result = parser.parse_complete(input).unwrap();
312
310 - assert_eq!(result.calls.len(), 2);
311 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
312 - assert_eq!(result.calls[1].name.as_deref(), Some("add"));
313 + assert_eq!(result.calls().len(), 2);
314 + assert_eq!(result.calls()[0].name.as_deref(), Some("get_weather"));
315 + assert_eq!(result.calls()[1].name.as_deref(), Some("add"));
316 }
317
318 /// The shared core requires an object after the start marker.
rust/src/parser/src/tool/json/qwen.rs
+37 -34
@@ -84,8 +84,8 @@ mod tests {
84 let mut parser = Qwen3XmlToolParser::new(&test_tools());
85 let output = parser.parse_complete("Hello, world!").unwrap();
86
87 - assert_eq!(output.normal_text, "Hello, world!");
88 - assert!(output.calls.is_empty());
87 + assert_eq!(output.normal_text(), "Hello, world!");
88 + assert!(output.calls().is_empty());
89 }
90
91 #[test]
@@ -99,11 +99,11 @@ mod tests {
99 ))
100 .unwrap();
101
102 - assert_eq!(output.normal_text, "Let me check.\n");
103 - assert_eq!(output.calls.len(), 1);
104 - assert_eq!(output.calls[0].tool_index, 0);
105 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
106 - assert_eq!(output.calls[0].arguments, arguments);
102 + assert_eq!(output.normal_text(), "Let me check.\n");
103 + assert_eq!(output.calls().len(), 1);
104 + assert_eq!(output.calls()[0].tool_index, 0);
105 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
106 + assert_eq!(output.calls()[0].arguments, arguments);
107 }
108
109 #[test]
@@ -112,7 +112,7 @@ mod tests {
112 let arguments = r#"{"location":"Tokyo",}"#;
113 let output = parser.parse_complete(&build_tool_call("get_weather", arguments)).unwrap();
114
115 - assert_eq!(output.calls[0].arguments, arguments);
115 + assert_eq!(output.calls()[0].arguments, arguments);
116 }
117
118 #[test]
@@ -132,7 +132,7 @@ mod tests {
132 for chunk in chunks {
133 let next = parser.parse_chunk(chunk).unwrap();
134 observed_arguments.extend(
135 - next.calls
135 + next.calls()
136 .iter()
137 .filter(|call| call.name.is_none())
138 .map(|call| call.arguments.clone()),
@@ -143,7 +143,7 @@ mod tests {
143
144 assert_eq!(observed_arguments, ["{\"location\":", "\"Beijing\"", "}"]);
145 assert_eq!(
146 - output.coalesce_calls().calls[0].arguments,
146 + output.coalesce().calls()[0].arguments,
147 r#"{"location":"Beijing"}"#
148 );
149 }
@@ -159,9 +159,9 @@ mod tests {
159
160 let output = collect_stream(&mut parser, &chunks);
161
162 - assert_eq!(output.normal_text, "hello ");
163 - assert_eq!(output.calls.len(), 1);
164 - assert_eq!(output.calls[0].arguments, r#"{"location":"Tokyo"}"#);
162 + assert_eq!(output.normal_text(), "hello ");
163 + assert_eq!(output.calls().len(), 1);
164 + assert_eq!(output.calls()[0].arguments, r#"{"location":"Tokyo"}"#);
165 }
166
167 #[test]
@@ -170,8 +170,8 @@ mod tests {
170 let arguments = r#"{"text":"literal </tool_call> inside"}"#;
171 let output = parser.parse_complete(&build_tool_call("echo", arguments)).unwrap();
172
173 - assert_eq!(output.calls.len(), 1);
174 - assert_eq!(output.calls[0].arguments, arguments);
173 + assert_eq!(output.calls().len(), 1);
174 + assert_eq!(output.calls()[0].arguments, arguments);
175 }
176
177 #[test]
@@ -185,7 +185,7 @@ mod tests {
185 )
186 .unwrap();
187
188 - assert_eq!(output.calls[0].name.as_deref(), Some("say_\"hi"));
188 + assert_eq!(output.calls()[0].name.as_deref(), Some("say_\"hi"));
189 }
190
191 #[test]
@@ -196,8 +196,8 @@ mod tests {
196
197 let output = parser.parse_complete(input).unwrap();
198
199 - assert_eq!(output.normal_text, input);
200 - assert!(output.calls.is_empty());
199 + assert_eq!(output.normal_text(), input);
200 + assert!(output.calls().is_empty());
201 }
202
203 #[test]
@@ -227,22 +227,25 @@ mod tests {
227
228 expect![[r#"
229 ToolParserOutput {
230 - normal_text: "",
231 - calls: [
232 - ToolCallDelta {
233 - tool_index: 0,
234 - name: Some(
235 - "get_weather",
236 - ),
237 - arguments: "{\"location\":\"Shanghai\"}",
238 - },
239 - ToolCallDelta {
240 - tool_index: 1,
241 - name: Some(
242 - "add",
243 - ),
244 - arguments: "{\"x\":1,\"y\":2}",
245 - },
230 + events: [
231 + ToolCall(
232 + ToolCallDelta {
233 + tool_index: 0,
234 + name: Some(
235 + "get_weather",
236 + ),
237 + arguments: "{\"location\":\"Shanghai\"}",
238 + },
239 + ),
240 + ToolCall(
241 + ToolCallDelta {
242 + tool_index: 1,
243 + name: Some(
244 + "add",
245 + ),
246 + arguments: "{\"x\":1,\"y\":2}",
247 + },
248 + ),
249 ],
250 }
251 "#]]
rust/src/parser/src/tool/kimi_k2.rs
+45 -42
@@ -81,7 +81,7 @@ impl KimiK2ToolParser {
81 fn apply_event(&mut self, event: KimiK2Event, output: &mut ToolParserOutput) -> Result<()> {
82 match event {
83 KimiK2Event::Text { len: consumed_len } => {
84 - output.normal_text.push_str(&self.buffer[..consumed_len]);
84 + output.push_text(&self.buffer[..consumed_len]);
85 }
86 KimiK2Event::ToolCallsStart => self.mode = KimiK2Mode::ToolBlock,
87 KimiK2Event::ToolCallStart => self.mode = KimiK2Mode::Header,
@@ -96,7 +96,7 @@ impl KimiK2ToolParser {
96 json_scan: JsonObjectScanState::default(),
97 };
98 self.call_ids.insert(tool_index, tool_call_id);
99 - output.calls.push(ToolCallDelta {
99 + output.push_call(ToolCallDelta {
100 tool_index,
101 name: Some(function_name),
102 arguments: String::new(),
@@ -108,7 +108,7 @@ impl KimiK2ToolParser {
108 "Kimi K2 arguments without an active tool call"
109 ));
110 };
111 - output.calls.push(ToolCallDelta {
111 + output.push_call(ToolCallDelta {
112 tool_index,
113 name: None,
114 arguments: self.buffer[..consumed_len].to_string(),
@@ -171,7 +171,7 @@ impl ToolParser for KimiK2ToolParser {
171 fn finish(&mut self) -> Result<ToolParserOutput> {
172 let mut output = ToolParserOutput::default();
173 match &self.mode {
174 - KimiK2Mode::Text => output.normal_text.push_str(&self.buffer),
174 + KimiK2Mode::Text => output.push_text(&self.buffer),
175 KimiK2Mode::ToolBlock | KimiK2Mode::Done => {}
176 KimiK2Mode::Header | KimiK2Mode::Arguments { .. } => {
177 return Err(parsing_failed!("incomplete Kimi K2 tool call"));
@@ -357,8 +357,8 @@ mod tests {
357 let mut parser = KimiK2ToolParser::new(&test_tools());
358 let output = parser.parse_complete("Hello, world!").unwrap();
359
360 - assert_eq!(output.normal_text, "Hello, world!");
361 - assert!(output.calls.is_empty());
360 + assert_eq!(output.normal_text(), "Hello, world!");
361 + assert!(output.calls().is_empty());
362 }
363
364 #[test]
@@ -372,11 +372,11 @@ mod tests {
372 ))
373 .unwrap();
374
375 - assert_eq!(output.normal_text, "Checking. ");
376 - assert_eq!(output.calls.len(), 1);
377 - assert_eq!(output.calls[0].tool_index, 0);
378 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
379 - assert_eq!(output.calls[0].arguments, arguments);
375 + assert_eq!(output.normal_text(), "Checking. ");
376 + assert_eq!(output.calls().len(), 1);
377 + assert_eq!(output.calls()[0].tool_index, 0);
378 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
379 + assert_eq!(output.calls()[0].arguments, arguments);
380 }
381
382 #[test]
@@ -391,7 +391,7 @@ mod tests {
391 )]))
392 .unwrap();
393
394 - assert_eq!(output.calls[0].arguments, arguments);
394 + assert_eq!(output.calls()[0].arguments, arguments);
395 }
396
397 #[test]
@@ -414,7 +414,7 @@ mod tests {
414 for chunk in chunks {
415 let next = parser.parse_chunk(chunk).unwrap();
416 observed_arguments.extend(
417 - next.calls
417 + next.calls()
418 .iter()
419 .filter(|call| call.name.is_none())
420 .map(|call| call.arguments.clone()),
@@ -424,8 +424,8 @@ mod tests {
424 output.append(parser.finish().unwrap());
425
426 assert_eq!(observed_arguments, ["{\"location\":", "\"Paris\"", "}"]);
427 - let output = output.coalesce_calls();
428 - assert_eq!(output.calls[0].arguments, r#"{"location":"Paris"}"#);
427 + let output = output.coalesce();
428 + assert_eq!(output.calls()[0].arguments, r#"{"location":"Paris"}"#);
429 }
430
431 #[test]
@@ -445,9 +445,9 @@ mod tests {
445
446 let output = collect_stream(&mut parser, &chunks);
447
448 - assert_eq!(output.normal_text, "hello ");
449 - assert_eq!(output.calls.len(), 1);
450 - assert_eq!(output.calls[0].arguments, r#"{"location":"NYC"}"#);
448 + assert_eq!(output.normal_text(), "hello ");
449 + assert_eq!(output.calls().len(), 1);
450 + assert_eq!(output.calls()[0].arguments, r#"{"location":"NYC"}"#);
451 }
452
453 #[test]
@@ -458,8 +458,8 @@ mod tests {
458
459 let output = parser.parse_complete(&input).unwrap();
460
461 - assert_eq!(output.calls.len(), 1);
462 - assert_eq!(output.calls[0].arguments, arguments);
461 + assert_eq!(output.calls().len(), 1);
462 + assert_eq!(output.calls()[0].arguments, arguments);
463 }
464
465 #[test]
@@ -478,9 +478,9 @@ mod tests {
478
479 let output = collect_stream(&mut parser, &chunks);
480
481 - assert_eq!(output.calls.len(), 1);
481 + assert_eq!(output.calls().len(), 1);
482 assert_eq!(
483 - output.calls[0].arguments,
483 + output.calls()[0].arguments,
484 r#"{"text":"literal <|tool_call_end|> inside"}"#
485 );
486 }
@@ -498,22 +498,25 @@ mod tests {
498
499 expect![[r#"
500 ToolParserOutput {
501 - normal_text: "",
502 - calls: [
503 - ToolCallDelta {
504 - tool_index: 0,
505 - name: Some(
506 - "get_weather",
507 - ),
508 - arguments: "{\"location\":\"Shanghai\"}",
509 - },
510 - ToolCallDelta {
511 - tool_index: 1,
512 - name: Some(
513 - "add",
514 - ),
515 - arguments: "{\"x\":1,\"y\":2}",
516 - },
501 + events: [
502 + ToolCall(
503 + ToolCallDelta {
504 + tool_index: 0,
505 + name: Some(
506 + "get_weather",
507 + ),
508 + arguments: "{\"location\":\"Shanghai\"}",
509 + },
510 + ),
511 + ToolCall(
512 + ToolCallDelta {
513 + tool_index: 1,
514 + name: Some(
515 + "add",
516 + ),
517 + arguments: "{\"x\":1,\"y\":2}",
518 + },
519 + ),
520 ],
521 }
522 "#]]
@@ -546,12 +549,12 @@ mod tests {
549 "{TOOL_CALLS_START}{TOOL_CALL_START}api.tools.search:42{TOOL_CALL_ARGUMENT_START}{{}}{TOOL_CALL_END}{TOOL_CALLS_END}"
550 );
551
549 - let output = parser.parse_chunk(&input).unwrap().coalesce_calls();
552 + let output = parser.parse_chunk(&input).unwrap().coalesce();
553
551 - assert_eq!(output.calls[0].tool_index, 42);
554 + assert_eq!(output.calls()[0].tool_index, 42);
555 assert_eq!(parser.tool_call_id(42), Some("api.tools.search:42"));
553 - assert_eq!(output.calls[0].name.as_deref(), Some("search"));
554 - assert_eq!(output.calls[0].arguments, "{}");
556 + assert_eq!(output.calls()[0].name.as_deref(), Some("search"));
557 + assert_eq!(output.calls()[0].arguments, "{}");
558 }
559
560 #[test]
rust/src/parser/src/tool/minimax_m2.rs
+43 -43
@@ -72,7 +72,7 @@ impl MinimaxM2ToolParser {
72 fn apply_event(&mut self, event: MinimaxM2Event, output: &mut ToolParserOutput) -> Result<()> {
73 match event {
74 MinimaxM2Event::Text { len: consumed_len } => {
75 - output.normal_text.push_str(&self.buffer[..consumed_len]);
75 + output.push_text(&self.buffer[..consumed_len]);
76 }
77 MinimaxM2Event::ToolBlockStart => {
78 self.mode = MinimaxM2Mode::ToolBlock {
@@ -84,7 +84,7 @@ impl MinimaxM2ToolParser {
84 let arguments = serde_json::to_string(&arguments)
85 .map_err(|error| parsing_failed!("failed to serialize arguments: {}", error))?;
86
87 - output.calls.push(ToolCallDelta {
87 + output.push_call(ToolCallDelta {
88 tool_index: self.emitted_tool_count,
89 name: Some(name),
90 arguments,
@@ -133,7 +133,7 @@ impl ToolParser for MinimaxM2ToolParser {
133 let mut output = ToolParserOutput::default();
134 match self.mode {
135 MinimaxM2Mode::Text => {
136 - output.normal_text.push_str(&self.buffer);
136 + output.push_text(&self.buffer);
137 }
138 MinimaxM2Mode::ToolBlock { .. } => {
139 return Err(parsing_failed!("incomplete MiniMax M2 tool call"));
@@ -295,8 +295,8 @@ mod tests {
295 let mut parser = MinimaxM2ToolParser::new(&test_tools());
296 let output = parser.parse_complete("Hello, world!").unwrap();
297
298 - assert_eq!(output.normal_text, "Hello, world!");
299 - assert!(output.calls.is_empty());
298 + assert_eq!(output.normal_text(), "Hello, world!");
299 + assert!(output.calls().is_empty());
300 }
301
302 #[test]
@@ -309,11 +309,11 @@ mod tests {
309 )]))
310 .unwrap();
311
312 - assert!(output.normal_text.is_empty());
313 - assert_eq!(output.calls.len(), 1);
314 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
312 + assert!(output.normal_text().is_empty());
313 + assert_eq!(output.calls().len(), 1);
314 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
315 assert_eq!(
316 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
316 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
317 json!({ "city": "Seattle", "days": 5 })
318 );
319 }
@@ -327,8 +327,8 @@ mod tests {
327 );
328 let output = parser.parse_complete(&output).unwrap();
329
330 - assert_eq!(output.normal_text, "Let me check. ");
331 - assert_eq!(output.calls.len(), 1);
330 + assert_eq!(output.normal_text(), "Let me check. ");
331 + assert_eq!(output.calls().len(), 1);
332 }
333
334 #[test]
@@ -341,15 +341,15 @@ mod tests {
341 ]))
342 .unwrap();
343
344 - assert_eq!(output.calls.len(), 2);
345 - assert_eq!(output.calls[0].tool_index, 0);
346 - assert_eq!(output.calls[1].tool_index, 1);
344 + assert_eq!(output.calls().len(), 2);
345 + assert_eq!(output.calls()[0].tool_index, 0);
346 + assert_eq!(output.calls()[1].tool_index, 1);
347 assert_eq!(
348 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
348 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
349 json!({ "city": "Seattle" })
350 );
351 assert_eq!(
352 - serde_json::from_str::<Value>(&output.calls[1].arguments).unwrap(),
352 + serde_json::from_str::<Value>(&output.calls()[1].arguments).unwrap(),
353 json!({ "city": "NYC" })
354 );
355 }
@@ -371,7 +371,7 @@ mod tests {
371 .unwrap();
372
373 assert_eq!(
374 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
374 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
375 json!({
376 "whole": 5.0,
377 "flag": true,
@@ -395,7 +395,7 @@ mod tests {
395 vec![("city", "Tom &amp; Jerry &lt;3")],
396 )]))
397 .unwrap();
398 - let args: Value = serde_json::from_str(&output.calls[0].arguments).unwrap();
398 + let args: Value = serde_json::from_str(&output.calls()[0].arguments).unwrap();
399 assert_eq!(args["city"], json!("Tom &amp; Jerry &lt;3"));
400 }
401
@@ -416,7 +416,7 @@ mod tests {
416 .unwrap();
417
418 assert_eq!(
419 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
419 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
420 json!({
421 "city": "Seattle &lt;/parameter&gt;&lt;/invoke&gt;&lt;/minimax:tool_call&gt;",
422 "days": 5,
@@ -440,7 +440,7 @@ mod tests {
440 .unwrap();
441
442 assert_eq!(
443 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
443 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
444 json!({
445 "shape": "\nrectangle\n",
446 "dimensions": { "width": 10, "height": 20 },
@@ -462,11 +462,11 @@ mod tests {
462 ],
463 );
464
465 - assert!(output.normal_text.is_empty());
466 - assert_eq!(output.calls.len(), 1);
467 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
465 + assert!(output.normal_text().is_empty());
466 + assert_eq!(output.calls().len(), 1);
467 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
468 assert_eq!(
469 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
469 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
470 json!({ "city": "Seattle" })
471 );
472 }
@@ -484,8 +484,8 @@ mod tests {
484 ],
485 );
486
487 - assert_eq!(output.normal_text, "Let me check. ");
488 - assert_eq!(output.calls.len(), 1);
487 + assert_eq!(output.normal_text(), "Let me check. ");
488 + assert_eq!(output.calls().len(), 1);
489 }
490
491 #[test]
@@ -493,8 +493,8 @@ mod tests {
493 let mut parser = MinimaxM2ToolParser::new(&test_tools());
494 let output = collect_stream(&mut parser, &["Hello, ", "world!"]);
495
496 - assert_eq!(output.normal_text, "Hello, world!");
497 - assert!(output.calls.is_empty());
496 + assert_eq!(output.normal_text(), "Hello, world!");
497 + assert!(output.calls().is_empty());
498 }
499
500 #[test]
@@ -504,8 +504,8 @@ mod tests {
504 let mut parser = MinimaxM2ToolParser::new(&test_tools());
505 let output = collect_stream(&mut parser, &chunks);
506
507 - assert_eq!(output.calls.len(), 1);
508 - assert!(output.normal_text.is_empty());
507 + assert_eq!(output.calls().len(), 1);
508 + assert!(output.normal_text().is_empty());
509 }
510
511 #[test]
@@ -518,9 +518,9 @@ mod tests {
518 let mut parser = MinimaxM2ToolParser::new(&test_tools());
519 let output = collect_stream(&mut parser, &chunks);
520
521 - assert_eq!(output.calls.len(), 2);
522 - assert_eq!(output.calls[0].tool_index, 0);
523 - assert_eq!(output.calls[1].tool_index, 1);
521 + assert_eq!(output.calls().len(), 2);
522 + assert_eq!(output.calls()[0].tool_index, 0);
523 + assert_eq!(output.calls()[1].tool_index, 1);
524 }
525
526 #[test]
@@ -540,12 +540,12 @@ mod tests {
540 let mut parser = MinimaxM2ToolParser::new(&test_tools());
541 let result = collect_stream(&mut parser, &chunks);
542
543 - assert_eq!(result.normal_text, "I will call the tools.\n");
544 - assert_eq!(result.calls.len(), 2);
545 - assert_eq!(result.calls[0].tool_index, 0);
546 - assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
547 - assert_eq!(result.calls[1].tool_index, 1);
548 - assert_eq!(result.calls[1].name.as_deref(), Some("get_weather"));
543 + assert_eq!(result.normal_text(), "I will call the tools.\n");
544 + assert_eq!(result.calls().len(), 2);
545 + assert_eq!(result.calls()[0].tool_index, 0);
546 + assert_eq!(result.calls()[0].name.as_deref(), Some("get_weather"));
547 + assert_eq!(result.calls()[1].tool_index, 1);
548 + assert_eq!(result.calls()[1].name.as_deref(), Some("get_weather"));
549 }
550
551 #[test]
@@ -558,8 +558,8 @@ mod tests {
558 let mut parser = MinimaxM2ToolParser::new(&test_tools());
559 let output = collect_stream(&mut parser, &chunks);
560
561 - assert!(output.normal_text.is_empty());
562 - assert_eq!(output.calls.len(), 1);
561 + assert!(output.normal_text().is_empty());
562 + assert_eq!(output.calls().len(), 1);
563 }
564
565 #[test]
@@ -568,8 +568,8 @@ mod tests {
568 let output =
569 parser.parse_chunk(r#"<minimax:tool_call><invoke name="get_weather">"#).unwrap();
570
571 - assert!(output.normal_text.is_empty());
572 - assert!(output.calls.is_empty());
571 + assert!(output.normal_text().is_empty());
572 + assert!(output.calls().is_empty());
573 }
574
575 #[test]
rust/src/parser/src/tool/minimax_m3.rs
+72 -44
@@ -107,7 +107,7 @@ impl MinimaxM3ToolParser {
107 fn apply_event(&mut self, event: MinimaxM3Event, output: &mut ToolParserOutput) -> Result<()> {
108 match event {
109 MinimaxM3Event::Text { len: consumed_len } => {
110 - output.normal_text.push_str(&self.buffer[..consumed_len]);
110 + output.push_text(&self.buffer[..consumed_len]);
111 }
112 MinimaxM3Event::ToolBlockStart => {
113 self.mode = MinimaxM3Mode::ToolBlock {
@@ -119,7 +119,7 @@ impl MinimaxM3ToolParser {
119 let arguments = serde_json::to_string(&arguments)
120 .map_err(|error| parsing_failed!("failed to serialize arguments: {}", error))?;
121
122 - output.calls.push(ToolCallDelta {
122 + output.push_call(ToolCallDelta {
123 tool_index: self.emitted_tool_count,
124 name: Some(name),
125 arguments,
@@ -158,7 +158,7 @@ impl ToolParser for MinimaxM3ToolParser {
158 let mut output = ToolParserOutput::default();
159 match self.mode {
160 MinimaxM3Mode::Text => {
161 - output.normal_text.push_str(&self.buffer);
161 + output.push_text(&self.buffer);
162 }
163 MinimaxM3Mode::ToolBlock { .. } => {
164 if !self.buffer.trim_start().is_empty() {
@@ -389,7 +389,7 @@ mod tests {
389 TOOL_CALL_END, TOOL_CALL_START, ToolParser,
390 };
391 use crate::tool::test_utils::{collect_stream, split_by_chars, test_tools};
392 - use crate::tool::{Tool, ToolParserTestExt as _};
392 + use crate::tool::{Tool, ToolParserEvent, ToolParserTestExt as _};
393
394 fn element(name: &str, body: &str) -> String {
395 format!("{ELEMENT_START}{name}>{body}{ELEMENT_END_START}{name}>")
@@ -510,8 +510,8 @@ mod tests {
510 let mut parser = MinimaxM3ToolParser::new(&m3_test_tools());
511 let output = parser.parse_complete("Hello, world!").unwrap();
512
513 - assert_eq!(output.normal_text, "Hello, world!");
514 - assert!(output.calls.is_empty());
513 + assert_eq!(output.normal_text(), "Hello, world!");
514 + assert!(output.calls().is_empty());
515 }
516
517 #[test]
@@ -524,11 +524,11 @@ mod tests {
524 )]))
525 .unwrap();
526
527 - assert!(output.normal_text.is_empty());
528 - assert_eq!(output.calls.len(), 1);
529 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
527 + assert!(output.normal_text().is_empty());
528 + assert_eq!(output.calls().len(), 1);
529 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
530 assert_eq!(
531 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
531 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
532 json!({ "city": "Seattle", "days": 5 })
533 );
534 }
@@ -542,8 +542,8 @@ mod tests {
542 );
543 let output = parser.parse_complete(&output).unwrap();
544
545 - assert_eq!(output.normal_text, "Let me check. ");
546 - assert_eq!(output.calls.len(), 1);
545 + assert_eq!(output.normal_text(), "Let me check. ");
546 + assert_eq!(output.calls().len(), 1);
547 }
548
549 #[test]
@@ -556,15 +556,15 @@ mod tests {
556 ]))
557 .unwrap();
558
559 - assert_eq!(output.calls.len(), 2);
560 - assert_eq!(output.calls[0].tool_index, 0);
561 - assert_eq!(output.calls[1].tool_index, 1);
559 + assert_eq!(output.calls().len(), 2);
560 + assert_eq!(output.calls()[0].tool_index, 0);
561 + assert_eq!(output.calls()[1].tool_index, 1);
562 assert_eq!(
563 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
563 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
564 json!({ "city": "Seattle" })
565 );
566 assert_eq!(
567 - serde_json::from_str::<Value>(&output.calls[1].arguments).unwrap(),
567 + serde_json::from_str::<Value>(&output.calls()[1].arguments).unwrap(),
568 json!({ "city": "NYC" })
569 );
570 }
@@ -585,7 +585,7 @@ mod tests {
585 .unwrap();
586
587 assert_eq!(
588 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
588 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
589 json!({ "city": "Seattle" })
590 );
591 }
@@ -608,7 +608,7 @@ mod tests {
608 .unwrap();
609
610 assert_eq!(
611 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
611 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
612 json!({
613 "whole": 5.0,
614 "flag": true,
@@ -627,7 +627,7 @@ mod tests {
627 .unwrap();
628
629 assert_eq!(
630 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
630 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
631 json!({
632 "user_id": 42,
633 "urgent": true,
@@ -677,7 +677,7 @@ mod tests {
677 .unwrap();
678
679 assert_eq!(
680 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
680 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
681 json!({
682 "shape": "\nrectangle\n",
683 "dimensions": { "width": 10, "height": 20 },
@@ -698,11 +698,11 @@ mod tests {
698 ],
699 );
700
701 - assert!(output.normal_text.is_empty());
702 - assert_eq!(output.calls.len(), 1);
703 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
701 + assert!(output.normal_text().is_empty());
702 + assert_eq!(output.calls().len(), 1);
703 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
704 assert_eq!(
705 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
705 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
706 json!({ "city": "Seattle" })
707 );
708 }
@@ -720,8 +720,36 @@ mod tests {
720 ],
721 );
722
723 - assert_eq!(output.normal_text, "Let me check. ");
724 - assert_eq!(output.calls.len(), 1);
723 + assert_eq!(output.normal_text(), "Let me check. ");
724 + assert_eq!(output.calls().len(), 1);
725 + }
726 +
727 + #[test]
728 + fn minimax_m3_streaming_preserves_ordered_events() {
729 + let mut parser = MinimaxM3ToolParser::new(&m3_test_tools());
730 + let output = collect_stream(
731 + &mut parser,
732 + &[
733 + "Let me check. ",
734 + TOOL_CALL_START,
735 + &invoke("get_weather", &element("city", "Seattle")),
736 + TOOL_CALL_END,
737 + ],
738 + );
739 +
740 + assert_eq!(output.events.len(), 2);
741 + assert_eq!(
742 + output.events[0],
743 + ToolParserEvent::Text("Let me check. ".to_string())
744 + );
745 + let ToolParserEvent::ToolCall(call) = &output.events[1] else {
746 + panic!("expected tool-call event");
747 + };
748 + assert_eq!(call.name.as_deref(), Some("get_weather"));
749 + assert_eq!(
750 + serde_json::from_str::<Value>(&call.arguments).unwrap(),
751 + json!({ "city": "Seattle" })
752 + );
753 }
754
755 #[test]
@@ -729,8 +757,8 @@ mod tests {
757 let mut parser = MinimaxM3ToolParser::new(&m3_test_tools());
758 let output = collect_stream(&mut parser, &["Hello, ", "world!"]);
759
732 - assert_eq!(output.normal_text, "Hello, world!");
733 - assert!(output.calls.is_empty());
760 + assert_eq!(output.normal_text(), "Hello, world!");
761 + assert!(output.calls().is_empty());
762 }
763
764 #[test]
@@ -740,8 +768,8 @@ mod tests {
768 let mut parser = MinimaxM3ToolParser::new(&m3_test_tools());
769 let output = collect_stream(&mut parser, &chunks);
770
743 - assert_eq!(output.calls.len(), 1);
744 - assert!(output.normal_text.is_empty());
771 + assert_eq!(output.calls().len(), 1);
772 + assert!(output.normal_text().is_empty());
773 }
774
775 #[test]
@@ -754,9 +782,9 @@ mod tests {
782 let mut parser = MinimaxM3ToolParser::new(&m3_test_tools());
783 let output = collect_stream(&mut parser, &chunks);
784
757 - assert_eq!(output.calls.len(), 2);
758 - assert_eq!(output.calls[0].tool_index, 0);
759 - assert_eq!(output.calls[1].tool_index, 1);
785 + assert_eq!(output.calls().len(), 2);
786 + assert_eq!(output.calls()[0].tool_index, 0);
787 + assert_eq!(output.calls()[1].tool_index, 1);
788 }
789
790 #[test]
@@ -768,8 +796,8 @@ mod tests {
796 ))
797 .unwrap();
798
771 - assert!(output.normal_text.is_empty());
772 - assert!(output.calls.is_empty());
799 + assert!(output.normal_text().is_empty());
800 + assert!(output.calls().is_empty());
801 }
802
803 #[test]
@@ -782,8 +810,8 @@ mod tests {
810 let mut parser = MinimaxM3ToolParser::new(&m3_test_tools());
811 let output = collect_stream(&mut parser, &chunks);
812
785 - assert!(output.normal_text.is_empty());
786 - assert_eq!(output.calls.len(), 1);
813 + assert!(output.normal_text().is_empty());
814 + assert_eq!(output.calls().len(), 1);
815 }
816
817 #[test]
@@ -804,8 +832,8 @@ mod tests {
832 parser.parse_chunk(TOOL_CALL_START).unwrap();
833
834 let output = parser.finish().unwrap();
807 - assert!(output.normal_text.is_empty());
808 - assert!(output.calls.is_empty());
835 + assert!(output.normal_text().is_empty());
836 + assert!(output.calls().is_empty());
837 }
838
839 #[test]
@@ -819,9 +847,9 @@ mod tests {
847 ))
848 .unwrap();
849
822 - assert_eq!(output.calls.len(), 1);
850 + assert_eq!(output.calls().len(), 1);
851 assert_eq!(
824 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
852 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
853 json!({ "city": "Seattle" })
854 );
855 }
@@ -863,7 +891,7 @@ mod tests {
891 let output = parser.parse_complete(&build_tool_block(&[("convert", body)])).unwrap();
892
893 assert_eq!(
866 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
894 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
895 json!({
896 "payload": {
897 "child": "value",
@@ -887,7 +915,7 @@ mod tests {
915 let output = parser.parse_complete(&build_tool_block(&[("convert", body)])).unwrap();
916
917 assert_eq!(
890 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
918 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
919 json!({
920 "payload": {
921 "$text": "child text",
rust/src/parser/src/tool/mod.rs
+79 -19
@@ -57,55 +57,115 @@ pub struct ToolCallDelta {
57 pub arguments: String,
58 }
59
60 +/// One ordered event emitted while parsing assistant text.
61 +#[derive(Debug, Clone, PartialEq, Eq)]
62 +pub enum ToolParserEvent {
63 + /// Plain assistant text that is not part of any tool call.
64 + Text(String),
65 + /// A tool-call update extracted from assistant text.
66 + ToolCall(ToolCallDelta),
67 +}
68 +
69 /// Result of advancing tool parsing with one assistant-text input.
70 #[derive(Debug, Clone, Default, PartialEq, Eq)]
71 pub struct ToolParserOutput {
63 - /// Plain assistant text that is not part of any tool call.
64 - pub normal_text: String,
65 - /// Tool-call updates extracted from this input.
66 - pub calls: Vec<ToolCallDelta>,
72 + /// Ordered parser events committed by this input.
73 + pub events: Vec<ToolParserEvent>,
74 }
75
76 impl ToolParserOutput {
77 + /// Append one visible text event if `text` is non-empty.
78 + pub fn push_text(&mut self, text: impl AsRef<str> + Into<String>) {
79 + if text.as_ref().is_empty() {
80 + return;
81 + }
82 + if let Some(ToolParserEvent::Text(last_text)) = self.events.last_mut() {
83 + last_text.push_str(text.as_ref());
84 + return;
85 + }
86 + self.events.push(ToolParserEvent::Text(text.into()));
87 + }
88 +
89 + /// Append one tool-call update event.
90 + pub fn push_call(&mut self, call: ToolCallDelta) {
91 + self.events.push(ToolParserEvent::ToolCall(call));
92 + }
93 +
94 + /// Return all plain assistant text committed by this output.
95 + ///
96 + /// Texts before and after tool calls will be concatenated into a single string. To preserve
97 + /// the original order of the text and tool-call events, directly access `events` instead.
98 + pub fn normal_text(&self) -> String {
99 + self.events
100 + .iter()
101 + .filter_map(|event| match event {
102 + ToolParserEvent::Text(text) => Some(text.as_str()),
103 + ToolParserEvent::ToolCall(_) => None,
104 + })
105 + .collect()
106 + }
107 +
108 + /// Return all tool-call updates committed by this output.
109 + pub fn calls(&self) -> Vec<&ToolCallDelta> {
110 + self.events
111 + .iter()
112 + .filter_map(|event| match event {
113 + ToolParserEvent::Text(_) => None,
114 + ToolParserEvent::ToolCall(call) => Some(call),
115 + })
116 + .collect()
117 + }
118 +
119 /// Append another parser output onto this one.
120 ///
72 - /// Note that this does not attempt to merge multiple deltas for the same
73 - /// tool call into one complete item. Call `coalesce_calls()` after if
74 - /// that behavior is desired.
75 - pub fn append(&mut self, mut other: Self) {
76 - self.normal_text.push_str(&other.normal_text);
77 - self.calls.append(&mut other.calls);
121 + /// Note that this keeps events exactly as they arrive. Call `coalesce()`
122 + /// after if final text and tool-call fragments should be flattened.
123 + pub fn append(&mut self, other: Self) {
124 + for event in other.events {
125 + match event {
126 + ToolParserEvent::Text(text) => self.push_text(text),
127 + ToolParserEvent::ToolCall(call) => self.push_call(call),
128 + }
129 + }
130 }
131
80 - /// Merge multiple deltas for the same tool call into one complete item.
132 + /// Flatten text and merge deltas for the same tool call.
133 + ///
134 + /// All text events are concatenated into one leading text event. Tool-call
135 + /// events follow that text event in first-seen tool index order, with
136 + /// argument fragments for the same tool call concatenated together.
137 ///
138 /// This is primarily used by the default `parse_complete()` implementation,
139 /// which delegates through the incremental parser lifecycle and then
140 /// needs to collapse streaming-style argument fragments into one final
141 /// tool call.
86 - pub fn coalesce_calls(mut self) -> Self {
142 + pub fn coalesce(self) -> Self {
143 let mut merged = BTreeMap::<usize, ToolCallDelta>::new();
144 let mut order = Vec::new();
145 + let normal_text = self.normal_text();
146
90 - for call in self.calls {
147 + for call in self.calls() {
148 match merged.entry(call.tool_index) {
149 btree_map::Entry::Vacant(entry) => {
150 order.push(call.tool_index);
94 - entry.insert(call);
151 + entry.insert(call.clone());
152 }
153 btree_map::Entry::Occupied(mut entry) => {
154 let existing = entry.get_mut();
155 if existing.name.is_none() {
99 - existing.name = call.name;
156 + existing.name = call.name.clone();
157 }
158 existing.arguments.push_str(&call.arguments);
159 }
160 }
161 }
162
106 - self.calls =
107 - order.into_iter().filter_map(|tool_index| merged.remove(&tool_index)).collect();
108 - self
163 + let mut output = Self::default();
164 + output.push_text(normal_text);
165 + for call in order.into_iter().filter_map(|tool_index| merged.remove(&tool_index)) {
166 + output.push_call(call);
167 + }
168 + output
169 }
170 }
171
@@ -183,7 +243,7 @@ impl<T: ToolParser + ?Sized> T {
243 pub fn parse_complete(&mut self, text: &str) -> Result<ToolParserOutput> {
244 let mut output = self.parse_chunk(text)?;
245 output.append(self.finish()?);
186 - Ok(output.coalesce_calls())
246 + Ok(output.coalesce())
247 }
248 }
249
rust/src/parser/src/tool/qwen_coder.rs
+65 -65
@@ -74,7 +74,7 @@ impl Qwen3CoderToolParser {
74 fn apply_event(&mut self, event: QwenCoderEvent, output: &mut ToolParserOutput) -> Result<()> {
75 match event {
76 QwenCoderEvent::Text { len: consumed_len } => {
77 - output.normal_text.push_str(&self.buffer[..consumed_len]);
77 + output.push_text(&self.buffer[..consumed_len]);
78 }
79 QwenCoderEvent::ToolCallStart => {
80 self.mode = QwenCoderMode::ToolCall {
@@ -87,7 +87,7 @@ impl Qwen3CoderToolParser {
87 let arguments = serde_json::to_string(&arguments)
88 .map_err(|error| parsing_failed!("failed to serialize arguments: {}", error))?;
89
90 - output.calls.push(ToolCallDelta {
90 + output.push_call(ToolCallDelta {
91 tool_index: self.emitted_tool_count,
92 name: Some(name),
93 arguments,
@@ -138,7 +138,7 @@ impl ToolParser for Qwen3CoderToolParser {
138 {
139 return Err(parsing_failed!("incomplete Qwen Coder tool call"));
140 }
141 - output.normal_text.push_str(&self.buffer);
141 + output.push_text(&self.buffer);
142 }
143 let _ = self.reset();
144 Ok(output)
@@ -268,8 +268,8 @@ mod tests {
268 let mut parser = Qwen3CoderToolParser::new(&test_tools());
269 let output = parser.parse_complete("Hello, world!").unwrap();
270
271 - assert_eq!(output.normal_text, "Hello, world!");
272 - assert!(output.calls.is_empty());
271 + assert_eq!(output.normal_text(), "Hello, world!");
272 + assert!(output.calls().is_empty());
273 }
274
275 #[test]
@@ -282,11 +282,11 @@ mod tests {
282 ))
283 .unwrap();
284
285 - assert!(output.normal_text.is_empty());
286 - assert_eq!(output.calls.len(), 1);
287 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
285 + assert!(output.normal_text().is_empty());
286 + assert_eq!(output.calls().len(), 1);
287 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
288 assert_eq!(
289 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
289 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
290 json!({
291 "location": "SF",
292 "date": "2026-04-29"
@@ -303,8 +303,8 @@ mod tests {
303 );
304 let output = parser.parse_complete(&output).unwrap();
305
306 - assert_eq!(output.normal_text, "Thinking... ");
307 - assert_eq!(output.calls.len(), 1);
306 + assert_eq!(output.normal_text(), "Thinking... ");
307 + assert_eq!(output.calls().len(), 1);
308 }
309
310 #[test]
@@ -323,9 +323,9 @@ mod tests {
323 ))
324 .unwrap();
325
326 - assert_eq!(output.calls.len(), 1);
326 + assert_eq!(output.calls().len(), 1);
327 assert_eq!(
328 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
328 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
329 json!({
330 "whole": 5.0,
331 "flag": true,
@@ -341,10 +341,10 @@ mod tests {
341 let mut parser = Qwen3CoderToolParser::new(&test_tools());
342 let output = parser.parse_complete(&build_tool_call("get_weather", &[])).unwrap();
343
344 - assert_eq!(output.calls.len(), 1);
345 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
344 + assert_eq!(output.calls().len(), 1);
345 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
346 assert_eq!(
347 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
347 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
348 json!({})
349 );
350 }
@@ -371,10 +371,10 @@ mod tests {
371 )
372 .unwrap();
373
374 - assert_eq!(output.calls.len(), 1);
375 - assert_eq!(output.calls[0].name.as_deref(), Some("calculate_area"));
374 + assert_eq!(output.calls().len(), 1);
375 + assert_eq!(output.calls()[0].name.as_deref(), Some("calculate_area"));
376 assert_eq!(
377 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
377 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
378 json!({
379 "shape": "rectangle",
380 "dimensions": { "width": 10, "height": 20 },
@@ -396,9 +396,9 @@ mod tests {
396 ))
397 .unwrap();
398
399 - assert_eq!(output.calls.len(), 1);
399 + assert_eq!(output.calls().len(), 1);
400 assert_eq!(
401 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
401 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
402 json!({
403 "payload": {
404 "nested": {
@@ -426,9 +426,9 @@ mod tests {
426 ))
427 .unwrap();
428
429 - assert_eq!(output.calls.len(), 1);
429 + assert_eq!(output.calls().len(), 1);
430 assert_eq!(
431 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
431 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
432 json!({
433 "html_content": r#"<div class="test"><span>Hello</span></div>"#,
434 "xml_snippet": r#"<root><child attr="value"/></root>"#,
@@ -452,9 +452,9 @@ mod tests {
452 ))
453 .unwrap();
454
455 - assert_eq!(output.calls.len(), 1);
455 + assert_eq!(output.calls().len(), 1);
456 assert_eq!(
457 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
457 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
458 json!({
459 "location": "杭州 &lt;/parameter&gt;&lt;/function&gt;&lt;/tool_call&gt;",
460 "date": "2026-05-08",
@@ -472,9 +472,9 @@ mod tests {
472 ))
473 .unwrap();
474
475 - assert_eq!(output.calls.len(), 1);
475 + assert_eq!(output.calls().len(), 1);
476 assert_eq!(
477 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
477 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
478 json!({
479 "data": { "key": "value", "count": 42 },
480 })
@@ -495,11 +495,11 @@ mod tests {
495 ],
496 );
497
498 - assert!(output.normal_text.is_empty());
499 - assert_eq!(output.calls.len(), 1);
500 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
498 + assert!(output.normal_text().is_empty());
499 + assert_eq!(output.calls().len(), 1);
500 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
501 assert_eq!(
502 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
502 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
503 json!({ "location": "SF" })
504 );
505 }
@@ -519,8 +519,8 @@ mod tests {
519 ],
520 );
521
522 - assert_eq!(output.normal_text, "Thinking... ");
523 - assert_eq!(output.calls.len(), 1);
522 + assert_eq!(output.normal_text(), "Thinking... ");
523 + assert_eq!(output.calls().len(), 1);
524 }
525
526 #[test]
@@ -528,8 +528,8 @@ mod tests {
528 let mut parser = Qwen3CoderToolParser::new(&test_tools());
529 let output = collect_stream(&mut parser, &["Hello, ", "world!"]);
530
531 - assert_eq!(output.normal_text, "Hello, world!");
532 - assert!(output.calls.is_empty());
531 + assert_eq!(output.normal_text(), "Hello, world!");
532 + assert!(output.calls().is_empty());
533 }
534
535 #[test]
@@ -542,17 +542,17 @@ mod tests {
542 let mut parser = Qwen3CoderToolParser::new(&test_tools());
543 let output = collect_stream(&mut parser, &[&text]);
544
545 - assert_eq!(output.calls.len(), 2);
546 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
547 - assert_eq!(output.calls[1].name.as_deref(), Some("get_weather"));
548 - assert_eq!(output.calls[0].tool_index, 0);
549 - assert_eq!(output.calls[1].tool_index, 1);
545 + assert_eq!(output.calls().len(), 2);
546 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
547 + assert_eq!(output.calls()[1].name.as_deref(), Some("get_weather"));
548 + assert_eq!(output.calls()[0].tool_index, 0);
549 + assert_eq!(output.calls()[1].tool_index, 1);
550 assert_eq!(
551 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
551 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
552 json!({ "location": "SF" })
553 );
554 assert_eq!(
555 - serde_json::from_str::<Value>(&output.calls[1].arguments).unwrap(),
555 + serde_json::from_str::<Value>(&output.calls()[1].arguments).unwrap(),
556 json!({ "location": "NYC" })
557 );
558 }
@@ -569,16 +569,16 @@ mod tests {
569 let output = collect_stream(&mut parser, &chunks);
570
571 assert_eq!(
572 - output.normal_text,
572 + output.normal_text(),
573 "I'll check two cities.Between calls.Done."
574 );
575 - assert_eq!(output.calls.len(), 2);
575 + assert_eq!(output.calls().len(), 2);
576 assert_eq!(
577 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
577 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
578 json!({ "city": "Dallas", "state": "TX" })
579 );
580 assert_eq!(
581 - serde_json::from_str::<Value>(&output.calls[1].arguments).unwrap(),
581 + serde_json::from_str::<Value>(&output.calls()[1].arguments).unwrap(),
582 json!({ "city": "Orlando", "state": "FL" })
583 );
584 }
@@ -590,9 +590,9 @@ mod tests {
590 let mut parser = Qwen3CoderToolParser::new(&test_tools());
591 let output = collect_stream(&mut parser, &chunks);
592
593 - assert_eq!(output.calls.len(), 1);
593 + assert_eq!(output.calls().len(), 1);
594 assert_eq!(
595 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
595 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
596 json!({ "location": "SF" })
597 );
598 }
@@ -610,19 +610,19 @@ mod tests {
610 )
611 .unwrap();
612
613 - assert!(output.normal_text.is_empty());
614 - assert!(output.calls.is_empty());
613 + assert!(output.normal_text().is_empty());
614 + assert!(output.calls().is_empty());
615
616 let mut output = output;
617 output.append(parser.parse_chunk("_call>").unwrap());
618 output.append(parser.finish().unwrap());
619 - let output = output.coalesce_calls();
619 + let output = output.coalesce();
620
621 - assert!(output.normal_text.is_empty());
622 - assert_eq!(output.calls.len(), 1);
623 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
621 + assert!(output.normal_text().is_empty());
622 + assert_eq!(output.calls().len(), 1);
623 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
624 assert_eq!(
625 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
625 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
626 json!({ "location": "SF" })
627 );
628 }
@@ -641,20 +641,20 @@ mod tests {
641
642 for chunk in chunks {
643 let chunk_output = parser.parse_chunk(chunk).unwrap();
644 - assert!(chunk_output.normal_text.is_empty());
645 - assert!(chunk_output.calls.is_empty());
644 + assert!(chunk_output.normal_text().is_empty());
645 + assert!(chunk_output.calls().is_empty());
646 output.append(chunk_output);
647 }
648
649 output.append(parser.parse_chunk(end_suffix).unwrap());
650 output.append(parser.finish().unwrap());
651 - let output = output.coalesce_calls();
651 + let output = output.coalesce();
652
653 - assert!(output.normal_text.is_empty());
654 - assert_eq!(output.calls.len(), 1);
655 - assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
653 + assert!(output.normal_text().is_empty());
654 + assert_eq!(output.calls().len(), 1);
655 + assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
656 assert_eq!(
657 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
657 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
658 json!({ "location": long_location })
659 );
660 }
@@ -666,8 +666,8 @@ mod tests {
666 .parse_chunk("<tool_call>\n<function=get_weather>\n<parameter=location>SF</parameter>")
667 .unwrap();
668
669 - assert!(output.normal_text.is_empty());
670 - assert!(output.calls.is_empty());
669 + assert!(output.normal_text().is_empty());
670 + assert!(output.calls().is_empty());
671 }
672
673 #[test]
@@ -710,7 +710,7 @@ mod tests {
710 .unwrap();
711
712 assert_eq!(
713 - serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
713 + serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
714 json!({ "location": "Hangzhou" })
715 );
716 }
rust/src/parser/src/tool/test_utils.rs
+3 -3
@@ -1,7 +1,7 @@
1 use serde_json::json;
2
3 use super::{ToolParser, ToolParserOutput};
4 -use crate::tool::{Tool, ToolParserTestExt as _};
4 +use crate::tool::Tool;
5
6 /// Build a reusable set of function tools for parser unit tests.
7 pub fn test_tools() -> Vec<Tool> {
@@ -87,10 +87,10 @@ pub fn test_tools() -> Vec<Tool> {
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 {
90 - output.append(parser.parse_chunk(chunk).unwrap());
90 + parser.parse_into(chunk, &mut output).unwrap();
91 }
92 output.append(parser.finish().unwrap());
93 - output.coalesce_calls()
93 + output.coalesce()
94 }
95
96 /// Split text into chunks containing at most `chunk_chars` Unicode scalar
rust/src/parser/src/tool/tests.rs
+81 -21
@@ -1,4 +1,4 @@
1 -use super::{Result, Tool, ToolCallDelta, ToolParser, ToolParserOutput};
1 +use super::{Result, Tool, ToolCallDelta, ToolParser, ToolParserEvent, ToolParserOutput};
2 use crate::tool::ToolParserTestExt as _;
3
4 struct DefaultParser;
@@ -31,6 +31,66 @@ fn tool_parser_does_not_preserve_special_tokens_by_default() {
31 assert!(!parser.preserve_special_tokens());
32 }
33
34 +#[test]
35 +fn tool_parser_output_coalesces_adjacent_text_events() {
36 + let mut output = ToolParserOutput::default();
37 + output.push_text("hello");
38 + output.push_text(" ");
39 + output.push_text("world");
40 + output.push_call(ToolCallDelta {
41 + tool_index: 0,
42 + name: Some("lookup".to_string()),
43 + arguments: "{}".to_string(),
44 + });
45 + output.push_text("!");
46 +
47 + assert_eq!(
48 + output.events,
49 + vec![
50 + ToolParserEvent::Text("hello world".to_string()),
51 + ToolParserEvent::ToolCall(ToolCallDelta {
52 + tool_index: 0,
53 + name: Some("lookup".to_string()),
54 + arguments: "{}".to_string(),
55 + }),
56 + ToolParserEvent::Text("!".to_string()),
57 + ]
58 + );
59 +}
60 +
61 +#[test]
62 +fn tool_parser_output_append_coalesces_adjacent_text_events() {
63 + let mut output = ToolParserOutput::default();
64 + output.push_text("hello");
65 +
66 + let mut other = ToolParserOutput::default();
67 + other.push_text(" ");
68 + other.push_text("world");
69 + output.append(other);
70 +
71 + let mut after_call = ToolParserOutput::default();
72 + after_call.push_call(ToolCallDelta {
73 + tool_index: 0,
74 + name: Some("lookup".to_string()),
75 + arguments: "{}".to_string(),
76 + });
77 + after_call.push_text("!");
78 + output.append(after_call);
79 +
80 + assert_eq!(
81 + output.events,
82 + vec![
83 + ToolParserEvent::Text("hello world".to_string()),
84 + ToolParserEvent::ToolCall(ToolCallDelta {
85 + tool_index: 0,
86 + name: Some("lookup".to_string()),
87 + arguments: "{}".to_string(),
88 + }),
89 + ToolParserEvent::Text("!".to_string()),
90 + ]
91 + );
92 +}
93 +
94 #[test]
95 fn default_parse_complete_delegates_through_parse_chunk_and_finish() {
96 struct StreamingParser;
@@ -44,8 +104,8 @@ fn default_parse_complete_delegates_through_parse_chunk_and_finish() {
104 }
105
106 fn parse_into(&mut self, _chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
47 - output.normal_text.push_str("prefix ");
48 - output.calls.extend([
107 + output.push_text("prefix ");
108 + for call in [
109 ToolCallDelta {
110 tool_index: 0,
111 name: Some("weather".to_string()),
@@ -61,26 +121,26 @@ fn default_parse_complete_delegates_through_parse_chunk_and_finish() {
121 name: Some("time".to_string()),
122 arguments: "{\"timezone\":".to_string(),
123 },
64 - ]);
124 + ] {
125 + output.push_call(call);
126 + }
127 Ok(())
128 }
129
130 fn finish(&mut self) -> Result<ToolParserOutput> {
69 - Ok(ToolParserOutput {
70 - normal_text: "suffix".to_string(),
71 - calls: vec![
72 - ToolCallDelta {
73 - tool_index: 0,
74 - name: None,
75 - arguments: "}".to_string(),
76 - },
77 - ToolCallDelta {
78 - tool_index: 1,
79 - name: None,
80 - arguments: "\"UTC\"}".to_string(),
81 - },
82 - ],
83 - })
131 + let mut output = ToolParserOutput::default();
132 + output.push_text("suffix");
133 + output.push_call(ToolCallDelta {
134 + tool_index: 0,
135 + name: None,
136 + arguments: "}".to_string(),
137 + });
138 + output.push_call(ToolCallDelta {
139 + tool_index: 1,
140 + name: None,
141 + arguments: "\"UTC\"}".to_string(),
142 + });
143 + Ok(output)
144 }
145
146 fn reset(&mut self) -> String {
@@ -90,9 +150,9 @@ fn default_parse_complete_delegates_through_parse_chunk_and_finish() {
150
151 let mut parser = StreamingParser;
152 let output = parser.parse_complete("ignored").unwrap();
93 - assert_eq!(output.normal_text, "prefix suffix");
153 + assert_eq!(output.normal_text(), "prefix suffix");
154 assert_eq!(
95 - output.calls,
155 + output.calls().into_iter().cloned().collect::<Vec<_>>(),
156 vec![
157 ToolCallDelta {
158 tool_index: 0,
rust/src/parser/src/unified/combined.rs
+3 -3
@@ -32,7 +32,7 @@ impl CombinedParser {
32
33 fn parse_tool(&mut self, content: &str, output: &mut UnifiedParserOutput) -> Result<()> {
34 let Some(tool) = self.tool.as_mut() else {
35 - output.push_text(content.to_string());
35 + output.push_text(content);
36 return Ok(());
37 };
38
@@ -228,7 +228,7 @@ mod tests {
228 chunk: &str,
229 output: &mut crate::tool::ToolParserOutput,
230 ) -> crate::tool::Result<()> {
231 - output.normal_text.push_str(chunk);
231 + output.push_text(chunk);
232 Ok(())
233 }
234
@@ -256,7 +256,7 @@ mod tests {
256 _chunk: &str,
257 output: &mut crate::tool::ToolParserOutput,
258 ) -> crate::tool::Result<()> {
259 - output.normal_text.push_str("committed");
259 + output.push_text("committed");
260 Err(crate::tool::ToolParserError::ParsingFailed {
261 message: "synthetic failure".to_string(),
262 })
rust/src/parser/src/unified/mod.rs
+98 -12
@@ -8,7 +8,9 @@ use vllm_tokenizer::DynTokenizer;
8 pub use combined::CombinedParser;
9
10 use crate::reasoning::ReasoningError;
11 -use crate::tool::{StructuralTagModel, Tool, ToolCallDelta, ToolParserError, ToolParserOutput};
11 +use crate::tool::{
12 + StructuralTagModel, Tool, ToolCallDelta, ToolParserError, ToolParserEvent, ToolParserOutput,
13 +};
14
15 /// Result alias for unified parser operations.
16 pub type Result<T> = std::result::Result<T, UnifiedParserError>;
@@ -33,31 +35,115 @@ pub struct UnifiedParserOutput {
35
36 impl UnifiedParserOutput {
37 /// Append one visible text event if `delta` is non-empty.
36 - pub fn push_text(&mut self, delta: String) {
37 - if delta.is_empty() {
38 + pub fn push_text(&mut self, delta: impl AsRef<str> + Into<String>) {
39 + if delta.as_ref().is_empty() {
40 return;
41 }
40 - self.events.push(UnifiedParserEvent::Text(delta));
42 + if let Some(UnifiedParserEvent::Text(last_text)) = self.events.last_mut() {
43 + last_text.push_str(delta.as_ref());
44 + return;
45 + }
46 + self.events.push(UnifiedParserEvent::Text(delta.into()));
47 }
48
49 /// Append one reasoning text event if `delta` is non-empty.
44 - pub fn push_reasoning(&mut self, delta: String) {
45 - if delta.is_empty() {
50 + pub fn push_reasoning(&mut self, delta: impl AsRef<str> + Into<String>) {
51 + if delta.as_ref().is_empty() {
52 + return;
53 + }
54 + if let Some(UnifiedParserEvent::Reasoning(last_text)) = self.events.last_mut() {
55 + last_text.push_str(delta.as_ref());
56 return;
57 }
48 - self.events.push(UnifiedParserEvent::Reasoning(delta));
58 + self.events.push(UnifiedParserEvent::Reasoning(delta.into()));
59 + }
60 +
61 + /// Append one tool-call event.
62 + pub fn push_call(&mut self, call: ToolCallDelta) {
63 + self.events.push(UnifiedParserEvent::ToolCall(call));
64 }
65
66 /// Append parsed tool parser output as unified events.
67 pub fn append_tool_output(&mut self, output: ToolParserOutput) {
53 - // TODO: make ToolParserOutput carry ordered events and remove this text-first flattening.
54 - self.push_text(output.normal_text);
55 - self.events.extend(output.calls.into_iter().map(UnifiedParserEvent::ToolCall));
68 + for event in output.events {
69 + match event {
70 + ToolParserEvent::Text(text) => self.push_text(text),
71 + ToolParserEvent::ToolCall(call) => self.push_call(call),
72 + }
73 + }
74 }
75
76 /// Append another parser output onto this one.
59 - pub fn append(&mut self, mut other: Self) {
60 - self.events.append(&mut other.events);
77 + pub fn append(&mut self, other: Self) {
78 + for event in other.events {
79 + match event {
80 + UnifiedParserEvent::Text(text) => self.push_text(text),
81 + UnifiedParserEvent::Reasoning(reasoning) => self.push_reasoning(reasoning),
82 + UnifiedParserEvent::ToolCall(call) => self.push_call(call),
83 + }
84 + }
85 + }
86 +}
87 +
88 +#[cfg(test)]
89 +mod tests {
90 + use super::{UnifiedParserEvent, UnifiedParserOutput};
91 + use crate::tool::ToolCallDelta;
92 +
93 + #[test]
94 + fn unified_parser_output_coalesces_adjacent_text_events() {
95 + let mut output = UnifiedParserOutput::default();
96 + output.push_text("hello");
97 + output.push_text(" ");
98 + output.push_text("world");
99 + output.push_reasoning("think");
100 + output.push_reasoning("ing");
101 + output.push_call(ToolCallDelta {
102 + tool_index: 0,
103 + name: Some("lookup".to_string()),
104 + arguments: "{}".to_string(),
105 + });
106 + output.push_text("!");
107 +
108 + assert_eq!(
109 + output.events,
110 + vec![
111 + UnifiedParserEvent::Text("hello world".to_string()),
112 + UnifiedParserEvent::Reasoning("thinking".to_string()),
113 + UnifiedParserEvent::ToolCall(ToolCallDelta {
114 + tool_index: 0,
115 + name: Some("lookup".to_string()),
116 + arguments: "{}".to_string(),
117 + }),
118 + UnifiedParserEvent::Text("!".to_string()),
119 + ]
120 + );
121 + }
122 +
123 + #[test]
124 + fn unified_parser_output_append_coalesces_adjacent_events() {
125 + let mut output = UnifiedParserOutput::default();
126 + output.push_text("hello");
127 +
128 + let mut other = UnifiedParserOutput::default();
129 + other.push_text(" ");
130 + other.push_text("world");
131 + other.push_reasoning("think");
132 + output.append(other);
133 +
134 + let mut after_reasoning = UnifiedParserOutput::default();
135 + after_reasoning.push_reasoning("ing");
136 + after_reasoning.push_text("!");
137 + output.append(after_reasoning);
138 +
139 + assert_eq!(
140 + output.events,
141 + vec![
142 + UnifiedParserEvent::Text("hello world".to_string()),
143 + UnifiedParserEvent::Reasoning("thinking".to_string()),
144 + UnifiedParserEvent::Text("!".to_string()),
145 + ]
146 + );
147 }
148 }
149
tests/tool_parsers/test_rust_tool_parser.py
+1 -1
@@ -171,7 +171,7 @@ def test_rust_tool_parser_extension_typed_api() -> None:
171
172 parser.parse_into(build_tool_call(), output)
173 output.append(parser.finish())
174 - output = output.coalesce_calls()
174 + output = output.coalesce()
175
176 assert parser.preserve_special_tokens()
177 assert output.normal_text == ""
vllm/tool_parsers/rust_tool_parser.py
+1 -1
@@ -224,7 +224,7 @@ class RustToolParser(ToolParser):
224 "Error parsing %s tool call output.", self.rust_parser_name
225 )
226 return None
227 - return output.coalesce_calls(), tool_call_ids
227 + return output.coalesce(), tool_call_ids
228
229 def extract_tool_calls(
230 self,