Tui: Rate limits (#3977)

### /limits: show rate limits graph

<img width="442" height="287" alt="image"
src="https://github.com/user-attachments/assets/3e29a241-a4b0-4df8-bf71-43dc4dd805ca"
/>

### Warning on close to rate limits:

<img width="507" height="96" alt="image"
src="https://github.com/user-attachments/assets/732a958b-d240-4a89-8289-caa92de83537"
/>

Based on #3965
This commit is contained in:
Ahmed Ibrahim
2025-09-21 10:20:49 -07:00
committed by GitHub
parent 04504d8218
commit a4ebd069e5
11 changed files with 910 additions and 1 deletions

View File

@@ -28,6 +28,7 @@ use codex_core::protocol::McpToolCallBeginEvent;
use codex_core::protocol::McpToolCallEndEvent;
use codex_core::protocol::Op;
use codex_core::protocol::PatchApplyBeginEvent;
use codex_core::protocol::RateLimitSnapshotEvent;
use codex_core::protocol::ReviewRequest;
use codex_core::protocol::StreamErrorEvent;
use codex_core::protocol::TaskCompleteEvent;
@@ -103,6 +104,42 @@ struct RunningCommand {
parsed_cmd: Vec<ParsedCommand>,
}
const RATE_LIMIT_WARNING_THRESHOLDS: [f64; 3] = [50.0, 75.0, 90.0];
#[derive(Default)]
struct RateLimitWarningState {
weekly_index: usize,
hourly_index: usize,
}
impl RateLimitWarningState {
fn take_warnings(&mut self, weekly_used_percent: f64, hourly_used_percent: f64) -> Vec<String> {
let mut warnings = Vec::new();
while self.weekly_index < RATE_LIMIT_WARNING_THRESHOLDS.len()
&& weekly_used_percent >= RATE_LIMIT_WARNING_THRESHOLDS[self.weekly_index]
{
let threshold = RATE_LIMIT_WARNING_THRESHOLDS[self.weekly_index];
warnings.push(format!(
"Weekly usage exceeded {threshold:.0}% of the limit. Run /limits for detailed usage."
));
self.weekly_index += 1;
}
while self.hourly_index < RATE_LIMIT_WARNING_THRESHOLDS.len()
&& hourly_used_percent >= RATE_LIMIT_WARNING_THRESHOLDS[self.hourly_index]
{
let threshold = RATE_LIMIT_WARNING_THRESHOLDS[self.hourly_index];
warnings.push(format!(
"Hourly usage exceeded {threshold:.0}% of the limit. Run /limits for detailed usage."
));
self.hourly_index += 1;
}
warnings
}
}
/// Common initialization parameters shared by all `ChatWidget` constructors.
pub(crate) struct ChatWidgetInit {
pub(crate) config: Config,
@@ -124,6 +161,8 @@ pub(crate) struct ChatWidget {
session_header: SessionHeader,
initial_user_message: Option<UserMessage>,
token_info: Option<TokenUsageInfo>,
rate_limit_snapshot: Option<RateLimitSnapshotEvent>,
rate_limit_warnings: RateLimitWarningState,
// Stream lifecycle controller
stream: StreamController,
running_commands: HashMap<String, RunningCommand>,
@@ -285,6 +324,21 @@ impl ChatWidget {
self.bottom_pane.set_token_usage(info.clone());
self.token_info = info;
}
fn on_rate_limit_snapshot(&mut self, snapshot: Option<RateLimitSnapshotEvent>) {
if let Some(snapshot) = snapshot {
let warnings = self
.rate_limit_warnings
.take_warnings(snapshot.weekly_used_percent, snapshot.primary_used_percent);
self.rate_limit_snapshot = Some(snapshot);
if !warnings.is_empty() {
for warning in warnings {
self.add_to_history(history_cell::new_warning_event(warning));
}
self.request_redraw();
}
}
}
/// Finalize any active exec as failed and stop/clear running UI state.
fn finalize_turn(&mut self) {
// Ensure any spinner is replaced by a red ✗ and flushed into history.
@@ -699,6 +753,8 @@ impl ChatWidget {
initial_images,
),
token_info: None,
rate_limit_snapshot: None,
rate_limit_warnings: RateLimitWarningState::default(),
stream: StreamController::new(config),
running_commands: HashMap::new(),
task_complete_pending: false,
@@ -756,6 +812,8 @@ impl ChatWidget {
initial_images,
),
token_info: None,
rate_limit_snapshot: None,
rate_limit_warnings: RateLimitWarningState::default(),
stream: StreamController::new(config),
running_commands: HashMap::new(),
task_complete_pending: false,
@@ -929,6 +987,9 @@ impl ChatWidget {
SlashCommand::Status => {
self.add_status_output();
}
SlashCommand::Limits => {
self.add_limits_output();
}
SlashCommand::Mcp => {
self.add_mcp_output();
}
@@ -1106,7 +1167,10 @@ impl ChatWidget {
EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message }) => {
self.on_task_complete(last_agent_message)
}
EventMsg::TokenCount(ev) => self.set_token_info(ev.info),
EventMsg::TokenCount(ev) => {
self.set_token_info(ev.info);
self.on_rate_limit_snapshot(ev.rate_limits);
}
EventMsg::Error(ErrorEvent { message }) => self.on_error(message),
EventMsg::TurnAborted(ev) => match ev.reason {
TurnAbortReason::Interrupted => {
@@ -1282,6 +1346,15 @@ impl ChatWidget {
self.request_redraw();
}
pub(crate) fn add_limits_output(&mut self) {
if let Some(snapshot) = &self.rate_limit_snapshot {
self.add_to_history(history_cell::new_limits_output(snapshot));
} else {
self.add_to_history(history_cell::new_limits_unavailable());
}
self.request_redraw();
}
pub(crate) fn add_status_output(&mut self) {
let default_usage;
let usage_ref = if let Some(ti) = &self.token_info {