vct_core/models/usage.rs
1//! Token-usage aggregation types shared between the usage calculator and the
2//! display layer.
3
4use crate::constants::FastHashMap;
5use crate::models::Provider;
6use serde::Serialize;
7
8/// Token usage data aggregated by model (across all dates)
9///
10/// Structure: Model Name -> Usage Metrics
11/// - Uses FastHashMap (ahash) for better performance than std HashMap
12/// - Usage format varies by provider:
13/// * Claude/Gemini: `{ input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens }`
14/// * Codex: `{ total_token_usage: { input_tokens, output_tokens } }`
15pub type UsageResult = FastHashMap<String, serde_json::Value>;
16
17/// Tracks the number of active days per AI provider
18///
19/// Used for calculating daily averages when data is aggregated by model only.
20/// Day counts are derived from file modification dates during processing.
21#[derive(Debug, Clone, Default, Serialize)]
22pub struct ProviderActiveDays {
23 /// Distinct active days observed for Claude Code.
24 pub claude: usize,
25 /// Distinct active days observed for Codex.
26 pub codex: usize,
27 /// Distinct active days observed for Copilot CLI.
28 pub copilot: usize,
29 /// Distinct active days observed for Gemini CLI.
30 pub gemini: usize,
31 /// Distinct active days observed for OpenCode.
32 pub opencode: usize,
33 /// Distinct active days observed for Cursor.
34 pub cursor: usize,
35 /// Distinct active days observed for Hermes.
36 pub hermes: usize,
37 /// Distinct active days observed for Grok CLI.
38 pub grok: usize,
39 /// Distinct active days across all providers combined.
40 pub total: usize,
41}
42
43/// Per-provider usage data, keyed by source directory rather than by model name.
44///
45/// The top-level `UsageResult` in `UsageData` intentionally merges same-named
46/// models across providers (so the per-model table shows one row for
47/// `claude-sonnet-4-6` regardless of whether Claude Code, Copilot CLI, or
48/// both invoked it). That merge loses the *source* information though, which
49/// matters for the per-provider summary: once Copilot CLI stopped writing
50/// the `copilot` sentinel and started recording real model names, the old
51/// "classify each row by model-name prefix" logic mis-attributed every
52/// Copilot session to Claude Code.
53///
54/// This struct keeps a separate `UsageResult` per provider so the display
55/// layer can sum tokens and cost by source directory directly, with no
56/// prefix heuristics involved. It is populated in `usage::aggregator` at
57/// the same time the global merged map is built.
58#[derive(Debug, Default, Clone, Serialize)]
59pub struct PerProviderUsage {
60 /// Per-model usage attributed to Claude Code.
61 pub claude: UsageResult,
62 /// Per-model usage attributed to Codex.
63 pub codex: UsageResult,
64 /// Per-model usage attributed to Copilot CLI.
65 pub copilot: UsageResult,
66 /// Per-model usage attributed to Gemini CLI.
67 pub gemini: UsageResult,
68 /// Per-model usage attributed to OpenCode.
69 pub opencode: UsageResult,
70 /// Per-model usage attributed to Cursor.
71 pub cursor: UsageResult,
72 /// Per-model usage attributed to Hermes.
73 pub hermes: UsageResult,
74 /// Per-model usage attributed to Grok CLI.
75 pub grok: UsageResult,
76}
77
78impl PerProviderUsage {
79 /// Returns the usage map for `provider`, or `None` for [`Provider::Unknown`].
80 pub fn get(&self, provider: Provider) -> Option<&UsageResult> {
81 match provider {
82 Provider::ClaudeCode => Some(&self.claude),
83 Provider::Codex => Some(&self.codex),
84 Provider::Copilot => Some(&self.copilot),
85 Provider::Gemini => Some(&self.gemini),
86 Provider::OpenCode => Some(&self.opencode),
87 Provider::Cursor => Some(&self.cursor),
88 Provider::Hermes => Some(&self.hermes),
89 Provider::Grok => Some(&self.grok),
90 Provider::Unknown => None,
91 }
92 }
93
94 /// Returns a mutable handle to the usage map for `provider`, or `None` for
95 /// [`Provider::Unknown`].
96 pub fn get_mut(&mut self, provider: Provider) -> Option<&mut UsageResult> {
97 match provider {
98 Provider::ClaudeCode => Some(&mut self.claude),
99 Provider::Codex => Some(&mut self.codex),
100 Provider::Copilot => Some(&mut self.copilot),
101 Provider::Gemini => Some(&mut self.gemini),
102 Provider::OpenCode => Some(&mut self.opencode),
103 Provider::Cursor => Some(&mut self.cursor),
104 Provider::Hermes => Some(&mut self.hermes),
105 Provider::Grok => Some(&mut self.grok),
106 Provider::Unknown => None,
107 }
108 }
109}