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#[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 #[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
74pub 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;