diff --git a/codex-rs/codex-mcp/src/codex_apps_cache_tests.rs b/codex-rs/codex-mcp/src/codex_apps_cache_tests.rs index 46ad311a1b56..0d1e2ffdc5e2 100644 --- a/codex-rs/codex-mcp/src/codex_apps_cache_tests.rs +++ b/codex-rs/codex-mcp/src/codex_apps_cache_tests.rs @@ -26,6 +26,7 @@ fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo { format!("Test tool: {tool_name}"), Arc::new(JsonObject::default()), ), + openai_file_input_optional_fields: Default::default(), connector_id: None, connector_name: None, plugin_display_names: Vec::new(), diff --git a/codex-rs/codex-mcp/src/connection_manager.rs b/codex-rs/codex-mcp/src/connection_manager.rs index 2143e3b57765..2a261a569711 100644 --- a/codex-rs/codex-mcp/src/connection_manager.rs +++ b/codex-rs/codex-mcp/src/connection_manager.rs @@ -34,6 +34,7 @@ use crate::runtime::emit_duration; use crate::server::EffectiveMcpServer; use crate::server::McpServerMetadata; use crate::tools::ToolInfo; +use crate::tools::declared_openai_file_input_optional_fields; use crate::tools::filter_tools; use crate::tools::normalize_tools_for_model_with_prefix; use crate::tools::tool_with_model_visible_input_schema; @@ -579,6 +580,8 @@ impl McpConnectionManager { let tools = filter_tools(tools, &managed_client.tool_filter) .into_iter() .map(|mut tool| { + tool.openai_file_input_optional_fields = + declared_openai_file_input_optional_fields(&tool.tool); tool.tool = tool_with_model_visible_input_schema(&tool.tool); self.with_server_metadata(tool) }); diff --git a/codex-rs/codex-mcp/src/connection_manager_tests.rs b/codex-rs/codex-mcp/src/connection_manager_tests.rs index 41fc19a594f5..ae285f2f9c15 100644 --- a/codex-rs/codex-mcp/src/connection_manager_tests.rs +++ b/codex-rs/codex-mcp/src/connection_manager_tests.rs @@ -17,6 +17,7 @@ use crate::server::McpServerMetadata; use crate::server::McpServerOrigin; use crate::tools::ToolFilter; use crate::tools::ToolInfo; +use crate::tools::declared_openai_file_input_optional_fields; use crate::tools::filter_tools; use crate::tools::normalize_tools_for_model_with_prefix; use crate::tools::tool_with_model_visible_input_schema; @@ -43,6 +44,7 @@ use rmcp::model::JsonObject; use rmcp::model::Meta; use rmcp::model::NumberOrString; use rmcp::model::Tool; +use std::collections::HashMap; use std::collections::HashSet; use std::io; use std::sync::Arc; @@ -63,6 +65,7 @@ fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo { format!("Test tool: {tool_name}"), Arc::new(JsonObject::default()), ), + openai_file_input_optional_fields: Default::default(), connector_id: None, connector_name: None, plugin_display_names: Vec::new(), @@ -231,11 +234,28 @@ fn tool_with_model_visible_input_schema_masks_file_params() { "properties": { "file": { "type": "object", - "description": "Original file payload." + "description": "Original file payload.", + "properties": { + "download_url": {"type": "string"}, + "file_id": {"type": "string"} + } }, "files": { - "type": "array", - "items": {"type": "object"} + "description": "Original file payloads.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "download_url": {"type": "string"}, + "file_id": {"type": "string"}, + "mime_type": {"type": "string"} + } + } + }, + {"type": "null"} + ] } } }) @@ -266,7 +286,7 @@ fn tool_with_model_visible_input_schema_masks_file_params() { "files": { "type": "array", "items": {"type": "string"}, - "description": "This parameter expects an absolute local file path. If you want to upload a file, provide the absolute path to that file here." + "description": "Original file payloads. This parameter expects an absolute local file path. If you want to upload a file, provide the absolute path to that file here." } } }) @@ -276,6 +296,107 @@ fn tool_with_model_visible_input_schema_masks_file_params() { ); } +#[test] +fn declared_openai_file_input_optional_fields_follow_canonical_payload_schemas() { + let mut tool = Tool::new( + "upload".to_string(), + "Upload files".to_string(), + Arc::new( + serde_json::json!({ + "type": "object", + "properties": { + "photoshop_image": { + "type": "object", + "properties": { + "download_url": {"type": "string"}, + "file_id": {"type": "string"} + } + }, + "drive_import": { + "type": "object", + "properties": { + "download_url": {"type": "string"}, + "file_id": {"type": "string"}, + "mime_type": {"type": "string"}, + "file_name": {"type": "string"} + } + }, + "attachments": { + "anyOf": [ + { + "type": "array", + "items": { + "oneOf": [ + { + "allOf": [{ + "type": "object", + "properties": { + "download_url": {"type": "string"}, + "file_id": {"type": "string"}, + "mime_type": {"type": "string"} + } + }] + }, + {"type": "null"} + ] + } + }, + {"type": "null"} + ] + }, + "noncanonical_file": { + "type": "object", + "properties": { + "download_url": {"type": "string"}, + "file_id": {"type": "string"}, + "mime_type": {"type": "string"}, + "uri": {"type": "string"} + } + }, + "unrelated_object": { + "type": "object", + "properties": { + "mime_type": {"type": "string"}, + "file_name": {"type": "string"} + } + } + } + }) + .as_object() + .expect("object") + .clone(), + ), + ); + tool.meta = Some(Meta( + serde_json::json!({ + "openai/fileParams": [ + "photoshop_image", + "drive_import", + "attachments", + "noncanonical_file", + "unrelated_object" + ] + }) + .as_object() + .expect("object") + .clone(), + )); + + assert_eq!( + declared_openai_file_input_optional_fields(&tool), + HashMap::from([ + ("photoshop_image".to_string(), Vec::new()), + ( + "drive_import".to_string(), + vec!["mime_type".to_string(), "file_name".to_string()] + ), + ("attachments".to_string(), vec!["mime_type".to_string()]), + ("noncanonical_file".to_string(), Vec::new()), + ("unrelated_object".to_string(), Vec::new()), + ]) + ); +} + #[test] fn tool_with_model_visible_input_schema_leaves_tools_without_file_params_unchanged() { let original_tool = create_test_tool("custom", "upload").tool; diff --git a/codex-rs/codex-mcp/src/rmcp_client.rs b/codex-rs/codex-mcp/src/rmcp_client.rs index 37819203f6ac..1a4af28afb39 100644 --- a/codex-rs/codex-mcp/src/rmcp_client.rs +++ b/codex-rs/codex-mcp/src/rmcp_client.rs @@ -33,6 +33,7 @@ use crate::server::EffectiveMcpServer; use crate::server::McpServerLaunch; use crate::tools::ToolFilter; use crate::tools::ToolInfo; +use crate::tools::declared_openai_file_input_optional_fields; use crate::tools::filter_tools; use crate::tools::tool_with_model_visible_input_schema; use anyhow::Result; @@ -594,6 +595,8 @@ fn prepare_codex_apps_tools_for_model( tool_plugin_provenance: &ToolPluginProvenance, ) -> Vec { for tool in &mut tools { + tool.openai_file_input_optional_fields = + declared_openai_file_input_optional_fields(&tool.tool); tool.tool = tool_with_model_visible_input_schema(&tool.tool); let plugin_names = match tool.connector_id.as_deref() { Some(connector_id) => { @@ -707,6 +710,7 @@ fn codex_apps_tool_info_from_listed_tool( callable_namespace, namespace_description, tool: tool_def, + openai_file_input_optional_fields: HashMap::new(), connector_id, connector_name, plugin_display_names: Vec::new(), @@ -730,6 +734,7 @@ fn regular_mcp_tool_info_from_listed_tool( callable_namespace: server_name.to_string(), namespace_description: server_instructions.map(str::to_string), tool: tool_def, + openai_file_input_optional_fields: HashMap::new(), connector_id: None, connector_name: None, plugin_display_names: Vec::new(), @@ -1117,6 +1122,7 @@ mod tests { callable_namespace: "codex_apps__gmail".to_string(), namespace_description: Some("Mail connector".to_string()), tool: expected_tool, + openai_file_input_optional_fields: HashMap::new(), connector_id: Some("connector_gmail".to_string()), connector_name: Some("Gmail".to_string()), plugin_display_names: Vec::new(), diff --git a/codex-rs/codex-mcp/src/tools.rs b/codex-rs/codex-mcp/src/tools.rs index f63ab002df41..f1d38e20152d 100644 --- a/codex-rs/codex-mcp/src/tools.rs +++ b/codex-rs/codex-mcp/src/tools.rs @@ -49,6 +49,11 @@ pub struct ToolInfo { pub namespace_description: Option, /// Raw MCP tool definition; `tool.name` is sent back to the MCP server. pub tool: Tool, + /// Optional provided-file fields accepted by each declared `openai/fileParams` + /// argument. This is derived from the raw MCP schema before file arguments are + /// masked as local paths for the model. + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub openai_file_input_optional_fields: HashMap>, pub connector_id: Option, pub connector_name: Option, #[serde(default)] @@ -261,6 +266,87 @@ const MCP_TOOL_NAME_DELIMITER: &str = "__"; const MAX_TOOL_NAME_LENGTH: usize = 64; const CALLABLE_NAME_HASH_LEN: usize = 12; const META_OPENAI_FILE_PARAMS: &str = "openai/fileParams"; +const OPENAI_FILE_FIELDS: [&str; 4] = ["download_url", "file_id", "mime_type", "file_name"]; + +#[derive(Default)] +struct OpenAiFileSchemaInfo { + is_array: bool, + accepts_mime_type: bool, + accepts_file_name: bool, +} + +pub(crate) fn declared_openai_file_input_optional_fields( + tool: &Tool, +) -> HashMap> { + let file_params = declared_openai_file_input_param_names(tool.meta.as_deref()); + let properties = tool + .input_schema + .get("properties") + .and_then(JsonValue::as_object); + + file_params + .into_iter() + .map(|field_name| { + let optional_fields = properties + .and_then(|properties| properties.get(&field_name)) + .map(|schema| { + let schema_info = openai_file_schema_info(schema); + let mut optional_fields = Vec::new(); + if schema_info.accepts_mime_type { + optional_fields.push("mime_type".to_string()); + } + if schema_info.accepts_file_name { + optional_fields.push("file_name".to_string()); + } + optional_fields + }) + .unwrap_or_default(); + (field_name, optional_fields) + }) + .collect() +} + +fn openai_file_schema_info(schema: &JsonValue) -> OpenAiFileSchemaInfo { + let mut info = OpenAiFileSchemaInfo::default(); + let mut pending = vec![(schema, false)]; + + while let Some((schema, inside_array)) = pending.pop() { + let Some(schema) = schema.as_object() else { + continue; + }; + + for keyword in ["anyOf", "oneOf", "allOf"] { + if let Some(variants) = schema.get(keyword).and_then(JsonValue::as_array) { + pending.extend(variants.iter().map(|variant| (variant, inside_array))); + } + } + + if schema.get("type").and_then(JsonValue::as_str) == Some("array") { + if let Some(items) = schema.get("items") { + pending.push((items, true)); + } + continue; + } + + let Some(properties) = schema.get("properties").and_then(JsonValue::as_object) else { + continue; + }; + if !properties.contains_key("download_url") + || !properties.contains_key("file_id") + || !properties + .keys() + .all(|field| OPENAI_FILE_FIELDS.contains(&field.as_str())) + { + continue; + } + + info.is_array |= inside_array; + info.accepts_mime_type |= properties.contains_key("mime_type"); + info.accepts_file_name |= properties.contains_key("file_name"); + } + + info +} fn rewrite_input_schema_for_local_file_paths(input_schema: &mut JsonValue, file_params: &[String]) { let Some(properties) = input_schema @@ -280,6 +366,7 @@ fn rewrite_input_schema_for_local_file_paths(input_schema: &mut JsonValue, file_ } fn rewrite_input_property_schema_as_local_file_path(schema: &mut JsonValue) { + let is_array = openai_file_schema_info(schema).is_array; let Some(object) = schema.as_object_mut() else { return; }; @@ -296,8 +383,6 @@ fn rewrite_input_property_schema_as_local_file_path(schema: &mut JsonValue) { description = format!("{description} {guidance}"); } - let is_array = object.get("type").and_then(JsonValue::as_str) == Some("array") - || object.get("items").is_some(); object.clear(); object.insert("description".to_string(), JsonValue::String(description)); if is_array { diff --git a/codex-rs/core/src/connectors_tests.rs b/codex-rs/core/src/connectors_tests.rs index fb285390484d..e28f0f204e98 100644 --- a/codex-rs/core/src/connectors_tests.rs +++ b/codex-rs/core/src/connectors_tests.rs @@ -71,6 +71,7 @@ fn codex_app_tool( callable_namespace: tool_namespace, namespace_description: None, tool: test_tool_definition(tool_name), + openai_file_input_optional_fields: Default::default(), connector_id: Some(connector_id.to_string()), connector_name: connector_name.map(ToOwned::to_owned), plugin_display_names: plugin_names(plugin_display_names), @@ -115,6 +116,7 @@ fn accessible_connectors_from_mcp_tools_carries_plugin_display_names() { callable_namespace: "sample".to_string(), namespace_description: None, tool: test_tool_definition("echo"), + openai_file_input_optional_fields: Default::default(), connector_id: None, connector_name: None, plugin_display_names: plugin_names(&["ignored"]), @@ -297,6 +299,7 @@ fn accessible_connectors_from_mcp_tools_preserves_description() { "Create a calendar event", Arc::new(JsonObject::default()), ), + openai_file_input_optional_fields: Default::default(), connector_id: Some("calendar".to_string()), connector_name: Some("Calendar".to_string()), plugin_display_names: Vec::new(), diff --git a/codex-rs/core/src/mcp_openai_file.rs b/codex-rs/core/src/mcp_openai_file.rs index 5044568cd62a..36d4fe184e5b 100644 --- a/codex-rs/core/src/mcp_openai_file.rs +++ b/codex-rs/core/src/mcp_openai_file.rs @@ -18,12 +18,13 @@ use codex_api::upload_openai_file; use codex_login::CodexAuth; use codex_utils_path_uri::PathUri; use serde_json::Value as JsonValue; +use std::collections::HashMap; pub(crate) async fn rewrite_mcp_tool_arguments_for_openai_files( sess: &Session, turn_context: &TurnContext, arguments_value: Option, - openai_file_input_params: Option<&[String]>, + openai_file_input_params: Option<&HashMap>>, ) -> Result, String> { let Some(openai_file_input_params) = openai_file_input_params else { return Ok(arguments_value); @@ -38,13 +39,18 @@ pub(crate) async fn rewrite_mcp_tool_arguments_for_openai_files( let auth = sess.services.auth_manager.auth().await; let mut rewritten_arguments = arguments.clone(); - for field_name in openai_file_input_params { + for (field_name, optional_fields) in openai_file_input_params { let Some(value) = arguments.get(field_name) else { continue; }; - let Some(uploaded_value) = - rewrite_argument_value_for_openai_files(turn_context, auth.as_ref(), field_name, value) - .await? + let Some(uploaded_value) = rewrite_argument_value_for_openai_files( + turn_context, + auth.as_ref(), + field_name, + optional_fields, + value, + ) + .await? else { continue; }; @@ -62,6 +68,7 @@ async fn rewrite_argument_value_for_openai_files( turn_context: &TurnContext, auth: Option<&CodexAuth>, field_name: &str, + optional_fields: &[String], value: &JsonValue, ) -> Result, String> { match value { @@ -71,6 +78,7 @@ async fn rewrite_argument_value_for_openai_files( auth, field_name, /*index*/ None, + optional_fields, file_path, ) .await?; @@ -87,6 +95,7 @@ async fn rewrite_argument_value_for_openai_files( auth, field_name, Some(index), + optional_fields, file_path, ) .await?; @@ -103,6 +112,7 @@ async fn build_uploaded_argument_value( auth: Option<&CodexAuth>, field_name: &str, index: Option, + optional_fields: &[String], file_path: &str, ) -> Result { let contextualize_error = |error: String| match index { @@ -168,14 +178,29 @@ async fn build_uploaded_argument_value( ) .await .map_err(|error| contextualize_error(error.to_string()))?; - Ok(serde_json::json!({ - "download_url": uploaded.download_url, - "file_id": uploaded.file_id, - "mime_type": uploaded.mime_type, - "file_name": uploaded.file_name, - "uri": uploaded.uri, - "file_size_bytes": uploaded.file_size_bytes, - })) + let mut payload = serde_json::Map::new(); + payload.insert( + "download_url".to_string(), + JsonValue::String(uploaded.download_url), + ); + payload.insert("file_id".to_string(), JsonValue::String(uploaded.file_id)); + if optional_fields + .iter() + .any(|optional_field| optional_field == "mime_type") + && let Some(mime_type) = uploaded.mime_type + { + payload.insert("mime_type".to_string(), JsonValue::String(mime_type)); + } + if optional_fields + .iter() + .any(|optional_field| optional_field == "file_name") + { + payload.insert( + "file_name".to_string(), + JsonValue::String(uploaded.file_name), + ); + } + Ok(JsonValue::Object(payload)) } #[cfg(test)] @@ -288,6 +313,7 @@ mod tests { Some(&auth), "file", /*index*/ None, + &["mime_type".to_string(), "file_name".to_string()], "file_report.csv", ) .await @@ -300,8 +326,6 @@ mod tests { "file_id": "file_123", "mime_type": "text/csv", "file_name": "file_report.csv", - "uri": "sediment://file_123", - "file_size_bytes": 5, }) ); } @@ -322,6 +346,7 @@ mod tests { Some(&auth), "file", /*index*/ None, + &[], "oversized.bin", ) .await @@ -392,6 +417,7 @@ mod tests { &turn_context, Some(&auth), "file", + &[], &serde_json::json!("file_report.csv"), ) .await @@ -402,10 +428,6 @@ mod tests { Some(serde_json::json!({ "download_url": format!("{}/download/file_123", server.uri()), "file_id": "file_123", - "mime_type": "text/csv", - "file_name": "file_report.csv", - "uri": "sediment://file_123", - "file_size_bytes": 5, })) ); } @@ -506,6 +528,7 @@ mod tests { &turn_context, Some(&auth), "files", + &[], &serde_json::json!(["one.csv", "two.csv"]), ) .await @@ -517,18 +540,10 @@ mod tests { { "download_url": format!("{}/download/file_1", server.uri()), "file_id": "file_1", - "mime_type": "text/csv", - "file_name": "one.csv", - "uri": "sediment://file_1", - "file_size_bytes": 3, }, { "download_url": format!("{}/download/file_2", server.uri()), "file_id": "file_2", - "mime_type": "text/csv", - "file_name": "two.csv", - "uri": "sediment://file_2", - "file_size_bytes": 3, } ])) ); @@ -546,7 +561,7 @@ mod tests { Some(serde_json::json!({ "file": "/definitely/missing/file.csv", })), - Some(&["file".to_string()]), + Some(&HashMap::from([("file".to_string(), Vec::new())])), ) .await .expect_err("missing file should fail"); diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index d03f232d12b3..f99f8d97832b 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -41,7 +41,6 @@ use codex_mcp::McpPermissionPromptAutoApproveContext; use codex_mcp::SandboxState; use codex_mcp::auth_elicitation_completed_result; use codex_mcp::build_auth_elicitation_plan; -use codex_mcp::declared_openai_file_input_param_names; use codex_mcp::mcp_permission_prompt_is_auto_approved; use codex_protocol::approvals::ElicitationRequest; use codex_protocol::items::McpToolCallError; @@ -382,7 +381,7 @@ async fn handle_approved_mcp_tool_call( sess, turn_context, arguments_value.clone(), - metadata.and_then(|metadata| metadata.openai_file_input_params.as_deref()), + metadata.and_then(|metadata| metadata.openai_file_input_params.as_ref()), ) .await; let tool_input = match &rewrite { @@ -1019,7 +1018,7 @@ pub(crate) struct McpToolApprovalMetadata { mcp_app_resource_uri: Option, template_id: Option, codex_apps_meta: Option>, - openai_file_input_params: Option>, + openai_file_input_params: Option>>, } const MCP_TOOL_OPENAI_OUTPUT_TEMPLATE_META_KEY: &str = "openai/outputTemplate"; @@ -1561,17 +1560,17 @@ pub(crate) async fn lookup_mcp_tool_metadata( // Disallow custom MCPs from uploading files via fileParams. openai_file_input_params: openai_file_input_params_for_server( server, - tool_info.tool.meta.as_deref(), + &tool_info.openai_file_input_optional_fields, ), }) } fn openai_file_input_params_for_server( server: &str, - meta: Option<&serde_json::Map>, -) -> Option> { + openai_file_input_optional_fields: &HashMap>, +) -> Option>> { (server == CODEX_APPS_MCP_SERVER_NAME) - .then_some(declared_openai_file_input_param_names(meta)) + .then(|| openai_file_input_optional_fields.clone()) .filter(|params| !params.is_empty()) } diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index f68d32ff4e69..b7c48fc916a5 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -349,17 +349,14 @@ fn mcp_app_resource_uri_reads_known_tool_meta_keys() { #[test] fn openai_file_params_are_only_honored_for_codex_apps() { - let meta = serde_json::json!({ - "openai/fileParams": ["file"], - }); - let meta = meta.as_object(); + let params = HashMap::from([("file".to_string(), Vec::new())]); assert_eq!( - openai_file_input_params_for_server(CODEX_APPS_MCP_SERVER_NAME, meta), - Some(vec!["file".to_string()]) + openai_file_input_params_for_server(CODEX_APPS_MCP_SERVER_NAME, ¶ms), + Some(params.clone()) ); assert_eq!( - openai_file_input_params_for_server("minimaltest", meta), + openai_file_input_params_for_server("minimaltest", ¶ms), None ); } diff --git a/codex-rs/core/src/mcp_tool_exposure_test.rs b/codex-rs/core/src/mcp_tool_exposure_test.rs index ff5617b073ce..8e9cc2256fba 100644 --- a/codex-rs/core/src/mcp_tool_exposure_test.rs +++ b/codex-rs/core/src/mcp_tool_exposure_test.rs @@ -56,6 +56,7 @@ fn make_mcp_tool( format!("Test tool: {tool_name}"), Arc::new(JsonObject::default()), ), + openai_file_input_optional_fields: Default::default(), connector_id: connector_id.map(str::to_string), connector_name: connector_name.map(str::to_string), plugin_display_names: Vec::new(), diff --git a/codex-rs/core/src/tools/handlers/mcp.rs b/codex-rs/core/src/tools/handlers/mcp.rs index c9533fd223ea..7cadbcf0c379 100644 --- a/codex-rs/core/src/tools/handlers/mcp.rs +++ b/codex-rs/core/src/tools/handlers/mcp.rs @@ -545,6 +545,7 @@ mod tests { "type": "object", }))), ), + openai_file_input_optional_fields: Default::default(), connector_id: None, connector_name: None, plugin_display_names: Vec::new(), diff --git a/codex-rs/core/src/tools/handlers/mcp_search_tests.rs b/codex-rs/core/src/tools/handlers/mcp_search_tests.rs index 8ddfa93d8480..b78f8c5205ef 100644 --- a/codex-rs/core/src/tools/handlers/mcp_search_tests.rs +++ b/codex-rs/core/src/tools/handlers/mcp_search_tests.rs @@ -63,6 +63,7 @@ fn tool_info() -> ToolInfo { }))), ) .with_title("Create event"), + openai_file_input_optional_fields: Default::default(), connector_id: None, connector_name: Some("Calendar".to_string()), plugin_display_names: vec![" Calendar plugin ".to_string(), " ".to_string()], diff --git a/codex-rs/core/src/tools/handlers/tool_search.rs b/codex-rs/core/src/tools/handlers/tool_search.rs index 272da2f226cb..e5e607a48ff9 100644 --- a/codex-rs/core/src/tools/handlers/tool_search.rs +++ b/codex-rs/core/src/tools/handlers/tool_search.rs @@ -342,6 +342,7 @@ mod tests { "additionalProperties": false, }))), ), + openai_file_input_optional_fields: Default::default(), connector_id: None, connector_name: None, plugin_display_names: Vec::new(), diff --git a/codex-rs/core/src/tools/router_tests.rs b/codex-rs/core/src/tools/router_tests.rs index 62391395f826..2bdc008476f0 100644 --- a/codex-rs/core/src/tools/router_tests.rs +++ b/codex-rs/core/src/tools/router_tests.rs @@ -361,6 +361,7 @@ fn mcp_tool_info( "type": "object", }))), ), + openai_file_input_optional_fields: Default::default(), connector_id: None, connector_name: None, plugin_display_names: Vec::new(), diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index d95cb5ff4491..085187a16942 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -381,6 +381,7 @@ fn mcp_tool(server: &str, namespace: &str, name: &str) -> ToolInfo { "additionalProperties": false, }))), ), + openai_file_input_optional_fields: Default::default(), connector_id: None, connector_name: None, plugin_display_names: Vec::new(), diff --git a/codex-rs/core/tests/common/apps_test_server.rs b/codex-rs/core/tests/common/apps_test_server.rs index 3daaada6c501..f034e1fcd1ee 100644 --- a/codex-rs/core/tests/common/apps_test_server.rs +++ b/codex-rs/core/tests/common/apps_test_server.rs @@ -553,9 +553,11 @@ impl Respond for CodexAppsJsonRpcResponder { "type": "object", "description": "Document file payload.", "properties": { + "download_url": { "type": "string" }, "file_id": { "type": "string" } }, - "required": ["file_id"] + "required": ["download_url", "file_id"], + "additionalProperties": false } }, "required": ["file"], diff --git a/codex-rs/core/tests/suite/openai_file_mcp.rs b/codex-rs/core/tests/suite/openai_file_mcp.rs index c6d7a1d43782..a33fd0f575e4 100644 --- a/codex-rs/core/tests/suite/openai_file_mcp.rs +++ b/codex-rs/core/tests/suite/openai_file_mcp.rs @@ -91,14 +91,10 @@ fn read_post_tool_use_hook_inputs(home: &Path) -> Result> { .collect() } -fn uploaded_file(server: &MockServer, file_size_bytes: u64) -> Value { +fn uploaded_file(server: &MockServer) -> Value { json!({ "download_url": format!("{}/download/file_123", server.uri()), "file_id": "file_123", - "mime_type": "text/plain", - "file_name": "report.txt", - "uri": "sediment://file_123", - "file_size_bytes": file_size_bytes, }) } @@ -230,7 +226,7 @@ async fn codex_apps_file_params_upload_environment_files_before_mcp_tool_call() assert_eq!( apps_tool_call.pointer("/params/arguments/file"), - Some(&uploaded_file(&server, STREAMED_FILE_SIZE as u64)) + Some(&uploaded_file(&server)) ); assert_eq!( apps_tool_call.pointer("/params/_meta/_codex_apps"), @@ -267,10 +263,7 @@ async fn codex_apps_file_params_pass_uploaded_file_to_post_tool_use_hook() -> Re let hook_inputs = read_post_tool_use_hook_inputs(test.codex_home_path())?; assert_eq!(hook_inputs.len(), 1); - assert_eq!( - hook_inputs[0]["tool_input"]["file"], - uploaded_file(&server, /*file_size_bytes*/ 11) - ); + assert_eq!(hook_inputs[0]["tool_input"]["file"], uploaded_file(&server)); server.verify().await; Ok(())