mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:23:33 +00:00
feat(core,tui): surface rate limit snapshots in token events
This commit is contained in:
@@ -551,6 +551,11 @@ impl ModelClient {
|
||||
self.config.model_family.clone()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_model_context_window(&self) -> Option<u64> {
|
||||
self.config.model_context_window
|
||||
}
|
||||
|
||||
// duplicate of earlier helpers removed during merge cleanup
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -599,9 +604,15 @@ impl From<ResponseCompletedUsage> for TokenUsage {
|
||||
fn from(val: ResponseCompletedUsage) -> Self {
|
||||
TokenUsage {
|
||||
input_tokens: val.input_tokens,
|
||||
cached_input_tokens: val.input_tokens_details.map(|d| d.cached_tokens),
|
||||
cached_input_tokens: val
|
||||
.input_tokens_details
|
||||
.map(|d| d.cached_tokens)
|
||||
.unwrap_or(0),
|
||||
output_tokens: val.output_tokens,
|
||||
reasoning_output_tokens: val.output_tokens_details.map(|d| d.reasoning_tokens),
|
||||
reasoning_output_tokens: val
|
||||
.output_tokens_details
|
||||
.map(|d| d.reasoning_tokens)
|
||||
.unwrap_or(0),
|
||||
total_tokens: val.total_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,6 +518,9 @@ use crate::protocol::Op;
|
||||
use crate::protocol::PatchApplyBeginEvent;
|
||||
use crate::protocol::PatchApplyEndEvent;
|
||||
use crate::protocol::RecordedEvent;
|
||||
use crate::protocol::RateLimitSnapshotEvent;
|
||||
use crate::protocol::TokenCountEvent;
|
||||
use crate::protocol::TokenUsageInfo;
|
||||
use crate::protocol::ReviewDecision;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::protocol::SessionConfiguredEvent;
|
||||
@@ -748,6 +751,8 @@ struct State {
|
||||
/// Background execs by call_id
|
||||
background_execs: std::collections::HashMap<String, BackgroundExecState>,
|
||||
next_internal_sub_id: u64,
|
||||
token_usage_info: Option<TokenUsageInfo>,
|
||||
latest_rate_limits: Option<RateLimitSnapshotEvent>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -3790,9 +3795,29 @@ async fn try_run_turn(
|
||||
response_id: _,
|
||||
token_usage,
|
||||
} => {
|
||||
if let Some(token_usage) = token_usage {
|
||||
let (new_info, rate_limits, should_emit);
|
||||
{
|
||||
let mut state = sess.state.lock().unwrap();
|
||||
let info = TokenUsageInfo::new_or_append(
|
||||
&state.token_usage_info,
|
||||
&token_usage,
|
||||
sess.client.get_model_context_window(),
|
||||
);
|
||||
let limits = state.latest_rate_limits.clone();
|
||||
let emit = info.is_some() || limits.is_some();
|
||||
state.token_usage_info = info.clone();
|
||||
new_info = info;
|
||||
rate_limits = limits;
|
||||
should_emit = emit;
|
||||
}
|
||||
|
||||
if should_emit {
|
||||
let payload = TokenCountEvent {
|
||||
info: new_info,
|
||||
rate_limits,
|
||||
};
|
||||
sess.tx_event
|
||||
.send(sess.make_event(&sub_id, EventMsg::TokenCount(token_usage)))
|
||||
.send(sess.make_event(&sub_id, EventMsg::TokenCount(payload)))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
@@ -3850,8 +3875,15 @@ async fn try_run_turn(
|
||||
sess.tx_event.send(stamped).await.ok();
|
||||
}
|
||||
}
|
||||
ResponseEvent::RateLimits(_rl) => {
|
||||
// Upstream emits periodic rate limit snapshots. No-op for now.
|
||||
ResponseEvent::RateLimits(snapshot) => {
|
||||
let mut state = sess.state.lock().unwrap();
|
||||
state.latest_rate_limits = Some(RateLimitSnapshotEvent {
|
||||
primary_used_percent: snapshot.primary_used_percent,
|
||||
weekly_used_percent: snapshot.weekly_used_percent,
|
||||
primary_to_weekly_ratio_percent: snapshot.primary_to_weekly_ratio_percent,
|
||||
primary_window_minutes: snapshot.primary_window_minutes,
|
||||
weekly_window_minutes: snapshot.weekly_window_minutes,
|
||||
});
|
||||
}
|
||||
// Note: ReasoningSummaryPartAdded handled above without scratchpad mutation.
|
||||
}
|
||||
|
||||
+100
-10
@@ -560,8 +560,8 @@ pub enum EventMsg {
|
||||
TaskComplete(TaskCompleteEvent),
|
||||
|
||||
/// Token count event, sent periodically to report the number of tokens
|
||||
/// used in the current session.
|
||||
TokenCount(TokenUsage),
|
||||
/// used in the current session and the latest rate limit snapshot.
|
||||
TokenCount(TokenCountEvent),
|
||||
|
||||
/// Agent text output message
|
||||
AgentMessage(AgentMessageEvent),
|
||||
@@ -672,9 +672,9 @@ pub struct TaskCompleteEvent {
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
pub struct TokenUsage {
|
||||
pub input_tokens: u64,
|
||||
pub cached_input_tokens: Option<u64>,
|
||||
pub cached_input_tokens: u64,
|
||||
pub output_tokens: u64,
|
||||
pub reasoning_output_tokens: Option<u64>,
|
||||
pub reasoning_output_tokens: u64,
|
||||
pub total_tokens: u64,
|
||||
}
|
||||
|
||||
@@ -684,7 +684,7 @@ impl TokenUsage {
|
||||
}
|
||||
|
||||
pub fn cached_input(&self) -> u64 {
|
||||
self.cached_input_tokens.unwrap_or(0)
|
||||
self.cached_input_tokens
|
||||
}
|
||||
|
||||
pub fn non_cached_input(&self) -> u64 {
|
||||
@@ -702,8 +702,97 @@ impl TokenUsage {
|
||||
/// This will be off for the current turn and pending function calls.
|
||||
pub fn tokens_in_context_window(&self) -> u64 {
|
||||
self.total_tokens
|
||||
.saturating_sub(self.reasoning_output_tokens.unwrap_or(0))
|
||||
.saturating_sub(self.reasoning_output_tokens)
|
||||
}
|
||||
|
||||
/// Estimate the remaining user-controllable percentage of the model's context window.
|
||||
pub fn percent_of_context_window_remaining(&self, context_window: u64) -> u8 {
|
||||
if context_window <= BASELINE_TOKENS {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let effective_window = context_window - BASELINE_TOKENS;
|
||||
let used = self
|
||||
.tokens_in_context_window()
|
||||
.saturating_sub(BASELINE_TOKENS);
|
||||
let remaining = effective_window.saturating_sub(used);
|
||||
((remaining as f32 / effective_window as f32) * 100.0).clamp(0.0, 100.0) as u8
|
||||
}
|
||||
|
||||
/// In-place element-wise sum of token counts.
|
||||
pub fn add_assign(&mut self, other: &TokenUsage) {
|
||||
self.input_tokens += other.input_tokens;
|
||||
self.cached_input_tokens += other.cached_input_tokens;
|
||||
self.output_tokens += other.output_tokens;
|
||||
self.reasoning_output_tokens += other.reasoning_output_tokens;
|
||||
self.total_tokens += other.total_tokens;
|
||||
}
|
||||
}
|
||||
|
||||
/// Includes prompts, tools and space to call compact.
|
||||
const BASELINE_TOKENS: u64 = 12_000;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
pub struct TokenUsageInfo {
|
||||
pub total_token_usage: TokenUsage,
|
||||
pub last_token_usage: TokenUsage,
|
||||
pub model_context_window: Option<u64>,
|
||||
}
|
||||
|
||||
impl TokenUsageInfo {
|
||||
pub fn new_or_append(
|
||||
info: &Option<TokenUsageInfo>,
|
||||
last: &Option<TokenUsage>,
|
||||
model_context_window: Option<u64>,
|
||||
) -> Option<Self> {
|
||||
if info.is_none() && last.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut info = match info {
|
||||
Some(info) => info.clone(),
|
||||
None => Self {
|
||||
total_token_usage: TokenUsage::default(),
|
||||
last_token_usage: TokenUsage::default(),
|
||||
model_context_window,
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(last) = last {
|
||||
info.append_last_usage(last);
|
||||
}
|
||||
|
||||
if info.model_context_window.is_none() {
|
||||
info.model_context_window = model_context_window;
|
||||
}
|
||||
|
||||
Some(info)
|
||||
}
|
||||
|
||||
pub fn append_last_usage(&mut self, last: &TokenUsage) {
|
||||
self.total_token_usage.add_assign(last);
|
||||
self.last_token_usage = last.clone();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct RateLimitSnapshotEvent {
|
||||
/// Percentage (0-100) of the primary window that has been consumed.
|
||||
pub primary_used_percent: f64,
|
||||
/// Percentage (0-100) of the protection window that has been consumed.
|
||||
pub weekly_used_percent: f64,
|
||||
/// Size of the primary window relative to weekly (0-100).
|
||||
pub primary_to_weekly_ratio_percent: f64,
|
||||
/// Rolling window duration for the primary limit, in minutes.
|
||||
pub primary_window_minutes: u64,
|
||||
/// Rolling window duration for the weekly limit, in minutes.
|
||||
pub weekly_window_minutes: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct TokenCountEvent {
|
||||
pub info: Option<TokenUsageInfo>,
|
||||
pub rate_limits: Option<RateLimitSnapshotEvent>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
@@ -756,10 +845,11 @@ impl fmt::Display for FinalOutput {
|
||||
String::new()
|
||||
},
|
||||
token_usage.output_tokens,
|
||||
token_usage
|
||||
.reasoning_output_tokens
|
||||
.map(|r| format!(" (reasoning {r})"))
|
||||
.unwrap_or_default()
|
||||
if token_usage.reasoning_output_tokens > 0 {
|
||||
format!(" (reasoning {})", token_usage.reasoning_output_tokens)
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,12 +284,21 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
return CodexStatus::InitiateShutdown;
|
||||
}
|
||||
EventMsg::TokenCount(ev) => {
|
||||
// Our fork carries TokenUsage directly
|
||||
ts_println!(
|
||||
self,
|
||||
"tokens used: {}",
|
||||
format_with_separators(ev.blended_total())
|
||||
);
|
||||
if let Some(info) = &ev.info {
|
||||
ts_println!(
|
||||
self,
|
||||
"tokens used: {}",
|
||||
format_with_separators(info.total_token_usage.blended_total())
|
||||
);
|
||||
}
|
||||
if let Some(snapshot) = &ev.rate_limits {
|
||||
ts_println!(
|
||||
self,
|
||||
"rate limits: hourly {:.1}% • weekly {:.1}%",
|
||||
snapshot.primary_used_percent,
|
||||
snapshot.weekly_used_percent
|
||||
);
|
||||
}
|
||||
}
|
||||
EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta }) => {
|
||||
if !self.answer_started {
|
||||
|
||||
@@ -480,7 +480,7 @@ impl ChatComposer {
|
||||
.token_usage_info
|
||||
.as_ref()
|
||||
.map(|info| info.initial_prompt_tokens)
|
||||
.unwrap_or_else(|| last_token_usage.cached_input_tokens.unwrap_or(0));
|
||||
.unwrap_or_else(|| last_token_usage.cached_input_tokens);
|
||||
|
||||
self.token_usage_info = Some(TokenUsageInfo {
|
||||
_total_token_usage: total_token_usage,
|
||||
|
||||
@@ -151,6 +151,7 @@ use codex_file_search::FileMatch;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::plan_tool::StepStatus;
|
||||
use codex_core::protocol::RateLimitSnapshotEvent;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyModifiers;
|
||||
use ratatui::style::Stylize;
|
||||
@@ -225,6 +226,7 @@ pub(crate) struct ChatWidget<'a> {
|
||||
initial_user_message: Option<UserMessage>,
|
||||
total_token_usage: TokenUsage,
|
||||
last_token_usage: TokenUsage,
|
||||
latest_rate_limits: Option<RateLimitSnapshotEvent>,
|
||||
content_buffer: String,
|
||||
// Buffer for streaming assistant answer text; we do not surface partial
|
||||
// We wait for the final AgentMessage event and then emit the full text
|
||||
@@ -2028,6 +2030,7 @@ impl ChatWidget<'_> {
|
||||
),
|
||||
total_token_usage: TokenUsage::default(),
|
||||
last_token_usage: TokenUsage::default(),
|
||||
latest_rate_limits: None,
|
||||
content_buffer: String::new(),
|
||||
last_assistant_message: None,
|
||||
exec: ExecState {
|
||||
@@ -2219,6 +2222,7 @@ impl ChatWidget<'_> {
|
||||
initial_user_message: None,
|
||||
total_token_usage: TokenUsage::default(),
|
||||
last_token_usage: TokenUsage::default(),
|
||||
latest_rate_limits: None,
|
||||
content_buffer: String::new(),
|
||||
last_assistant_message: None,
|
||||
exec: ExecState {
|
||||
@@ -4132,9 +4136,14 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
self.mark_needs_redraw();
|
||||
}
|
||||
EventMsg::TokenCount(token_usage) => {
|
||||
self.total_token_usage = add_token_usage(&self.total_token_usage, &token_usage);
|
||||
self.last_token_usage = token_usage;
|
||||
EventMsg::TokenCount(event) => {
|
||||
if let Some(info) = &event.info {
|
||||
self.total_token_usage = info.total_token_usage.clone();
|
||||
self.last_token_usage = info.last_token_usage.clone();
|
||||
}
|
||||
if let Some(snapshot) = event.rate_limits {
|
||||
self.latest_rate_limits = Some(snapshot);
|
||||
}
|
||||
self.bottom_pane.set_token_usage(
|
||||
self.total_token_usage.clone(),
|
||||
self.last_token_usage.clone(),
|
||||
@@ -14465,34 +14474,6 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn add_token_usage(current_usage: &TokenUsage, new_usage: &TokenUsage) -> TokenUsage {
|
||||
let cached_input_tokens = match (
|
||||
current_usage.cached_input_tokens,
|
||||
new_usage.cached_input_tokens,
|
||||
) {
|
||||
(Some(current), Some(new)) => Some(current + new),
|
||||
(Some(current), None) => Some(current),
|
||||
(None, Some(new)) => Some(new),
|
||||
(None, None) => None,
|
||||
};
|
||||
let reasoning_output_tokens = match (
|
||||
current_usage.reasoning_output_tokens,
|
||||
new_usage.reasoning_output_tokens,
|
||||
) {
|
||||
(Some(current), Some(new)) => Some(current + new),
|
||||
(Some(current), None) => Some(current),
|
||||
(None, Some(new)) => Some(new),
|
||||
(None, None) => None,
|
||||
};
|
||||
TokenUsage {
|
||||
input_tokens: current_usage.input_tokens + new_usage.input_tokens,
|
||||
cached_input_tokens,
|
||||
output_tokens: current_usage.output_tokens + new_usage.output_tokens,
|
||||
reasoning_output_tokens,
|
||||
total_tokens: current_usage.total_tokens + new_usage.total_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
// Coalesce adjacent Read entries of the same file with contiguous ranges in a rendered lines vector.
|
||||
// Expects the vector to contain a header line at index 0 (e.g., "Read"). Modifies in place.
|
||||
#[allow(dead_code)]
|
||||
|
||||
@@ -131,6 +131,7 @@ fn make_chatwidget_manual() -> (
|
||||
initial_user_message: None,
|
||||
total_token_usage: TokenUsage::default(),
|
||||
last_token_usage: TokenUsage::default(),
|
||||
latest_rate_limits: None,
|
||||
stream: StreamController::new(cfg),
|
||||
last_stream_kind: None,
|
||||
running_commands: HashMap::new(),
|
||||
|
||||
@@ -9408,10 +9408,14 @@ pub(crate) fn new_status_output(
|
||||
" • Input: ".into(),
|
||||
format_with_separators(last_usage.non_cached_input()).into(),
|
||||
];
|
||||
if let Some(cached) = last_usage.cached_input_tokens {
|
||||
if cached > 0 {
|
||||
input_line_spans.push(format!(" (+ {} cached)", format_with_separators(cached)).into());
|
||||
}
|
||||
if last_usage.cached_input_tokens > 0 {
|
||||
input_line_spans.push(
|
||||
format!(
|
||||
" (+ {} cached)",
|
||||
format_with_separators(last_usage.cached_input_tokens)
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
lines.push(Line::from(input_line_spans));
|
||||
// Output: <output>
|
||||
|
||||
Reference in New Issue
Block a user