1use std::collections::HashMap;
2use std::sync::Arc;
3
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7use super::codex::NamespaceMap;
8use super::{CodexNamespaceHandler, GatewayExecutor, ToolError, ToolOutput};
9use crate::types::io::OutputItem;
10use crate::types::io::output::FunctionToolCall;
11use crate::types::tools::{CodexNamespaceMember, ResponsesTool};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum ToolType {
16 Function,
17 CodexNamespace,
18 Mcp,
19 WebSearch,
23 FileSearch,
24 CodeInterpreter,
25}
26
27impl ToolType {
28 #[must_use]
29 pub const fn is_gateway_owned(self) -> bool {
30 !matches!(self, Self::Function | Self::CodexNamespace)
31 }
32}
33
34#[derive(Clone)]
36pub struct ToolEntry {
37 pub tool_type: ToolType,
38 pub config: Value,
40 pub server_label: Option<String>,
42 pub handler: Option<Arc<dyn GatewayExecutor>>,
43}
44
45impl std::fmt::Debug for ToolEntry {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 f.debug_struct("ToolEntry")
48 .field("tool_type", &self.tool_type)
49 .field("config", &self.config)
50 .field("server_label", &self.server_label)
51 .field("handler", &self.handler.is_some())
52 .finish()
53 }
54}
55
56pub struct GatewayDispatchResult {
57 pub tool_type: ToolType,
58 pub output: Result<ToolOutput, ToolError>,
59}
60
61#[derive(Debug, Default)]
64pub struct ToolRegistry {
65 entries: HashMap<String, ToolEntry>,
66 namespace_map: Option<NamespaceMap>,
70}
71
72impl ToolRegistry {
73 pub fn build(tools: &[ResponsesTool]) -> Result<Self, ToolError> {
87 Self::build_with_handlers(tools, |_| None)
88 }
89
90 pub fn build_with_handlers(
102 tools: &[ResponsesTool],
103 mut handler_for: impl FnMut(ToolType) -> Option<Arc<dyn GatewayExecutor>>,
104 ) -> Result<Self, ToolError> {
105 let mut entries = HashMap::with_capacity(tools.len());
106 let resolved_tools = CodexNamespaceHandler.resolve_namespace_members(tools)?;
110
111 for tool in &resolved_tools {
112 match tool {
113 ResponsesTool::Function(p) => {
114 if entries
117 .insert(
118 p.name.as_str().to_owned(),
119 ToolEntry {
120 tool_type: ToolType::Function,
121 config: serde_json::to_value(p).expect("serialization of known struct is infallible"),
122 server_label: None,
123 handler: None,
124 },
125 )
126 .is_some()
127 {
128 tracing::warn!(name = %p.name, "duplicate tool name — previous definition overwritten");
129 }
130 }
131 ResponsesTool::Mcp(p) => {
132 tracing::debug!(
139 server_label = %p.server_label,
140 "MCP server declared but skipped in registry — tool names unknown until discovery (PR C)"
141 );
142 }
143 ResponsesTool::WebSearch(p) => {
144 entries.insert(
145 "web_search".to_owned(),
146 ToolEntry {
147 tool_type: ToolType::WebSearch,
148 config: serde_json::to_value(p).expect("serialization of known struct is infallible"),
149 server_label: None,
150 handler: handler_for(ToolType::WebSearch),
151 },
152 );
153 }
154 ResponsesTool::FileSearch(p) => {
155 entries.insert(
156 "file_search".to_owned(),
157 ToolEntry {
158 tool_type: ToolType::FileSearch,
159 config: serde_json::to_value(p).expect("serialization of known struct is infallible"),
160 server_label: None,
161 handler: handler_for(ToolType::FileSearch),
162 },
163 );
164 }
165 ResponsesTool::CodeInterpreter(p) => {
166 entries.insert(
167 "code_interpreter".to_owned(),
168 ToolEntry {
169 tool_type: ToolType::CodeInterpreter,
170 config: serde_json::to_value(p).expect("serialization of known struct is infallible"),
171 server_label: None,
172 handler: handler_for(ToolType::CodeInterpreter),
173 },
174 );
175 }
176 ResponsesTool::Namespace(p) => {
177 let config = serde_json::to_value(p).expect("serialization of known struct is infallible");
180 for member in &p.tools {
181 let CodexNamespaceMember::Function(function) = member else {
182 continue;
183 };
184 let name = function.name.as_str().to_owned();
185 if entries
186 .insert(
187 name.clone(),
188 ToolEntry {
189 tool_type: ToolType::CodexNamespace,
190 config: config.clone(),
191 server_label: Some(p.name.clone()),
192 handler: None,
193 },
194 )
195 .is_some()
196 {
197 tracing::warn!(name = %name, namespace = %p.name, "duplicate tool name - previous definition overwritten");
198 }
199 }
200 }
201 ResponsesTool::Unknown => {
202 tracing::debug!("unknown tool declared but skipped in registry");
203 }
204 }
205 }
206
207 let namespace_map = CodexNamespaceHandler.build_namespace_map((!tools.is_empty()).then_some(tools))?;
208
209 Ok(Self { entries, namespace_map })
210 }
211
212 #[must_use]
213 pub fn lookup(&self, tool_name: &str) -> Option<&ToolEntry> {
214 self.entries.get(tool_name)
215 }
216
217 #[must_use]
218 pub fn is_empty(&self) -> bool {
219 self.entries.is_empty()
220 }
221
222 #[must_use]
223 pub fn len(&self) -> usize {
224 self.entries.len()
225 }
226
227 pub fn restore_final_payload_output(&self, output: &mut [OutputItem]) {
228 CodexNamespaceHandler.restore_output_items(output, self.namespace_map.as_ref());
229 }
230
231 pub fn restore_stream_event_value(&self, value: &mut Value) -> bool {
232 CodexNamespaceHandler.restore_response_value(value, self.namespace_map.as_ref())
233 }
234
235 #[must_use]
237 pub fn gateway_owned<'a>(&self, calls: &'a [FunctionToolCall]) -> Vec<&'a FunctionToolCall> {
238 calls
239 .iter()
240 .filter(|c| {
241 self.entries
242 .get(&c.name)
243 .is_some_and(|e| e.tool_type.is_gateway_owned())
244 })
245 .collect()
246 }
247
248 #[must_use]
249 pub fn is_gateway_owned_name(&self, name: &str) -> bool {
250 self.entries
251 .get(name)
252 .is_some_and(|entry| entry.tool_type.is_gateway_owned())
253 }
254
255 #[must_use]
258 pub fn client_owned<'a>(&self, calls: &'a [FunctionToolCall]) -> Vec<&'a FunctionToolCall> {
259 calls
260 .iter()
261 .filter(|c| {
262 self.entries
263 .get(&c.name)
264 .is_none_or(|e| !e.tool_type.is_gateway_owned())
265 })
266 .collect()
267 }
268
269 pub async fn dispatch(&self, call: &FunctionToolCall) -> Option<GatewayDispatchResult> {
270 let entry = self.entries.get(&call.name)?;
271 let handler = entry.handler.clone()?;
272 let tool_type = entry.tool_type;
273 let config = entry.config.clone();
274 Some(GatewayDispatchResult {
275 tool_type,
276 output: handler
277 .execute(&call.call_id, &call.name, &call.arguments, &config)
278 .await,
279 })
280 }
281}