use adk_core::Part;
pub fn contains_tool_call_tag(text: &str) -> bool {
text.contains("<tool_call>")
|| text.contains("<|tool_call>")
|| text.contains("<|python_tag|>")
|| text.contains("[TOOL_CALLS]")
|| text.contains("<|tool▁call")
|| text.contains("<|action_start|>")
|| (text.contains("```json") && text.contains("\"name\""))
}
pub fn parse_text_tool_calls(text: &str) -> Option<Vec<Part>> {
if !contains_tool_call_tag(text) {
return None;
}
let mut parts = Vec::new();
if let Some(parsed) = parse_qwen_format(text, &mut parts) {
return Some(parsed);
}
if let Some(parsed) = parse_llama_format(text, &mut parts) {
return Some(parsed);
}
if let Some(parsed) = parse_mistral_nemo_format(text, &mut parts) {
return Some(parsed);
}
if let Some(parsed) = parse_deepseek_format(text) {
return Some(parsed);
}
if let Some(parsed) = parse_gemma4_format(text) {
return Some(parsed);
}
if let Some(parsed) = parse_action_tag_format(text) {
return Some(parsed);
}
None
}
fn parse_qwen_format(text: &str, _parts: &mut Vec<Part>) -> Option<Vec<Part>> {
let mut result = Vec::new();
let mut remaining = text;
loop {
let start = remaining.find("<tool_call>")?;
let before = remaining[..start].trim();
if !before.is_empty() {
result.push(Part::Text { text: before.to_string() });
}
let after_open = &remaining[start + "<tool_call>".len()..];
let end = after_open.find("</tool_call>")?;
let inner = after_open[..end].trim();
if let Some(part) = parse_json_tool_call(inner) {
result.push(part);
}
else if let Some(part) = parse_function_tag(inner) {
result.push(part);
} else {
result.push(Part::Text {
text: remaining[start..start + "<tool_call>".len() + end + "</tool_call>".len()]
.to_string(),
});
}
remaining = &after_open[end + "</tool_call>".len()..];
if remaining.trim().is_empty() || !remaining.contains("<tool_call>") {
let trailing = remaining.trim();
if !trailing.is_empty() {
result.push(Part::Text { text: trailing.to_string() });
}
break;
}
}
if result.is_empty() { None } else { Some(result) }
}
fn parse_function_tag(inner: &str) -> Option<Part> {
let func_start = inner.find("<function=")?;
let after_eq = &inner[func_start + "<function=".len()..];
let name_end = after_eq.find('>')?;
let name = after_eq[..name_end].trim().to_string();
let body_start = name_end + 1;
let func_end = after_eq.find("</function>")?;
let body = after_eq[body_start..func_end].trim();
let args = if body.is_empty() {
serde_json::json!({})
} else {
serde_json::from_str(body).unwrap_or_else(|_| serde_json::json!({}))
};
Some(Part::FunctionCall { name, args, id: None, thought_signature: None })
}
fn parse_json_tool_call(json_str: &str) -> Option<Part> {
let value: serde_json::Value = serde_json::from_str(json_str).ok()?;
let obj = value.as_object()?;
let name =
obj.get("name").or_else(|| obj.get("function")).and_then(|v| v.as_str())?.to_string();
let args = obj
.get("arguments")
.or_else(|| obj.get("parameters"))
.cloned()
.unwrap_or(serde_json::json!({}));
Some(Part::FunctionCall { name, args, id: None, thought_signature: None })
}
fn parse_llama_format(text: &str, _parts: &mut Vec<Part>) -> Option<Vec<Part>> {
let tag = "<|python_tag|>";
let start = text.find(tag)?;
let mut result = Vec::new();
let before = text[..start].trim();
if !before.is_empty() {
result.push(Part::Text { text: before.to_string() });
}
let json_str = text[start + tag.len()..].trim();
if let Some(part) = parse_json_tool_call(json_str) {
result.push(part);
} else {
return None;
}
Some(result)
}
fn parse_mistral_nemo_format(text: &str, _parts: &mut Vec<Part>) -> Option<Vec<Part>> {
let tag = "[TOOL_CALLS]";
let start = text.find(tag)?;
let mut result = Vec::new();
let before = text[..start].trim();
if !before.is_empty() {
result.push(Part::Text { text: before.to_string() });
}
let json_str = text[start + tag.len()..].trim();
let arr: Vec<serde_json::Value> = serde_json::from_str(json_str).ok()?;
for item in &arr {
let obj = item.as_object()?;
let name = obj.get("name").and_then(|v| v.as_str())?.to_string();
let args = obj
.get("arguments")
.or_else(|| obj.get("parameters"))
.cloned()
.unwrap_or(serde_json::json!({}));
result.push(Part::FunctionCall { name, args, id: None, thought_signature: None });
}
if result.is_empty() { None } else { Some(result) }
}
fn parse_deepseek_format(text: &str) -> Option<Vec<Part>> {
let fence_start = text.find("```json")?;
let json_start = fence_start + "```json".len();
let after_fence = &text[json_start..];
let fence_end = after_fence.find("```")?;
let json_str = after_fence[..fence_end].trim();
let mut result = Vec::new();
let before = text[..fence_start].trim();
if !before.is_empty() {
result.push(Part::Text { text: before.to_string() });
}
if let Some(part) = parse_json_tool_call(json_str) {
result.push(part);
} else if let Ok(arr) = serde_json::from_str::<Vec<serde_json::Value>>(json_str) {
for item in &arr {
if let Some(obj) = item.as_object() {
let name = obj
.get("name")
.or_else(|| obj.get("function"))
.and_then(|v| v.as_str())?
.to_string();
let args = obj
.get("arguments")
.or_else(|| obj.get("parameters"))
.cloned()
.unwrap_or(serde_json::json!({}));
result.push(Part::FunctionCall { name, args, id: None, thought_signature: None });
}
}
} else {
return None;
}
if result.is_empty() { None } else { Some(result) }
}
fn parse_gemma4_format(text: &str) -> Option<Vec<Part>> {
let mut result = Vec::new();
let mut remaining = text;
loop {
let start = remaining.find("<|tool_call>")?;
let before = remaining[..start].trim();
if !before.is_empty() {
result.push(Part::Text { text: before.to_string() });
}
let after_open = &remaining[start + "<|tool_call>".len()..];
let end = after_open.find("<tool_call|>")?;
let inner = after_open[..end].trim();
if let Some(call_body) = inner.strip_prefix("call:") {
let brace_start = call_body.find('{');
let name = if let Some(bs) = brace_start {
call_body[..bs].trim().to_string()
} else {
call_body.trim().to_string()
};
let args = if let Some(bs) = brace_start {
let args_raw = &call_body[bs..];
let json_str = args_raw.replace("<|\"|>", "\"");
serde_json::from_str(&json_str).unwrap_or(serde_json::json!({}))
} else {
serde_json::json!({})
};
result.push(Part::FunctionCall { name, args, id: None, thought_signature: None });
}
remaining = &after_open[end + "<tool_call|>".len()..];
if remaining.trim().is_empty() || !remaining.contains("<|tool_call>") {
let trailing = remaining.trim();
if !trailing.is_empty() {
result.push(Part::Text { text: trailing.to_string() });
}
break;
}
}
if result.is_empty() { None } else { Some(result) }
}
fn parse_action_tag_format(text: &str) -> Option<Vec<Part>> {
let start_tag = "<|action_start|>";
let end_tag = "<|action_end|>";
let start = text.find(start_tag)?;
let mut result = Vec::new();
let before = text[..start].trim();
if !before.is_empty() {
result.push(Part::Text { text: before.to_string() });
}
let after_open = &text[start + start_tag.len()..];
let end = after_open.find(end_tag)?;
let inner = after_open[..end].trim();
if let Some(part) = parse_json_tool_call(inner) {
result.push(part);
} else {
return None;
}
let trailing = after_open[end + end_tag.len()..].trim();
if !trailing.is_empty() {
result.push(Part::Text { text: trailing.to_string() });
}
Some(result)
}
const TOOL_CALL_PREFIXES: &[&str] = &[
"<tool_call",
"<|tool_call>",
"<|python_tag|",
"[TOOL_CALLS]",
"<|action_start|>",
"<\u{ff5c}\u{2581}tool", ];
const MAX_BUFFER_SIZE: usize = 4096;
pub struct ToolCallBuffer {
buffer: String,
buffering: bool,
}
pub enum BufferAction {
Emit(Vec<Part>),
Buffering,
}
impl ToolCallBuffer {
pub fn new() -> Self {
Self { buffer: String::new(), buffering: false }
}
pub fn push(&mut self, text: &str) -> BufferAction {
self.buffer.push_str(text);
if self.buffering {
if self.has_complete_tool_call() {
return self.try_parse_and_emit();
}
if self.buffer.len() > MAX_BUFFER_SIZE {
return self.flush_as_emit();
}
BufferAction::Buffering
} else {
if self.starts_tool_call_prefix() {
self.buffering = true;
if self.has_complete_tool_call() {
return self.try_parse_and_emit();
}
BufferAction::Buffering
} else if self.has_partial_prefix() {
self.buffering = true;
BufferAction::Buffering
} else {
self.flush_as_emit()
}
}
}
pub fn flush(&mut self) -> Vec<Part> {
if self.buffer.is_empty() {
return Vec::new();
}
if let Some(parts) = parse_text_tool_calls(&self.buffer) {
self.buffer.clear();
self.buffering = false;
return parts;
}
let text = std::mem::take(&mut self.buffer);
self.buffering = false;
if text.trim().is_empty() { Vec::new() } else { vec![Part::Text { text }] }
}
fn starts_tool_call_prefix(&self) -> bool {
TOOL_CALL_PREFIXES.iter().any(|prefix| self.buffer.contains(prefix))
}
fn has_partial_prefix(&self) -> bool {
let buf = &self.buffer;
for prefix in TOOL_CALL_PREFIXES {
let prefix_chars: Vec<char> = prefix.chars().collect();
for i in 1..prefix_chars.len() {
let partial: String = prefix_chars[..i].iter().collect();
if buf.ends_with(&partial) {
return true;
}
}
}
false
}
fn has_complete_tool_call(&self) -> bool {
(self.buffer.contains("<tool_call>") && self.buffer.contains("</tool_call>"))
|| (self.buffer.contains("<|tool_call>") && self.buffer.contains("<tool_call|>"))
|| (self.buffer.contains("<|python_tag|>")
&& self.buffer.contains('\n')
&& self.buffer.len() > "<|python_tag|>".len() + 5)
|| (self.buffer.contains("[TOOL_CALLS]")
&& self.buffer.contains(']')
&& self.buffer.rfind(']') > self.buffer.find("[TOOL_CALLS]").map(|i| i + 12))
|| (self.buffer.contains("```json") && self.buffer.matches("```").count() >= 2)
|| (self.buffer.contains("<|action_start|>") && self.buffer.contains("<|action_end|>"))
}
fn try_parse_and_emit(&mut self) -> BufferAction {
if let Some(parts) = parse_text_tool_calls(&self.buffer) {
self.buffer.clear();
self.buffering = false;
BufferAction::Emit(parts)
} else {
self.flush_as_emit()
}
}
fn flush_as_emit(&mut self) -> BufferAction {
let text = std::mem::take(&mut self.buffer);
self.buffering = false;
if text.trim().is_empty() {
BufferAction::Emit(Vec::new())
} else {
BufferAction::Emit(vec![Part::Text { text }])
}
}
}
impl Default for ToolCallBuffer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_qwen_json_format() {
let text =
r#"<tool_call>{"name": "get_weather", "arguments": {"city": "Tokyo"}}</tool_call>"#;
let parts = parse_text_tool_calls(text).unwrap();
assert_eq!(parts.len(), 1);
match &parts[0] {
Part::FunctionCall { name, args, .. } => {
assert_eq!(name, "get_weather");
assert_eq!(args["city"], "Tokyo");
}
_ => panic!("expected FunctionCall"),
}
}
#[test]
fn test_qwen_function_tag_format() {
let text = r#"<tool_call><function=screenshot></function></tool_call>"#;
let parts = parse_text_tool_calls(text).unwrap();
assert_eq!(parts.len(), 1);
match &parts[0] {
Part::FunctionCall { name, args, .. } => {
assert_eq!(name, "screenshot");
assert_eq!(*args, serde_json::json!({}));
}
_ => panic!("expected FunctionCall"),
}
}
#[test]
fn test_qwen_function_tag_with_args() {
let text = r#"<tool_call><function=get_weather>{"city": "Tokyo"}</function></tool_call>"#;
let parts = parse_text_tool_calls(text).unwrap();
assert_eq!(parts.len(), 1);
match &parts[0] {
Part::FunctionCall { name, args, .. } => {
assert_eq!(name, "get_weather");
assert_eq!(args["city"], "Tokyo");
}
_ => panic!("expected FunctionCall"),
}
}
#[test]
fn test_text_before_tool_call() {
let text = r#"Let me check that for you.
<tool_call>{"name": "search", "arguments": {"query": "rust"}}</tool_call>"#;
let parts = parse_text_tool_calls(text).unwrap();
assert_eq!(parts.len(), 2);
assert!(matches!(&parts[0], Part::Text { text } if text.contains("check that")));
assert!(matches!(&parts[1], Part::FunctionCall { name, .. } if name == "search"));
}
#[test]
fn test_multiple_tool_calls() {
let text = r#"<tool_call>{"name": "a", "arguments": {}}</tool_call>
<tool_call>{"name": "b", "arguments": {"x": 1}}</tool_call>"#;
let parts = parse_text_tool_calls(text).unwrap();
assert_eq!(parts.len(), 2);
assert!(matches!(&parts[0], Part::FunctionCall { name, .. } if name == "a"));
assert!(matches!(&parts[1], Part::FunctionCall { name, .. } if name == "b"));
}
#[test]
fn test_llama_format() {
let text = r#"<|python_tag|>{"name": "get_weather", "parameters": {"city": "NYC"}}"#;
let parts = parse_text_tool_calls(text).unwrap();
assert_eq!(parts.len(), 1);
match &parts[0] {
Part::FunctionCall { name, args, .. } => {
assert_eq!(name, "get_weather");
assert_eq!(args["city"], "NYC");
}
_ => panic!("expected FunctionCall"),
}
}
#[test]
fn test_mistral_nemo_format() {
let text = r#"[TOOL_CALLS][{"name": "search", "arguments": {"q": "rust"}}]"#;
let parts = parse_text_tool_calls(text).unwrap();
assert_eq!(parts.len(), 1);
match &parts[0] {
Part::FunctionCall { name, args, .. } => {
assert_eq!(name, "search");
assert_eq!(args["q"], "rust");
}
_ => panic!("expected FunctionCall"),
}
}
#[test]
fn test_no_tool_call_returns_none() {
assert!(parse_text_tool_calls("Hello, how can I help?").is_none());
assert!(parse_text_tool_calls("").is_none());
}
#[test]
fn test_contains_tool_call_tag() {
assert!(contains_tool_call_tag("<tool_call>"));
assert!(contains_tool_call_tag("text <tool_call> more"));
assert!(contains_tool_call_tag("<|python_tag|>"));
assert!(contains_tool_call_tag("[TOOL_CALLS]"));
assert!(!contains_tool_call_tag("normal text"));
}
#[test]
fn test_buffer_normal_text_emits_immediately() {
let mut buf = ToolCallBuffer::new();
match buf.push("Hello world") {
BufferAction::Emit(parts) => {
assert_eq!(parts.len(), 1);
assert!(matches!(&parts[0], Part::Text { text } if text == "Hello world"));
}
BufferAction::Buffering => panic!("should emit immediately"),
}
}
#[test]
fn test_buffer_complete_tool_call_in_one_chunk() {
let mut buf = ToolCallBuffer::new();
let text = r#"<tool_call>{"name": "search", "arguments": {"q": "rust"}}</tool_call>"#;
match buf.push(text) {
BufferAction::Emit(parts) => {
assert_eq!(parts.len(), 1);
assert!(matches!(&parts[0], Part::FunctionCall { name, .. } if name == "search"));
}
BufferAction::Buffering => panic!("should emit parsed tool call"),
}
}
#[test]
fn test_buffer_tool_call_split_across_chunks() {
let mut buf = ToolCallBuffer::new();
assert!(matches!(buf.push("<tool_call>"), BufferAction::Buffering));
assert!(matches!(
buf.push(r#"{"name": "get_weather", "arguments": {"city": "Tokyo"}}"#),
BufferAction::Buffering
));
match buf.push("</tool_call>") {
BufferAction::Emit(parts) => {
assert_eq!(parts.len(), 1);
assert!(
matches!(&parts[0], Part::FunctionCall { name, .. } if name == "get_weather")
);
}
BufferAction::Buffering => panic!("should emit after closing tag"),
}
}
#[test]
fn test_buffer_text_then_tool_call() {
let mut buf = ToolCallBuffer::new();
match buf.push("Let me check. ") {
BufferAction::Emit(parts) => {
assert_eq!(parts.len(), 1);
assert!(matches!(&parts[0], Part::Text { .. }));
}
BufferAction::Buffering => panic!("should emit text"),
}
let tc = r#"<tool_call>{"name": "search", "arguments": {}}</tool_call>"#;
match buf.push(tc) {
BufferAction::Emit(parts) => {
assert_eq!(parts.len(), 1);
assert!(matches!(&parts[0], Part::FunctionCall { name, .. } if name == "search"));
}
BufferAction::Buffering => panic!("should emit tool call"),
}
}
#[test]
fn test_buffer_flush_incomplete_as_text() {
let mut buf = ToolCallBuffer::new();
assert!(matches!(buf.push("<tool_call>partial"), BufferAction::Buffering));
let parts = buf.flush();
assert_eq!(parts.len(), 1);
assert!(matches!(&parts[0], Part::Text { text } if text.contains("<tool_call>")));
}
#[test]
fn test_buffer_flush_empty() {
let mut buf = ToolCallBuffer::new();
let parts = buf.flush();
assert!(parts.is_empty());
}
#[test]
fn test_buffer_partial_prefix_detection() {
let mut buf = ToolCallBuffer::new();
assert!(matches!(buf.push("<tool"), BufferAction::Buffering));
assert!(matches!(buf.push("_call>"), BufferAction::Buffering));
match buf.push(r#"{"name":"x","arguments":{}}</tool_call>"#) {
BufferAction::Emit(parts) => {
assert_eq!(parts.len(), 1);
assert!(matches!(&parts[0], Part::FunctionCall { name, .. } if name == "x"));
}
BufferAction::Buffering => panic!("should emit"),
}
}
#[test]
fn test_deepseek_json_fence() {
let text = "```json\n{\"name\": \"search\", \"arguments\": {\"q\": \"rust\"}}\n```";
let parts = parse_text_tool_calls(text).unwrap();
assert_eq!(parts.len(), 1);
match &parts[0] {
Part::FunctionCall { name, args, .. } => {
assert_eq!(name, "search");
assert_eq!(args["q"], "rust");
}
_ => panic!("expected FunctionCall"),
}
}
#[test]
fn test_deepseek_with_text_before() {
let text = "I'll search for that.\n```json\n{\"name\": \"search\", \"arguments\": {\"q\": \"rust\"}}\n```\n<|tool▁call▁end|>";
let parts = parse_text_tool_calls(text).unwrap();
assert!(!parts.is_empty());
let has_fn_call =
parts.iter().any(|p| matches!(p, Part::FunctionCall { name, .. } if name == "search"));
assert!(has_fn_call);
}
#[test]
fn test_gemma4_simple() {
let text = "<|tool_call>call:get_weather{}<tool_call|>";
let parts = parse_text_tool_calls(text).unwrap();
assert_eq!(parts.len(), 1);
match &parts[0] {
Part::FunctionCall { name, .. } => assert_eq!(name, "get_weather"),
_ => panic!("expected FunctionCall"),
}
}
#[test]
fn test_gemma4_with_args() {
let text = "<|tool_call>call:get_weather{<|\"|>city<|\"|>:<|\"|>Tokyo<|\"|>}<tool_call|>";
let parts = parse_text_tool_calls(text).unwrap();
assert_eq!(parts.len(), 1);
match &parts[0] {
Part::FunctionCall { name, args, .. } => {
assert_eq!(name, "get_weather");
assert_eq!(args["city"], "Tokyo");
}
_ => panic!("expected FunctionCall"),
}
}
#[test]
fn test_action_tags() {
let text = "<|action_start|>{\"name\": \"search\", \"arguments\": {\"q\": \"rust\"}}<|action_end|>";
let parts = parse_text_tool_calls(text).unwrap();
assert_eq!(parts.len(), 1);
match &parts[0] {
Part::FunctionCall { name, args, .. } => {
assert_eq!(name, "search");
assert_eq!(args["q"], "rust");
}
_ => panic!("expected FunctionCall"),
}
}
#[test]
fn test_action_tags_with_surrounding_text() {
let text = "Let me look that up. <|action_start|>{\"name\": \"search\", \"arguments\": {}}<|action_end|> Done.";
let parts = parse_text_tool_calls(text).unwrap();
assert!(parts.len() >= 2); let has_fn_call =
parts.iter().any(|p| matches!(p, Part::FunctionCall { name, .. } if name == "search"));
assert!(has_fn_call);
}
}