pub mod base_json_parser;
pub mod deepseek_v3_1_parser;
pub mod deepseek_v3_parser;
pub use super::{config, response};
pub use base_json_parser::{detect_tool_call_start_basic_json, try_tool_call_parse_basic_json};
pub use deepseek_v3_1_parser::{
detect_tool_call_start_deepseek_v3_1, parse_tool_calls_deepseek_v3_1,
};
pub use deepseek_v3_parser::{detect_tool_call_start_deepseek_v3, parse_tool_calls_deepseek_v3};
pub use super::config::JsonParserConfig;
pub use super::response::ToolCallResponse;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
pub enum JsonParserType {
#[default]
Basic,
DeepseekV3,
DeepseekV31,
}
pub fn try_tool_call_parse_json(
message: &str,
config: &JsonParserConfig,
tools: Option<&[super::ToolDefinition]>,
) -> anyhow::Result<(Vec<ToolCallResponse>, Option<String>)> {
match config.parser_type {
JsonParserType::Basic => try_tool_call_parse_basic_json(message, config, tools),
JsonParserType::DeepseekV3 => parse_tool_calls_deepseek_v3(message, config, tools),
JsonParserType::DeepseekV31 => parse_tool_calls_deepseek_v3_1(message, config, tools),
}
}
pub fn detect_tool_call_start_json(chunk: &str, config: &JsonParserConfig) -> bool {
match config.parser_type {
JsonParserType::Basic => detect_tool_call_start_basic_json(chunk, config),
JsonParserType::DeepseekV3 => detect_tool_call_start_deepseek_v3(chunk, config),
JsonParserType::DeepseekV31 => detect_tool_call_start_deepseek_v3_1(chunk, config),
}
}
pub fn find_tool_call_end_position_json(
chunk: &str,
parser: &str,
config: &JsonParserConfig,
) -> usize {
match parser {
"hermes" | "nemotron_deci" | "qwen25" => {
let start_token = config.tool_call_start_tokens.first().map(|s| s.as_str());
if let Some(end_token) = config.tool_call_end_tokens.first() {
let Some(first_end) = chunk.find(end_token.as_str()) else {
return chunk.len();
};
let mut cursor = first_end + end_token.len();
if let Some(start_tok) = start_token {
loop {
let rest = &chunk[cursor..];
let trimmed = rest.trim_start();
if !trimmed.starts_with(start_tok) {
break;
}
let trim_offset = rest.len() - trimmed.len();
let search_from = cursor + trim_offset + start_tok.len();
if let Some(end_pos) = chunk[search_from..].find(end_token.as_str()) {
cursor = search_from + end_pos + end_token.len();
} else {
break;
}
}
}
cursor
} else {
chunk.len()
}
}
"mistral" | "phi4" => {
if let Some(pos) = chunk.rfind(']') {
pos + 1
} else {
chunk.len()
}
}
"deepseek_v3" | "deepseek_v3_1" => {
if config
.tool_call_start_tokens
.iter()
.any(|token| !token.is_empty() && chunk.contains(token.as_str()))
{
return config
.tool_call_end_tokens
.iter()
.find(|token| !token.is_empty())
.and_then(|token| chunk.find(token.as_str()).map(|pos| pos + token.len()))
.unwrap_or(chunk.len());
}
let begin_token = "<|tool▁call▁begin|>";
let end_token = "<|tool▁call▁end|>";
if let Some(pos) = chunk.find(end_token) {
let mut cursor = pos + end_token.len();
loop {
let rest = &chunk[cursor..];
let trimmed = rest.trim_start();
let trim_offset = rest.len() - trimmed.len();
if trimmed.starts_with(end_token) {
cursor += trim_offset + end_token.len();
} else if trimmed.starts_with(begin_token) {
match trimmed.find(end_token) {
Some(next_end) => cursor += trim_offset + next_end + end_token.len(),
None => break,
}
} else {
break;
}
}
cursor
} else {
chunk.len()
}
}
_ => chunk.len(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test] fn test_find_tool_call_end_position_parallel_calls() {
let config = JsonParserConfig {
tool_call_start_tokens: vec!["<tool_call>".to_string()],
tool_call_end_tokens: vec!["</tool_call>".to_string()],
..Default::default()
};
let two_calls = concat!(
"<tool_call>{\"name\": \"foo\", \"arguments\": {\"x\": 1}}</tool_call>",
"<tool_call>{\"name\": \"bar\", \"arguments\": {\"y\": 2}}</tool_call>",
"trailing"
);
let pos = find_tool_call_end_position_json(two_calls, "hermes", &config);
assert!(
two_calls[..pos].ends_with("</tool_call>"),
"should end at last </tool_call>, got: {:?}",
&two_calls[..pos]
);
assert_eq!(&two_calls[pos..], "trailing");
let three_calls = concat!(
"<tool_call>{\"name\": \"a\"}</tool_call>\n",
"<tool_call>{\"name\": \"b\"}</tool_call>\n",
"<tool_call>{\"name\": \"c\"}</tool_call> done"
);
let pos3 = find_tool_call_end_position_json(three_calls, "hermes", &config);
assert!(
three_calls[..pos3].ends_with("</tool_call>"),
"should end at last </tool_call>, got: {:?}",
&three_calls[..pos3]
);
assert_eq!(three_calls[pos3..].trim(), "done");
let incomplete = concat!(
"<tool_call>{\"name\": \"a\"}</tool_call>",
"<tool_call>{\"name\": \"b\""
);
let pos_inc = find_tool_call_end_position_json(incomplete, "hermes", &config);
let first_end = "<tool_call>{\"name\": \"a\"}</tool_call>".len();
assert_eq!(
pos_inc, first_end,
"should stop at end of first complete call when second is incomplete"
);
}
#[test] fn test_find_tool_call_end_position_parallel_calls_qwen25() {
let config = JsonParserConfig {
tool_call_start_tokens: vec!["<tool_call>".to_string()],
tool_call_end_tokens: vec!["</tool_call>".to_string()],
..Default::default()
};
let two_calls = concat!(
"<tool_call>\n{\"name\": \"get_weather\", \"arguments\": {\"location\": \"NYC\"}}\n</tool_call>\n",
"<tool_call>\n{\"name\": \"get_time\", \"arguments\": {\"timezone\": \"EST\"}}\n</tool_call>",
"trailing"
);
let pos = find_tool_call_end_position_json(two_calls, "qwen25", &config);
assert!(
two_calls[..pos].ends_with("</tool_call>"),
"qwen25 should end at last </tool_call>, got: {:?}",
&two_calls[..pos]
);
assert_eq!(&two_calls[pos..], "trailing");
}
#[test] fn test_find_tool_call_end_position_deepseek_bare_multi_call() {
let config = JsonParserConfig {
tool_call_start_tokens: vec!["<|tool▁calls▁begin|>".to_string()],
tool_call_end_tokens: vec!["<|tool▁calls▁end|>".to_string()],
..Default::default()
};
let two = concat!(
"<|tool▁call▁begin|>get_weather<|tool▁sep|>{\"location\":\"NYC\"}<|tool▁call▁end|>",
"<|tool▁call▁begin|>get_time<|tool▁sep|>{\"tz\":\"EST\"}<|tool▁call▁end|>",
"trailing"
);
let pos = find_tool_call_end_position_json(two, "deepseek_v3", &config);
assert!(
two[..pos].ends_with("<|tool▁call▁end|>"),
"should end at last call_end, got: {:?}",
&two[..pos]
);
assert_eq!(
&two[pos..],
"trailing",
"must span BOTH bare calls, not split after the first"
);
let incomplete = concat!(
"<|tool▁call▁begin|>get_weather<|tool▁sep|>{\"location\":\"NYC\"}<|tool▁call▁end|>",
"<|tool▁call▁begin|>get_time<|tool▁sep|>{\"tz\":"
);
let pos_inc = find_tool_call_end_position_json(incomplete, "deepseek_v3", &config);
assert!(incomplete[..pos_inc].ends_with("<|tool▁call▁end|>"));
assert!(
incomplete[pos_inc..].starts_with("<|tool▁call▁begin|>"),
"incomplete trailing call must remain unconsumed"
);
}
#[test] fn test_parse_nemotron_deci_no_outer_close_recovers() {
let config = JsonParserConfig {
tool_call_start_tokens: vec!["<TOOLCALL>".to_string()],
tool_call_end_tokens: vec!["</TOOLCALL>".to_string()],
allow_eof_recovery: true,
..Default::default()
};
let input = r#"<TOOLCALL>[{"name":"get_weather","arguments":{"city":"NYC"}}]"#;
let (calls, _normal_text) = try_tool_call_parse_json(input, &config, None).unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].function.name, "get_weather");
let args: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
assert_eq!(args["city"], "NYC");
}
#[test] fn test_parse_nemotron_deci_multiple_calls() {
let config = JsonParserConfig {
tool_call_start_tokens: vec!["<TOOLCALL>".to_string()],
tool_call_end_tokens: vec!["</TOOLCALL>".to_string()],
..Default::default()
};
let input = r#"<TOOLCALL>[{"name":"get_weather","arguments":{"city":"NYC"}},{"name":"get_time","arguments":{"tz":"EST"}}]</TOOLCALL>"#;
let (calls, normal_text) = try_tool_call_parse_json(input, &config, None).unwrap();
assert_eq!(calls.len(), 2);
assert_eq!(calls[0].function.name, "get_weather");
assert_eq!(calls[1].function.name, "get_time");
assert_eq!(normal_text, Some("".to_string()));
}
#[test] fn test_parse_nemotron_deci_truncated_json_recovers() {
let config = JsonParserConfig {
tool_call_start_tokens: vec!["<TOOLCALL>".to_string()],
tool_call_end_tokens: vec!["</TOOLCALL>".to_string()],
allow_eof_recovery: true,
..Default::default()
};
let input = r#"<TOOLCALL>[{"name":"get_weather","arguments":{"city":"NYC</TOOLCALL>"#;
let (calls, _) = try_tool_call_parse_json(input, &config, None).unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].function.name, "get_weather");
let args: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
assert_eq!(args["city"], "NYC");
}
fn nemotron_deci_config() -> JsonParserConfig {
JsonParserConfig {
tool_call_start_tokens: vec!["<TOOLCALL>".to_string()],
tool_call_end_tokens: vec!["</TOOLCALL>".to_string()],
strip_markup_on_recovery: true,
..Default::default()
}
}
#[test]
fn test_nemotron_deci_parser_output_independent_of_upstream_finish() {
let config = nemotron_deci_config();
let input = r#"<TOOLCALL>[{"name":"get_weather","arguments":{"city":"NYC"}}]</TOOLCALL>"#;
let (calls, _) = try_tool_call_parse_json(input, &config, None).unwrap();
assert_eq!(calls.len(), 1);
}
#[test] fn test_parse_nemotron_deci_empty_args() {
let config = nemotron_deci_config();
let input = r#"<TOOLCALL>[{"name":"current_time","arguments":{}}]</TOOLCALL>"#;
let (calls, _) = try_tool_call_parse_json(input, &config, None).unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].function.name, "current_time");
let args: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
assert_eq!(args, serde_json::json!({}));
}
#[test]
fn test_parse_nemotron_deci_preamble_orphan_close_recovers() {
let config = JsonParserConfig {
allow_eof_recovery: true,
..nemotron_deci_config()
};
let input =
r#"Let me check.[{"name":"get_weather","arguments":{"location":"NYC"}}]</TOOLCALL>"#;
let (calls, normal) = try_tool_call_parse_json(input, &config, None).unwrap();
assert_eq!(
calls.len(),
1,
"preamble must not drop the recoverable call"
);
assert_eq!(calls[0].function.name, "get_weather");
let args: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
assert_eq!(args, serde_json::json!({"location": "NYC"}));
assert_eq!(
normal.as_deref(),
Some(""),
"wrapper markers and preamble are stripped, not leaked into normal_text"
);
}
#[test] fn test_parse_nemotron_deci_empty_and_whitespace_inputs() {
let config = nemotron_deci_config();
for input in &["", " ", "\n", "\t\n \t"] {
let (calls, normal) = try_tool_call_parse_json(input, &config, None).unwrap();
assert!(
calls.is_empty(),
"Empty/whitespace input must yield no calls (input={:?})",
input
);
assert_eq!(
normal.as_deref(),
Some(""),
"Empty/whitespace input collapses to empty normal_text (input={:?})",
input
);
}
}
#[test] fn test_parse_nemotron_deci_duplicate_calls_same_name() {
let config = nemotron_deci_config();
let input = r#"<TOOLCALL>[{"name":"get_weather","arguments":{"city":"NYC"}},{"name":"get_weather","arguments":{"city":"LA"}}]</TOOLCALL>"#;
let (calls, _) = try_tool_call_parse_json(input, &config, None).unwrap();
assert_eq!(calls.len(), 2, "Both duplicate-name calls must be returned");
assert_eq!(calls[0].function.name, "get_weather");
assert_eq!(calls[1].function.name, "get_weather");
assert_ne!(
calls[0].id, calls[1].id,
"Duplicate calls must have distinct ids"
);
let args0: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
let args1: serde_json::Value = serde_json::from_str(&calls[1].function.arguments).unwrap();
assert_eq!(args0["city"], "NYC");
assert_eq!(args1["city"], "LA");
}
}