From 7151387474792e9febcc3cb6e01ceb98280ce5de Mon Sep 17 00:00:00 2001 From: Celia Chen Date: Thu, 29 Jan 2026 17:10:00 -0800 Subject: [PATCH 1/3] [feat] persist dynamic tools in session rollout file (#10130) Add dynamic tools to rollout file for persistence & read from rollout on resume. Ran a real example and spotted the following in the rollout file: ``` {"timestamp":"2026-01-29T01:27:57.468Z","type":"session_meta","payload":{"id":"019c075d-3f0b-77e3-894e-c1c159b04b1e","timestamp":"2026-01-29T01:27:57.451Z","...."dynamic_tools":[{"name":"demo_tool","description":"Demo dynamic tool","inputSchema":{"additionalProperties":false,"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"}}],"git":{"commit_hash":"ebc573f15c01b8af158e060cfedd401f043e9dfa","branch":"dev/cc/dynamic-tools","repository_url":"https://github.com/openai/codex.git"}}} ``` --- codex-rs/app-server/tests/common/rollout.rs | 2 ++ codex-rs/core/src/codex.rs | 7 +++++++ codex-rs/core/src/rollout/metadata.rs | 1 + codex-rs/core/src/rollout/recorder.rs | 10 ++++++++++ codex-rs/core/src/rollout/tests.rs | 1 + codex-rs/core/tests/suite/sqlite_state.rs | 1 + codex-rs/protocol/src/protocol.rs | 20 ++++++++++++++++++++ 7 files changed, 42 insertions(+) diff --git a/codex-rs/app-server/tests/common/rollout.rs b/codex-rs/app-server/tests/common/rollout.rs index 26316eddbd..14dce02c64 100644 --- a/codex-rs/app-server/tests/common/rollout.rs +++ b/codex-rs/app-server/tests/common/rollout.rs @@ -81,6 +81,7 @@ pub fn create_fake_rollout_with_source( source, model_provider: model_provider.map(str::to_string), base_instructions: None, + dynamic_tools: None, }; let payload = serde_json::to_value(SessionMetaLine { meta, @@ -159,6 +160,7 @@ pub fn create_fake_rollout_with_text_elements( source: SessionSource::Cli, model_provider: model_provider.map(str::to_string), base_instructions: None, + dynamic_tools: None, }; let payload = serde_json::to_value(SessionMetaLine { meta, diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 1b7b7312c7..1fb5572183 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -324,6 +324,12 @@ impl Codex { .clone() .or_else(|| conversation_history.get_base_instructions().map(|s| s.text)) .unwrap_or_else(|| model_info.get_model_instructions(config.model_personality)); + // Respect explicit thread-start tools; fall back to persisted tools when resuming a thread. + let dynamic_tools = if dynamic_tools.is_empty() { + conversation_history.get_dynamic_tools().unwrap_or_default() + } else { + dynamic_tools + }; // TODO (aibrahim): Consolidate config.model and config.model_reasoning_effort into config.collaboration_mode // to avoid extracting these fields separately and constructing CollaborationMode here. @@ -715,6 +721,7 @@ impl Session { BaseInstructions { text: session_configuration.base_instructions.clone(), }, + session_configuration.dynamic_tools.clone(), ), ) } diff --git a/codex-rs/core/src/rollout/metadata.rs b/codex-rs/core/src/rollout/metadata.rs index d08e77e241..5523274bde 100644 --- a/codex-rs/core/src/rollout/metadata.rs +++ b/codex-rs/core/src/rollout/metadata.rs @@ -303,6 +303,7 @@ mod tests { source: SessionSource::default(), model_provider: Some("openai".to_string()), base_instructions: None, + dynamic_tools: None, }; let session_meta_line = SessionMetaLine { meta: session_meta, diff --git a/codex-rs/core/src/rollout/recorder.rs b/codex-rs/core/src/rollout/recorder.rs index cc35850544..8157133165 100644 --- a/codex-rs/core/src/rollout/recorder.rs +++ b/codex-rs/core/src/rollout/recorder.rs @@ -7,6 +7,7 @@ use std::path::Path; use std::path::PathBuf; use codex_protocol::ThreadId; +use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::models::BaseInstructions; use serde_json::Value; use time::OffsetDateTime; @@ -68,6 +69,7 @@ pub enum RolloutRecorderParams { forked_from_id: Option, source: SessionSource, base_instructions: BaseInstructions, + dynamic_tools: Vec, }, Resume { path: PathBuf, @@ -91,12 +93,14 @@ impl RolloutRecorderParams { forked_from_id: Option, source: SessionSource, base_instructions: BaseInstructions, + dynamic_tools: Vec, ) -> Self { Self::Create { conversation_id, forked_from_id, source, base_instructions, + dynamic_tools, } } @@ -259,6 +263,7 @@ impl RolloutRecorder { forked_from_id, source, base_instructions, + dynamic_tools, } => { let LogFileInfo { file, @@ -288,6 +293,11 @@ impl RolloutRecorder { source, model_provider: Some(config.model_provider_id.clone()), base_instructions: Some(base_instructions), + dynamic_tools: if dynamic_tools.is_empty() { + None + } else { + Some(dynamic_tools) + }, }), ) } diff --git a/codex-rs/core/src/rollout/tests.rs b/codex-rs/core/src/rollout/tests.rs index 34afae2c94..cdf16291d3 100644 --- a/codex-rs/core/src/rollout/tests.rs +++ b/codex-rs/core/src/rollout/tests.rs @@ -873,6 +873,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> { source: SessionSource::VSCode, model_provider: Some("test-provider".into()), base_instructions: None, + dynamic_tools: None, }, git: None, }), diff --git a/codex-rs/core/tests/suite/sqlite_state.rs b/codex-rs/core/tests/suite/sqlite_state.rs index bf3b5a4f51..2582f90cbe 100644 --- a/codex-rs/core/tests/suite/sqlite_state.rs +++ b/codex-rs/core/tests/suite/sqlite_state.rs @@ -93,6 +93,7 @@ async fn backfill_scans_existing_rollouts() -> Result<()> { source: SessionSource::default(), model_provider: None, base_instructions: None, + dynamic_tools: None, }, git: None, }; diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index aea7d3f013..7ba01fa4bd 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -20,6 +20,7 @@ use crate::config_types::WindowsSandboxLevel; use crate::custom_prompts::CustomPrompt; use crate::dynamic_tools::DynamicToolCallRequest; use crate::dynamic_tools::DynamicToolResponse; +use crate::dynamic_tools::DynamicToolSpec; use crate::items::TurnItem; use crate::message_history::HistoryEntry; use crate::models::BaseInstructions; @@ -1513,6 +1514,22 @@ impl InitialHistory { }), } } + + pub fn get_dynamic_tools(&self) -> Option> { + match self { + InitialHistory::New => None, + InitialHistory::Resumed(resumed) => { + resumed.history.iter().find_map(|item| match item { + RolloutItem::SessionMeta(meta_line) => meta_line.meta.dynamic_tools.clone(), + _ => None, + }) + } + InitialHistory::Forked(items) => items.iter().find_map(|item| match item { + RolloutItem::SessionMeta(meta_line) => meta_line.meta.dynamic_tools.clone(), + _ => None, + }), + } + } } fn session_cwd_from_items(items: &[RolloutItem]) -> Option { @@ -1599,6 +1616,8 @@ pub struct SessionMeta { /// but may be missing for older sessions. If not present, fall back to rendering the base_instructions /// from ModelsManager. pub base_instructions: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamic_tools: Option>, } impl Default for SessionMeta { @@ -1613,6 +1632,7 @@ impl Default for SessionMeta { source: SessionSource::default(), model_provider: None, base_instructions: None, + dynamic_tools: None, } } } From a9cf449a80e9bfaf059651e3b8be9badcca4b145 Mon Sep 17 00:00:00 2001 From: willwang-openai Date: Thu, 29 Jan 2026 17:17:25 -0800 Subject: [PATCH 2/3] add error messages for the go plan type (#10181) Adds support for the Go plan type Updates rate limit error messages to point to the usage page --- codex-rs/backend-client/src/client.rs | 2 +- codex-rs/core/src/auth.rs | 1 + codex-rs/core/src/error.rs | 23 +++++++++++++++---- codex-rs/core/src/token_data.rs | 33 +++++++++++++++++++++++++++ codex-rs/protocol/src/account.rs | 1 + 5 files changed, 55 insertions(+), 5 deletions(-) diff --git a/codex-rs/backend-client/src/client.rs b/codex-rs/backend-client/src/client.rs index ec5fd3f61b..6fa36d1ffd 100644 --- a/codex-rs/backend-client/src/client.rs +++ b/codex-rs/backend-client/src/client.rs @@ -351,6 +351,7 @@ impl Client { fn map_plan_type(plan_type: crate::types::PlanType) -> AccountPlanType { match plan_type { crate::types::PlanType::Free => AccountPlanType::Free, + crate::types::PlanType::Go => AccountPlanType::Go, crate::types::PlanType::Plus => AccountPlanType::Plus, crate::types::PlanType::Pro => AccountPlanType::Pro, crate::types::PlanType::Team => AccountPlanType::Team, @@ -358,7 +359,6 @@ impl Client { crate::types::PlanType::Enterprise => AccountPlanType::Enterprise, crate::types::PlanType::Edu | crate::types::PlanType::Education => AccountPlanType::Edu, crate::types::PlanType::Guest - | crate::types::PlanType::Go | crate::types::PlanType::FreeWorkspace | crate::types::PlanType::Quorum | crate::types::PlanType::K12 => AccountPlanType::Unknown, diff --git a/codex-rs/core/src/auth.rs b/codex-rs/core/src/auth.rs index d6cfec5506..d4520dc855 100644 --- a/codex-rs/core/src/auth.rs +++ b/codex-rs/core/src/auth.rs @@ -159,6 +159,7 @@ impl CodexAuth { pub fn account_plan_type(&self) -> Option { let map_known = |kp: &InternalKnownPlan| match kp { InternalKnownPlan::Free => AccountPlanType::Free, + InternalKnownPlan::Go => AccountPlanType::Go, InternalKnownPlan::Plus => AccountPlanType::Plus, InternalKnownPlan::Pro => AccountPlanType::Pro, InternalKnownPlan::Team => AccountPlanType::Team, diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index 6c284074c9..f9f896c3b8 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -375,9 +375,11 @@ impl std::fmt::Display for UsageLimitReachedError { retry_suffix_after_or(self.resets_at.as_ref()) ) } - Some(PlanType::Known(KnownPlan::Free)) => { - "You've hit your usage limit. Upgrade to Plus to continue using Codex (https://openai.com/chatgpt/pricing)." - .to_string() + Some(PlanType::Known(KnownPlan::Free)) | Some(PlanType::Known(KnownPlan::Go)) => { + format!( + "You've hit your usage limit. Upgrade to Plus to continue using Codex (https://openai.com/chatgpt/pricing),{}", + retry_suffix_after_or(self.resets_at.as_ref()) + ) } Some(PlanType::Known(KnownPlan::Pro)) => format!( "You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits{}", @@ -817,7 +819,20 @@ mod tests { }; assert_eq!( err.to_string(), - "You've hit your usage limit. Upgrade to Plus to continue using Codex (https://openai.com/chatgpt/pricing)." + "You've hit your usage limit. Upgrade to Plus to continue using Codex (https://openai.com/chatgpt/pricing), or try again later." + ); + } + + #[test] + fn usage_limit_reached_error_formats_go_plan() { + let err = UsageLimitReachedError { + plan_type: Some(PlanType::Known(KnownPlan::Go)), + resets_at: None, + rate_limits: Some(rate_limit_snapshot()), + }; + assert_eq!( + err.to_string(), + "You've hit your usage limit. Upgrade to Plus to continue using Codex (https://openai.com/chatgpt/pricing), or try again later." ); } diff --git a/codex-rs/core/src/token_data.rs b/codex-rs/core/src/token_data.rs index 2010af16f7..e38660ff5a 100644 --- a/codex-rs/core/src/token_data.rs +++ b/codex-rs/core/src/token_data.rs @@ -64,6 +64,7 @@ pub(crate) enum PlanType { #[serde(rename_all = "lowercase")] pub(crate) enum KnownPlan { Free, + Go, Plus, Pro, Team, @@ -195,6 +196,38 @@ mod tests { assert_eq!(info.get_chatgpt_plan_type().as_deref(), Some("Pro")); } + #[test] + fn id_token_info_parses_go_plan() { + #[derive(Serialize)] + struct Header { + alg: &'static str, + typ: &'static str, + } + let header = Header { + alg: "none", + typ: "JWT", + }; + let payload = serde_json::json!({ + "email": "user@example.com", + "https://api.openai.com/auth": { + "chatgpt_plan_type": "go" + } + }); + + fn b64url_no_pad(bytes: &[u8]) -> String { + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes) + } + + let header_b64 = b64url_no_pad(&serde_json::to_vec(&header).unwrap()); + let payload_b64 = b64url_no_pad(&serde_json::to_vec(&payload).unwrap()); + let signature_b64 = b64url_no_pad(b"sig"); + let fake_jwt = format!("{header_b64}.{payload_b64}.{signature_b64}"); + + let info = parse_id_token(&fake_jwt).expect("should parse"); + assert_eq!(info.email.as_deref(), Some("user@example.com")); + assert_eq!(info.get_chatgpt_plan_type().as_deref(), Some("Go")); + } + #[test] fn id_token_info_handles_missing_fields() { #[derive(Serialize)] diff --git a/codex-rs/protocol/src/account.rs b/codex-rs/protocol/src/account.rs index fb707c3a73..1cb58a020f 100644 --- a/codex-rs/protocol/src/account.rs +++ b/codex-rs/protocol/src/account.rs @@ -9,6 +9,7 @@ use ts_rs::TS; pub enum PlanType { #[default] Free, + Go, Plus, Pro, Team, From bac0597b690beeaed2d091b99bd55832ff712a7f Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 29 Jan 2026 22:26:54 -0800 Subject: [PATCH 3/3] feat: refactor CodexAuth so invalid state cannot be represented --- .../src/protocol/common.rs | 4 - .../app-server/src/codex_message_processor.rs | 39 ++- codex-rs/cli/src/login.rs | 2 +- codex-rs/core/src/auth.rs | 295 ++++++++++++------ codex-rs/core/src/client.rs | 14 +- codex-rs/core/src/codex.rs | 2 +- codex-rs/core/src/model_provider_info.rs | 7 +- codex-rs/core/src/models_manager/manager.rs | 14 +- .../core/src/models_manager/model_presets.rs | 2 +- codex-rs/tui/src/app.rs | 2 +- codex-rs/tui/src/chatwidget.rs | 13 +- codex-rs/tui/src/lib.rs | 2 +- codex-rs/tui/src/status/helpers.rs | 8 +- 13 files changed, 263 insertions(+), 141 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index b9c58a5883..ebf891f22b 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -24,10 +24,6 @@ impl GitSha { } /// Authentication mode for OpenAI-backed providers. -/// -/// This is used internally to determine the base URL for generating responses, and to gate -/// ChatGPT-only behaviors like rate limits and available models (as opposed to API key-based -/// auth). #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)] #[serde(rename_all = "lowercase")] pub enum AuthMode { diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 660b4b87d0..93aacd88c2 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -131,6 +131,7 @@ use codex_app_server_protocol::build_turns_from_event_msgs; use codex_backend_client::Client as BackendClient; use codex_chatgpt::connectors; use codex_core::AuthManager; +use codex_core::CodexAuth; use codex_core::CodexThread; use codex_core::Cursor as RolloutCursor; use codex_core::InitialHistory; @@ -681,7 +682,11 @@ impl CodexMessageProcessor { .await; let payload = AuthStatusChangeNotification { - auth_method: self.auth_manager.auth_cached().map(|auth| auth.mode), + auth_method: self + .auth_manager + .auth_cached() + .as_ref() + .map(CodexAuth::api_auth_mode), }; self.outgoing .send_server_notification(ServerNotification::AuthStatusChange(payload)) @@ -711,7 +716,11 @@ impl CodexMessageProcessor { .await; let payload_v2 = AccountUpdatedNotification { - auth_mode: self.auth_manager.auth_cached().map(|auth| auth.mode), + auth_mode: self + .auth_manager + .auth_cached() + .as_ref() + .map(CodexAuth::api_auth_mode), }; self.outgoing .send_server_notification(ServerNotification::AccountUpdated(payload_v2)) @@ -807,7 +816,10 @@ impl CodexMessageProcessor { auth_manager.reload(); // Notify clients with the actual current auth mode. - let current_auth_method = auth_manager.auth_cached().map(|a| a.mode); + let current_auth_method = auth_manager + .auth_cached() + .as_ref() + .map(CodexAuth::api_auth_mode); let payload = AuthStatusChangeNotification { auth_method: current_auth_method, }; @@ -897,7 +909,10 @@ impl CodexMessageProcessor { auth_manager.reload(); // Notify clients with the actual current auth mode. - let current_auth_method = auth_manager.auth_cached().map(|a| a.mode); + let current_auth_method = auth_manager + .auth_cached() + .as_ref() + .map(CodexAuth::api_auth_mode); let payload_v2 = AccountUpdatedNotification { auth_mode: current_auth_method, }; @@ -1101,7 +1116,11 @@ impl CodexMessageProcessor { } // Reflect the current auth method after logout (likely None). - Ok(self.auth_manager.auth_cached().map(|auth| auth.mode)) + Ok(self + .auth_manager + .auth_cached() + .as_ref() + .map(CodexAuth::api_auth_mode)) } async fn logout_v1(&mut self, request_id: RequestId) { @@ -1173,7 +1192,7 @@ impl CodexMessageProcessor { } else { match self.auth_manager.auth().await { Some(auth) => { - let auth_mode = auth.mode; + let auth_mode = auth.api_auth_mode(); let (reported_auth_method, token_opt) = match auth.get_token() { Ok(token) if !token.is_empty() => { let tok = if include_token { Some(token) } else { None }; @@ -1220,9 +1239,9 @@ impl CodexMessageProcessor { } let account = match self.auth_manager.auth_cached() { - Some(auth) => Some(match auth.mode { - AuthMode::ApiKey => Account::ApiKey {}, - AuthMode::ChatGPT | AuthMode::ChatgptAuthTokens => { + Some(auth) => Some(match auth { + CodexAuth::ApiKey(_) => Account::ApiKey {}, + CodexAuth::ChatGpt(_) | CodexAuth::ChatGptAuthTokens(_) => { let email = auth.get_account_email(); let plan_type = auth.account_plan_type(); @@ -1281,7 +1300,7 @@ impl CodexMessageProcessor { }); }; - if !matches!(auth.mode, AuthMode::ChatGPT | AuthMode::ChatgptAuthTokens) { + if !auth.is_chatgpt_auth() { return Err(JSONRPCErrorError { code: INVALID_REQUEST_ERROR_CODE, message: "chatgpt authentication required to read rate limits".to_string(), diff --git a/codex-rs/cli/src/login.rs b/codex-rs/cli/src/login.rs index 0f4e4eb39f..cee002f339 100644 --- a/codex-rs/cli/src/login.rs +++ b/codex-rs/cli/src/login.rs @@ -225,7 +225,7 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! { let config = load_config_or_exit(cli_config_overrides).await; match CodexAuth::from_auth_storage(&config.codex_home, config.cli_auth_credentials_store_mode) { - Ok(Some(auth)) => match auth.mode { + Ok(Some(auth)) => match auth.api_auth_mode() { AuthMode::ApiKey => match auth.get_token() { Ok(api_key) => { eprintln!("Logged in using an API key - {}", safe_format_key(&api_key)); diff --git a/codex-rs/core/src/auth.rs b/codex-rs/core/src/auth.rs index d4520dc855..d4d0715342 100644 --- a/codex-rs/core/src/auth.rs +++ b/codex-rs/core/src/auth.rs @@ -14,7 +14,7 @@ use std::path::PathBuf; use std::sync::Arc; use std::sync::Mutex; -use codex_app_server_protocol::AuthMode; +use codex_app_server_protocol::AuthMode as ApiAuthMode; use codex_protocol::config_types::ForcedLoginMethod; pub use crate::auth::storage::AuthCredentialsStoreMode; @@ -35,19 +35,50 @@ use codex_protocol::account::PlanType as AccountPlanType; use serde_json::Value; use thiserror::Error; -#[derive(Debug, Clone)] -pub struct CodexAuth { - pub mode: AuthMode, +/// Account type for the current user. +/// +/// This is used internally to determine the base URL for generating responses, +/// and to gate ChatGPT-only behaviors like rate limits and available models (as +/// opposed to API key-based auth). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AuthMode { + ApiKey, + ChatGPT, +} - pub(crate) api_key: Option, - pub(crate) auth_dot_json: Arc>>, +#[derive(Debug, Clone)] +pub struct ApiKeyAuth { + api_key: String, +} + +#[derive(Debug, Clone)] +struct ChatGptAuthState { + auth_dot_json: Arc>>, + client: CodexHttpClient, +} + +#[derive(Debug, Clone)] +pub struct ChatGptAuth { + state: ChatGptAuthState, storage: Arc, - pub(crate) client: CodexHttpClient, +} + +#[derive(Debug, Clone)] +pub struct ChatGptAuthTokens { + state: ChatGptAuthState, +} + +/// Authentication mechanism used by the current user. +#[derive(Debug, Clone)] +pub enum CodexAuth { + ApiKey(ApiKeyAuth), + ChatGpt(ChatGptAuth), + ChatGptAuthTokens(ChatGptAuthTokens), } impl PartialEq for CodexAuth { fn eq(&self, other: &Self) -> bool { - self.mode == other.mode + self.api_auth_mode() == other.api_auth_mode() } } @@ -122,6 +153,36 @@ impl CodexAuth { load_auth(codex_home, false, auth_credentials_store_mode) } + pub fn internal_auth_mode(&self) -> AuthMode { + match self { + Self::ApiKey(_) => AuthMode::ApiKey, + Self::ChatGpt(_) | Self::ChatGptAuthTokens(_) => AuthMode::ChatGPT, + } + } + + pub fn api_auth_mode(&self) -> ApiAuthMode { + match self { + Self::ApiKey(_) => ApiAuthMode::ApiKey, + Self::ChatGpt(_) => ApiAuthMode::ChatGPT, + Self::ChatGptAuthTokens(_) => ApiAuthMode::ChatgptAuthTokens, + } + } + + pub fn is_chatgpt_auth(&self) -> bool { + self.internal_auth_mode() == AuthMode::ChatGPT + } + + pub fn is_external_chatgpt_tokens(&self) -> bool { + matches!(self, Self::ChatGptAuthTokens(_)) + } + + pub fn api_key(&self) -> Option<&str> { + match self { + Self::ApiKey(auth) => Some(auth.api_key.as_str()), + Self::ChatGpt(_) | Self::ChatGptAuthTokens(_) => None, + } + } + pub fn get_token_data(&self) -> Result { let auth_dot_json: Option = self.get_current_auth_json(); match auth_dot_json { @@ -135,9 +196,9 @@ impl CodexAuth { } pub fn get_token(&self) -> Result { - match self.mode { - AuthMode::ApiKey => Ok(self.api_key.clone().unwrap_or_default()), - AuthMode::ChatGPT | AuthMode::ChatgptAuthTokens => { + match self { + Self::ApiKey(auth) => Ok(auth.api_key.clone()), + Self::ChatGpt(_) | Self::ChatGptAuthTokens(_) => { let access_token = self.get_token_data()?.access_token; Ok(access_token) } @@ -177,18 +238,28 @@ impl CodexAuth { } fn get_current_auth_json(&self) -> Option { + let state = match self { + Self::ChatGpt(auth) => &auth.state, + Self::ChatGptAuthTokens(auth) => &auth.state, + Self::ApiKey(_) => return None, + }; #[expect(clippy::unwrap_used)] - self.auth_dot_json.lock().unwrap().clone() + state.auth_dot_json.lock().unwrap().clone() } fn get_current_token_data(&self) -> Option { self.get_current_auth_json().and_then(|t| t.tokens) } + #[cfg(test)] + fn auth_dot_json_for_testing(&self) -> Option { + self.get_current_auth_json() + } + /// Consider this private to integration tests. pub fn create_dummy_chatgpt_auth_for_testing() -> Self { let auth_dot_json = AuthDotJson { - auth_mode: Some(AuthMode::ChatGPT), + auth_mode: Some(ApiAuthMode::ChatGPT), openai_api_key: None, tokens: Some(TokenData { id_token: Default::default(), @@ -199,24 +270,19 @@ impl CodexAuth { last_refresh: Some(Utc::now()), }; - let auth_dot_json = Arc::new(Mutex::new(Some(auth_dot_json))); - Self { - api_key: None, - mode: AuthMode::ChatGPT, - storage: create_auth_storage(PathBuf::new(), AuthCredentialsStoreMode::File), - auth_dot_json, - client: crate::default_client::create_client(), - } + let client = crate::default_client::create_client(); + let state = ChatGptAuthState { + auth_dot_json: Arc::new(Mutex::new(Some(auth_dot_json))), + client, + }; + let storage = create_auth_storage(PathBuf::new(), AuthCredentialsStoreMode::File); + Self::ChatGpt(ChatGptAuth { state, storage }) } - fn from_api_key_with_client(api_key: &str, client: CodexHttpClient) -> Self { - Self { - api_key: Some(api_key.to_owned()), - mode: AuthMode::ApiKey, - storage: create_auth_storage(PathBuf::new(), AuthCredentialsStoreMode::File), - auth_dot_json: Arc::new(Mutex::new(None)), - client, - } + fn from_api_key_with_client(api_key: &str, _client: CodexHttpClient) -> Self { + Self::ApiKey(ApiKeyAuth { + api_key: api_key.to_owned(), + }) } pub fn from_api_key(api_key: &str) -> Self { @@ -224,6 +290,25 @@ impl CodexAuth { } } +impl ChatGptAuth { + fn current_auth_json(&self) -> Option { + #[expect(clippy::unwrap_used)] + self.state.auth_dot_json.lock().unwrap().clone() + } + + fn current_token_data(&self) -> Option { + self.current_auth_json().and_then(|auth| auth.tokens) + } + + fn storage(&self) -> &Arc { + &self.storage + } + + fn client(&self) -> &CodexHttpClient { + &self.state.client + } +} + pub const OPENAI_API_KEY_ENV_VAR: &str = "OPENAI_API_KEY"; pub const CODEX_API_KEY_ENV_VAR: &str = "CODEX_API_KEY"; @@ -258,7 +343,7 @@ pub fn login_with_api_key( auth_credentials_store_mode: AuthCredentialsStoreMode, ) -> std::io::Result<()> { let auth_dot_json = AuthDotJson { - auth_mode: Some(AuthMode::ApiKey), + auth_mode: Some(ApiAuthMode::ApiKey), openai_api_key: Some(api_key.to_string()), tokens: None, last_refresh: None, @@ -314,10 +399,10 @@ pub fn enforce_login_restrictions(config: &Config) -> std::io::Result<()> { }; if let Some(required_method) = config.forced_login_method { - let method_violation = match (required_method, auth.mode) { + let method_violation = match (required_method, auth.internal_auth_mode()) { (ForcedLoginMethod::Api, AuthMode::ApiKey) => None, - (ForcedLoginMethod::Chatgpt, AuthMode::ChatGPT | AuthMode::ChatgptAuthTokens) => None, - (ForcedLoginMethod::Api, AuthMode::ChatGPT | AuthMode::ChatgptAuthTokens) => Some( + (ForcedLoginMethod::Chatgpt, AuthMode::ChatGPT) => None, + (ForcedLoginMethod::Api, AuthMode::ChatGPT) => Some( "API key login is required, but ChatGPT is currently being used. Logging out." .to_string(), ), @@ -337,7 +422,7 @@ pub fn enforce_login_restrictions(config: &Config) -> std::io::Result<()> { } if let Some(expected_account_id) = config.forced_chatgpt_workspace_id.as_deref() { - if !matches!(auth.mode, AuthMode::ChatGPT | AuthMode::ChatgptAuthTokens) { + if !auth.is_chatgpt_auth() { return Ok(()); } @@ -607,7 +692,7 @@ impl AuthDotJson { }; Self { - auth_mode: Some(AuthMode::ChatgptAuthTokens), + auth_mode: Some(ApiAuthMode::ChatgptAuthTokens), openai_api_key: None, tokens: Some(tokens), last_refresh: Some(Utc::now()), @@ -623,21 +708,21 @@ impl AuthDotJson { Ok(Self::from_external_tokens(&external, id_token_info)) } - fn resolved_mode(&self) -> AuthMode { + fn resolved_mode(&self) -> ApiAuthMode { if let Some(mode) = self.auth_mode { return mode; } if self.openai_api_key.is_some() { - return AuthMode::ApiKey; + return ApiAuthMode::ApiKey; } - AuthMode::ChatGPT + ApiAuthMode::ChatGPT } fn storage_mode( &self, auth_credentials_store_mode: AuthCredentialsStoreMode, ) -> AuthCredentialsStoreMode { - if self.resolved_mode() == AuthMode::ChatgptAuthTokens { + if self.resolved_mode() == ApiAuthMode::ChatgptAuthTokens { AuthCredentialsStoreMode::Ephemeral } else { auth_credentials_store_mode @@ -653,7 +738,7 @@ impl CodexAuth { client: CodexHttpClient, ) -> std::io::Result { let auth_mode = auth_dot_json.resolved_mode(); - if auth_mode == AuthMode::ApiKey { + if auth_mode == ApiAuthMode::ApiKey { let Some(api_key) = auth_dot_json.openai_api_key.as_deref() else { return Err(std::io::Error::other("API key auth is missing a key.")); }; @@ -661,14 +746,21 @@ impl CodexAuth { } let storage_mode = auth_dot_json.storage_mode(auth_credentials_store_mode); - let storage = create_auth_storage(codex_home.to_path_buf(), storage_mode); - Ok(Self { - api_key: None, - mode: auth_mode, - storage, + let state = ChatGptAuthState { auth_dot_json: Arc::new(Mutex::new(Some(auth_dot_json))), client, - }) + }; + + match auth_mode { + ApiAuthMode::ChatGPT => { + let storage = create_auth_storage(codex_home.to_path_buf(), storage_mode); + Ok(Self::ChatGpt(ChatGptAuth { state, storage })) + } + ApiAuthMode::ChatgptAuthTokens => { + Ok(Self::ChatGptAuthTokens(ChatGptAuthTokens { state })) + } + ApiAuthMode::ApiKey => unreachable!("api key mode is handled above"), + } } } @@ -685,7 +777,10 @@ struct CachedAuth { impl Debug for CachedAuth { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("CachedAuth") - .field("auth_mode", &self.auth.as_ref().map(|auth| auth.mode)) + .field( + "auth_mode", + &self.auth.as_ref().map(CodexAuth::api_auth_mode), + ) .field( "external_refresher", &self.external_refresher.as_ref().map(|_| "present"), @@ -736,11 +831,13 @@ impl UnauthorizedRecovery { fn new(manager: Arc) -> Self { let cached_auth = manager.auth_cached(); let expected_account_id = cached_auth.as_ref().and_then(CodexAuth::get_account_id); - let mode = match cached_auth { - Some(auth) if auth.mode == AuthMode::ChatgptAuthTokens => { - UnauthorizedRecoveryMode::External - } - _ => UnauthorizedRecoveryMode::Managed, + let mode = if cached_auth + .as_ref() + .is_some_and(CodexAuth::is_external_chatgpt_tokens) + { + UnauthorizedRecoveryMode::External + } else { + UnauthorizedRecoveryMode::Managed }; let step = match mode { UnauthorizedRecoveryMode::Managed => UnauthorizedRecoveryStep::Reload, @@ -755,9 +852,12 @@ impl UnauthorizedRecovery { } pub fn has_next(&self) -> bool { - if !self.manager.auth_cached().is_some_and(|auth| { - matches!(auth.mode, AuthMode::ChatGPT | AuthMode::ChatgptAuthTokens) - }) { + if !self + .manager + .auth_cached() + .as_ref() + .is_some_and(CodexAuth::is_chatgpt_auth) + { return false; } @@ -996,7 +1096,8 @@ impl AuthManager { pub fn is_external_auth_active(&self) -> bool { self.auth_cached() - .is_some_and(|auth| auth.mode == AuthMode::ChatgptAuthTokens) + .as_ref() + .is_some_and(CodexAuth::is_external_chatgpt_tokens) } /// Convenience constructor returning an `Arc` wrapper. @@ -1026,18 +1127,25 @@ impl AuthManager { Some(auth) => auth, None => return Ok(()), }; - if auth.mode == AuthMode::ChatgptAuthTokens { - return self - .refresh_external_auth(ExternalAuthRefreshReason::Unauthorized) - .await; + match auth { + CodexAuth::ChatGptAuthTokens(_) => { + self.refresh_external_auth(ExternalAuthRefreshReason::Unauthorized) + .await + } + CodexAuth::ChatGpt(chatgpt_auth) => { + let token_data = chatgpt_auth.current_token_data().ok_or_else(|| { + RefreshTokenError::Transient(std::io::Error::other( + "Token data is not available.", + )) + })?; + self.refresh_tokens(&chatgpt_auth, token_data.refresh_token) + .await?; + // Reload to pick up persisted changes. + self.reload(); + Ok(()) + } + CodexAuth::ApiKey(_) => Ok(()), } - let token_data = auth.get_current_token_data().ok_or_else(|| { - RefreshTokenError::Transient(std::io::Error::other("Token data is not available.")) - })?; - self.refresh_tokens(&auth, token_data.refresh_token).await?; - // Reload to pick up persisted changes. - self.reload(); - Ok(()) } /// Log out by deleting the on‑disk auth.json (if present). Returns Ok(true) @@ -1051,16 +1159,23 @@ impl AuthManager { Ok(removed) } - pub fn get_auth_mode(&self) -> Option { - self.auth_cached().map(|a| a.mode) + pub fn get_auth_mode(&self) -> Option { + self.auth_cached().as_ref().map(CodexAuth::api_auth_mode) + } + + pub fn get_internal_auth_mode(&self) -> Option { + self.auth_cached() + .as_ref() + .map(CodexAuth::internal_auth_mode) } async fn refresh_if_stale(&self, auth: &CodexAuth) -> Result { - if auth.mode != AuthMode::ChatGPT { - return Ok(false); - } + let chatgpt_auth = match auth { + CodexAuth::ChatGpt(chatgpt_auth) => chatgpt_auth, + _ => return Ok(false), + }; - let auth_dot_json = match auth.get_current_auth_json() { + let auth_dot_json = match chatgpt_auth.current_auth_json() { Some(auth_dot_json) => auth_dot_json, None => return Ok(false), }; @@ -1075,7 +1190,8 @@ impl AuthManager { if last_refresh >= Utc::now() - chrono::Duration::days(TOKEN_REFRESH_INTERVAL) { return Ok(false); } - self.refresh_tokens(auth, tokens.refresh_token).await?; + self.refresh_tokens(chatgpt_auth, tokens.refresh_token) + .await?; self.reload(); Ok(true) } @@ -1135,13 +1251,13 @@ impl AuthManager { async fn refresh_tokens( &self, - auth: &CodexAuth, + auth: &ChatGptAuth, refresh_token: String, ) -> Result<(), RefreshTokenError> { - let refresh_response = try_refresh_token(refresh_token, &auth.client).await?; + let refresh_response = try_refresh_token(refresh_token, auth.client()).await?; update_tokens( - &auth.storage, + auth.storage(), refresh_response.id_token, refresh_response.access_token, refresh_response.refresh_token, @@ -1254,26 +1370,21 @@ mod tests { ) .expect("failed to write auth file"); - let CodexAuth { - api_key, - mode, - auth_dot_json, - storage: _, - .. - } = super::load_auth(codex_home.path(), false, AuthCredentialsStoreMode::File) + let auth = super::load_auth(codex_home.path(), false, AuthCredentialsStoreMode::File) .unwrap() .unwrap(); - assert_eq!(None, api_key); - assert_eq!(AuthMode::ChatGPT, mode); + assert_eq!(None, auth.api_key()); + assert_eq!(AuthMode::ChatGPT, auth.internal_auth_mode()); - let guard = auth_dot_json.lock().unwrap(); - let auth_dot_json = guard.as_ref().expect("AuthDotJson should exist"); + let auth_dot_json = auth + .auth_dot_json_for_testing() + .expect("AuthDotJson should exist"); let last_refresh = auth_dot_json .last_refresh .expect("last_refresh should be recorded"); assert_eq!( - &AuthDotJson { + AuthDotJson { auth_mode: None, openai_api_key: None, tokens: Some(TokenData { @@ -1308,8 +1419,8 @@ mod tests { let auth = super::load_auth(dir.path(), false, AuthCredentialsStoreMode::File) .unwrap() .unwrap(); - assert_eq!(auth.mode, AuthMode::ApiKey); - assert_eq!(auth.api_key, Some("sk-test-key".to_string())); + assert_eq!(auth.internal_auth_mode(), AuthMode::ApiKey); + assert_eq!(auth.api_key(), Some("sk-test-key")); assert!(auth.get_token_data().is_err()); } @@ -1318,7 +1429,7 @@ mod tests { fn logout_removes_auth_file() -> Result<(), std::io::Error> { let dir = tempdir()?; let auth_dot_json = AuthDotJson { - auth_mode: Some(AuthMode::ApiKey), + auth_mode: Some(ApiAuthMode::ApiKey), openai_api_key: Some("sk-test-key".to_string()), tokens: None, last_refresh: None, diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 0f05f58c47..f01c145e56 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -27,7 +27,6 @@ use codex_api::common::ResponsesWsRequest; use codex_api::create_text_param_for_request; use codex_api::error::ApiError; use codex_api::requests::responses::Compression; -use codex_app_server_protocol::AuthMode; use codex_otel::OtelManager; use codex_protocol::ThreadId; @@ -50,6 +49,7 @@ use tokio::sync::mpsc; use tracing::warn; use crate::AuthManager; +use crate::auth::CodexAuth; use crate::auth::RefreshTokenError; use crate::client_common::Prompt; use crate::client_common::ResponseEvent; @@ -220,7 +220,7 @@ impl ModelClient { let api_provider = self .state .provider - .to_api_provider(auth.as_ref().map(|a| a.mode))?; + .to_api_provider(auth.as_ref().map(CodexAuth::internal_auth_mode))?; let api_auth = auth_provider_from_auth(auth.clone(), &self.state.provider)?; let transport = ReqwestTransport::new(build_reqwest_client()); let request_telemetry = self.build_request_telemetry(); @@ -469,9 +469,7 @@ impl ModelClientSession { .config .features .enabled(Feature::EnableRequestCompression) - && auth.is_some_and(|auth| { - matches!(auth.mode, AuthMode::ChatGPT | AuthMode::ChatgptAuthTokens) - }) + && auth.is_some_and(CodexAuth::is_chatgpt_auth) && self.state.provider.is_openai() { Compression::Zstd @@ -509,7 +507,7 @@ impl ModelClientSession { let api_provider = self .state .provider - .to_api_provider(auth.as_ref().map(|a| a.mode))?; + .to_api_provider(auth.as_ref().map(CodexAuth::internal_auth_mode))?; let api_auth = auth_provider_from_auth(auth.clone(), &self.state.provider)?; let transport = ReqwestTransport::new(build_reqwest_client()); let (request_telemetry, sse_telemetry) = self.build_streaming_telemetry(); @@ -565,7 +563,7 @@ impl ModelClientSession { let api_provider = self .state .provider - .to_api_provider(auth.as_ref().map(|a| a.mode))?; + .to_api_provider(auth.as_ref().map(CodexAuth::internal_auth_mode))?; let api_auth = auth_provider_from_auth(auth.clone(), &self.state.provider)?; let transport = ReqwestTransport::new(build_reqwest_client()); let (request_telemetry, sse_telemetry) = self.build_streaming_telemetry(); @@ -611,7 +609,7 @@ impl ModelClientSession { let api_provider = self .state .provider - .to_api_provider(auth.as_ref().map(|a| a.mode))?; + .to_api_provider(auth.as_ref().map(CodexAuth::internal_auth_mode))?; let api_auth = auth_provider_from_auth(auth.clone(), &self.state.provider)?; let compression = self.responses_request_compression(auth.as_ref()); diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 1fb5572183..0228a4f251 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -822,7 +822,7 @@ impl Session { session_configuration.collaboration_mode.model(), auth.and_then(CodexAuth::get_account_id), auth.and_then(CodexAuth::get_account_email), - auth.map(|a| a.mode), + auth.map(CodexAuth::api_auth_mode), config.otel.log_user_prompt, terminal::user_agent(), session_configuration.session_source.clone(), diff --git a/codex-rs/core/src/model_provider_info.rs b/codex-rs/core/src/model_provider_info.rs index 0f3551480c..cbfd129195 100644 --- a/codex-rs/core/src/model_provider_info.rs +++ b/codex-rs/core/src/model_provider_info.rs @@ -8,7 +8,6 @@ use codex_api::Provider as ApiProvider; use codex_api::WireApi as ApiWireApi; use codex_api::provider::RetryConfig as ApiRetryConfig; -use codex_app_server_protocol::AuthMode; use http::HeaderMap; use http::header::HeaderName; use http::header::HeaderValue; @@ -19,6 +18,7 @@ use std::collections::HashMap; use std::env::VarError; use std::time::Duration; +use crate::auth::AuthMode; use crate::error::EnvVarError; const DEFAULT_STREAM_IDLE_TIMEOUT_MS: u64 = 300_000; const DEFAULT_STREAM_MAX_RETRIES: u64 = 5; @@ -137,10 +137,7 @@ impl ModelProviderInfo { &self, auth_mode: Option, ) -> crate::error::Result { - let default_base_url = if matches!( - auth_mode, - Some(AuthMode::ChatGPT | AuthMode::ChatgptAuthTokens) - ) { + let default_base_url = if matches!(auth_mode, Some(AuthMode::ChatGPT)) { "https://chatgpt.com/backend-api/codex" } else { "https://api.openai.com/v1" diff --git a/codex-rs/core/src/models_manager/manager.rs b/codex-rs/core/src/models_manager/manager.rs index c993032ea9..40f9c8ccdc 100644 --- a/codex-rs/core/src/models_manager/manager.rs +++ b/codex-rs/core/src/models_manager/manager.rs @@ -2,6 +2,7 @@ use super::cache::ModelsCacheManager; use crate::api_bridge::auth_provider_from_auth; use crate::api_bridge::map_api_error; use crate::auth::AuthManager; +use crate::auth::AuthMode; use crate::config::Config; use crate::default_client::build_reqwest_client; use crate::error::CodexErr; @@ -13,7 +14,6 @@ use crate::models_manager::model_info; use crate::models_manager::model_presets::builtin_model_presets; use codex_api::ModelsClient; use codex_api::ReqwestTransport; -use codex_app_server_protocol::AuthMode; use codex_protocol::config_types::CollaborationModeMask; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelPreset; @@ -61,7 +61,7 @@ impl ModelsManager { let cache_path = codex_home.join(MODEL_CACHE_FILE); let cache_manager = ModelsCacheManager::new(cache_path, DEFAULT_MODEL_CACHE_TTL); Self { - local_models: builtin_model_presets(auth_manager.get_auth_mode()), + local_models: builtin_model_presets(auth_manager.get_internal_auth_mode()), remote_models: RwLock::new(Self::load_remote_models_from_file().unwrap_or_default()), auth_manager, etag: RwLock::new(None), @@ -175,7 +175,7 @@ impl ModelsManager { refresh_strategy: RefreshStrategy, ) -> CoreResult<()> { if !config.features.enabled(Feature::RemoteModels) - || self.auth_manager.get_auth_mode() == Some(AuthMode::ApiKey) + || self.auth_manager.get_internal_auth_mode() == Some(AuthMode::ApiKey) { return Ok(()); } @@ -204,7 +204,7 @@ impl ModelsManager { let _timer = codex_otel::start_global_timer("codex.remote_models.fetch_update.duration_ms", &[]); let auth = self.auth_manager.auth().await; - let auth_mode = self.auth_manager.get_auth_mode(); + let auth_mode = self.auth_manager.get_internal_auth_mode(); let api_provider = self.provider.to_api_provider(auth_mode)?; let api_auth = auth_provider_from_auth(auth.clone(), &self.provider)?; let transport = ReqwestTransport::new(build_reqwest_client()); @@ -273,8 +273,8 @@ impl ModelsManager { let existing_presets = self.local_models.clone(); let mut merged_presets = ModelPreset::merge(remote_presets, existing_presets); let chatgpt_mode = matches!( - self.auth_manager.get_auth_mode(), - Some(AuthMode::ChatGPT | AuthMode::ChatgptAuthTokens) + self.auth_manager.get_internal_auth_mode(), + Some(AuthMode::ChatGPT) ); merged_presets = ModelPreset::filter_by_auth(merged_presets, chatgpt_mode); @@ -319,7 +319,7 @@ impl ModelsManager { let cache_path = codex_home.join(MODEL_CACHE_FILE); let cache_manager = ModelsCacheManager::new(cache_path, DEFAULT_MODEL_CACHE_TTL); Self { - local_models: builtin_model_presets(auth_manager.get_auth_mode()), + local_models: builtin_model_presets(auth_manager.get_internal_auth_mode()), remote_models: RwLock::new(Self::load_remote_models_from_file().unwrap_or_default()), auth_manager, etag: RwLock::new(None), diff --git a/codex-rs/core/src/models_manager/model_presets.rs b/codex-rs/core/src/models_manager/model_presets.rs index a8f4931f8d..9fb904f60b 100644 --- a/codex-rs/core/src/models_manager/model_presets.rs +++ b/codex-rs/core/src/models_manager/model_presets.rs @@ -1,4 +1,3 @@ -use codex_app_server_protocol::AuthMode; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ModelUpgrade; use codex_protocol::openai_models::ReasoningEffort; @@ -6,6 +5,7 @@ use codex_protocol::openai_models::ReasoningEffortPreset; use indoc::indoc; use once_cell::sync::Lazy; +use crate::auth::AuthMode; pub const HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG: &str = "hide_gpt5_1_migration_prompt"; pub const HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG: &str = "hide_gpt-5.1-codex-max_migration_prompt"; diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index ae1dc4b322..04dd27c121 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -963,7 +963,7 @@ impl App { model.as_str(), auth_ref.and_then(CodexAuth::get_account_id), auth_ref.and_then(CodexAuth::get_account_email), - auth_ref.map(|auth| auth.mode), + auth_ref.map(CodexAuth::api_auth_mode), config.otel.log_user_prompt, codex_core::terminal::user_agent(), SessionSource::Cli, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 74200e470a..72a3521014 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -29,7 +29,6 @@ use std::time::Duration; use std::time::Instant; use crate::version::CODEX_CLI_VERSION; -use codex_app_server_protocol::AuthMode; use codex_backend_client::Client as BackendClient; use codex_chatgpt::connectors; use codex_core::config::Config; @@ -3474,10 +3473,12 @@ impl ChatWidget { fn prefetch_rate_limits(&mut self) { self.stop_rate_limit_poller(); - if !matches!( - self.auth_manager.auth_cached().map(|auth| auth.mode), - Some(AuthMode::ChatGPT | AuthMode::ChatgptAuthTokens) - ) { + if !self + .auth_manager + .auth_cached() + .as_ref() + .is_some_and(CodexAuth::is_chatgpt_auth) + { return; } @@ -3490,7 +3491,7 @@ impl ChatWidget { loop { if let Some(auth) = auth_manager.auth().await - && matches!(auth.mode, AuthMode::ChatGPT | AuthMode::ChatgptAuthTokens) + && auth.is_chatgpt_auth() && let Some(snapshot) = fetch_rate_limits(base_url.clone(), auth).await { app_event_tx.send(AppEvent::RateLimitSnapshotFetched(snapshot)); diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 010f8c83e5..22c2d6db54 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -782,7 +782,7 @@ fn get_login_status(config: &Config) -> LoginStatus { // to refresh the token. Block on it. let codex_home = config.codex_home.clone(); match CodexAuth::from_auth_storage(&codex_home, config.cli_auth_credentials_store_mode) { - Ok(Some(auth)) => LoginStatus::AuthMode(auth.mode), + Ok(Some(auth)) => LoginStatus::AuthMode(auth.api_auth_mode()), Ok(None) => LoginStatus::NotAuthenticated, Err(err) => { error!("Failed to read auth.json: {err}"); diff --git a/codex-rs/tui/src/status/helpers.rs b/codex-rs/tui/src/status/helpers.rs index 91f958a9cf..0a801227f5 100644 --- a/codex-rs/tui/src/status/helpers.rs +++ b/codex-rs/tui/src/status/helpers.rs @@ -2,8 +2,8 @@ use crate::exec_command::relativize_to_home; use crate::text_formatting; use chrono::DateTime; use chrono::Local; -use codex_app_server_protocol::AuthMode; use codex_core::AuthManager; +use codex_core::CodexAuth; use codex_core::config::Config; use codex_core::project_doc::discover_project_doc_paths; use codex_protocol::account::PlanType; @@ -90,15 +90,15 @@ pub(crate) fn compose_account_display( ) -> Option { let auth = auth_manager.auth_cached()?; - match auth.mode { - AuthMode::ChatGPT | AuthMode::ChatgptAuthTokens => { + match auth { + CodexAuth::ChatGpt(_) | CodexAuth::ChatGptAuthTokens(_) => { let email = auth.get_account_email(); let plan = plan .map(|plan_type| title_case(format!("{plan_type:?}").as_str())) .or_else(|| Some("Unknown".to_string())); Some(StatusAccountDisplay::ChatGpt { email, plan }) } - AuthMode::ApiKey => Some(StatusAccountDisplay::ApiKey), + CodexAuth::ApiKey(_) => Some(StatusAccountDisplay::ApiKey), } }