use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StatsCache {
pub version: u32,
#[serde(default)]
pub last_computed_date: Option<String>,
#[serde(default)]
pub daily_activity: Vec<DailyActivity>,
#[serde(default)]
pub daily_model_tokens: Vec<DailyModelTokens>,
#[serde(default)]
pub model_usage: HashMap<String, ModelUsage>,
#[serde(default)]
pub total_sessions: u64,
#[serde(default)]
pub total_messages: u64,
#[serde(default)]
pub longest_session: Option<LongestSession>,
#[serde(default)]
pub first_session_date: Option<String>,
#[serde(default)]
pub hour_counts: HashMap<String, u64>,
#[serde(default)]
pub total_speculation_time_saved_ms: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DailyActivity {
pub date: String,
#[serde(default)]
pub message_count: u64,
#[serde(default)]
pub session_count: u64,
#[serde(default)]
pub tool_call_count: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DailyModelTokens {
pub date: String,
#[serde(default)]
pub tokens_by_model: HashMap<String, u64>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelUsage {
#[serde(default)]
pub input_tokens: u64,
#[serde(default)]
pub output_tokens: u64,
#[serde(default)]
pub cache_read_input_tokens: u64,
#[serde(default)]
pub cache_creation_input_tokens: u64,
#[serde(default)]
pub web_search_requests: u64,
#[serde(default)]
pub cost_usd: f64,
#[serde(default)]
pub context_window: u64,
#[serde(default)]
pub max_output_tokens: u64,
}
impl ModelUsage {
pub fn total_tokens(&self) -> u64 {
self.input_tokens + self.output_tokens + self.cache_read_input_tokens + self.cache_creation_input_tokens
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LongestSession {
#[serde(default)]
pub session_id: String,
#[serde(default)]
pub duration: u64,
#[serde(default)]
pub message_count: u64,
#[serde(default)]
pub timestamp: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_model_usage_total_tokens() {
let mu = ModelUsage {
input_tokens: 1000,
output_tokens: 500,
cache_read_input_tokens: 300,
cache_creation_input_tokens: 200,
web_search_requests: 0,
cost_usd: 0.0,
context_window: 0,
max_output_tokens: 0,
};
assert_eq!(mu.total_tokens(), 2000);
}
#[test]
fn test_model_usage_total_tokens_zero() {
let mu = ModelUsage {
input_tokens: 0,
output_tokens: 0,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
web_search_requests: 0,
cost_usd: 0.0,
context_window: 0,
max_output_tokens: 0,
};
assert_eq!(mu.total_tokens(), 0);
}
#[test]
fn test_deserialize_stats_cache_empty() {
let json = r#"{"version":3}"#;
let stats: StatsCache = serde_json::from_str(json).unwrap();
assert_eq!(stats.version, 3);
assert!(stats.daily_activity.is_empty());
assert!(stats.model_usage.is_empty());
}
#[test]
fn test_deserialize_stats_cache_full() {
let json = r#"{
"version": 3,
"lastComputedDate": "2026-05-30",
"totalSessions": 42,
"totalMessages": 500,
"firstSessionDate": "2025-11-04T08:34:16.259Z",
"totalSpeculationTimeSavedMs": 12345,
"modelUsage": {
"opus": {
"inputTokens": 10000,
"outputTokens": 5000,
"cacheReadInputTokens": 3000,
"cacheCreationInputTokens": 1000,
"webSearchRequests": 10,
"costUsd": 1.5,
"contextWindow": 200000,
"maxOutputTokens": 8192
}
},
"hourCounts": {"10": 50, "14": 80, "22": 30},
"longestSession": {
"sessionId": "abc123",
"duration": 7200,
"messageCount": 100,
"timestamp": "2026-04-15T10:00:00Z"
}
}"#;
let stats: StatsCache = serde_json::from_str(json).unwrap();
assert_eq!(stats.version, 3);
assert_eq!(stats.last_computed_date.as_deref(), Some("2026-05-30"));
assert_eq!(stats.total_sessions, 42);
assert_eq!(stats.total_messages, 500);
assert_eq!(stats.first_session_date.as_deref(), Some("2025-11-04T08:34:16.259Z"));
assert_eq!(stats.total_speculation_time_saved_ms, 12345);
let opus = stats.model_usage.get("opus").unwrap();
assert_eq!(opus.input_tokens, 10000);
assert_eq!(opus.cost_usd, 1.5);
assert_eq!(opus.context_window, 200000);
assert_eq!(stats.hour_counts.get("14").unwrap(), &80);
let longest = stats.longest_session.unwrap();
assert_eq!(longest.session_id, "abc123");
assert_eq!(longest.duration, 7200);
assert_eq!(longest.message_count, 100);
}
#[test]
fn test_deserialize_daily_model_tokens() {
let json = r#"{"date":"2026-04-15","tokensByModel":{"opus":1000,"sonnet":500}}"#;
let dmt: DailyModelTokens = serde_json::from_str(json).unwrap();
assert_eq!(dmt.date, "2026-04-15");
assert_eq!(dmt.tokens_by_model.get("opus").unwrap(), &1000);
assert_eq!(dmt.tokens_by_model.get("sonnet").unwrap(), &500);
}
#[test]
fn test_deserialize_daily_model_tokens_empty() {
let json = r#"{"date":"2026-04-15","tokensByModel":{}}"#;
let dmt: DailyModelTokens = serde_json::from_str(json).unwrap();
assert_eq!(dmt.date, "2026-04-15");
assert!(dmt.tokens_by_model.is_empty());
}
#[test]
fn test_deserialize_stats_cache_with_daily_model_tokens() {
let json = r#"{"version":3,"dailyModelTokens":[{"date":"2026-04-15","tokensByModel":{"opus":100}},{"date":"2026-04-16","tokensByModel":{"sonnet":200}}]}"#;
let stats: StatsCache = serde_json::from_str(json).unwrap();
assert_eq!(stats.daily_model_tokens.len(), 2);
assert_eq!(stats.daily_model_tokens[0].date, "2026-04-15");
assert_eq!(stats.daily_model_tokens[0].tokens_by_model.get("opus").unwrap(), &100);
}
#[test]
fn test_deserialize_daily_activity() {
let json = r#"{"date":"2026-04-15","messageCount":25,"sessionCount":3,"toolCallCount":10}"#;
let da: DailyActivity = serde_json::from_str(json).unwrap();
assert_eq!(da.date, "2026-04-15");
assert_eq!(da.message_count, 25);
assert_eq!(da.session_count, 3);
assert_eq!(da.tool_call_count, 10);
}
#[test]
fn test_deserialize_daily_activity_defaults() {
let json = r#"{"date":"2026-04-15"}"#;
let da: DailyActivity = serde_json::from_str(json).unwrap();
assert_eq!(da.date, "2026-04-15");
assert_eq!(da.message_count, 0);
assert_eq!(da.session_count, 0);
assert_eq!(da.tool_call_count, 0);
}
}