mirror of
https://github.com/openai/codex.git
synced 2026-11-14 21:40:39 +00:00
Expose token usage totals in statusline state (#61408)
## What changed Add `thread.usage.total` to the provider-facing statusline snapshot with total input, cache-read input, cache-write input, other input, and output token counts. Preserve cache-write counts when converting app-server token reports for the TUI. Omit totals when usage is unavailable, the breakdown is invalid, or the report represents a synthetic context fill. Keep measured zero totals. ## Testing Add coverage for the reported token breakdown, cache counts exceeding total input, synthetic context fills, and measured zero usage. GitOrigin-RevId: 6c506af30d6e51a49a780a0316304ef9c067fe43
This commit is contained in:
@@ -1211,6 +1211,7 @@ async fn active_thread_drain_yields_after_frame_deadline_without_dropping_events
|
||||
crate::token_usage::TokenUsage {
|
||||
input_tokens: 4,
|
||||
cached_input_tokens: 1,
|
||||
cache_write_input_tokens: 0,
|
||||
output_tokens: 5,
|
||||
reasoning_output_tokens: 0,
|
||||
total_tokens: 10,
|
||||
@@ -1239,6 +1240,7 @@ async fn active_thread_drain_yields_after_frame_deadline_without_dropping_events
|
||||
crate::token_usage::TokenUsage {
|
||||
input_tokens: 4,
|
||||
cached_input_tokens: 1,
|
||||
cache_write_input_tokens: 0,
|
||||
output_tokens: 10,
|
||||
reasoning_output_tokens: 0,
|
||||
total_tokens: 15,
|
||||
|
||||
@@ -953,20 +953,8 @@ fn request_permissions_from_params(
|
||||
|
||||
fn token_usage_info_from_app_server(token_usage: ThreadTokenUsage) -> TokenUsageInfo {
|
||||
TokenUsageInfo {
|
||||
total_token_usage: TokenUsage {
|
||||
total_tokens: token_usage.total.total_tokens,
|
||||
input_tokens: token_usage.total.input_tokens,
|
||||
cached_input_tokens: token_usage.total.cached_input_tokens,
|
||||
output_tokens: token_usage.total.output_tokens,
|
||||
reasoning_output_tokens: token_usage.total.reasoning_output_tokens,
|
||||
},
|
||||
last_token_usage: TokenUsage {
|
||||
total_tokens: token_usage.last.total_tokens,
|
||||
input_tokens: token_usage.last.input_tokens,
|
||||
cached_input_tokens: token_usage.last.cached_input_tokens,
|
||||
output_tokens: token_usage.last.output_tokens,
|
||||
reasoning_output_tokens: token_usage.last.reasoning_output_tokens,
|
||||
},
|
||||
total_token_usage: token_usage.total.into(),
|
||||
last_token_usage: token_usage.last.into(),
|
||||
model_context_window: token_usage.model_context_window,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! Projects the displayed conversation into the provider-facing statusline snapshot.
|
||||
|
||||
mod usage;
|
||||
|
||||
use super::ChatWidget;
|
||||
use super::normalize_thread_name;
|
||||
use super::status_state::TerminalTitleStatusKind as TerminalTitleStatus;
|
||||
@@ -44,6 +46,7 @@ struct StatuslineStateThread {
|
||||
agent: StatuslineStateAgent,
|
||||
model: StatuslineStateModel,
|
||||
workspace: StatuslineStateWorkspace,
|
||||
usage: usage::Usage,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
approval: Option<StatuslineStateApproval>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -248,6 +251,7 @@ impl ChatWidget {
|
||||
name,
|
||||
agent,
|
||||
model,
|
||||
usage: self.into(),
|
||||
workspace: StatuslineStateWorkspace {
|
||||
cwd: codex_utils_path_uri::LegacyAppPathString::from_path(
|
||||
self.status_line_workspace_cwd(),
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
//! Provider-facing usage comes from the same token report as the built-in footer.
|
||||
|
||||
use super::ChatWidget;
|
||||
use crate::token_usage::TokenUsage;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(super) struct Usage {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
total: Option<Counts>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Counts {
|
||||
total_input_tokens: i64,
|
||||
cache_read_input_tokens: i64,
|
||||
cache_write_input_tokens: i64,
|
||||
other_input_tokens: i64,
|
||||
output_tokens: i64,
|
||||
}
|
||||
|
||||
impl Counts {
|
||||
fn reported(usage: &TokenUsage) -> Option<Self> {
|
||||
let other_input_tokens = usage
|
||||
.input_tokens
|
||||
.checked_sub(usage.cached_input_tokens)?
|
||||
.checked_sub(usage.cache_write_input_tokens)?;
|
||||
if [
|
||||
other_input_tokens,
|
||||
usage.cached_input_tokens,
|
||||
usage.cache_write_input_tokens,
|
||||
usage.output_tokens,
|
||||
]
|
||||
.into_iter()
|
||||
.any(|count| count < 0)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// A synthetic context fill reports its size without a token breakdown.
|
||||
if usage.total_tokens != 0 && usage.input_tokens == 0 && usage.output_tokens == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(Self {
|
||||
total_input_tokens: usage.input_tokens,
|
||||
cache_read_input_tokens: usage.cached_input_tokens,
|
||||
cache_write_input_tokens: usage.cache_write_input_tokens,
|
||||
other_input_tokens,
|
||||
output_tokens: usage.output_tokens,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ChatWidget> for Usage {
|
||||
fn from(chat: &ChatWidget) -> Self {
|
||||
Self {
|
||||
total: chat
|
||||
.token_info
|
||||
.as_ref()
|
||||
.and_then(|info| Counts::reported(&info.total_token_usage)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -515,7 +515,7 @@ fn token_usage_breakdown(usage: TokenUsage) -> codex_app_server_protocol::TokenU
|
||||
total_tokens: usage.total_tokens,
|
||||
input_tokens: usage.input_tokens,
|
||||
cached_input_tokens: usage.cached_input_tokens,
|
||||
cache_write_input_tokens: 0,
|
||||
cache_write_input_tokens: usage.cache_write_input_tokens,
|
||||
output_tokens: usage.output_tokens,
|
||||
reasoning_output_tokens: usage.reasoning_output_tokens,
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ async fn statusline_state_projects_the_displayed_settings() {
|
||||
"config": {"statusline_items": ["run-state"]},
|
||||
"thread": {
|
||||
"workspace": {"cwd": expected_cwd},
|
||||
"usage": {},
|
||||
"id": id, "name": "Saved carts", "agent": {"state": "ready"},
|
||||
"model": {"id": "gpt-5.5", "display_name": "Test model", "provider": "openai",
|
||||
"reasoning_level": "none", "service_tier": "acme-priority", "service_tier_display_name": "Turbo"},
|
||||
@@ -47,3 +48,47 @@ async fn statusline_state_projects_the_displayed_settings() {
|
||||
let state = serde_json::to_value(chat.statusline_state()).unwrap();
|
||||
assert_eq!(state["thread"]["model"]["reasoning_level"], "default");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn statusline_totals_use_the_reported_token_breakdown() {
|
||||
let (mut chat, _events, _ops) = make_chatwidget_manual(Some("gpt-5.2")).await;
|
||||
chat.handle_thread_session(configured_thread_session(ThreadId::new()));
|
||||
let mut info =
|
||||
helpers::make_token_info(/*total_tokens*/ 15, /*context_window*/ 100_000);
|
||||
info.total_token_usage = TokenUsage {
|
||||
input_tokens: 110,
|
||||
cached_input_tokens: 60,
|
||||
cache_write_input_tokens: 10,
|
||||
output_tokens: 20,
|
||||
total_tokens: 130,
|
||||
..Default::default()
|
||||
};
|
||||
helpers::handle_token_count(&mut chat, Some(info.clone()));
|
||||
let state = serde_json::to_value(chat.statusline_state()).unwrap();
|
||||
assert_eq!(
|
||||
state["thread"]["usage"]["total"],
|
||||
json!({
|
||||
"total_input_tokens": 110, "cache_read_input_tokens": 60,
|
||||
"cache_write_input_tokens": 10, "other_input_tokens": 40, "output_tokens": 20
|
||||
})
|
||||
);
|
||||
|
||||
info.total_token_usage.cache_write_input_tokens = 51;
|
||||
for (report, has_total) in [
|
||||
(info, false), // The cache counts exceed total input.
|
||||
(
|
||||
helpers::make_token_info(
|
||||
/*total_tokens*/ 100_000, /*context_window*/ 100_000,
|
||||
),
|
||||
false,
|
||||
), // Synthetic context fill.
|
||||
(
|
||||
helpers::make_token_info(/*total_tokens*/ 0, /*context_window*/ 100_000),
|
||||
true,
|
||||
), // Measured zero.
|
||||
] {
|
||||
helpers::handle_token_count(&mut chat, Some(report));
|
||||
let state = serde_json::to_value(chat.statusline_state()).unwrap();
|
||||
assert_eq!(state["thread"]["usage"].get("total").is_some(), has_total);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +283,7 @@ async fn status_snapshot_includes_reasoning_details() {
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 1_200,
|
||||
cached_input_tokens: 200,
|
||||
cache_write_input_tokens: 0,
|
||||
output_tokens: 900,
|
||||
reasoning_output_tokens: 150,
|
||||
total_tokens: 2_250,
|
||||
@@ -981,6 +982,7 @@ async fn status_snapshot_includes_forked_from() {
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 800,
|
||||
cached_input_tokens: 0,
|
||||
cache_write_input_tokens: 0,
|
||||
output_tokens: 400,
|
||||
reasoning_output_tokens: 0,
|
||||
total_tokens: 1_200,
|
||||
@@ -1035,6 +1037,7 @@ async fn status_snapshot_includes_monthly_limit() {
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 800,
|
||||
cached_input_tokens: 0,
|
||||
cache_write_input_tokens: 0,
|
||||
output_tokens: 400,
|
||||
reasoning_output_tokens: 0,
|
||||
total_tokens: 1_200,
|
||||
@@ -1101,6 +1104,7 @@ async fn status_snapshot_includes_enterprise_monthly_credit_limit() {
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 800,
|
||||
cached_input_tokens: 0,
|
||||
cache_write_input_tokens: 0,
|
||||
output_tokens: 400,
|
||||
reasoning_output_tokens: 0,
|
||||
total_tokens: 1_200,
|
||||
@@ -1179,6 +1183,7 @@ async fn status_snapshot_uses_generic_limit_labels_for_unsupported_windows() {
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 800,
|
||||
cached_input_tokens: 0,
|
||||
cache_write_input_tokens: 0,
|
||||
output_tokens: 400,
|
||||
reasoning_output_tokens: 0,
|
||||
total_tokens: 1_200,
|
||||
@@ -1468,6 +1473,7 @@ async fn status_card_token_usage_excludes_cached_tokens() {
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 1_200,
|
||||
cached_input_tokens: 200,
|
||||
cache_write_input_tokens: 0,
|
||||
output_tokens: 900,
|
||||
reasoning_output_tokens: 0,
|
||||
total_tokens: 2_100,
|
||||
@@ -1570,6 +1576,7 @@ async fn status_snapshot_wraps_in_narrow_terminal() {
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 1_200,
|
||||
cached_input_tokens: 200,
|
||||
cache_write_input_tokens: 0,
|
||||
output_tokens: 900,
|
||||
reasoning_output_tokens: 150,
|
||||
total_tokens: 2_250,
|
||||
@@ -1677,6 +1684,7 @@ async fn status_snapshot_shows_missing_limits_message() {
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 500,
|
||||
cached_input_tokens: 0,
|
||||
cache_write_input_tokens: 0,
|
||||
output_tokens: 250,
|
||||
reasoning_output_tokens: 0,
|
||||
total_tokens: 750,
|
||||
@@ -1725,6 +1733,7 @@ async fn status_snapshot_uses_default_reasoning_when_config_empty() {
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 500,
|
||||
cached_input_tokens: 0,
|
||||
cache_write_input_tokens: 0,
|
||||
output_tokens: 250,
|
||||
reasoning_output_tokens: 0,
|
||||
total_tokens: 750,
|
||||
@@ -1793,6 +1802,7 @@ async fn status_snapshot_shows_refreshing_limits_notice() {
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 500,
|
||||
cached_input_tokens: 0,
|
||||
cache_write_input_tokens: 0,
|
||||
output_tokens: 250,
|
||||
reasoning_output_tokens: 0,
|
||||
total_tokens: 750,
|
||||
@@ -1951,6 +1961,7 @@ async fn status_snapshot_includes_credits_and_limits() {
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 1_500,
|
||||
cached_input_tokens: 100,
|
||||
cache_write_input_tokens: 0,
|
||||
output_tokens: 600,
|
||||
reasoning_output_tokens: 0,
|
||||
total_tokens: 2_200,
|
||||
@@ -2024,6 +2035,7 @@ async fn status_snapshot_shows_unavailable_limits_message() {
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 500,
|
||||
cached_input_tokens: 0,
|
||||
cache_write_input_tokens: 0,
|
||||
output_tokens: 250,
|
||||
reasoning_output_tokens: 0,
|
||||
total_tokens: 750,
|
||||
@@ -2084,6 +2096,7 @@ async fn status_snapshot_treats_refreshing_empty_limits_as_unavailable() {
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 500,
|
||||
cached_input_tokens: 0,
|
||||
cache_write_input_tokens: 0,
|
||||
output_tokens: 250,
|
||||
reasoning_output_tokens: 0,
|
||||
total_tokens: 750,
|
||||
@@ -2146,6 +2159,7 @@ async fn status_snapshot_shows_stale_limits_message() {
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 1_200,
|
||||
cached_input_tokens: 200,
|
||||
cache_write_input_tokens: 0,
|
||||
output_tokens: 900,
|
||||
reasoning_output_tokens: 150,
|
||||
total_tokens: 2_250,
|
||||
@@ -2216,6 +2230,7 @@ async fn status_snapshot_cached_limits_hide_credits_without_flag() {
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 900,
|
||||
cached_input_tokens: 200,
|
||||
cache_write_input_tokens: 0,
|
||||
output_tokens: 350,
|
||||
reasoning_output_tokens: 0,
|
||||
total_tokens: 1_450,
|
||||
@@ -2289,6 +2304,7 @@ async fn status_context_window_uses_last_usage() {
|
||||
let total_usage = TokenUsage {
|
||||
input_tokens: 12_800,
|
||||
cached_input_tokens: 0,
|
||||
cache_write_input_tokens: 0,
|
||||
output_tokens: 879,
|
||||
reasoning_output_tokens: 0,
|
||||
total_tokens: 102_000,
|
||||
@@ -2296,6 +2312,7 @@ async fn status_context_window_uses_last_usage() {
|
||||
let last_usage = TokenUsage {
|
||||
input_tokens: 12_800,
|
||||
cached_input_tokens: 0,
|
||||
cache_write_input_tokens: 0,
|
||||
output_tokens: 879,
|
||||
reasoning_output_tokens: 0,
|
||||
total_tokens: 13_679,
|
||||
|
||||
@@ -12,11 +12,26 @@ const BASELINE_TOKENS: i64 = 12000;
|
||||
pub struct TokenUsage {
|
||||
pub input_tokens: i64,
|
||||
pub cached_input_tokens: i64,
|
||||
#[serde(default)]
|
||||
pub cache_write_input_tokens: i64,
|
||||
pub output_tokens: i64,
|
||||
pub reasoning_output_tokens: i64,
|
||||
pub total_tokens: i64,
|
||||
}
|
||||
|
||||
impl From<codex_app_server_protocol::TokenUsageBreakdown> for TokenUsage {
|
||||
fn from(usage: codex_app_server_protocol::TokenUsageBreakdown) -> Self {
|
||||
Self {
|
||||
input_tokens: usage.input_tokens,
|
||||
cached_input_tokens: usage.cached_input_tokens,
|
||||
cache_write_input_tokens: usage.cache_write_input_tokens,
|
||||
output_tokens: usage.output_tokens,
|
||||
reasoning_output_tokens: usage.reasoning_output_tokens,
|
||||
total_tokens: usage.total_tokens,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TokenUsage {
|
||||
pub fn is_zero(&self) -> bool {
|
||||
self.total_tokens == 0
|
||||
|
||||
Reference in New Issue
Block a user