use serde_json::{json, Map, Value};
use crate::{ToolChoice, UniversalTool};
use super::super::common::empty_extensions;
pub(crate) fn tool_choice_from_openai_value(value: Option<&Value>) -> Option<ToolChoice> {
match value {
Some(Value::String(value)) => match value.as_str() {
"auto" => Some(ToolChoice::Auto),
"none" => Some(ToolChoice::None),
"required" => Some(ToolChoice::Required),
_ => None,
},
Some(Value::Object(object)) => object
.get("function")
.and_then(Value::as_object)
.and_then(|function| function.get("name"))
.or_else(|| object.get("name"))
.and_then(Value::as_str)
.map(|name| ToolChoice::Tool {
name: name.to_string(),
}),
_ => None,
}
}
pub(crate) fn tool_choice_to_openai(value: &ToolChoice) -> Value {
match value {
ToolChoice::Auto => json!("auto"),
ToolChoice::None => json!("none"),
ToolChoice::Required => json!("required"),
ToolChoice::Tool { name } => json!({
"type": "function",
"function": { "name": name }
}),
}
}
pub(crate) fn tool_choice_to_openai_responses(value: &ToolChoice) -> Value {
match value {
ToolChoice::Auto => json!("auto"),
ToolChoice::None => json!("none"),
ToolChoice::Required => json!("required"),
ToolChoice::Tool { name } => json!({
"type": "function",
"name": name
}),
}
}
pub(crate) fn openai_tool_from_value(value: &Value) -> Option<UniversalTool> {
let object = value.as_object()?;
let function = object
.get("function")
.and_then(Value::as_object)
.unwrap_or(object);
let name = function
.get("name")
.and_then(Value::as_str)
.map(str::trim)
.filter(|name| !name.is_empty())?;
Some(UniversalTool {
name: name.to_string(),
description: function
.get("description")
.and_then(Value::as_str)
.map(ToString::to_string),
input_schema: function
.get("parameters")
.or_else(|| function.get("input_schema"))
.cloned(),
extensions: empty_extensions(),
})
}
pub(crate) fn tool_to_openai_chat(tool: &UniversalTool) -> Value {
let mut function = Map::new();
function.insert("name".to_string(), Value::String(tool.name.clone()));
if let Some(description) = &tool.description {
function.insert(
"description".to_string(),
Value::String(description.clone()),
);
}
function.insert(
"parameters".to_string(),
sanitize_openai_parameters(tool.input_schema.as_ref()),
);
json!({
"type": "function",
"function": function
})
}
pub(crate) fn tool_to_openai_responses(tool: &UniversalTool) -> Value {
let mut object = Map::new();
object.insert("type".to_string(), Value::String("function".to_string()));
object.insert("name".to_string(), Value::String(tool.name.clone()));
if let Some(description) = &tool.description {
object.insert(
"description".to_string(),
Value::String(description.clone()),
);
}
object.insert(
"parameters".to_string(),
sanitize_openai_parameters(tool.input_schema.as_ref()),
);
Value::Object(object)
}
fn sanitize_openai_parameters(input_schema: Option<&Value>) -> Value {
let Some(Value::Object(object)) = input_schema else {
return empty_object_schema();
};
if object.is_empty() {
return empty_object_schema();
}
Value::Object(object.clone())
}
fn empty_object_schema() -> Value {
json!({
"type": "object",
"properties": {}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn skips_tools_with_blank_names() {
assert!(openai_tool_from_value(&json!({
"type": "function",
"name": " ",
"parameters": { "type": "object" }
}))
.is_none());
}
#[test]
fn chat_tools_always_include_non_empty_parameters() {
let tool = UniversalTool {
name: "list_files".to_string(),
description: None,
input_schema: None,
extensions: Default::default(),
};
let encoded = tool_to_openai_chat(&tool);
assert_eq!(
encoded["function"]["parameters"],
json!({
"type": "object",
"properties": {}
})
);
}
#[test]
fn responses_tools_normalize_empty_parameter_objects() {
let tool = UniversalTool {
name: "list_files".to_string(),
description: None,
input_schema: Some(json!({})),
extensions: Default::default(),
};
let encoded = tool_to_openai_responses(&tool);
assert_eq!(
encoded["parameters"],
json!({
"type": "object",
"properties": {}
})
);
}
}