use std::collections::HashMap;
use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::error::OpenRouterError;
#[derive(Serialize, Deserialize, Debug, Clone)]
#[non_exhaustive]
pub struct Tool {
#[serde(rename = "type")]
pub tool_type: String,
pub function: FunctionDefinition,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<Value>,
}
impl Tool {
pub fn builder() -> ToolBuilder {
ToolBuilder::default()
}
pub fn new(name: &str, description: &str, parameters: Value) -> Self {
Self {
tool_type: "function".to_string(),
function: FunctionDefinition {
name: name.to_string(),
description: description.to_string(),
parameters,
strict: None,
},
cache_control: None,
}
}
}
#[derive(Debug, Default, Clone)]
pub struct ToolBuilder {
tool_type: Option<String>,
name: Option<String>,
description: Option<String>,
parameters: Option<Value>,
strict: Option<bool>,
cache_control: Option<Value>,
}
impl ToolBuilder {
pub fn tool_type(&mut self, tool_type: impl Into<String>) -> &mut Self {
self.tool_type = Some(tool_type.into());
self
}
pub fn function(&mut self, function: FunctionDefinition) -> &mut Self {
self.name = Some(function.name);
self.description = Some(function.description);
self.parameters = Some(function.parameters);
self.strict = function.strict;
self
}
pub fn build(&self) -> Result<Tool, OpenRouterError> {
let name = self
.name
.clone()
.ok_or_else(|| OpenRouterError::ConfigError("Tool name is required".to_string()))?;
Ok(Tool {
tool_type: self
.tool_type
.clone()
.unwrap_or_else(|| "function".to_string()),
function: FunctionDefinition {
name,
description: self.description.clone().unwrap_or_default(),
parameters: self.parameters.clone().unwrap_or(Value::Null),
strict: self.strict,
},
cache_control: self.cache_control.clone(),
})
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
#[builder(build_fn(error = "OpenRouterError"))]
#[non_exhaustive]
pub struct FunctionDefinition {
#[builder(setter(into))]
pub name: String,
#[builder(setter(into))]
pub description: String,
#[builder(setter(custom))]
pub parameters: Value,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
}
impl FunctionDefinition {
pub fn builder() -> FunctionDefinitionBuilder {
FunctionDefinitionBuilder::default()
}
}
impl ToolBuilder {
pub fn name(&mut self, name: &str) -> &mut Self {
self.name = Some(name.to_string());
self
}
pub fn description(&mut self, description: &str) -> &mut Self {
self.description = Some(description.to_string());
self
}
pub fn parameters(&mut self, parameters: Value) -> &mut Self {
self.parameters = Some(parameters);
self
}
pub fn parameters_from<T: Serialize>(
&mut self,
params: &T,
) -> Result<&mut Self, OpenRouterError> {
let value = serde_json::to_value(params).map_err(OpenRouterError::Serialization)?;
Ok(self.parameters(value))
}
pub fn parameters_json(&mut self, json: &str) -> Result<&mut Self, OpenRouterError> {
let value: Value = serde_json::from_str(json).map_err(OpenRouterError::Serialization)?;
Ok(self.parameters(value))
}
pub fn strict(&mut self, strict: bool) -> &mut Self {
self.strict = Some(strict);
self
}
pub fn cache_control(&mut self, cache_control: impl Into<Value>) -> &mut Self {
self.cache_control = Some(cache_control.into());
self
}
}
impl FunctionDefinitionBuilder {
pub fn parameters(&mut self, parameters: Value) -> &mut Self {
self.parameters = Some(parameters);
self
}
pub fn parameters_from<T: Serialize>(
&mut self,
params: &T,
) -> Result<&mut Self, OpenRouterError> {
let value = serde_json::to_value(params).map_err(OpenRouterError::Serialization)?;
self.parameters = Some(value);
Ok(self)
}
pub fn parameters_json(&mut self, json: &str) -> Result<&mut Self, OpenRouterError> {
let value: Value = serde_json::from_str(json).map_err(OpenRouterError::Serialization)?;
self.parameters = Some(value);
Ok(self)
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[non_exhaustive]
pub struct ServerTool {
#[serde(rename = "type")]
pub tool_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<Value>,
#[serde(flatten)]
pub extra: HashMap<String, Value>,
}
impl ServerTool {
pub fn new(tool_type: impl Into<String>) -> Self {
Self {
tool_type: tool_type.into(),
parameters: None,
extra: HashMap::new(),
}
}
pub fn with_parameters(tool_type: impl Into<String>, parameters: impl Into<Value>) -> Self {
Self::new(tool_type).parameters(parameters)
}
pub fn parameters(mut self, parameters: impl Into<Value>) -> Self {
self.parameters = Some(parameters.into());
self
}
pub fn parameters_from<T: Serialize>(mut self, params: &T) -> Result<Self, OpenRouterError> {
self.parameters =
Some(serde_json::to_value(params).map_err(OpenRouterError::Serialization)?);
Ok(self)
}
pub fn option(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
self.extra.insert(key.into(), value.into());
self
}
pub fn web_search() -> Self {
Self::new("openrouter:web_search")
}
pub fn web_search_with_parameters(parameters: impl Into<Value>) -> Self {
Self::with_parameters("openrouter:web_search", parameters)
}
pub fn web_search_preview() -> Self {
Self::new("web_search_preview")
}
pub fn datetime() -> Self {
Self::new("openrouter:datetime")
}
pub fn datetime_with_timezone(timezone: impl Into<String>) -> Self {
Self::with_parameters(
"openrouter:datetime",
serde_json::json!({ "timezone": timezone.into() }),
)
}
pub fn files() -> Self {
Self::new("openrouter:files")
}
pub fn bash() -> Self {
Self::new("openrouter:bash")
}
pub fn web_fetch() -> Self {
Self::new("openrouter:web_fetch")
}
pub fn advisor() -> Self {
Self::new("openrouter:advisor")
}
pub fn subagent() -> Self {
Self::new("openrouter:subagent")
}
pub fn image_generation() -> Self {
Self::new("openrouter:image_generation")
}
pub fn search_models() -> Self {
Self::new("openrouter:experimental__search_models")
}
pub fn apply_patch() -> Self {
Self::new("openrouter:apply_patch")
}
pub(crate) fn is_server_tool_type(tool_type: &str) -> bool {
tool_type.starts_with("openrouter:")
|| matches!(
tool_type,
"web_search"
| "web_search_2025_08_26"
| "web_search_preview"
| "web_search_preview_2025_03_11"
| "apply_patch"
| "shell"
| "namespace"
)
}
pub(crate) fn is_files_tool_type(tool_type: &str) -> bool {
matches!(tool_type, "openrouter:files" | "files")
}
pub(crate) fn is_files_tool(&self) -> bool {
Self::is_files_tool_type(&self.tool_type)
}
pub(crate) fn is_server_tool_value(value: &Value) -> bool {
value
.get("type")
.and_then(Value::as_str)
.is_some_and(Self::is_server_tool_type)
}
pub(crate) fn is_files_tool_value(value: &Value) -> bool {
value
.get("type")
.and_then(Value::as_str)
.is_some_and(Self::is_files_tool_type)
}
}
impl From<ServerTool> for Value {
fn from(tool: ServerTool) -> Self {
serde_json::to_value(tool).expect("server tool serialization should not fail")
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[non_exhaustive]
#[serde(untagged)]
pub enum ToolChoice {
String(String),
Specific(SpecificToolChoice),
Server(ServerToolChoice),
}
impl ToolChoice {
pub fn none() -> Self {
Self::String("none".to_string())
}
pub fn auto() -> Self {
Self::String("auto".to_string())
}
pub fn required() -> Self {
Self::String("required".to_string())
}
pub fn force_tool(tool_name: &str) -> Self {
Self::Specific(SpecificToolChoice {
tool_type: "function".to_string(),
function: SpecificToolFunction {
name: tool_name.to_string(),
},
})
}
pub fn force_server_tool(tool_type: impl Into<String>) -> Self {
Self::Server(ServerToolChoice {
tool_type: tool_type.into(),
})
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[non_exhaustive]
pub struct SpecificToolChoice {
#[serde(rename = "type")]
pub tool_type: String,
pub function: SpecificToolFunction,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[non_exhaustive]
pub struct SpecificToolFunction {
pub name: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[non_exhaustive]
pub struct ServerToolChoice {
#[serde(rename = "type")]
pub tool_type: String,
}
pub fn create_tool(name: &str, description: &str, properties: Value, required: &[&str]) -> Tool {
let parameters = serde_json::json!({
"type": "object",
"properties": properties,
"required": required
});
Tool::new(name, description, parameters)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_tool_creation() {
let tool = Tool::builder()
.name("test_function")
.description("A test function")
.parameters(json!({"type": "object"}))
.build()
.unwrap();
assert_eq!(tool.tool_type, "function");
assert_eq!(tool.function.name, "test_function");
assert_eq!(tool.function.description, "A test function");
}
#[test]
fn test_tool_choice_variants() {
let auto = ToolChoice::auto();
let none = ToolChoice::none();
let required = ToolChoice::required();
let specific = ToolChoice::force_tool("my_function");
assert_eq!(serde_json::to_string(&auto).unwrap(), r#""auto""#);
assert_eq!(serde_json::to_string(&none).unwrap(), r#""none""#);
assert_eq!(serde_json::to_string(&required).unwrap(), r#""required""#);
if let ToolChoice::Specific(spec) = specific {
assert_eq!(spec.function.name, "my_function");
} else {
panic!("Expected specific tool choice");
}
}
#[test]
fn test_create_tool_helper() {
let tool = create_tool(
"weather",
"Get weather",
json!({"location": {"type": "string"}}),
&["location"],
);
assert_eq!(tool.function.name, "weather");
assert_eq!(tool.function.description, "Get weather");
let params = &tool.function.parameters;
assert_eq!(params["type"], "object");
assert_eq!(params["required"], json!(["location"]));
}
}