use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolFunction {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default = "default_params")]
pub parameters: Value,
}
fn default_params() -> Value {
serde_json::json!({"type": "object", "properties": {}})
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tool {
#[serde(rename = "type", default = "default_tool_type")]
pub kind: String,
pub function: ToolFunction,
}
fn default_tool_type() -> String {
"function".to_string()
}
impl Tool {
pub fn new(name: impl Into<String>, description: impl Into<String>, parameters: Value) -> Self {
Tool {
kind: "function".to_string(),
function: ToolFunction {
name: name.into(),
description: Some(description.into()),
parameters,
},
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolCallFormat {
Hermes,
Gemma,
None,
}
pub fn parse_tool_calls(text: &str, format: ToolCallFormat) -> Vec<ToolCall> {
match format {
ToolCallFormat::Hermes => parse_hermes(text),
ToolCallFormat::Gemma => parse_gemma(text),
ToolCallFormat::None => Vec::new(),
}
}
fn parse_hermes(text: &str) -> Vec<ToolCall> {
const OPEN: &str = "<tool_call>";
const CLOSE: &str = "</tool_call>";
let mut calls = Vec::new();
let mut rest = text;
let mut idx = 0usize;
while let Some(start) = rest.find(OPEN) {
let after_open = &rest[start + OPEN.len()..];
let Some(end) = after_open.find(CLOSE) else {
break;
};
let payload = after_open[..end].trim();
if let Ok(v) = serde_json::from_str::<Value>(payload) {
let name = v
.get("name")
.and_then(|n| n.as_str())
.unwrap_or_default()
.to_string();
let arguments = v.get("arguments").cloned().unwrap_or(Value::Null);
if !name.is_empty() {
calls.push(ToolCall {
id: format!("call_{idx}"),
name,
arguments,
});
idx += 1;
}
}
rest = &after_open[end + CLOSE.len()..];
}
calls
}
fn parse_gemma(text: &str) -> Vec<ToolCall> {
const OPEN: &str = "<|tool_call>call:";
const CLOSE: &str = "<tool_call|>";
let mut calls = Vec::new();
let mut rest = text;
let mut idx = 0usize;
while let Some(start) = rest.find(OPEN) {
let after_open = &rest[start + OPEN.len()..];
let Some(end) = after_open.find(CLOSE) else {
break;
};
let body = &after_open[..end];
if let Some((name, call)) = parse_gemma_call(body) {
calls.push(ToolCall {
id: format!("call_{idx}"),
name,
arguments: call,
});
idx += 1;
}
rest = &after_open[end + CLOSE.len()..];
}
calls
}
fn parse_gemma_call(body: &str) -> Option<(String, Value)> {
let brace_start = body.find('{')?;
let name = body[..brace_start].trim().to_string();
let brace_end = body.rfind('}')?;
let args_body = &body[brace_start + 1..brace_end];
let mut obj = serde_json::Map::new();
for pair in split_top_level(args_body, ',') {
let Some(colon) = pair.find(':') else {
continue;
};
let key = pair[..colon].trim().to_string();
let raw_value = pair[colon + 1..].trim();
obj.insert(key, parse_gemma_value(raw_value));
}
Some((name, Value::Object(obj)))
}
fn parse_gemma_value(raw: &str) -> Value {
if raw.starts_with('[') && raw.ends_with(']') {
let inner = &raw[1..raw.len() - 1];
let items = split_top_level(inner, ',')
.into_iter()
.filter(|s| !s.trim().is_empty())
.map(|s| parse_gemma_scalar(s.trim()))
.collect();
Value::Array(items)
} else {
parse_gemma_scalar(raw)
}
}
fn parse_gemma_scalar(raw: &str) -> Value {
let unquoted = raw.trim().trim_matches('"').trim_matches('\'');
if let Ok(i) = unquoted.parse::<i64>() {
Value::from(i)
} else if let Ok(f) = unquoted.parse::<f64>() {
Value::from(f)
} else if unquoted == "true" || unquoted == "false" {
Value::from(unquoted == "true")
} else {
Value::from(unquoted.to_string())
}
}
fn split_top_level(s: &str, sep: char) -> Vec<&str> {
let mut parts = Vec::new();
let mut depth = 0i32;
let mut start = 0usize;
for (i, c) in s.char_indices() {
match c {
'[' => depth += 1,
']' => depth -= 1,
c if c == sep && depth == 0 => {
parts.push(&s[start..i]);
start = i + 1;
}
_ => {}
}
}
parts.push(&s[start..]);
parts
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hermes_single_call() {
let text = r#"sure, let me check.<tool_call>{"name": "get_weather", "arguments": {"location": "Paris"}}</tool_call>"#;
let calls = parse_tool_calls(text, ToolCallFormat::Hermes);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].name, "get_weather");
assert_eq!(calls[0].arguments["location"], "Paris");
}
#[test]
fn hermes_multiple_calls() {
let text = r#"<tool_call>{"name": "a", "arguments": {}}</tool_call>text<tool_call>{"name": "b", "arguments": {"x": 1}}</tool_call>"#;
let calls = parse_tool_calls(text, ToolCallFormat::Hermes);
assert_eq!(calls.len(), 2);
assert_eq!(calls[0].name, "a");
assert_eq!(calls[1].name, "b");
assert_eq!(calls[1].arguments["x"], 1);
}
#[test]
fn hermes_no_call() {
let calls = parse_tool_calls("just plain text", ToolCallFormat::Hermes);
assert!(calls.is_empty());
}
#[test]
fn hermes_malformed_json_is_skipped() {
let text = "<tool_call>{not json}</tool_call>";
let calls = parse_tool_calls(text, ToolCallFormat::Hermes);
assert!(calls.is_empty());
}
#[test]
fn gemma_scalar_and_string_args() {
let text = "<|tool_call>call:get_weather{location:\"Paris\",units:celsius}<tool_call|>";
let calls = parse_tool_calls(text, ToolCallFormat::Gemma);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].name, "get_weather");
assert_eq!(calls[0].arguments["location"], "Paris");
assert_eq!(calls[0].arguments["units"], "celsius");
}
#[test]
fn gemma_list_args() {
let text = "<|tool_call>call:sum{values:[1,2,3]}<tool_call|>";
let calls = parse_tool_calls(text, ToolCallFormat::Gemma);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].arguments["values"], serde_json::json!([1, 2, 3]));
}
#[test]
fn none_format_is_noop() {
let calls = parse_tool_calls(
"<tool_call>{\"name\":\"x\",\"arguments\":{}}</tool_call>",
ToolCallFormat::None,
);
assert!(calls.is_empty());
}
#[test]
fn tool_serializes_openai_shape() {
let tool = Tool::new(
"get_weather",
"Get weather for a location",
serde_json::json!({
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"],
}),
);
let v = serde_json::to_value(&tool).unwrap();
assert_eq!(v["type"], "function");
assert_eq!(v["function"]["name"], "get_weather");
}
}