Skip to main content

codex_tools/
responses_api.rs

1use 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    /// TODO: Validation. When strict is set to true, the JSON schema,
30    /// `required` and `additional_properties` must be present. All fields in
31    /// `properties` must be present in `required`.
32    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;