use regex::Regex;
use serde_json::Value;
use std::collections::HashMap;
use tracing::warn;
use uuid::Uuid;
use super::super::ToolDefinition;
use super::super::config::Glm47ParserConfig;
use super::response::{CalledFunction, ToolCallResponse, ToolCallType};
pub fn detect_tool_call_start_glm47(chunk: &str, config: &Glm47ParserConfig) -> bool {
let start_token = &config.tool_call_start;
if chunk.contains(start_token.as_str()) {
return true;
}
for i in 1..start_token.len() {
if chunk.ends_with(&start_token[..i]) {
return true;
}
}
false
}
pub fn find_tool_call_end_position_glm47(chunk: &str, config: &Glm47ParserConfig) -> usize {
let end_token = &config.tool_call_end;
if let Some(pos) = chunk.find(end_token.as_str()) {
pos + end_token.len()
} else {
chunk.len()
}
}
pub fn try_tool_call_parse_glm47(
message: &str,
config: &Glm47ParserConfig,
tools: Option<&[ToolDefinition]>,
) -> anyhow::Result<(Vec<ToolCallResponse>, Option<String>)> {
let (normal_text, tool_calls) = extract_tool_calls(message, config, tools)?;
let normal_content = if normal_text.is_empty() {
Some("".to_string())
} else {
Some(normal_text)
};
Ok((tool_calls, normal_content))
}
fn extract_tool_calls(
text: &str,
config: &Glm47ParserConfig,
tools: Option<&[ToolDefinition]>,
) -> anyhow::Result<(String, Vec<ToolCallResponse>)> {
let mut normal_parts = Vec::new();
let mut calls = Vec::new();
let mut cursor = 0;
let start_token = &config.tool_call_start;
let end_token = &config.tool_call_end;
while cursor < text.len() {
if let Some(start_pos) = text[cursor..].find(start_token.as_str()) {
let abs_start = cursor + start_pos;
normal_parts.push(&text[cursor..abs_start]);
if let Some(end_pos) = text[abs_start..].find(end_token.as_str()) {
let abs_end = abs_start + end_pos + end_token.len();
let block = &text[abs_start..abs_end];
match parse_tool_call_block(block, config, tools) {
Ok(parsed_call) => calls.push(parsed_call),
Err(e) => {
warn!("Failed to parse GLM-4.7 tool call block: {e}");
normal_parts.push(block);
}
}
cursor = abs_end;
} else {
let block = &text[abs_start..];
let arg_key_start = &config.arg_key_start;
if config.allow_eof_recovery && block.contains(arg_key_start.as_str()) {
match parse_tool_call_block(block, config, tools) {
Ok(parsed_call) => {
calls.push(parsed_call);
cursor = text.len();
continue;
}
Err(e) => {
warn!("Failed to parse GLM-4.7 tool call block (no end token): {e}");
}
}
}
normal_parts.push(&text[abs_start..]);
break;
}
} else {
normal_parts.push(&text[cursor..]);
break;
}
}
let normal_text = normal_parts.join("").trim().to_string();
Ok((normal_text, calls))
}
fn decode_xml_entities(s: &str) -> String {
s.replace("<", "<")
.replace(">", ">")
.replace("&", "&")
.replace(""", "\"")
.replace("'", "'")
}
fn coerce_value(raw: &str, schema_type: Option<&str>) -> Value {
let trimmed = raw.trim();
if (trimmed.starts_with('{') || trimmed.starts_with('[') || trimmed.starts_with('"'))
&& let Ok(v) = serde_json::from_str(trimmed)
{
return v;
}
match schema_type {
Some("integer") | Some("int") => {
if let Ok(n) = trimmed.parse::<i64>() {
return Value::Number(n.into());
}
}
Some("number") | Some("float") | Some("double") => {
if let Ok(n) = trimmed.parse::<f64>()
&& let Some(num) = serde_json::Number::from_f64(n)
{
return Value::Number(num);
}
}
Some("boolean") | Some("bool") => match trimmed.to_lowercase().as_str() {
"true" | "1" | "yes" => return Value::Bool(true),
"false" | "0" | "no" => return Value::Bool(false),
_ => {}
},
Some("array") => {
if let Ok(v) = serde_json::from_str::<Value>(trimmed)
&& v.is_array()
{
return v;
}
let items: Vec<Value> = trimmed
.split(',')
.map(|s| Value::String(s.trim().to_string()))
.collect();
return Value::Array(items);
}
Some("null") => {
if trimmed == "null" || trimmed == "None" || trimmed.is_empty() {
return Value::Null;
}
}
_ => {}
}
Value::String(raw.to_string())
}
fn get_param_schema_type<'a>(
tools: Option<&'a [ToolDefinition]>,
function_name: &str,
param_name: &str,
) -> Option<&'a str> {
let tool = tools?.iter().find(|t| t.name == function_name)?;
let schema = tool.parameters.as_ref()?;
let props = schema.get("properties")?;
let param = props.get(param_name)?;
param.get("type")?.as_str()
}
fn parse_tool_call_block(
block: &str,
config: &Glm47ParserConfig,
tools: Option<&[ToolDefinition]>,
) -> anyhow::Result<ToolCallResponse> {
let start_token = &config.tool_call_start;
let end_token = &config.tool_call_end;
let after_start = block
.strip_prefix(start_token.as_str())
.ok_or_else(|| anyhow::anyhow!("Invalid tool call block format"))?;
let content = after_start
.strip_suffix(end_token.as_str())
.unwrap_or(after_start);
let arg_key_start = &config.arg_key_start;
let function_name = if let Some(pos) = content.find(arg_key_start.as_str()) {
content[..pos].trim().to_string()
} else {
content.trim().to_string()
};
if function_name.is_empty() {
anyhow::bail!("Empty function name in tool call");
}
let mut arguments = HashMap::new();
let args_section = &content[function_name.len()..];
let arg_key_start_escaped = regex::escape(&config.arg_key_start);
let arg_key_end_escaped = regex::escape(&config.arg_key_end);
let arg_value_start_escaped = regex::escape(&config.arg_value_start);
let arg_value_end_escaped = regex::escape(&config.arg_value_end);
let pattern = format!(
r"(?s){}([^<]+){}{}(.*?){}",
arg_key_start_escaped, arg_key_end_escaped, arg_value_start_escaped, arg_value_end_escaped
);
let regex = Regex::new(&pattern)?;
for cap in regex.captures_iter(args_section) {
let key = cap.get(1).map(|m| m.as_str().trim()).unwrap_or("");
let raw_value = cap.get(2).map(|m| m.as_str()).unwrap_or("");
if !key.is_empty() {
let decoded = decode_xml_entities(raw_value);
let schema_type = get_param_schema_type(tools, &function_name, key);
let json_value = coerce_value(&decoded, schema_type);
arguments.insert(key.to_string(), json_value);
}
}
if let Some(tools_list) = tools {
let tool_exists = tools_list.iter().any(|t| t.name == function_name);
if !tool_exists {
anyhow::bail!("Function '{}' not found in available tools", function_name);
}
}
Ok(ToolCallResponse {
id: Uuid::new_v4().to_string(),
tp: ToolCallType::Function,
function: CalledFunction {
name: function_name,
arguments: serde_json::to_string(&arguments)?,
},
})
}
#[cfg(test)]
mod tests {
use super::*;
fn get_test_config() -> Glm47ParserConfig {
Glm47ParserConfig::default()
}
#[test] fn test_detect_tool_call_start() {
let config = get_test_config();
assert!(detect_tool_call_start_glm47(
"<tool_call>get_weather",
&config
));
assert!(detect_tool_call_start_glm47("Some text <tool", &config));
assert!(detect_tool_call_start_glm47("Some text <tool_c", &config));
assert!(!detect_tool_call_start_glm47("Just normal text", &config));
}
#[test] fn test_parse_simple_tool_call() {
let config = get_test_config();
let message = "<tool_call>get_weather<arg_key>location</arg_key><arg_value>San Francisco</arg_value></tool_call>";
let (calls, normal_text) = try_tool_call_parse_glm47(message, &config, None).unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].function.name, "get_weather");
let args: HashMap<String, Value> =
serde_json::from_str(&calls[0].function.arguments).unwrap();
assert_eq!(
args.get("location").unwrap().as_str().unwrap(),
"San Francisco"
);
assert_eq!(normal_text, Some("".to_string()));
}
#[test] fn test_parse_tool_call_with_multiple_args() {
let config = get_test_config();
let message = "<tool_call>book_flight<arg_key>from</arg_key><arg_value>NYC</arg_value><arg_key>to</arg_key><arg_value>LAX</arg_value><arg_key>date</arg_key><arg_value>2026-03-15</arg_value></tool_call>";
let (calls, _) = try_tool_call_parse_glm47(message, &config, None).unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].function.name, "book_flight");
let args: HashMap<String, Value> =
serde_json::from_str(&calls[0].function.arguments).unwrap();
assert_eq!(args.get("from").unwrap().as_str().unwrap(), "NYC");
assert_eq!(args.get("to").unwrap().as_str().unwrap(), "LAX");
assert_eq!(args.get("date").unwrap().as_str().unwrap(), "2026-03-15");
}
#[test] fn test_parse_tool_call_with_json_value() {
let config = get_test_config();
let message = r#"<tool_call>search<arg_key>filters</arg_key><arg_value>{"category": "books", "price_max": 50}</arg_value></tool_call>"#;
let (calls, _) = try_tool_call_parse_glm47(message, &config, None).unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].function.name, "search");
let args: HashMap<String, Value> =
serde_json::from_str(&calls[0].function.arguments).unwrap();
let filters = args.get("filters").unwrap();
assert!(filters.is_object());
}
#[test] fn test_parse_multiple_tool_calls() {
let config = get_test_config();
let message = "<tool_call>get_weather<arg_key>location</arg_key><arg_value>NYC</arg_value></tool_call><tool_call>get_time<arg_key>timezone</arg_key><arg_value>EST</arg_value></tool_call>";
let (calls, _) = try_tool_call_parse_glm47(message, &config, None).unwrap();
assert_eq!(calls.len(), 2);
assert_eq!(calls[0].function.name, "get_weather");
assert_eq!(calls[1].function.name, "get_time");
}
#[test] fn test_parse_with_normal_text() {
let config = get_test_config();
let message = "I'll check the weather for you. <tool_call>get_weather<arg_key>location</arg_key><arg_value>Paris</arg_value></tool_call>";
let (calls, normal_text) = try_tool_call_parse_glm47(message, &config, None).unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].function.name, "get_weather");
assert_eq!(
normal_text,
Some("I'll check the weather for you.".to_string())
);
}
#[test] fn test_parse_tool_call_no_args() {
let config = get_test_config();
let message = "<tool_call>get_current_time</tool_call>";
let (calls, _) = try_tool_call_parse_glm47(message, &config, None).unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].function.name, "get_current_time");
let args: HashMap<String, Value> =
serde_json::from_str(&calls[0].function.arguments).unwrap();
assert!(args.is_empty());
}
#[test] fn test_find_tool_call_end_position() {
let config = get_test_config();
let chunk =
"<tool_call>func<arg_key>k</arg_key><arg_value>v</arg_value></tool_call>more text";
let end_pos = find_tool_call_end_position_glm47(chunk, &config);
assert_eq!(
&chunk[..end_pos],
"<tool_call>func<arg_key>k</arg_key><arg_value>v</arg_value></tool_call>"
);
}
#[test] fn test_parse_multiline_arg_value() {
let config = get_test_config();
let message = "<tool_call>write_file<arg_key>path</arg_key><arg_value>/tmp/hello.py</arg_value><arg_key>content</arg_key><arg_value>#!/usr/bin/env python3\nprint(\"Hello, World!\")\n</arg_value></tool_call>";
let (calls, _) = try_tool_call_parse_glm47(message, &config, None).unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].function.name, "write_file");
let args: HashMap<String, Value> =
serde_json::from_str(&calls[0].function.arguments).unwrap();
assert_eq!(args.get("path").unwrap().as_str().unwrap(), "/tmp/hello.py");
assert!(
args.contains_key("content"),
"content argument must be parsed even when it contains newlines"
);
let content = args.get("content").unwrap().as_str().unwrap();
assert!(content.contains("print(\"Hello, World!\")"));
}
#[test] fn test_malformed_tool_call() {
let config = get_test_config();
let message = "<tool_call>get_weather";
let result = try_tool_call_parse_glm47(message, &config, None);
assert!(result.is_ok());
let (calls, _) = result.unwrap();
assert_eq!(calls.len(), 0);
}
#[test] fn test_parse_no_end_tag_complete_args_recovers() {
let config = Glm47ParserConfig {
allow_eof_recovery: true,
..get_test_config()
};
let message = "<tool_call>get_weather<arg_key>location</arg_key><arg_value>NYC</arg_value>";
let (calls, _) = try_tool_call_parse_glm47(message, &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["location"], "NYC");
}
#[test] fn test_parse_no_end_tag_multiple_calls_recovers() {
let config = Glm47ParserConfig {
allow_eof_recovery: true,
..get_test_config()
};
let message = "<tool_call>get_weather<arg_key>city</arg_key><arg_value>NYC</arg_value></tool_call><tool_call>get_time<arg_key>tz</arg_key><arg_value>EST</arg_value>";
let (calls, _) = try_tool_call_parse_glm47(message, &config, None).unwrap();
assert_eq!(calls.len(), 2);
assert_eq!(calls[0].function.name, "get_weather");
assert_eq!(calls[1].function.name, "get_time");
}
#[test] fn test_unparseable_block_preserved_as_normal_text() {
let config = get_test_config();
let tools = vec![ToolDefinition {
name: "get_weather".to_string(),
parameters: None,
}];
let message = "Here is the result: <tool_call>unknown_func<arg_key>x</arg_key><arg_value>1</arg_value></tool_call> done";
let (calls, normal_text) =
try_tool_call_parse_glm47(message, &config, Some(&tools)).unwrap();
assert_eq!(calls.len(), 0);
let text = normal_text.unwrap();
assert!(
text.contains("unknown_func"),
"Unparseable block should be in normal text, got: {text}"
);
}
#[test] fn test_xml_entity_decoding() {
let config = get_test_config();
let message = r#"<tool_call>write_file<arg_key>content</arg_key><arg_value>x < y && y > z</arg_value></tool_call>"#;
let (calls, _) = try_tool_call_parse_glm47(message, &config, None).unwrap();
assert_eq!(calls.len(), 1);
let args: HashMap<String, Value> =
serde_json::from_str(&calls[0].function.arguments).unwrap();
assert_eq!(
args.get("content").unwrap().as_str().unwrap(),
"x < y && y > z"
);
}
#[test] fn test_type_coercion_with_schema() {
let config = get_test_config();
let tools = vec![ToolDefinition {
name: "set_temperature".to_string(),
parameters: Some(serde_json::json!({
"type": "object",
"properties": {
"degrees": {"type": "number"},
"enabled": {"type": "boolean"},
"count": {"type": "integer"},
"label": {"type": "string"}
}
})),
}];
let message = "<tool_call>set_temperature<arg_key>degrees</arg_key><arg_value>72.5</arg_value><arg_key>enabled</arg_key><arg_value>true</arg_value><arg_key>count</arg_key><arg_value>3</arg_value><arg_key>label</arg_key><arg_value>warm</arg_value></tool_call>";
let (calls, _) = try_tool_call_parse_glm47(message, &config, Some(&tools)).unwrap();
assert_eq!(calls.len(), 1);
let args: HashMap<String, Value> =
serde_json::from_str(&calls[0].function.arguments).unwrap();
assert_eq!(args.get("degrees").unwrap().as_f64().unwrap(), 72.5);
assert!(args.get("enabled").unwrap().as_bool().unwrap());
assert_eq!(args.get("count").unwrap().as_i64().unwrap(), 3);
assert_eq!(args.get("label").unwrap().as_str().unwrap(), "warm");
}
#[test] fn test_type_coercion_array_comma_separated() {
let config = get_test_config();
let tools = vec![ToolDefinition {
name: "tag_item".to_string(),
parameters: Some(serde_json::json!({
"type": "object",
"properties": {
"tags": {"type": "array"}
}
})),
}];
let message = "<tool_call>tag_item<arg_key>tags</arg_key><arg_value>rust, python, go</arg_value></tool_call>";
let (calls, _) = try_tool_call_parse_glm47(message, &config, Some(&tools)).unwrap();
let args: HashMap<String, Value> =
serde_json::from_str(&calls[0].function.arguments).unwrap();
let tags = args.get("tags").unwrap().as_array().unwrap();
assert_eq!(tags.len(), 3);
assert_eq!(tags[0].as_str().unwrap(), "rust");
assert_eq!(tags[1].as_str().unwrap(), "python");
assert_eq!(tags[2].as_str().unwrap(), "go");
}
#[test] fn test_type_coercion_array_json() {
let config = get_test_config();
let tools = vec![ToolDefinition {
name: "tag_item".to_string(),
parameters: Some(serde_json::json!({
"type": "object",
"properties": {
"ids": {"type": "array"}
}
})),
}];
let message = r#"<tool_call>tag_item<arg_key>ids</arg_key><arg_value>[1, 2, 3]</arg_value></tool_call>"#;
let (calls, _) = try_tool_call_parse_glm47(message, &config, Some(&tools)).unwrap();
let args: HashMap<String, Value> =
serde_json::from_str(&calls[0].function.arguments).unwrap();
let ids = args.get("ids").unwrap().as_array().unwrap();
assert_eq!(ids.len(), 3);
assert_eq!(ids[0].as_i64().unwrap(), 1);
}
#[test] fn test_type_coercion_falls_back_to_string() {
let config = get_test_config();
let tools = vec![ToolDefinition {
name: "test_func".to_string(),
parameters: Some(serde_json::json!({
"type": "object",
"properties": {
"count": {"type": "integer"}
}
})),
}];
let message = "<tool_call>test_func<arg_key>count</arg_key><arg_value>not_a_number</arg_value></tool_call>";
let (calls, _) = try_tool_call_parse_glm47(message, &config, Some(&tools)).unwrap();
let args: HashMap<String, Value> =
serde_json::from_str(&calls[0].function.arguments).unwrap();
assert!(
args.get("count").unwrap().is_string(),
"Should fall back to string when coercion fails"
);
}
#[test]
fn test_glm47_parser_output_independent_of_upstream_finish() {
let config = get_test_config();
let input = "<tool_call>get_weather<arg_key>location</arg_key><arg_value>NYC</arg_value></tool_call>";
let (calls, _) = try_tool_call_parse_glm47(input, &config, None).unwrap();
assert_eq!(calls.len(), 1);
}
#[test] fn test_parse_glm47_empty_and_whitespace_inputs() {
let config = get_test_config();
for input in &["", " ", "\n", "\t\n \t"] {
let (calls, normal) = try_tool_call_parse_glm47(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_glm47_duplicate_calls_same_name() {
let config = get_test_config();
let input = "<tool_call>get_weather<arg_key>location</arg_key><arg_value>NYC</arg_value></tool_call><tool_call>get_weather<arg_key>location</arg_key><arg_value>LA</arg_value></tool_call>";
let (calls, _) = try_tool_call_parse_glm47(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: HashMap<String, Value> =
serde_json::from_str(&calls[0].function.arguments).unwrap();
let args1: HashMap<String, Value> =
serde_json::from_str(&calls[1].function.arguments).unwrap();
assert_eq!(args0.get("location").unwrap().as_str().unwrap(), "NYC");
assert_eq!(args1.get("location").unwrap().as_str().unwrap(), "LA");
}
}