Skip to main content

openrouter_rs/types/
tool.rs

1//! # Tool and Function Call Types
2//!
3//! This module contains types for defining and working with tools (function calls)
4//! in OpenRouter API requests. Tools allow LLMs to call external functions and
5//! use their results in generating responses.
6//!
7//! ## Tool Definition
8//!
9//! Tools are defined using the [`Tool`] struct which follows OpenRouter's API format:
10//!
11//! ```rust
12//! use openrouter_rs::types::tool::Tool;
13//! use serde_json::json;
14//!
15//! let tool = Tool::builder()
16//!     .name("get_weather")
17//!     .description("Get the current weather for a location")
18//!     .parameters(json!({
19//!         "type": "object",
20//!         "properties": {
21//!             "location": {
22//!                 "type": "string",
23//!                 "description": "The city and state, e.g. San Francisco, CA"
24//!             }
25//!         },
26//!         "required": ["location"]
27//!     }))
28//!     .build()?;
29//! # Ok::<(), Box<dyn std::error::Error>>(())
30//! ```
31//!
32//! ## Tool Choice Control
33//!
34//! Control how the model uses tools with [`ToolChoice`]:
35//!
36//! ```rust
37//! use openrouter_rs::types::tool::ToolChoice;
38//!
39//! // Model chooses whether to use tools
40//! let auto_choice = ToolChoice::auto();
41//!
42//! // Force model to use tools
43//! let required_choice = ToolChoice::required();
44//!
45//! // Force specific tool
46//! let specific_choice = ToolChoice::force_tool("get_weather");
47//! ```
48
49use std::collections::HashMap;
50
51use derive_builder::Builder;
52use serde::{Deserialize, Serialize};
53use serde_json::Value;
54
55use crate::error::OpenRouterError;
56
57/// Tool definition for function calling
58///
59/// Represents a tool that can be called by the LLM. Tools follow OpenRouter's
60/// standardized format and are automatically converted to the appropriate
61/// format for different model providers.
62///
63/// # Examples
64///
65/// ```rust
66/// use openrouter_rs::types::tool::Tool;
67/// use serde_json::json;
68///
69/// let weather_tool = Tool::builder()
70///     .name("get_weather")
71///     .description("Get current weather for a location")
72///     .parameters(json!({
73///         "type": "object",
74///         "properties": {
75///             "location": {"type": "string", "description": "City and state"}
76///         },
77///         "required": ["location"]
78///     }))
79///     .build()?;
80/// # Ok::<(), Box<dyn std::error::Error>>(())
81/// ```
82#[derive(Serialize, Deserialize, Debug, Clone)]
83#[non_exhaustive]
84pub struct Tool {
85    /// Type of tool (always "function" for now)
86    #[serde(rename = "type")]
87    pub tool_type: String,
88
89    /// Function definition
90    pub function: FunctionDefinition,
91
92    /// Optional cache-control directive for provider-side prompt caching.
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub cache_control: Option<Value>,
95}
96
97impl Tool {
98    /// Create a new tool builder
99    pub fn builder() -> ToolBuilder {
100        ToolBuilder::default()
101    }
102
103    /// Create a simple tool with name, description, and parameters
104    pub fn new(name: &str, description: &str, parameters: Value) -> Self {
105        Self {
106            tool_type: "function".to_string(),
107            function: FunctionDefinition {
108                name: name.to_string(),
109                description: description.to_string(),
110                parameters,
111                strict: None,
112            },
113            cache_control: None,
114        }
115    }
116}
117
118#[derive(Debug, Default, Clone)]
119pub struct ToolBuilder {
120    tool_type: Option<String>,
121    name: Option<String>,
122    description: Option<String>,
123    parameters: Option<Value>,
124    strict: Option<bool>,
125    cache_control: Option<Value>,
126}
127
128impl ToolBuilder {
129    /// Override the tool type. Defaults to `"function"`.
130    pub fn tool_type(&mut self, tool_type: impl Into<String>) -> &mut Self {
131        self.tool_type = Some(tool_type.into());
132        self
133    }
134
135    /// Set the full function definition at once.
136    pub fn function(&mut self, function: FunctionDefinition) -> &mut Self {
137        self.name = Some(function.name);
138        self.description = Some(function.description);
139        self.parameters = Some(function.parameters);
140        self.strict = function.strict;
141        self
142    }
143
144    /// Build the tool, validating that the function name is present.
145    pub fn build(&self) -> Result<Tool, OpenRouterError> {
146        let name = self
147            .name
148            .clone()
149            .ok_or_else(|| OpenRouterError::ConfigError("Tool name is required".to_string()))?;
150
151        Ok(Tool {
152            tool_type: self
153                .tool_type
154                .clone()
155                .unwrap_or_else(|| "function".to_string()),
156            function: FunctionDefinition {
157                name,
158                description: self.description.clone().unwrap_or_default(),
159                parameters: self.parameters.clone().unwrap_or(Value::Null),
160                strict: self.strict,
161            },
162            cache_control: self.cache_control.clone(),
163        })
164    }
165}
166
167/// Function definition within a tool
168///
169/// Defines the function that can be called, including its name,
170/// description, and parameter schema.
171#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
172#[builder(build_fn(error = "OpenRouterError"))]
173#[non_exhaustive]
174pub struct FunctionDefinition {
175    /// Name of the function
176    #[builder(setter(into))]
177    pub name: String,
178
179    /// Description of what the function does
180    #[builder(setter(into))]
181    pub description: String,
182
183    /// JSON Schema defining the function parameters
184    #[builder(setter(custom))]
185    pub parameters: Value,
186
187    /// Whether the model must strictly adhere to the parameter schema.
188    #[builder(setter(strip_option), default)]
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub strict: Option<bool>,
191}
192
193impl FunctionDefinition {
194    /// Create a new function definition builder
195    pub fn builder() -> FunctionDefinitionBuilder {
196        FunctionDefinitionBuilder::default()
197    }
198}
199
200impl ToolBuilder {
201    /// Set the function name
202    pub fn name(&mut self, name: &str) -> &mut Self {
203        self.name = Some(name.to_string());
204        self
205    }
206
207    /// Set the function description
208    pub fn description(&mut self, description: &str) -> &mut Self {
209        self.description = Some(description.to_string());
210        self
211    }
212
213    /// Set the parameters as a JSON Value
214    pub fn parameters(&mut self, parameters: Value) -> &mut Self {
215        self.parameters = Some(parameters);
216        self
217    }
218
219    /// Set parameters from a serializable struct
220    pub fn parameters_from<T: Serialize>(
221        &mut self,
222        params: &T,
223    ) -> Result<&mut Self, OpenRouterError> {
224        let value = serde_json::to_value(params).map_err(OpenRouterError::Serialization)?;
225        Ok(self.parameters(value))
226    }
227
228    /// Set parameters from a JSON string
229    pub fn parameters_json(&mut self, json: &str) -> Result<&mut Self, OpenRouterError> {
230        let value: Value = serde_json::from_str(json).map_err(OpenRouterError::Serialization)?;
231        Ok(self.parameters(value))
232    }
233
234    /// Set the function strict-schema flag.
235    pub fn strict(&mut self, strict: bool) -> &mut Self {
236        self.strict = Some(strict);
237        self
238    }
239
240    /// Set the top-level tool cache-control payload.
241    pub fn cache_control(&mut self, cache_control: impl Into<Value>) -> &mut Self {
242        self.cache_control = Some(cache_control.into());
243        self
244    }
245}
246
247impl FunctionDefinitionBuilder {
248    /// Set parameters from a JSON Value
249    pub fn parameters(&mut self, parameters: Value) -> &mut Self {
250        self.parameters = Some(parameters);
251        self
252    }
253
254    /// Set parameters from a serializable struct
255    pub fn parameters_from<T: Serialize>(
256        &mut self,
257        params: &T,
258    ) -> Result<&mut Self, OpenRouterError> {
259        let value = serde_json::to_value(params).map_err(OpenRouterError::Serialization)?;
260        self.parameters = Some(value);
261        Ok(self)
262    }
263
264    /// Set parameters from a JSON string
265    pub fn parameters_json(&mut self, json: &str) -> Result<&mut Self, OpenRouterError> {
266        let value: Value = serde_json::from_str(json).map_err(OpenRouterError::Serialization)?;
267        self.parameters = Some(value);
268        Ok(self)
269    }
270}
271
272/// OpenRouter built-in server tool definition.
273///
274/// Server tools are OpenRouter-hosted capabilities such as web search,
275/// datetime lookup, files, bash, and model search. They share a common wire
276/// shape: a `type`, optional `parameters`, and optional tool-specific
277/// top-level fields.
278#[derive(Serialize, Deserialize, Debug, Clone)]
279#[non_exhaustive]
280pub struct ServerTool {
281    #[serde(rename = "type")]
282    pub tool_type: String,
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub parameters: Option<Value>,
285    #[serde(flatten)]
286    pub extra: HashMap<String, Value>,
287}
288
289impl ServerTool {
290    pub fn new(tool_type: impl Into<String>) -> Self {
291        Self {
292            tool_type: tool_type.into(),
293            parameters: None,
294            extra: HashMap::new(),
295        }
296    }
297
298    pub fn with_parameters(tool_type: impl Into<String>, parameters: impl Into<Value>) -> Self {
299        Self::new(tool_type).parameters(parameters)
300    }
301
302    pub fn parameters(mut self, parameters: impl Into<Value>) -> Self {
303        self.parameters = Some(parameters.into());
304        self
305    }
306
307    pub fn parameters_from<T: Serialize>(mut self, params: &T) -> Result<Self, OpenRouterError> {
308        self.parameters =
309            Some(serde_json::to_value(params).map_err(OpenRouterError::Serialization)?);
310        Ok(self)
311    }
312
313    pub fn option(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
314        self.extra.insert(key.into(), value.into());
315        self
316    }
317
318    pub fn web_search() -> Self {
319        Self::new("openrouter:web_search")
320    }
321
322    pub fn web_search_with_parameters(parameters: impl Into<Value>) -> Self {
323        Self::with_parameters("openrouter:web_search", parameters)
324    }
325
326    pub fn web_search_preview() -> Self {
327        Self::new("web_search_preview")
328    }
329
330    pub fn datetime() -> Self {
331        Self::new("openrouter:datetime")
332    }
333
334    pub fn datetime_with_timezone(timezone: impl Into<String>) -> Self {
335        Self::with_parameters(
336            "openrouter:datetime",
337            serde_json::json!({ "timezone": timezone.into() }),
338        )
339    }
340
341    pub fn files() -> Self {
342        Self::new("openrouter:files")
343    }
344
345    pub fn bash() -> Self {
346        Self::new("openrouter:bash")
347    }
348
349    pub fn web_fetch() -> Self {
350        Self::new("openrouter:web_fetch")
351    }
352
353    pub fn advisor() -> Self {
354        Self::new("openrouter:advisor")
355    }
356
357    pub fn subagent() -> Self {
358        Self::new("openrouter:subagent")
359    }
360
361    pub fn image_generation() -> Self {
362        Self::new("openrouter:image_generation")
363    }
364
365    pub fn search_models() -> Self {
366        Self::new("openrouter:experimental__search_models")
367    }
368
369    pub fn apply_patch() -> Self {
370        Self::new("openrouter:apply_patch")
371    }
372
373    pub(crate) fn is_server_tool_type(tool_type: &str) -> bool {
374        tool_type.starts_with("openrouter:")
375            || matches!(
376                tool_type,
377                "web_search"
378                    | "web_search_2025_08_26"
379                    | "web_search_preview"
380                    | "web_search_preview_2025_03_11"
381                    | "apply_patch"
382                    | "shell"
383                    | "namespace"
384            )
385    }
386
387    pub(crate) fn is_files_tool_type(tool_type: &str) -> bool {
388        matches!(tool_type, "openrouter:files" | "files")
389    }
390
391    pub(crate) fn is_files_tool(&self) -> bool {
392        Self::is_files_tool_type(&self.tool_type)
393    }
394
395    pub(crate) fn is_server_tool_value(value: &Value) -> bool {
396        value
397            .get("type")
398            .and_then(Value::as_str)
399            .is_some_and(Self::is_server_tool_type)
400    }
401
402    pub(crate) fn is_files_tool_value(value: &Value) -> bool {
403        value
404            .get("type")
405            .and_then(Value::as_str)
406            .is_some_and(Self::is_files_tool_type)
407    }
408}
409
410impl From<ServerTool> for Value {
411    fn from(tool: ServerTool) -> Self {
412        serde_json::to_value(tool).expect("server tool serialization should not fail")
413    }
414}
415
416/// Control how the model chooses to use tools
417///
418/// Specifies whether the model should use tools, and if so, how it should
419/// choose which tools to call.
420///
421/// # Examples
422///
423/// ```rust
424/// use openrouter_rs::types::tool::ToolChoice;
425///
426/// // Let model decide
427/// let auto = ToolChoice::auto();
428///
429/// // Prevent tool use
430/// let none = ToolChoice::none();
431///
432/// // Require tool use
433/// let required = ToolChoice::required();
434///
435/// // Force specific tool
436/// let specific = ToolChoice::force_tool("get_weather");
437/// ```
438#[derive(Serialize, Deserialize, Debug, Clone)]
439#[non_exhaustive]
440#[serde(untagged)]
441pub enum ToolChoice {
442    /// Simple string choices: "none", "auto", "required"
443    String(String),
444    /// Force a specific tool to be called
445    Specific(SpecificToolChoice),
446    /// Force a specific OpenRouter server tool to be called
447    Server(ServerToolChoice),
448}
449
450impl ToolChoice {
451    /// Model will not call any tools
452    pub fn none() -> Self {
453        Self::String("none".to_string())
454    }
455
456    /// Model can choose whether to call tools
457    pub fn auto() -> Self {
458        Self::String("auto".to_string())
459    }
460
461    /// Model must call at least one tool
462    pub fn required() -> Self {
463        Self::String("required".to_string())
464    }
465
466    /// Force the model to call a specific tool
467    pub fn force_tool(tool_name: &str) -> Self {
468        Self::Specific(SpecificToolChoice {
469            tool_type: "function".to_string(),
470            function: SpecificToolFunction {
471                name: tool_name.to_string(),
472            },
473        })
474    }
475
476    /// Force the model to call a specific OpenRouter server tool.
477    pub fn force_server_tool(tool_type: impl Into<String>) -> Self {
478        Self::Server(ServerToolChoice {
479            tool_type: tool_type.into(),
480        })
481    }
482}
483
484/// Specific tool choice for forcing a particular tool
485#[derive(Serialize, Deserialize, Debug, Clone)]
486#[non_exhaustive]
487pub struct SpecificToolChoice {
488    #[serde(rename = "type")]
489    pub tool_type: String,
490    pub function: SpecificToolFunction,
491}
492
493/// Function specification for specific tool choice
494#[derive(Serialize, Deserialize, Debug, Clone)]
495#[non_exhaustive]
496pub struct SpecificToolFunction {
497    pub name: String,
498}
499
500/// Specific server-tool choice for forcing an OpenRouter built-in tool.
501#[derive(Serialize, Deserialize, Debug, Clone)]
502#[non_exhaustive]
503pub struct ServerToolChoice {
504    #[serde(rename = "type")]
505    pub tool_type: String,
506}
507
508/// Helper function to create a tool with common parameter structure
509///
510/// Creates a tool with an object-type parameter schema and the specified properties.
511///
512/// # Examples
513///
514/// ```rust
515/// use openrouter_rs::types::tool::create_tool;
516/// use serde_json::json;
517///
518/// let tool = create_tool(
519///     "calculator",
520///     "Perform basic arithmetic operations",
521///     json!({
522///         "operation": {"type": "string", "enum": ["add", "subtract", "multiply", "divide"]},
523///         "a": {"type": "number"},
524///         "b": {"type": "number"}
525///     }),
526///     &["operation", "a", "b"]
527/// );
528/// ```
529pub fn create_tool(name: &str, description: &str, properties: Value, required: &[&str]) -> Tool {
530    let parameters = serde_json::json!({
531        "type": "object",
532        "properties": properties,
533        "required": required
534    });
535
536    Tool::new(name, description, parameters)
537}
538
539#[cfg(test)]
540mod tests {
541    use super::*;
542    use serde_json::json;
543
544    #[test]
545    fn test_tool_creation() {
546        let tool = Tool::builder()
547            .name("test_function")
548            .description("A test function")
549            .parameters(json!({"type": "object"}))
550            .build()
551            .unwrap();
552
553        assert_eq!(tool.tool_type, "function");
554        assert_eq!(tool.function.name, "test_function");
555        assert_eq!(tool.function.description, "A test function");
556    }
557
558    #[test]
559    fn test_tool_choice_variants() {
560        let auto = ToolChoice::auto();
561        let none = ToolChoice::none();
562        let required = ToolChoice::required();
563        let specific = ToolChoice::force_tool("my_function");
564
565        // Test serialization
566        assert_eq!(serde_json::to_string(&auto).unwrap(), r#""auto""#);
567        assert_eq!(serde_json::to_string(&none).unwrap(), r#""none""#);
568        assert_eq!(serde_json::to_string(&required).unwrap(), r#""required""#);
569
570        if let ToolChoice::Specific(spec) = specific {
571            assert_eq!(spec.function.name, "my_function");
572        } else {
573            panic!("Expected specific tool choice");
574        }
575    }
576
577    #[test]
578    fn test_create_tool_helper() {
579        let tool = create_tool(
580            "weather",
581            "Get weather",
582            json!({"location": {"type": "string"}}),
583            &["location"],
584        );
585
586        assert_eq!(tool.function.name, "weather");
587        assert_eq!(tool.function.description, "Get weather");
588
589        let params = &tool.function.parameters;
590        assert_eq!(params["type"], "object");
591        assert_eq!(params["required"], json!(["location"]));
592    }
593}