Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 26 additions & 26 deletions codex-rs/core/src/tools/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ use codex_protocol::openai_models::WebSearchToolType;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_tools::ParsedToolDefinition;
use codex_tools::parse_dynamic_tool;
use codex_tools::parse_mcp_tool;
pub use codex_tools::parse_tool_input_schema;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Deserialize;
use serde::Serialize;
Expand Down Expand Up @@ -2366,16 +2367,10 @@ pub(crate) fn mcp_tool_to_openai_tool(
fully_qualified_name: String,
tool: rmcp::model::Tool,
) -> Result<ResponsesApiTool, serde_json::Error> {
let parsed_tool = parse_mcp_tool(&tool)?;

Ok(ResponsesApiTool {
name: fully_qualified_name,
description: parsed_tool.description,
strict: false,
defer_loading: None,
parameters: parsed_tool.input_schema,
output_schema: Some(parsed_tool.output_schema),
})
Ok(parsed_tool_to_openai_tool(
fully_qualified_name,
parse_mcp_tool(&tool)?,
))
}

pub(crate) fn mcp_tool_to_deferred_openai_tool(
Expand All @@ -2384,29 +2379,34 @@ pub(crate) fn mcp_tool_to_deferred_openai_tool(
) -> Result<ResponsesApiTool, serde_json::Error> {
let parsed_tool = parse_mcp_tool(&tool)?;

Ok(ResponsesApiTool {
Ok(parsed_tool_to_openai_tool(
name,
description: parsed_tool.description,
strict: false,
defer_loading: Some(true),
parameters: parsed_tool.input_schema,
output_schema: None,
})
ParsedToolDefinition {
output_schema: None,
defer_loading: true,
..parsed_tool
},
))
}

fn dynamic_tool_to_openai_tool(
tool: &DynamicToolSpec,
) -> Result<ResponsesApiTool, serde_json::Error> {
let input_schema = parse_tool_input_schema(&tool.input_schema)?;
Ok(parsed_tool_to_openai_tool(
tool.name.clone(),
parse_dynamic_tool(tool)?,
))
}

Ok(ResponsesApiTool {
name: tool.name.clone(),
description: tool.description.clone(),
fn parsed_tool_to_openai_tool(name: String, parsed_tool: ParsedToolDefinition) -> ResponsesApiTool {
ResponsesApiTool {
name,
description: parsed_tool.description,
strict: false,
defer_loading: None,
parameters: input_schema,
output_schema: None,
})
defer_loading: parsed_tool.defer_loading.then_some(true),
parameters: parsed_tool.input_schema,
output_schema: parsed_tool.output_schema,
}
}

/// Builds the tool registry builder while collecting tool specs for later serialization.
Expand Down
39 changes: 39 additions & 0 deletions codex-rs/core/src/tools/spec_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::tools::ToolRouter;
use crate::tools::registry::ConfiguredToolSpec;
use crate::tools::router::ToolRouterParams;
use codex_app_server_protocol::AppInfo;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_protocol::openai_models::InputModality;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelsResponse;
Expand Down Expand Up @@ -126,6 +127,44 @@ 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 {
match tool {
ToolSpec::Function(ResponsesApiTool { name, .. }) => name,
Expand Down
1 change: 1 addition & 0 deletions codex-rs/tools/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ version.workspace = true
workspace = true

[dependencies]
codex-protocol = { workspace = true }
rmcp = { workspace = true, default-features = false, features = [
"base64",
"macros",
Expand Down
3 changes: 2 additions & 1 deletion codex-rs/tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ schema primitives that no longer need to live in `core/src/tools/spec.rs`:

- `JsonSchema`
- `AdditionalProperties`
- `ParsedToolDefinition`
- `parse_tool_input_schema()`
- `ParsedMcpTool`
- `parse_dynamic_tool()`
- `parse_mcp_tool()`
- `mcp_call_tool_result_output_schema()`

Expand Down
24 changes: 24 additions & 0 deletions codex-rs/tools/src/dynamic_tool.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use crate::ParsedToolDefinition;
use crate::parse_tool_input_schema;
use codex_protocol::dynamic_tools::DynamicToolSpec;

pub fn parse_dynamic_tool(
tool: &DynamicToolSpec,
) -> Result<ParsedToolDefinition, serde_json::Error> {
let DynamicToolSpec {
name: _,
description,
input_schema,
defer_loading,
} = tool;
Ok(ParsedToolDefinition {
description: description.clone(),
input_schema: parse_tool_input_schema(input_schema)?,
output_schema: None,
defer_loading: *defer_loading,
})
}

#[cfg(test)]
#[path = "dynamic_tool_tests.rs"]
mod tests;
68 changes: 68 additions & 0 deletions codex-rs/tools/src/dynamic_tool_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use super::parse_dynamic_tool;
use crate::JsonSchema;
use crate::ParsedToolDefinition;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;

#[test]
fn parse_dynamic_tool_sanitizes_input_schema() {
let tool = DynamicToolSpec {
name: "lookup_ticket".to_string(),
description: "Fetch a ticket".to_string(),
input_schema: serde_json::json!({
"properties": {
"id": {
"description": "Ticket identifier"
}
}
}),
defer_loading: false,
};

assert_eq!(
parse_dynamic_tool(&tool).expect("parse dynamic tool"),
ParsedToolDefinition {
description: "Fetch a ticket".to_string(),
input_schema: JsonSchema::Object {
properties: BTreeMap::from([(
"id".to_string(),
JsonSchema::String {
description: Some("Ticket identifier".to_string()),
},
)]),
required: None,
additional_properties: None,
},
output_schema: None,
defer_loading: false,
}
);
}

#[test]
fn parse_dynamic_tool_preserves_defer_loading() {
let tool = DynamicToolSpec {
name: "lookup_ticket".to_string(),
description: "Fetch a ticket".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {}
}),
defer_loading: true,
};

assert_eq!(
parse_dynamic_tool(&tool).expect("parse dynamic tool"),
ParsedToolDefinition {
description: "Fetch a ticket".to_string(),
input_schema: JsonSchema::Object {
properties: BTreeMap::new(),
required: None,
additional_properties: None,
},
output_schema: None,
defer_loading: true,
}
);
}
5 changes: 4 additions & 1 deletion codex-rs/tools/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
//! Shared tool-schema parsing primitives that can live outside `codex-core`.

mod dynamic_tool;
mod json_schema;
mod mcp_tool;
mod parsed_tool_definition;

pub use dynamic_tool::parse_dynamic_tool;
pub use json_schema::AdditionalProperties;
pub use json_schema::JsonSchema;
pub use json_schema::parse_tool_input_schema;
pub use mcp_tool::ParsedMcpTool;
pub use mcp_tool::mcp_call_tool_result_output_schema;
pub use mcp_tool::parse_mcp_tool;
pub use parsed_tool_definition::ParsedToolDefinition;
20 changes: 7 additions & 13 deletions codex-rs/tools/src/mcp_tool.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
use crate::JsonSchema;
use crate::ParsedToolDefinition;
use crate::parse_tool_input_schema;
use serde_json::Value as JsonValue;
use serde_json::json;

/// Parsed MCP tool metadata and schemas that can be adapted into a higher-level
/// tool spec by downstream crates.
#[derive(Debug, PartialEq)]
pub struct ParsedMcpTool {
pub description: String,
pub input_schema: JsonSchema,
pub output_schema: JsonValue,
}

pub fn parse_mcp_tool(tool: &rmcp::model::Tool) -> Result<ParsedMcpTool, serde_json::Error> {
pub fn parse_mcp_tool(tool: &rmcp::model::Tool) -> Result<ParsedToolDefinition, serde_json::Error> {
let mut serialized_input_schema = serde_json::Value::Object(tool.input_schema.as_ref().clone());

// OpenAI models mandate the "properties" field in the schema. Some MCP
Expand All @@ -34,10 +25,13 @@ pub fn parse_mcp_tool(tool: &rmcp::model::Tool) -> Result<ParsedMcpTool, serde_j
.map(|output_schema| serde_json::Value::Object(output_schema.as_ref().clone()))
.unwrap_or_else(|| JsonValue::Object(serde_json::Map::new()));

Ok(ParsedMcpTool {
Ok(ParsedToolDefinition {
description: tool.description.clone().map(Into::into).unwrap_or_default(),
input_schema,
output_schema: mcp_call_tool_result_output_schema(structured_content_schema),
output_schema: Some(mcp_call_tool_result_output_schema(
structured_content_schema,
)),
defer_loading: false,
})
}

Expand Down
21 changes: 12 additions & 9 deletions codex-rs/tools/src/mcp_tool_tests.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::ParsedMcpTool;
use super::mcp_call_tool_result_output_schema;
use super::parse_mcp_tool;
use crate::JsonSchema;
use crate::ParsedToolDefinition;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;

Expand Down Expand Up @@ -31,14 +31,15 @@ fn parse_mcp_tool_inserts_empty_properties() {

assert_eq!(
parse_mcp_tool(&tool).expect("parse MCP tool"),
ParsedMcpTool {
ParsedToolDefinition {
description: "No properties".to_string(),
input_schema: JsonSchema::Object {
properties: BTreeMap::new(),
required: None,
additional_properties: None,
},
output_schema: mcp_call_tool_result_output_schema(serde_json::json!({})),
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({}))),
defer_loading: false,
}
);
}
Expand Down Expand Up @@ -67,14 +68,14 @@ fn parse_mcp_tool_preserves_top_level_output_schema() {

assert_eq!(
parse_mcp_tool(&tool).expect("parse MCP tool"),
ParsedMcpTool {
ParsedToolDefinition {
description: "Has output schema".to_string(),
input_schema: JsonSchema::Object {
properties: BTreeMap::new(),
required: None,
additional_properties: None,
},
output_schema: mcp_call_tool_result_output_schema(serde_json::json!({
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({
"properties": {
"result": {
"properties": {
Expand All @@ -83,7 +84,8 @@ fn parse_mcp_tool_preserves_top_level_output_schema() {
}
},
"required": ["result"]
})),
}))),
defer_loading: false,
}
);
}
Expand All @@ -105,16 +107,17 @@ fn parse_mcp_tool_preserves_output_schema_without_inferred_type() {

assert_eq!(
parse_mcp_tool(&tool).expect("parse MCP tool"),
ParsedMcpTool {
ParsedToolDefinition {
description: "Has enum output schema".to_string(),
input_schema: JsonSchema::Object {
properties: BTreeMap::new(),
required: None,
additional_properties: None,
},
output_schema: mcp_call_tool_result_output_schema(serde_json::json!({
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({
"enum": ["ok", "error"]
})),
}))),
defer_loading: false,
}
);
}
12 changes: 12 additions & 0 deletions codex-rs/tools/src/parsed_tool_definition.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use crate::JsonSchema;
use serde_json::Value as JsonValue;

/// Parsed tool metadata and schemas that downstream crates can adapt into
/// higher-level tool specs.
#[derive(Debug, PartialEq)]
pub struct ParsedToolDefinition {
pub description: String,
pub input_schema: JsonSchema,
pub output_schema: Option<JsonValue>,
pub defer_loading: bool,
}
Loading