use std::collections::BTreeMap;
use std::sync::OnceLock;
use regex_lite::Regex;
use tokenizer::{JsonValue, ParsedToolCall};
fn cached_regex(cell: &'static OnceLock<Regex>, pattern: &str) -> &'static Regex {
cell.get_or_init(|| Regex::new(pattern).expect("rescue regex pattern is valid"))
}
pub(super) fn rescue(text: &str, available_tools: &[&str]) -> Vec<ParsedToolCall> {
let built = parse_glm(text, available_tools)
.or_else(|| parse_invoke_parameters(text, available_tools))
.or_else(|| parse_qwen_xml(text, available_tools))
.or_else(|| parse_gemma(text, available_tools))
.or_else(|| parse_kimi(text, available_tools));
built
.unwrap_or_default()
.into_iter()
.enumerate()
.map(|(i, call)| {
let arguments = JsonValue::Object(call.arguments);
ParsedToolCall {
id: format!("toolu_rescued_{i}"),
name: call.name,
arguments_json: arguments.encoded(),
arguments,
}
})
.collect()
}
struct RawCall {
name: String,
arguments: BTreeMap<String, JsonValue>,
}
struct Groups<'a> {
first: &'a str,
second: &'a str,
}
fn groups<'a>(caps: ®ex_lite::Captures<'a>) -> Groups<'a> {
let at = |i: usize| caps.get(i).map(|m| m.as_str()).unwrap_or_default();
Groups {
first: at(1),
second: at(2),
}
}
fn parse_glm(text: &str, available_tools: &[&str]) -> Option<Vec<RawCall>> {
static CALL: OnceLock<Regex> = OnceLock::new();
static PAIR: OnceLock<Regex> = OnceLock::new();
let call = cached_regex(
&CALL,
r"([A-Za-z0-9_\-]{1,64})\s*((?:<arg_key>[\s\S]*?</arg_key>\s*<arg_value>[\s\S]*?</arg_value>\s*)+)",
);
let pair = cached_regex(
&PAIR,
r"<arg_key>([\s\S]*?)</arg_key>\s*<arg_value>([\s\S]*?)</arg_value>",
);
let mut calls = Vec::new();
for caps in call.captures_iter(text) {
let g = groups(&caps);
if !available_tools.contains(&g.first) {
continue;
}
let mut arguments = BTreeMap::new();
for p in pair.captures_iter(g.second) {
let kv = groups(&p);
arguments.insert(kv.first.to_string(), coerce_value(kv.second));
}
calls.push(RawCall {
name: g.first.to_string(),
arguments,
});
}
(!calls.is_empty()).then_some(calls)
}
fn parse_invoke_parameters(text: &str, available_tools: &[&str]) -> Option<Vec<RawCall>> {
static BLOCK: OnceLock<Regex> = OnceLock::new();
static PAIR: OnceLock<Regex> = OnceLock::new();
let block = cached_regex(
&BLOCK,
r#"<invoke\s+name="([A-Za-z0-9_\-]+)"\s*>([\s\S]*?)</invoke>"#,
);
let pair = cached_regex(
&PAIR,
r#"<parameter\s+name="([\s\S]*?)"\s*>([\s\S]*?)</parameter>"#,
);
collect_matches(text, block, pair, available_tools)
}
fn parse_qwen_xml(text: &str, available_tools: &[&str]) -> Option<Vec<RawCall>> {
static BLOCK: OnceLock<Regex> = OnceLock::new();
static PAIR: OnceLock<Regex> = OnceLock::new();
let block = cached_regex(&BLOCK, r"<function=([A-Za-z0-9_\-]+)>([\s\S]*?)</function>");
let pair = cached_regex(
&PAIR,
r"<parameter=([A-Za-z0-9_\-]+)>([\s\S]*?)</parameter>",
);
collect_matches(text, block, pair, available_tools)
}
fn parse_gemma(text: &str, available_tools: &[&str]) -> Option<Vec<RawCall>> {
static HEAD: OnceLock<Regex> = OnceLock::new();
let head = cached_regex(&HEAD, r"call:([A-Za-z0-9_\-]+)\s*\{");
let allowed: std::collections::HashSet<String> =
available_tools.iter().map(|&s| s.to_string()).collect();
let parser = tokenizer::GemmaToolCallParser::new();
let mut calls = Vec::new();
let mut cursor = 0usize;
while cursor <= text.len() {
let Some(caps) = head.captures(&text[cursor..]) else {
break;
};
let whole_match = caps.get(0).map(|m| m.as_str()).unwrap_or_default();
let name = caps
.get(1)
.map(|m| m.as_str())
.unwrap_or_default()
.to_string();
let rel_start = caps.get(0).map(|m| m.start()).unwrap_or(0);
let brace_start = cursor + rel_start + whole_match.len() - 1;
let Some(brace_len) = balanced_brace_len(&text[brace_start..]) else {
break;
};
let body_end = brace_start + brace_len;
let match_start = cursor + rel_start;
cursor = body_end;
if !available_tools.contains(&name.as_str()) {
continue;
}
let matched = &text[match_start..body_end];
if let Ok(parsed) = parser.parse(matched, &allowed, "id") {
if let JsonValue::Object(map) = parsed.arguments {
calls.push(RawCall {
name: parsed.name,
arguments: map,
});
continue;
}
}
let body = &text[brace_start..body_end];
if let Some(map) = json_object(body) {
calls.push(RawCall {
name,
arguments: map,
});
}
}
(!calls.is_empty()).then_some(calls)
}
fn balanced_brace_len(s: &str) -> Option<usize> {
let mut depth = 0i32;
let mut in_string = false;
let mut escaped = false;
for (i, b) in s.bytes().enumerate() {
if in_string {
if escaped {
escaped = false;
} else if b == b'\\' {
escaped = true;
} else if b == b'"' {
in_string = false;
}
continue;
}
match b {
b'"' => in_string = true,
b'{' => depth += 1,
b'}' => {
depth -= 1;
if depth == 0 {
return Some(i + 1);
}
}
_ => {}
}
}
None
}
fn collect_matches(
text: &str,
block: &Regex,
pair: &Regex,
available_tools: &[&str],
) -> Option<Vec<RawCall>> {
let mut calls = Vec::new();
for caps in block.captures_iter(text) {
let g = groups(&caps);
if !available_tools.contains(&g.first) {
continue;
}
let mut arguments = BTreeMap::new();
for p in pair.captures_iter(g.second) {
let kv = groups(&p);
arguments.insert(kv.first.to_string(), coerce_value(kv.second));
}
if arguments.is_empty() {
if let Some(object) = json_object(g.second) {
arguments = object;
}
}
calls.push(RawCall {
name: g.first.to_string(),
arguments,
});
}
(!calls.is_empty()).then_some(calls)
}
fn parse_kimi(text: &str, available_tools: &[&str]) -> Option<Vec<RawCall>> {
static CALL: OnceLock<Regex> = OnceLock::new();
let call = cached_regex(
&CALL,
r"<\|tool_call_begin\|>\s*([\w.:\-]+)\s*<\|tool_call_argument_begin\|>([\s\S]*?)<\|tool_call_end\|>",
);
let mut calls = Vec::new();
for caps in call.captures_iter(text) {
let g = groups(&caps);
let Some(name) = name_from_kimi_id(g.first) else {
continue;
};
if !available_tools.contains(&name.as_str()) {
continue;
}
let body = g.second.trim();
let arguments = if body.is_empty() {
BTreeMap::new()
} else {
match json_object(body) {
Some(object) => object,
None => continue,
}
};
calls.push(RawCall { name, arguments });
}
(!calls.is_empty()).then_some(calls)
}
fn name_from_kimi_id(id: &str) -> Option<String> {
let without_namespace = id.strip_prefix("functions.").unwrap_or(id);
let name = without_namespace.split(':').next()?;
(!name.is_empty()).then(|| name.to_string())
}
fn coerce_value(raw: &str) -> JsonValue {
let trimmed = raw.trim();
let Some(first) = trimmed.chars().next() else {
return JsonValue::String(trimmed.to_string());
};
if !"{[-0123456789tfn".contains(first) {
return JsonValue::String(trimmed.to_string());
}
match JsonValue::parse(trimmed) {
Ok(JsonValue::String(_)) => JsonValue::String(trimmed.to_string()),
Ok(value) => value,
Err(_) => JsonValue::String(trimmed.to_string()),
}
}
fn json_object(text: &str) -> Option<BTreeMap<String, JsonValue>> {
match JsonValue::parse(text.trim()) {
Ok(JsonValue::Object(map)) => Some(map),
_ => None,
}
}