codex-tools: extract responses API tool models (#16031)

## Why

The previous extraction steps moved shared tool-schema parsing into
`codex-tools`, but `codex-core` still owned the generic Responses API
tool models and the last adapter layer that turned parsed tool
definitions into `ResponsesApiTool` values.

That left `core/src/tools/spec.rs` and `core/src/client_common.rs`
holding a chunk of tool-shaping code that does not need session state,
runtime plumbing, or any other `codex-core`-specific dependency. As a
result, `codex-tools` owned the parsed tool definition, but `codex-core`
still owned the generic wire model that those definitions are converted
into.

This change moves that boundary one step further. `codex-tools` now owns
the reusable Responses/tool wire structs and the shared conversion
helpers for dynamic tools, MCP tools, and deferred MCP aliases.
`codex-core` continues to own `ToolSpec` orchestration and the remaining
web-search-specific request shapes.

## What changed

- added `tools/src/responses_api.rs` to own `ResponsesApiTool`,
`FreeformTool`, `ToolSearchOutputTool`, namespace output types, and the
shared `ToolDefinition -> ResponsesApiTool` adapter helpers
- added `tools/src/responses_api_tests.rs` for deferred-loading
behavior, adapter coverage, and namespace serialization coverage
- rewired `core/src/tools/spec.rs` to use the extracted dynamic/MCP
adapter helpers instead of defining those conversions locally
- rewired `core/src/tools/handlers/tool_search.rs` to use the extracted
deferred MCP adapter and namespace output types directly
- slimmed `core/src/client_common.rs` so it now keeps `ToolSpec` and the
web-search-specific wire types, while reusing the extracted tool models
from `codex-tools`
- moved the extracted seam tests out of `core` and updated
`codex-rs/tools/README.md` plus `tools/src/lib.rs` to reflect the
expanded `codex-tools` boundary

## Test plan

- `cargo test -p codex-tools`
- `cargo test -p codex-core --lib tools::spec::`
- `cargo test -p codex-core --lib tools::handlers::tool_search::`
- `just fix -p codex-tools -p codex-core`
- `just argument-comment-lint`

## References

- [#15923](https://github.com/openai/codex/pull/15923) `codex-tools:
extract shared tool schema parsing`
- [#15928](https://github.com/openai/codex/pull/15928) `codex-tools:
extract MCP schema adapters`
- [#15944](https://github.com/openai/codex/pull/15944) `codex-tools:
extract dynamic tool adapters`
- [#15953](https://github.com/openai/codex/pull/15953) `codex-tools:
introduce named tool definitions`
This commit is contained in:
Michael Bolin authored and GitHub committed 2026-03-27 14:26:54 -07:00
1 parent 82e8031338
commit 16d4ea9ca8
10 files changed
+320 -217

No files matched your search

+5 -56
View File
@@ -157,14 +157,16 @@ fn strip_total_output_header(output: &str) -> Option<(&str, u32)> {
} }
pub(crate) mod tools { pub(crate) mod tools {
use crate::tools::spec::JsonSchema;
use codex_protocol::config_types::WebSearchContextSize; use codex_protocol::config_types::WebSearchContextSize;
use codex_protocol::config_types::WebSearchFilters as ConfigWebSearchFilters; use codex_protocol::config_types::WebSearchFilters as ConfigWebSearchFilters;
use codex_protocol::config_types::WebSearchUserLocation as ConfigWebSearchUserLocation; use codex_protocol::config_types::WebSearchUserLocation as ConfigWebSearchUserLocation;
use codex_protocol::config_types::WebSearchUserLocationType; use codex_protocol::config_types::WebSearchUserLocationType;
use serde::Deserialize; pub(crate) use codex_tools::FreeformTool;
pub(crate) use codex_tools::FreeformToolFormat;
use codex_tools::JsonSchema;
pub(crate) use codex_tools::ResponsesApiTool;
pub(crate) use codex_tools::ToolSearchOutputTool;
use serde::Serialize; use serde::Serialize;
use serde_json::Value;
/// When serialized as JSON, this produces a valid "Tool" in the OpenAI /// When serialized as JSON, this produces a valid "Tool" in the OpenAI
/// Responses API. /// Responses API.
@@ -256,59 +258,6 @@ pub(crate) mod tools {
} }
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FreeformTool {
pub(crate) name: String,
pub(crate) description: String,
pub(crate) format: FreeformToolFormat,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FreeformToolFormat {
pub(crate) r#type: String,
pub(crate) syntax: String,
pub(crate) definition: String,
}
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct ResponsesApiTool {
pub(crate) name: String,
pub(crate) 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(crate) strict: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) defer_loading: Option<bool>,
pub(crate) parameters: JsonSchema,
#[serde(skip)]
pub(crate) output_schema: Option<Value>,
}
#[derive(Debug, Clone, Serialize, PartialEq)]
#[serde(tag = "type")]
pub(crate) enum ToolSearchOutputTool {
#[allow(dead_code)]
#[serde(rename = "function")]
Function(ResponsesApiTool),
#[serde(rename = "namespace")]
Namespace(ResponsesApiNamespace),
}
#[derive(Debug, Clone, Serialize, PartialEq)]
pub(crate) struct ResponsesApiNamespace {
pub(crate) name: String,
pub(crate) description: String,
pub(crate) tools: Vec<ResponsesApiNamespaceTool>,
}
#[derive(Debug, Clone, Serialize, PartialEq)]
#[serde(tag = "type")]
pub(crate) enum ResponsesApiNamespaceTool {
#[serde(rename = "function")]
Function(ResponsesApiTool),
}
} }
pub struct ResponseStream { pub struct ResponseStream {
-46
View File
@@ -197,49 +197,3 @@ fn reserializes_shell_outputs_for_function_and_custom_tool_calls() {
] ]
); );
} }
#[test]
fn tool_search_output_namespace_serializes_with_deferred_child_tools() {
let namespace = tools::ToolSearchOutputTool::Namespace(tools::ResponsesApiNamespace {
name: "mcp__codex_apps__calendar".to_string(),
description: "Plan events".to_string(),
tools: vec![tools::ResponsesApiNamespaceTool::Function(
tools::ResponsesApiTool {
name: "create_event".to_string(),
description: "Create a calendar event.".to_string(),
strict: false,
defer_loading: Some(true),
parameters: crate::tools::spec::JsonSchema::Object {
properties: Default::default(),
required: None,
additional_properties: None,
},
output_schema: None,
},
)],
});
let value = serde_json::to_value(namespace).expect("serialize namespace");
assert_eq!(
value,
serde_json::json!({
"type": "namespace",
"name": "mcp__codex_apps__calendar",
"description": "Plan events",
"tools": [
{
"type": "function",
"name": "create_event",
"description": "Create a calendar event.",
"strict": false,
"defer_loading": true,
"parameters": {
"type": "object",
"properties": {}
}
}
]
})
);
}
@@ -1,6 +1,3 @@
use crate::client_common::tools::ResponsesApiNamespace;
use crate::client_common::tools::ResponsesApiNamespaceTool;
use crate::client_common::tools::ToolSearchOutputTool;
use crate::function_tool::FunctionCallError; use crate::function_tool::FunctionCallError;
use crate::mcp_connection_manager::ToolInfo; use crate::mcp_connection_manager::ToolInfo;
use crate::tools::context::ToolInvocation; use crate::tools::context::ToolInvocation;
@@ -8,17 +5,17 @@ use crate::tools::context::ToolPayload;
use crate::tools::context::ToolSearchOutput; use crate::tools::context::ToolSearchOutput;
use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind; use crate::tools::registry::ToolKind;
use crate::tools::spec::mcp_tool_to_deferred_openai_tool;
use async_trait::async_trait; use async_trait::async_trait;
use bm25::Document; use bm25::Document;
use bm25::Language; use bm25::Language;
use bm25::SearchEngineBuilder; use bm25::SearchEngineBuilder;
use codex_tools::ResponsesApiNamespace;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ToolSearchOutputTool;
use codex_tools::mcp_tool_to_deferred_responses_api_tool;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::collections::HashMap; use std::collections::HashMap;
#[cfg(test)]
use crate::client_common::tools::ResponsesApiTool;
pub struct ToolSearchHandler { pub struct ToolSearchHandler {
tools: HashMap<String, ToolInfo>, tools: HashMap<String, ToolInfo>,
} }
@@ -129,7 +126,7 @@ fn serialize_tool_search_output_tools(
let tools = tools let tools = tools
.iter() .iter()
.map(|tool| { .map(|tool| {
mcp_tool_to_deferred_openai_tool(tool.tool_name.clone(), tool.tool.clone()) mcp_tool_to_deferred_responses_api_tool(tool.tool_name.clone(), &tool.tool)
.map(ResponsesApiNamespaceTool::Function) .map(ResponsesApiNamespaceTool::Function)
}) })
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
@@ -1,5 +1,6 @@
use super::*; use super::*;
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME; use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_tools::ResponsesApiTool;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use rmcp::model::JsonObject; use rmcp::model::JsonObject;
use rmcp::model::Tool; use rmcp::model::Tool;
+7 -43
View File
@@ -1,6 +1,3 @@
use crate::client_common::tools::FreeformTool;
use crate::client_common::tools::FreeformToolFormat;
use crate::client_common::tools::ResponsesApiTool;
use crate::client_common::tools::ToolSpec; use crate::client_common::tools::ToolSpec;
use crate::config::AgentRoleConfig; use crate::config::AgentRoleConfig;
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME; use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
@@ -46,9 +43,11 @@ use codex_protocol::openai_models::WebSearchToolType;
use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource; use codex_protocol::protocol::SubAgentSource;
use codex_tools::ToolDefinition; use codex_tools::FreeformTool;
use codex_tools::parse_dynamic_tool; use codex_tools::FreeformToolFormat;
use codex_tools::parse_mcp_tool; use codex_tools::ResponsesApiTool;
use codex_tools::dynamic_tool_to_responses_api_tool;
use codex_tools::mcp_tool_to_responses_api_tool;
use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_template::Template; use codex_utils_template::Template;
use serde::Deserialize; use serde::Deserialize;
@@ -2382,41 +2381,6 @@ fn push_tool_spec(
} }
} }
pub(crate) fn mcp_tool_to_openai_tool(
fully_qualified_name: String,
tool: rmcp::model::Tool,
) -> Result<ResponsesApiTool, serde_json::Error> {
Ok(tool_definition_to_openai_tool(
parse_mcp_tool(&tool)?.renamed(fully_qualified_name),
))
}
pub(crate) fn mcp_tool_to_deferred_openai_tool(
name: String,
tool: rmcp::model::Tool,
) -> Result<ResponsesApiTool, serde_json::Error> {
Ok(tool_definition_to_openai_tool(
parse_mcp_tool(&tool)?.renamed(name).into_deferred(),
))
}
fn dynamic_tool_to_openai_tool(
tool: &DynamicToolSpec,
) -> Result<ResponsesApiTool, serde_json::Error> {
Ok(tool_definition_to_openai_tool(parse_dynamic_tool(tool)?))
}
fn tool_definition_to_openai_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,
}
}
/// Builds the tool registry builder while collecting tool specs for later serialization. /// Builds the tool registry builder while collecting tool specs for later serialization.
#[cfg(test)] #[cfg(test)]
pub(crate) fn build_specs( pub(crate) fn build_specs(
@@ -2911,7 +2875,7 @@ pub(crate) fn build_specs_with_discoverable_tools(
entries.sort_by(|a, b| a.0.cmp(&b.0)); entries.sort_by(|a, b| a.0.cmp(&b.0));
for (name, tool) in entries.into_iter() { for (name, tool) in entries.into_iter() {
match mcp_tool_to_openai_tool(name.clone(), tool.clone()) { match mcp_tool_to_responses_api_tool(name.clone(), &tool) {
Ok(converted_tool) => { Ok(converted_tool) => {
push_tool_spec( push_tool_spec(
&mut builder, &mut builder,
@@ -2930,7 +2894,7 @@ pub(crate) fn build_specs_with_discoverable_tools(
if !dynamic_tools.is_empty() { if !dynamic_tools.is_empty() {
for tool in dynamic_tools { for tool in dynamic_tools {
match dynamic_tool_to_openai_tool(tool) { match dynamic_tool_to_responses_api_tool(tool) {
Ok(converted_tool) => { Ok(converted_tool) => {
push_tool_spec( push_tool_spec(
&mut builder, &mut builder,
+2 -62
View File
@@ -8,11 +8,11 @@ use crate::tools::ToolRouter;
use crate::tools::registry::ConfiguredToolSpec; use crate::tools::registry::ConfiguredToolSpec;
use crate::tools::router::ToolRouterParams; use crate::tools::router::ToolRouterParams;
use codex_app_server_protocol::AppInfo; use codex_app_server_protocol::AppInfo;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::InputModality;
use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelsResponse; use codex_protocol::openai_models::ModelsResponse;
use codex_tools::AdditionalProperties; use codex_tools::AdditionalProperties;
use codex_tools::mcp_tool_to_deferred_responses_api_tool;
use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use std::path::PathBuf; use std::path::PathBuf;
@@ -64,28 +64,6 @@ fn search_capable_model_info() -> ModelInfo {
model_info model_info
} }
#[test]
fn search_tool_deferred_tools_always_set_defer_loading_true() {
let tool = mcp_tool(
"lookup_order",
"Look up an order",
serde_json::json!({
"type": "object",
"properties": {
"order_id": {"type": "string"}
},
"required": ["order_id"],
"additionalProperties": false,
}),
);
let openai_tool =
mcp_tool_to_deferred_openai_tool("mcp__codex_apps__lookup_order".to_string(), tool)
.expect("convert deferred tool");
assert_eq!(openai_tool.defer_loading, Some(true));
}
#[test] #[test]
fn deferred_responses_api_tool_serializes_with_defer_loading() { fn deferred_responses_api_tool_serializes_with_defer_loading() {
let tool = mcp_tool( let tool = mcp_tool(
@@ -102,7 +80,7 @@ fn deferred_responses_api_tool_serializes_with_defer_loading() {
); );
let serialized = serde_json::to_value(ToolSpec::Function( let serialized = serde_json::to_value(ToolSpec::Function(
mcp_tool_to_deferred_openai_tool("mcp__codex_apps__lookup_order".to_string(), tool) mcp_tool_to_deferred_responses_api_tool("mcp__codex_apps__lookup_order".to_string(), &tool)
.expect("convert deferred tool"), .expect("convert deferred tool"),
)) ))
.expect("serialize deferred tool"); .expect("serialize deferred tool");
@@ -127,44 +105,6 @@ fn deferred_responses_api_tool_serializes_with_defer_loading() {
); );
} }
#[test]
fn dynamic_tool_preserves_defer_loading() {
let tool = DynamicToolSpec {
name: "lookup_order".to_string(),
description: "Look up an order".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"order_id": {"type": "string"}
},
"required": ["order_id"],
"additionalProperties": false,
}),
defer_loading: true,
};
let openai_tool = dynamic_tool_to_openai_tool(&tool).expect("convert dynamic tool");
assert_eq!(
openai_tool,
ResponsesApiTool {
name: "lookup_order".to_string(),
description: "Look up an order".to_string(),
strict: false,
defer_loading: Some(true),
parameters: JsonSchema::Object {
properties: BTreeMap::from([(
"order_id".to_string(),
JsonSchema::String { description: None },
)]),
required: Some(vec!["order_id".to_string()]),
additional_properties: Some(false.into()),
},
output_schema: None,
}
);
}
fn tool_name(tool: &ToolSpec) -> &str { fn tool_name(tool: &ToolSpec) -> &str {
match tool { match tool {
ToolSpec::Function(ResponsesApiTool { name, .. }) => name, ToolSpec::Function(ResponsesApiTool { name, .. }) => name,
+12 -1
View File
@@ -5,15 +5,26 @@ shared across multiple crates and does not need to stay coupled to
`codex-core`. `codex-core`.
Today this crate is intentionally small. It currently owns the shared tool Today this crate is intentionally small. It currently owns the shared tool
schema primitives that no longer need to live in `core/src/tools/spec.rs`: schema and Responses API tool primitives that no longer need to live in
`core/src/tools/spec.rs` or `core/src/client_common.rs`:
- `JsonSchema` - `JsonSchema`
- `AdditionalProperties` - `AdditionalProperties`
- `ToolDefinition` - `ToolDefinition`
- `ResponsesApiTool`
- `FreeformTool`
- `FreeformToolFormat`
- `ToolSearchOutputTool`
- `ResponsesApiNamespace`
- `ResponsesApiNamespaceTool`
- `parse_tool_input_schema()` - `parse_tool_input_schema()`
- `parse_dynamic_tool()` - `parse_dynamic_tool()`
- `parse_mcp_tool()` - `parse_mcp_tool()`
- `mcp_call_tool_result_output_schema()` - `mcp_call_tool_result_output_schema()`
- `tool_definition_to_responses_api_tool()`
- `dynamic_tool_to_responses_api_tool()`
- `mcp_tool_to_responses_api_tool()`
- `mcp_tool_to_deferred_responses_api_tool()`
That extraction is the first step in a longer migration. The goal is not to That extraction is the first step in a longer migration. The goal is not to
move all of `core/src/tools` into this crate in one shot. Instead, the plan is move all of `core/src/tools` into this crate in one shot. Instead, the plan is
+13 -1
View File
@@ -1,8 +1,10 @@
//! Shared tool-schema parsing primitives that can live outside `codex-core`. //! Shared tool definitions and Responses API tool primitives that can live
//! outside `codex-core`.
mod dynamic_tool; mod dynamic_tool;
mod json_schema; mod json_schema;
mod mcp_tool; mod mcp_tool;
mod responses_api;
mod tool_definition; mod tool_definition;
pub use dynamic_tool::parse_dynamic_tool; pub use dynamic_tool::parse_dynamic_tool;
@@ -11,4 +13,14 @@ pub use json_schema::JsonSchema;
pub use json_schema::parse_tool_input_schema; pub use json_schema::parse_tool_input_schema;
pub use mcp_tool::mcp_call_tool_result_output_schema; pub use mcp_tool::mcp_call_tool_result_output_schema;
pub use mcp_tool::parse_mcp_tool; pub use mcp_tool::parse_mcp_tool;
pub use responses_api::FreeformTool;
pub use responses_api::FreeformToolFormat;
pub use responses_api::ResponsesApiNamespace;
pub use responses_api::ResponsesApiNamespaceTool;
pub use responses_api::ResponsesApiTool;
pub use responses_api::ToolSearchOutputTool;
pub use responses_api::dynamic_tool_to_responses_api_tool;
pub use responses_api::mcp_tool_to_deferred_responses_api_tool;
pub use responses_api::mcp_tool_to_responses_api_tool;
pub use responses_api::tool_definition_to_responses_api_tool;
pub use tool_definition::ToolDefinition; pub use tool_definition::ToolDefinition;
+102
View File
@@ -0,0 +1,102 @@
use crate::JsonSchema;
use crate::ToolDefinition;
use crate::parse_dynamic_tool;
use crate::parse_mcp_tool;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FreeformTool {
pub name: String,
pub description: String,
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<bool>,
pub parameters: JsonSchema,
#[serde(skip)]
pub output_schema: Option<Value>,
}
#[derive(Debug, Clone, Serialize, PartialEq)]
#[serde(tag = "type")]
pub enum ToolSearchOutputTool {
#[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<ResponsesApiNamespaceTool>,
}
#[derive(Debug, Clone, Serialize, PartialEq)]
#[serde(tag = "type")]
pub enum ResponsesApiNamespaceTool {
#[serde(rename = "function")]
Function(ResponsesApiTool),
}
pub fn dynamic_tool_to_responses_api_tool(
tool: &DynamicToolSpec,
) -> Result<ResponsesApiTool, serde_json::Error> {
Ok(tool_definition_to_responses_api_tool(parse_dynamic_tool(
tool,
)?))
}
pub fn mcp_tool_to_responses_api_tool(
name: String,
tool: &rmcp::model::Tool,
) -> Result<ResponsesApiTool, serde_json::Error> {
Ok(tool_definition_to_responses_api_tool(
parse_mcp_tool(tool)?.renamed(name),
))
}
pub fn mcp_tool_to_deferred_responses_api_tool(
name: String,
tool: &rmcp::model::Tool,
) -> Result<ResponsesApiTool, serde_json::Error> {
Ok(tool_definition_to_responses_api_tool(
parse_mcp_tool(tool)?.renamed(name).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;
+173
View File
@@ -0,0 +1,173 @@
use super::ResponsesApiNamespace;
use super::ResponsesApiNamespaceTool;
use super::ResponsesApiTool;
use super::ToolSearchOutputTool;
use super::dynamic_tool_to_responses_api_tool;
use super::mcp_tool_to_deferred_responses_api_tool;
use super::tool_definition_to_responses_api_tool;
use crate::JsonSchema;
use crate::ToolDefinition;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::collections::BTreeMap;
#[test]
fn tool_definition_to_responses_api_tool_omits_false_defer_loading() {
assert_eq!(
tool_definition_to_responses_api_tool(ToolDefinition {
name: "lookup_order".to_string(),
description: "Look up an order".to_string(),
input_schema: JsonSchema::Object {
properties: BTreeMap::from([(
"order_id".to_string(),
JsonSchema::String { description: None },
)]),
required: Some(vec!["order_id".to_string()]),
additional_properties: Some(false.into()),
},
output_schema: Some(json!({"type": "object"})),
defer_loading: false,
}),
ResponsesApiTool {
name: "lookup_order".to_string(),
description: "Look up an order".to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::Object {
properties: BTreeMap::from([(
"order_id".to_string(),
JsonSchema::String { description: None },
)]),
required: Some(vec!["order_id".to_string()]),
additional_properties: Some(false.into()),
},
output_schema: Some(json!({"type": "object"})),
}
);
}
#[test]
fn dynamic_tool_to_responses_api_tool_preserves_defer_loading() {
let tool = DynamicToolSpec {
name: "lookup_order".to_string(),
description: "Look up an order".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"order_id": {"type": "string"}
},
"required": ["order_id"],
"additionalProperties": false,
}),
defer_loading: true,
};
assert_eq!(
dynamic_tool_to_responses_api_tool(&tool).expect("convert dynamic tool"),
ResponsesApiTool {
name: "lookup_order".to_string(),
description: "Look up an order".to_string(),
strict: false,
defer_loading: Some(true),
parameters: JsonSchema::Object {
properties: BTreeMap::from([(
"order_id".to_string(),
JsonSchema::String { description: None },
)]),
required: Some(vec!["order_id".to_string()]),
additional_properties: Some(false.into()),
},
output_schema: None,
}
);
}
#[test]
fn mcp_tool_to_deferred_responses_api_tool_sets_defer_loading() {
let tool = rmcp::model::Tool {
name: "lookup_order".to_string().into(),
title: None,
description: Some("Look up an order".to_string().into()),
input_schema: std::sync::Arc::new(rmcp::model::object(json!({
"type": "object",
"properties": {
"order_id": {"type": "string"}
},
"required": ["order_id"],
"additionalProperties": false,
}))),
output_schema: None,
annotations: None,
execution: None,
icons: None,
meta: None,
};
assert_eq!(
mcp_tool_to_deferred_responses_api_tool(
"mcp__codex_apps__lookup_order".to_string(),
&tool,
)
.expect("convert deferred tool"),
ResponsesApiTool {
name: "mcp__codex_apps__lookup_order".to_string(),
description: "Look up an order".to_string(),
strict: false,
defer_loading: Some(true),
parameters: JsonSchema::Object {
properties: BTreeMap::from([(
"order_id".to_string(),
JsonSchema::String { description: None },
)]),
required: Some(vec!["order_id".to_string()]),
additional_properties: Some(false.into()),
},
output_schema: None,
}
);
}
#[test]
fn tool_search_output_namespace_serializes_with_deferred_child_tools() {
let namespace = ToolSearchOutputTool::Namespace(ResponsesApiNamespace {
name: "mcp__codex_apps__calendar".to_string(),
description: "Plan events".to_string(),
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
name: "create_event".to_string(),
description: "Create a calendar event.".to_string(),
strict: false,
defer_loading: Some(true),
parameters: JsonSchema::Object {
properties: Default::default(),
required: None,
additional_properties: None,
},
output_schema: None,
})],
});
let value = serde_json::to_value(namespace).expect("serialize namespace");
assert_eq!(
value,
json!({
"type": "namespace",
"name": "mcp__codex_apps__calendar",
"description": "Plan events",
"tools": [
{
"type": "function",
"name": "create_event",
"description": "Create a calendar event.",
"strict": false,
"defer_loading": true,
"parameters": {
"type": "object",
"properties": {}
}
}
]
})
);
}