From be7f751567415a0cd0dd25e7e63bf6f7ccba2a33 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 7 Jul 2026 15:37:58 -0700 Subject: [PATCH] codex-api: route file uploads through HTTP client factory --- codex-rs/codex-api/src/files.rs | 107 +++++++++++++++++++-------- codex-rs/core/src/mcp_openai_file.rs | 2 + 2 files changed, 78 insertions(+), 31 deletions(-) diff --git a/codex-rs/codex-api/src/files.rs b/codex-rs/codex-api/src/files.rs index 3cd3932d0681..c56855588537 100644 --- a/codex-rs/codex-api/src/files.rs +++ b/codex-rs/codex-api/src/files.rs @@ -2,7 +2,10 @@ use std::time::Duration; use crate::AuthProvider; use bytes::Bytes; -use codex_http_client::build_reqwest_client_with_custom_ca; +use codex_http_client::BuildRouteAwareHttpClientError; +use codex_http_client::ClientRouteClass; +use codex_http_client::HttpClientFactory; +use codex_http_client::OutboundProxyPolicy; use futures::Stream; use reqwest::StatusCode; use reqwest::header::CONTENT_LENGTH; @@ -55,6 +58,12 @@ pub enum OpenAiFileError { #[source] source: serde_json::Error, }, + #[error("failed to build OpenAI file client for {url}: {source}")] + ClientBuild { + url: String, + #[source] + source: BuildRouteAwareHttpClientError, + }, #[error("OpenAI file upload for `{file_id}` is not ready yet")] UploadNotReady { file_id: String }, #[error("OpenAI file upload for `{file_id}` failed: {message}")] @@ -84,6 +93,7 @@ pub fn openai_file_uri(file_id: &str) -> String { pub async fn upload_openai_file( base_url: &str, auth: &dyn AuthProvider, + http_client_factory: &HttpClientFactory, file_name: String, file_size_bytes: u64, contents: impl Stream> + Send + 'static, @@ -97,18 +107,23 @@ pub async fn upload_openai_file( } let create_url = format!("{}/files", base_url.trim_end_matches('/')); - let create_response = authorized_request(auth, reqwest::Method::POST, &create_url) - .json(&serde_json::json!({ - "file_name": file_name.as_str(), - "file_size": file_size_bytes, - "use_case": OPENAI_FILE_USE_CASE, - })) - .send() - .await - .map_err(|source| OpenAiFileError::Request { - url: create_url.clone(), - source, - })?; + let create_response = authorized_request( + http_client_factory, + auth, + reqwest::Method::POST, + &create_url, + )? + .json(&serde_json::json!({ + "file_name": file_name.as_str(), + "file_size": file_size_bytes, + "use_case": OPENAI_FILE_USE_CASE, + })) + .send() + .await + .map_err(|source| OpenAiFileError::Request { + url: create_url.clone(), + source, + })?; let create_status = create_response.status(); let create_body = create_response.text().await.unwrap_or_default(); if !create_status.is_success() { @@ -124,7 +139,7 @@ pub async fn upload_openai_file( source, })?; - let upload_response = build_reqwest_client() + let upload_response = build_reqwest_client(http_client_factory, &create_payload.upload_url)? .put(&create_payload.upload_url) .timeout(OPENAI_FILE_REQUEST_TIMEOUT) .header("x-ms-blob-type", "BlockBlob") @@ -153,14 +168,19 @@ pub async fn upload_openai_file( ); let finalize_started_at = Instant::now(); loop { - let finalize_response = authorized_request(auth, reqwest::Method::POST, &finalize_url) - .json(&serde_json::json!({})) - .send() - .await - .map_err(|source| OpenAiFileError::Request { - url: finalize_url.clone(), - source, - })?; + let finalize_response = authorized_request( + http_client_factory, + auth, + reqwest::Method::POST, + &finalize_url, + )? + .json(&serde_json::json!({})) + .send() + .await + .map_err(|source| OpenAiFileError::Request { + url: finalize_url.clone(), + source, + })?; let finalize_status = finalize_response.status(); let finalize_body = finalize_response.text().await.unwrap_or_default(); if !finalize_status.is_success() { @@ -213,25 +233,45 @@ pub async fn upload_openai_file( } fn authorized_request( + http_client_factory: &HttpClientFactory, auth: &dyn AuthProvider, method: reqwest::Method, url: &str, -) -> reqwest::RequestBuilder { +) -> Result { let mut headers = http::HeaderMap::new(); auth.add_auth_headers(&mut headers); - let client = build_reqwest_client(); - client + let client = build_reqwest_client(http_client_factory, url)?; + Ok(client .request(method, url) .timeout(OPENAI_FILE_REQUEST_TIMEOUT) - .headers(headers) + .headers(headers)) } -fn build_reqwest_client() -> reqwest::Client { - build_reqwest_client_with_custom_ca(reqwest::Client::builder()).unwrap_or_else(|error| { - tracing::warn!(error = %error, "failed to build OpenAI file upload client"); - reqwest::Client::new() - }) +fn build_reqwest_client( + http_client_factory: &HttpClientFactory, + url: &str, +) -> Result { + match http_client_factory.build_reqwest_client( + reqwest::Client::builder(), + url, + ClientRouteClass::Api, + ) { + Ok(client) => Ok(client), + Err(error) + if matches!( + http_client_factory.outbound_proxy_policy(), + OutboundProxyPolicy::ReqwestDefault + ) => + { + tracing::warn!(%error, "failed to build OpenAI file upload client"); + Ok(reqwest::Client::new()) + } + Err(source) => Err(OpenAiFileError::ClientBuild { + url: url.to_string(), + source, + }), + } } #[cfg(test)] @@ -254,6 +294,10 @@ mod tests { #[derive(Clone, Copy)] struct ChatGptTestAuth; + fn default_http_client_factory() -> HttpClientFactory { + HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault) + } + impl AuthProvider for ChatGptTestAuth { fn add_auth_headers(&self, headers: &mut reqwest::header::HeaderMap) { headers.insert( @@ -324,6 +368,7 @@ mod tests { let uploaded = upload_openai_file( &base_url, &chatgpt_auth(), + &default_http_client_factory(), "hello.txt".to_string(), /*file_size_bytes*/ 5, contents, diff --git a/codex-rs/core/src/mcp_openai_file.rs b/codex-rs/core/src/mcp_openai_file.rs index 5044568cd62a..462b27ffd8de 100644 --- a/codex-rs/core/src/mcp_openai_file.rs +++ b/codex-rs/core/src/mcp_openai_file.rs @@ -159,9 +159,11 @@ async fn build_uploaded_argument_value( .unwrap_or("file") .to_string(); let upload_auth = codex_model_provider::auth_provider_from_auth(auth); + let http_client_factory = turn_context.config.http_client_factory(); let uploaded = upload_openai_file( turn_context.config.chatgpt_base_url.trim_end_matches('/'), upload_auth.as_ref(), + &http_client_factory, file_name, metadata.size, contents,