mirror of
https://github.com/openai/codex.git
synced 2026-11-14 21:40:39 +00:00
Expose context usage in statusline state (#61409)
## What changed Add `thread.usage.context` with optional `context_window_size`, `used_tokens`, and `used_percentage` fields. Use the latest token report for context usage and the built-in footer's window size and percentage calculations. Omit unavailable fields, including the percentage when the context window is unknown. ## Testing Extend statusline state tests to cover missing usage, unknown context windows, and footer-consistent percentages, with context usage distinct from cumulative token totals. GitOrigin-RevId: 699b65f6c047e76fcc7d1ba729eebd19a19e4bcd
This commit is contained in:
@@ -6,10 +6,21 @@ use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(super) struct Usage {
|
||||
context: Context,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
total: Option<Counts>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Context {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
context_window_size: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
used_tokens: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
used_percentage: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Counts {
|
||||
total_input_tokens: i64,
|
||||
@@ -52,7 +63,17 @@ impl Counts {
|
||||
|
||||
impl From<&ChatWidget> for Usage {
|
||||
fn from(chat: &ChatWidget) -> Self {
|
||||
let context_window_size = chat.status_line_context_window_size();
|
||||
Self {
|
||||
context: Context {
|
||||
context_window_size,
|
||||
used_tokens: chat
|
||||
.token_info
|
||||
.as_ref()
|
||||
.map(|info| info.last_token_usage.tokens_in_context_window()),
|
||||
used_percentage: context_window_size
|
||||
.and_then(|_| chat.status_line_context_used_percent()),
|
||||
},
|
||||
total: chat
|
||||
.token_info
|
||||
.as_ref()
|
||||
|
||||
@@ -388,6 +388,7 @@ async fn app_server_model_verification_renders_warning() {
|
||||
async fn context_indicator_shows_used_tokens_when_window_unknown() {
|
||||
let (mut chat, _rx, _ops) = make_chatwidget_manual(Some("unknown-model")).await;
|
||||
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.config.model_context_window = None;
|
||||
let auto_compact_limit = 200_000;
|
||||
chat.config.model_auto_compact_token_limit = Some(auto_compact_limit);
|
||||
@@ -399,18 +400,39 @@ async fn context_indicator_shows_used_tokens_when_window_unknown() {
|
||||
..TokenUsage::default()
|
||||
};
|
||||
let token_info = TokenUsageInfo {
|
||||
total_token_usage: token_usage.clone(),
|
||||
last_token_usage: token_usage,
|
||||
total_token_usage: token_usage,
|
||||
last_token_usage: TokenUsage {
|
||||
total_tokens: 40_000,
|
||||
..TokenUsage::default()
|
||||
},
|
||||
model_context_window: None,
|
||||
};
|
||||
|
||||
handle_token_count(&mut chat, Some(token_info));
|
||||
handle_token_count(&mut chat, Some(token_info.clone()));
|
||||
|
||||
assert_eq!(chat.bottom_pane.context_window_percent(), None);
|
||||
assert_eq!(
|
||||
chat.bottom_pane.context_window_used_tokens(),
|
||||
Some(total_tokens)
|
||||
);
|
||||
let state = serde_json::to_value(chat.statusline_state()).unwrap();
|
||||
assert_eq!(
|
||||
state["thread"]["usage"]["context"],
|
||||
json!({"used_tokens": 40_000})
|
||||
);
|
||||
|
||||
handle_token_count(
|
||||
&mut chat,
|
||||
Some(TokenUsageInfo {
|
||||
model_context_window: Some(68_000),
|
||||
..token_info
|
||||
}),
|
||||
);
|
||||
let state = serde_json::to_value(chat.statusline_state()).unwrap();
|
||||
assert_eq!(
|
||||
state["thread"]["usage"]["context"],
|
||||
json!({"context_window_size": 68_000, "used_tokens": 40_000, "used_percentage": 50})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -34,7 +34,7 @@ async fn statusline_state_projects_the_displayed_settings() {
|
||||
"config": {"statusline_items": ["run-state"]},
|
||||
"thread": {
|
||||
"workspace": {"cwd": expected_cwd},
|
||||
"usage": {},
|
||||
"usage": {"context": {}},
|
||||
"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"},
|
||||
@@ -92,3 +92,30 @@ async fn statusline_totals_use_the_reported_token_breakdown() {
|
||||
assert_eq!(state["thread"]["usage"].get("total").is_some(), has_total);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn statusline_context_uses_the_footer_window_and_percentage() {
|
||||
let (mut chat, _events, _ops) = make_chatwidget_manual(Some("gpt-5.2")).await;
|
||||
chat.handle_thread_session(configured_thread_session(ThreadId::new()));
|
||||
chat.config.model_context_window = Some(80_000);
|
||||
helpers::handle_token_count(&mut chat, /*info*/ None);
|
||||
let state = serde_json::to_value(chat.statusline_state()).unwrap();
|
||||
assert_eq!(
|
||||
state["thread"]["usage"]["context"],
|
||||
json!({"context_window_size": 80_000})
|
||||
);
|
||||
|
||||
// The last response occupies half the usable window after the footer's fixed overhead.
|
||||
let mut info = helpers::make_token_info(
|
||||
/*total_tokens*/ 56_000, /*context_window*/ 100_000,
|
||||
);
|
||||
info.total_token_usage.total_tokens = 220_000;
|
||||
helpers::handle_token_count(&mut chat, Some(info));
|
||||
let state = serde_json::to_value(chat.statusline_state()).unwrap();
|
||||
assert_eq!(
|
||||
state["thread"]["usage"]["context"],
|
||||
json!({
|
||||
"context_window_size": 100_000, "used_tokens": 56_000, "used_percentage": 50
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user