[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_`
This commit is contained in:
Brooks
2026-06-24 14:43:17 -04:00
committed by GitHub
Unverified
parent a781761eda
commit 5013d10824
2 changed files with 89 additions and 7 deletions
+13 -7
View File
@@ -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::<f64>()
.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()
@@ -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;