use crate::JsonSchema; use crate::ToolDefinition; use crate::ToolName; use crate::ToolOutputSchema; use crate::mcp_tool::parse_mcp_tool_with_schema_max_bytes; use crate::parse_agent_plugin_mcp_tool; use crate::parse_dynamic_tool; use crate::parse_mcp_tool; use codex_protocol::DEFAULT_FUNCTION_NAMESPACE; use codex_protocol::dynamic_tools::DynamicToolFunctionSpec; use serde::Deserialize; use serde::Serialize; const MAX_SERIALIZED_MCP_TOOL_BYTES: usize = 8_000; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct FreeformTool { pub name: String, pub description: String, #[serde(skip_serializing_if = "Option::is_none")] pub defer_loading: Option, pub format: FreeformToolFormat, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct FreeformToolFormat { pub r#type: String, pub syntax: String, pub definition: String, } #[derive(Debug, Clone, Serialize, PartialEq)] pub struct ResponsesApiTool { pub name: String, pub description: String, /// TODO: Validation. When strict is set to true, the JSON schema, /// `required` and `additional_properties` must be present. All fields in /// `properties` must be present in `required`. pub strict: bool, #[serde(skip_serializing_if = "Option::is_none")] pub defer_loading: Option, pub parameters: JsonSchema, #[serde(skip)] pub output_schema: Option, } #[derive(Debug, Clone, Serialize, PartialEq)] #[serde(tag = "type")] #[allow(clippy::large_enum_variant)] pub enum LoadableToolSpec { #[allow(dead_code)] #[serde(rename = "function")] Function(ResponsesApiTool), #[serde(rename = "namespace")] Namespace(ResponsesApiNamespace), } #[derive(Debug, Clone, Serialize, PartialEq)] pub struct ResponsesApiNamespace { pub name: String, pub description: String, pub tools: Vec, } pub fn default_namespace_description(namespace_name: &str) -> String { if namespace_name == DEFAULT_FUNCTION_NAMESPACE { String::new() } else { format!("Tools in the {namespace_name} namespace.") } } #[derive(Debug, Clone, Serialize, PartialEq)] #[serde(tag = "type")] #[allow(clippy::large_enum_variant)] pub enum ResponsesApiNamespaceTool { #[serde(rename = "function")] Function(ResponsesApiTool), #[serde(rename = "custom")] Custom(FreeformTool), } pub fn dynamic_tool_to_responses_api_tool( tool: &DynamicToolFunctionSpec, ) -> Result { Ok(tool_definition_to_responses_api_tool(parse_dynamic_tool( tool, )?)) } pub fn coalesce_loadable_tool_specs( specs: impl IntoIterator, ) -> Vec { let mut coalesced_specs = Vec::new(); for spec in specs { match spec { LoadableToolSpec::Function(tool) => { coalesced_specs.push(LoadableToolSpec::Function(tool)); } LoadableToolSpec::Namespace(mut namespace) => { if let Some(existing_namespace) = coalesced_specs.iter_mut().find_map(|spec| match spec { LoadableToolSpec::Namespace(existing_namespace) if existing_namespace.name == namespace.name => { Some(existing_namespace) } LoadableToolSpec::Function(_) | LoadableToolSpec::Namespace(_) => None, }) { existing_namespace.tools.append(&mut namespace.tools); } else { coalesced_specs.push(LoadableToolSpec::Namespace(namespace)); } } } } coalesced_specs } pub fn mcp_tool_to_responses_api_tool( tool_name: &ToolName, tool: &rmcp::model::Tool, schema_max_bytes: Option, ) -> Result { let definition = match schema_max_bytes { Some(max_bytes) => parse_mcp_tool_with_schema_max_bytes(tool, max_bytes)?, None => parse_mcp_tool(tool)?, }; let mut tool = tool_definition_to_responses_api_tool(definition.renamed(tool_name.name.clone())); tool.parameters.mcp_input_schema_max_bytes = schema_max_bytes; Ok(tool) } pub fn agent_plugin_mcp_tool_to_responses_api_tool( tool_name: &ToolName, tool: &rmcp::model::Tool, ) -> Result { let mut tool = tool_definition_to_responses_api_tool( parse_agent_plugin_mcp_tool(tool)?.renamed(tool_name.name.clone()), ); if serde_json::to_vec(&tool)?.len() > MAX_SERIALIZED_MCP_TOOL_BYTES { tool.parameters = JsonSchema::object( Default::default(), /*required*/ None, Some(true.into()), ); } Ok(tool) } pub fn mcp_tool_to_deferred_responses_api_tool( tool_name: &ToolName, tool: &rmcp::model::Tool, ) -> Result { Ok(tool_definition_to_responses_api_tool( parse_mcp_tool(tool)? .renamed(tool_name.name.clone()) .into_deferred(), )) } pub fn tool_definition_to_responses_api_tool(tool_definition: ToolDefinition) -> ResponsesApiTool { ResponsesApiTool { name: tool_definition.name, description: tool_definition.description, strict: false, defer_loading: tool_definition.defer_loading.then_some(true), parameters: tool_definition.input_schema, output_schema: tool_definition.output_schema, } } #[cfg(test)] #[path = "responses_api_tests.rs"] mod tests;