From 1ce4c6064cc8fe322a9b2ed4d293dd6a28b3216d Mon Sep 17 00:00:00 2001 From: Liang-Ting Jiang Date: Wed, 22 Apr 2026 01:00:23 -0700 Subject: [PATCH] clean --- codex-rs/codex-api/src/files.rs | 33 +++-- codex-rs/config/src/config_toml.rs | 2 + codex-rs/config/src/profile_toml.rs | 1 + codex-rs/core/config.schema.json | 12 +- codex-rs/core/src/codex_apps_file_download.rs | 107 ++++++--------- codex-rs/core/src/config/config_tests.rs | 4 + codex-rs/core/src/config/mod.rs | 11 ++ codex-rs/core/src/mcp_openai_file.rs | 122 ++++-------------- codex-rs/core/src/mcp_tool_call_tests.rs | 36 ------ codex-rs/core/src/mcp_tool_exposure_test.rs | 30 ----- codex-rs/core/tests/suite/openai_file_mcp.rs | 24 ++-- 11 files changed, 133 insertions(+), 249 deletions(-) diff --git a/codex-rs/codex-api/src/files.rs b/codex-rs/codex-api/src/files.rs index 11bdb9ed76..c3aa0ce450 100644 --- a/codex-rs/codex-api/src/files.rs +++ b/codex-rs/codex-api/src/files.rs @@ -37,7 +37,7 @@ impl Default for OpenAiFileUploadOptions { pub struct UploadedOpenAiFile { pub file_id: String, pub uri: String, - pub download_url: String, + pub download_url: Option, pub file_name: String, pub file_size_bytes: u64, pub mime_type: Option, @@ -114,6 +114,10 @@ pub fn openai_file_uri(file_id: &str) -> String { format!("{OPENAI_FILE_URI_PREFIX}{file_id}") } +fn openai_file_api_base_url(base_url: &str) -> String { + base_url.trim_end_matches('/').to_string() +} + pub async fn download_openai_file( base_url: &str, auth: &impl AuthProvider, @@ -188,7 +192,8 @@ pub async fn upload_local_file( .and_then(|value| value.to_str()) .unwrap_or("file") .to_string(); - let create_url = format!("{}/files", base_url.trim_end_matches('/')); + let api_base_url = openai_file_api_base_url(base_url); + let create_url = format!("{api_base_url}/files"); let mut create_request = serde_json::json!({ "file_name": file_name, "file_size": metadata.len(), @@ -248,11 +253,19 @@ pub async fn upload_local_file( }); } - let finalize_url = format!( - "{}/files/{}/uploaded", - base_url.trim_end_matches('/'), - create_payload.file_id, - ); + if options.store_in_library { + return Ok(UploadedOpenAiFile { + file_id: create_payload.file_id.clone(), + uri: openai_file_uri(&create_payload.file_id), + download_url: None, + file_name, + file_size_bytes: metadata.len(), + mime_type: None, + path: path.to_path_buf(), + }); + } + + let finalize_url = format!("{api_base_url}/files/{}/uploaded", create_payload.file_id); let finalize_started_at = Instant::now(); loop { let finalize_response = authorized_request(auth, reqwest::Method::POST, &finalize_url) @@ -283,12 +296,12 @@ pub async fn upload_local_file( return Ok(UploadedOpenAiFile { file_id: create_payload.file_id.clone(), uri: openai_file_uri(&create_payload.file_id), - download_url: finalize_payload.download_url.ok_or_else(|| { + download_url: Some(finalize_payload.download_url.ok_or_else(|| { OpenAiFileError::UploadFailed { file_id: create_payload.file_id.clone(), message: "missing download_url".to_string(), } - })?, + })?), file_name: finalize_payload.file_name.unwrap_or(file_name), file_size_bytes: metadata.len(), mime_type: finalize_payload.mime_type, @@ -506,7 +519,7 @@ mod tests { assert_eq!(uploaded.uri, "sediment://file_123"); assert_eq!( uploaded.download_url, - format!("{}/download/file_123", server.uri()) + Some(format!("{}/download/file_123", server.uri())) ); assert_eq!(uploaded.file_name, "hello.txt"); assert_eq!(uploaded.mime_type, Some("text/plain".to_string())); diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index 0c8e14c16d..d5d74c04ad 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -272,6 +272,8 @@ pub struct ConfigToml { /// Base URL for requests to ChatGPT (as opposed to the OpenAI API). pub chatgpt_base_url: Option, + /// Optional override for the OpenAI file upload/download API base. + pub openai_file_api_base_url: Option, /// Base URL override for the built-in `openai` model provider. pub openai_base_url: Option, diff --git a/codex-rs/config/src/profile_toml.rs b/codex-rs/config/src/profile_toml.rs index 69215c0440..fede906466 100644 --- a/codex-rs/config/src/profile_toml.rs +++ b/codex-rs/config/src/profile_toml.rs @@ -38,6 +38,7 @@ pub struct ConfigProfile { pub model_catalog_json: Option, pub personality: Option, pub chatgpt_base_url: Option, + pub openai_file_api_base_url: Option, /// Optional path to a file containing model instructions. pub model_instructions_file: Option, pub js_repl_node_path: Option, diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index a5d6fbdf85..683bade6c4 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -318,6 +318,9 @@ "chatgpt_base_url": { "type": "string" }, + "openai_file_api_base_url": { + "type": "string" + }, "experimental_compact_prompt_file": { "$ref": "#/definitions/AbsolutePathBuf" }, @@ -2263,6 +2266,13 @@ "description": "Base URL for requests to ChatGPT (as opposed to the OpenAI API).", "type": "string" }, + "openai_file_api_base_url": { + "description": "Optional override for the OpenAI file upload/download API base.", + "type": [ + "string", + "null" + ] + }, "check_for_update_on_startup": { "description": "When `true`, checks for Codex updates on startup and surfaces update prompts. Set to `false` only if your Codex updates are centrally managed. Defaults to `true`.", "type": "boolean" @@ -2982,4 +2992,4 @@ }, "title": "ConfigToml", "type": "object" -} \ No newline at end of file +} diff --git a/codex-rs/core/src/codex_apps_file_download.rs b/codex-rs/core/src/codex_apps_file_download.rs index 6a3eb2e874..34a62a9f4f 100644 --- a/codex-rs/core/src/codex_apps_file_download.rs +++ b/codex-rs/core/src/codex_apps_file_download.rs @@ -1,6 +1,6 @@ +use crate::codex_apps_mcp_tools::should_materialize_codex_apps_file_download; use crate::session::session::Session; use crate::session::turn_context::TurnContext; -use crate::codex_apps_mcp_tools::should_materialize_codex_apps_file_download; use codex_api::download_openai_file; use codex_login::CodexAuth; use codex_model_provider::AuthorizationHeaderAuthProvider; @@ -29,6 +29,14 @@ struct CodexAppsFileUri { file_name: Option, } +fn codex_apps_download_base_url<'a>(turn_context: &'a TurnContext, download_url: &str) -> &'a str { + if download_url.starts_with("/api/codex/") { + turn_context.config.chatgpt_base_url.as_str() + } else { + turn_context.config.openai_file_api_base_url() + } +} + pub(crate) async fn maybe_materialize_codex_apps_file_download_result( sess: &Session, turn_context: &TurnContext, @@ -37,7 +45,8 @@ pub(crate) async fn maybe_materialize_codex_apps_file_download_result( result: CallToolResult, ) -> CallToolResult { let auth = sess.services.auth_manager.auth().await; - let authorization_header_value = match sess.authorization_header_for_current_agent_task().await { + let authorization_header_value = match sess.authorization_header_for_current_agent_task().await + { Ok(value) => value, Err(error) => { warn!(error = %error, "failed to build agent assertion authorization for codex_apps file download materialization"); @@ -74,6 +83,8 @@ async fn maybe_materialize_codex_apps_file_download_result_with_auth( let Some(payload) = extract_codex_apps_file_download_payload(&result) else { return result; }; + let download_base_url = + codex_apps_download_base_url(turn_context, &payload.file_uri.download_url); if result.structured_content.is_none() && let Ok(structured_content) = serde_json::to_value(&payload) { @@ -95,7 +106,7 @@ async fn maybe_materialize_codex_apps_file_download_result_with_auth( auth_provider = auth_provider.with_fedramp_routing_header(); } download_openai_file( - turn_context.config.chatgpt_base_url.trim_end_matches('/'), + download_base_url, &auth_provider, &payload.file_uri.download_url, ) @@ -114,7 +125,7 @@ async fn maybe_materialize_codex_apps_file_download_result_with_auth( is_fedramp_account: auth.is_fedramp_account(), }; download_openai_file( - turn_context.config.chatgpt_base_url.trim_end_matches('/'), + download_base_url, &auth_provider, &payload.file_uri.download_url, ) @@ -267,35 +278,11 @@ mod tests { .expect("_codex_apps metadata object") } - #[tokio::test] - async fn codex_apps_file_download_materialization_ignores_results_without_metadata_flag() { - let (_, turn_context) = make_session_and_context().await; - let original = CallToolResult { - content: vec![serde_json::json!({"type": "text", "text": "hello"})], - structured_content: Some(serde_json::json!({"x": 1})), - is_error: Some(false), - meta: None, - }; - - let result = maybe_materialize_codex_apps_file_download_result_with_auth( - &turn_context, - "session-1", - Some(&CodexAuth::create_dummy_chatgpt_auth_for_testing()), - None, - "custom_server", - /*codex_apps_meta*/ None, - original.clone(), - ) - .await; - - assert_eq!(result, original); - } - #[tokio::test] async fn codex_apps_file_download_materialization_adds_local_path_for_marked_tools() { let server = MockServer::start().await; Mock::given(method("GET")) - .and(path("/api/codex/files/file_123/content")) + .and(path("/download/file_123")) .and(header("authorization", "Bearer Access Token")) .and(header("chatgpt-account-id", "account_id")) .respond_with( @@ -319,7 +306,7 @@ mod tests { "file_id": "file_123", "file_name": "testing-file.txt", "file_uri": { - "download_url": "/api/codex/files/file_123/content", + "download_url": format!("{}/download/file_123", server.uri()), "file_id": "file_123", "file_name": "testing-file.txt", "mime_type": "text/plain", @@ -361,9 +348,9 @@ mod tests { } #[tokio::test] - async fn codex_apps_file_download_materialization_uses_json_text_when_structured_content_is_missing() - { - let server = MockServer::start().await; + async fn codex_apps_file_download_materialization_uses_chatgpt_base_for_relative_codex_urls() { + let chatgpt_server = MockServer::start().await; + let file_api_server = MockServer::start().await; Mock::given(method("GET")) .and(path("/api/codex/files/file_123/content")) .and(header("authorization", "Bearer Access Token")) @@ -371,29 +358,28 @@ mod tests { .respond_with( ResponseTemplate::new(200) .insert_header("content-type", "text/plain") - .set_body_bytes(b"downloaded contents".to_vec()), + .set_body_bytes(b"downloaded via codex backend".to_vec()), ) - .mount(&server) + .mount(&chatgpt_server) .await; let (_, mut turn_context) = make_session_and_context().await; let mut config = (*turn_context.config).clone(); - config.chatgpt_base_url = format!("{}/backend-api/codex", server.uri()); + config.chatgpt_base_url = chatgpt_server.uri(); + config.openai_file_api_base_url = Some(format!("{}/backend-api", file_api_server.uri())); turn_context.config = Arc::new(config); let original = CallToolResult { - content: vec![serde_json::json!({ - "type": "text", - "text": serde_json::json!({ + content: vec![], + structured_content: Some(serde_json::json!({ + "file_id": "file_123", + "file_name": "testing-file.txt", + "file_uri": { + "download_url": "/api/codex/files/file_123/content", "file_id": "file_123", "file_name": "testing-file.txt", - "file_uri": { - "download_url": "/api/codex/files/file_123/content", - "file_name": "testing-file.txt", - } - }) - .to_string(), - })], - structured_content: None, + "mime_type": "text/plain", + } + })), is_error: Some(false), meta: None, }; @@ -410,29 +396,14 @@ mod tests { .await; let local_path = result - .content - .iter() - .find_map(|item| { - item.get("text") - .and_then(|text| text.as_str()) - .and_then(|text| text.strip_prefix("Downloaded file to local path: ")) - }) - .expect("expected local path announcement"); - assert_eq!( - result.structured_content, - Some(serde_json::json!({ - "file_id": "file_123", - "file_name": "testing-file.txt", - "file_uri": { - "download_url": "/api/codex/files/file_123/content", - "file_name": "testing-file.txt", - }, - "local_path": local_path, - })) - ); + .structured_content + .as_ref() + .and_then(|value| value.get("local_path")) + .and_then(JsonValue::as_str) + .expect("local_path in structured content"); assert_eq!( tokio::fs::read(local_path).await.expect("downloaded file"), - b"downloaded contents" + b"downloaded via codex backend" ); } } diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 966691f246..f827b4e154 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -5005,6 +5005,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { model_verbosity: None, personality: Some(Personality::Pragmatic), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), + openai_file_api_base_url: None, realtime_audio: RealtimeAudioConfig::default(), experimental_realtime_start_instructions: None, experimental_realtime_ws_base_url: None, @@ -5157,6 +5158,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { model_verbosity: None, personality: Some(Personality::Pragmatic), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), + openai_file_api_base_url: None, realtime_audio: RealtimeAudioConfig::default(), experimental_realtime_start_instructions: None, experimental_realtime_ws_base_url: None, @@ -5307,6 +5309,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { model_verbosity: None, personality: Some(Personality::Pragmatic), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), + openai_file_api_base_url: None, realtime_audio: RealtimeAudioConfig::default(), experimental_realtime_start_instructions: None, experimental_realtime_ws_base_url: None, @@ -5442,6 +5445,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { model_verbosity: Some(Verbosity::High), personality: Some(Personality::Pragmatic), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), + openai_file_api_base_url: None, realtime_audio: RealtimeAudioConfig::default(), experimental_realtime_start_instructions: None, experimental_realtime_ws_base_url: None, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 185dc9f938..9419f748a0 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -512,6 +512,8 @@ pub struct Config { /// Base URL for requests to ChatGPT (as opposed to the OpenAI API). pub chatgpt_base_url: String, + /// Optional override for the OpenAI file upload/download API base. + pub openai_file_api_base_url: Option, /// Machine-local realtime audio device preferences used by realtime voice. pub realtime_audio: RealtimeAudioConfig, @@ -798,6 +800,12 @@ impl ConfigBuilder { } impl Config { + pub fn openai_file_api_base_url(&self) -> &str { + self.openai_file_api_base_url + .as_deref() + .unwrap_or(self.chatgpt_base_url.as_str()) + } + pub fn to_models_manager_config(&self) -> ModelsManagerConfig { ModelsManagerConfig { model_context_window: self.model_context_window, @@ -2309,6 +2317,9 @@ impl Config { .chatgpt_base_url .or(cfg.chatgpt_base_url) .unwrap_or("https://chatgpt.com/backend-api/".to_string()), + openai_file_api_base_url: config_profile + .openai_file_api_base_url + .or(cfg.openai_file_api_base_url), realtime_audio: cfg .audio .map_or_else(RealtimeAudioConfig::default, |audio| RealtimeAudioConfig { diff --git a/codex-rs/core/src/mcp_openai_file.rs b/codex-rs/core/src/mcp_openai_file.rs index 45e8d7b7b2..2d9add4dc9 100644 --- a/codex-rs/core/src/mcp_openai_file.rs +++ b/codex-rs/core/src/mcp_openai_file.rs @@ -141,8 +141,16 @@ async fn build_uploaded_local_argument_value( }; let default_upload_options = OpenAiFileUploadOptions::default(); let uploaded = upload_local_file( +<<<<<<< HEAD turn_context.config.chatgpt_base_url.trim_end_matches('/'), &upload_auth, +======= + turn_context + .config + .openai_file_api_base_url() + .trim_end_matches('/'), + upload_auth.as_ref(), +>>>>>>> e66c01c9f (clean) &resolved_path, upload_options.unwrap_or(&default_upload_options), ) @@ -153,14 +161,25 @@ async fn build_uploaded_local_argument_value( } None => format!("failed to upload `{file_path}` for `{field_name}`: {error}"), })?; - 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 uploaded_value = serde_json::Map::from_iter([ + ("file_id".to_string(), serde_json::json!(uploaded.file_id)), + ( + "file_name".to_string(), + serde_json::json!(uploaded.file_name), + ), + ("uri".to_string(), serde_json::json!(uploaded.uri)), + ( + "file_size_bytes".to_string(), + serde_json::json!(uploaded.file_size_bytes), + ), + ]); + if let Some(download_url) = uploaded.download_url { + uploaded_value.insert("download_url".to_string(), serde_json::json!(download_url)); + } + if let Some(mime_type) = uploaded.mime_type { + uploaded_value.insert("mime_type".to_string(), serde_json::json!(mime_type)); + } + Ok(JsonValue::Object(uploaded_value)) } #[cfg(test)] @@ -274,93 +293,6 @@ mod tests { ); } - #[tokio::test] - async fn build_uploaded_local_argument_value_honors_upload_options() { - use wiremock::Mock; - use wiremock::MockServer; - use wiremock::ResponseTemplate; - use wiremock::matchers::body_json; - use wiremock::matchers::header; - use wiremock::matchers::method; - use wiremock::matchers::path; - - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path("/backend-api/files")) - .and(header("chatgpt-account-id", "account_id")) - .and(body_json(serde_json::json!({ - "file_name": "library.txt", - "file_size": 7, - "use_case": "codex", - "store_in_library": true, - }))) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "file_id": "file_library", - "upload_url": format!("{}/upload/file_library", server.uri()), - }))) - .expect(1) - .mount(&server) - .await; - Mock::given(method("PUT")) - .and(path("/upload/file_library")) - .respond_with(ResponseTemplate::new(200)) - .expect(1) - .mount(&server) - .await; - Mock::given(method("POST")) - .and(path("/backend-api/files/file_library/uploaded")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "status": "success", - "download_url": format!("{}/download/file_library", server.uri()), - "file_name": "library.txt", - "mime_type": "text/plain", - "file_size_bytes": 7, - }))) - .expect(1) - .mount(&server) - .await; - - let (session, mut turn_context) = make_session_and_context().await; - let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); - let dir = tempdir().expect("temp dir"); - let local_path = dir.path().join("library.txt"); - tokio::fs::write(&local_path, b"library") - .await - .expect("write local file"); - turn_context.cwd = AbsolutePathBuf::try_from(dir.path()).expect("absolute path"); - - let mut config = (*turn_context.config).clone(); - config.chatgpt_base_url = format!("{}/backend-api", server.uri()); - turn_context.config = Arc::new(config); - - let upload_options = OpenAiFileUploadOptions { - store_in_library: true, - }; - let rewritten = build_uploaded_local_argument_value( - &session, - &turn_context, - Some(&auth), - "file", - /*index*/ None, - "library.txt", - Some(&upload_options), - ) - .await - .expect("rewrite should upload the local file"); - - assert_eq!( - rewritten, - serde_json::json!({ - "download_url": format!("{}/download/file_library", server.uri()), - "file_id": "file_library", - "mime_type": "text/plain", - "file_name": "library.txt", - "uri": "sediment://file_library", - "file_size_bytes": 7, - }) - ); - } - #[tokio::test] async fn rewrite_argument_value_for_openai_files_rewrites_scalar_path() { use wiremock::Mock; diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index d1bcff2a31..8b8df553fd 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -67,16 +67,6 @@ fn approval_metadata( } } -fn direct_exposed_builtin_codex_apps_meta() -> serde_json::Map { - serde_json::json!({ - "provider": "builtin", - "direct_expose": true, - }) - .as_object() - .cloned() - .expect("_codex_apps metadata should be an object") -} - fn prompt_options( allow_session_remember: bool, allow_persistent_approval: bool, @@ -550,32 +540,6 @@ fn codex_apps_connectors_support_persistent_approval() { ); } -#[test] -fn direct_exposed_builtin_codex_apps_tools_do_not_support_persistent_approval() { - let invocation = McpInvocation { - server: CODEX_APPS_MCP_SERVER_NAME.to_string(), - tool: "builtin_search_file".to_string(), - arguments: None, - }; - let mut metadata = approval_metadata( - /*connector_id*/ None, - /*connector_name*/ None, - /*connector_description*/ None, - /*tool_title*/ Some("Builtin Search File"), - /*tool_description*/ Some("Search builtin files."), - ); - metadata.codex_apps_meta = Some(direct_exposed_builtin_codex_apps_meta()); - - assert_eq!( - session_mcp_tool_approval_key(&invocation, Some(&metadata), AppToolApproval::Auto), - None - ); - assert_eq!( - persistent_mcp_tool_approval_key(&invocation, Some(&metadata), AppToolApproval::Auto), - None - ); -} - #[test] fn sanitize_mcp_tool_result_for_model_rewrites_image_content() { let result = Ok(CallToolResult { diff --git a/codex-rs/core/src/mcp_tool_exposure_test.rs b/codex-rs/core/src/mcp_tool_exposure_test.rs index 9dcde1f175..c62f8bdd57 100644 --- a/codex-rs/core/src/mcp_tool_exposure_test.rs +++ b/codex-rs/core/src/mcp_tool_exposure_test.rs @@ -280,36 +280,6 @@ async fn always_defer_feature_preserves_explicit_apps() { assert!(!deferred_tools.contains_key("mcp__codex_apps__calendar_create_event")); } -#[tokio::test] -async fn directly_exposes_builtin_codex_apps_tools_marked_for_direct_exposure() { - let config = test_config().await; - let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await; - let mcp_tools = HashMap::from([( - "mcp__codex_apps__builtin_search_file".to_string(), - make_mcp_tool_with_meta( - CODEX_APPS_MCP_SERVER_NAME, - "builtin_search_file", - /*connector_id*/ None, - /*connector_name*/ None, - Some(direct_exposed_builtin_codex_apps_meta()), - ), - )]); - - let exposure = build_mcp_tool_exposure( - &mcp_tools, - /*connectors*/ None, - &[], - &config, - &tools_config, - ); - - assert_eq!( - exposure.direct_tools.into_keys().collect::>(), - vec!["mcp__codex_apps__builtin_search_file".to_string()] - ); - assert!(exposure.deferred_tools.is_none()); -} - #[tokio::test] async fn keeps_direct_exposed_builtin_codex_apps_tools_direct_in_large_search_sets() { let config = test_config().await; diff --git a/codex-rs/core/tests/suite/openai_file_mcp.rs b/codex-rs/core/tests/suite/openai_file_mcp.rs index 211aa67381..89528dde3a 100644 --- a/codex-rs/core/tests/suite/openai_file_mcp.rs +++ b/codex-rs/core/tests/suite/openai_file_mcp.rs @@ -39,10 +39,12 @@ fn configure_apps(config: &mut Config, chatgpt_base_url: &str) { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn codex_apps_file_params_upload_local_paths_before_mcp_tool_call() -> Result<()> { let server = start_mock_server().await; + let upload_server = start_mock_server().await; + let upload_server_uri = upload_server.uri(); let apps_server = AppsTestServer::mount(&server).await?; Mock::given(method("POST")) - .and(path("/files")) + .and(path("/api/files")) .and(header("chatgpt-account-id", "account_id")) .and(body_json(json!({ "file_name": "report.txt", @@ -51,32 +53,33 @@ async fn codex_apps_file_params_upload_local_paths_before_mcp_tool_call() -> Res }))) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "file_id": "file_123", - "upload_url": format!("{}/upload/file_123", server.uri()), + "upload_url": format!("{}/upload/file_123", upload_server_uri), }))) .expect(1) - .mount(&server) + .mount(&upload_server) .await; Mock::given(method("PUT")) .and(path("/upload/file_123")) .and(header("content-length", "11")) .respond_with(ResponseTemplate::new(200)) .expect(1) - .mount(&server) + .mount(&upload_server) .await; Mock::given(method("POST")) - .and(path("/files/file_123/uploaded")) + .and(path("/api/files/file_123/uploaded")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "status": "success", - "download_url": format!("{}/download/file_123", server.uri()), + "download_url": format!("{}/download/file_123", upload_server_uri), "file_name": "report.txt", "mime_type": "text/plain", "file_size_bytes": 11, }))) .expect(1) - .mount(&server) + .mount(&upload_server) .await; let call_id = "extract-call-1"; + let upload_server_api_base = format!("{}/api", upload_server_uri); let mock = mount_sse_sequence( &server, vec![ @@ -101,7 +104,10 @@ async fn codex_apps_file_params_upload_local_paths_before_mcp_tool_call() -> Res let mut builder = test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) - .with_config(move |config| configure_apps(config, apps_server.chatgpt_base_url.as_str())); + .with_config(move |config| { + configure_apps(config, apps_server.chatgpt_base_url.as_str()); + config.openai_file_api_base_url = Some(upload_server_api_base.clone()); + }); let test = builder.build(&server).await?; tokio::fs::write(test.cwd.path().join("report.txt"), b"hello world").await?; @@ -147,7 +153,7 @@ async fn codex_apps_file_params_upload_local_paths_before_mcp_tool_call() -> Res assert_eq!( apps_tool_call.pointer("/params/arguments/file"), Some(&json!({ - "download_url": format!("{}/download/file_123", server.uri()), + "download_url": format!("{}/download/file_123", upload_server_uri), "file_id": "file_123", "mime_type": "text/plain", "file_name": "report.txt",