Skip to main content

codex_tools/
tool_spec.rs

1use crate::FreeformTool;
2use crate::JsonSchema;
3use crate::LoadableToolSpec;
4use crate::ResponsesApiNamespace;
5use crate::ResponsesApiTool;
6use codex_protocol::config_types::WebSearchContextSize;
7use codex_protocol::config_types::WebSearchFilters as ConfigWebSearchFilters;
8use codex_protocol::config_types::WebSearchUserLocation as ConfigWebSearchUserLocation;
9use codex_protocol::config_types::WebSearchUserLocationType;
10use serde::Serialize;
11use serde_json::Value;
12
13/// When serialized as JSON, this produces a valid "Tool" in the OpenAI
14/// Responses API.
15#[derive(Debug, Clone, Serialize, PartialEq)]
16#[serde(tag = "type")]
17pub enum ToolSpec {
18    #[serde(rename = "function")]
19    Function(ResponsesApiTool),
20    #[serde(rename = "namespace")]
21    Namespace(ResponsesApiNamespace),
22    #[serde(rename = "tool_search")]
23    ToolSearch {
24        execution: String,
25        description: String,
26        parameters: JsonSchema,
27    },
28    // TODO: Understand why we get an error on web_search although the API docs
29    // say it's supported.
30    // https://platform.openai.com/docs/guides/tools-web-search?api-mode=responses#:~:text=%7B%20type%3A%20%22web_search%22%20%7D%2C
31    // `external_web_access` distinguishes cached from live-capable search, while
32    // `indexed_web_access` restricts live fetches to indexed URLs.
33    // https://platform.openai.com/docs/guides/tools-web-search#live-internet-access
34    #[serde(rename = "web_search")]
35    WebSearch {
36        #[serde(skip_serializing_if = "Option::is_none")]
37        external_web_access: Option<bool>,
38        #[serde(skip_serializing_if = "Option::is_none")]
39        indexed_web_access: Option<bool>,
40        #[serde(skip_serializing_if = "Option::is_none")]
41        filters: Option<ResponsesApiWebSearchFilters>,
42        #[serde(skip_serializing_if = "Option::is_none")]
43        user_location: Option<ResponsesApiWebSearchUserLocation>,
44        #[serde(skip_serializing_if = "Option::is_none")]
45        search_context_size: Option<WebSearchContextSize>,
46        #[serde(skip_serializing_if = "Option::is_none")]
47        search_content_types: Option<Vec<String>>,
48    },
49    #[serde(rename = "custom")]
50    Freeform(FreeformTool),
51}
52
53impl ToolSpec {
54    pub fn name(&self) -> &str {
55        match self {
56            ToolSpec::Function(tool) => tool.name.as_str(),
57            ToolSpec::Namespace(namespace) => namespace.name.as_str(),
58            ToolSpec::ToolSearch { .. } => "tool_search",
59            ToolSpec::WebSearch { .. } => "web_search",
60            ToolSpec::Freeform(tool) => tool.name.as_str(),
61        }
62    }
63}
64
65impl From<LoadableToolSpec> for ToolSpec {
66    fn from(value: LoadableToolSpec) -> Self {
67        match value {
68            LoadableToolSpec::Function(tool) => ToolSpec::Function(tool),
69            LoadableToolSpec::Namespace(namespace) => ToolSpec::Namespace(namespace),
70        }
71    }
72}
73
74/// Returns JSON values that are compatible with Function Calling in the
75/// Responses API:
76/// https://platform.openai.com/docs/guides/function-calling?api-mode=responses
77pub fn create_tools_json_for_responses_api(
78    tools: &[ToolSpec],
79) -> Result<Vec<Value>, serde_json::Error> {
80    let mut tools_json = Vec::new();
81
82    for tool in tools {
83        let json = serde_json::to_value(tool)?;
84        tools_json.push(json);
85    }
86
87    Ok(tools_json)
88}
89
90#[derive(Debug, Clone, Serialize, PartialEq)]
91pub struct ResponsesApiWebSearchFilters {
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub allowed_domains: Option<Vec<String>>,
94}
95
96impl From<ConfigWebSearchFilters> for ResponsesApiWebSearchFilters {
97    fn from(filters: ConfigWebSearchFilters) -> Self {
98        Self {
99            allowed_domains: filters.allowed_domains,
100        }
101    }
102}
103
104#[derive(Debug, Clone, Serialize, PartialEq)]
105pub struct ResponsesApiWebSearchUserLocation {
106    #[serde(rename = "type")]
107    pub r#type: WebSearchUserLocationType,
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub country: Option<String>,
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub region: Option<String>,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub city: Option<String>,
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub timezone: Option<String>,
116}
117
118impl From<ConfigWebSearchUserLocation> for ResponsesApiWebSearchUserLocation {
119    fn from(user_location: ConfigWebSearchUserLocation) -> Self {
120        Self {
121            r#type: user_location.r#type,
122            country: user_location.country,
123            region: user_location.region,
124            city: user_location.city,
125            timezone: user_location.timezone,
126        }
127    }
128}
129
130#[cfg(test)]
131#[path = "tool_spec_tests.rs"]
132mod tests;