const OPEN: &str = "<tool_call>";
const CLOSE: &str = "</tool_call>";
fn partial_marker_len(s: &str, pat: &str) -> usize {
let max = pat.len().saturating_sub(1).min(s.len());
(1..=max)
.rev()
.find(|&n| {
s.is_char_boundary(s.len() - n)
&& pat.as_bytes().starts_with(&s.as_bytes()[s.len() - n..])
})
.unwrap_or(0)
}
#[derive(Default)]
pub struct ToolMarkupFilter {
pending: String,
inside: bool,
captured: Vec<String>,
}
impl ToolMarkupFilter {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, chunk: &str) -> String {
self.pending.push_str(chunk);
let mut out = String::new();
loop {
if self.inside {
let Some(end) = self.pending.find(CLOSE) else {
return out;
};
let payload: String = self.pending[..end].to_string();
self.captured.push(payload);
self.pending = self.pending[end + CLOSE.len()..].to_string();
self.inside = false;
continue;
}
match self.pending.find(OPEN) {
Some(start) => {
out.push_str(&self.pending[..start]);
self.pending = self.pending[start + OPEN.len()..].to_string();
self.inside = true;
}
None => {
let hold = partial_marker_len(&self.pending, OPEN);
let cut = self.pending.len() - hold;
out.push_str(&self.pending[..cut]);
self.pending = self.pending[cut..].to_string();
return out;
}
}
}
}
pub fn finish(&mut self) -> String {
let mut out = String::new();
if self.inside {
out.push_str(OPEN);
self.inside = false;
}
out.push_str(&std::mem::take(&mut self.pending));
out
}
pub fn captured(&self) -> &[String] {
&self.captured
}
}
pub fn split_tool_markup(text: &str) -> (String, Vec<String>) {
let mut f = ToolMarkupFilter::new();
let mut out = f.push(text);
out.push_str(&f.finish());
(out, f.captured)
}
pub fn markup_to_tool_calls(payloads: &[String], start_index: usize) -> Vec<serde_json::Value> {
let mut out = Vec::new();
for payload in payloads {
let Ok(v) = serde_json::from_str::<serde_json::Value>(payload.trim()) else {
continue;
};
let Some(name) = v.get("name").and_then(|n| n.as_str()) else {
continue;
};
let arguments = match v.get("arguments").or_else(|| v.get("parameters")) {
Some(serde_json::Value::String(s)) => s.clone(),
Some(other) => other.to_string(),
None => "{}".to_string(),
};
out.push(serde_json::json!({
"index": start_index + out.len(),
"id": new_call_id(),
"type": "function",
"function": { "name": name, "arguments": arguments },
}));
}
out
}
pub fn new_call_id() -> String {
format!("call_{}", crate::admin::random_token(12))
}
pub fn normalize_delta_tool_calls(
raw: &[serde_json::Value],
seen: &mut std::collections::HashSet<u64>,
) -> Vec<serde_json::Value> {
let mut out = Vec::new();
for (i, tc) in raw.iter().enumerate() {
let index = tc.get("index").and_then(|v| v.as_u64()).unwrap_or(i as u64);
let mut entry = serde_json::Map::new();
entry.insert("index".into(), serde_json::json!(index));
let first = seen.insert(index);
if let Some(id) = tc
.get("id")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
{
entry.insert("id".into(), serde_json::json!(id));
} else if first {
entry.insert("id".into(), serde_json::json!(new_call_id()));
}
if first {
let kind = tc
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("function");
entry.insert("type".into(), serde_json::json!(kind));
}
if let Some(f) = tc.get("function") {
let mut func = serde_json::Map::new();
if let Some(name) = f.get("name").and_then(|v| v.as_str()) {
func.insert("name".into(), serde_json::json!(name));
} else if first {
func.insert("name".into(), serde_json::json!(""));
}
match f.get("arguments") {
Some(serde_json::Value::String(s)) => {
func.insert("arguments".into(), serde_json::json!(s));
}
Some(serde_json::Value::Null) | None => {
if first {
func.insert("arguments".into(), serde_json::json!(""));
}
}
Some(other) => {
func.insert("arguments".into(), serde_json::json!(other.to_string()));
}
}
entry.insert("function".into(), serde_json::Value::Object(func));
}
out.push(serde_json::Value::Object(entry));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const CALL: &str = r#"<tool_call>
{"name": "get_weather", "arguments": {"city": "Paris"}}
</tool_call>"#;
#[test]
fn markup_is_stripped_from_finished_text() {
let (clean, payloads) = split_tool_markup(&format!("Sure!{CALL}"));
assert_eq!(clean, "Sure!");
assert_eq!(payloads.len(), 1);
}
#[test]
fn markup_split_across_stream_chunks_never_leaks() {
let mut f = ToolMarkupFilter::new();
let mut seen = String::new();
for chunk in [
"Let me check. <tool",
"_call>",
"{\"name\":\"get_weather\",",
"\"arguments\":{\"city\":\"Paris\"}}",
"</tool",
"_call>",
" done",
] {
seen.push_str(&f.push(chunk));
}
seen.push_str(&f.finish());
assert_eq!(seen, "Let me check. done");
assert!(!seen.contains("tool_call"));
assert_eq!(f.captured().len(), 1);
}
#[test]
fn captured_markup_becomes_a_real_tool_call() {
let (_, payloads) = split_tool_markup(CALL);
let calls = markup_to_tool_calls(&payloads, 0);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0]["type"], "function");
assert_eq!(calls[0]["index"], 0);
assert_eq!(calls[0]["function"]["name"], "get_weather");
assert_eq!(
calls[0]["function"]["arguments"].as_str().unwrap(),
r#"{"city":"Paris"}"#
);
assert!(calls[0]["id"].as_str().unwrap().starts_with("call_"));
}
#[test]
fn a_stray_marker_does_not_become_a_call() {
let calls = markup_to_tool_calls(&["not json at all".into(), "{\"no_name\":1}".into()], 0);
assert!(calls.is_empty());
}
#[test]
fn an_unterminated_block_is_given_back_as_text() {
let mut f = ToolMarkupFilter::new();
let mut out = f.push("thinking <tool_call>{\"name\":\"x\"");
out.push_str(&f.finish());
assert_eq!(out, "thinking <tool_call>{\"name\":\"x\"");
assert!(f.captured().is_empty());
}
#[test]
fn delta_tool_calls_get_identity_once_and_object_arguments_become_strings() {
let mut seen = std::collections::HashSet::new();
let opening = normalize_delta_tool_calls(
&[serde_json::json!({"index": 0, "function": {"name": "f", "arguments": {"a": 1}}})],
&mut seen,
);
assert_eq!(opening[0]["type"], "function");
assert!(opening[0]["id"].as_str().unwrap().starts_with("call_"));
assert_eq!(opening[0]["function"]["arguments"], r#"{"a":1}"#);
let cont = normalize_delta_tool_calls(
&[serde_json::json!({"index": 0, "function": {"arguments": "\"}"}})],
&mut seen,
);
assert!(cont[0].get("id").is_none());
assert!(cont[0].get("type").is_none());
}
}