Add a common way to create HTTP client (#3110)

Ensure User-Agent and originator are always sent.
This commit is contained in:
pakrym-oai
2025-09-03 10:11:02 -07:00
committed by GitHub
parent af338cc505
commit c636f821ae
15 changed files with 218 additions and 99 deletions

View File

@@ -31,7 +31,7 @@ pub async fn run_apply_command(
ConfigOverrides::default(), 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?; let task_response = get_task(&config, apply_cli.task_id).await?;
apply_diff_from_task(task_response, cwd).await apply_diff_from_task(task_response, cwd).await

View File

@@ -1,5 +1,5 @@
use codex_core::config::Config; 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::get_chatgpt_token_data;
use crate::chatgpt_token::init_chatgpt_token_from_auth; use crate::chatgpt_token::init_chatgpt_token_from_auth;
@@ -13,10 +13,10 @@ pub(crate) async fn chatgpt_get_request<T: DeserializeOwned>(
path: String, path: String,
) -> anyhow::Result<T> { ) -> anyhow::Result<T> {
let chatgpt_base_url = &config.chatgpt_base_url; 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 // 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 url = format!("{chatgpt_base_url}{path}");
let token = let token =
@@ -31,7 +31,6 @@ pub(crate) async fn chatgpt_get_request<T: DeserializeOwned>(
.bearer_auth(&token.access_token) .bearer_auth(&token.access_token)
.header("chatgpt-account-id", account_id?) .header("chatgpt-account-id", account_id?)
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.header("User-Agent", get_codex_user_agent(None))
.send() .send()
.await .await
.context("Failed to send request")?; .context("Failed to send request")?;

View File

@@ -19,8 +19,11 @@ pub fn set_chatgpt_token_data(value: TokenData) {
} }
/// Initialize the ChatGPT token from auth.json file /// Initialize the ChatGPT token from auth.json file
pub async fn init_chatgpt_token_from_auth(codex_home: &Path) -> std::io::Result<()> { pub async fn init_chatgpt_token_from_auth(
let auth = CodexAuth::from_codex_home(codex_home, AuthMode::ChatGPT)?; codex_home: &Path,
originator: &str,
) -> std::io::Result<()> {
let auth = CodexAuth::from_codex_home(codex_home, AuthMode::ChatGPT, originator)?;
if let Some(auth) = auth { if let Some(auth) = auth {
let token_data = auth.get_token_data().await?; let token_data = auth.get_token_data().await?;
set_chatgpt_token_data(token_data); set_chatgpt_token_data(token_data);

View File

@@ -60,7 +60,11 @@ pub async fn run_login_with_api_key(
pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! { pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
let config = load_config_or_exit(cli_config_overrides); 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 { Ok(Some(auth)) => match auth.mode {
AuthMode::ApiKey => match auth.get_token().await { AuthMode::ApiKey => match auth.get_token().await {
Ok(api_key) => { Ok(api_key) => {

View File

@@ -40,6 +40,7 @@ pub async fn run_main(opts: ProtoCli) -> anyhow::Result<()> {
let conversation_manager = ConversationManager::new(AuthManager::shared( let conversation_manager = ConversationManager::new(AuthManager::shared(
config.codex_home.clone(), config.codex_home.clone(),
config.preferred_auth_method, config.preferred_auth_method,
config.responses_originator_header.clone(),
)); ));
let NewConversation { let NewConversation {
conversation_id: _, conversation_id: _,

View File

@@ -27,6 +27,7 @@ pub struct CodexAuth {
pub(crate) api_key: Option<String>, pub(crate) api_key: Option<String>,
pub(crate) auth_dot_json: Arc<Mutex<Option<AuthDotJson>>>, pub(crate) auth_dot_json: Arc<Mutex<Option<AuthDotJson>>>,
pub(crate) auth_file: PathBuf, pub(crate) auth_file: PathBuf,
pub(crate) client: reqwest::Client,
} }
impl PartialEq for CodexAuth { impl PartialEq for CodexAuth {
@@ -36,22 +37,13 @@ impl PartialEq for CodexAuth {
} }
impl 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<String, std::io::Error> { pub async fn refresh_token(&self) -> Result<String, std::io::Error> {
let token_data = self let token_data = self
.get_current_token_data() .get_current_token_data()
.ok_or(std::io::Error::other("Token data is not available."))?; .ok_or(std::io::Error::other("Token data is not available."))?;
let token = token_data.refresh_token; let token = token_data.refresh_token;
let refresh_response = try_refresh_token(token) let refresh_response = try_refresh_token(token, &self.client)
.await .await
.map_err(std::io::Error::other)?; .map_err(std::io::Error::other)?;
@@ -83,8 +75,9 @@ impl CodexAuth {
pub fn from_codex_home( pub fn from_codex_home(
codex_home: &Path, codex_home: &Path,
preferred_auth_method: AuthMode, preferred_auth_method: AuthMode,
originator: &str,
) -> std::io::Result<Option<CodexAuth>> { ) -> std::io::Result<Option<CodexAuth>> {
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<TokenData, std::io::Error> { pub async fn get_token_data(&self) -> Result<TokenData, std::io::Error> {
@@ -98,7 +91,7 @@ impl CodexAuth {
if last_refresh < Utc::now() - chrono::Duration::days(28) { if last_refresh < Utc::now() - chrono::Duration::days(28) {
let refresh_response = tokio::time::timeout( let refresh_response = tokio::time::timeout(
Duration::from_secs(60), Duration::from_secs(60),
try_refresh_token(tokens.refresh_token.clone()), try_refresh_token(tokens.refresh_token.clone(), &self.client),
) )
.await .await
.map_err(|_| { .map_err(|_| {
@@ -180,8 +173,26 @@ impl CodexAuth {
mode: AuthMode::ChatGPT, mode: AuthMode::ChatGPT,
auth_file: PathBuf::new(), auth_file: PathBuf::new(),
auth_dot_json, 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"; pub const OPENAI_API_KEY_ENV_VAR: &str = "OPENAI_API_KEY";
@@ -221,18 +232,20 @@ fn load_auth(
codex_home: &Path, codex_home: &Path,
include_env_var: bool, include_env_var: bool,
preferred_auth_method: AuthMode, preferred_auth_method: AuthMode,
originator: &str,
) -> std::io::Result<Option<CodexAuth>> { ) -> std::io::Result<Option<CodexAuth>> {
// First, check to see if there is a valid auth.json file. If not, we fall // 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 // back to AuthMode::ApiKey using the OPENAI_API_KEY environment variable
// (if it is set). // (if it is set).
let auth_file = get_auth_file(codex_home); 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) { let auth_dot_json = match try_read_auth_json(&auth_file) {
Ok(auth) => auth, Ok(auth) => auth,
// If auth.json does not exist, try to read the OPENAI_API_KEY from the // If auth.json does not exist, try to read the OPENAI_API_KEY from the
// environment variable. // environment variable.
Err(e) if e.kind() == std::io::ErrorKind::NotFound && include_env_var => { Err(e) if e.kind() == std::io::ErrorKind::NotFound && include_env_var => {
return match read_openai_api_key_from_env() { 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), None => Ok(None),
}; };
} }
@@ -258,7 +271,7 @@ fn load_auth(
match &tokens { match &tokens {
Some(tokens) => { Some(tokens) => {
if tokens.should_use_api_key(preferred_auth_method, tokens.is_openai_email()) { 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 { } else {
// Ignore the API key and fall through to ChatGPT auth. // 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 <KEY>` or updated // Perhaps the user ran `codex login --api-key <KEY>` or updated
// auth.json by hand. Either way, let's assume they are trying // auth.json by hand. Either way, let's assume they are trying
// to use their API key. // 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, tokens,
last_refresh, last_refresh,
}))), }))),
client,
})) }))
} }
@@ -333,7 +347,10 @@ async fn update_tokens(
Ok(auth_dot_json) Ok(auth_dot_json)
} }
async fn try_refresh_token(refresh_token: String) -> std::io::Result<RefreshResponse> { async fn try_refresh_token(
refresh_token: String,
client: &reqwest::Client,
) -> std::io::Result<RefreshResponse> {
let refresh_request = RefreshRequest { let refresh_request = RefreshRequest {
client_id: CLIENT_ID, client_id: CLIENT_ID,
grant_type: "refresh_token", grant_type: "refresh_token",
@@ -341,7 +358,7 @@ async fn try_refresh_token(refresh_token: String) -> std::io::Result<RefreshResp
scope: "openid profile email", scope: "openid profile email",
}; };
let client = reqwest::Client::new(); // Use shared client factory to include standard headers
let response = client let response = client
.post("https://auth.openai.com/oauth/token") .post("https://auth.openai.com/oauth/token")
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
@@ -455,7 +472,8 @@ mod tests {
mode, mode,
auth_dot_json, auth_dot_json,
auth_file: _, auth_file: _,
} = super::load_auth(codex_home.path(), false, AuthMode::ChatGPT) ..
} = super::load_auth(codex_home.path(), false, AuthMode::ChatGPT, "codex_cli_rs")
.unwrap() .unwrap()
.unwrap(); .unwrap();
assert_eq!(None, api_key); assert_eq!(None, api_key);
@@ -506,7 +524,8 @@ mod tests {
mode, mode,
auth_dot_json, auth_dot_json,
auth_file: _, auth_file: _,
} = super::load_auth(codex_home.path(), false, AuthMode::ChatGPT) ..
} = super::load_auth(codex_home.path(), false, AuthMode::ChatGPT, "codex_cli_rs")
.unwrap() .unwrap()
.unwrap(); .unwrap();
assert_eq!(None, api_key); assert_eq!(None, api_key);
@@ -556,7 +575,8 @@ mod tests {
mode, mode,
auth_dot_json, auth_dot_json,
auth_file: _, auth_file: _,
} = super::load_auth(codex_home.path(), false, AuthMode::ChatGPT) ..
} = super::load_auth(codex_home.path(), false, AuthMode::ChatGPT, "codex_cli_rs")
.unwrap() .unwrap()
.unwrap(); .unwrap();
assert_eq!(Some("sk-test-key".to_string()), api_key); assert_eq!(Some("sk-test-key".to_string()), api_key);
@@ -576,7 +596,7 @@ mod tests {
) )
.unwrap(); .unwrap();
let auth = super::load_auth(dir.path(), false, AuthMode::ChatGPT) let auth = super::load_auth(dir.path(), false, AuthMode::ChatGPT, "codex_cli_rs")
.unwrap() .unwrap()
.unwrap(); .unwrap();
assert_eq!(auth.mode, AuthMode::ApiKey); assert_eq!(auth.mode, AuthMode::ApiKey);
@@ -660,6 +680,7 @@ mod tests {
#[derive(Debug)] #[derive(Debug)]
pub struct AuthManager { pub struct AuthManager {
codex_home: PathBuf, codex_home: PathBuf,
originator: String,
inner: RwLock<CachedAuth>, inner: RwLock<CachedAuth>,
} }
@@ -668,12 +689,13 @@ impl AuthManager {
/// preferred auth method. Errors loading auth are swallowed; `auth()` will /// preferred auth method. Errors loading auth are swallowed; `auth()` will
/// simply return `None` in that case so callers can treat it as an /// simply return `None` in that case so callers can treat it as an
/// unauthenticated state. /// unauthenticated state.
pub fn new(codex_home: PathBuf, preferred_auth_mode: AuthMode) -> Self { pub fn new(codex_home: PathBuf, preferred_auth_mode: AuthMode, originator: String) -> Self {
let auth = CodexAuth::from_codex_home(&codex_home, preferred_auth_mode) let auth = CodexAuth::from_codex_home(&codex_home, preferred_auth_mode, &originator)
.ok() .ok()
.flatten(); .flatten();
Self { Self {
codex_home, codex_home,
originator,
inner: RwLock::new(CachedAuth { inner: RwLock::new(CachedAuth {
preferred_auth_mode, preferred_auth_mode,
auth, auth,
@@ -690,6 +712,7 @@ impl AuthManager {
}; };
Arc::new(Self { Arc::new(Self {
codex_home: PathBuf::new(), codex_home: PathBuf::new(),
originator: "codex_cli_rs".to_string(),
inner: RwLock::new(cached), inner: RwLock::new(cached),
}) })
} }
@@ -711,7 +734,7 @@ impl AuthManager {
/// whether the auth value changed. /// whether the auth value changed.
pub fn reload(&self) -> bool { pub fn reload(&self) -> bool {
let preferred = self.preferred_auth_method(); 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() .ok()
.flatten(); .flatten();
if let Ok(mut guard) = self.inner.write() { if let Ok(mut guard) = self.inner.write() {
@@ -732,8 +755,12 @@ impl AuthManager {
} }
/// Convenience constructor returning an `Arc` wrapper. /// Convenience constructor returning an `Arc` wrapper.
pub fn shared(codex_home: PathBuf, preferred_auth_mode: AuthMode) -> Arc<Self> { pub fn shared(
Arc::new(Self::new(codex_home, preferred_auth_mode)) codex_home: PathBuf,
preferred_auth_mode: AuthMode,
originator: String,
) -> Arc<Self> {
Arc::new(Self::new(codex_home, preferred_auth_mode, originator))
} }
/// Attempt to refresh the current auth token (if any). On success, reload /// Attempt to refresh the current auth token (if any). On success, reload

View File

@@ -30,6 +30,7 @@ use crate::client_common::ResponsesApiRequest;
use crate::client_common::create_reasoning_param_for_request; use crate::client_common::create_reasoning_param_for_request;
use crate::client_common::create_text_param_for_request; use crate::client_common::create_text_param_for_request;
use crate::config::Config; use crate::config::Config;
use crate::default_client::create_client;
use crate::error::CodexErr; use crate::error::CodexErr;
use crate::error::Result; use crate::error::Result;
use crate::error::UsageLimitReachedError; 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_model_info::get_model_info;
use crate::openai_tools::create_tools_json_for_responses_api; use crate::openai_tools::create_tools_json_for_responses_api;
use crate::protocol::TokenUsage; use crate::protocol::TokenUsage;
use crate::user_agent::get_codex_user_agent;
use crate::util::backoff; use crate::util::backoff;
use codex_protocol::config_types::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::config_types::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
@@ -84,10 +84,12 @@ impl ModelClient {
summary: ReasoningSummaryConfig, summary: ReasoningSummaryConfig,
session_id: Uuid, session_id: Uuid,
) -> Self { ) -> Self {
let client = create_client(&config.responses_originator_header);
Self { Self {
config, config,
auth_manager, auth_manager,
client: reqwest::Client::new(), client,
provider, provider,
session_id, session_id,
effort, effort,
@@ -242,10 +244,6 @@ impl ModelClient {
req_builder = req_builder.header("chatgpt-account-id", account_id); 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; let res = req_builder.send().await;
if let Ok(resp) = &res { if let Ok(resp) = &res {
trace!( trace!(

View File

@@ -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));
}
}

View File

@@ -45,6 +45,7 @@ pub use conversation_manager::NewConversation;
// Re-export common auth types for workspace consumers // Re-export common auth types for workspace consumers
pub use auth::AuthManager; pub use auth::AuthManager;
pub use auth::CodexAuth; pub use auth::CodexAuth;
pub mod default_client;
pub mod model_family; pub mod model_family;
mod openai_model_info; mod openai_model_info;
mod openai_tools; mod openai_tools;
@@ -58,7 +59,6 @@ pub mod spawn;
pub mod terminal; pub mod terminal;
mod tool_apply_patch; mod tool_apply_patch;
pub mod turn_diff_tracker; pub mod turn_diff_tracker;
pub mod user_agent;
pub use rollout::list::ConversationsPage; pub use rollout::list::ConversationsPage;
mod user_notification; mod user_notification;
pub mod util; pub mod util;

View File

@@ -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));
}
}

View File

@@ -414,12 +414,15 @@ async fn prefers_chatgpt_token_when_config_prefers_chatgpt() {
config.model_provider = model_provider; config.model_provider = model_provider;
config.preferred_auth_method = AuthMode::ChatGPT; config.preferred_auth_method = AuthMode::ChatGPT;
let auth_manager = let auth_manager = match CodexAuth::from_codex_home(
match CodexAuth::from_codex_home(codex_home.path(), config.preferred_auth_method) { codex_home.path(),
Ok(Some(auth)) => codex_core::AuthManager::from_auth_for_testing(auth), config.preferred_auth_method,
Ok(None) => panic!("No CodexAuth found in codex_home"), &config.responses_originator_header,
Err(e) => panic!("Failed to load CodexAuth: {e}"), ) {
}; 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 conversation_manager = ConversationManager::new(auth_manager);
let NewConversation { let NewConversation {
conversation: codex, conversation: codex,
@@ -495,12 +498,15 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() {
config.model_provider = model_provider; config.model_provider = model_provider;
config.preferred_auth_method = AuthMode::ApiKey; config.preferred_auth_method = AuthMode::ApiKey;
let auth_manager = let auth_manager = match CodexAuth::from_codex_home(
match CodexAuth::from_codex_home(codex_home.path(), config.preferred_auth_method) { codex_home.path(),
Ok(Some(auth)) => codex_core::AuthManager::from_auth_for_testing(auth), config.preferred_auth_method,
Ok(None) => panic!("No CodexAuth found in codex_home"), &config.responses_originator_header,
Err(e) => panic!("Failed to load CodexAuth: {e}"), ) {
}; 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 conversation_manager = ConversationManager::new(auth_manager);
let NewConversation { let NewConversation {
conversation: codex, conversation: codex,

View File

@@ -191,6 +191,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
let conversation_manager = ConversationManager::new(AuthManager::shared( let conversation_manager = ConversationManager::new(AuthManager::shared(
config.codex_home.clone(), config.codex_home.clone(),
config.preferred_auth_method, config.preferred_auth_method,
config.responses_originator_header.clone(),
)); ));
let NewConversation { let NewConversation {
conversation_id: _, conversation_id: _,

View File

@@ -53,8 +53,11 @@ impl MessageProcessor {
config: Arc<Config>, config: Arc<Config>,
) -> Self { ) -> Self {
let outgoing = Arc::new(outgoing); let outgoing = Arc::new(outgoing);
let auth_manager = let auth_manager = AuthManager::shared(
AuthManager::shared(config.codex_home.clone(), config.preferred_auth_method); config.codex_home.clone(),
config.preferred_auth_method,
config.responses_originator_header.clone(),
);
let conversation_manager = Arc::new(ConversationManager::new(auth_manager.clone())); let conversation_manager = Arc::new(ConversationManager::new(auth_manager.clone()));
let codex_message_processor = CodexMessageProcessor::new( let codex_message_processor = CodexMessageProcessor::new(
auth_manager, auth_manager,

View File

@@ -301,7 +301,11 @@ async fn run_ratatui_app(
let Cli { prompt, images, .. } = cli; 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 login_status = get_login_status(&config);
let should_show_onboarding = let should_show_onboarding =
should_show_onboarding(login_status, &config, should_show_trust_screen); 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 // Reading the OpenAI API key is an async operation because it may need
// to refresh the token. Block on it. // to refresh the token. Block on it.
let codex_home = config.codex_home.clone(); 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(Some(auth)) => LoginStatus::AuthMode(auth.mode),
Ok(None) => LoginStatus::NotAuthenticated, Ok(None) => LoginStatus::NotAuthenticated,
Err(err) => { Err(err) => {

View File

@@ -9,7 +9,7 @@ use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use codex_core::config::Config; 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<String> { pub fn get_upgrade_version(config: &Config) -> Option<String> {
let version_file = version_filepath(config); let version_file = version_filepath(config);
@@ -22,8 +22,9 @@ pub fn get_upgrade_version(config: &Config) -> Option<String> {
// Refresh the cached latest version in the background so TUI startup // Refresh the cached latest version in the background so TUI startup
// isnt blocked by a network call. The UI reads the previously cached // isnt 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. // 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 { tokio::spawn(async move {
check_for_update(&version_file) check_for_update(&version_file, &originator)
.await .await
.inspect_err(|e| tracing::error!("Failed to update version: {e}")) .inspect_err(|e| tracing::error!("Failed to update version: {e}"))
}); });
@@ -63,12 +64,11 @@ fn read_version_info(version_file: &Path) -> anyhow::Result<VersionInfo> {
Ok(serde_json::from_str(&contents)?) 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 { let ReleaseInfo {
tag_name: latest_tag_name, tag_name: latest_tag_name,
} = reqwest::Client::new() } = create_client(originator)
.get(LATEST_RELEASE_URL) .get(LATEST_RELEASE_URL)
.header("User-Agent", get_codex_user_agent(None))
.send() .send()
.await? .await?
.error_for_status()? .error_for_status()?