mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
fix(tui/limits): refresh rate limits off main thread
This commit is contained in:
@@ -697,6 +697,10 @@ impl App<'_> {
|
||||
}
|
||||
AppState::Onboarding { .. } => {}
|
||||
},
|
||||
AppEvent::RateLimitFetchFailed { message } => match &mut self.app_state {
|
||||
AppState::Chat { widget } => widget.on_rate_limit_refresh_failed(message),
|
||||
AppState::Onboarding { .. } => {}
|
||||
},
|
||||
AppEvent::RequestRedraw => {
|
||||
self.schedule_redraw();
|
||||
}
|
||||
|
||||
@@ -192,6 +192,9 @@ pub(crate) enum AppEvent {
|
||||
/// follows previously rendered content.
|
||||
InsertBackgroundEventLate(String),
|
||||
|
||||
/// Background rate limit refresh failed (threaded request).
|
||||
RateLimitFetchFailed { message: String },
|
||||
|
||||
#[allow(dead_code)]
|
||||
StartCommitAnimation,
|
||||
#[allow(dead_code)]
|
||||
|
||||
@@ -42,6 +42,7 @@ mod interrupts;
|
||||
mod layout_scroll;
|
||||
mod message;
|
||||
mod perf;
|
||||
mod rate_limit_refresh;
|
||||
mod streaming;
|
||||
mod terminal_handlers;
|
||||
mod tools;
|
||||
@@ -51,6 +52,7 @@ use self::agent_install::{
|
||||
start_prompt_terminal_session,
|
||||
wrap_command,
|
||||
};
|
||||
use self::rate_limit_refresh::start_rate_limit_refresh;
|
||||
use self::history_render::HistoryRenderState;
|
||||
use codex_core::parse_command::ParsedCommand;
|
||||
use codex_core::protocol::AgentMessageDeltaEvent;
|
||||
@@ -221,6 +223,7 @@ struct RunningCommand {
|
||||
}
|
||||
|
||||
const RATE_LIMIT_WARNING_THRESHOLDS: [f64; 3] = [50.0, 75.0, 90.0];
|
||||
const RATE_LIMIT_REFRESH_INTERVAL: chrono::Duration = chrono::Duration::minutes(10);
|
||||
|
||||
const RATE_LIMIT_RESET_FILE: &str = "state/rate_limit_resets.json";
|
||||
|
||||
@@ -278,7 +281,7 @@ pub(crate) struct ChatWidget<'a> {
|
||||
rate_limit_warnings: RateLimitWarningState,
|
||||
rate_limit_fetch_inflight: bool,
|
||||
rate_limit_fetch_placeholder: Option<usize>,
|
||||
rate_limit_fetch_ack_pending: bool,
|
||||
rate_limit_last_fetch_at: Option<DateTime<Utc>>,
|
||||
rate_limit_last_primary_reset_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
rate_limit_last_weekly_reset_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
content_buffer: String,
|
||||
@@ -2094,7 +2097,7 @@ impl ChatWidget<'_> {
|
||||
rate_limit_warnings: RateLimitWarningState::default(),
|
||||
rate_limit_fetch_inflight: false,
|
||||
rate_limit_fetch_placeholder: None,
|
||||
rate_limit_fetch_ack_pending: false,
|
||||
rate_limit_last_fetch_at: None,
|
||||
rate_limit_last_primary_reset_at: primary_last_reset,
|
||||
rate_limit_last_weekly_reset_at: weekly_last_reset,
|
||||
content_buffer: String::new(),
|
||||
@@ -2298,7 +2301,7 @@ impl ChatWidget<'_> {
|
||||
rate_limit_warnings: RateLimitWarningState::default(),
|
||||
rate_limit_fetch_inflight: false,
|
||||
rate_limit_fetch_placeholder: None,
|
||||
rate_limit_fetch_ack_pending: false,
|
||||
rate_limit_last_fetch_at: None,
|
||||
rate_limit_last_primary_reset_at: primary_last_reset,
|
||||
rate_limit_last_weekly_reset_at: weekly_last_reset,
|
||||
content_buffer: String::new(),
|
||||
@@ -3792,14 +3795,6 @@ impl ChatWidget<'_> {
|
||||
tracing::debug!("Ignoring AgentMessage after interrupt");
|
||||
return;
|
||||
}
|
||||
if self.rate_limit_fetch_ack_pending && message.trim().eq_ignore_ascii_case("ok") {
|
||||
self.rate_limit_fetch_ack_pending = false;
|
||||
self.stream_state
|
||||
.closed_answer_ids
|
||||
.insert(StreamId(id.clone()));
|
||||
self.maybe_hide_spinner();
|
||||
return;
|
||||
}
|
||||
self.stream_state.seq_answer_final = Some(event.event_seq);
|
||||
// Strict order for the stream id
|
||||
let ok = match event.order.as_ref() {
|
||||
@@ -4264,8 +4259,8 @@ impl ChatWidget<'_> {
|
||||
reset_info,
|
||||
));
|
||||
}
|
||||
self.rate_limit_last_fetch_at = Some(Utc::now());
|
||||
self.rate_limit_fetch_inflight = false;
|
||||
self.rate_limit_fetch_ack_pending = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5131,18 +5126,31 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
|
||||
pub(crate) fn add_limits_output(&mut self) {
|
||||
if let Some(snapshot) = &self.rate_limit_snapshot {
|
||||
self.history_push(history_cell::new_limits_output(
|
||||
snapshot,
|
||||
self.rate_limit_reset_info(),
|
||||
));
|
||||
if let Some(snapshot) = self.rate_limit_snapshot.clone() {
|
||||
let reset_info = self.rate_limit_reset_info();
|
||||
let key = self.next_internal_key();
|
||||
let idx = self.history_insert_with_key_global_tagged(
|
||||
Box::new(history_cell::new_limits_output(&snapshot, reset_info)),
|
||||
key,
|
||||
"limits",
|
||||
);
|
||||
|
||||
if self.should_refresh_limits() {
|
||||
self.request_latest_rate_limits(Some(idx));
|
||||
}
|
||||
} else {
|
||||
self.request_latest_rate_limits();
|
||||
self.request_latest_rate_limits(None);
|
||||
}
|
||||
}
|
||||
|
||||
fn request_latest_rate_limits(&mut self) {
|
||||
if self.rate_limit_fetch_placeholder.is_none() {
|
||||
fn request_latest_rate_limits(&mut self, target_idx: Option<usize>) {
|
||||
if self.rate_limit_fetch_inflight {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(idx) = target_idx {
|
||||
self.rate_limit_fetch_placeholder = Some(idx);
|
||||
} else if self.rate_limit_fetch_placeholder.is_none() {
|
||||
let key = self.next_internal_key();
|
||||
let idx = self.history_insert_with_key_global_tagged(
|
||||
Box::new(history_cell::new_limits_fetching()),
|
||||
@@ -5152,23 +5160,44 @@ impl ChatWidget<'_> {
|
||||
self.rate_limit_fetch_placeholder = Some(idx);
|
||||
}
|
||||
|
||||
self.rate_limit_fetch_inflight = true;
|
||||
|
||||
start_rate_limit_refresh(
|
||||
self.app_event_tx.clone(),
|
||||
self.config.clone(),
|
||||
self.config.debug,
|
||||
);
|
||||
}
|
||||
|
||||
fn should_refresh_limits(&self) -> bool {
|
||||
if self.rate_limit_fetch_inflight {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
match self.rate_limit_last_fetch_at {
|
||||
Some(ts) => Utc::now() - ts > RATE_LIMIT_REFRESH_INTERVAL,
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn on_rate_limit_refresh_failed(&mut self, message: String) {
|
||||
if let Some(idx) = self.rate_limit_fetch_placeholder.take() {
|
||||
if self.rate_limit_snapshot.is_none() {
|
||||
if idx < self.history_cells.len() {
|
||||
self.history_replace_at(
|
||||
idx,
|
||||
Box::new(history_cell::new_error_event(message.clone())),
|
||||
);
|
||||
} else {
|
||||
self.history_push(history_cell::new_error_event(message.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.rate_limit_fetch_inflight = true;
|
||||
self.rate_limit_fetch_ack_pending = true;
|
||||
self.rate_limit_fetch_inflight = false;
|
||||
|
||||
use crate::chatwidget::message::UserMessage;
|
||||
use codex_core::protocol::InputItem;
|
||||
|
||||
let ping = UserMessage {
|
||||
display_text: String::new(),
|
||||
ordered_items: vec![InputItem::Text {
|
||||
text: "Yield immediately with only the message \"ok\"".to_string(),
|
||||
}],
|
||||
};
|
||||
self.submit_user_message(ping);
|
||||
if self.rate_limit_snapshot.is_some() {
|
||||
self.history_push(history_cell::new_warning_event(message));
|
||||
}
|
||||
}
|
||||
|
||||
fn rate_limit_reset_info(&self) -> RateLimitResetInfo {
|
||||
@@ -10719,6 +10748,7 @@ fn update_rate_limit_resets(
|
||||
self.total_token_usage = TokenUsage::default();
|
||||
self.rate_limit_snapshot = None;
|
||||
self.rate_limit_warnings.reset();
|
||||
self.rate_limit_last_fetch_at = None;
|
||||
self.bottom_pane.set_token_usage(
|
||||
self.total_token_usage.clone(),
|
||||
self.last_token_usage.clone(),
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use codex_core::{AuthManager, ModelClient, Prompt, ResponseEvent};
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config_types::ReasoningEffort;
|
||||
use codex_core::debug_logger::DebugLogger;
|
||||
use codex_core::protocol::{Event, EventMsg, TokenCountEvent, RateLimitSnapshotEvent as CoreRateLimitSnapshotEvent};
|
||||
use codex_protocol::models::{ContentItem, ResponseItem};
|
||||
use futures::StreamExt;
|
||||
use tokio::runtime::Runtime;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
|
||||
/// Fire-and-forget helper that refreshes rate limit data using a dedicated model
|
||||
/// request. Results are funneled back into the main TUI loop via `AppEvent` so
|
||||
/// history ordering stays consistent.
|
||||
pub(super) fn start_rate_limit_refresh(
|
||||
app_event_tx: AppEventSender,
|
||||
config: Config,
|
||||
debug_enabled: bool,
|
||||
) {
|
||||
std::thread::spawn(move || {
|
||||
if let Err(err) = run_refresh(app_event_tx.clone(), config, debug_enabled) {
|
||||
let message = format!("Failed to refresh rate limits: {err}");
|
||||
app_event_tx.send(AppEvent::RateLimitFetchFailed { message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn run_refresh(
|
||||
app_event_tx: AppEventSender,
|
||||
config: Config,
|
||||
debug_enabled: bool,
|
||||
) -> Result<()> {
|
||||
let runtime = build_runtime()?;
|
||||
runtime.block_on(async move {
|
||||
let auth_mode = if config.using_chatgpt_auth {
|
||||
codex_protocol::mcp_protocol::AuthMode::ChatGPT
|
||||
} else {
|
||||
codex_protocol::mcp_protocol::AuthMode::ApiKey
|
||||
};
|
||||
|
||||
let auth_mgr = AuthManager::shared(
|
||||
config.codex_home.clone(),
|
||||
auth_mode,
|
||||
config.responses_originator_header.clone(),
|
||||
);
|
||||
|
||||
let client = build_model_client(&config, auth_mgr, debug_enabled)?;
|
||||
|
||||
let mut prompt = Prompt::default();
|
||||
prompt.store = false;
|
||||
prompt.input.push(ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "Yield immediately with only the message \"ok\"".to_string(),
|
||||
}],
|
||||
});
|
||||
|
||||
let mut stream = client
|
||||
.stream(&prompt)
|
||||
.await
|
||||
.context("requesting rate limit snapshot")?;
|
||||
|
||||
let mut snapshot = None;
|
||||
while let Some(event) = stream.next().await {
|
||||
match event? {
|
||||
ResponseEvent::RateLimits(s) => {
|
||||
snapshot = Some(s);
|
||||
break;
|
||||
}
|
||||
ResponseEvent::Completed { .. } => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let proto_snapshot = snapshot.context("rate limit snapshot missing from response")?;
|
||||
|
||||
let snapshot = CoreRateLimitSnapshotEvent {
|
||||
primary_used_percent: proto_snapshot.primary_used_percent,
|
||||
weekly_used_percent: proto_snapshot.weekly_used_percent,
|
||||
primary_to_weekly_ratio_percent: proto_snapshot.primary_to_weekly_ratio_percent,
|
||||
primary_window_minutes: proto_snapshot.primary_window_minutes,
|
||||
weekly_window_minutes: proto_snapshot.weekly_window_minutes,
|
||||
};
|
||||
|
||||
let event = Event {
|
||||
id: "rate-limit-refresh".to_string(),
|
||||
event_seq: 0,
|
||||
msg: EventMsg::TokenCount(TokenCountEvent {
|
||||
info: None,
|
||||
rate_limits: Some(snapshot),
|
||||
}),
|
||||
order: None,
|
||||
};
|
||||
|
||||
app_event_tx.send(AppEvent::CodexEvent(event));
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn build_runtime() -> Result<Runtime> {
|
||||
Ok(
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.context("building rate limit refresh runtime")?,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_model_client(
|
||||
config: &Config,
|
||||
auth_mgr: Arc<AuthManager>,
|
||||
debug_enabled: bool,
|
||||
) -> Result<ModelClient> {
|
||||
let debug_logger = DebugLogger::new(debug_enabled)
|
||||
.or_else(|_| DebugLogger::new(false))
|
||||
.context("initializing debug logger")?;
|
||||
|
||||
let client = ModelClient::new(
|
||||
Arc::new(config.clone()),
|
||||
Some(auth_mgr),
|
||||
config.model_provider.clone(),
|
||||
ReasoningEffort::Low,
|
||||
config.model_reasoning_summary,
|
||||
config.model_text_verbosity,
|
||||
Uuid::new_v4(),
|
||||
Arc::new(Mutex::new(debug_logger)),
|
||||
);
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
Reference in New Issue
Block a user