Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/tui/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2238,6 +2238,7 @@ pub(super) fn parse_usage(usage: Option<&Value>) -> Usage {
output_tokens: output_tokens.min(u64::from(u32::MAX)) as u32,
prompt_cache_hit_tokens,
prompt_cache_miss_tokens,
prompt_cache_write_tokens: None,
reasoning_tokens,
reasoning_replay_tokens: None,
server_tool_use,
Expand Down
34 changes: 26 additions & 8 deletions crates/tui/src/client/anthropic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@
//! CodeWhale's `reasoning_effort` tiers, sampling-parameter rules for
//! models that reject them, and `cache_control` breakpoint placement
//! aligned with the prefix-zone model in `prefix_cache.rs`;
//! - usage normalization (#2961): `prompt_cache_hit_tokens` comes from
//! `cache_read_input_tokens`, `prompt_cache_miss_tokens` is `input_tokens`
//! plus `cache_creation_input_tokens`, and the normalized `input_tokens`
//! is the sum of all three (total prompt, the DeepSeek convention);
//! - usage normalization (#2961 / #4318): `prompt_cache_hit_tokens` comes from
//! `cache_read_input_tokens`, `prompt_cache_write_tokens` from
//! `cache_creation_input_tokens`, `prompt_cache_miss_tokens` is the raw
//! non-cached `input_tokens`, and the normalized `input_tokens` is the sum
//! of all three (total prompt, the DeepSeek convention);
//! - signed-thinking handling: `signature_delta` is captured into
//! [`crate::models::Delta::SignatureDelta`] and assistant thinking blocks
//! replay verbatim (signature included); unsigned thinking blocks are
Expand Down Expand Up @@ -496,8 +497,8 @@ fn convert_anthropic_sse_data(data: &str) -> Option<Result<StreamEvent>> {
}

/// Map Anthropic's usage payload onto the normalized [`Usage`] convention
/// (#2961): hit = cache reads, miss = uncached input + cache writes,
/// `input_tokens` = the total prompt across all three.
/// (#2961 / #4318): hit = cache reads, write = cache creation, miss = raw
/// uncached input, `input_tokens` = the total prompt across all three.
fn parse_anthropic_usage(usage: &Value) -> Usage {
let field = |name: &str| {
usage
Expand All @@ -517,7 +518,8 @@ fn parse_anthropic_usage(usage: &Value) -> Usage {
.saturating_add(cache_read),
output_tokens: output,
prompt_cache_hit_tokens: Some(cache_read),
prompt_cache_miss_tokens: Some(input_raw.saturating_add(cache_creation)),
prompt_cache_miss_tokens: Some(input_raw),
prompt_cache_write_tokens: Some(cache_creation),
reasoning_tokens: None,
reasoning_replay_tokens: None,
server_tool_use: None,
Expand Down Expand Up @@ -852,7 +854,8 @@ mod tests {
};
assert_eq!(message.usage.input_tokens, 3 + 2045 + 18000);
assert_eq!(message.usage.prompt_cache_hit_tokens, Some(18000));
assert_eq!(message.usage.prompt_cache_miss_tokens, Some(3 + 2045));
assert_eq!(message.usage.prompt_cache_miss_tokens, Some(3));
assert_eq!(message.usage.prompt_cache_write_tokens, Some(2045));

assert!(matches!(
&decoded[1],
Expand Down Expand Up @@ -927,6 +930,21 @@ mod tests {
assert_eq!(usage.output_tokens, 5);
assert_eq!(usage.prompt_cache_hit_tokens, Some(0));
assert_eq!(usage.prompt_cache_miss_tokens, Some(10));
assert_eq!(usage.prompt_cache_write_tokens, Some(0));
}

#[test]
fn usage_mapping_keeps_cache_write_separate_from_miss() {
let usage = parse_anthropic_usage(&json!({
"input_tokens": 3,
"cache_creation_input_tokens": 2045,
"cache_read_input_tokens": 18000,
"output_tokens": 1,
}));
assert_eq!(usage.input_tokens, 3 + 2045 + 18000);
assert_eq!(usage.prompt_cache_hit_tokens, Some(18000));
assert_eq!(usage.prompt_cache_miss_tokens, Some(3));
assert_eq!(usage.prompt_cache_write_tokens, Some(2045));
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions crates/tui/src/client/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,7 @@ fn parse_responses_usage(val: &Value) -> Usage {
output_tokens: output,
prompt_cache_hit_tokens,
prompt_cache_miss_tokens,
prompt_cache_write_tokens: None,
reasoning_tokens,
reasoning_replay_tokens: None,
server_tool_use: None,
Expand Down
4 changes: 2 additions & 2 deletions crates/tui/src/core/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2069,8 +2069,8 @@ impl Engine {
if let Some(hit_tokens) = usage.cache_read_input_tokens {
line.push_str(&format!(", cache hits {hit_tokens}"));
}
if let Some(miss_tokens) = usage.cache_creation_input_tokens {
line.push_str(&format!(", cache misses {miss_tokens}"));
if let Some(write_tokens) = usage.cache_creation_input_tokens {
line.push_str(&format!(", cache writes {write_tokens}"));
}
Some(line)
}
Expand Down
3 changes: 2 additions & 1 deletion crates/tui/src/core/engine/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5461,6 +5461,7 @@ fn turn_metadata_surfaces_context_and_resource_usage() {
output_tokens: 300,
prompt_cache_hit_tokens: Some(800),
prompt_cache_miss_tokens: Some(400),
prompt_cache_write_tokens: Some(400),
..Default::default()
});
{
Expand All @@ -5487,7 +5488,7 @@ fn turn_metadata_surfaces_context_and_resource_usage() {
"session usage should be model-visible: {text}"
);
assert!(text.contains("cache hits 800"), "got: {text}");
assert!(text.contains("cache misses 400"), "got: {text}");
assert!(text.contains("cache writes 400"), "got: {text}");
assert!(
text.contains("Active goal resource usage:"),
"active goal resource usage should be model-visible: {text}"
Expand Down
17 changes: 10 additions & 7 deletions crates/tui/src/core/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ pub struct Session {
pub struct SessionUsage {
pub input_tokens: u64,
pub output_tokens: u64,
/// Cache creation (miss) tokens. `None` when never observed by the API —
/// do NOT display as 0, which would be indistinguishable from "no misses".
/// Cache creation (write) tokens. `None` when never observed by the API —
/// do NOT display as 0, which would be indistinguishable from "no writes".
pub cache_creation_input_tokens: Option<u64>,
/// Cache read (hit) tokens. `None` when never observed by the API —
/// do NOT display as 0, which would be indistinguishable from "no hits".
Expand All @@ -111,7 +111,7 @@ impl SessionUsage {
pub fn add(&mut self, usage: &Usage) {
self.input_tokens += u64::from(usage.input_tokens);
self.output_tokens += u64::from(usage.output_tokens);
if let Some(tokens) = usage.prompt_cache_miss_tokens {
if let Some(tokens) = usage.prompt_cache_write_tokens {
self.cache_creation_input_tokens =
Some(self.cache_creation_input_tokens.unwrap_or(0) + u64::from(tokens));
}
Expand Down Expand Up @@ -218,6 +218,7 @@ mod tests {
output_tokens: 50,
prompt_cache_hit_tokens: None,
prompt_cache_miss_tokens: None,
prompt_cache_write_tokens: None,
reasoning_tokens: None,
reasoning_replay_tokens: None,
server_tool_use: None,
Expand All @@ -234,17 +235,18 @@ mod tests {
input_tokens: 100,
output_tokens: 50,
prompt_cache_hit_tokens: Some(30),
prompt_cache_miss_tokens: Some(70),
prompt_cache_miss_tokens: Some(50),
prompt_cache_write_tokens: Some(20),
reasoning_tokens: None,
reasoning_replay_tokens: None,
server_tool_use: None,
};
usage.add(&api_usage);
assert_eq!(usage.cache_read_input_tokens, Some(30));
assert_eq!(usage.cache_creation_input_tokens, Some(70));
assert_eq!(usage.cache_creation_input_tokens, Some(20));
usage.add(&api_usage);
assert_eq!(usage.cache_read_input_tokens, Some(60));
assert_eq!(usage.cache_creation_input_tokens, Some(140));
assert_eq!(usage.cache_creation_input_tokens, Some(40));
}

#[test]
Expand All @@ -254,7 +256,8 @@ mod tests {
input_tokens: 100,
output_tokens: 50,
prompt_cache_hit_tokens: Some(0), // explicit zero from provider
prompt_cache_miss_tokens: Some(1234),
prompt_cache_miss_tokens: Some(50),
prompt_cache_write_tokens: Some(1234),
reasoning_tokens: None,
reasoning_replay_tokens: None,
server_tool_use: None,
Expand Down
4 changes: 4 additions & 0 deletions crates/tui/src/core/turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ impl TurnContext {
self.usage.prompt_cache_miss_tokens,
usage.prompt_cache_miss_tokens,
);
self.usage.prompt_cache_write_tokens = add_optional_usage(
self.usage.prompt_cache_write_tokens,
usage.prompt_cache_write_tokens,
);
self.usage.reasoning_tokens =
add_optional_usage(self.usage.reasoning_tokens, usage.reasoning_tokens);
}
Expand Down
3 changes: 3 additions & 0 deletions crates/tui/src/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1301,6 +1301,7 @@ pub fn turn_end_payload(input: TurnEndPayloadInput<'_>) -> serde_json::Value {
"output_tokens": input.usage.output_tokens,
"prompt_cache_hit_tokens": input.usage.prompt_cache_hit_tokens,
"prompt_cache_miss_tokens": input.usage.prompt_cache_miss_tokens,
"prompt_cache_write_tokens": input.usage.prompt_cache_write_tokens,
"reasoning_tokens": input.usage.reasoning_tokens,
"reasoning_replay_tokens": input.usage.reasoning_replay_tokens,
},
Expand Down Expand Up @@ -1567,6 +1568,7 @@ NOEQUAL line dropped
output_tokens: 9,
prompt_cache_hit_tokens: Some(10),
prompt_cache_miss_tokens: Some(30),
prompt_cache_write_tokens: None,
reasoning_tokens: Some(4),
reasoning_replay_tokens: Some(2),
server_tool_use: None,
Expand Down Expand Up @@ -1873,6 +1875,7 @@ printf '%s\n' '{{"text":"stdout is not a mutation contract"}}'
output_tokens: 3,
prompt_cache_hit_tokens: None,
prompt_cache_miss_tokens: None,
prompt_cache_write_tokens: None,
reasoning_tokens: None,
reasoning_replay_tokens: None,
server_tool_use: None,
Expand Down
1 change: 1 addition & 0 deletions crates/tui/src/llm_response_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ mod tests {
output_tokens: 7,
prompt_cache_hit_tokens: Some(3),
prompt_cache_miss_tokens: Some(39),
prompt_cache_write_tokens: None,
reasoning_tokens: Some(5),
reasoning_replay_tokens: Some(2),
server_tool_use: None,
Expand Down
4 changes: 4 additions & 0 deletions crates/tui/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,10 @@ pub struct Usage {
pub prompt_cache_hit_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_cache_miss_tokens: Option<u32>,
/// Cache-creation / cache-write tokens (Anthropic `cache_creation_input_tokens`).
/// Billed at the cache-write rate when the pricing row publishes one (#4318).
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_cache_write_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_tokens: Option<u32>,
/// Approximate input tokens spent re-sending prior `reasoning_content`
Expand Down
Loading
Loading