From 5013d108248baad05b897b65aa6e3ca2626d7293 Mon Sep 17 00:00:00 2001 From: Brooks Date: Wed, 24 Jun 2026 14:43:17 -0400 Subject: [PATCH] [codex] suppress low usage remaining warnings when credits are available (#28593) ## Why The TUI computed proactive `Heads up, you have less than ...` warnings before considering workspace credits. As a result, users could see included-limit warnings even when they could continue using Codex with workspace credits. `has_credits` alone is not sufficient to determine whether finite credits are usable: a spend-control hard limit can cap the reported balance to zero while `has_credits` still reflects the workspace's raw balance. Unlimited credits are the opposite case: they are usable even though no numeric balance is reported. ## What changed - suppress proactive TUI rate-limit usage warnings and the lower-cost model nudge when usable workspace credits are available - treat credits as usable when `has_credits` is true and either `unlimited` is true or the parsed balance is positive - continue showing warnings when the usable balance is zero, including when a spend-control limit has capped otherwise available workspace credits - add regression coverage for zero-balance, positive-balance, and unlimited workspace-credit snapshots ## Validation - `just test -p codex-tui rate_limit_usage_warnings_` --- codex-rs/tui/src/chatwidget/rate_limits.rs | 20 +++-- .../src/chatwidget/tests/status_and_layout.rs | 76 +++++++++++++++++++ 2 files changed, 89 insertions(+), 7 deletions(-) diff --git a/codex-rs/tui/src/chatwidget/rate_limits.rs b/codex-rs/tui/src/chatwidget/rate_limits.rs index 77a0c5c7e..498f43263 100644 --- a/codex-rs/tui/src/chatwidget/rate_limits.rs +++ b/codex-rs/tui/src/chatwidget/rate_limits.rs @@ -210,7 +210,19 @@ impl ChatWidget { { self.codex_rate_limit_reached_type = Some(rate_limit_reached_type); } - let warnings = if is_codex_limit { + + let has_workspace_credits = snapshot.credits.as_ref().is_some_and(|credits| { + credits.has_credits + && (credits.unlimited + || credits.balance.as_deref().is_some_and(|balance| { + balance + .trim() + .parse::() + .is_ok_and(|balance| balance > 0.0) + })) + }); + let should_warn_about_rate_limit_usage = is_codex_limit && !has_workspace_credits; + let warnings = if should_warn_about_rate_limit_usage { self.rate_limit_warnings.take_warnings( snapshot .secondary @@ -245,12 +257,6 @@ impl ChatWidget { .map(|w| f64::from(w.used_percent) >= RATE_LIMIT_SWITCH_PROMPT_THRESHOLD) .unwrap_or(false)); - let has_workspace_credits = snapshot - .credits - .as_ref() - .map(|credits| credits.has_credits) - .unwrap_or(false); - if high_usage && !has_workspace_credits && !self.rate_limit_switch_prompt_hidden() diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index 57de08cee..329d24934 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -1009,6 +1009,82 @@ async fn rate_limit_switch_prompt_skips_non_codex_limit() { )); } +#[tokio::test] +async fn rate_limit_usage_warnings_show_when_workspace_credits_zero_balance() { + let (mut chat, mut rx, _) = make_chatwidget_manual(Some("gpt-5")).await; + chat.has_chatgpt_account = true; + + let mut rate_limit_snapshot = snapshot(/*percent*/ 95.0); + rate_limit_snapshot.credits = Some(CreditsSnapshot { + has_credits: true, + unlimited: false, + balance: Some("0".to_string()), + }); + + chat.on_rate_limit_snapshot(Some(rate_limit_snapshot)); + + assert!( + !drain_insert_history(&mut rx).is_empty(), + "zero-balance workspace credits should not suppress proactive usage warnings" + ); + assert!(matches!( + chat.rate_limit_switch_prompt, + RateLimitSwitchPromptState::Pending + )); +} + +#[tokio::test] +async fn rate_limit_usage_warnings_skip_when_workspace_credits_are_available() { + let (mut chat, mut rx, _) = make_chatwidget_manual(Some("gpt-5")).await; + chat.has_chatgpt_account = true; + + let mut rate_limit_snapshot = snapshot(/*percent*/ 95.0); + rate_limit_snapshot.credits = Some(CreditsSnapshot { + has_credits: true, + unlimited: false, + balance: Some("25.00".to_string()), + }); + + chat.on_rate_limit_snapshot(Some(rate_limit_snapshot)); + + assert!( + drain_insert_history(&mut rx).is_empty(), + "workspace credits should suppress proactive usage warnings" + ); + assert!(matches!( + chat.rate_limit_switch_prompt, + RateLimitSwitchPromptState::Idle + )); + assert_eq!( + chat.rate_limit_warnings.primary_index, 0, + "suppressed warnings should not consume warning thresholds" + ); +} + +#[tokio::test] +async fn rate_limit_usage_warnings_skip_with_unlimited_workspace_credits() { + let (mut chat, mut rx, _) = make_chatwidget_manual(Some("gpt-5")).await; + chat.has_chatgpt_account = true; + + let mut rate_limit_snapshot = snapshot(/*percent*/ 95.0); + rate_limit_snapshot.credits = Some(CreditsSnapshot { + has_credits: true, + unlimited: true, + balance: None, + }); + + chat.on_rate_limit_snapshot(Some(rate_limit_snapshot)); + + assert!( + drain_insert_history(&mut rx).is_empty(), + "unlimited workspace credits should suppress proactive usage warnings" + ); + assert!(matches!( + chat.rate_limit_switch_prompt, + RateLimitSwitchPromptState::Idle + )); +} + #[tokio::test] async fn rate_limit_switch_prompt_shows_once_per_session() { let (mut chat, _, _) = make_chatwidget_manual(Some("gpt-5")).await;