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

Signed-off-by: Bugen Zhao <i@bugenzhao.com>
This commit is contained in:
Bugen Zhao
2026-06-25 06:27:07 +00:00
committed by GitHub
parent 77c1d9fe9b
commit 4d3b4b9b01
32 changed files with 1155 additions and 820 deletions
@@ -425,6 +425,18 @@ mod tests {
}
}
fn tool_call_arguments(arguments: &str) -> UnifiedParserOutput {
UnifiedParserOutput {
events: vec![vllm_parser::unified::UnifiedParserEvent::ToolCall(
ToolCallDelta {
tool_index: 0,
name: None,
arguments: arguments.to_string(),
},
)],
}
}
fn combined(first: UnifiedParserOutput, second: UnifiedParserOutput) -> UnifiedParserOutput {
let mut output = first;
output.append(second);
@@ -531,6 +543,38 @@ mod tests {
);
}
#[tokio::test]
async fn unified_stream_emits_tool_arguments_before_trailing_text() {
let events = collect(
ScriptedParser::new([
ScriptedStep::Output(tool_call("get_weather", "")),
ScriptedStep::Output(combined(
tool_call_arguments(r#"{"location":"Paris"}"#),
text(" done"),
)),
]),
vec![decoded_delta("start"), decoded_delta("finish")],
)
.await;
assert_eq!(
events,
vec![
AssistantEvent::ToolCallStart {
id: "call_test".to_string(),
name: "get_weather".to_string(),
},
AssistantEvent::ToolCallArgumentsDelta {
delta: r#"{"location":"Paris"}"#.to_string(),
},
AssistantEvent::TextDelta {
kind: AssistantBlockKind::Text,
delta: " done".to_string(),
},
]
);
}
#[tokio::test]
async fn unified_stream_fallback_keeps_committed_output_and_disables_later_parsing() {
let events = collect(
+1 -1
View File
@@ -23,7 +23,7 @@ pub(super) fn openai_tools(tools: &[Tool]) -> Vec<OpenAiTool> {
pub(super) fn feed_parser(parser: &mut dyn ToolParser, chunks: &[&str]) -> (String, usize) {
let result = collect_stream(parser, chunks);
(result.normal_text, result.calls.len())
(result.normal_text(), result.calls().len())
}
pub(super) fn feed_external_parser(
+14 -14
View File
@@ -146,30 +146,30 @@ impl PyToolParserOutput {
#[new]
#[pyo3(signature = (normal_text="", calls=None))]
fn new(py: Python<'_>, normal_text: &str, calls: Option<Vec<Py<PyToolCallDelta>>>) -> Self {
let calls =
calls.unwrap_or_default().iter().map(|call| call.borrow(py).0.clone()).collect();
Self(ToolParserOutput {
normal_text: normal_text.to_owned(),
calls,
})
let mut output = ToolParserOutput::default();
output.push_text(normal_text);
for call in calls.unwrap_or_default() {
output.push_call(call.borrow(py).0.clone());
}
Self(output)
}
#[getter]
fn normal_text(&self) -> &str {
&self.0.normal_text
fn normal_text(&self) -> String {
self.0.normal_text()
}
#[getter]
fn calls(&self) -> Vec<PyToolCallDelta> {
self.0.calls.iter().cloned().map(PyToolCallDelta).collect()
self.0.calls().into_iter().cloned().map(PyToolCallDelta).collect()
}
fn append(&mut self, other: PyRef<'_, PyToolParserOutput>) {
self.0.append(other.0.clone());
}
fn coalesce_calls(&self) -> Self {
Self(self.0.clone().coalesce_calls())
fn coalesce(&self) -> Self {
Self(self.0.clone().coalesce())
}
}
@@ -300,7 +300,7 @@ mod tests {
}
#[test]
fn output_append_and_coalesce_calls() {
fn output_append_and_coalesce() {
with_python(|py| {
let first = Py::new(
py,
@@ -311,7 +311,7 @@ mod tests {
let other = Py::new(py, PyToolParserOutput::new(py, "", Some(vec![second])))?;
output.append(other.borrow(py));
let coalesced = output.coalesce_calls();
let coalesced = output.coalesce();
assert_eq!(coalesced.normal_text(), "text");
let calls = coalesced.calls();
assert_eq!(calls.len(), 1);
@@ -334,7 +334,7 @@ mod tests {
parser.parse_into_output(&build_call(), &mut output)?;
let finish = Py::new(py, parser.finish()?)?;
output.append(finish.borrow(py));
let output = output.coalesce_calls();
let output = output.coalesce();
assert_eq!(output.normal_text(), "");
let calls = output.calls();
@@ -90,8 +90,8 @@ mod tests {
let mut parser = DeepSeekV32ToolParser::new(&test_tools());
let output = parser.parse_complete("Hello, world!").unwrap();
assert_eq!(output.normal_text, "Hello, world!");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "Hello, world!");
assert!(output.calls().is_empty());
}
#[test]
@@ -104,11 +104,11 @@ mod tests {
))
.unwrap();
assert!(output.normal_text.is_empty());
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert!(output.normal_text().is_empty());
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({
"location": "SF",
"date": "2024-01-16"
@@ -125,8 +125,8 @@ mod tests {
);
let output = parser.parse_complete(&output).unwrap();
assert_eq!(output.normal_text, "Thinking... ");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.normal_text(), "Thinking... ");
assert_eq!(output.calls().len(), 1);
}
#[test]
@@ -146,9 +146,9 @@ mod tests {
)
.unwrap();
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls().len(), 1);
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({
"whole": 5.0,
"flag": true,
@@ -176,9 +176,9 @@ mod tests {
)
.unwrap();
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls().len(), 1);
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({
"whole": "5.0",
"flag": "true",
@@ -206,7 +206,7 @@ mod tests {
.unwrap();
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({
"location": "Hangzhou &lt;/|DSML|parameter&gt;&lt;/|DSML|invoke&gt;&lt;/|DSML|function_calls&gt;",
"date": "2026-05-08",
@@ -228,11 +228,11 @@ mod tests {
],
);
assert!(output.normal_text.is_empty());
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert!(output.normal_text().is_empty());
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({ "location": "SF" })
);
}
@@ -252,8 +252,8 @@ mod tests {
],
);
assert_eq!(output.normal_text, "Thinking... ");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.normal_text(), "Thinking... ");
assert_eq!(output.calls().len(), 1);
}
#[test]
@@ -261,8 +261,8 @@ mod tests {
let mut parser = DeepSeekV32ToolParser::new(&test_tools());
let output = collect_stream(&mut parser, &["Hello, ", "world!"]);
assert_eq!(output.normal_text, "Hello, world!");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "Hello, world!");
assert!(output.calls().is_empty());
}
#[test]
@@ -278,17 +278,17 @@ mod tests {
)],
);
assert_eq!(output.calls.len(), 2);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls[1].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls[0].tool_index, 0);
assert_eq!(output.calls[1].tool_index, 1);
assert_eq!(output.calls().len(), 2);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls()[1].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls()[0].tool_index, 0);
assert_eq!(output.calls()[1].tool_index, 1);
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({ "location": "SF" })
);
assert_eq!(
serde_json::from_str::<Value>(&output.calls[1].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[1].arguments).unwrap(),
json!({ "location": "NYC" })
);
}
@@ -300,9 +300,9 @@ mod tests {
let mut parser = DeepSeekV32ToolParser::new(&test_tools());
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls().len(), 1);
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({ "location": "SF" })
);
}
@@ -337,11 +337,11 @@ mod tests {
],
);
assert!(output.normal_text.is_empty());
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert!(output.normal_text().is_empty());
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({ "location": "Beijing" })
);
}
@@ -373,9 +373,9 @@ mod tests {
],
);
assert!(output.normal_text.is_empty());
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert!(output.normal_text().is_empty());
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
}
#[test]
@@ -393,8 +393,8 @@ mod tests {
],
);
assert!(output.normal_text.is_empty());
assert_eq!(output.calls.len(), 1);
assert!(output.normal_text().is_empty());
assert_eq!(output.calls().len(), 1);
}
#[test]
@@ -421,10 +421,10 @@ mod tests {
.parse_complete(&build_tool_call("get_weather", &[("location", "NYC")]))
.unwrap();
assert_eq!(first.calls.len(), 1);
assert_eq!(second.calls.len(), 1);
assert_eq!(first.calls().len(), 1);
assert_eq!(second.calls().len(), 1);
assert_eq!(
serde_json::from_str::<Value>(&second.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&second.calls()[0].arguments).unwrap(),
json!({ "location": "NYC" })
);
}
@@ -439,7 +439,7 @@ mod tests {
let mut parser = DeepSeekV32ToolParser::new(&test_tools());
let complete = parser.parse_complete(&full_text).unwrap();
assert_eq!(streamed.normal_text, complete.normal_text);
assert_eq!(streamed.calls, complete.calls);
assert_eq!(streamed.normal_text(), complete.normal_text());
assert_eq!(streamed.calls(), complete.calls());
}
}
@@ -107,11 +107,11 @@ mod tests {
))
.unwrap();
assert!(output.normal_text.is_empty());
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert!(output.normal_text().is_empty());
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({
"location": "SF",
"date": "2024-01-16"
@@ -137,11 +137,11 @@ mod tests {
],
);
assert_eq!(output.normal_text, "Thinking... ");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.normal_text(), "Thinking... ");
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({ "location": "Beijing" })
);
}
@@ -92,7 +92,7 @@ impl DeepSeekDsmlToolParser {
fn apply_event(&mut self, event: DsmlEvent, output: &mut ToolParserOutput) -> Result<()> {
match event {
DsmlEvent::Text { len: consumed_len } => {
output.normal_text.push_str(&self.buffer[..consumed_len]);
output.push_text(&self.buffer[..consumed_len]);
}
DsmlEvent::ToolCallsStart => {
self.mode = DsmlMode::ToolBlock {
@@ -116,7 +116,7 @@ impl DeepSeekDsmlToolParser {
let arguments = serde_json::to_string(&arguments)
.map_err(|error| parsing_failed!("failed to serialize arguments: {}", error))?;
output.calls.push(ToolCallDelta {
output.push_call(ToolCallDelta {
tool_index: self.emitted_invoke_count,
name: Some(name),
arguments,
@@ -156,7 +156,7 @@ impl DeepSeekDsmlToolParser {
fn finish(&mut self) -> Result<ToolParserOutput> {
let mut output = ToolParserOutput::default();
match self.mode {
DsmlMode::Text => output.normal_text.push_str(&self.buffer),
DsmlMode::Text => output.push_text(&self.buffer),
DsmlMode::Done => {}
DsmlMode::ToolBlock { .. } => {
return Err(parsing_failed!("incomplete DeepSeek DSML tool call"));
@@ -77,8 +77,8 @@ mod tests {
let mut parser = DeepSeekV3ToolParser::new(&test_tools());
let output = parser.parse_complete("Hello, world!").unwrap();
assert_eq!(output.normal_text, "Hello, world!");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "Hello, world!");
assert!(output.calls().is_empty());
}
#[test]
@@ -92,11 +92,11 @@ mod tests {
))
.unwrap();
assert_eq!(output.normal_text, "Let me check.\n");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].tool_index, 0);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls[0].arguments, arguments);
assert_eq!(output.normal_text(), "Let me check.\n");
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].tool_index, 0);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls()[0].arguments, arguments);
}
#[test]
@@ -107,7 +107,7 @@ mod tests {
.parse_complete(&tool_section(&[v3_tool_call("get_weather", arguments)]))
.unwrap();
assert_eq!(output.calls[0].arguments, arguments);
assert_eq!(output.calls()[0].arguments, arguments);
}
#[test]
@@ -132,7 +132,7 @@ mod tests {
for chunk in chunks {
let next = parser.parse_chunk(chunk).unwrap();
observed_arguments.extend(
next.calls
next.calls()
.iter()
.filter(|call| call.name.is_none())
.map(|call| call.arguments.clone()),
@@ -143,7 +143,7 @@ mod tests {
assert_eq!(observed_arguments, ["{\"location\":", "\"Beijing\"", "}"]);
assert_eq!(
output.coalesce_calls().calls[0].arguments,
output.coalesce().calls()[0].arguments,
r#"{"location":"Beijing"}"#
);
}
@@ -159,9 +159,9 @@ mod tests {
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.normal_text, "hello ");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].arguments, r#"{"location":"Tokyo"}"#);
assert_eq!(output.normal_text(), "hello ");
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].arguments, r#"{"location":"Tokyo"}"#);
}
#[test]
@@ -172,8 +172,8 @@ mod tests {
let output = parser.parse_complete(&input).unwrap();
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].arguments, arguments);
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].arguments, arguments);
}
#[test]
@@ -189,22 +189,25 @@ mod tests {
expect![[r#"
ToolParserOutput {
normal_text: "",
calls: [
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"location\":\"Shanghai\"}",
},
ToolCallDelta {
tool_index: 1,
name: Some(
"add",
),
arguments: "{\"x\":1,\"y\":2}",
},
events: [
ToolCall(
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"location\":\"Shanghai\"}",
},
),
ToolCall(
ToolCallDelta {
tool_index: 1,
name: Some(
"add",
),
arguments: "{\"x\":1,\"y\":2}",
},
),
],
}
"#]]
@@ -70,8 +70,8 @@ mod tests {
let mut parser = DeepSeekV31ToolParser::new(&test_tools());
let output = parser.parse_complete("Hello, world!").unwrap();
assert_eq!(output.normal_text, "Hello, world!");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "Hello, world!");
assert!(output.calls().is_empty());
}
#[test]
@@ -85,11 +85,11 @@ mod tests {
))
.unwrap();
assert_eq!(output.normal_text, "Let me check.");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].tool_index, 0);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls[0].arguments, arguments);
assert_eq!(output.normal_text(), "Let me check.");
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].tool_index, 0);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls()[0].arguments, arguments);
}
#[test]
@@ -100,7 +100,7 @@ mod tests {
.parse_complete(&tool_section(&[v31_tool_call("get_weather", arguments)]))
.unwrap();
assert_eq!(output.calls[0].arguments, arguments);
assert_eq!(output.calls()[0].arguments, arguments);
}
#[test]
@@ -123,7 +123,7 @@ mod tests {
for chunk in chunks {
let next = parser.parse_chunk(chunk).unwrap();
observed_arguments.extend(
next.calls
next.calls()
.iter()
.filter(|call| call.name.is_none())
.map(|call| call.arguments.clone()),
@@ -134,7 +134,7 @@ mod tests {
assert_eq!(observed_arguments, ["{\"location\":", "\"Beijing\"", "}"]);
assert_eq!(
output.coalesce_calls().calls[0].arguments,
output.coalesce().calls()[0].arguments,
r#"{"location":"Beijing"}"#
);
}
@@ -150,9 +150,9 @@ mod tests {
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.normal_text, "hello ");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].arguments, r#"{"location":"Tokyo"}"#);
assert_eq!(output.normal_text(), "hello ");
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].arguments, r#"{"location":"Tokyo"}"#);
}
#[test]
@@ -163,8 +163,8 @@ mod tests {
let output = parser.parse_complete(&input).unwrap();
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].arguments, arguments);
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].arguments, arguments);
}
#[test]
@@ -180,22 +180,25 @@ mod tests {
expect![[r#"
ToolParserOutput {
normal_text: "",
calls: [
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"location\":\"Shanghai\"}",
},
ToolCallDelta {
tool_index: 1,
name: Some(
"add",
),
arguments: "{\"x\":1,\"y\":2}",
},
events: [
ToolCall(
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"location\":\"Shanghai\"}",
},
),
ToolCall(
ToolCallDelta {
tool_index: 1,
name: Some(
"add",
),
arguments: "{\"x\":1,\"y\":2}",
},
),
],
}
"#]]
@@ -212,9 +215,9 @@ mod tests {
let output = collect_stream(&mut parser, &[&input]);
assert!(output.normal_text.is_empty());
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].arguments, r#"{"location":"Tokyo"}"#);
assert!(output.normal_text().is_empty());
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].arguments, r#"{"location":"Tokyo"}"#);
}
#[test]
@@ -96,7 +96,7 @@ impl DeepSeekJsonToolParser {
) -> Result<()> {
match event {
DeepSeekJsonEvent::Text { len: consumed_len } => {
output.normal_text.push_str(&self.buffer[..consumed_len]);
output.push_text(&self.buffer[..consumed_len]);
}
DeepSeekJsonEvent::ToolCallsStart => self.mode = DeepSeekJsonMode::ToolBlock,
DeepSeekJsonEvent::ToolCallStart => self.mode = DeepSeekJsonMode::Header,
@@ -107,7 +107,7 @@ impl DeepSeekJsonToolParser {
self.mode = DeepSeekJsonMode::Arguments {
json_scan: JsonObjectScanState::default(),
};
output.calls.push(ToolCallDelta {
output.push_call(ToolCallDelta {
tool_index,
name: Some(function_name),
arguments: String::new(),
@@ -120,7 +120,7 @@ impl DeepSeekJsonToolParser {
self.format.parser_name()
));
};
output.calls.push(ToolCallDelta {
output.push_call(ToolCallDelta {
tool_index,
name: None,
arguments: self.buffer[..consumed_len].to_string(),
@@ -155,7 +155,7 @@ impl DeepSeekJsonToolParser {
fn finish(&mut self) -> Result<ToolParserOutput> {
let mut output = ToolParserOutput::default();
match &self.mode {
DeepSeekJsonMode::Text => output.normal_text.push_str(&self.buffer),
DeepSeekJsonMode::Text => output.push_text(&self.buffer),
DeepSeekJsonMode::ToolBlock | DeepSeekJsonMode::Done => {}
DeepSeekJsonMode::Header | DeepSeekJsonMode::Arguments { .. } => {
return Err(parsing_failed!(
+14 -14
View File
@@ -70,7 +70,7 @@ impl Gemma4ToolParser {
fn apply_event(&mut self, event: Gemma4Event, output: &mut ToolParserOutput) -> Result<()> {
match event {
Gemma4Event::Text { len: consumed_len } => {
output.normal_text.push_str(&self.buffer[..consumed_len]);
output.push_text(&self.buffer[..consumed_len]);
}
Gemma4Event::ToolCallStart => self.mode = Gemma4Mode::Header,
Gemma4Event::ToolCallHeader { name } => {
@@ -89,7 +89,7 @@ impl Gemma4ToolParser {
let arguments = serde_json::to_string(&args)
.map_err(|error| parsing_failed!("failed to serialize arguments: {}", error))?;
output.calls.push(ToolCallDelta {
output.push_call(ToolCallDelta {
tool_index: self.emitted_tool_count,
name: Some(name),
arguments,
@@ -153,7 +153,7 @@ impl ToolParser for Gemma4ToolParser {
let mut output = ToolParserOutput::default();
match &self.mode {
Gemma4Mode::Text => output.normal_text.push_str(&self.buffer),
Gemma4Mode::Text => output.push_text(&self.buffer),
Gemma4Mode::Header | Gemma4Mode::ToolCall { .. } => {
return Err(parsing_failed!("incomplete Gemma4 tool call"));
}
@@ -501,11 +501,11 @@ mod tests {
output.append(parser.parse_chunk(chunk).unwrap());
}
output.append(parser.finish().unwrap());
output.coalesce_calls()
output.coalesce()
}
fn first_call(output: &ToolParserOutput) -> &ToolCallDelta {
output.calls.first().expect("expected one tool call")
fn first_call(output: &ToolParserOutput) -> ToolCallDelta {
(*output.calls().first().expect("expected one tool call")).clone()
}
#[test]
@@ -547,8 +547,8 @@ mod tests {
.parse_complete("<|tool_call>call:get_weather{location:<|\"|>London<|\"|>}<tool_call|>")
.unwrap();
assert!(output.normal_text.is_empty());
assert_eq!(output.calls.len(), 1);
assert!(output.normal_text().is_empty());
assert_eq!(output.calls().len(), 1);
assert_eq!(first_call(&output).name.as_deref(), Some("get_weather"));
assert_eq!(
serde_json::from_str::<Value>(&first_call(&output).arguments).unwrap(),
@@ -577,7 +577,7 @@ mod tests {
"<tool_call|>",
]);
assert!(output.normal_text.is_empty());
assert!(output.normal_text().is_empty());
assert_eq!(first_call(&output).name.as_deref(), Some("get_weather"));
assert_eq!(
serde_json::from_str::<Value>(&first_call(&output).arguments).unwrap(),
@@ -597,7 +597,7 @@ mod tests {
"div>",
]);
assert_eq!(output.normal_text, "Let me check the weather. <div>");
assert_eq!(output.normal_text(), "Let me check the weather. <div>");
assert_eq!(first_call(&output).name.as_deref(), Some("get_weather"));
assert_eq!(
serde_json::from_str::<Value>(&first_call(&output).arguments).unwrap(),
@@ -616,11 +616,11 @@ mod tests {
"location:<|\"|>Paris<|\"|>}",
] {
output.append(parser.parse_chunk(chunk).unwrap());
assert!(output.calls.is_empty());
assert!(output.calls().is_empty());
}
output.append(parser.parse_chunk("<tool_call|>").unwrap());
let output = output.coalesce_calls();
let output = output.coalesce();
assert_eq!(first_call(&output).name.as_deref(), Some("get_weather"));
assert_eq!(
@@ -777,8 +777,8 @@ mod tests {
let mut output = parser.parse_chunk("<").unwrap();
output.append(parser.finish().unwrap());
assert_eq!(output.normal_text, "<");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "<");
assert!(output.calls().is_empty());
}
#[test]
+13 -13
View File
@@ -69,11 +69,11 @@ mod tests {
let output = parser.parse_complete(&output).unwrap();
assert_eq!(output.normal_text, "Let me search for that.\n");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.normal_text(), "Let me search for that.\n");
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({"city": "Beijing", "date": "2024-12-25"})
);
}
@@ -90,12 +90,12 @@ mod tests {
let chunks = split_by_chars(&output, 7);
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.normal_text, "");
assert_eq!(output.calls.len(), 2);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls[1].name.as_deref(), Some("add"));
assert_eq!(output.normal_text(), "");
assert_eq!(output.calls().len(), 2);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls()[1].name.as_deref(), Some("add"));
assert_eq!(
serde_json::from_str::<Value>(&output.calls[1].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[1].arguments).unwrap(),
json!({"x": 1, "y": 2})
);
}
@@ -117,7 +117,7 @@ mod tests {
.unwrap();
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({
"whole": 42,
"flag": true,
@@ -134,10 +134,10 @@ mod tests {
let output = parser.parse_complete("<tool_call>add</tool_call>").unwrap();
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("add"));
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("add"));
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({})
);
}
+26 -26
View File
@@ -79,7 +79,7 @@ impl GlmXmlToolParser {
fn apply_event(&mut self, event: GlmEvent, output: &mut ToolParserOutput) -> Result<()> {
match event {
GlmEvent::Text { len: consumed_len } => {
output.normal_text.push_str(&self.buffer[..consumed_len]);
output.push_text(&self.buffer[..consumed_len]);
}
GlmEvent::ToolCallStart => {
self.mode = GlmMode::ToolCall {
@@ -92,7 +92,7 @@ impl GlmXmlToolParser {
let arguments = serde_json::to_string(&arguments)
.map_err(|error| parsing_failed!("failed to serialize arguments: {}", error))?;
output.calls.push(ToolCallDelta {
output.push_call(ToolCallDelta {
tool_index: self.emitted_tool_count,
name: Some(name),
arguments,
@@ -127,7 +127,7 @@ impl GlmXmlToolParser {
let mut output = ToolParserOutput::default();
if !self.buffer.is_empty() {
match self.mode {
GlmMode::Text => output.normal_text.push_str(&self.buffer),
GlmMode::Text => output.push_text(&self.buffer),
GlmMode::ToolCall { .. } => {
return Err(parsing_failed!("incomplete GLM MoE tool call"));
}
@@ -283,8 +283,8 @@ mod tests {
let mut parser = Glm45MoeToolParser::new(&test_tools());
let output = parser.parse_complete("Hello, world!").unwrap();
assert_eq!(output.normal_text, "Hello, world!");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "Hello, world!");
assert!(output.calls().is_empty());
}
#[test]
@@ -300,11 +300,11 @@ mod tests {
let output = parser.parse_complete(&output).unwrap();
assert_eq!(output.normal_text, "Let me search for that.\n");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.normal_text(), "Let me search for that.\n");
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({"city": "Beijing", "date": "2024-12-25"})
);
}
@@ -321,12 +321,12 @@ mod tests {
let chunks = split_by_chars(&output, 11);
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.normal_text, "");
assert_eq!(output.calls.len(), 2);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls[1].name.as_deref(), Some("add"));
assert_eq!(output.normal_text(), "");
assert_eq!(output.calls().len(), 2);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls()[1].name.as_deref(), Some("add"));
assert_eq!(
serde_json::from_str::<Value>(&output.calls[1].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[1].arguments).unwrap(),
json!({"x": 1, "y": 2})
);
}
@@ -345,7 +345,7 @@ mod tests {
.unwrap();
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({
"city": "Paris &lt;/arg_value&gt;&lt;/tool_call&gt;",
"date": "2026-05-08",
@@ -359,8 +359,8 @@ mod tests {
let output = collect_stream(&mut parser, &["hello ", "world"]);
assert_eq!(output.normal_text, "hello world");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "hello world");
assert!(output.calls().is_empty());
}
#[test]
@@ -375,8 +375,8 @@ mod tests {
],
);
assert_eq!(output.normal_text, "Prefix ");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.normal_text(), "Prefix ");
assert_eq!(output.calls().len(), 1);
}
#[test]
@@ -391,9 +391,9 @@ mod tests {
],
);
assert_eq!(output.normal_text, "hello ");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.normal_text(), "hello ");
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
}
#[test]
@@ -402,8 +402,8 @@ mod tests {
let output = parser.parse_chunk("<tool_call>get_weather\n<arg_key>city</arg_key>").unwrap();
assert_eq!(output.normal_text, "");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "");
assert!(output.calls().is_empty());
}
#[test]
@@ -437,7 +437,7 @@ mod tests {
)],
);
assert_eq!(output.normal_text, "");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.normal_text(), "");
assert_eq!(output.calls().len(), 1);
}
}
+44 -41
View File
@@ -79,7 +79,7 @@ impl HyV3ToolParser {
fn apply_event(&mut self, event: HyV3Event, output: &mut ToolParserOutput) -> Result<()> {
match event {
HyV3Event::Text { len: consumed_len } => {
output.normal_text.push_str(&self.buffer[..consumed_len]);
output.push_text(&self.buffer[..consumed_len]);
}
HyV3Event::ToolBlockStart => {
self.mode = HyV3Mode::ToolBlock {
@@ -91,7 +91,7 @@ impl HyV3ToolParser {
let arguments = serde_json::to_string(&arguments)
.map_err(|error| parsing_failed!("failed to serialize arguments: {}", error))?;
output.calls.push(ToolCallDelta {
output.push_call(ToolCallDelta {
tool_index: self.emitted_tool_count,
name: Some(name),
arguments,
@@ -133,7 +133,7 @@ impl ToolParser for HyV3ToolParser {
fn finish(&mut self) -> Result<ToolParserOutput> {
let mut output = ToolParserOutput::default();
match self.mode {
HyV3Mode::Text => output.normal_text.push_str(&self.buffer),
HyV3Mode::Text => output.push_text(&self.buffer),
HyV3Mode::ToolBlock { .. } => return Err(parsing_failed!("incomplete HY3 tool call")),
HyV3Mode::Done => {}
}
@@ -266,7 +266,7 @@ mod tests {
}
fn parsed_arguments(output: &ToolParserOutput, index: usize) -> Value {
serde_json::from_str(&output.calls[index].arguments).unwrap()
serde_json::from_str(&output.calls()[index].arguments).unwrap()
}
#[test]
@@ -281,8 +281,8 @@ mod tests {
let mut parser = HyV3ToolParser::new(&test_tools());
let output = parser.parse_complete("This is a plain response.").unwrap();
assert_eq!(output.normal_text, "This is a plain response.");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "This is a plain response.");
assert!(output.calls().is_empty());
}
#[test]
@@ -294,9 +294,9 @@ mod tests {
)
.unwrap();
assert_eq!(output.normal_text, "");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_current_date"));
assert_eq!(output.normal_text(), "");
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_current_date"));
assert_eq!(parsed_arguments(&output, 0), json!({}));
}
@@ -309,7 +309,7 @@ mod tests {
)
.unwrap();
assert_eq!(output.calls[0].name.as_deref(), Some("get_current_date"));
assert_eq!(output.calls()[0].name.as_deref(), Some("get_current_date"));
assert_eq!(parsed_arguments(&output, 0), json!({}));
}
@@ -354,8 +354,8 @@ mod tests {
))
.unwrap();
assert_eq!(output.normal_text, "Checking.");
assert_eq!(output.calls[0].name.as_deref(), Some("get_current_date"));
assert_eq!(output.normal_text(), "Checking.");
assert_eq!(output.calls()[0].name.as_deref(), Some("get_current_date"));
}
#[test]
@@ -376,22 +376,25 @@ mod tests {
expect![[r#"
ToolParserOutput {
normal_text: "",
calls: [
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"city\":\"Beijing\",\"date\":\"2026-03-30\"}",
},
ToolCallDelta {
tool_index: 1,
name: Some(
"get_weather",
),
arguments: "{\"city\":\"Hangzhou\",\"date\":\"2026-03-30\"}",
},
events: [
ToolCall(
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"city\":\"Beijing\",\"date\":\"2026-03-30\"}",
},
),
ToolCall(
ToolCallDelta {
tool_index: 1,
name: Some(
"get_weather",
),
arguments: "{\"city\":\"Hangzhou\",\"date\":\"2026-03-30\"}",
},
),
],
}
"#]]
@@ -434,8 +437,8 @@ mod tests {
output.append(parser.parse_chunk("response.").unwrap());
output.append(parser.finish().unwrap());
assert_eq!(output.normal_text, "This is a plain response.");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "This is a plain response.");
assert!(output.calls().is_empty());
}
#[test]
@@ -452,8 +455,8 @@ mod tests {
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_current_date"));
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_current_date"));
assert_eq!(parsed_arguments(&output, 0), json!({}));
}
@@ -475,8 +478,8 @@ mod tests {
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(
parsed_arguments(&output, 0),
json!({ "city": "Beijing", "date": "2026-03-30" })
@@ -498,8 +501,8 @@ mod tests {
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.normal_text, "Checking.");
assert_eq!(output.calls[0].name.as_deref(), Some("get_current_date"));
assert_eq!(output.normal_text(), "Checking.");
assert_eq!(output.calls()[0].name.as_deref(), Some("get_current_date"));
}
#[test]
@@ -519,7 +522,7 @@ mod tests {
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.calls.len(), 2);
assert_eq!(output.calls().len(), 2);
assert_eq!(parsed_arguments(&output, 0)["city"], json!("Beijing"));
assert_eq!(parsed_arguments(&output, 1)["city"], json!("Hangzhou"));
}
@@ -535,8 +538,8 @@ mod tests {
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.normal_text, "hello ");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.normal_text(), "hello ");
assert_eq!(output.calls().len(), 1);
assert_eq!(parsed_arguments(&output, 0), json!({ "city": "Beijing" }));
}
@@ -552,8 +555,8 @@ mod tests {
)
.unwrap();
assert_eq!(output.normal_text, "");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "");
assert!(output.calls().is_empty());
}
#[test]
+75 -61
View File
@@ -86,14 +86,14 @@ impl Granite4ToolParser {
/// Apply one parsed Granite 4 event to parser state and output.
fn apply_event(&mut self, event: Granite4Event, output: &mut ToolParserOutput) -> Result<()> {
match event {
Granite4Event::Text { len } => output.normal_text.push_str(&self.buffer[..len]),
Granite4Event::Text { len } => output.push_text(&self.buffer[..len]),
Granite4Event::ToolCallStart => self.mode = Granite4Mode::Header,
Granite4Event::ToolCallHeader { function_name } => {
let tool_index = self.emitted_tool_count;
self.emitted_tool_count += 1;
self.active_tool_index = Some(tool_index);
self.mode = Granite4Mode::Args { json_scan: None };
output.calls.push(ToolCallDelta {
output.push_call(ToolCallDelta {
tool_index,
name: Some(function_name),
arguments: String::new(),
@@ -125,7 +125,7 @@ impl Granite4ToolParser {
"Granite4 arguments without an active tool call"
));
};
output.calls.push(ToolCallDelta {
output.push_call(ToolCallDelta {
tool_index,
name: None,
arguments,
@@ -165,7 +165,7 @@ impl ToolParser for Granite4ToolParser {
fn finish(&mut self) -> Result<ToolParserOutput> {
let mut output = ToolParserOutput::default();
match &self.mode {
Granite4Mode::Text => output.normal_text.push_str(&self.buffer),
Granite4Mode::Text => output.push_text(&self.buffer),
Granite4Mode::Header | Granite4Mode::Args { .. } | Granite4Mode::Close => {
return Err(parsing_failed!("incomplete Granite4 tool call"));
}
@@ -287,8 +287,8 @@ mod tests {
let mut parser = Granite4ToolParser::new(&test_tools());
let output = parser.parse_complete("Hello, world!").unwrap();
assert_eq!(output.normal_text, "Hello, world!");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "Hello, world!");
assert!(output.calls().is_empty());
}
#[test]
@@ -300,10 +300,10 @@ mod tests {
)
.unwrap();
assert_eq!(output.normal_text, "");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls[0].arguments, r#"{"city":"Boston"}"#);
assert_eq!(output.normal_text(), "");
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls()[0].arguments, r#"{"city":"Boston"}"#);
}
#[test]
@@ -317,9 +317,9 @@ mod tests {
)
.unwrap();
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls[0].arguments, r#"{"city":"Boston"}"#);
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls()[0].arguments, r#"{"city":"Boston"}"#);
}
#[test]
@@ -333,22 +333,28 @@ mod tests {
expect![[r#"
ToolParserOutput {
normal_text: "before middle after",
calls: [
ToolCallDelta {
tool_index: 0,
name: Some(
"find_bbox",
),
arguments: "{\"x\":1}",
},
ToolCallDelta {
tool_index: 1,
name: Some(
"get_weather",
),
arguments: "{\"city\":\"Boston\"}",
},
events: [
Text(
"before middle after",
),
ToolCall(
ToolCallDelta {
tool_index: 0,
name: Some(
"find_bbox",
),
arguments: "{\"x\":1}",
},
),
ToolCall(
ToolCallDelta {
tool_index: 1,
name: Some(
"get_weather",
),
arguments: "{\"city\":\"Boston\"}",
},
),
],
}
"#]]
@@ -363,10 +369,10 @@ mod tests {
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.normal_text, "hello bye");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls[0].arguments, r#"{"city":"Tokyo"}"#);
assert_eq!(output.normal_text(), "hello bye");
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls()[0].arguments, r#"{"city":"Tokyo"}"#);
}
#[test]
@@ -385,7 +391,7 @@ mod tests {
for chunk in chunks {
let next = parser.parse_chunk(chunk).unwrap();
observed_arguments.extend(
next.calls
next.calls()
.iter()
.filter(|call| call.name.is_none())
.map(|call| call.arguments.clone()),
@@ -396,7 +402,7 @@ mod tests {
assert_eq!(observed_arguments, [r#"{"city":"#, r#""Beijing""#, r#"}"#]);
assert_eq!(
output.coalesce_calls().calls[0].arguments,
output.coalesce().calls()[0].arguments,
r#"{"city":"Beijing"}"#
);
}
@@ -409,9 +415,9 @@ mod tests {
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("f"));
assert_eq!(output.calls[0].arguments, r#"{"a":1}"#);
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("f"));
assert_eq!(output.calls()[0].arguments, r#"{"a":1}"#);
}
#[test]
@@ -435,29 +441,37 @@ mod tests {
expect![[r#"
ToolParserOutput {
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.",
calls: [
ToolCallDelta {
tool_index: 0,
name: Some(
"find_bbox",
),
arguments: "{\"coordinates\": [[23.54, 43.1], [-12.2, 54.3], [4, 5]], \"coordinate_type\": \"latlong\"}",
},
ToolCallDelta {
tool_index: 1,
name: Some(
"get_stock_price",
),
arguments: "{\"symbol\": \"AAPL\", \"start_date\": \"2021-01-01\", \"end_date\": \"2021-12-31\"}",
},
ToolCallDelta {
tool_index: 2,
name: Some(
"find_bbox",
),
arguments: "{\"coordinates\": [[23.54, 43.1], [-12.2, 54.3], [4, 5]], \"coordinate_type\": \"latlong\"}",
},
events: [
Text(
"Here goes the bbox call: \n Now the stock price call: \n Now another bbox call: \n See? I'm a helpful assistant.",
),
ToolCall(
ToolCallDelta {
tool_index: 0,
name: Some(
"find_bbox",
),
arguments: "{\"coordinates\": [[23.54, 43.1], [-12.2, 54.3], [4, 5]], \"coordinate_type\": \"latlong\"}",
},
),
ToolCall(
ToolCallDelta {
tool_index: 1,
name: Some(
"get_stock_price",
),
arguments: "{\"symbol\": \"AAPL\", \"start_date\": \"2021-01-01\", \"end_date\": \"2021-12-31\"}",
},
),
ToolCall(
ToolCallDelta {
tool_index: 2,
name: Some(
"find_bbox",
),
arguments: "{\"coordinates\": [[23.54, 43.1], [-12.2, 54.3], [4, 5]], \"coordinate_type\": \"latlong\"}",
},
),
],
}
"#]].assert_debug_eq(&output);
+35 -32
View File
@@ -80,8 +80,8 @@ mod tests {
let mut parser = HermesToolParser::new(&test_tools());
let output = parser.parse_complete("Hello, world!").unwrap();
assert_eq!(output.normal_text, "Hello, world!");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "Hello, world!");
assert!(output.calls().is_empty());
}
#[test]
@@ -95,11 +95,11 @@ mod tests {
))
.unwrap();
assert_eq!(output.normal_text, "Let me check.\n");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].tool_index, 0);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls[0].arguments, arguments);
assert_eq!(output.normal_text(), "Let me check.\n");
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].tool_index, 0);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls()[0].arguments, arguments);
}
#[test]
@@ -112,8 +112,8 @@ mod tests {
)
.unwrap();
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
}
#[test]
@@ -122,7 +122,7 @@ mod tests {
let arguments = r#"{"location":"Tokyo",}"#;
let output = parser.parse_complete(&build_tool_call("get_weather", arguments)).unwrap();
assert_eq!(output.calls[0].arguments, arguments);
assert_eq!(output.calls()[0].arguments, arguments);
}
#[test]
@@ -142,7 +142,7 @@ mod tests {
for chunk in chunks {
let next = parser.parse_chunk(chunk).unwrap();
observed_arguments.extend(
next.calls
next.calls()
.iter()
.filter(|call| call.name.is_none())
.map(|call| call.arguments.clone()),
@@ -152,9 +152,9 @@ mod tests {
output.append(parser.finish().unwrap());
assert_eq!(observed_arguments, ["{\"location\":", "\"Beijing\"", "}"]);
assert_eq!(output.normal_text, "preface suffix");
assert_eq!(output.normal_text(), "preface suffix");
assert_eq!(
output.coalesce_calls().calls[0].arguments,
output.coalesce().calls()[0].arguments,
r#"{"location":"Beijing"}"#
);
}
@@ -170,9 +170,9 @@ mod tests {
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.normal_text, "hello ");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].arguments, r#"{"location":"Tokyo"}"#);
assert_eq!(output.normal_text(), "hello ");
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].arguments, r#"{"location":"Tokyo"}"#);
}
#[test]
@@ -189,22 +189,25 @@ mod tests {
expect![[r#"
ToolParserOutput {
normal_text: "",
calls: [
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"location\":\"Shanghai\"}",
},
ToolCallDelta {
tool_index: 1,
name: Some(
"add",
),
arguments: "{\"x\":1,\"y\":2}",
},
events: [
ToolCall(
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"location\":\"Shanghai\"}",
},
),
ToolCall(
ToolCallDelta {
tool_index: 1,
name: Some(
"add",
),
arguments: "{\"x\":1,\"y\":2}",
},
),
],
}
"#]]
+41 -38
View File
@@ -140,8 +140,8 @@ mod tests {
let mut parser = Internlm2ToolParser::new(&test_tools());
let result = parser.parse_complete("Hello, world!").unwrap();
assert_eq!(result.normal_text, "Hello, world!");
assert!(result.calls.is_empty());
assert_eq!(result.normal_text(), "Hello, world!");
assert!(result.calls().is_empty());
}
#[test]
@@ -155,11 +155,11 @@ mod tests {
))
.unwrap();
assert_eq!(result.normal_text, "Let me check.\n");
assert_eq!(result.calls.len(), 1);
assert_eq!(result.calls[0].tool_index, 0);
assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(result.calls[0].arguments, arguments);
assert_eq!(result.normal_text(), "Let me check.\n");
assert_eq!(result.calls().len(), 1);
assert_eq!(result.calls()[0].tool_index, 0);
assert_eq!(result.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(result.calls()[0].arguments, arguments);
}
#[test]
@@ -170,9 +170,9 @@ mod tests {
.parse_complete(&build_tool_call("get_weather", "arguments", arguments))
.unwrap();
assert_eq!(result.calls.len(), 1);
assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(result.calls[0].arguments, arguments);
assert_eq!(result.calls().len(), 1);
assert_eq!(result.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(result.calls()[0].arguments, arguments);
}
#[test]
@@ -185,8 +185,8 @@ mod tests {
))
.unwrap();
assert_eq!(result.calls.len(), 1);
assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(result.calls().len(), 1);
assert_eq!(result.calls()[0].name.as_deref(), Some("get_weather"));
}
#[test]
@@ -197,7 +197,7 @@ mod tests {
.parse_complete(&build_tool_call("get_weather", "parameters", arguments))
.unwrap();
assert_eq!(result.calls[0].arguments, arguments);
assert_eq!(result.calls()[0].arguments, arguments);
}
#[test]
@@ -218,7 +218,7 @@ mod tests {
for chunk in chunks {
let next = parser.parse_chunk(chunk).unwrap();
observed_arguments.extend(
next.calls
next.calls()
.iter()
.filter(|call| call.name.is_none())
.map(|call| call.arguments.clone()),
@@ -231,9 +231,9 @@ mod tests {
observed_arguments,
[r#"{"location":"#, r#""Beijing""#, r#"}"#]
);
assert_eq!(result.normal_text, "preface suffix");
assert_eq!(result.normal_text(), "preface suffix");
assert_eq!(
result.coalesce_calls().calls[0].arguments,
result.coalesce().calls()[0].arguments,
r#"{"location":"Beijing"}"#
);
}
@@ -249,9 +249,9 @@ mod tests {
let result = collect_stream(&mut parser, &chunks);
assert_eq!(result.normal_text, "hello ");
assert_eq!(result.calls.len(), 1);
assert_eq!(result.calls[0].arguments, r#"{"location":"Tokyo"}"#);
assert_eq!(result.normal_text(), "hello ");
assert_eq!(result.calls().len(), 1);
assert_eq!(result.calls()[0].arguments, r#"{"location":"Tokyo"}"#);
}
#[test]
@@ -268,22 +268,25 @@ mod tests {
expect![[r#"
ToolParserOutput {
normal_text: "",
calls: [
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"location\":\"Shanghai\"}",
},
ToolCallDelta {
tool_index: 1,
name: Some(
"add",
),
arguments: "{\"x\":1,\"y\":2}",
},
events: [
ToolCall(
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"location\":\"Shanghai\"}",
},
),
ToolCall(
ToolCallDelta {
tool_index: 1,
name: Some(
"add",
),
arguments: "{\"x\":1,\"y\":2}",
},
),
],
}
"#]]
@@ -298,8 +301,8 @@ mod tests {
let result = parser.parse_complete(&input).unwrap();
assert_eq!(result.calls.len(), 1);
assert_eq!(result.calls[0].arguments, arguments);
assert_eq!(result.calls().len(), 1);
assert_eq!(result.calls()[0].arguments, arguments);
}
#[test]
@@ -313,7 +316,7 @@ mod tests {
let error = parser.finish().unwrap_err();
assert_eq!(
pre_finish.calls[0].name.as_deref(),
pre_finish.calls()[0].name.as_deref(),
Some("get_weather"),
"name delta is still emitted from parse_chunk() before truncation",
);
+45 -42
View File
@@ -87,7 +87,7 @@ impl Llama3JsonToolParser {
self.mode = LlamaJsonMode::Arguments {
json_scan: JsonObjectScanState::default(),
};
output.calls.push(ToolCallDelta {
output.push_call(ToolCallDelta {
tool_index,
name: Some(function_name),
arguments: String::new(),
@@ -99,7 +99,7 @@ impl Llama3JsonToolParser {
"Llama JSON arguments without an active tool call"
));
};
output.calls.push(ToolCallDelta {
output.push_call(ToolCallDelta {
tool_index,
name: None,
arguments: self.buffer[..consumed_len].to_string(),
@@ -145,7 +145,7 @@ impl ToolParser for Llama3JsonToolParser {
}
if matches!(self.mode, LlamaJsonMode::Passthrough) {
output.normal_text.push_str(&self.buffer);
output.push_text(&self.buffer);
self.buffer.clear();
return Ok(());
}
@@ -164,7 +164,7 @@ impl ToolParser for Llama3JsonToolParser {
let mut output = ToolParserOutput::default();
match &self.mode {
LlamaJsonMode::Start | LlamaJsonMode::Passthrough => {
output.normal_text.push_str(&self.buffer);
output.push_text(&self.buffer);
}
LlamaJsonMode::AfterCall if self.buffer.trim().is_empty() => {}
LlamaJsonMode::Header | LlamaJsonMode::Arguments { .. } => {
@@ -268,8 +268,8 @@ mod tests {
let mut parser = Llama3JsonToolParser::new(&test_tools());
let output = parser.parse_complete("Hello, world!").unwrap();
assert_eq!(output.normal_text, "Hello, world!");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "Hello, world!");
assert!(output.calls().is_empty());
}
#[test]
@@ -284,10 +284,10 @@ mod tests {
output.append(parser.finish().unwrap());
assert_eq!(
output.normal_text,
output.normal_text(),
r#"plain text first {"name":"get_weather","parameters":{"location":"Tokyo"}}"#
);
assert!(output.calls.is_empty());
assert!(output.calls().is_empty());
}
#[test]
@@ -299,8 +299,8 @@ mod tests {
);
let output = parser.parse_complete(&input).unwrap();
assert_eq!(output.normal_text, input);
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), input);
assert!(output.calls().is_empty());
}
#[test]
@@ -312,8 +312,8 @@ mod tests {
);
let output = parser.parse_complete(&input).unwrap();
assert_eq!(output.normal_text, input);
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), input);
assert!(output.calls().is_empty());
}
#[test]
@@ -322,10 +322,10 @@ mod tests {
let arguments = r#"{ "location": "Tokyo", "days": 3 }"#;
let output = parser.parse_complete(&build_tool_call("get_weather", arguments)).unwrap();
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].tool_index, 0);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls[0].arguments, arguments);
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].tool_index, 0);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls()[0].arguments, arguments);
}
#[test]
@@ -353,22 +353,25 @@ mod tests {
expect![[r#"
ToolParserOutput {
normal_text: "",
calls: [
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"location\":\"Shanghai\"}",
},
ToolCallDelta {
tool_index: 1,
name: Some(
"add",
),
arguments: "{\"x\":1,\"y\":2}",
},
events: [
ToolCall(
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"location\":\"Shanghai\"}",
},
),
ToolCall(
ToolCallDelta {
tool_index: 1,
name: Some(
"add",
),
arguments: "{\"x\":1,\"y\":2}",
},
),
],
}
"#]]
@@ -390,7 +393,7 @@ mod tests {
for chunk in chunks {
let next = parser.parse_chunk(chunk).unwrap();
observed_arguments.extend(
next.calls
next.calls()
.iter()
.filter(|call| call.name.is_none())
.map(|call| call.arguments.clone()),
@@ -401,7 +404,7 @@ mod tests {
assert_eq!(observed_arguments, ["{\"location\":", "\"Beijing\"", "}"]);
assert_eq!(
output.coalesce_calls().calls[0].arguments,
output.coalesce().calls()[0].arguments,
r#"{"location":"Beijing"}"#
);
}
@@ -418,14 +421,14 @@ mod tests {
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.normal_text, "");
assert_eq!(output.calls.len(), 2);
assert_eq!(output.normal_text(), "");
assert_eq!(output.calls().len(), 2);
assert_eq!(
output.calls[0].arguments,
output.calls()[0].arguments,
r#"{"location":"Dallas","state":"TX"}"#
);
assert_eq!(output.calls[1].name.as_deref(), Some("add"));
assert_eq!(output.calls[1].arguments, r#"{"x":4,"y":5}"#);
assert_eq!(output.calls()[1].name.as_deref(), Some("add"));
assert_eq!(output.calls()[1].arguments, r#"{"x":4,"y":5}"#);
}
#[test]
@@ -437,7 +440,7 @@ mod tests {
}"#;
let output = parser.parse_complete(&build_tool_call("convert", arguments)).unwrap();
assert_eq!(output.calls[0].arguments, arguments);
assert_eq!(output.calls()[0].arguments, arguments);
}
#[test]
@@ -450,8 +453,8 @@ mod tests {
))
.unwrap();
assert_eq!(output.normal_text, "");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.normal_text(), "");
assert_eq!(output.calls().len(), 1);
}
#[test]
+38 -32
View File
@@ -77,8 +77,8 @@ mod tests {
let mut parser = MistralToolParser::new(&test_tools());
let output = parser.parse_complete("Hello, world!").unwrap();
assert_eq!(output.normal_text, "Hello, world!");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "Hello, world!");
assert!(output.calls().is_empty());
}
#[test]
@@ -92,11 +92,11 @@ mod tests {
))
.unwrap();
assert_eq!(output.normal_text, "Let me check.\n");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].tool_index, 0);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls[0].arguments, arguments);
assert_eq!(output.normal_text(), "Let me check.\n");
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].tool_index, 0);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls()[0].arguments, arguments);
}
#[test]
@@ -115,22 +115,28 @@ mod tests {
expect![[r#"
ToolParserOutput {
normal_text: "I'll help.\n",
calls: [
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"city\": \"Tokyo\", \"units\": \"celsius\"}",
},
ToolCallDelta {
tool_index: 1,
name: Some(
"add",
),
arguments: "{\"x\": 1, \"y\": 2}",
},
events: [
Text(
"I'll help.\n",
),
ToolCall(
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"city\": \"Tokyo\", \"units\": \"celsius\"}",
},
),
ToolCall(
ToolCallDelta {
tool_index: 1,
name: Some(
"add",
),
arguments: "{\"x\": 1, \"y\": 2}",
},
),
],
}
"#]]
@@ -148,7 +154,7 @@ mod tests {
)]))
.unwrap();
assert_eq!(output.calls[0].arguments, arguments);
assert_eq!(output.calls()[0].arguments, arguments);
}
#[test]
@@ -168,7 +174,7 @@ mod tests {
for chunk in chunks {
let next = parser.parse_chunk(chunk).unwrap();
observed_arguments.extend(
next.calls
next.calls()
.iter()
.filter(|call| call.name.is_none())
.map(|call| call.arguments.clone()),
@@ -178,9 +184,9 @@ mod tests {
output.append(parser.finish().unwrap());
assert_eq!(observed_arguments, ["{\"location\":", "\"Beijing\"", "}"]);
assert_eq!(output.normal_text, "preface suffix");
assert_eq!(output.normal_text(), "preface suffix");
assert_eq!(
output.coalesce_calls().calls[0].arguments,
output.coalesce().calls()[0].arguments,
r#"{"location":"Beijing"}"#
);
}
@@ -196,9 +202,9 @@ mod tests {
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.normal_text, "hello ");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].arguments, r#"{"location":"Tokyo"}"#);
assert_eq!(output.normal_text(), "hello ");
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].arguments, r#"{"location":"Tokyo"}"#);
}
#[test]
@@ -209,8 +215,8 @@ mod tests {
.parse_complete(&build_tool_calls(&[build_tool_call("echo", arguments)]))
.unwrap();
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].arguments, arguments);
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].arguments, arguments);
}
#[test]
+56 -46
View File
@@ -106,7 +106,7 @@ impl JsonToolCallParser {
fn finish(&mut self) -> Result<ToolParserOutput> {
let mut output = ToolParserOutput::default();
match &self.mode {
JsonToolCallMode::Text => output.normal_text.push_str(&self.buffer),
JsonToolCallMode::Text => output.push_text(&self.buffer),
JsonToolCallMode::Header | JsonToolCallMode::Arguments { .. } => {
return Err(parsing_failed!(
"incomplete {} tool call",
@@ -126,7 +126,7 @@ impl JsonToolCallParser {
) -> Result<()> {
match event {
JsonToolCallEvent::Text { len: consumed_len } => {
output.normal_text.push_str(&self.buffer[..consumed_len]);
output.push_text(&self.buffer[..consumed_len]);
}
JsonToolCallEvent::ToolCallStart => self.mode = JsonToolCallMode::Header,
JsonToolCallEvent::ToolCallHeader { function_name } => {
@@ -136,7 +136,7 @@ impl JsonToolCallParser {
self.mode = JsonToolCallMode::Arguments {
json_scan: JsonObjectScanState::default(),
};
output.calls.push(ToolCallDelta {
output.push_call(ToolCallDelta {
tool_index,
name: Some(function_name),
arguments: String::new(),
@@ -149,7 +149,7 @@ impl JsonToolCallParser {
self.config.parser_name
));
};
output.calls.push(ToolCallDelta {
output.push_call(ToolCallDelta {
tool_index,
name: None,
arguments: self.buffer[..consumed_len].to_string(),
@@ -400,7 +400,7 @@ mod tests {
parser.parse_into(chunk, &mut output).unwrap();
}
output.append(parser.finish().unwrap());
output.coalesce_calls()
output.coalesce()
}
#[test]
@@ -415,22 +415,25 @@ mod tests {
expect![[r#"
ToolParserOutput {
normal_text: "",
calls: [
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"location\":\"Shanghai\"}",
},
ToolCallDelta {
tool_index: 1,
name: Some(
"add",
),
arguments: "{\"x\":1,\"y\":2}",
},
events: [
ToolCall(
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"location\":\"Shanghai\"}",
},
),
ToolCall(
ToolCallDelta {
tool_index: 1,
name: Some(
"add",
),
arguments: "{\"x\":1,\"y\":2}",
},
),
],
}
"#]]
@@ -451,22 +454,25 @@ mod tests {
expect![[r#"
ToolParserOutput {
normal_text: "",
calls: [
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"location\":\"Shanghai\"}",
},
ToolCallDelta {
tool_index: 1,
name: Some(
"add",
),
arguments: "{\"x\":1,\"y\":2}",
},
events: [
ToolCall(
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"location\":\"Shanghai\"}",
},
),
ToolCall(
ToolCallDelta {
tool_index: 1,
name: Some(
"add",
),
arguments: "{\"x\":1,\"y\":2}",
},
),
],
}
"#]]
@@ -486,15 +492,19 @@ mod tests {
expect![[r#"
ToolParserOutput {
normal_text: " trailing text",
calls: [
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"location\":\"Shanghai\"}",
},
events: [
Text(
" trailing text",
),
ToolCall(
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"location\":\"Shanghai\"}",
},
),
],
}
"#]]
+49 -46
View File
@@ -87,8 +87,8 @@ mod tests {
let mut parser = Phi4MiniJsonToolParser::new(&test_tools());
let result = parser.parse_complete("Hello, world!").unwrap();
assert_eq!(result.normal_text, "Hello, world!");
assert!(result.calls.is_empty());
assert_eq!(result.normal_text(), "Hello, world!");
assert!(result.calls().is_empty());
}
#[test]
@@ -99,10 +99,10 @@ mod tests {
.parse_complete(&wrap(&[build_call("get_weather", "arguments", arguments)]))
.unwrap();
assert_eq!(result.calls.len(), 1);
assert_eq!(result.calls[0].tool_index, 0);
assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(result.calls[0].arguments, arguments);
assert_eq!(result.calls().len(), 1);
assert_eq!(result.calls()[0].tool_index, 0);
assert_eq!(result.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(result.calls()[0].arguments, arguments);
}
#[test]
@@ -113,9 +113,9 @@ mod tests {
.parse_complete(&wrap(&[build_call("get_weather", "parameters", arguments)]))
.unwrap();
assert_eq!(result.calls.len(), 1);
assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(result.calls[0].arguments, arguments);
assert_eq!(result.calls().len(), 1);
assert_eq!(result.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(result.calls()[0].arguments, arguments);
}
#[test]
@@ -130,22 +130,25 @@ mod tests {
expect![[r#"
ToolParserOutput {
normal_text: "",
calls: [
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"location\":\"Shanghai\"}",
},
ToolCallDelta {
tool_index: 1,
name: Some(
"add",
),
arguments: "{\"x\":1,\"y\":2}",
},
events: [
ToolCall(
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"location\":\"Shanghai\"}",
},
),
ToolCall(
ToolCallDelta {
tool_index: 1,
name: Some(
"add",
),
arguments: "{\"x\":1,\"y\":2}",
},
),
],
}
"#]]
@@ -162,8 +165,8 @@ mod tests {
.parse_complete(&wrap(&[build_call("convert", "arguments", arguments)]))
.unwrap();
assert_eq!(result.calls.len(), 1);
assert_eq!(result.calls[0].arguments, arguments);
assert_eq!(result.calls().len(), 1);
assert_eq!(result.calls()[0].arguments, arguments);
}
/// Preface text before a tool call is preserved as normal_text, consistent
@@ -182,8 +185,8 @@ mod tests {
let result = parser.parse_complete(&input).unwrap();
assert_eq!(result.normal_text, "Let me check.\n");
assert_eq!(result.calls.len(), 1);
assert_eq!(result.normal_text(), "Let me check.\n");
assert_eq!(result.calls().len(), 1);
}
#[test]
@@ -194,7 +197,7 @@ mod tests {
.parse_complete(&wrap(&[build_call("get_weather", "arguments", arguments)]))
.unwrap();
assert_eq!(result.calls[0].arguments, arguments);
assert_eq!(result.calls()[0].arguments, arguments);
}
/// The bundled `tool_chat_template_phi4_mini.jinja` emits objects with
@@ -208,9 +211,9 @@ mod tests {
let result = parser.parse_complete(input).unwrap();
assert_eq!(result.calls.len(), 1);
assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(result.calls[0].arguments, r#"{"location": "Tokyo"}"#);
assert_eq!(result.calls().len(), 1);
assert_eq!(result.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(result.calls()[0].arguments, r#"{"location": "Tokyo"}"#);
}
/// Argument deltas are streamed through the shared JSON core.
@@ -230,10 +233,10 @@ mod tests {
let result = collect_stream(&mut parser, &chunks);
assert_eq!(result.normal_text, "preface suffix");
assert_eq!(result.calls.len(), 1);
assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(result.calls[0].arguments, r#"{"location":"Beijing"}"#);
assert_eq!(result.normal_text(), "preface suffix");
assert_eq!(result.calls().len(), 1);
assert_eq!(result.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(result.calls()[0].arguments, r#"{"location":"Beijing"}"#);
}
#[test]
@@ -251,9 +254,9 @@ mod tests {
let result = collect_stream(&mut parser, &chunks);
assert_eq!(result.normal_text, "hello ");
assert_eq!(result.calls.len(), 1);
assert_eq!(result.calls[0].arguments, r#"{"location":"Tokyo"}"#);
assert_eq!(result.normal_text(), "hello ");
assert_eq!(result.calls().len(), 1);
assert_eq!(result.calls()[0].arguments, r#"{"location":"Tokyo"}"#);
}
#[test]
@@ -287,9 +290,9 @@ mod tests {
.parse_complete(&wrap(&[build_call("convert", "arguments", arguments)]))
.unwrap();
assert_eq!(result.calls.len(), 1);
assert_eq!(result.calls[0].name.as_deref(), Some("convert"));
assert_eq!(result.calls[0].arguments, arguments);
assert_eq!(result.calls().len(), 1);
assert_eq!(result.calls()[0].name.as_deref(), Some("convert"));
assert_eq!(result.calls()[0].arguments, arguments);
}
/// The chat template emits parallel calls as `},\n {` (comma + newline +
@@ -307,9 +310,9 @@ mod tests {
let result = parser.parse_complete(input).unwrap();
assert_eq!(result.calls.len(), 2);
assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(result.calls[1].name.as_deref(), Some("add"));
assert_eq!(result.calls().len(), 2);
assert_eq!(result.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(result.calls()[1].name.as_deref(), Some("add"));
}
/// The shared core requires an object after the start marker.
+37 -34
View File
@@ -84,8 +84,8 @@ mod tests {
let mut parser = Qwen3XmlToolParser::new(&test_tools());
let output = parser.parse_complete("Hello, world!").unwrap();
assert_eq!(output.normal_text, "Hello, world!");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "Hello, world!");
assert!(output.calls().is_empty());
}
#[test]
@@ -99,11 +99,11 @@ mod tests {
))
.unwrap();
assert_eq!(output.normal_text, "Let me check.\n");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].tool_index, 0);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls[0].arguments, arguments);
assert_eq!(output.normal_text(), "Let me check.\n");
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].tool_index, 0);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls()[0].arguments, arguments);
}
#[test]
@@ -112,7 +112,7 @@ mod tests {
let arguments = r#"{"location":"Tokyo",}"#;
let output = parser.parse_complete(&build_tool_call("get_weather", arguments)).unwrap();
assert_eq!(output.calls[0].arguments, arguments);
assert_eq!(output.calls()[0].arguments, arguments);
}
#[test]
@@ -132,7 +132,7 @@ mod tests {
for chunk in chunks {
let next = parser.parse_chunk(chunk).unwrap();
observed_arguments.extend(
next.calls
next.calls()
.iter()
.filter(|call| call.name.is_none())
.map(|call| call.arguments.clone()),
@@ -143,7 +143,7 @@ mod tests {
assert_eq!(observed_arguments, ["{\"location\":", "\"Beijing\"", "}"]);
assert_eq!(
output.coalesce_calls().calls[0].arguments,
output.coalesce().calls()[0].arguments,
r#"{"location":"Beijing"}"#
);
}
@@ -159,9 +159,9 @@ mod tests {
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.normal_text, "hello ");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].arguments, r#"{"location":"Tokyo"}"#);
assert_eq!(output.normal_text(), "hello ");
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].arguments, r#"{"location":"Tokyo"}"#);
}
#[test]
@@ -170,8 +170,8 @@ mod tests {
let arguments = r#"{"text":"literal </tool_call> inside"}"#;
let output = parser.parse_complete(&build_tool_call("echo", arguments)).unwrap();
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].arguments, arguments);
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].arguments, arguments);
}
#[test]
@@ -185,7 +185,7 @@ mod tests {
)
.unwrap();
assert_eq!(output.calls[0].name.as_deref(), Some("say_\"hi"));
assert_eq!(output.calls()[0].name.as_deref(), Some("say_\"hi"));
}
#[test]
@@ -196,8 +196,8 @@ mod tests {
let output = parser.parse_complete(input).unwrap();
assert_eq!(output.normal_text, input);
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), input);
assert!(output.calls().is_empty());
}
#[test]
@@ -227,22 +227,25 @@ mod tests {
expect![[r#"
ToolParserOutput {
normal_text: "",
calls: [
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"location\":\"Shanghai\"}",
},
ToolCallDelta {
tool_index: 1,
name: Some(
"add",
),
arguments: "{\"x\":1,\"y\":2}",
},
events: [
ToolCall(
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"location\":\"Shanghai\"}",
},
),
ToolCall(
ToolCallDelta {
tool_index: 1,
name: Some(
"add",
),
arguments: "{\"x\":1,\"y\":2}",
},
),
],
}
"#]]
+45 -42
View File
@@ -81,7 +81,7 @@ impl KimiK2ToolParser {
fn apply_event(&mut self, event: KimiK2Event, output: &mut ToolParserOutput) -> Result<()> {
match event {
KimiK2Event::Text { len: consumed_len } => {
output.normal_text.push_str(&self.buffer[..consumed_len]);
output.push_text(&self.buffer[..consumed_len]);
}
KimiK2Event::ToolCallsStart => self.mode = KimiK2Mode::ToolBlock,
KimiK2Event::ToolCallStart => self.mode = KimiK2Mode::Header,
@@ -96,7 +96,7 @@ impl KimiK2ToolParser {
json_scan: JsonObjectScanState::default(),
};
self.call_ids.insert(tool_index, tool_call_id);
output.calls.push(ToolCallDelta {
output.push_call(ToolCallDelta {
tool_index,
name: Some(function_name),
arguments: String::new(),
@@ -108,7 +108,7 @@ impl KimiK2ToolParser {
"Kimi K2 arguments without an active tool call"
));
};
output.calls.push(ToolCallDelta {
output.push_call(ToolCallDelta {
tool_index,
name: None,
arguments: self.buffer[..consumed_len].to_string(),
@@ -171,7 +171,7 @@ impl ToolParser for KimiK2ToolParser {
fn finish(&mut self) -> Result<ToolParserOutput> {
let mut output = ToolParserOutput::default();
match &self.mode {
KimiK2Mode::Text => output.normal_text.push_str(&self.buffer),
KimiK2Mode::Text => output.push_text(&self.buffer),
KimiK2Mode::ToolBlock | KimiK2Mode::Done => {}
KimiK2Mode::Header | KimiK2Mode::Arguments { .. } => {
return Err(parsing_failed!("incomplete Kimi K2 tool call"));
@@ -357,8 +357,8 @@ mod tests {
let mut parser = KimiK2ToolParser::new(&test_tools());
let output = parser.parse_complete("Hello, world!").unwrap();
assert_eq!(output.normal_text, "Hello, world!");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "Hello, world!");
assert!(output.calls().is_empty());
}
#[test]
@@ -372,11 +372,11 @@ mod tests {
))
.unwrap();
assert_eq!(output.normal_text, "Checking. ");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].tool_index, 0);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls[0].arguments, arguments);
assert_eq!(output.normal_text(), "Checking. ");
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].tool_index, 0);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls()[0].arguments, arguments);
}
#[test]
@@ -391,7 +391,7 @@ mod tests {
)]))
.unwrap();
assert_eq!(output.calls[0].arguments, arguments);
assert_eq!(output.calls()[0].arguments, arguments);
}
#[test]
@@ -414,7 +414,7 @@ mod tests {
for chunk in chunks {
let next = parser.parse_chunk(chunk).unwrap();
observed_arguments.extend(
next.calls
next.calls()
.iter()
.filter(|call| call.name.is_none())
.map(|call| call.arguments.clone()),
@@ -424,8 +424,8 @@ mod tests {
output.append(parser.finish().unwrap());
assert_eq!(observed_arguments, ["{\"location\":", "\"Paris\"", "}"]);
let output = output.coalesce_calls();
assert_eq!(output.calls[0].arguments, r#"{"location":"Paris"}"#);
let output = output.coalesce();
assert_eq!(output.calls()[0].arguments, r#"{"location":"Paris"}"#);
}
#[test]
@@ -445,9 +445,9 @@ mod tests {
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.normal_text, "hello ");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].arguments, r#"{"location":"NYC"}"#);
assert_eq!(output.normal_text(), "hello ");
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].arguments, r#"{"location":"NYC"}"#);
}
#[test]
@@ -458,8 +458,8 @@ mod tests {
let output = parser.parse_complete(&input).unwrap();
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].arguments, arguments);
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].arguments, arguments);
}
#[test]
@@ -478,9 +478,9 @@ mod tests {
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls().len(), 1);
assert_eq!(
output.calls[0].arguments,
output.calls()[0].arguments,
r#"{"text":"literal <|tool_call_end|> inside"}"#
);
}
@@ -498,22 +498,25 @@ mod tests {
expect![[r#"
ToolParserOutput {
normal_text: "",
calls: [
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"location\":\"Shanghai\"}",
},
ToolCallDelta {
tool_index: 1,
name: Some(
"add",
),
arguments: "{\"x\":1,\"y\":2}",
},
events: [
ToolCall(
ToolCallDelta {
tool_index: 0,
name: Some(
"get_weather",
),
arguments: "{\"location\":\"Shanghai\"}",
},
),
ToolCall(
ToolCallDelta {
tool_index: 1,
name: Some(
"add",
),
arguments: "{\"x\":1,\"y\":2}",
},
),
],
}
"#]]
@@ -546,12 +549,12 @@ mod tests {
"{TOOL_CALLS_START}{TOOL_CALL_START}api.tools.search:42{TOOL_CALL_ARGUMENT_START}{{}}{TOOL_CALL_END}{TOOL_CALLS_END}"
);
let output = parser.parse_chunk(&input).unwrap().coalesce_calls();
let output = parser.parse_chunk(&input).unwrap().coalesce();
assert_eq!(output.calls[0].tool_index, 42);
assert_eq!(output.calls()[0].tool_index, 42);
assert_eq!(parser.tool_call_id(42), Some("api.tools.search:42"));
assert_eq!(output.calls[0].name.as_deref(), Some("search"));
assert_eq!(output.calls[0].arguments, "{}");
assert_eq!(output.calls()[0].name.as_deref(), Some("search"));
assert_eq!(output.calls()[0].arguments, "{}");
}
#[test]
+43 -43
View File
@@ -72,7 +72,7 @@ impl MinimaxM2ToolParser {
fn apply_event(&mut self, event: MinimaxM2Event, output: &mut ToolParserOutput) -> Result<()> {
match event {
MinimaxM2Event::Text { len: consumed_len } => {
output.normal_text.push_str(&self.buffer[..consumed_len]);
output.push_text(&self.buffer[..consumed_len]);
}
MinimaxM2Event::ToolBlockStart => {
self.mode = MinimaxM2Mode::ToolBlock {
@@ -84,7 +84,7 @@ impl MinimaxM2ToolParser {
let arguments = serde_json::to_string(&arguments)
.map_err(|error| parsing_failed!("failed to serialize arguments: {}", error))?;
output.calls.push(ToolCallDelta {
output.push_call(ToolCallDelta {
tool_index: self.emitted_tool_count,
name: Some(name),
arguments,
@@ -133,7 +133,7 @@ impl ToolParser for MinimaxM2ToolParser {
let mut output = ToolParserOutput::default();
match self.mode {
MinimaxM2Mode::Text => {
output.normal_text.push_str(&self.buffer);
output.push_text(&self.buffer);
}
MinimaxM2Mode::ToolBlock { .. } => {
return Err(parsing_failed!("incomplete MiniMax M2 tool call"));
@@ -295,8 +295,8 @@ mod tests {
let mut parser = MinimaxM2ToolParser::new(&test_tools());
let output = parser.parse_complete("Hello, world!").unwrap();
assert_eq!(output.normal_text, "Hello, world!");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "Hello, world!");
assert!(output.calls().is_empty());
}
#[test]
@@ -309,11 +309,11 @@ mod tests {
)]))
.unwrap();
assert!(output.normal_text.is_empty());
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert!(output.normal_text().is_empty());
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({ "city": "Seattle", "days": 5 })
);
}
@@ -327,8 +327,8 @@ mod tests {
);
let output = parser.parse_complete(&output).unwrap();
assert_eq!(output.normal_text, "Let me check. ");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.normal_text(), "Let me check. ");
assert_eq!(output.calls().len(), 1);
}
#[test]
@@ -341,15 +341,15 @@ mod tests {
]))
.unwrap();
assert_eq!(output.calls.len(), 2);
assert_eq!(output.calls[0].tool_index, 0);
assert_eq!(output.calls[1].tool_index, 1);
assert_eq!(output.calls().len(), 2);
assert_eq!(output.calls()[0].tool_index, 0);
assert_eq!(output.calls()[1].tool_index, 1);
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({ "city": "Seattle" })
);
assert_eq!(
serde_json::from_str::<Value>(&output.calls[1].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[1].arguments).unwrap(),
json!({ "city": "NYC" })
);
}
@@ -371,7 +371,7 @@ mod tests {
.unwrap();
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({
"whole": 5.0,
"flag": true,
@@ -395,7 +395,7 @@ mod tests {
vec![("city", "Tom &amp; Jerry &lt;3")],
)]))
.unwrap();
let args: Value = serde_json::from_str(&output.calls[0].arguments).unwrap();
let args: Value = serde_json::from_str(&output.calls()[0].arguments).unwrap();
assert_eq!(args["city"], json!("Tom &amp; Jerry &lt;3"));
}
@@ -416,7 +416,7 @@ mod tests {
.unwrap();
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({
"city": "Seattle &lt;/parameter&gt;&lt;/invoke&gt;&lt;/minimax:tool_call&gt;",
"days": 5,
@@ -440,7 +440,7 @@ mod tests {
.unwrap();
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({
"shape": "\nrectangle\n",
"dimensions": { "width": 10, "height": 20 },
@@ -462,11 +462,11 @@ mod tests {
],
);
assert!(output.normal_text.is_empty());
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert!(output.normal_text().is_empty());
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({ "city": "Seattle" })
);
}
@@ -484,8 +484,8 @@ mod tests {
],
);
assert_eq!(output.normal_text, "Let me check. ");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.normal_text(), "Let me check. ");
assert_eq!(output.calls().len(), 1);
}
#[test]
@@ -493,8 +493,8 @@ mod tests {
let mut parser = MinimaxM2ToolParser::new(&test_tools());
let output = collect_stream(&mut parser, &["Hello, ", "world!"]);
assert_eq!(output.normal_text, "Hello, world!");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "Hello, world!");
assert!(output.calls().is_empty());
}
#[test]
@@ -504,8 +504,8 @@ mod tests {
let mut parser = MinimaxM2ToolParser::new(&test_tools());
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.calls.len(), 1);
assert!(output.normal_text.is_empty());
assert_eq!(output.calls().len(), 1);
assert!(output.normal_text().is_empty());
}
#[test]
@@ -518,9 +518,9 @@ mod tests {
let mut parser = MinimaxM2ToolParser::new(&test_tools());
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.calls.len(), 2);
assert_eq!(output.calls[0].tool_index, 0);
assert_eq!(output.calls[1].tool_index, 1);
assert_eq!(output.calls().len(), 2);
assert_eq!(output.calls()[0].tool_index, 0);
assert_eq!(output.calls()[1].tool_index, 1);
}
#[test]
@@ -540,12 +540,12 @@ mod tests {
let mut parser = MinimaxM2ToolParser::new(&test_tools());
let result = collect_stream(&mut parser, &chunks);
assert_eq!(result.normal_text, "I will call the tools.\n");
assert_eq!(result.calls.len(), 2);
assert_eq!(result.calls[0].tool_index, 0);
assert_eq!(result.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(result.calls[1].tool_index, 1);
assert_eq!(result.calls[1].name.as_deref(), Some("get_weather"));
assert_eq!(result.normal_text(), "I will call the tools.\n");
assert_eq!(result.calls().len(), 2);
assert_eq!(result.calls()[0].tool_index, 0);
assert_eq!(result.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(result.calls()[1].tool_index, 1);
assert_eq!(result.calls()[1].name.as_deref(), Some("get_weather"));
}
#[test]
@@ -558,8 +558,8 @@ mod tests {
let mut parser = MinimaxM2ToolParser::new(&test_tools());
let output = collect_stream(&mut parser, &chunks);
assert!(output.normal_text.is_empty());
assert_eq!(output.calls.len(), 1);
assert!(output.normal_text().is_empty());
assert_eq!(output.calls().len(), 1);
}
#[test]
@@ -568,8 +568,8 @@ mod tests {
let output =
parser.parse_chunk(r#"<minimax:tool_call><invoke name="get_weather">"#).unwrap();
assert!(output.normal_text.is_empty());
assert!(output.calls.is_empty());
assert!(output.normal_text().is_empty());
assert!(output.calls().is_empty());
}
#[test]
+72 -44
View File
@@ -107,7 +107,7 @@ impl MinimaxM3ToolParser {
fn apply_event(&mut self, event: MinimaxM3Event, output: &mut ToolParserOutput) -> Result<()> {
match event {
MinimaxM3Event::Text { len: consumed_len } => {
output.normal_text.push_str(&self.buffer[..consumed_len]);
output.push_text(&self.buffer[..consumed_len]);
}
MinimaxM3Event::ToolBlockStart => {
self.mode = MinimaxM3Mode::ToolBlock {
@@ -119,7 +119,7 @@ impl MinimaxM3ToolParser {
let arguments = serde_json::to_string(&arguments)
.map_err(|error| parsing_failed!("failed to serialize arguments: {}", error))?;
output.calls.push(ToolCallDelta {
output.push_call(ToolCallDelta {
tool_index: self.emitted_tool_count,
name: Some(name),
arguments,
@@ -158,7 +158,7 @@ impl ToolParser for MinimaxM3ToolParser {
let mut output = ToolParserOutput::default();
match self.mode {
MinimaxM3Mode::Text => {
output.normal_text.push_str(&self.buffer);
output.push_text(&self.buffer);
}
MinimaxM3Mode::ToolBlock { .. } => {
if !self.buffer.trim_start().is_empty() {
@@ -389,7 +389,7 @@ mod tests {
TOOL_CALL_END, TOOL_CALL_START, ToolParser,
};
use crate::tool::test_utils::{collect_stream, split_by_chars, test_tools};
use crate::tool::{Tool, ToolParserTestExt as _};
use crate::tool::{Tool, ToolParserEvent, ToolParserTestExt as _};
fn element(name: &str, body: &str) -> String {
format!("{ELEMENT_START}{name}>{body}{ELEMENT_END_START}{name}>")
@@ -510,8 +510,8 @@ mod tests {
let mut parser = MinimaxM3ToolParser::new(&m3_test_tools());
let output = parser.parse_complete("Hello, world!").unwrap();
assert_eq!(output.normal_text, "Hello, world!");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "Hello, world!");
assert!(output.calls().is_empty());
}
#[test]
@@ -524,11 +524,11 @@ mod tests {
)]))
.unwrap();
assert!(output.normal_text.is_empty());
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert!(output.normal_text().is_empty());
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({ "city": "Seattle", "days": 5 })
);
}
@@ -542,8 +542,8 @@ mod tests {
);
let output = parser.parse_complete(&output).unwrap();
assert_eq!(output.normal_text, "Let me check. ");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.normal_text(), "Let me check. ");
assert_eq!(output.calls().len(), 1);
}
#[test]
@@ -556,15 +556,15 @@ mod tests {
]))
.unwrap();
assert_eq!(output.calls.len(), 2);
assert_eq!(output.calls[0].tool_index, 0);
assert_eq!(output.calls[1].tool_index, 1);
assert_eq!(output.calls().len(), 2);
assert_eq!(output.calls()[0].tool_index, 0);
assert_eq!(output.calls()[1].tool_index, 1);
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({ "city": "Seattle" })
);
assert_eq!(
serde_json::from_str::<Value>(&output.calls[1].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[1].arguments).unwrap(),
json!({ "city": "NYC" })
);
}
@@ -585,7 +585,7 @@ mod tests {
.unwrap();
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({ "city": "Seattle" })
);
}
@@ -608,7 +608,7 @@ mod tests {
.unwrap();
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({
"whole": 5.0,
"flag": true,
@@ -627,7 +627,7 @@ mod tests {
.unwrap();
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({
"user_id": 42,
"urgent": true,
@@ -677,7 +677,7 @@ mod tests {
.unwrap();
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({
"shape": "\nrectangle\n",
"dimensions": { "width": 10, "height": 20 },
@@ -698,11 +698,11 @@ mod tests {
],
);
assert!(output.normal_text.is_empty());
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert!(output.normal_text().is_empty());
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({ "city": "Seattle" })
);
}
@@ -720,8 +720,36 @@ mod tests {
],
);
assert_eq!(output.normal_text, "Let me check. ");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.normal_text(), "Let me check. ");
assert_eq!(output.calls().len(), 1);
}
#[test]
fn minimax_m3_streaming_preserves_ordered_events() {
let mut parser = MinimaxM3ToolParser::new(&m3_test_tools());
let output = collect_stream(
&mut parser,
&[
"Let me check. ",
TOOL_CALL_START,
&invoke("get_weather", &element("city", "Seattle")),
TOOL_CALL_END,
],
);
assert_eq!(output.events.len(), 2);
assert_eq!(
output.events[0],
ToolParserEvent::Text("Let me check. ".to_string())
);
let ToolParserEvent::ToolCall(call) = &output.events[1] else {
panic!("expected tool-call event");
};
assert_eq!(call.name.as_deref(), Some("get_weather"));
assert_eq!(
serde_json::from_str::<Value>(&call.arguments).unwrap(),
json!({ "city": "Seattle" })
);
}
#[test]
@@ -729,8 +757,8 @@ mod tests {
let mut parser = MinimaxM3ToolParser::new(&m3_test_tools());
let output = collect_stream(&mut parser, &["Hello, ", "world!"]);
assert_eq!(output.normal_text, "Hello, world!");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "Hello, world!");
assert!(output.calls().is_empty());
}
#[test]
@@ -740,8 +768,8 @@ mod tests {
let mut parser = MinimaxM3ToolParser::new(&m3_test_tools());
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.calls.len(), 1);
assert!(output.normal_text.is_empty());
assert_eq!(output.calls().len(), 1);
assert!(output.normal_text().is_empty());
}
#[test]
@@ -754,9 +782,9 @@ mod tests {
let mut parser = MinimaxM3ToolParser::new(&m3_test_tools());
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.calls.len(), 2);
assert_eq!(output.calls[0].tool_index, 0);
assert_eq!(output.calls[1].tool_index, 1);
assert_eq!(output.calls().len(), 2);
assert_eq!(output.calls()[0].tool_index, 0);
assert_eq!(output.calls()[1].tool_index, 1);
}
#[test]
@@ -768,8 +796,8 @@ mod tests {
))
.unwrap();
assert!(output.normal_text.is_empty());
assert!(output.calls.is_empty());
assert!(output.normal_text().is_empty());
assert!(output.calls().is_empty());
}
#[test]
@@ -782,8 +810,8 @@ mod tests {
let mut parser = MinimaxM3ToolParser::new(&m3_test_tools());
let output = collect_stream(&mut parser, &chunks);
assert!(output.normal_text.is_empty());
assert_eq!(output.calls.len(), 1);
assert!(output.normal_text().is_empty());
assert_eq!(output.calls().len(), 1);
}
#[test]
@@ -804,8 +832,8 @@ mod tests {
parser.parse_chunk(TOOL_CALL_START).unwrap();
let output = parser.finish().unwrap();
assert!(output.normal_text.is_empty());
assert!(output.calls.is_empty());
assert!(output.normal_text().is_empty());
assert!(output.calls().is_empty());
}
#[test]
@@ -819,9 +847,9 @@ mod tests {
))
.unwrap();
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls().len(), 1);
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({ "city": "Seattle" })
);
}
@@ -863,7 +891,7 @@ mod tests {
let output = parser.parse_complete(&build_tool_block(&[("convert", body)])).unwrap();
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({
"payload": {
"child": "value",
@@ -887,7 +915,7 @@ mod tests {
let output = parser.parse_complete(&build_tool_block(&[("convert", body)])).unwrap();
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({
"payload": {
"$text": "child text",
+81 -21
View File
@@ -57,55 +57,115 @@ pub struct ToolCallDelta {
pub arguments: String,
}
/// One ordered event emitted while parsing assistant text.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ToolParserEvent {
/// Plain assistant text that is not part of any tool call.
Text(String),
/// A tool-call update extracted from assistant text.
ToolCall(ToolCallDelta),
}
/// Result of advancing tool parsing with one assistant-text input.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ToolParserOutput {
/// Plain assistant text that is not part of any tool call.
pub normal_text: String,
/// Tool-call updates extracted from this input.
pub calls: Vec<ToolCallDelta>,
/// Ordered parser events committed by this input.
pub events: Vec<ToolParserEvent>,
}
impl ToolParserOutput {
/// Append another parser output onto this one.
///
/// Note that this does not attempt to merge multiple deltas for the same
/// tool call into one complete item. Call `coalesce_calls()` after if
/// that behavior is desired.
pub fn append(&mut self, mut other: Self) {
self.normal_text.push_str(&other.normal_text);
self.calls.append(&mut other.calls);
/// Append one visible text event if `text` is non-empty.
pub fn push_text(&mut self, text: impl AsRef<str> + Into<String>) {
if text.as_ref().is_empty() {
return;
}
if let Some(ToolParserEvent::Text(last_text)) = self.events.last_mut() {
last_text.push_str(text.as_ref());
return;
}
self.events.push(ToolParserEvent::Text(text.into()));
}
/// Merge multiple deltas for the same tool call into one complete item.
/// Append one tool-call update event.
pub fn push_call(&mut self, call: ToolCallDelta) {
self.events.push(ToolParserEvent::ToolCall(call));
}
/// Return all plain assistant text committed by this output.
///
/// Texts before and after tool calls will be concatenated into a single string. To preserve
/// the original order of the text and tool-call events, directly access `events` instead.
pub fn normal_text(&self) -> String {
self.events
.iter()
.filter_map(|event| match event {
ToolParserEvent::Text(text) => Some(text.as_str()),
ToolParserEvent::ToolCall(_) => None,
})
.collect()
}
/// Return all tool-call updates committed by this output.
pub fn calls(&self) -> Vec<&ToolCallDelta> {
self.events
.iter()
.filter_map(|event| match event {
ToolParserEvent::Text(_) => None,
ToolParserEvent::ToolCall(call) => Some(call),
})
.collect()
}
/// Append another parser output onto this one.
///
/// Note that this keeps events exactly as they arrive. Call `coalesce()`
/// after if final text and tool-call fragments should be flattened.
pub fn append(&mut self, other: Self) {
for event in other.events {
match event {
ToolParserEvent::Text(text) => self.push_text(text),
ToolParserEvent::ToolCall(call) => self.push_call(call),
}
}
}
/// Flatten text and merge deltas for the same tool call.
///
/// All text events are concatenated into one leading text event. Tool-call
/// events follow that text event in first-seen tool index order, with
/// argument fragments for the same tool call concatenated together.
///
/// This is primarily used by the default `parse_complete()` implementation,
/// which delegates through the incremental parser lifecycle and then
/// needs to collapse streaming-style argument fragments into one final
/// tool call.
pub fn coalesce_calls(mut self) -> Self {
pub fn coalesce(self) -> Self {
let mut merged = BTreeMap::<usize, ToolCallDelta>::new();
let mut order = Vec::new();
let normal_text = self.normal_text();
for call in self.calls {
for call in self.calls() {
match merged.entry(call.tool_index) {
btree_map::Entry::Vacant(entry) => {
order.push(call.tool_index);
entry.insert(call);
entry.insert(call.clone());
}
btree_map::Entry::Occupied(mut entry) => {
let existing = entry.get_mut();
if existing.name.is_none() {
existing.name = call.name;
existing.name = call.name.clone();
}
existing.arguments.push_str(&call.arguments);
}
}
}
self.calls =
order.into_iter().filter_map(|tool_index| merged.remove(&tool_index)).collect();
self
let mut output = Self::default();
output.push_text(normal_text);
for call in order.into_iter().filter_map(|tool_index| merged.remove(&tool_index)) {
output.push_call(call);
}
output
}
}
@@ -183,7 +243,7 @@ impl<T: ToolParser + ?Sized> T {
pub fn parse_complete(&mut self, text: &str) -> Result<ToolParserOutput> {
let mut output = self.parse_chunk(text)?;
output.append(self.finish()?);
Ok(output.coalesce_calls())
Ok(output.coalesce())
}
}
+65 -65
View File
@@ -74,7 +74,7 @@ impl Qwen3CoderToolParser {
fn apply_event(&mut self, event: QwenCoderEvent, output: &mut ToolParserOutput) -> Result<()> {
match event {
QwenCoderEvent::Text { len: consumed_len } => {
output.normal_text.push_str(&self.buffer[..consumed_len]);
output.push_text(&self.buffer[..consumed_len]);
}
QwenCoderEvent::ToolCallStart => {
self.mode = QwenCoderMode::ToolCall {
@@ -87,7 +87,7 @@ impl Qwen3CoderToolParser {
let arguments = serde_json::to_string(&arguments)
.map_err(|error| parsing_failed!("failed to serialize arguments: {}", error))?;
output.calls.push(ToolCallDelta {
output.push_call(ToolCallDelta {
tool_index: self.emitted_tool_count,
name: Some(name),
arguments,
@@ -138,7 +138,7 @@ impl ToolParser for Qwen3CoderToolParser {
{
return Err(parsing_failed!("incomplete Qwen Coder tool call"));
}
output.normal_text.push_str(&self.buffer);
output.push_text(&self.buffer);
}
let _ = self.reset();
Ok(output)
@@ -268,8 +268,8 @@ mod tests {
let mut parser = Qwen3CoderToolParser::new(&test_tools());
let output = parser.parse_complete("Hello, world!").unwrap();
assert_eq!(output.normal_text, "Hello, world!");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "Hello, world!");
assert!(output.calls().is_empty());
}
#[test]
@@ -282,11 +282,11 @@ mod tests {
))
.unwrap();
assert!(output.normal_text.is_empty());
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert!(output.normal_text().is_empty());
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({
"location": "SF",
"date": "2026-04-29"
@@ -303,8 +303,8 @@ mod tests {
);
let output = parser.parse_complete(&output).unwrap();
assert_eq!(output.normal_text, "Thinking... ");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.normal_text(), "Thinking... ");
assert_eq!(output.calls().len(), 1);
}
#[test]
@@ -323,9 +323,9 @@ mod tests {
))
.unwrap();
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls().len(), 1);
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({
"whole": 5.0,
"flag": true,
@@ -341,10 +341,10 @@ mod tests {
let mut parser = Qwen3CoderToolParser::new(&test_tools());
let output = parser.parse_complete(&build_tool_call("get_weather", &[])).unwrap();
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({})
);
}
@@ -371,10 +371,10 @@ mod tests {
)
.unwrap();
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("calculate_area"));
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("calculate_area"));
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({
"shape": "rectangle",
"dimensions": { "width": 10, "height": 20 },
@@ -396,9 +396,9 @@ mod tests {
))
.unwrap();
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls().len(), 1);
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({
"payload": {
"nested": {
@@ -426,9 +426,9 @@ mod tests {
))
.unwrap();
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls().len(), 1);
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({
"html_content": r#"<div class="test"><span>Hello</span></div>"#,
"xml_snippet": r#"<root><child attr="value"/></root>"#,
@@ -452,9 +452,9 @@ mod tests {
))
.unwrap();
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls().len(), 1);
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({
"location": "杭州 &lt;/parameter&gt;&lt;/function&gt;&lt;/tool_call&gt;",
"date": "2026-05-08",
@@ -472,9 +472,9 @@ mod tests {
))
.unwrap();
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls().len(), 1);
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({
"data": { "key": "value", "count": 42 },
})
@@ -495,11 +495,11 @@ mod tests {
],
);
assert!(output.normal_text.is_empty());
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert!(output.normal_text().is_empty());
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({ "location": "SF" })
);
}
@@ -519,8 +519,8 @@ mod tests {
],
);
assert_eq!(output.normal_text, "Thinking... ");
assert_eq!(output.calls.len(), 1);
assert_eq!(output.normal_text(), "Thinking... ");
assert_eq!(output.calls().len(), 1);
}
#[test]
@@ -528,8 +528,8 @@ mod tests {
let mut parser = Qwen3CoderToolParser::new(&test_tools());
let output = collect_stream(&mut parser, &["Hello, ", "world!"]);
assert_eq!(output.normal_text, "Hello, world!");
assert!(output.calls.is_empty());
assert_eq!(output.normal_text(), "Hello, world!");
assert!(output.calls().is_empty());
}
#[test]
@@ -542,17 +542,17 @@ mod tests {
let mut parser = Qwen3CoderToolParser::new(&test_tools());
let output = collect_stream(&mut parser, &[&text]);
assert_eq!(output.calls.len(), 2);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls[1].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls[0].tool_index, 0);
assert_eq!(output.calls[1].tool_index, 1);
assert_eq!(output.calls().len(), 2);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls()[1].name.as_deref(), Some("get_weather"));
assert_eq!(output.calls()[0].tool_index, 0);
assert_eq!(output.calls()[1].tool_index, 1);
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({ "location": "SF" })
);
assert_eq!(
serde_json::from_str::<Value>(&output.calls[1].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[1].arguments).unwrap(),
json!({ "location": "NYC" })
);
}
@@ -569,16 +569,16 @@ mod tests {
let output = collect_stream(&mut parser, &chunks);
assert_eq!(
output.normal_text,
output.normal_text(),
"I'll check two cities.Between calls.Done."
);
assert_eq!(output.calls.len(), 2);
assert_eq!(output.calls().len(), 2);
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({ "city": "Dallas", "state": "TX" })
);
assert_eq!(
serde_json::from_str::<Value>(&output.calls[1].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[1].arguments).unwrap(),
json!({ "city": "Orlando", "state": "FL" })
);
}
@@ -590,9 +590,9 @@ mod tests {
let mut parser = Qwen3CoderToolParser::new(&test_tools());
let output = collect_stream(&mut parser, &chunks);
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls().len(), 1);
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({ "location": "SF" })
);
}
@@ -610,19 +610,19 @@ mod tests {
)
.unwrap();
assert!(output.normal_text.is_empty());
assert!(output.calls.is_empty());
assert!(output.normal_text().is_empty());
assert!(output.calls().is_empty());
let mut output = output;
output.append(parser.parse_chunk("_call>").unwrap());
output.append(parser.finish().unwrap());
let output = output.coalesce_calls();
let output = output.coalesce();
assert!(output.normal_text.is_empty());
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert!(output.normal_text().is_empty());
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({ "location": "SF" })
);
}
@@ -641,20 +641,20 @@ mod tests {
for chunk in chunks {
let chunk_output = parser.parse_chunk(chunk).unwrap();
assert!(chunk_output.normal_text.is_empty());
assert!(chunk_output.calls.is_empty());
assert!(chunk_output.normal_text().is_empty());
assert!(chunk_output.calls().is_empty());
output.append(chunk_output);
}
output.append(parser.parse_chunk(end_suffix).unwrap());
output.append(parser.finish().unwrap());
let output = output.coalesce_calls();
let output = output.coalesce();
assert!(output.normal_text.is_empty());
assert_eq!(output.calls.len(), 1);
assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
assert!(output.normal_text().is_empty());
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({ "location": long_location })
);
}
@@ -666,8 +666,8 @@ mod tests {
.parse_chunk("<tool_call>\n<function=get_weather>\n<parameter=location>SF</parameter>")
.unwrap();
assert!(output.normal_text.is_empty());
assert!(output.calls.is_empty());
assert!(output.normal_text().is_empty());
assert!(output.calls().is_empty());
}
#[test]
@@ -710,7 +710,7 @@ mod tests {
.unwrap();
assert_eq!(
serde_json::from_str::<Value>(&output.calls[0].arguments).unwrap(),
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
json!({ "location": "Hangzhou" })
);
}
+3 -3
View File
@@ -1,7 +1,7 @@
use serde_json::json;
use super::{ToolParser, ToolParserOutput};
use crate::tool::{Tool, ToolParserTestExt as _};
use crate::tool::Tool;
/// Build a reusable set of function tools for parser unit tests.
pub fn test_tools() -> Vec<Tool> {
@@ -87,10 +87,10 @@ pub fn test_tools() -> Vec<Tool> {
pub fn collect_stream<T: ToolParser + ?Sized>(parser: &mut T, chunks: &[&str]) -> ToolParserOutput {
let mut output = ToolParserOutput::default();
for chunk in chunks {
output.append(parser.parse_chunk(chunk).unwrap());
parser.parse_into(chunk, &mut output).unwrap();
}
output.append(parser.finish().unwrap());
output.coalesce_calls()
output.coalesce()
}
/// Split text into chunks containing at most `chunk_chars` Unicode scalar
+81 -21
View File
@@ -1,4 +1,4 @@
use super::{Result, Tool, ToolCallDelta, ToolParser, ToolParserOutput};
use super::{Result, Tool, ToolCallDelta, ToolParser, ToolParserEvent, ToolParserOutput};
use crate::tool::ToolParserTestExt as _;
struct DefaultParser;
@@ -31,6 +31,66 @@ fn tool_parser_does_not_preserve_special_tokens_by_default() {
assert!(!parser.preserve_special_tokens());
}
#[test]
fn tool_parser_output_coalesces_adjacent_text_events() {
let mut output = ToolParserOutput::default();
output.push_text("hello");
output.push_text(" ");
output.push_text("world");
output.push_call(ToolCallDelta {
tool_index: 0,
name: Some("lookup".to_string()),
arguments: "{}".to_string(),
});
output.push_text("!");
assert_eq!(
output.events,
vec![
ToolParserEvent::Text("hello world".to_string()),
ToolParserEvent::ToolCall(ToolCallDelta {
tool_index: 0,
name: Some("lookup".to_string()),
arguments: "{}".to_string(),
}),
ToolParserEvent::Text("!".to_string()),
]
);
}
#[test]
fn tool_parser_output_append_coalesces_adjacent_text_events() {
let mut output = ToolParserOutput::default();
output.push_text("hello");
let mut other = ToolParserOutput::default();
other.push_text(" ");
other.push_text("world");
output.append(other);
let mut after_call = ToolParserOutput::default();
after_call.push_call(ToolCallDelta {
tool_index: 0,
name: Some("lookup".to_string()),
arguments: "{}".to_string(),
});
after_call.push_text("!");
output.append(after_call);
assert_eq!(
output.events,
vec![
ToolParserEvent::Text("hello world".to_string()),
ToolParserEvent::ToolCall(ToolCallDelta {
tool_index: 0,
name: Some("lookup".to_string()),
arguments: "{}".to_string(),
}),
ToolParserEvent::Text("!".to_string()),
]
);
}
#[test]
fn default_parse_complete_delegates_through_parse_chunk_and_finish() {
struct StreamingParser;
@@ -44,8 +104,8 @@ fn default_parse_complete_delegates_through_parse_chunk_and_finish() {
}
fn parse_into(&mut self, _chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
output.normal_text.push_str("prefix ");
output.calls.extend([
output.push_text("prefix ");
for call in [
ToolCallDelta {
tool_index: 0,
name: Some("weather".to_string()),
@@ -61,26 +121,26 @@ fn default_parse_complete_delegates_through_parse_chunk_and_finish() {
name: Some("time".to_string()),
arguments: "{\"timezone\":".to_string(),
},
]);
] {
output.push_call(call);
}
Ok(())
}
fn finish(&mut self) -> Result<ToolParserOutput> {
Ok(ToolParserOutput {
normal_text: "suffix".to_string(),
calls: vec![
ToolCallDelta {
tool_index: 0,
name: None,
arguments: "}".to_string(),
},
ToolCallDelta {
tool_index: 1,
name: None,
arguments: "\"UTC\"}".to_string(),
},
],
})
let mut output = ToolParserOutput::default();
output.push_text("suffix");
output.push_call(ToolCallDelta {
tool_index: 0,
name: None,
arguments: "}".to_string(),
});
output.push_call(ToolCallDelta {
tool_index: 1,
name: None,
arguments: "\"UTC\"}".to_string(),
});
Ok(output)
}
fn reset(&mut self) -> String {
@@ -90,9 +150,9 @@ fn default_parse_complete_delegates_through_parse_chunk_and_finish() {
let mut parser = StreamingParser;
let output = parser.parse_complete("ignored").unwrap();
assert_eq!(output.normal_text, "prefix suffix");
assert_eq!(output.normal_text(), "prefix suffix");
assert_eq!(
output.calls,
output.calls().into_iter().cloned().collect::<Vec<_>>(),
vec![
ToolCallDelta {
tool_index: 0,
+3 -3
View File
@@ -32,7 +32,7 @@ impl CombinedParser {
fn parse_tool(&mut self, content: &str, output: &mut UnifiedParserOutput) -> Result<()> {
let Some(tool) = self.tool.as_mut() else {
output.push_text(content.to_string());
output.push_text(content);
return Ok(());
};
@@ -228,7 +228,7 @@ mod tests {
chunk: &str,
output: &mut crate::tool::ToolParserOutput,
) -> crate::tool::Result<()> {
output.normal_text.push_str(chunk);
output.push_text(chunk);
Ok(())
}
@@ -256,7 +256,7 @@ mod tests {
_chunk: &str,
output: &mut crate::tool::ToolParserOutput,
) -> crate::tool::Result<()> {
output.normal_text.push_str("committed");
output.push_text("committed");
Err(crate::tool::ToolParserError::ParsingFailed {
message: "synthetic failure".to_string(),
})
+98 -12
View File
@@ -8,7 +8,9 @@ use vllm_tokenizer::DynTokenizer;
pub use combined::CombinedParser;
use crate::reasoning::ReasoningError;
use crate::tool::{StructuralTagModel, Tool, ToolCallDelta, ToolParserError, ToolParserOutput};
use crate::tool::{
StructuralTagModel, Tool, ToolCallDelta, ToolParserError, ToolParserEvent, ToolParserOutput,
};
/// Result alias for unified parser operations.
pub type Result<T> = std::result::Result<T, UnifiedParserError>;
@@ -33,31 +35,115 @@ pub struct UnifiedParserOutput {
impl UnifiedParserOutput {
/// Append one visible text event if `delta` is non-empty.
pub fn push_text(&mut self, delta: String) {
if delta.is_empty() {
pub fn push_text(&mut self, delta: impl AsRef<str> + Into<String>) {
if delta.as_ref().is_empty() {
return;
}
self.events.push(UnifiedParserEvent::Text(delta));
if let Some(UnifiedParserEvent::Text(last_text)) = self.events.last_mut() {
last_text.push_str(delta.as_ref());
return;
}
self.events.push(UnifiedParserEvent::Text(delta.into()));
}
/// Append one reasoning text event if `delta` is non-empty.
pub fn push_reasoning(&mut self, delta: String) {
if delta.is_empty() {
pub fn push_reasoning(&mut self, delta: impl AsRef<str> + Into<String>) {
if delta.as_ref().is_empty() {
return;
}
self.events.push(UnifiedParserEvent::Reasoning(delta));
if let Some(UnifiedParserEvent::Reasoning(last_text)) = self.events.last_mut() {
last_text.push_str(delta.as_ref());
return;
}
self.events.push(UnifiedParserEvent::Reasoning(delta.into()));
}
/// Append one tool-call event.
pub fn push_call(&mut self, call: ToolCallDelta) {
self.events.push(UnifiedParserEvent::ToolCall(call));
}
/// Append parsed tool parser output as unified events.
pub fn append_tool_output(&mut self, output: ToolParserOutput) {
// TODO: make ToolParserOutput carry ordered events and remove this text-first flattening.
self.push_text(output.normal_text);
self.events.extend(output.calls.into_iter().map(UnifiedParserEvent::ToolCall));
for event in output.events {
match event {
ToolParserEvent::Text(text) => self.push_text(text),
ToolParserEvent::ToolCall(call) => self.push_call(call),
}
}
}
/// Append another parser output onto this one.
pub fn append(&mut self, mut other: Self) {
self.events.append(&mut other.events);
pub fn append(&mut self, other: Self) {
for event in other.events {
match event {
UnifiedParserEvent::Text(text) => self.push_text(text),
UnifiedParserEvent::Reasoning(reasoning) => self.push_reasoning(reasoning),
UnifiedParserEvent::ToolCall(call) => self.push_call(call),
}
}
}
}
#[cfg(test)]
mod tests {
use super::{UnifiedParserEvent, UnifiedParserOutput};
use crate::tool::ToolCallDelta;
#[test]
fn unified_parser_output_coalesces_adjacent_text_events() {
let mut output = UnifiedParserOutput::default();
output.push_text("hello");
output.push_text(" ");
output.push_text("world");
output.push_reasoning("think");
output.push_reasoning("ing");
output.push_call(ToolCallDelta {
tool_index: 0,
name: Some("lookup".to_string()),
arguments: "{}".to_string(),
});
output.push_text("!");
assert_eq!(
output.events,
vec![
UnifiedParserEvent::Text("hello world".to_string()),
UnifiedParserEvent::Reasoning("thinking".to_string()),
UnifiedParserEvent::ToolCall(ToolCallDelta {
tool_index: 0,
name: Some("lookup".to_string()),
arguments: "{}".to_string(),
}),
UnifiedParserEvent::Text("!".to_string()),
]
);
}
#[test]
fn unified_parser_output_append_coalesces_adjacent_events() {
let mut output = UnifiedParserOutput::default();
output.push_text("hello");
let mut other = UnifiedParserOutput::default();
other.push_text(" ");
other.push_text("world");
other.push_reasoning("think");
output.append(other);
let mut after_reasoning = UnifiedParserOutput::default();
after_reasoning.push_reasoning("ing");
after_reasoning.push_text("!");
output.append(after_reasoning);
assert_eq!(
output.events,
vec![
UnifiedParserEvent::Text("hello world".to_string()),
UnifiedParserEvent::Reasoning("thinking".to_string()),
UnifiedParserEvent::Text("!".to_string()),
]
);
}
}
+1 -1
View File
@@ -171,7 +171,7 @@ def test_rust_tool_parser_extension_typed_api() -> None:
parser.parse_into(build_tool_call(), output)
output.append(parser.finish())
output = output.coalesce_calls()
output = output.coalesce()
assert parser.preserve_special_tokens()
assert output.normal_text == ""
+1 -1
View File
@@ -224,7 +224,7 @@ class RustToolParser(ToolParser):
"Error parsing %s tool call output.", self.rust_parser_name
)
return None
return output.coalesce_calls(), tool_call_ids
return output.coalesce(), tool_call_ids
def extract_tool_calls(
self,