Skip to main content

openai_compat/types/responses/
tools.rs

1//! The `tools` / `tool_choice` polymorphism for Responses requests, mirroring
2//! `openai-python/src/openai/types/responses/tool_param.py` (v1 subset:
3//! function + thin web_search/file_search/code_interpreter — everything else
4//! round-trips through `Tool::Other`).
5
6use serde::{Deserialize, Serialize};
7
8/// A function the model may call.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct FunctionTool {
11    pub name: String,
12    #[serde(default, skip_serializing_if = "Option::is_none")]
13    pub description: Option<String>,
14    #[serde(default, skip_serializing_if = "Option::is_none")]
15    pub parameters: Option<serde_json::Value>,
16    #[serde(default, skip_serializing_if = "Option::is_none")]
17    pub strict: Option<bool>,
18}
19
20/// The typed tool variants this crate models directly. Built-ins beyond
21/// `web_search`/`file_search`/`code_interpreter` (`mcp`, `computer_use_preview`,
22/// `image_generation`, `local_shell`, `custom`, ...) fall back to
23/// [`Tool::Other`].
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[serde(tag = "type", rename_all = "snake_case")]
26pub enum KnownTool {
27    Function(FunctionTool),
28    WebSearch {
29        #[serde(default, skip_serializing_if = "Option::is_none")]
30        search_context_size: Option<String>,
31    },
32    FileSearch {
33        vector_store_ids: Vec<String>,
34        #[serde(default, skip_serializing_if = "Option::is_none")]
35        max_num_results: Option<u32>,
36    },
37    CodeInterpreter {
38        /// `"auto"` or `{"type": "auto", "file_ids": [...]}` shape from the
39        /// Python SDK; kept untyped to avoid over-modeling a rarely-varied
40        /// field.
41        container: serde_json::Value,
42    },
43}
44
45/// A tool the model may call, mirroring `ToolParam`.
46///
47/// Unrecognized `type` values deserialize into [`Tool::Other`] and round-trip
48/// without data loss (object key order is normalized through
49/// `serde_json::Value`, but no field is dropped).
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(untagged)]
52pub enum Tool {
53    Known(KnownTool),
54    Other(serde_json::Value),
55}
56
57impl Tool {
58    /// A `function` tool with a JSON-schema `parameters` object.
59    pub fn function(
60        name: impl Into<String>,
61        description: impl Into<String>,
62        parameters: serde_json::Value,
63    ) -> Self {
64        Self::Known(KnownTool::Function(FunctionTool {
65            name: name.into(),
66            description: Some(description.into()),
67            parameters: Some(parameters),
68            strict: None,
69        }))
70    }
71
72    /// The built-in web search tool.
73    pub fn web_search() -> Self {
74        Self::Known(KnownTool::WebSearch {
75            search_context_size: None,
76        })
77    }
78
79    /// The built-in file search tool over the given vector stores.
80    pub fn file_search(vector_store_ids: Vec<String>) -> Self {
81        Self::Known(KnownTool::FileSearch {
82            vector_store_ids,
83            max_num_results: None,
84        })
85    }
86}
87
88/// How the model chooses tools: `"none" | "auto" | "required"`, or a
89/// provider-specific typed choice (e.g. forcing a named function).
90#[derive(Debug, Clone, Serialize, Deserialize)]
91#[serde(untagged)]
92pub enum ToolChoice {
93    Mode(String),
94    Typed(serde_json::Value),
95}
96
97impl ToolChoice {
98    pub const NONE: &'static str = "none";
99    pub const AUTO: &'static str = "auto";
100    pub const REQUIRED: &'static str = "required";
101
102    pub fn mode(mode: impl Into<String>) -> Self {
103        Self::Mode(mode.into())
104    }
105
106    /// Force a call to the named function.
107    pub fn function(name: impl Into<String>) -> Self {
108        Self::Typed(serde_json::json!({
109            "type": "function",
110            "name": name.into(),
111        }))
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn function_tool_serializes() {
121        let tool = Tool::function(
122            "get_weather",
123            "Get current weather",
124            serde_json::json!({"type": "object"}),
125        );
126        assert_eq!(
127            serde_json::to_value(&tool).unwrap(),
128            serde_json::json!({
129                "type": "function",
130                "name": "get_weather",
131                "description": "Get current weather",
132                "parameters": {"type": "object"}
133            })
134        );
135    }
136
137    #[test]
138    fn unknown_tool_preserved() {
139        let json = serde_json::json!({"type": "mcp", "server_label": "s", "server_url": "u"});
140        let tool: Tool = serde_json::from_value(json.clone()).unwrap();
141        assert!(matches!(tool, Tool::Other(_)));
142        assert_eq!(serde_json::to_value(&tool).unwrap(), json);
143    }
144
145    #[test]
146    fn tool_choice_mode_serializes_as_plain_string() {
147        assert_eq!(
148            serde_json::to_value(ToolChoice::mode("auto")).unwrap(),
149            "auto"
150        );
151    }
152}