use super::custom_openai_compatible::{KNOWN_TOOL_NAMES, extract_balanced_json};
use serde_json::Value;
pub(crate) struct BareToolCallMatch {
pub name: String,
pub args: Value,
pub strip_start: usize,
pub strip_end: usize,
pub already_in_existing: bool,
}
pub(crate) fn has_bare_name_args_signal(text: &str) -> bool {
(text.contains("\"name\":") || text.contains("\"name\" :"))
&& (text.contains("\"arguments\":") || text.contains("\"arguments\" :"))
}
pub(crate) fn extract_bare_name_args_calls(
text: &str,
existing_strip_ranges: &[(usize, usize)],
existing_calls: &[(String, Value)],
) -> Vec<BareToolCallMatch> {
let mut out = Vec::new();
if !has_bare_name_args_signal(text) {
return out;
}
let name_anchors = ["\"name\":", "\"name\" :"];
let mut search_from = 0;
loop {
let next = name_anchors
.iter()
.filter_map(|a| text[search_from..].find(a).map(|p| (search_from + p, *a)))
.min_by_key(|(p, _)| *p);
let Some((anchor_pos, anchor_lit)) = next else {
break;
};
let advance_past_anchor = || anchor_pos + anchor_lit.len();
if in_any_range(existing_strip_ranges, anchor_pos)
|| in_any_range(&existing_ranges_from(&out), anchor_pos)
{
search_from = advance_past_anchor();
continue;
}
let mut back = anchor_pos;
while back > 0 {
let b = text.as_bytes()[back - 1];
if b.is_ascii_whitespace() || b == b'\n' || b == b'\r' {
back -= 1;
continue;
}
break;
}
if back == 0 || text.as_bytes()[back - 1] != b'{' {
search_from = advance_past_anchor();
continue;
}
let obj_start = back - 1;
if in_any_range(existing_strip_ranges, obj_start)
|| in_any_range(&existing_ranges_from(&out), obj_start)
{
search_from = advance_past_anchor();
continue;
}
let Some(consumed) = extract_balanced_json(&text[obj_start..]) else {
search_from = advance_past_anchor();
continue;
};
let obj_slice = &text[obj_start..obj_start + consumed];
let Ok(v) = serde_json::from_str::<Value>(obj_slice) else {
search_from = advance_past_anchor();
continue;
};
let Some(name_str) = v.get("name").and_then(|n| n.as_str()) else {
search_from = advance_past_anchor();
continue;
};
if !KNOWN_TOOL_NAMES.contains(&name_str) {
search_from = advance_past_anchor();
continue;
}
let Some(args) = parse_arguments_value(v.get("arguments")) else {
search_from = advance_past_anchor();
continue;
};
let already_in_existing = existing_calls
.iter()
.any(|(n, a)| n == name_str && a == &args);
let strip_end = obj_start + consumed;
out.push(BareToolCallMatch {
name: name_str.to_string(),
args,
strip_start: obj_start,
strip_end,
already_in_existing,
});
search_from = strip_end;
}
out
}
fn in_any_range(ranges: &[(usize, usize)], pos: usize) -> bool {
ranges.iter().any(|(s, e)| *s <= pos && pos < *e)
}
fn existing_ranges_from(matches: &[BareToolCallMatch]) -> Vec<(usize, usize)> {
matches
.iter()
.map(|m| (m.strip_start, m.strip_end))
.collect()
}
fn parse_arguments_value(arguments_val: Option<&Value>) -> Option<Value> {
match arguments_val {
Some(v) if v.is_object() => Some(v.clone()),
Some(v) if v.is_string() => {
let s = v.as_str().unwrap_or("{}").trim();
if !s.starts_with('{') || !s.ends_with('}') {
return None;
}
serde_json::from_str::<Value>(s)
.ok()
.filter(|p| p.is_object())
}
_ => None,
}
}