use serde_json::{json, Map, Value};
pub fn normalise_tool(value: &Value) -> Value {
let Some(obj) = value.as_object() else {
return value.clone();
};
if obj.get("function").and_then(Value::as_object).is_some() {
return value.clone();
}
if obj.get("type").and_then(Value::as_str) == Some("function") && obj.contains_key("name") {
return wrap_function(obj);
}
if obj.contains_key("input_schema") && obj.contains_key("name") {
let mut inner = Map::new();
insert_str(&mut inner, "name", obj.get("name"));
insert_str(&mut inner, "description", obj.get("description"));
if let Some(schema) = obj.get("input_schema") {
inner.insert("parameters".into(), schema.clone());
}
return json!({ "type": "function", "function": Value::Object(inner) });
}
if obj.contains_key("name") && (obj.contains_key("parameters") || obj.contains_key("description")) {
return wrap_function(obj);
}
value.clone()
}
fn wrap_function(obj: &Map<String, Value>) -> Value {
let mut inner = Map::new();
insert_str(&mut inner, "name", obj.get("name"));
insert_str(&mut inner, "description", obj.get("description"));
if let Some(params) = obj.get("parameters") {
inner.insert("parameters".into(), params.clone());
}
if let Some(strict) = obj.get("strict") {
inner.insert("strict".into(), strict.clone());
}
json!({ "type": "function", "function": Value::Object(inner) })
}
fn insert_str(out: &mut Map<String, Value>, key: &str, src: Option<&Value>) {
if let Some(s) = src.and_then(Value::as_str) {
out.insert(key.into(), Value::String(s.to_string()));
}
}
pub fn normalise_tools(tools: &[Value]) -> Vec<Value> {
tools.iter().map(normalise_tool).collect()
}
pub fn tool_to_responses(value: &Value) -> Value {
let Some(obj) = value.as_object() else {
return value.clone();
};
let Some(func) = obj.get("function").and_then(Value::as_object) else {
return value.clone();
};
let mut out = Map::new();
out.insert("type".into(), Value::String("function".into()));
insert_str(&mut out, "name", func.get("name"));
insert_str(&mut out, "description", func.get("description"));
if let Some(params) = func.get("parameters") {
out.insert("parameters".into(), params.clone());
}
if let Some(strict) = func.get("strict") {
out.insert("strict".into(), strict.clone());
}
Value::Object(out)
}
pub fn tool_to_messages(value: &Value) -> Value {
let Some(obj) = value.as_object() else {
return value.clone();
};
let Some(func) = obj.get("function").and_then(Value::as_object) else {
return value.clone();
};
let mut out = Map::new();
insert_str(&mut out, "name", func.get("name"));
insert_str(&mut out, "description", func.get("description"));
let schema = func
.get("parameters")
.cloned()
.unwrap_or_else(|| json!({ "type": "object", "properties": {} }));
out.insert("input_schema".into(), schema);
Value::Object(out)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn responses_shape_is_normalised_to_chat() {
let input = json!({
"type": "function",
"name": "exec_command",
"description": "Run a command.",
"parameters": { "type": "object", "properties": {} },
"strict": false,
});
let n = normalise_tool(&input);
assert_eq!(n["type"], "function");
assert_eq!(n["function"]["name"], "exec_command");
assert_eq!(n["function"]["description"], "Run a command.");
assert_eq!(n["function"]["strict"], false);
assert!(n["function"]["parameters"].is_object());
}
#[test]
fn chat_shape_is_left_untouched() {
let input = json!({
"type": "function",
"function": { "name": "foo", "description": "bar", "parameters": {} },
});
assert_eq!(normalise_tool(&input), input);
}
#[test]
fn messages_shape_is_normalised_to_chat() {
let input = json!({
"name": "search",
"description": "Search the web.",
"input_schema": { "type": "object" },
});
let n = normalise_tool(&input);
assert_eq!(n["function"]["name"], "search");
assert_eq!(n["function"]["parameters"], json!({"type": "object"}));
}
#[test]
fn unknown_shape_passes_through() {
let input = json!({ "type": "web_search", "max_results": 5 });
assert_eq!(normalise_tool(&input), input);
}
#[test]
fn chat_to_responses_drops_function_envelope() {
let chat = json!({
"type": "function",
"function": { "name": "foo", "description": "bar", "parameters": { "type": "object" } },
});
let resp = tool_to_responses(&chat);
assert_eq!(resp["type"], "function");
assert_eq!(resp["name"], "foo");
assert_eq!(resp["description"], "bar");
assert_eq!(resp["parameters"], json!({ "type": "object" }));
}
#[test]
fn chat_to_messages_uses_input_schema() {
let chat = json!({
"type": "function",
"function": { "name": "foo", "parameters": { "type": "object" } },
});
let m = tool_to_messages(&chat);
assert_eq!(m["name"], "foo");
assert_eq!(m["input_schema"], json!({ "type": "object" }));
assert!(m.get("type").is_none());
}
}