use regex::RegexBuilder;
use serde_json::value::RawValue;
use uuid::Uuid;
use super::super::ToolDefinition;
use super::config::JsonParserConfig;
use super::response::{CalledFunction, ToolCallResponse, ToolCallType};
#[derive(Debug, serde::Deserialize)]
pub struct CalledFunctionParameters {
pub name: String,
pub parameters: Box<RawValue>,
}
#[derive(Debug, serde::Deserialize)]
pub struct CalledFunctionArguments {
pub name: String,
pub arguments: Box<RawValue>,
}
fn extract_tool_call_content(input: &str, start_token: &str, end_token: &str) -> Option<String> {
let escaped_start = regex::escape(start_token);
let escaped_end = regex::escape(end_token);
let pattern = format!(r"{}(.*?){}", escaped_start, escaped_end);
match RegexBuilder::new(&pattern)
.dot_matches_new_line(true)
.build()
{
Ok(regex) => {
let matches: Vec<_> = regex
.captures_iter(input)
.filter_map(|captures| captures.get(1))
.map(|m| m.as_str().trim().to_string())
.collect();
if !matches.is_empty() {
if matches.len() == 1 {
return Some(matches.last().unwrap().clone());
} else {
return Some(format!("[{}]", matches.join(",")));
}
}
None
}
Err(_) => None,
}
}
fn extract_tool_call_content_eof_recovery(input: &str, start_token: &str) -> Option<String> {
let start_pos = input.find(start_token)?;
let tail = input[start_pos + start_token.len()..].trim();
if tail.starts_with('{') || tail.starts_with('[') {
Some(tail.to_string())
} else {
None
}
}
fn handle_single_token_tool_calls(input: &str, start_token: &str) -> Option<String> {
if !input.contains(start_token) {
return None;
}
let mut items: Vec<String> = Vec::new();
for seg in input.split(start_token) {
let s = seg.trim();
if s.is_empty() {
continue;
}
if s.starts_with('{') {
let mut remaining = s.trim_start();
while !remaining.is_empty() {
let mut stream =
serde_json::Deserializer::from_str(remaining).into_iter::<Box<RawValue>>();
match stream.next() {
Some(Ok(rv)) => {
let raw = rv.get();
if raw.is_empty() {
break; }
items.push(raw.to_string());
remaining = remaining[raw.len()..].trim_start();
if let Some(rest) = remaining.strip_prefix(';') {
remaining = rest.trim_start();
} else {
break; }
}
_ => break, }
}
} else if s.starts_with('[') {
if let Some(pos) = s.rfind(']') {
let candidate = &s[..=pos].trim();
if let Ok(arr) = serde_json::from_str::<Vec<Box<RawValue>>>(candidate) {
for item in arr {
items.push(item.get().to_string());
}
}
}
}
}
if items.is_empty() {
return Some(String::new());
}
Some(format!("[{}]", items.join(",")))
}
pub(crate) fn try_repair_truncated_json(s: &str) -> Option<String> {
let mut stack: Vec<char> = Vec::new();
let mut in_string = false;
let mut escape = false;
for c in s.chars() {
if escape {
escape = false;
continue;
}
if in_string {
match c {
'\\' => escape = true,
'"' => in_string = false,
_ => {}
}
continue;
}
match c {
'"' => in_string = true,
'{' => stack.push('}'),
'[' => stack.push(']'),
'}' | ']' => {
stack.pop();
}
_ => {}
}
}
if !escape && !in_string && stack.is_empty() {
return None;
}
let mut repaired = s.to_string();
if escape {
repaired.push('\\');
}
if in_string {
repaired.push('"');
}
while let Some(closer) = stack.pop() {
repaired.push(closer);
}
Some(repaired)
}
fn recover_leading_complete_objects(json: &str) -> Vec<String> {
let trimmed = json.trim();
let body = trimmed.strip_prefix('[').unwrap_or(trimmed);
let mut out: Vec<String> = Vec::new();
let mut remaining = body.trim_start();
while !remaining.is_empty() {
let mut stream = serde_json::Deserializer::from_str(remaining).into_iter::<Box<RawValue>>();
match stream.next() {
Some(Ok(rv)) => {
let raw = rv.get();
if raw.is_empty() || !raw.trim_start().starts_with('{') {
break;
}
out.push(raw.to_string());
remaining = remaining[raw.len()..].trim_start();
match remaining.strip_prefix(',') {
Some(rest) => remaining = rest.trim_start(),
None => break,
}
}
_ => break,
}
}
out
}
fn try_parse_normal_text(input: &str, start_token: &str) -> String {
if let Some(idx) = input.find(start_token) {
let prefix = &input[..idx];
return if start_token == "[TOOL_CALLS]" {
prefix.to_string()
} else {
prefix.trim().to_string()
};
}
String::new()
}
fn parse_calls(payload: &str) -> anyhow::Result<Option<Vec<ToolCallResponse>>> {
let mk = |name: String, args: &RawValue| ToolCallResponse {
id: format!("call-{}", Uuid::new_v4()),
tp: ToolCallType::Function,
function: CalledFunction {
name,
arguments: args.get().to_string(),
},
};
if let Ok(array) = serde_json::from_str::<Vec<Box<RawValue>>>(payload) {
let mut calls = Vec::new();
for item in array {
let item_str = item.get();
if let Ok(func_args) = serde_json::from_str::<CalledFunctionArguments>(item_str) {
calls.push(mk(func_args.name, &func_args.arguments));
} else if let Ok(func_params) =
serde_json::from_str::<CalledFunctionParameters>(item_str)
{
calls.push(mk(func_params.name, &func_params.parameters));
}
}
return Ok(Some(calls));
}
if let Ok(single) = serde_json::from_str::<CalledFunctionParameters>(payload) {
return Ok(Some(vec![mk(single.name, &single.parameters)]));
}
if let Ok(single) = serde_json::from_str::<CalledFunctionArguments>(payload) {
return Ok(Some(vec![mk(single.name, &single.arguments)]));
}
Ok(None)
}
pub fn try_tool_call_parse_basic_json(
message: &str,
config: &JsonParserConfig,
_tools: Option<&[ToolDefinition]>,
) -> anyhow::Result<(Vec<ToolCallResponse>, Option<String>)> {
tracing::debug!("Using JSON parser config: {:?}", config);
let trimmed = message.trim();
if trimmed.is_empty() {
return Ok((vec![], Some(String::new())));
}
let tool_call_start_tokens = &config.tool_call_start_tokens;
let tool_call_end_tokens = &config.tool_call_end_tokens;
if tool_call_start_tokens.is_empty() && !config.bare_json_mode {
return Ok((vec![], Some(trimmed.to_string())));
}
let mut json = trimmed.to_string();
let mut normal_text = trimmed.to_string();
let mut found_start_token_with_no_valid_json = false;
let has_start_token = !config.bare_json_mode
&& tool_call_start_tokens
.iter()
.any(|token| !token.is_empty() && normal_text.contains(token));
if !has_start_token {
if let Some(idx) = normal_text.find(['{', '[']) {
let extracted_normal = normal_text[..idx].trim().to_string();
let extracted_json = normal_text[idx..].trim().to_string();
if !extracted_json.is_empty() {
normal_text = extracted_normal;
json = extracted_json;
}
}
} else {
'outer: for start_token in tool_call_start_tokens.iter() {
for end_token in tool_call_end_tokens.iter() {
let new_normal_text = try_parse_normal_text(&normal_text, start_token);
match (start_token.is_empty(), end_token.is_empty()) {
(false, true) => {
let result = handle_single_token_tool_calls(&json, start_token);
if let Some(content) = result {
if !content.starts_with('[') {
found_start_token_with_no_valid_json = true;
}
json = content;
normal_text = new_normal_text;
break 'outer; }
}
(false, false) => {
let mut result = extract_tool_call_content(&json, start_token, end_token);
if result.is_none()
&& config.allow_eof_recovery
&& json.contains(start_token.as_str())
{
result = extract_tool_call_content_eof_recovery(&json, start_token);
}
if let Some(content) = result {
if content.is_empty() {
found_start_token_with_no_valid_json = true;
}
json = content;
normal_text = new_normal_text;
break 'outer; }
}
_ => {
continue;
}
}
}
}
}
let json = json.as_str();
if let Some(calls) = parse_calls(json)? {
return Ok((calls, Some(normal_text)));
}
if config.allow_eof_recovery
&& config
.tool_call_start_tokens
.iter()
.any(|t| t == "[TOOL_CALLS]")
&& trimmed.contains("[TOOL_CALLS]")
&& !trimmed.contains("[/TOOL_CALLS]")
{
let recovered = recover_leading_complete_objects(json);
if !recovered.is_empty()
&& let Some(calls) = parse_calls(&format!("[{}]", recovered.join(",")))?
&& !calls.is_empty()
{
return Ok((calls, Some(normal_text)));
}
return Ok((vec![], Some(normal_text)));
}
if config.allow_eof_recovery
&& let Some(repaired) = try_repair_truncated_json(json)
&& let Some(calls) = parse_calls(repaired.as_str())?
&& !calls.is_empty()
{
return Ok((calls, Some(normal_text)));
}
if found_start_token_with_no_valid_json {
return Ok((vec![], Some(String::new())));
}
let mistral_end_token_present = config
.tool_call_start_tokens
.iter()
.any(|t| t == "[TOOL_CALLS]")
&& config
.tool_call_end_tokens
.iter()
.any(|token| !token.is_empty() && trimmed.contains(token.as_str()));
if config.strip_markup_on_recovery && (config.allow_eof_recovery || mistral_end_token_present) {
let has_marker = config
.tool_call_start_tokens
.iter()
.chain(config.tool_call_end_tokens.iter())
.any(|token| !token.is_empty() && trimmed.contains(token.as_str()));
if has_marker {
let mut payload = json;
loop {
payload = payload.trim();
match config
.tool_call_start_tokens
.iter()
.filter(|token| !token.is_empty())
.find_map(|token| payload.strip_prefix(token.as_str()))
{
Some(rest) => payload = rest,
None => break,
}
}
loop {
payload = payload.trim();
match config
.tool_call_end_tokens
.iter()
.filter(|token| !token.is_empty())
.find_map(|token| payload.strip_suffix(token.as_str()))
{
Some(rest) => payload = rest,
None => break,
}
}
let payload = payload.trim();
let calls = parse_calls(payload)?.unwrap_or_default();
if !calls.is_empty() {
tracing::warn!(
recovered_calls = calls.len(),
"Recovered {} tool call(s) from malformed tool-call framing; stripped wrapper markers instead of leaking them into normal_text",
calls.len()
);
return Ok((calls, Some(String::new())));
}
tracing::warn!(
dropped_content = %trimmed,
"Dropping unparseable tool-call content; wrapper markers stripped, no valid tool call recovered"
);
return Ok((vec![], Some(String::new())));
}
}
Ok((vec![], Some(trimmed.to_string())))
}
pub fn detect_tool_call_start_basic_json(chunk: &str, config: &JsonParserConfig) -> bool {
let trimmed = chunk.trim();
if trimmed.is_empty() {
return false;
}
let contains_complete_token = config
.tool_call_start_tokens
.iter()
.any(|token| !token.is_empty() && trimmed.contains(token));
if contains_complete_token {
return true;
}
let has_partial_token = config.tool_call_start_tokens.iter().any(|token| {
if token.is_empty() {
return false;
}
for i in 1..=token.chars().count() {
if let Some(prefix) = token.chars().take(i).collect::<String>().get(..) {
let prefix_str = &prefix[..prefix.len()];
if trimmed == prefix_str {
return true;
}
if prefix_str.len() >= 3 && trimmed.contains(prefix_str) {
return true;
}
if prefix_str.len() < 3 && trimmed.ends_with(prefix_str) {
return true;
}
}
}
false
});
has_partial_token || trimmed.contains('{') || trimmed.contains('[')
}
#[cfg(test)]
mod repair_tests {
use super::*;
#[test]
fn test_repair_eof_after_backslash() {
let repaired = try_repair_truncated_json(r#"{"k":"a\"#).expect("must repair");
assert!(
serde_json::from_str::<serde_json::Value>(&repaired).is_ok(),
"repaired must parse: {:?}",
repaired
);
}
}
#[cfg(test)]
mod detect_parser_tests {
use super::*;
#[test] fn detect_tool_call_start_basic_json_chunk_with_tool_call_start_token_hermes() {
let text =
r#"<tool_call>{"name": "search", "parameters": { "query": "rust" } }</tool_call>"#;
let config = JsonParserConfig {
tool_call_start_tokens: vec!["<tool_call>".to_string()],
tool_call_end_tokens: vec!["</tool_call>".to_string()],
..Default::default()
};
let result = detect_tool_call_start_basic_json(text, &config);
assert!(result);
}
#[test] fn detect_tool_call_start_basic_json_chunk_without_tool_call_start_token() {
let text = r#"{"name": "search", "parameters": { "query": "rust" } }"#;
let config = JsonParserConfig {
tool_call_start_tokens: vec!["<tool_call>".to_string()],
tool_call_end_tokens: vec!["</tool_call>".to_string()],
..Default::default()
};
let result = detect_tool_call_start_basic_json(text, &config);
assert!(result);
}
#[test] fn detect_tool_call_start_basic_json_chunk_without_tool_call_start_token_with_normal_text() {
let text = r#"Here it is {"name": "#;
let config = JsonParserConfig {
tool_call_start_tokens: vec!["<tool_call>".to_string()],
tool_call_end_tokens: vec!["</tool_call>".to_string()],
..Default::default()
};
let result = detect_tool_call_start_basic_json(text, &config);
assert!(result);
}
#[test] fn detect_tool_call_start_basic_json_chunk_with_square_brackets() {
let text = r#"Here it is [{"name": "search","#;
let config = JsonParserConfig {
tool_call_start_tokens: vec!["<tool_call>".to_string()],
tool_call_end_tokens: vec!["</tool_call>".to_string()],
..Default::default()
};
let result = detect_tool_call_start_basic_json(text, &config);
assert!(result);
}
#[test] fn detect_tool_call_start_basic_json_chunk_false_positive() {
let text = r#"Here it is { Whats up"#;
let config = JsonParserConfig {
tool_call_start_tokens: vec!["<tool_call>".to_string()],
tool_call_end_tokens: vec!["</tool_call>".to_string()],
..Default::default()
};
let result = detect_tool_call_start_basic_json(text, &config);
assert!(result);
}
#[test] fn detect_tool_call_start_basic_json_chunk_with_tool_call_start_token_nemotron_deci() {
let text =
r#"<TOOLCALL>[{"name": "search", "parameters": { "query": "rust" } }]</TOOLCALL>"#;
let config = JsonParserConfig {
tool_call_start_tokens: vec!["<TOOLCALL>".to_string()],
tool_call_end_tokens: vec!["</TOOLCALL>".to_string()],
..Default::default()
};
let result = detect_tool_call_start_basic_json(text, &config);
assert!(result);
}
#[test] fn detect_tool_call_start_basic_json_chunk_with_lllama3_json_token() {
let text = r#"<|python_tag|>{ "name": }"#;
let config = JsonParserConfig {
tool_call_start_tokens: vec!["<|python_tag|>".to_string()],
tool_call_end_tokens: vec!["".to_string()],
..Default::default()
};
let result = detect_tool_call_start_basic_json(text, &config);
assert!(result);
}
#[test] fn detect_tool_call_start_basic_json_chunk_mistral_token() {
let text = r#"Hello Yo ! [TOOL_CALLS]{"name": "search", "#;
let config = JsonParserConfig {
tool_call_start_tokens: vec!["[TOOL_CALLS]".to_string()],
tool_call_end_tokens: vec!["".to_string()],
..Default::default()
};
let result = detect_tool_call_start_basic_json(text, &config);
assert!(result);
}
#[test] fn detect_tool_call_start_basic_json_chunk_phi4_token() {
let text = r#"functools{"name": "search", "#;
let config = JsonParserConfig {
tool_call_start_tokens: vec!["functools".to_string()],
tool_call_end_tokens: vec!["".to_string()],
..Default::default()
};
let result = detect_tool_call_start_basic_json(text, &config);
assert!(result);
}
#[test] fn detect_tool_call_start_basic_json_chunk_phi4_partial_token_fun() {
let text = r#"fun"#;
let config = JsonParserConfig {
tool_call_start_tokens: vec!["functools".to_string()],
tool_call_end_tokens: vec!["".to_string()],
..Default::default()
};
let result = detect_tool_call_start_basic_json(text, &config);
assert!(
result,
"Should detect 'fun' as potential start of 'functools'"
);
}
#[test] fn detect_tool_call_start_basic_json_chunk_phi4_partial_token_func() {
let text = r#"func"#;
let config = JsonParserConfig {
tool_call_start_tokens: vec!["functools".to_string()],
tool_call_end_tokens: vec!["".to_string()],
..Default::default()
};
let result = detect_tool_call_start_basic_json(text, &config);
assert!(
result,
"Should detect 'func' as potential start of 'functools'"
);
}
#[test] fn detect_tool_call_start_basic_json_chunk_phi4_partial_token_f() {
let text = r#"f"#;
let config = JsonParserConfig {
tool_call_start_tokens: vec!["functools".to_string()],
tool_call_end_tokens: vec!["".to_string()],
..Default::default()
};
let result = detect_tool_call_start_basic_json(text, &config);
assert!(
result,
"Should detect 'f' as potential start of 'functools'"
);
}
#[test] fn detect_tool_call_start_basic_json_chunk_phi4_partial_with_prefix() {
let text = r#"Hello fun"#;
let config = JsonParserConfig {
tool_call_start_tokens: vec!["functools".to_string()],
tool_call_end_tokens: vec!["".to_string()],
..Default::default()
};
let result = detect_tool_call_start_basic_json(text, &config);
assert!(
result,
"Should detect text ending with 'fun' as potential tool call start"
);
}
#[test] fn detect_tool_call_start_basic_json_chunk_phi4_avoid_false_positive() {
let text = r#"funny joke"#;
let config = JsonParserConfig {
tool_call_start_tokens: vec!["functools".to_string()],
tool_call_end_tokens: vec!["".to_string()],
..Default::default()
};
let result = detect_tool_call_start_basic_json(text, &config);
assert!(result);
}
#[test] fn detect_tool_call_start_basic_json_chunk_phi4_no_match() {
let text = r#"hello world"#;
let config = JsonParserConfig {
tool_call_start_tokens: vec!["functools".to_string()],
tool_call_end_tokens: vec!["".to_string()],
..Default::default()
};
let result = detect_tool_call_start_basic_json(text, &config);
assert!(
!result,
"Should not detect unrelated text as tool call start"
);
}
}