use rig_core::message::{AssistantContent, Reasoning, ToolCall, ToolFunction};
use rig_core::one_or_many::OneOrMany;
use serde_json::Value;
pub(crate) fn extract_structured_json(raw_text: &str) -> Option<String> {
let bytes = raw_text.as_bytes();
let start = bytes.iter().position(|&b| b == b'{')?;
let mut depth: usize = 0;
let mut in_string = false;
let mut escaped = false;
for (i, &b) in bytes.iter().enumerate().skip(start) {
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(raw_text[start..=i].to_string());
}
}
_ => {}
}
}
None
}
pub(crate) fn parse_completion_output(
raw_text: &str,
has_json_schema: bool,
) -> Result<OneOrMany<AssistantContent>, String> {
log::debug!("raw output:\n{raw_text}");
if has_json_schema && let Some(json) = extract_structured_json(raw_text) {
return Ok(OneOrMany::one(AssistantContent::text(json)));
}
if let Some(tool_calls) = parse_tool_calls(raw_text) {
let mut content: Vec<AssistantContent> = Vec::new();
for (i, (name, arguments)) in tool_calls.into_iter().enumerate() {
content.push(AssistantContent::ToolCall(ToolCall::new(
format!("tool-call-{i}"),
ToolFunction::new(name, arguments),
)));
}
if let Ok(result) = OneOrMany::many(content) {
return Ok(result);
}
}
if let Some((reasoning, text)) = split_thinking(raw_text) {
let mut content = Vec::new();
if !reasoning.is_empty() {
content.push(AssistantContent::Reasoning(Reasoning::new(&reasoning)));
}
content.push(AssistantContent::text(text));
if let Ok(result) = OneOrMany::many(content) {
return Ok(result);
}
}
Ok(OneOrMany::one(AssistantContent::text(raw_text.to_string())))
}
pub(crate) fn parse_tool_calls(output: &str) -> Option<Vec<(String, Value)>> {
if let Some(xml) = parse_xml_tool_calls(output) {
return Some(xml);
}
if let Some(json) = extract_structured_json(output)
&& let Ok(value) = serde_json::from_str::<Value>(&json)
&& let Some(tc) = json_value_to_tool_call(&value)
{
return Some(vec![tc]);
}
None
}
pub(crate) fn parse_xml_tool_calls(output: &str) -> Option<Vec<(String, Value)>> {
let mut results = Vec::new();
for block in output.split("<tool_call>").skip(1) {
let block = block.split("</tool_call>").next().unwrap_or(block);
let trimmed = block.trim();
if trimmed.starts_with('{')
&& let Ok(value) = serde_json::from_str::<Value>(trimmed)
&& let Some(tc) = json_value_to_tool_call(&value)
{
results.push(tc);
continue;
}
if let Some(tc) = parse_parameter_form(block) {
results.push(tc);
}
}
if results.is_empty() {
None
} else {
Some(results)
}
}
fn json_value_to_tool_call(value: &Value) -> Option<(String, Value)> {
let name = value.get("name").and_then(Value::as_str).or_else(|| {
value
.get("function")
.and_then(|f| f.get("name"))
.and_then(Value::as_str)
})?;
let arguments = match value.get("arguments") {
Some(Value::String(s)) => {
serde_json::from_str(s).unwrap_or_else(|_| Value::String(s.clone()))
}
Some(other) => other.clone(),
None => value
.get("function")
.and_then(|f| f.get("arguments"))
.map(|a| match a {
Value::String(s) => {
serde_json::from_str(s).unwrap_or_else(|_| Value::String(s.clone()))
}
other => other.clone(),
})
.unwrap_or(Value::Null),
};
Some((name.to_string(), arguments))
}
fn parse_parameter_form(block: &str) -> Option<(String, Value)> {
let func_start = block.find("<function=")?;
let after_eq = &block[func_start + "<function=".len()..];
let func_name_end = after_eq.find('>')?;
let func_name = after_eq[..func_name_end].trim().to_string();
let mut args = serde_json::Map::new();
let mut search_from = 0;
while let Some(param_start) = block[search_from..].find("<parameter=") {
let abs_start = search_from + param_start;
let after_param_eq = &block[abs_start + "<parameter=".len()..];
let Some(key_end) = after_param_eq.find('>') else {
break;
};
let key = after_param_eq[..key_end].trim();
let value_start = abs_start + "<parameter=".len() + key_end + 1;
let Some(param_end) = block[value_start..].find("</parameter>") else {
break;
};
let value = block[value_start..value_start + param_end].trim();
args.insert(key.to_string(), Value::String(value.to_string()));
search_from = value_start + param_end + "</parameter>".len();
}
if func_name.is_empty() {
None
} else {
Some((func_name, Value::Object(args)))
}
}
fn split_thinking(output: &str) -> Option<(String, String)> {
let start = output.find("<think>")?;
let end = output.find("</think>").unwrap_or(output.len());
let reasoning = output[start + "<think>".len()..end].trim().to_string();
let mut text = String::new();
text.push_str(&output[..start]);
if end < output.len() {
text.push_str(&output[end.min(output.len())..]);
}
let text = text.replace("</think>", "");
Some((reasoning, text.trim().to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_structured_json_plain_object() {
let out = extract_structured_json(r#"{"name":"Ada","age":36}"#).unwrap();
assert_eq!(out, r#"{"name":"Ada","age":36}"#);
}
#[test]
fn extract_structured_json_strips_markdown_fence() {
let raw = "```json\n{\n \"name\": \"Ada\",\n \"age\": 36\n}\n```";
let out = extract_structured_json(raw).unwrap();
assert_eq!(out, "{\n \"name\": \"Ada\",\n \"age\": 36\n}");
}
#[test]
fn extract_structured_json_strips_plain_fence() {
let raw = "```\n{\"ok\": true}\n```";
let out = extract_structured_json(raw).unwrap();
assert_eq!(out, r#"{"ok": true}"#);
}
#[test]
fn extract_structured_json_strips_chatml_role_prefix() {
let raw = "<|im_start|>assistant\n```json\n{\"value\": 1}\n```";
let out = extract_structured_json(raw).unwrap();
assert_eq!(out, r#"{"value": 1}"#);
}
#[test]
fn extract_structured_json_strips_leading_prose() {
let raw = "Sure, here is the answer: {\"answer\": 42}";
let out = extract_structured_json(raw).unwrap();
assert_eq!(out, r#"{"answer": 42}"#);
}
#[test]
fn extract_structured_json_handles_nested_objects() {
let raw = r#"```json
{"person": {"name": "Ada", "skills": {"lang": "rust"}}, "age": 36}
```"#;
let out = extract_structured_json(raw).unwrap();
assert_eq!(
out,
r#"{"person": {"name": "Ada", "skills": {"lang": "rust"}}, "age": 36}"#
);
}
#[test]
fn extract_structured_json_ignores_braces_inside_strings() {
let raw = r#"{"text": "an { inside } string", "ok": true}"#;
let out = extract_structured_json(raw).unwrap();
assert_eq!(out, raw);
}
#[test]
fn extract_structured_json_handles_escaped_quotes_in_strings() {
let raw = r#"{"text": "she said \"hi\"", "brace": "}"}"#;
let out = extract_structured_json(raw).unwrap();
assert_eq!(out, raw);
}
#[test]
fn extract_structured_json_stops_at_first_balanced_object() {
let raw = r#"{"first": 1} and then {"second": 2}"#;
let out = extract_structured_json(raw).unwrap();
assert_eq!(out, r#"{"first": 1}"#);
}
#[test]
fn extract_structured_json_returns_none_when_unbalanced() {
assert!(extract_structured_json(r#"{"broken": "#).is_none());
}
#[test]
fn extract_structured_json_returns_none_when_no_object() {
assert!(extract_structured_json("just plain text, no json").is_none());
assert!(extract_structured_json("").is_none());
}
#[test]
fn extract_structured_json_handles_real_qwen_output() {
let raw = "<|im_start|>assistant\n```json\n{\n \"age\": 36,\n \"name\": \"Ada\",\n \"occupation\": \"Software Engineer\"\n}\n```";
let out = extract_structured_json(raw).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(parsed["name"], "Ada");
assert_eq!(parsed["age"], 36);
assert_eq!(parsed["occupation"], "Software Engineer");
}
#[test]
fn parse_xml_tool_calls_json_form() {
let raw = "<tool_call>\n{\"name\": \"get_time\", \"arguments\": {\"timezone\": \"UTC\"}}\n</tool_call>";
let out = parse_xml_tool_calls(raw).unwrap();
assert_eq!(out.len(), 1);
assert_eq!(out[0].0, "get_time");
assert_eq!(out[0].1["timezone"], "UTC");
}
#[test]
fn parse_xml_tool_calls_parameter_form() {
let raw = "<tool_call>\n<function=write_file>\n<parameter=path>\noutput.txt\n</parameter>\n<parameter=content>\nHello\n</parameter>\n</function>\n</tool_call>";
let out = parse_xml_tool_calls(raw).unwrap();
assert_eq!(out.len(), 1);
assert_eq!(out[0].0, "write_file");
assert_eq!(out[0].1["path"], "output.txt");
assert_eq!(out[0].1["content"], "Hello");
}
#[test]
fn parse_xml_tool_calls_multiple_blocks() {
let raw = "<tool_call>{\"name\": \"a\", \"arguments\": {}}</tool_call>\n<tool_call>{\"name\": \"b\", \"arguments\": {}}</tool_call>";
let out = parse_xml_tool_calls(raw).unwrap();
assert_eq!(out.len(), 2);
assert_eq!(out[0].0, "a");
assert_eq!(out[1].0, "b");
}
#[test]
fn split_thinking_extracts_reasoning() {
let raw = "<think>let me consider</think>The answer is 42.";
let (reasoning, text) = split_thinking(raw).unwrap();
assert_eq!(reasoning, "let me consider");
assert_eq!(text, "The answer is 42.");
}
}