Skip to main content

lean_ctx/core/stats/
model.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4/// Persistent store for all-time token savings, command stats, and daily history.
5#[derive(Serialize, Deserialize, Default, Clone)]
6pub struct StatsStore {
7    pub total_commands: u64,
8    pub total_input_tokens: u64,
9    pub total_output_tokens: u64,
10    pub first_use: Option<String>,
11    pub last_use: Option<String>,
12    pub commands: HashMap<String, CommandStats>,
13    pub daily: Vec<DayStats>,
14    #[serde(default)]
15    pub cep: CepStats,
16}
17
18/// Aggregated CEP (Cognitive Efficiency Protocol) metrics across sessions.
19#[derive(Serialize, Deserialize, Clone, Default)]
20pub struct CepStats {
21    pub sessions: u64,
22    pub total_cache_hits: u64,
23    pub total_cache_reads: u64,
24    pub total_tokens_original: u64,
25    pub total_tokens_compressed: u64,
26    pub modes: HashMap<String, u64>,
27    pub scores: Vec<CepSessionSnapshot>,
28    #[serde(default)]
29    pub last_session_pid: Option<u32>,
30    #[serde(default)]
31    pub last_session_original: Option<u64>,
32    #[serde(default)]
33    pub last_session_compressed: Option<u64>,
34    /// Cumulative cache hits/reads observed for the current PID at the last
35    /// snapshot. Used to accumulate *deltas* across repeated snapshots within
36    /// one server process, so `total_cache_hits` keeps tracking cache activity
37    /// after the first checkpoint instead of freezing (#361).
38    #[serde(default)]
39    pub last_session_cache_hits: Option<u64>,
40    #[serde(default)]
41    pub last_session_cache_reads: Option<u64>,
42}
43
44/// Point-in-time snapshot of CEP scores for a single session.
45#[derive(Serialize, Deserialize, Clone)]
46pub struct CepSessionSnapshot {
47    pub timestamp: String,
48    pub score: u32,
49    pub cache_hit_rate: u32,
50    pub mode_diversity: u32,
51    pub compression_rate: u32,
52    pub tool_calls: u64,
53    pub tokens_saved: u64,
54    pub complexity: String,
55}
56
57/// Per-command token statistics: invocation count and input/output totals.
58#[derive(Serialize, Deserialize, Clone, Default, Debug)]
59pub struct CommandStats {
60    pub count: u64,
61    pub input_tokens: u64,
62    pub output_tokens: u64,
63}
64
65/// Daily aggregate: command count and token totals for one calendar day.
66#[derive(Serialize, Deserialize, Clone, Default)]
67pub struct DayStats {
68    pub date: String,
69    pub commands: u64,
70    pub input_tokens: u64,
71    pub output_tokens: u64,
72    /// lean-ctx version active when this day's stats were last recorded.
73    /// Lets `lean-ctx gain` attribute per-day compression changes to a release
74    /// (#307). Empty for days recorded before this field existed.
75    #[serde(default)]
76    pub version: String,
77}
78
79/// High-level token savings summary for display.
80pub struct GainSummary {
81    pub total_saved: u64,
82    pub total_calls: u64,
83}
84
85/// Average LLM pricing per 1M tokens (blended across Claude, GPT, Gemini).
86pub const DEFAULT_INPUT_PRICE_PER_M: f64 = 2.50;
87pub const DEFAULT_OUTPUT_PRICE_PER_M: f64 = 10.0;
88
89/// LLM pricing model for estimating dollar savings from token compression.
90pub struct CostModel {
91    pub input_price_per_m: f64,
92    pub output_price_per_m: f64,
93    pub avg_verbose_output_per_call: u64,
94    pub avg_concise_output_per_call: u64,
95}
96
97impl Default for CostModel {
98    fn default() -> Self {
99        let env_model = std::env::var("LEAN_CTX_MODEL")
100            .or_else(|_| std::env::var("LCTX_MODEL"))
101            .ok();
102        let pricing = crate::core::gain::model_pricing::ModelPricing::load();
103        let quote = pricing.quote(env_model.as_deref());
104        Self {
105            input_price_per_m: quote.cost.input_per_m,
106            output_price_per_m: quote.cost.output_per_m,
107            avg_verbose_output_per_call: 180,
108            avg_concise_output_per_call: 120,
109        }
110    }
111}
112
113/// Detailed cost comparison: with vs. without lean-ctx compression.
114pub struct CostBreakdown {
115    pub input_cost_without: f64,
116    pub input_cost_with: f64,
117    pub output_cost_without: f64,
118    pub output_cost_with: f64,
119    pub total_cost_without: f64,
120    pub total_cost_with: f64,
121    pub total_saved: f64,
122    pub estimated_output_tokens_without: u64,
123    pub estimated_output_tokens_with: u64,
124    pub output_tokens_saved: u64,
125}
126
127impl CostModel {
128    /// Calculates the full cost breakdown from the stats store.
129    pub fn calculate(&self, store: &StatsStore) -> CostBreakdown {
130        let input_cost_without =
131            store.total_input_tokens as f64 / 1_000_000.0 * self.input_price_per_m;
132        let input_cost_with =
133            store.total_output_tokens as f64 / 1_000_000.0 * self.input_price_per_m;
134
135        let input_saved = store
136            .total_input_tokens
137            .saturating_sub(store.total_output_tokens);
138        let compression_rate = if store.total_input_tokens > 0 {
139            input_saved as f64 / store.total_input_tokens as f64
140        } else {
141            0.0
142        };
143        let est_output_without = store.total_commands * self.avg_verbose_output_per_call;
144        let est_output_with = if compression_rate > 0.01 {
145            store.total_commands * self.avg_concise_output_per_call
146        } else {
147            est_output_without
148        };
149        let output_saved = est_output_without.saturating_sub(est_output_with);
150
151        let output_cost_without = est_output_without as f64 / 1_000_000.0 * self.output_price_per_m;
152        let output_cost_with = est_output_with as f64 / 1_000_000.0 * self.output_price_per_m;
153
154        let total_without = input_cost_without + output_cost_without;
155        let total_with = input_cost_with + output_cost_with;
156
157        CostBreakdown {
158            input_cost_without,
159            input_cost_with,
160            output_cost_without,
161            output_cost_with,
162            total_cost_without: total_without,
163            total_cost_with: total_with,
164            total_saved: total_without - total_with,
165            estimated_output_tokens_without: est_output_without,
166            estimated_output_tokens_with: est_output_with,
167            output_tokens_saved: output_saved,
168        }
169    }
170}