[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:
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 </|DSML|parameter></|DSML|invoke></|DSML|function_calls>",
|
||||
"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!(
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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!({})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 </arg_value></tool_call>",
|
||||
"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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}",
|
||||
},
|
||||
),
|
||||
],
|
||||
}
|
||||
"#]]
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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\"}",
|
||||
},
|
||||
),
|
||||
],
|
||||
}
|
||||
"#]]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}",
|
||||
},
|
||||
),
|
||||
],
|
||||
}
|
||||
"#]]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 & Jerry <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 & Jerry <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 </parameter></invoke></minimax:tool_call>",
|
||||
"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]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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": "杭州 </parameter></function></tool_call>",
|
||||
"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" })
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
|
||||
@@ -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()),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 == ""
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user