mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
fix(tui/limits): persist rate-limit warning logs
This commit is contained in:
@@ -13,6 +13,22 @@ use crate::protocol::TokenUsage;
|
||||
const USAGE_VERSION: u32 = 1;
|
||||
const USAGE_SUBDIR: &str = "usage";
|
||||
const HOURLY_HISTORY_DAYS: i64 = 7;
|
||||
const UNKNOWN_RESET_RELOG_INTERVAL: Duration = Duration::hours(24);
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum RateLimitWarningScope {
|
||||
Primary,
|
||||
Secondary,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
struct RateLimitWarningRecord {
|
||||
threshold: f64,
|
||||
#[serde(default)]
|
||||
reset_at: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
logged_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct TokenTotals {
|
||||
@@ -78,6 +94,10 @@ struct RateLimitInfo {
|
||||
secondary_next_reset_at: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
last_usage_limit_hit_at: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
primary_threshold_logs: Vec<RateLimitWarningRecord>,
|
||||
#[serde(default)]
|
||||
secondary_threshold_logs: Vec<RateLimitWarningRecord>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -167,6 +187,10 @@ fn usage_dir(codex_home: &Path) -> PathBuf {
|
||||
codex_home.join(USAGE_SUBDIR)
|
||||
}
|
||||
|
||||
fn warning_log_path(codex_home: &Path) -> PathBuf {
|
||||
usage_dir(codex_home).join("rate_limit_warnings.log")
|
||||
}
|
||||
|
||||
fn usage_file_path(codex_home: &Path, account_id: &str) -> PathBuf {
|
||||
usage_dir(codex_home).join(format!("{account_id}.json"))
|
||||
}
|
||||
@@ -351,6 +375,136 @@ pub fn record_usage_limit_hint(
|
||||
})
|
||||
}
|
||||
|
||||
fn reset_changed(prev: Option<DateTime<Utc>>, next: Option<DateTime<Utc>>) -> bool {
|
||||
match (prev, next) {
|
||||
(Some(a), Some(b)) => a
|
||||
.signed_duration_since(b)
|
||||
.num_seconds()
|
||||
.abs()
|
||||
> 5,
|
||||
(None, None) => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn record_threshold_log(
|
||||
logs: &mut Vec<RateLimitWarningRecord>,
|
||||
threshold: f64,
|
||||
reset_at: Option<DateTime<Utc>>,
|
||||
observed_at: DateTime<Utc>,
|
||||
) -> bool {
|
||||
if let Some(existing) = logs.iter_mut().find(|entry| {
|
||||
(entry.threshold - threshold).abs() < f64::EPSILON
|
||||
}) {
|
||||
if reset_changed(existing.reset_at, reset_at) {
|
||||
existing.logged_at = None;
|
||||
} else if reset_at.is_none()
|
||||
&& existing
|
||||
.logged_at
|
||||
.is_some_and(|logged| observed_at.signed_duration_since(logged) >= UNKNOWN_RESET_RELOG_INTERVAL)
|
||||
{
|
||||
existing.logged_at = None;
|
||||
}
|
||||
existing.reset_at = reset_at;
|
||||
if existing.logged_at.is_none() {
|
||||
existing.logged_at = Some(observed_at);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
logs.push(RateLimitWarningRecord {
|
||||
threshold,
|
||||
reset_at,
|
||||
logged_at: Some(observed_at),
|
||||
});
|
||||
true
|
||||
}
|
||||
|
||||
fn append_rate_limit_warning_log(
|
||||
codex_home: &Path,
|
||||
account_id: &str,
|
||||
plan: Option<&str>,
|
||||
scope: RateLimitWarningScope,
|
||||
threshold: f64,
|
||||
reset_at: Option<DateTime<Utc>>,
|
||||
observed_at: DateTime<Utc>,
|
||||
message: &str,
|
||||
) -> std::io::Result<()> {
|
||||
let dir = usage_dir(codex_home);
|
||||
fs::create_dir_all(&dir)?;
|
||||
let path = warning_log_path(codex_home);
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.read(true)
|
||||
.open(&path)?;
|
||||
file.lock_exclusive()?;
|
||||
let scope_field = match scope {
|
||||
RateLimitWarningScope::Primary => "primary",
|
||||
RateLimitWarningScope::Secondary => "secondary",
|
||||
};
|
||||
let plan_field = plan.unwrap_or("-");
|
||||
let reset_field = reset_at
|
||||
.map(|dt| dt.to_rfc3339())
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
let line = format!(
|
||||
"{}\t{}\t{}\t{:.0}\t{}\t{}\t{}\n",
|
||||
observed_at.to_rfc3339(),
|
||||
account_id,
|
||||
plan_field,
|
||||
threshold,
|
||||
scope_field,
|
||||
reset_field,
|
||||
message,
|
||||
);
|
||||
let write_res = file.write_all(line.as_bytes());
|
||||
let unlock_res = file.unlock();
|
||||
write_res?;
|
||||
unlock_res?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn record_rate_limit_warning(
|
||||
codex_home: &Path,
|
||||
account_id: &str,
|
||||
plan: Option<&str>,
|
||||
scope: RateLimitWarningScope,
|
||||
threshold: f64,
|
||||
reset_at: Option<DateTime<Utc>>,
|
||||
observed_at: DateTime<Utc>,
|
||||
message: &str,
|
||||
) -> std::io::Result<bool> {
|
||||
let mut should_log = false;
|
||||
with_usage_file(codex_home, account_id, plan, |data| {
|
||||
data.last_updated = observed_at;
|
||||
let mut info = data.rate_limit.take().unwrap_or_default();
|
||||
let logs = match scope {
|
||||
RateLimitWarningScope::Primary => &mut info.primary_threshold_logs,
|
||||
RateLimitWarningScope::Secondary => &mut info.secondary_threshold_logs,
|
||||
};
|
||||
if record_threshold_log(logs, threshold, reset_at, observed_at) {
|
||||
should_log = true;
|
||||
}
|
||||
data.rate_limit = Some(info);
|
||||
})?;
|
||||
|
||||
if should_log {
|
||||
append_rate_limit_warning_log(
|
||||
codex_home,
|
||||
account_id,
|
||||
plan,
|
||||
scope,
|
||||
threshold,
|
||||
reset_at,
|
||||
observed_at,
|
||||
message,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(should_log)
|
||||
}
|
||||
|
||||
pub fn load_account_usage(
|
||||
codex_home: &Path,
|
||||
account_id: &str,
|
||||
|
||||
@@ -28,7 +28,12 @@ use codex_core::config_types::TextVerbosity;
|
||||
use codex_core::plan_tool::{PlanItemArg, StepStatus, UpdatePlanArgs};
|
||||
use codex_core::model_family::derive_default_model_family;
|
||||
use codex_core::model_family::find_family_for_model;
|
||||
use codex_core::account_usage::{self, StoredRateLimitSnapshot, StoredUsageSummary};
|
||||
use codex_core::account_usage::{
|
||||
self,
|
||||
RateLimitWarningScope,
|
||||
StoredRateLimitSnapshot,
|
||||
StoredUsageSummary,
|
||||
};
|
||||
use codex_core::auth_accounts::{self, StoredAccount};
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::AuthMode;
|
||||
@@ -273,6 +278,13 @@ const RATE_LIMIT_REFRESH_INTERVAL: chrono::Duration = chrono::Duration::minutes(
|
||||
|
||||
const MAX_TRACKED_GHOST_COMMITS: usize = 20;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RateLimitWarning {
|
||||
scope: RateLimitWarningScope,
|
||||
threshold: f64,
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct RateLimitWarningState {
|
||||
weekly_index: usize,
|
||||
@@ -280,26 +292,38 @@ struct RateLimitWarningState {
|
||||
}
|
||||
|
||||
impl RateLimitWarningState {
|
||||
fn take_warnings(&mut self, weekly_used_percent: f64, hourly_used_percent: f64) -> Vec<String> {
|
||||
fn take_warnings(
|
||||
&mut self,
|
||||
secondary_used_percent: f64,
|
||||
primary_used_percent: f64,
|
||||
) -> Vec<RateLimitWarning> {
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
while self.weekly_index < RATE_LIMIT_WARNING_THRESHOLDS.len()
|
||||
&& weekly_used_percent >= RATE_LIMIT_WARNING_THRESHOLDS[self.weekly_index]
|
||||
&& secondary_used_percent >= RATE_LIMIT_WARNING_THRESHOLDS[self.weekly_index]
|
||||
{
|
||||
let threshold = RATE_LIMIT_WARNING_THRESHOLDS[self.weekly_index];
|
||||
warnings.push(format!(
|
||||
"Secondary usage exceeded {threshold:.0}% of the limit. Run /limits for detailed usage."
|
||||
));
|
||||
warnings.push(RateLimitWarning {
|
||||
scope: RateLimitWarningScope::Secondary,
|
||||
threshold,
|
||||
message: format!(
|
||||
"Secondary 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]
|
||||
&& primary_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."
|
||||
));
|
||||
warnings.push(RateLimitWarning {
|
||||
scope: RateLimitWarningScope::Primary,
|
||||
threshold,
|
||||
message: format!(
|
||||
"Hourly usage exceeded {threshold:.0}% of the limit. Run /limits for detailed usage."
|
||||
),
|
||||
});
|
||||
self.hourly_index += 1;
|
||||
}
|
||||
|
||||
@@ -5873,10 +5897,16 @@ impl ChatWidget<'_> {
|
||||
let warnings = self
|
||||
.rate_limit_warnings
|
||||
.take_warnings(snapshot.secondary_used_percent, snapshot.primary_used_percent);
|
||||
if !warnings.is_empty() {
|
||||
for warning in warnings {
|
||||
self.history_push(history_cell::new_warning_event(warning));
|
||||
let mut displayed_warning = false;
|
||||
for warning in warnings {
|
||||
if self.log_and_should_display_warning(&warning) {
|
||||
self.history_push(history_cell::new_warning_event(
|
||||
warning.message.clone(),
|
||||
));
|
||||
displayed_warning = true;
|
||||
}
|
||||
}
|
||||
if displayed_warning {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
@@ -13303,6 +13333,50 @@ impl ChatWidget<'_> {
|
||||
);
|
||||
}
|
||||
|
||||
fn log_and_should_display_warning(&self, warning: &RateLimitWarning) -> bool {
|
||||
let reset_at = match warning.scope {
|
||||
RateLimitWarningScope::Primary => self.rate_limit_primary_next_reset_at,
|
||||
RateLimitWarningScope::Secondary => self.rate_limit_secondary_next_reset_at,
|
||||
};
|
||||
|
||||
let account_id = auth_accounts::get_active_account_id(&self.config.codex_home)
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| "_default".to_string());
|
||||
|
||||
let plan = if account_id == "_default" {
|
||||
None
|
||||
} else {
|
||||
match account_usage::list_rate_limit_snapshots(&self.config.codex_home) {
|
||||
Ok(records) => records
|
||||
.into_iter()
|
||||
.find(|record| record.account_id == account_id)
|
||||
.and_then(|record| record.plan.clone()),
|
||||
Err(err) => {
|
||||
tracing::warn!(?err, "failed to load rate limit snapshots while logging warning");
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match account_usage::record_rate_limit_warning(
|
||||
&self.config.codex_home,
|
||||
&account_id,
|
||||
plan.as_deref(),
|
||||
warning.scope,
|
||||
warning.threshold,
|
||||
reset_at,
|
||||
Utc::now(),
|
||||
&warning.message,
|
||||
) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
tracing::warn!(?err, "failed to persist rate limit warning log");
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Export transcript for buffer-mode mirroring: omit internal sentinels
|
||||
/// and include gutter icons and a blank line between items for readability.
|
||||
pub(crate) fn export_transcript_lines_for_buffer(&self) -> Vec<ratatui::text::Line<'static>> {
|
||||
|
||||
Reference in New Issue
Block a user