mirror of
https://github.com/openai/codex.git
synced 2026-10-21 05:22:21 +00:00
Add early rate-limit warnings for Plus and Team plans (#42142)
## What changed - Warn Plus and Team users when they have less than 50% of an approximately five-hour usage window remaining. - Preserve the existing 75%, 90%, and 95% usage thresholds for other plans and window lengths. - Continue deduplicating warnings across rolling rate-limit updates. ## Testing - Cover eligible and ineligible plan and window combinations, retained plan metadata, and warning deduplication. GitOrigin-RevId: b221def052b979c91490e7878829db2478c6e264
This commit is contained in:
committed by
copyberry
parent
b192442fc7
commit
a876a9e494
@@ -15,7 +15,7 @@ pub(super) const NUDGE_MODEL_SLUG: &str = "gpt-5.6-luna";
|
||||
pub(super) const RATE_LIMIT_SWITCH_PROMPT_THRESHOLD: f64 = 90.0;
|
||||
pub(super) const RATE_LIMIT_SWITCH_PROMPT_VIEW_ID: &str = "rate-limit-switch-prompt";
|
||||
|
||||
const RATE_LIMIT_WARNING_THRESHOLDS: [f64; 3] = [75.0, 90.0, 95.0];
|
||||
const RATE_LIMIT_WARNING_THRESHOLDS: [f64; 4] = [50.0, 75.0, 90.0, 95.0];
|
||||
const PRIMARY_LIMIT_FALLBACK_LABEL: &str = "usage";
|
||||
const SECONDARY_LIMIT_FALLBACK_LABEL: &str = "secondary usage";
|
||||
|
||||
@@ -28,6 +28,7 @@ pub(super) struct RateLimitWarningState {
|
||||
impl RateLimitWarningState {
|
||||
pub(super) fn take_warnings(
|
||||
&mut self,
|
||||
plan_type: Option<PlanType>,
|
||||
secondary_used_percent: Option<f64>,
|
||||
secondary_window_minutes: Option<i64>,
|
||||
primary_used_percent: Option<f64>,
|
||||
@@ -47,7 +48,14 @@ impl RateLimitWarningState {
|
||||
while self.secondary_index < RATE_LIMIT_WARNING_THRESHOLDS.len()
|
||||
&& secondary_used_percent >= RATE_LIMIT_WARNING_THRESHOLDS[self.secondary_index]
|
||||
{
|
||||
highest_secondary = Some(RATE_LIMIT_WARNING_THRESHOLDS[self.secondary_index]);
|
||||
let threshold = RATE_LIMIT_WARNING_THRESHOLDS[self.secondary_index];
|
||||
if threshold != 50.0
|
||||
|| (matches!(plan_type, Some(PlanType::Plus | PlanType::Team))
|
||||
&& secondary_window_minutes
|
||||
.is_some_and(|minutes| is_approximate_window(minutes, 5 * 60)))
|
||||
{
|
||||
highest_secondary = Some(threshold);
|
||||
}
|
||||
self.secondary_index += 1;
|
||||
}
|
||||
if let Some(threshold) = highest_secondary {
|
||||
@@ -65,7 +73,14 @@ impl RateLimitWarningState {
|
||||
while self.primary_index < RATE_LIMIT_WARNING_THRESHOLDS.len()
|
||||
&& primary_used_percent >= RATE_LIMIT_WARNING_THRESHOLDS[self.primary_index]
|
||||
{
|
||||
highest_primary = Some(RATE_LIMIT_WARNING_THRESHOLDS[self.primary_index]);
|
||||
let threshold = RATE_LIMIT_WARNING_THRESHOLDS[self.primary_index];
|
||||
if threshold != 50.0
|
||||
|| (matches!(plan_type, Some(PlanType::Plus | PlanType::Team))
|
||||
&& primary_window_minutes
|
||||
.is_some_and(|minutes| is_approximate_window(minutes, 5 * 60)))
|
||||
{
|
||||
highest_primary = Some(threshold);
|
||||
}
|
||||
self.primary_index += 1;
|
||||
}
|
||||
if let Some(threshold) = highest_primary {
|
||||
@@ -280,6 +295,7 @@ impl ChatWidget {
|
||||
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(
|
||||
self.plan_type,
|
||||
snapshot
|
||||
.secondary
|
||||
.as_ref()
|
||||
|
||||
@@ -545,14 +545,15 @@ async fn prefetch_rate_limits_is_gated_on_chatgpt_auth_provider() {
|
||||
#[tokio::test]
|
||||
async fn rate_limit_warnings_emit_thresholds() {
|
||||
let mut state = RateLimitWarningState::default();
|
||||
let plan = None;
|
||||
let mut warnings: Vec<String> = Vec::new();
|
||||
|
||||
warnings.extend(state.take_warnings(Some(10.0), Some(10079), Some(55.0), Some(299)));
|
||||
warnings.extend(state.take_warnings(Some(55.0), Some(10081), Some(10.0), Some(299)));
|
||||
warnings.extend(state.take_warnings(Some(10.0), Some(10081), Some(80.0), Some(299)));
|
||||
warnings.extend(state.take_warnings(Some(80.0), Some(10081), Some(10.0), Some(299)));
|
||||
warnings.extend(state.take_warnings(Some(10.0), Some(10081), Some(95.0), Some(299)));
|
||||
warnings.extend(state.take_warnings(Some(95.0), Some(10079), Some(10.0), Some(299)));
|
||||
warnings.extend(state.take_warnings(plan, Some(10.0), Some(10079), Some(55.0), Some(299)));
|
||||
warnings.extend(state.take_warnings(plan, Some(55.0), Some(10081), Some(10.0), Some(299)));
|
||||
warnings.extend(state.take_warnings(plan, Some(10.0), Some(10081), Some(80.0), Some(299)));
|
||||
warnings.extend(state.take_warnings(plan, Some(80.0), Some(10081), Some(10.0), Some(299)));
|
||||
warnings.extend(state.take_warnings(plan, Some(10.0), Some(10081), Some(95.0), Some(299)));
|
||||
warnings.extend(state.take_warnings(plan, Some(95.0), Some(10079), Some(10.0), Some(299)));
|
||||
|
||||
assert_eq!(
|
||||
warnings,
|
||||
@@ -574,12 +575,59 @@ async fn rate_limit_warnings_emit_thresholds() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rate_limit_usage_warnings_early_threshold_is_scoped_and_deduplicated() {
|
||||
for (plan_type, window_minutes, should_warn_early) in [
|
||||
(Some(PlanType::Plus), Some(300), true),
|
||||
(Some(PlanType::Team), Some(299), true),
|
||||
(Some(PlanType::Pro), Some(300), false),
|
||||
(Some(PlanType::Business), Some(300), false),
|
||||
(Some(PlanType::SelfServeBusinessProLite), Some(300), false),
|
||||
(None, Some(300), false),
|
||||
(Some(PlanType::Plus), Some(10080), false),
|
||||
(Some(PlanType::Team), None, false),
|
||||
] {
|
||||
let (mut chat, mut rx, _) = make_chatwidget_manual(Some("gpt-5")).await;
|
||||
let mut usage = snapshot(/*percent*/ 49.0);
|
||||
usage.plan_type = plan_type;
|
||||
usage.primary.as_mut().unwrap().window_duration_mins = window_minutes;
|
||||
chat.on_rate_limit_snapshot(Some(usage.clone()));
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
|
||||
// Rolling updates retain the plan learned from the account usage response.
|
||||
usage.plan_type = None;
|
||||
usage.primary.as_mut().unwrap().used_percent = 50;
|
||||
chat.on_rolling_rate_limit_snapshot(usage.clone());
|
||||
let warnings = drain_insert_history(&mut rx);
|
||||
assert_eq!(!warnings.is_empty(), should_warn_early);
|
||||
if should_warn_early {
|
||||
insta::allow_duplicates! {
|
||||
insta::assert_snapshot!(lines_to_single_string(&warnings.concat()), @r"
|
||||
⚠ Heads up, you have less than 50% of your 5h limit left. Run /status for a
|
||||
breakdown.
|
||||
");
|
||||
}
|
||||
}
|
||||
|
||||
chat.on_rolling_rate_limit_snapshot(usage.clone());
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
for used_percent in [75, 90, 95] {
|
||||
usage.primary.as_mut().unwrap().used_percent = used_percent;
|
||||
chat.on_rolling_rate_limit_snapshot(usage.clone());
|
||||
assert_eq!(drain_insert_history(&mut rx).len(), 1);
|
||||
chat.on_rolling_rate_limit_snapshot(usage.clone());
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_rate_limit_warnings_monthly() {
|
||||
let mut state = RateLimitWarningState::default();
|
||||
let mut warnings: Vec<String> = Vec::new();
|
||||
|
||||
warnings.extend(state.take_warnings(
|
||||
/*plan_type*/ None,
|
||||
Some(75.0),
|
||||
Some(43199),
|
||||
/*primary_used_percent*/ None,
|
||||
@@ -610,6 +658,7 @@ async fn test_rate_limit_warnings_use_generic_fallback_labels() {
|
||||
|
||||
assert_eq!(
|
||||
state.take_warnings(
|
||||
/*plan_type*/ None,
|
||||
/*secondary_used_percent*/ Some(75.0),
|
||||
/*secondary_window_minutes*/ None,
|
||||
/*primary_used_percent*/ Some(75.0),
|
||||
@@ -632,6 +681,7 @@ async fn test_rate_limit_warnings_use_secondary_fallback_for_unsupported_window(
|
||||
|
||||
assert_eq!(
|
||||
state.take_warnings(
|
||||
/*plan_type*/ None,
|
||||
/*secondary_used_percent*/ Some(75.0),
|
||||
/*secondary_window_minutes*/ Some(2 * 60),
|
||||
/*primary_used_percent*/ None,
|
||||
|
||||
Reference in New Issue
Block a user