codex_tools/
responses_api.rs1use crate::JsonSchema;
2use crate::ToolDefinition;
3use crate::ToolName;
4use crate::parse_dynamic_tool;
5use crate::parse_mcp_tool;
6use codex_protocol::dynamic_tools::DynamicToolFunctionSpec;
7use serde::Deserialize;
8use serde::Serialize;
9use serde_json::Value;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12pub struct FreeformTool {
13 pub name: String,
14 pub description: String,
15 pub format: FreeformToolFormat,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
19pub struct FreeformToolFormat {
20 pub r#type: String,
21 pub syntax: String,
22 pub definition: String,
23}
24
25#[derive(Debug, Clone, Serialize, PartialEq)]
26pub struct ResponsesApiTool {
27 pub name: String,
28 pub description: String,
29 pub strict: bool,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub defer_loading: Option<bool>,
35 pub parameters: JsonSchema,
36 #[serde(skip)]
37 pub output_schema: Option<Value>,
38}
39
40#[derive(Debug, Clone, Serialize, PartialEq)]
41#[serde(tag = "type")]
42#[allow(clippy::large_enum_variant)]
43pub enum LoadableToolSpec {
44 #[allow(dead_code)]
45 #[serde(rename = "function")]
46 Function(ResponsesApiTool),
47 #[serde(rename = "namespace")]
48 Namespace(ResponsesApiNamespace),
49}
50
51#[derive(Debug, Clone, Serialize, PartialEq)]
52pub struct ResponsesApiNamespace {
53 pub name: String,
54 pub description: String,
55 pub tools: Vec<ResponsesApiNamespaceTool>,
56}
57
58pub fn default_namespace_description(namespace_name: &str) -> String {
59 format!("Tools in the {namespace_name} namespace.")
60}
61
62#[derive(Debug, Clone, Serialize, PartialEq)]
63#[serde(tag = "type")]
64pub enum ResponsesApiNamespaceTool {
65 #[serde(rename = "function")]
66 Function(ResponsesApiTool),
67}
68
69pub fn dynamic_tool_to_responses_api_tool(
70 tool: &DynamicToolFunctionSpec,
71) -> Result<ResponsesApiTool, serde_json::Error> {
72 Ok(tool_definition_to_responses_api_tool(parse_dynamic_tool(
73 tool,
74 )?))
75}
76
77pub fn coalesce_loadable_tool_specs(
78 specs: impl IntoIterator<Item = LoadableToolSpec>,
79) -> Vec<LoadableToolSpec> {
80 let mut coalesced_specs = Vec::new();
81 for spec in specs {
82 match spec {
83 LoadableToolSpec::Function(tool) => {
84 coalesced_specs.push(LoadableToolSpec::Function(tool));
85 }
86 LoadableToolSpec::Namespace(mut namespace) => {
87 if let Some(existing_namespace) =
88 coalesced_specs.iter_mut().find_map(|spec| match spec {
89 LoadableToolSpec::Namespace(existing_namespace)
90 if existing_namespace.name == namespace.name =>
91 {
92 Some(existing_namespace)
93 }
94 LoadableToolSpec::Function(_) | LoadableToolSpec::Namespace(_) => None,
95 })
96 {
97 existing_namespace.tools.append(&mut namespace.tools);
98 } else {
99 coalesced_specs.push(LoadableToolSpec::Namespace(namespace));
100 }
101 }
102 }
103 }
104 coalesced_specs
105}
106
107pub fn mcp_tool_to_responses_api_tool(
108 tool_name: &ToolName,
109 tool: &rmcp::model::Tool,
110) -> Result<ResponsesApiTool, serde_json::Error> {
111 Ok(tool_definition_to_responses_api_tool(
112 parse_mcp_tool(tool)?.renamed(tool_name.name.clone()),
113 ))
114}
115
116pub fn mcp_tool_to_deferred_responses_api_tool(
117 tool_name: &ToolName,
118 tool: &rmcp::model::Tool,
119) -> Result<ResponsesApiTool, serde_json::Error> {
120 Ok(tool_definition_to_responses_api_tool(
121 parse_mcp_tool(tool)?
122 .renamed(tool_name.name.clone())
123 .into_deferred(),
124 ))
125}
126
127pub fn tool_definition_to_responses_api_tool(tool_definition: ToolDefinition) -> ResponsesApiTool {
128 ResponsesApiTool {
129 name: tool_definition.name,
130 description: tool_definition.description,
131 strict: false,
132 defer_loading: tool_definition.defer_loading.then_some(true),
133 parameters: tool_definition.input_schema,
134 output_schema: tool_definition.output_schema,
135 }
136}
137
138#[cfg(test)]
139#[path = "responses_api_tests.rs"]
140mod tests;