mirror of
https://github.com/openai/codex.git
synced 2026-11-14 21:40:39 +00:00
Expose Codex rate limits in statusline state (#61417)
## What changed Add `account.rate_limits` to the statusline snapshot, including the `codex` limit's name, primary and secondary usage percentages, window lengths, and Unix reset timestamps. Emit an empty array when no Codex rate-limit windows are available, and exclude other limit IDs. Preserve raw reset timestamps alongside the formatted display values. ## Testing Extend serialization assertions to cover both rate-limit windows, a missing reset timestamp, exclusion of other limit IDs, and the empty-array default. GitOrigin-RevId: f775c281a17ea2b72cbca810db397f00f16ea5e9
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
//! Projects the account values displayed by the built-in status line.
|
||||
|
||||
use super::ChatWidget;
|
||||
use crate::status::RateLimitWindowDisplay;
|
||||
use codex_protocol::protocol::RateLimitWindow;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(super) struct Account {
|
||||
rate_limits: Vec<RateLimit>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
workspace: Option<Workspace>,
|
||||
}
|
||||
@@ -14,9 +17,40 @@ struct Workspace {
|
||||
headline: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RateLimit {
|
||||
limit_id: &'static str,
|
||||
limit_name: Option<String>,
|
||||
primary_limit: Option<RateLimitWindow>,
|
||||
secondary_limit: Option<RateLimitWindow>,
|
||||
}
|
||||
|
||||
impl From<&RateLimitWindowDisplay> for RateLimitWindow {
|
||||
fn from(window: &RateLimitWindowDisplay) -> Self {
|
||||
Self {
|
||||
used_percent: window.used_percent,
|
||||
window_minutes: window.window_minutes,
|
||||
resets_at: window.resets_at_unix,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ChatWidget> for Account {
|
||||
fn from(chat: &ChatWidget) -> Self {
|
||||
let codex = chat
|
||||
.rate_limit_snapshots_by_limit_id
|
||||
.get("codex")
|
||||
.filter(|sample| sample.primary.is_some() || sample.secondary.is_some());
|
||||
Self {
|
||||
rate_limits: codex
|
||||
.into_iter()
|
||||
.map(|sample| RateLimit {
|
||||
limit_id: "codex",
|
||||
limit_name: (!sample.limit_name.is_empty()).then(|| sample.limit_name.clone()),
|
||||
primary_limit: sample.primary.as_ref().map(Into::into),
|
||||
secondary_limit: sample.secondary.as_ref().map(Into::into),
|
||||
})
|
||||
.collect(),
|
||||
workspace: chat
|
||||
.status_line_workspace_headline
|
||||
.clone()
|
||||
|
||||
@@ -1339,6 +1339,8 @@ async fn rate_limit_snapshot_updates_and_retains_plan_type() {
|
||||
async fn rate_limit_snapshots_keep_separate_entries_per_limit_id() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
|
||||
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
|
||||
limit_id: Some("codex".to_string()),
|
||||
limit_name: Some("codex".to_string()),
|
||||
@@ -1348,7 +1350,11 @@ async fn rate_limit_snapshots_keep_separate_entries_per_limit_id() {
|
||||
window_duration_mins: Some(300),
|
||||
resets_at: Some(100),
|
||||
}),
|
||||
secondary: None,
|
||||
secondary: Some(RateLimitWindow {
|
||||
used_percent: 8,
|
||||
window_duration_mins: Some(10_080),
|
||||
resets_at: None,
|
||||
}),
|
||||
credits: Some(CreditsSnapshot {
|
||||
has_credits: true,
|
||||
unlimited: false,
|
||||
@@ -1377,6 +1383,15 @@ async fn rate_limit_snapshots_keep_separate_entries_per_limit_id() {
|
||||
rate_limit_reached_type: None,
|
||||
}));
|
||||
|
||||
let state = serde_json::to_value(chat.statusline_state()).unwrap();
|
||||
assert_eq!(
|
||||
state["account"]["rate_limits"],
|
||||
serde_json::json!([{
|
||||
"limit_id": "codex", "limit_name": "codex",
|
||||
"primary_limit": {"used_percent": 20.0, "window_minutes": 300, "resets_at": 100},
|
||||
"secondary_limit": {"used_percent": 8.0, "window_minutes": 10_080, "resets_at": null}
|
||||
}])
|
||||
);
|
||||
let codex = chat
|
||||
.rate_limit_snapshots_by_limit_id
|
||||
.get("codex")
|
||||
@@ -3983,10 +3998,8 @@ async fn status_line_workspace_headline_renders_cached_value() {
|
||||
);
|
||||
let state = serde_json::to_value(chat.statusline_state()).unwrap();
|
||||
assert_eq!(
|
||||
state["account"],
|
||||
serde_json::json!({
|
||||
"workspace": {"headline": "Workspace maintenance starts at 5pm"}
|
||||
})
|
||||
state["account"]["workspace"],
|
||||
serde_json::json!({"headline": "Workspace maintenance starts at 5pm"})
|
||||
);
|
||||
assert!(
|
||||
drain_insert_history(&mut rx).is_empty(),
|
||||
|
||||
@@ -30,7 +30,7 @@ async fn statusline_state_projects_the_displayed_settings() {
|
||||
assert_eq!(
|
||||
serde_json::to_value(chat.statusline_state()).unwrap(),
|
||||
json!({
|
||||
"account": {},
|
||||
"account": {"rate_limits": []},
|
||||
"app": {"cli_version": CODEX_CLI_VERSION, "display_mode": "standard"},
|
||||
"config": {"statusline_items": ["run-state"]},
|
||||
"thread": {
|
||||
|
||||
@@ -70,6 +70,8 @@ pub(crate) const RATE_LIMIT_STALE_THRESHOLD_MINUTES: i64 = 15;
|
||||
pub(crate) struct RateLimitWindowDisplay {
|
||||
/// Percent used for the window.
|
||||
pub used_percent: f64,
|
||||
/// Unix time from the server, before formatting for the local clock.
|
||||
pub resets_at_unix: Option<i64>,
|
||||
/// Human-readable local reset time.
|
||||
pub resets_at: Option<String>,
|
||||
/// Window length in minutes when provided by the server.
|
||||
@@ -91,6 +93,7 @@ impl RateLimitWindowDisplay {
|
||||
|
||||
Self {
|
||||
used_percent: f64::from(window.used_percent),
|
||||
resets_at_unix: window.resets_at,
|
||||
resets_at,
|
||||
window_minutes: window.window_duration_mins,
|
||||
}
|
||||
@@ -459,6 +462,7 @@ mod tests {
|
||||
fn window(used_percent: f64) -> RateLimitWindowDisplay {
|
||||
RateLimitWindowDisplay {
|
||||
used_percent,
|
||||
resets_at_unix: None,
|
||||
resets_at: Some("soon".to_string()),
|
||||
window_minutes: Some(300),
|
||||
}
|
||||
@@ -521,11 +525,13 @@ mod tests {
|
||||
captured_at: now,
|
||||
primary: Some(RateLimitWindowDisplay {
|
||||
used_percent: 20.0,
|
||||
resets_at_unix: None,
|
||||
resets_at: Some("soon".to_string()),
|
||||
window_minutes: Some(60),
|
||||
}),
|
||||
secondary: Some(RateLimitWindowDisplay {
|
||||
used_percent: 40.0,
|
||||
resets_at_unix: None,
|
||||
resets_at: Some("later".to_string()),
|
||||
window_minutes: Some(2 * 60),
|
||||
}),
|
||||
|
||||
@@ -69,6 +69,7 @@ fn stale_monthly_limit_marks_fresh_rolling_snapshot_stale() {
|
||||
captured_at: now,
|
||||
primary: Some(RateLimitWindowDisplay {
|
||||
used_percent: 20.0,
|
||||
resets_at_unix: None,
|
||||
resets_at: Some("soon".to_string()),
|
||||
window_minutes: Some(300),
|
||||
}),
|
||||
@@ -1910,11 +1911,13 @@ async fn transcript_overlay_remeasures_status_after_rate_limit_refresh() {
|
||||
captured_at: now,
|
||||
primary: Some(RateLimitWindowDisplay {
|
||||
used_percent: 45.0,
|
||||
resets_at_unix: None,
|
||||
resets_at: Some("soon".to_string()),
|
||||
window_minutes: Some(300),
|
||||
}),
|
||||
secondary: Some(RateLimitWindowDisplay {
|
||||
used_percent: 30.0,
|
||||
resets_at_unix: None,
|
||||
resets_at: Some("later".to_string()),
|
||||
window_minutes: Some(10_080),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user