[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 </|DSML|parameter></|DSML|invoke></|DSML|function_calls>",
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 </arg_value></tool_call>",
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 & Jerry <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 & Jerry <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 </parameter></invoke></minimax:tool_call>",
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": "杭州 </parameter></function></tool_call>",
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,