Skip to main content

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}