From c636f821aebd07620173da0c2360788d3d43bb3b Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Wed, 3 Sep 2025 10:11:02 -0700 Subject: [PATCH] Add a common way to create HTTP client (#3110) Ensure User-Agent and originator are always sent. --- codex-rs/chatgpt/src/apply_command.rs | 2 +- codex-rs/chatgpt/src/chatgpt_client.rs | 7 +- codex-rs/chatgpt/src/chatgpt_token.rs | 7 +- codex-rs/cli/src/login.rs | 6 +- codex-rs/cli/src/proto.rs | 1 + codex-rs/core/src/auth.rs | 79 +++++++++----- codex-rs/core/src/client.rs | 10 +- codex-rs/core/src/default_client.rs | 106 +++++++++++++++++++ codex-rs/core/src/lib.rs | 2 +- codex-rs/core/src/user_agent.rs | 37 ------- codex-rs/core/tests/suite/client.rs | 30 +++--- codex-rs/exec/src/lib.rs | 1 + codex-rs/mcp-server/src/message_processor.rs | 7 +- codex-rs/tui/src/lib.rs | 12 ++- codex-rs/tui/src/updates.rs | 10 +- 15 files changed, 218 insertions(+), 99 deletions(-) create mode 100644 codex-rs/core/src/default_client.rs delete mode 100644 codex-rs/core/src/user_agent.rs diff --git a/codex-rs/chatgpt/src/apply_command.rs b/codex-rs/chatgpt/src/apply_command.rs index 52ab205a..3dbfeb16 100644 --- a/codex-rs/chatgpt/src/apply_command.rs +++ b/codex-rs/chatgpt/src/apply_command.rs @@ -31,7 +31,7 @@ pub async fn run_apply_command( ConfigOverrides::default(), )?; - init_chatgpt_token_from_auth(&config.codex_home).await?; + init_chatgpt_token_from_auth(&config.codex_home, &config.responses_originator_header).await?; let task_response = get_task(&config, apply_cli.task_id).await?; apply_diff_from_task(task_response, cwd).await diff --git a/codex-rs/chatgpt/src/chatgpt_client.rs b/codex-rs/chatgpt/src/chatgpt_client.rs index db756321..8991003f 100644 --- a/codex-rs/chatgpt/src/chatgpt_client.rs +++ b/codex-rs/chatgpt/src/chatgpt_client.rs @@ -1,5 +1,5 @@ use codex_core::config::Config; -use codex_core::user_agent::get_codex_user_agent; +use codex_core::default_client::create_client; use crate::chatgpt_token::get_chatgpt_token_data; use crate::chatgpt_token::init_chatgpt_token_from_auth; @@ -13,10 +13,10 @@ pub(crate) async fn chatgpt_get_request( path: String, ) -> anyhow::Result { let chatgpt_base_url = &config.chatgpt_base_url; - init_chatgpt_token_from_auth(&config.codex_home).await?; + init_chatgpt_token_from_auth(&config.codex_home, &config.responses_originator_header).await?; // Make direct HTTP request to ChatGPT backend API with the token - let client = reqwest::Client::new(); + let client = create_client(&config.responses_originator_header); let url = format!("{chatgpt_base_url}{path}"); let token = @@ -31,7 +31,6 @@ pub(crate) async fn chatgpt_get_request( .bearer_auth(&token.access_token) .header("chatgpt-account-id", account_id?) .header("Content-Type", "application/json") - .header("User-Agent", get_codex_user_agent(None)) .send() .await .context("Failed to send request")?; diff --git a/codex-rs/chatgpt/src/chatgpt_token.rs b/codex-rs/chatgpt/src/chatgpt_token.rs index 15192ce3..c51be9ec 100644 --- a/codex-rs/chatgpt/src/chatgpt_token.rs +++ b/codex-rs/chatgpt/src/chatgpt_token.rs @@ -19,8 +19,11 @@ pub fn set_chatgpt_token_data(value: TokenData) { } /// Initialize the ChatGPT token from auth.json file -pub async fn init_chatgpt_token_from_auth(codex_home: &Path) -> std::io::Result<()> { - let auth = CodexAuth::from_codex_home(codex_home, AuthMode::ChatGPT)?; +pub async fn init_chatgpt_token_from_auth( + codex_home: &Path, + originator: &str, +) -> std::io::Result<()> { + let auth = CodexAuth::from_codex_home(codex_home, AuthMode::ChatGPT, originator)?; if let Some(auth) = auth { let token_data = auth.get_token_data().await?; set_chatgpt_token_data(token_data); diff --git a/codex-rs/cli/src/login.rs b/codex-rs/cli/src/login.rs index f0350961..b8a8e45e 100644 --- a/codex-rs/cli/src/login.rs +++ b/codex-rs/cli/src/login.rs @@ -60,7 +60,11 @@ pub async fn run_login_with_api_key( pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! { let config = load_config_or_exit(cli_config_overrides); - match CodexAuth::from_codex_home(&config.codex_home, config.preferred_auth_method) { + match CodexAuth::from_codex_home( + &config.codex_home, + config.preferred_auth_method, + &config.responses_originator_header, + ) { Ok(Some(auth)) => match auth.mode { AuthMode::ApiKey => match auth.get_token().await { Ok(api_key) => { diff --git a/codex-rs/cli/src/proto.rs b/codex-rs/cli/src/proto.rs index 9b8cb92e..8fd8e30d 100644 --- a/codex-rs/cli/src/proto.rs +++ b/codex-rs/cli/src/proto.rs @@ -40,6 +40,7 @@ pub async fn run_main(opts: ProtoCli) -> anyhow::Result<()> { let conversation_manager = ConversationManager::new(AuthManager::shared( config.codex_home.clone(), config.preferred_auth_method, + config.responses_originator_header.clone(), )); let NewConversation { conversation_id: _, diff --git a/codex-rs/core/src/auth.rs b/codex-rs/core/src/auth.rs index d2954ba6..776a111c 100644 --- a/codex-rs/core/src/auth.rs +++ b/codex-rs/core/src/auth.rs @@ -27,6 +27,7 @@ pub struct CodexAuth { pub(crate) api_key: Option, pub(crate) auth_dot_json: Arc>>, pub(crate) auth_file: PathBuf, + pub(crate) client: reqwest::Client, } impl PartialEq for CodexAuth { @@ -36,22 +37,13 @@ impl PartialEq for CodexAuth { } impl CodexAuth { - pub fn from_api_key(api_key: &str) -> Self { - Self { - api_key: Some(api_key.to_owned()), - mode: AuthMode::ApiKey, - auth_file: PathBuf::new(), - auth_dot_json: Arc::new(Mutex::new(None)), - } - } - pub async fn refresh_token(&self) -> Result { let token_data = self .get_current_token_data() .ok_or(std::io::Error::other("Token data is not available."))?; let token = token_data.refresh_token; - let refresh_response = try_refresh_token(token) + let refresh_response = try_refresh_token(token, &self.client) .await .map_err(std::io::Error::other)?; @@ -83,8 +75,9 @@ impl CodexAuth { pub fn from_codex_home( codex_home: &Path, preferred_auth_method: AuthMode, + originator: &str, ) -> std::io::Result> { - load_auth(codex_home, true, preferred_auth_method) + load_auth(codex_home, true, preferred_auth_method, originator) } pub async fn get_token_data(&self) -> Result { @@ -98,7 +91,7 @@ impl CodexAuth { if last_refresh < Utc::now() - chrono::Duration::days(28) { let refresh_response = tokio::time::timeout( Duration::from_secs(60), - try_refresh_token(tokens.refresh_token.clone()), + try_refresh_token(tokens.refresh_token.clone(), &self.client), ) .await .map_err(|_| { @@ -180,8 +173,26 @@ impl CodexAuth { mode: AuthMode::ChatGPT, auth_file: PathBuf::new(), auth_dot_json, + client: crate::default_client::create_client("codex_cli_rs"), } } + + fn from_api_key_with_client(api_key: &str, client: reqwest::Client) -> Self { + Self { + api_key: Some(api_key.to_owned()), + mode: AuthMode::ApiKey, + auth_file: PathBuf::new(), + auth_dot_json: Arc::new(Mutex::new(None)), + client, + } + } + + pub fn from_api_key(api_key: &str) -> Self { + Self::from_api_key_with_client( + api_key, + crate::default_client::create_client(crate::default_client::DEFAULT_ORIGINATOR), + ) + } } pub const OPENAI_API_KEY_ENV_VAR: &str = "OPENAI_API_KEY"; @@ -221,18 +232,20 @@ fn load_auth( codex_home: &Path, include_env_var: bool, preferred_auth_method: AuthMode, + originator: &str, ) -> std::io::Result> { // First, check to see if there is a valid auth.json file. If not, we fall // back to AuthMode::ApiKey using the OPENAI_API_KEY environment variable // (if it is set). let auth_file = get_auth_file(codex_home); + let client = crate::default_client::create_client(originator); let auth_dot_json = match try_read_auth_json(&auth_file) { Ok(auth) => auth, // If auth.json does not exist, try to read the OPENAI_API_KEY from the // environment variable. Err(e) if e.kind() == std::io::ErrorKind::NotFound && include_env_var => { return match read_openai_api_key_from_env() { - Some(api_key) => Ok(Some(CodexAuth::from_api_key(&api_key))), + Some(api_key) => Ok(Some(CodexAuth::from_api_key_with_client(&api_key, client))), None => Ok(None), }; } @@ -258,7 +271,7 @@ fn load_auth( match &tokens { Some(tokens) => { if tokens.should_use_api_key(preferred_auth_method, tokens.is_openai_email()) { - return Ok(Some(CodexAuth::from_api_key(api_key))); + return Ok(Some(CodexAuth::from_api_key_with_client(api_key, client))); } else { // Ignore the API key and fall through to ChatGPT auth. } @@ -268,7 +281,7 @@ fn load_auth( // Perhaps the user ran `codex login --api-key ` or updated // auth.json by hand. Either way, let's assume they are trying // to use their API key. - return Ok(Some(CodexAuth::from_api_key(api_key))); + return Ok(Some(CodexAuth::from_api_key_with_client(api_key, client))); } } } @@ -284,6 +297,7 @@ fn load_auth( tokens, last_refresh, }))), + client, })) } @@ -333,7 +347,10 @@ async fn update_tokens( Ok(auth_dot_json) } -async fn try_refresh_token(refresh_token: String) -> std::io::Result { +async fn try_refresh_token( + refresh_token: String, + client: &reqwest::Client, +) -> std::io::Result { let refresh_request = RefreshRequest { client_id: CLIENT_ID, grant_type: "refresh_token", @@ -341,7 +358,7 @@ async fn try_refresh_token(refresh_token: String) -> std::io::Result, } @@ -668,12 +689,13 @@ impl AuthManager { /// preferred auth method. Errors loading auth are swallowed; `auth()` will /// simply return `None` in that case so callers can treat it as an /// unauthenticated state. - pub fn new(codex_home: PathBuf, preferred_auth_mode: AuthMode) -> Self { - let auth = CodexAuth::from_codex_home(&codex_home, preferred_auth_mode) + pub fn new(codex_home: PathBuf, preferred_auth_mode: AuthMode, originator: String) -> Self { + let auth = CodexAuth::from_codex_home(&codex_home, preferred_auth_mode, &originator) .ok() .flatten(); Self { codex_home, + originator, inner: RwLock::new(CachedAuth { preferred_auth_mode, auth, @@ -690,6 +712,7 @@ impl AuthManager { }; Arc::new(Self { codex_home: PathBuf::new(), + originator: "codex_cli_rs".to_string(), inner: RwLock::new(cached), }) } @@ -711,7 +734,7 @@ impl AuthManager { /// whether the auth value changed. pub fn reload(&self) -> bool { let preferred = self.preferred_auth_method(); - let new_auth = CodexAuth::from_codex_home(&self.codex_home, preferred) + let new_auth = CodexAuth::from_codex_home(&self.codex_home, preferred, &self.originator) .ok() .flatten(); if let Ok(mut guard) = self.inner.write() { @@ -732,8 +755,12 @@ impl AuthManager { } /// Convenience constructor returning an `Arc` wrapper. - pub fn shared(codex_home: PathBuf, preferred_auth_mode: AuthMode) -> Arc { - Arc::new(Self::new(codex_home, preferred_auth_mode)) + pub fn shared( + codex_home: PathBuf, + preferred_auth_mode: AuthMode, + originator: String, + ) -> Arc { + Arc::new(Self::new(codex_home, preferred_auth_mode, originator)) } /// Attempt to refresh the current auth token (if any). On success, reload diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index b92acef4..04817670 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -30,6 +30,7 @@ use crate::client_common::ResponsesApiRequest; use crate::client_common::create_reasoning_param_for_request; use crate::client_common::create_text_param_for_request; use crate::config::Config; +use crate::default_client::create_client; use crate::error::CodexErr; use crate::error::Result; use crate::error::UsageLimitReachedError; @@ -40,7 +41,6 @@ use crate::model_provider_info::WireApi; use crate::openai_model_info::get_model_info; use crate::openai_tools::create_tools_json_for_responses_api; use crate::protocol::TokenUsage; -use crate::user_agent::get_codex_user_agent; use crate::util::backoff; use codex_protocol::config_types::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; @@ -84,10 +84,12 @@ impl ModelClient { summary: ReasoningSummaryConfig, session_id: Uuid, ) -> Self { + let client = create_client(&config.responses_originator_header); + Self { config, auth_manager, - client: reqwest::Client::new(), + client, provider, session_id, effort, @@ -242,10 +244,6 @@ impl ModelClient { req_builder = req_builder.header("chatgpt-account-id", account_id); } - let originator = &self.config.responses_originator_header; - req_builder = req_builder.header("originator", originator); - req_builder = req_builder.header("User-Agent", get_codex_user_agent(Some(originator))); - let res = req_builder.send().await; if let Ok(resp) = &res { trace!( diff --git a/codex-rs/core/src/default_client.rs b/codex-rs/core/src/default_client.rs new file mode 100644 index 00000000..d2743517 --- /dev/null +++ b/codex-rs/core/src/default_client.rs @@ -0,0 +1,106 @@ +pub const DEFAULT_ORIGINATOR: &str = "codex_cli_rs"; + +pub fn get_codex_user_agent(originator: Option<&str>) -> String { + let build_version = env!("CARGO_PKG_VERSION"); + let os_info = os_info::get(); + format!( + "{}/{build_version} ({} {}; {}) {}", + originator.unwrap_or(DEFAULT_ORIGINATOR), + os_info.os_type(), + os_info.version(), + os_info.architecture().unwrap_or("unknown"), + crate::terminal::user_agent() + ) +} + +/// Create a reqwest client with default `originator` and `User-Agent` headers set. +pub fn create_client(originator: &str) -> reqwest::Client { + use reqwest::header::HeaderMap; + use reqwest::header::HeaderValue; + + let mut headers = HeaderMap::new(); + let originator_value = HeaderValue::from_str(originator) + .unwrap_or_else(|_| HeaderValue::from_static(DEFAULT_ORIGINATOR)); + headers.insert("originator", originator_value); + let ua = get_codex_user_agent(Some(originator)); + + match reqwest::Client::builder() + // Set UA via dedicated helper to avoid header validation pitfalls + .user_agent(ua) + .default_headers(headers) + .build() + { + Ok(client) => client, + Err(_) => reqwest::Client::new(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_codex_user_agent() { + let user_agent = get_codex_user_agent(None); + assert!(user_agent.starts_with("codex_cli_rs/")); + } + + #[tokio::test] + async fn test_create_client_sets_default_headers() { + use wiremock::Mock; + use wiremock::MockServer; + use wiremock::ResponseTemplate; + use wiremock::matchers::method; + use wiremock::matchers::path; + + let originator = "test_originator"; + let client = create_client(originator); + + // Spin up a local mock server and capture a request. + let server = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/")) + .respond_with(ResponseTemplate::new(200)) + .mount(&server) + .await; + + let resp = client + .get(server.uri()) + .send() + .await + .expect("failed to send request"); + assert!(resp.status().is_success()); + + let requests = server + .received_requests() + .await + .expect("failed to fetch received requests"); + assert!(!requests.is_empty()); + let headers = &requests[0].headers; + + // originator header is set to the provided value + let originator_header = headers + .get("originator") + .expect("originator header missing"); + assert_eq!(originator_header.to_str().unwrap(), originator); + + // User-Agent matches the computed Codex UA for that originator + let expected_ua = get_codex_user_agent(Some(originator)); + let ua_header = headers + .get("user-agent") + .expect("user-agent header missing"); + assert_eq!(ua_header.to_str().unwrap(), expected_ua); + } + + #[test] + #[cfg(target_os = "macos")] + fn test_macos() { + use regex_lite::Regex; + let user_agent = get_codex_user_agent(None); + let re = Regex::new( + r"^codex_cli_rs/\d+\.\d+\.\d+ \(Mac OS \d+\.\d+\.\d+; (x86_64|arm64)\) (\S+)$", + ) + .unwrap(); + assert!(re.is_match(&user_agent)); + } +} diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 8ae3e335..9f731298 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -45,6 +45,7 @@ pub use conversation_manager::NewConversation; // Re-export common auth types for workspace consumers pub use auth::AuthManager; pub use auth::CodexAuth; +pub mod default_client; pub mod model_family; mod openai_model_info; mod openai_tools; @@ -58,7 +59,6 @@ pub mod spawn; pub mod terminal; mod tool_apply_patch; pub mod turn_diff_tracker; -pub mod user_agent; pub use rollout::list::ConversationsPage; mod user_notification; pub mod util; diff --git a/codex-rs/core/src/user_agent.rs b/codex-rs/core/src/user_agent.rs deleted file mode 100644 index a63170ce..00000000 --- a/codex-rs/core/src/user_agent.rs +++ /dev/null @@ -1,37 +0,0 @@ -const DEFAULT_ORIGINATOR: &str = "codex_cli_rs"; - -pub fn get_codex_user_agent(originator: Option<&str>) -> String { - let build_version = env!("CARGO_PKG_VERSION"); - let os_info = os_info::get(); - format!( - "{}/{build_version} ({} {}; {}) {}", - originator.unwrap_or(DEFAULT_ORIGINATOR), - os_info.os_type(), - os_info.version(), - os_info.architecture().unwrap_or("unknown"), - crate::terminal::user_agent() - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_get_codex_user_agent() { - let user_agent = get_codex_user_agent(None); - assert!(user_agent.starts_with("codex_cli_rs/")); - } - - #[test] - #[cfg(target_os = "macos")] - fn test_macos() { - use regex_lite::Regex; - let user_agent = get_codex_user_agent(None); - let re = Regex::new( - r"^codex_cli_rs/\d+\.\d+\.\d+ \(Mac OS \d+\.\d+\.\d+; (x86_64|arm64)\) (\S+)$", - ) - .unwrap(); - assert!(re.is_match(&user_agent)); - } -} diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index f77749b4..07a51f07 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -414,12 +414,15 @@ async fn prefers_chatgpt_token_when_config_prefers_chatgpt() { config.model_provider = model_provider; config.preferred_auth_method = AuthMode::ChatGPT; - let auth_manager = - match CodexAuth::from_codex_home(codex_home.path(), config.preferred_auth_method) { - Ok(Some(auth)) => codex_core::AuthManager::from_auth_for_testing(auth), - Ok(None) => panic!("No CodexAuth found in codex_home"), - Err(e) => panic!("Failed to load CodexAuth: {e}"), - }; + let auth_manager = match CodexAuth::from_codex_home( + codex_home.path(), + config.preferred_auth_method, + &config.responses_originator_header, + ) { + Ok(Some(auth)) => codex_core::AuthManager::from_auth_for_testing(auth), + Ok(None) => panic!("No CodexAuth found in codex_home"), + Err(e) => panic!("Failed to load CodexAuth: {e}"), + }; let conversation_manager = ConversationManager::new(auth_manager); let NewConversation { conversation: codex, @@ -495,12 +498,15 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() { config.model_provider = model_provider; config.preferred_auth_method = AuthMode::ApiKey; - let auth_manager = - match CodexAuth::from_codex_home(codex_home.path(), config.preferred_auth_method) { - Ok(Some(auth)) => codex_core::AuthManager::from_auth_for_testing(auth), - Ok(None) => panic!("No CodexAuth found in codex_home"), - Err(e) => panic!("Failed to load CodexAuth: {e}"), - }; + let auth_manager = match CodexAuth::from_codex_home( + codex_home.path(), + config.preferred_auth_method, + &config.responses_originator_header, + ) { + Ok(Some(auth)) => codex_core::AuthManager::from_auth_for_testing(auth), + Ok(None) => panic!("No CodexAuth found in codex_home"), + Err(e) => panic!("Failed to load CodexAuth: {e}"), + }; let conversation_manager = ConversationManager::new(auth_manager); let NewConversation { conversation: codex, diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index d57d55bc..d5991efd 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -191,6 +191,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any let conversation_manager = ConversationManager::new(AuthManager::shared( config.codex_home.clone(), config.preferred_auth_method, + config.responses_originator_header.clone(), )); let NewConversation { conversation_id: _, diff --git a/codex-rs/mcp-server/src/message_processor.rs b/codex-rs/mcp-server/src/message_processor.rs index dbd1077d..bf5dbf98 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -53,8 +53,11 @@ impl MessageProcessor { config: Arc, ) -> Self { let outgoing = Arc::new(outgoing); - let auth_manager = - AuthManager::shared(config.codex_home.clone(), config.preferred_auth_method); + let auth_manager = AuthManager::shared( + config.codex_home.clone(), + config.preferred_auth_method, + config.responses_originator_header.clone(), + ); let conversation_manager = Arc::new(ConversationManager::new(auth_manager.clone())); let codex_message_processor = CodexMessageProcessor::new( auth_manager, diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 17506094..5ce17f7a 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -301,7 +301,11 @@ async fn run_ratatui_app( let Cli { prompt, images, .. } = cli; - let auth_manager = AuthManager::shared(config.codex_home.clone(), config.preferred_auth_method); + let auth_manager = AuthManager::shared( + config.codex_home.clone(), + config.preferred_auth_method, + config.responses_originator_header.clone(), + ); let login_status = get_login_status(&config); let should_show_onboarding = should_show_onboarding(login_status, &config, should_show_trust_screen); @@ -357,7 +361,11 @@ fn get_login_status(config: &Config) -> LoginStatus { // Reading the OpenAI API key is an async operation because it may need // to refresh the token. Block on it. let codex_home = config.codex_home.clone(); - match CodexAuth::from_codex_home(&codex_home, config.preferred_auth_method) { + match CodexAuth::from_codex_home( + &codex_home, + config.preferred_auth_method, + &config.responses_originator_header, + ) { Ok(Some(auth)) => LoginStatus::AuthMode(auth.mode), Ok(None) => LoginStatus::NotAuthenticated, Err(err) => { diff --git a/codex-rs/tui/src/updates.rs b/codex-rs/tui/src/updates.rs index fae170ff..39d084b4 100644 --- a/codex-rs/tui/src/updates.rs +++ b/codex-rs/tui/src/updates.rs @@ -9,7 +9,7 @@ use std::path::Path; use std::path::PathBuf; use codex_core::config::Config; -use codex_core::user_agent::get_codex_user_agent; +use codex_core::default_client::create_client; pub fn get_upgrade_version(config: &Config) -> Option { let version_file = version_filepath(config); @@ -22,8 +22,9 @@ pub fn get_upgrade_version(config: &Config) -> Option { // Refresh the cached latest version in the background so TUI startup // isn’t blocked by a network call. The UI reads the previously cached // value (if any) for this run; the next run shows the banner if needed. + let originator = config.responses_originator_header.clone(); tokio::spawn(async move { - check_for_update(&version_file) + check_for_update(&version_file, &originator) .await .inspect_err(|e| tracing::error!("Failed to update version: {e}")) }); @@ -63,12 +64,11 @@ fn read_version_info(version_file: &Path) -> anyhow::Result { Ok(serde_json::from_str(&contents)?) } -async fn check_for_update(version_file: &Path) -> anyhow::Result<()> { +async fn check_for_update(version_file: &Path, originator: &str) -> anyhow::Result<()> { let ReleaseInfo { tag_name: latest_tag_name, - } = reqwest::Client::new() + } = create_client(originator) .get(LATEST_RELEASE_URL) - .header("User-Agent", get_codex_user_agent(None)) .send() .await? .error_for_status()?