From fa0051190b0f4a28536a0de51bd921479b083562 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Thu, 7 Aug 2025 18:24:34 -0700 Subject: [PATCH] Adjust error messages (#1969) image --- codex-rs/core/src/client.rs | 7 +++- codex-rs/core/src/codex.rs | 4 +- codex-rs/core/src/error.rs | 70 ++++++++++++++++++++++++++++++-- codex-rs/login/src/lib.rs | 27 +++++++----- codex-rs/login/src/token_data.rs | 7 ++++ 5 files changed, 98 insertions(+), 17 deletions(-) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 34aecad1..0caf1170 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -31,6 +31,7 @@ use crate::config_types::ReasoningEffort as ReasoningEffortConfig; use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; use crate::error::CodexErr; use crate::error::Result; +use crate::error::UsageLimitReachedError; use crate::flags::CODEX_RS_SSE_FIXTURE; use crate::model_provider_info::ModelProviderInfo; use crate::model_provider_info::WireApi; @@ -195,7 +196,7 @@ impl ModelClient { if let Some(auth) = auth.as_ref() && auth.mode == AuthMode::ChatGPT - && let Some(account_id) = auth.get_account_id().await + && let Some(account_id) = auth.get_account_id() { req_builder = req_builder.header("chatgpt-account-id", account_id); } @@ -263,7 +264,9 @@ impl ModelClient { }) = body { if r#type == "usage_limit_reached" { - return Err(CodexErr::UsageLimitReached); + return Err(CodexErr::UsageLimitReached(UsageLimitReachedError { + plan_type: auth.and_then(|a| a.get_plan_type()), + })); } else if r#type == "usage_not_included" { return Err(CodexErr::UsageNotIncluded); } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index aaef73de..385361e8 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1290,7 +1290,9 @@ async fn run_turn( Ok(output) => return Ok(output), Err(CodexErr::Interrupted) => return Err(CodexErr::Interrupted), Err(CodexErr::EnvVar(var)) => return Err(CodexErr::EnvVar(var)), - Err(e @ (CodexErr::UsageLimitReached | CodexErr::UsageNotIncluded)) => return Err(e), + Err(e @ (CodexErr::UsageLimitReached(_) | CodexErr::UsageNotIncluded)) => { + return Err(e); + } Err(e) => { // Use the configured provider-specific stream retry budget. let max_retries = sess.client.get_provider().stream_max_retries(); diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index f6394b71..7d6dc2cc 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -62,14 +62,16 @@ pub enum CodexErr { #[error("unexpected status {0}: {1}")] UnexpectedStatus(StatusCode, String), - #[error("Usage limit has been reached")] - UsageLimitReached, + #[error("{0}")] + UsageLimitReached(UsageLimitReachedError), - #[error("Usage not included with the plan")] + #[error( + "To use Codex with your ChatGPT plan, upgrade to Plus: https://openai.com/chatgpt/pricing." + )] UsageNotIncluded, #[error( - "We’re currently experiencing high demand, which may cause temporary errors. We’re adding capacity in East and West Europe to restore normal service." + "We're currently experiencing high demand, which may cause temporary errors. We’re adding capacity in East and West Europe to restore normal service." )] InternalServerError, @@ -115,6 +117,30 @@ pub enum CodexErr { EnvVar(EnvVarError), } +#[derive(Debug)] +pub struct UsageLimitReachedError { + pub plan_type: Option, +} + +impl std::fmt::Display for UsageLimitReachedError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(plan_type) = &self.plan_type + && plan_type == "plus" + { + write!( + f, + "You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing), or wait for limits to reset (every 5h and every week.)." + )?; + } else { + write!( + f, + "You've hit usage your usage limit. Limits reset every 5h and every week." + )?; + } + Ok(()) + } +} + #[derive(Debug)] pub struct EnvVarError { /// Name of the environment variable that is missing. @@ -150,3 +176,39 @@ pub fn get_error_message_ui(e: &CodexErr) -> String { _ => e.to_string(), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn usage_limit_reached_error_formats_plus_plan() { + let err = UsageLimitReachedError { + plan_type: Some("plus".to_string()), + }; + assert_eq!( + err.to_string(), + "You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing), or wait for limits to reset (every 5h and every week.)." + ); + } + + #[test] + fn usage_limit_reached_error_formats_default_when_none() { + let err = UsageLimitReachedError { plan_type: None }; + assert_eq!( + err.to_string(), + "You've hit usage your usage limit. Limits reset every 5h and every week." + ); + } + + #[test] + fn usage_limit_reached_error_formats_default_for_other_plans() { + let err = UsageLimitReachedError { + plan_type: Some("pro".to_string()), + }; + assert_eq!( + err.to_string(), + "You've hit usage your usage limit. Limits reset every 5h and every week." + ); + } +} diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index 7e693ccd..2a8f6749 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -68,8 +68,7 @@ impl CodexAuth { } pub async fn get_token_data(&self) -> Result { - #[expect(clippy::unwrap_used)] - let auth_dot_json = self.auth_dot_json.lock().unwrap().clone(); + let auth_dot_json: Option = self.get_current_auth_json(); match auth_dot_json { Some(AuthDotJson { tokens: Some(mut tokens), @@ -124,15 +123,23 @@ impl CodexAuth { } } - pub async fn get_account_id(&self) -> Option { - match self.mode { - AuthMode::ApiKey => None, - AuthMode::ChatGPT => { - let token_data = self.get_token_data().await.ok()?; + pub fn get_account_id(&self) -> Option { + self.get_current_token_data() + .and_then(|t| t.account_id.clone()) + } - token_data.account_id.clone() - } - } + pub fn get_plan_type(&self) -> Option { + self.get_current_token_data() + .and_then(|t| t.id_token.chatgpt_plan_type.as_ref().map(|p| p.as_string())) + } + + fn get_current_auth_json(&self) -> Option { + #[expect(clippy::unwrap_used)] + self.auth_dot_json.lock().unwrap().clone() + } + + fn get_current_token_data(&self) -> Option { + self.get_current_auth_json().and_then(|t| t.tokens.clone()) } /// Consider this private to integration tests. diff --git a/codex-rs/login/src/token_data.rs b/codex-rs/login/src/token_data.rs index 86ddaf58..fb4d8395 100644 --- a/codex-rs/login/src/token_data.rs +++ b/codex-rs/login/src/token_data.rs @@ -67,6 +67,13 @@ impl PlanType { } } } + + pub fn as_string(&self) -> String { + match self { + Self::Known(known) => format!("{known:?}").to_lowercase(), + Self::Unknown(s) => s.clone(), + } + } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]