From 4aa538db4316bfb1cd9b166f55f0be9adc886d0d Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Fri, 6 Feb 2026 09:36:44 -0800 Subject: [PATCH] fix: include cache tokens in cost calculations --- src-tauri/src/stats.rs | 43 +++++++++++++++++++++++++++++++++---- src-tauri/src/types.rs | 4 ++++ src-tauri/src/wsl_bridge.rs | 43 +++++++++++++++++++++++++++++++------ 3 files changed, 79 insertions(+), 11 deletions(-) diff --git a/src-tauri/src/stats.rs b/src-tauri/src/stats.rs index 346d073..19eb422 100644 --- a/src-tauri/src/stats.rs +++ b/src-tauri/src/stats.rs @@ -163,13 +163,26 @@ impl UsageStats { stats } - pub fn add_usage(&mut self, input_tokens: u64, output_tokens: u64, model: &str) { + pub fn add_usage( + &mut self, + input_tokens: u64, + output_tokens: u64, + model: &str, + cache_creation_tokens: Option, + cache_read_tokens: Option, + ) { self.total_input_tokens += input_tokens; self.total_output_tokens += output_tokens; self.session_input_tokens += input_tokens; self.session_output_tokens += output_tokens; - let cost = calculate_cost(input_tokens, output_tokens, model); + let cost = calculate_cost( + input_tokens, + output_tokens, + model, + cache_creation_tokens, + cache_read_tokens, + ); self.total_cost_usd += cost; self.session_cost_usd += cost; @@ -462,7 +475,14 @@ fn is_consecutive_day(prev_date: &str, current_date: &str) -> bool { // Pricing as of February 2026 // https://platform.claude.com/docs/en/about-claude/models/overview -pub fn calculate_cost(input_tokens: u64, output_tokens: u64, model: &str) -> f64 { +// Cache pricing: https://platform.claude.com/docs/en/build-with-claude/prompt-caching +pub fn calculate_cost( + input_tokens: u64, + output_tokens: u64, + model: &str, + cache_creation_tokens: Option, + cache_read_tokens: Option, +) -> f64 { let (input_price_per_million, output_price_per_million) = match model { // Current generation (Claude 4.5) "claude-opus-4-5-20251101" => (5.0, 25.0), @@ -487,10 +507,25 @@ pub fn calculate_cost(input_tokens: u64, output_tokens: u64, model: &str) -> f64 _ => (3.0, 15.0), }; + // Regular input/output tokens let input_cost = (input_tokens as f64 / 1_000_000.0) * input_price_per_million; let output_cost = (output_tokens as f64 / 1_000_000.0) * output_price_per_million; - input_cost + output_cost + // Cache write tokens (cache creation) cost 1.25x the base input price + let cache_write_cost = if let Some(cache_creation) = cache_creation_tokens { + (cache_creation as f64 / 1_000_000.0) * input_price_per_million * 1.25 + } else { + 0.0 + }; + + // Cache read tokens cost 0.1x (10%) the base input price + let cache_read_cost = if let Some(cache_read) = cache_read_tokens { + (cache_read as f64 / 1_000_000.0) * input_price_per_million * 0.1 + } else { + 0.0 + }; + + input_cost + output_cost + cache_write_cost + cache_read_cost } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index 3607d89..7560a63 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -4,6 +4,10 @@ use serde::{Deserialize, Serialize}; pub struct UsageInfo { pub input_tokens: u64, pub output_tokens: u64, + #[serde(default)] + pub cache_creation_input_tokens: Option, + #[serde(default)] + pub cache_read_input_tokens: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] diff --git a/src-tauri/src/wsl_bridge.rs b/src-tauri/src/wsl_bridge.rs index 5f99b16..68019ba 100644 --- a/src-tauri/src/wsl_bridge.rs +++ b/src-tauri/src/wsl_bridge.rs @@ -604,8 +604,14 @@ fn process_json_line( // Only update stats if we have usage information if let Some(usage) = &message.usage { if let Some(model) = &message.model { - // Calculate cost for historical tracking - let cost_usd = calculate_cost(usage.input_tokens, usage.output_tokens, model); + // Calculate cost for historical tracking (including cache tokens) + let cost_usd = calculate_cost( + usage.input_tokens, + usage.output_tokens, + model, + usage.cache_creation_input_tokens, + usage.cache_read_input_tokens, + ); // Store cost for later use in output events message_cost = Some(MessageCost { @@ -618,7 +624,13 @@ fn process_json_line( { let mut stats_guard = stats.write(); stats_guard.increment_messages(); - stats_guard.add_usage(usage.input_tokens, usage.output_tokens, model); + stats_guard.add_usage( + usage.input_tokens, + usage.output_tokens, + model, + usage.cache_creation_input_tokens, + usage.cache_read_input_tokens, + ); stats_guard.get_session_duration(); // Attribute tokens to tools if any tools were used in this message @@ -768,12 +780,29 @@ fn process_json_line( stats_guard.model.clone().unwrap_or_else(|| "claude-opus-4-20250514".to_string()) }; - // Calculate cost for historical tracking - let cost_usd = calculate_cost(usage_info.input_tokens, usage_info.output_tokens, &model); + // Calculate cost for historical tracking (including cache tokens) + let cost_usd = calculate_cost( + usage_info.input_tokens, + usage_info.output_tokens, + &model, + usage_info.cache_creation_input_tokens, + usage_info.cache_read_input_tokens, + ); let mut stats_guard = stats.write(); - stats_guard.add_usage(usage_info.input_tokens, usage_info.output_tokens, &model); - println!("Result message tokens - input: {}, output: {}", usage_info.input_tokens, usage_info.output_tokens); + stats_guard.add_usage( + usage_info.input_tokens, + usage_info.output_tokens, + &model, + usage_info.cache_creation_input_tokens, + usage_info.cache_read_input_tokens, + ); + println!("Result message tokens - input: {}, output: {}, cache_creation: {:?}, cache_read: {:?}", + usage_info.input_tokens, + usage_info.output_tokens, + usage_info.cache_creation_input_tokens, + usage_info.cache_read_input_tokens + ); // Record to historical cost tracking let app_clone = app.clone();