From ed36facc9358e8646266492e380b94ddde76cfae Mon Sep 17 00:00:00 2001 From: Jacob Zhou Date: Mon, 6 Jul 2026 17:40:02 -0700 Subject: [PATCH 1/4] Make Codex app file payloads schema-aware --- .../codex-mcp/src/codex_apps_cache_tests.rs | 1 + codex-rs/codex-mcp/src/connection_manager.rs | 3 + .../codex-mcp/src/connection_manager_tests.rs | 62 ++++++++++++++++ codex-rs/codex-mcp/src/rmcp_client.rs | 6 ++ codex-rs/codex-mcp/src/tools.rs | 47 ++++++++++++ codex-rs/core/src/connectors_tests.rs | 3 + codex-rs/core/src/mcp_openai_file.rs | 71 +++++++++++-------- codex-rs/core/src/mcp_tool_call.rs | 13 ++-- codex-rs/core/src/mcp_tool_call_tests.rs | 11 ++- codex-rs/core/src/mcp_tool_exposure_test.rs | 1 + codex-rs/core/src/tools/handlers/mcp.rs | 1 + .../src/tools/handlers/mcp_search_tests.rs | 1 + .../core/src/tools/handlers/tool_search.rs | 1 + codex-rs/core/src/tools/router_tests.rs | 1 + codex-rs/core/src/tools/spec_plan_tests.rs | 1 + 15 files changed, 181 insertions(+), 42 deletions(-) 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..f99437bb7c54 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(), @@ -276,6 +279,65 @@ fn tool_with_model_visible_input_schema_masks_file_params() { ); } +#[test] +fn declared_openai_file_input_optional_fields_follows_payload_schema() { + let mut tool = Tool::new( + "upload".to_string(), + "Upload a file".to_string(), + Arc::new( + serde_json::json!({ + "type": "object", + "properties": { + "file": { + "type": "object", + "properties": { + "download_url": {"type": "string"}, + "file_id": {"type": "string"}, + "mime_type": {"type": "string"}, + "file_name": {"type": "string"}, + "uri": {"type": "string"}, + "file_size_bytes": {"type": "integer"} + } + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "download_url": {"type": "string"}, + "file_id": {"type": "string"}, + "mime_type": {"type": "string"} + } + } + } + } + }) + .as_object() + .expect("object") + .clone(), + ), + ); + tool.meta = Some(Meta( + serde_json::json!({ + "openai/fileParams": ["file", "files"] + }) + .as_object() + .expect("object") + .clone(), + )); + + assert_eq!( + declared_openai_file_input_optional_fields(&tool), + HashMap::from([ + ( + "file".to_string(), + vec!["mime_type".to_string(), "file_name".to_string()] + ), + ("files".to_string(), vec!["mime_type".to_string()]), + ]) + ); +} + #[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..87f3a199e325 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,48 @@ 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 OPTIONAL_OPENAI_FILE_FIELDS: [&str; 2] = ["mime_type", "file_name"]; + +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)) + .and_then(file_payload_schema) + .and_then(|schema| schema.get("properties")) + .and_then(JsonValue::as_object) + .map(|properties| { + OPTIONAL_OPENAI_FILE_FIELDS + .into_iter() + .filter(|field| properties.contains_key(*field)) + .map(str::to_string) + .collect() + }) + .unwrap_or_default(); + (field_name, optional_fields) + }) + .collect() +} + +fn file_payload_schema(schema: &JsonValue) -> Option<&serde_json::Map> { + let schema = schema.as_object()?; + if schema.get("type").and_then(JsonValue::as_str) == Some("array") + || schema.contains_key("items") + { + schema.get("items")?.as_object() + } else { + Some(schema) + } +} fn rewrite_input_schema_for_local_file_paths(input_schema: &mut JsonValue, file_params: &[String]) { let Some(properties) = input_schema 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(), From 8e0a76db5f5981f5fd202b5f6091694f830388c6 Mon Sep 17 00:00:00 2001 From: Jacob Zhou Date: Tue, 7 Jul 2026 09:33:03 -0700 Subject: [PATCH 2/4] Handle composed app file schemas --- .../codex-mcp/src/connection_manager_tests.rs | 76 +++++++++++++++++++ codex-rs/codex-mcp/src/tools.rs | 38 +++++++--- .../core/tests/common/apps_test_server.rs | 4 +- codex-rs/core/tests/suite/openai_file_mcp.rs | 13 +--- 4 files changed, 108 insertions(+), 23 deletions(-) diff --git a/codex-rs/codex-mcp/src/connection_manager_tests.rs b/codex-rs/codex-mcp/src/connection_manager_tests.rs index f99437bb7c54..c9f7a4c90fb2 100644 --- a/codex-rs/codex-mcp/src/connection_manager_tests.rs +++ b/codex-rs/codex-mcp/src/connection_manager_tests.rs @@ -338,6 +338,82 @@ fn declared_openai_file_input_optional_fields_follows_payload_schema() { ); } +#[test] +fn declared_openai_file_input_optional_fields_supports_composed_schemas() { + let mut tool = Tool::new( + "upload".to_string(), + "Upload files".to_string(), + Arc::new( + serde_json::json!({ + "type": "object", + "properties": { + "nullable_file": { + "anyOf": [ + { + "type": "object", + "properties": { + "download_url": {"type": "string"}, + "file_id": {"type": "string"}, + "file_name": {"type": "string"} + } + }, + {"type": "null"} + ] + }, + "files": { + "type": "array", + "items": { + "oneOf": [ + { + "allOf": [ + { + "type": "object", + "properties": { + "download_url": {"type": "string"}, + "file_id": {"type": "string"}, + "mime_type": {"type": "string"} + } + }, + { + "type": "object", + "properties": { + "file_name": {"type": "string"} + } + } + ] + }, + {"type": "null"} + ] + } + } + } + }) + .as_object() + .expect("object") + .clone(), + ), + ); + tool.meta = Some(Meta( + serde_json::json!({ + "openai/fileParams": ["nullable_file", "files"] + }) + .as_object() + .expect("object") + .clone(), + )); + + assert_eq!( + declared_openai_file_input_optional_fields(&tool), + HashMap::from([ + ("nullable_file".to_string(), vec!["file_name".to_string()]), + ( + "files".to_string(), + vec!["mime_type".to_string(), "file_name".to_string()] + ), + ]) + ); +} + #[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/tools.rs b/codex-rs/codex-mcp/src/tools.rs index 87f3a199e325..36775bedf49d 100644 --- a/codex-rs/codex-mcp/src/tools.rs +++ b/codex-rs/codex-mcp/src/tools.rs @@ -282,13 +282,10 @@ pub(crate) fn declared_openai_file_input_optional_fields( .map(|field_name| { let optional_fields = properties .and_then(|properties| properties.get(&field_name)) - .and_then(file_payload_schema) - .and_then(|schema| schema.get("properties")) - .and_then(JsonValue::as_object) - .map(|properties| { + .map(|schema| { OPTIONAL_OPENAI_FILE_FIELDS .into_iter() - .filter(|field| properties.contains_key(*field)) + .filter(|field| file_payload_schema_declares_property(schema, field)) .map(str::to_string) .collect() }) @@ -298,15 +295,32 @@ pub(crate) fn declared_openai_file_input_optional_fields( .collect() } -fn file_payload_schema(schema: &JsonValue) -> Option<&serde_json::Map> { - let schema = schema.as_object()?; - if schema.get("type").and_then(JsonValue::as_str) == Some("array") - || schema.contains_key("items") +fn file_payload_schema_declares_property(schema: &JsonValue, property_name: &str) -> bool { + let Some(schema) = schema.as_object() else { + return false; + }; + + if schema + .get("properties") + .and_then(JsonValue::as_object) + .is_some_and(|properties| properties.contains_key(property_name)) { - schema.get("items")?.as_object() - } else { - Some(schema) + return true; } + + schema + .get("items") + .is_some_and(|items| file_payload_schema_declares_property(items, property_name)) + || ["anyOf", "oneOf", "allOf"].into_iter().any(|keyword| { + schema + .get(keyword) + .and_then(JsonValue::as_array) + .is_some_and(|variants| { + variants.iter().any(|variant| { + file_payload_schema_declares_property(variant, property_name) + }) + }) + }) } fn rewrite_input_schema_for_local_file_paths(input_schema: &mut JsonValue, file_params: &[String]) { 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(()) From 00a35b1bf7756fef83d5100d40804d2a9c4e6a04 Mon Sep 17 00:00:00 2001 From: Jacob Zhou Date: Tue, 7 Jul 2026 10:22:07 -0700 Subject: [PATCH 3/4] Resolve referenced app file schemas --- .../codex-mcp/src/connection_manager_tests.rs | 76 +++++++++++++++++ codex-rs/codex-mcp/src/tools.rs | 83 +++++++++++++++---- 2 files changed, 145 insertions(+), 14 deletions(-) diff --git a/codex-rs/codex-mcp/src/connection_manager_tests.rs b/codex-rs/codex-mcp/src/connection_manager_tests.rs index c9f7a4c90fb2..02e7cf7bc65a 100644 --- a/codex-rs/codex-mcp/src/connection_manager_tests.rs +++ b/codex-rs/codex-mcp/src/connection_manager_tests.rs @@ -414,6 +414,82 @@ fn declared_openai_file_input_optional_fields_supports_composed_schemas() { ); } +#[test] +fn declared_openai_file_input_optional_fields_supports_local_schema_refs() { + let mut tool = Tool::new( + "upload".to_string(), + "Upload files".to_string(), + Arc::new( + serde_json::json!({ + "type": "object", + "$defs": { + "Provided/File": { + "type": "object", + "properties": { + "download_url": {"type": "string"}, + "file_id": {"type": "string"}, + "mime_type": {"type": "string"} + } + }, + "ProvidedFileAlias": { + "$ref": "#/$defs/Provided~1File" + }, + "RecursiveFile": { + "$ref": "#/$defs/RecursiveFile" + } + }, + "definitions": { + "NamedFile": { + "type": "object", + "properties": { + "download_url": {"type": "string"}, + "file_id": {"type": "string"}, + "file_name": {"type": "string"} + } + } + }, + "properties": { + "file": { + "anyOf": [ + {"$ref": "#/$defs/ProvidedFileAlias"}, + {"type": "null"} + ] + }, + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/NamedFile" + } + }, + "recursive_file": { + "$ref": "#/$defs/RecursiveFile" + } + } + }) + .as_object() + .expect("object") + .clone(), + ), + ); + tool.meta = Some(Meta( + serde_json::json!({ + "openai/fileParams": ["file", "files", "recursive_file"] + }) + .as_object() + .expect("object") + .clone(), + )); + + assert_eq!( + declared_openai_file_input_optional_fields(&tool), + HashMap::from([ + ("file".to_string(), vec!["mime_type".to_string()]), + ("files".to_string(), vec!["file_name".to_string()]), + ("recursive_file".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/tools.rs b/codex-rs/codex-mcp/src/tools.rs index 36775bedf49d..df282fa87a06 100644 --- a/codex-rs/codex-mcp/src/tools.rs +++ b/codex-rs/codex-mcp/src/tools.rs @@ -285,7 +285,14 @@ pub(crate) fn declared_openai_file_input_optional_fields( .map(|schema| { OPTIONAL_OPENAI_FILE_FIELDS .into_iter() - .filter(|field| file_payload_schema_declares_property(schema, field)) + .filter(|field| { + file_payload_schema_declares_property( + schema, + tool.input_schema.as_ref(), + field, + &mut HashSet::new(), + ) + }) .map(str::to_string) .collect() }) @@ -295,11 +302,34 @@ pub(crate) fn declared_openai_file_input_optional_fields( .collect() } -fn file_payload_schema_declares_property(schema: &JsonValue, property_name: &str) -> bool { +fn file_payload_schema_declares_property( + schema: &JsonValue, + root_schema: &Map, + property_name: &str, + visited_refs: &mut HashSet, +) -> bool { let Some(schema) = schema.as_object() else { return false; }; + if let Some(schema_ref) = schema.get("$ref").and_then(JsonValue::as_str) + && visited_refs.insert(schema_ref.to_string()) + { + let declares_property = + resolve_local_schema_ref(root_schema, schema_ref).is_some_and(|referenced_schema| { + file_payload_schema_declares_property( + referenced_schema, + root_schema, + property_name, + visited_refs, + ) + }); + visited_refs.remove(schema_ref); + if declares_property { + return true; + } + } + if schema .get("properties") .and_then(JsonValue::as_object) @@ -308,19 +338,44 @@ fn file_payload_schema_declares_property(schema: &JsonValue, property_name: &str return true; } - schema - .get("items") - .is_some_and(|items| file_payload_schema_declares_property(items, property_name)) - || ["anyOf", "oneOf", "allOf"].into_iter().any(|keyword| { - schema - .get(keyword) - .and_then(JsonValue::as_array) - .is_some_and(|variants| { - variants.iter().any(|variant| { - file_payload_schema_declares_property(variant, property_name) - }) + schema.get("items").is_some_and(|items| { + file_payload_schema_declares_property(items, root_schema, property_name, visited_refs) + }) || ["anyOf", "oneOf", "allOf"].into_iter().any(|keyword| { + schema + .get(keyword) + .and_then(JsonValue::as_array) + .is_some_and(|variants| { + variants.iter().any(|variant| { + file_payload_schema_declares_property( + variant, + root_schema, + property_name, + visited_refs, + ) }) - }) + }) + }) +} + +fn resolve_local_schema_ref<'a>( + root_schema: &'a Map, + schema_ref: &str, +) -> Option<&'a JsonValue> { + let pointer = schema_ref.strip_prefix("#/")?; + let mut segments = pointer.split('/'); + let first_segment = segments.next()?.replace("~1", "/").replace("~0", "~"); + let mut referenced_schema = root_schema.get(&first_segment)?; + + for segment in segments { + let segment = segment.replace("~1", "/").replace("~0", "~"); + referenced_schema = match referenced_schema { + JsonValue::Object(object) => object.get(&segment)?, + JsonValue::Array(array) => array.get(segment.parse::().ok()?)?, + _ => return None, + }; + } + + Some(referenced_schema) } fn rewrite_input_schema_for_local_file_paths(input_schema: &mut JsonValue, file_params: &[String]) { From 31fe67800051b7f3bde9ee64e43357eb14eeaaee Mon Sep 17 00:00:00 2001 From: Jacob Zhou Date: Tue, 7 Jul 2026 10:58:53 -0700 Subject: [PATCH 4/4] Simplify app file schema inspection --- .../codex-mcp/src/connection_manager_tests.rs | 229 ++++++------------ codex-rs/codex-mcp/src/tools.rs | 133 ++++------ 2 files changed, 119 insertions(+), 243 deletions(-) diff --git a/codex-rs/codex-mcp/src/connection_manager_tests.rs b/codex-rs/codex-mcp/src/connection_manager_tests.rs index 02e7cf7bc65a..ae285f2f9c15 100644 --- a/codex-rs/codex-mcp/src/connection_manager_tests.rs +++ b/codex-rs/codex-mcp/src/connection_manager_tests.rs @@ -234,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"} + ] } } }) @@ -269,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." } } }) @@ -280,190 +297,69 @@ fn tool_with_model_visible_input_schema_masks_file_params() { } #[test] -fn declared_openai_file_input_optional_fields_follows_payload_schema() { +fn declared_openai_file_input_optional_fields_follow_canonical_payload_schemas() { let mut tool = Tool::new( "upload".to_string(), - "Upload a file".to_string(), + "Upload files".to_string(), Arc::new( serde_json::json!({ "type": "object", "properties": { - "file": { + "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"}, - "uri": {"type": "string"}, - "file_size_bytes": {"type": "integer"} + "file_name": {"type": "string"} } }, - "files": { - "type": "array", - "items": { - "type": "object", - "properties": { - "download_url": {"type": "string"}, - "file_id": {"type": "string"}, - "mime_type": {"type": "string"} - } - } - } - } - }) - .as_object() - .expect("object") - .clone(), - ), - ); - tool.meta = Some(Meta( - serde_json::json!({ - "openai/fileParams": ["file", "files"] - }) - .as_object() - .expect("object") - .clone(), - )); - - assert_eq!( - declared_openai_file_input_optional_fields(&tool), - HashMap::from([ - ( - "file".to_string(), - vec!["mime_type".to_string(), "file_name".to_string()] - ), - ("files".to_string(), vec!["mime_type".to_string()]), - ]) - ); -} - -#[test] -fn declared_openai_file_input_optional_fields_supports_composed_schemas() { - let mut tool = Tool::new( - "upload".to_string(), - "Upload files".to_string(), - Arc::new( - serde_json::json!({ - "type": "object", - "properties": { - "nullable_file": { + "attachments": { "anyOf": [ { - "type": "object", - "properties": { - "download_url": {"type": "string"}, - "file_id": {"type": "string"}, - "file_name": {"type": "string"} + "type": "array", + "items": { + "oneOf": [ + { + "allOf": [{ + "type": "object", + "properties": { + "download_url": {"type": "string"}, + "file_id": {"type": "string"}, + "mime_type": {"type": "string"} + } + }] + }, + {"type": "null"} + ] } }, {"type": "null"} ] }, - "files": { - "type": "array", - "items": { - "oneOf": [ - { - "allOf": [ - { - "type": "object", - "properties": { - "download_url": {"type": "string"}, - "file_id": {"type": "string"}, - "mime_type": {"type": "string"} - } - }, - { - "type": "object", - "properties": { - "file_name": {"type": "string"} - } - } - ] - }, - {"type": "null"} - ] - } - } - } - }) - .as_object() - .expect("object") - .clone(), - ), - ); - tool.meta = Some(Meta( - serde_json::json!({ - "openai/fileParams": ["nullable_file", "files"] - }) - .as_object() - .expect("object") - .clone(), - )); - - assert_eq!( - declared_openai_file_input_optional_fields(&tool), - HashMap::from([ - ("nullable_file".to_string(), vec!["file_name".to_string()]), - ( - "files".to_string(), - vec!["mime_type".to_string(), "file_name".to_string()] - ), - ]) - ); -} - -#[test] -fn declared_openai_file_input_optional_fields_supports_local_schema_refs() { - let mut tool = Tool::new( - "upload".to_string(), - "Upload files".to_string(), - Arc::new( - serde_json::json!({ - "type": "object", - "$defs": { - "Provided/File": { + "noncanonical_file": { "type": "object", "properties": { "download_url": {"type": "string"}, "file_id": {"type": "string"}, - "mime_type": {"type": "string"} + "mime_type": {"type": "string"}, + "uri": {"type": "string"} } }, - "ProvidedFileAlias": { - "$ref": "#/$defs/Provided~1File" - }, - "RecursiveFile": { - "$ref": "#/$defs/RecursiveFile" - } - }, - "definitions": { - "NamedFile": { + "unrelated_object": { "type": "object", "properties": { - "download_url": {"type": "string"}, - "file_id": {"type": "string"}, + "mime_type": {"type": "string"}, "file_name": {"type": "string"} } } - }, - "properties": { - "file": { - "anyOf": [ - {"$ref": "#/$defs/ProvidedFileAlias"}, - {"type": "null"} - ] - }, - "files": { - "type": "array", - "items": { - "$ref": "#/definitions/NamedFile" - } - }, - "recursive_file": { - "$ref": "#/$defs/RecursiveFile" - } } }) .as_object() @@ -473,7 +369,13 @@ fn declared_openai_file_input_optional_fields_supports_local_schema_refs() { ); tool.meta = Some(Meta( serde_json::json!({ - "openai/fileParams": ["file", "files", "recursive_file"] + "openai/fileParams": [ + "photoshop_image", + "drive_import", + "attachments", + "noncanonical_file", + "unrelated_object" + ] }) .as_object() .expect("object") @@ -483,9 +385,14 @@ fn declared_openai_file_input_optional_fields_supports_local_schema_refs() { assert_eq!( declared_openai_file_input_optional_fields(&tool), HashMap::from([ - ("file".to_string(), vec!["mime_type".to_string()]), - ("files".to_string(), vec!["file_name".to_string()]), - ("recursive_file".to_string(), Vec::new()), + ("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()), ]) ); } diff --git a/codex-rs/codex-mcp/src/tools.rs b/codex-rs/codex-mcp/src/tools.rs index df282fa87a06..f1d38e20152d 100644 --- a/codex-rs/codex-mcp/src/tools.rs +++ b/codex-rs/codex-mcp/src/tools.rs @@ -266,7 +266,14 @@ 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 OPTIONAL_OPENAI_FILE_FIELDS: [&str; 2] = ["mime_type", "file_name"]; +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, @@ -283,18 +290,15 @@ pub(crate) fn declared_openai_file_input_optional_fields( let optional_fields = properties .and_then(|properties| properties.get(&field_name)) .map(|schema| { - OPTIONAL_OPENAI_FILE_FIELDS - .into_iter() - .filter(|field| { - file_payload_schema_declares_property( - schema, - tool.input_schema.as_ref(), - field, - &mut HashSet::new(), - ) - }) - .map(str::to_string) - .collect() + 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) @@ -302,80 +306,46 @@ pub(crate) fn declared_openai_file_input_optional_fields( .collect() } -fn file_payload_schema_declares_property( - schema: &JsonValue, - root_schema: &Map, - property_name: &str, - visited_refs: &mut HashSet, -) -> bool { - let Some(schema) = schema.as_object() else { - return false; - }; +fn openai_file_schema_info(schema: &JsonValue) -> OpenAiFileSchemaInfo { + let mut info = OpenAiFileSchemaInfo::default(); + let mut pending = vec![(schema, false)]; - if let Some(schema_ref) = schema.get("$ref").and_then(JsonValue::as_str) - && visited_refs.insert(schema_ref.to_string()) - { - let declares_property = - resolve_local_schema_ref(root_schema, schema_ref).is_some_and(|referenced_schema| { - file_payload_schema_declares_property( - referenced_schema, - root_schema, - property_name, - visited_refs, - ) - }); - visited_refs.remove(schema_ref); - if declares_property { - return true; - } - } + while let Some((schema, inside_array)) = pending.pop() { + let Some(schema) = schema.as_object() else { + continue; + }; - if schema - .get("properties") - .and_then(JsonValue::as_object) - .is_some_and(|properties| properties.contains_key(property_name)) - { - return true; - } + 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))); + } + } - schema.get("items").is_some_and(|items| { - file_payload_schema_declares_property(items, root_schema, property_name, visited_refs) - }) || ["anyOf", "oneOf", "allOf"].into_iter().any(|keyword| { - schema - .get(keyword) - .and_then(JsonValue::as_array) - .is_some_and(|variants| { - variants.iter().any(|variant| { - file_payload_schema_declares_property( - variant, - root_schema, - property_name, - visited_refs, - ) - }) - }) - }) -} + if schema.get("type").and_then(JsonValue::as_str) == Some("array") { + if let Some(items) = schema.get("items") { + pending.push((items, true)); + } + continue; + } -fn resolve_local_schema_ref<'a>( - root_schema: &'a Map, - schema_ref: &str, -) -> Option<&'a JsonValue> { - let pointer = schema_ref.strip_prefix("#/")?; - let mut segments = pointer.split('/'); - let first_segment = segments.next()?.replace("~1", "/").replace("~0", "~"); - let mut referenced_schema = root_schema.get(&first_segment)?; - - for segment in segments { - let segment = segment.replace("~1", "/").replace("~0", "~"); - referenced_schema = match referenced_schema { - JsonValue::Object(object) => object.get(&segment)?, - JsonValue::Array(array) => array.get(segment.parse::().ok()?)?, - _ => return None, + 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"); } - Some(referenced_schema) + info } fn rewrite_input_schema_for_local_file_paths(input_schema: &mut JsonValue, file_params: &[String]) { @@ -396,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; }; @@ -412,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 {