1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
// SPDX-License-Identifier: Apache-2.0
//! On-disk record of the proxy's measured provider spend (`proxy_usage.json`).
//!
//! The proxy's usage meter (`proxy::usage_meter`) aggregates the real
//! provider usage of every finalized turn into these per-model sums and
//! persists them; the dashboard, CLI, stats and the savings ledger (which run
//! in *other* processes) read them back through [`load_persisted`] to learn
//! the user's real provider bill.
use std::collections::{HashMap, VecDeque};
use serde::{Deserialize, Serialize};
/// Where provider usage was observed, not an assertion of answer quality or
/// successful generation. Terminal usage also accompanies failed responses.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum UsageBoundary {
BodyEnd,
ProviderTerminalUsage,
StreamEnd,
StreamError,
}
/// Payload-free local measurement. Unknown cost stays unknown; neither HTTP
/// status nor receipt of usage supplies the private runtime's quality label.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ResponseObservation {
pub schema_version: u8,
pub http_status: u16,
/// Monotonic elapsed time from upstream dispatch (including retries) to
/// the indicated usage boundary, not necessarily full response latency.
pub usage_observed_ms: u64,
pub boundary: UsageBoundary,
pub provider_cost_usd: Option<f64>,
}
/// Cumulative real token counts for one model. Cost is derived at read time so a
/// pricing-table change re-values historical usage consistently.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct ModelUsage {
pub requests: u64,
pub input_tokens: u64,
pub output_tokens: u64,
pub cache_read_tokens: u64,
pub cache_write_tokens: u64,
pub reasoning_tokens: u64,
/// Requests whose free `count_tokens` probe answered (#701) — the rows the
/// verified-savings pair below covers. `serde(default)` keeps pre-#701
/// usage files loadable.
#[serde(default)]
pub counterfactual_requests: u64,
/// Provider-counted input tokens the covered requests would have billed
/// WITHOUT lean-ctx (sum of probe answers on the original bodies).
#[serde(default)]
pub counterfactual_input_tokens: u64,
/// Input-side tokens those same requests actually billed (input +
/// cache read + cache write) — same request, same moment, no confound.
#[serde(default)]
pub counterfactual_billed_tokens: u64,
/// The turns whose response carried the provider's own USD charge
/// (OpenRouter usage accounting, #1179). Their tokens are *inside* the
/// totals above; pricing subtracts this slice and books its measured USD
/// instead of a table estimate. `serde(default)` keeps older files loadable.
#[serde(default)]
pub measured: MeasuredSlice,
/// Recent factual response measurements, not quality/training labels.
/// This local diagnostic history must not be pooled into private ranking:
/// it has no verified actor/endpoint scope or user-accepted outcome yet.
#[serde(default)]
pub response_observations: VecDeque<ResponseObservation>,
}
/// Token/cost sums of the turns that reported a measured provider charge.
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
pub struct MeasuredSlice {
pub requests: u64,
pub input_tokens: u64,
pub output_tokens: u64,
pub cache_read_tokens: u64,
pub cache_write_tokens: u64,
/// Sum of provider-reported USD for those turns — the bill, not a table.
pub cost_usd: f64,
}
impl MeasuredSlice {
pub fn merge(&mut self, other: &Self) {
self.requests += other.requests;
self.input_tokens += other.input_tokens;
self.output_tokens += other.output_tokens;
self.cache_read_tokens += other.cache_read_tokens;
self.cache_write_tokens += other.cache_write_tokens;
self.cost_usd += other.cost_usd;
}
}
impl ModelUsage {
#[must_use]
pub fn billable_tokens(&self) -> u64 {
self.input_tokens + self.output_tokens + self.cache_read_tokens + self.cache_write_tokens
}
pub fn add_observation(&mut self, observation: ResponseObservation) {
const MAX_RESPONSE_OBSERVATIONS: usize = 32;
self.response_observations.push_back(observation);
while self.response_observations.len() > MAX_RESPONSE_OBSERVATIONS {
self.response_observations.pop_front();
}
}
}
/// Cumulative holdout cohort totals. Keyed by arm name: `"control"` |
/// `"treatment"` for output savings (#895 Track B), `"compression:control"` |
/// `"compression:treatment"` for input compression (#1905). The average tokens
/// per turn are `*_tokens / requests`. Only populated while a holdout is active.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct CohortUsage {
pub requests: u64,
pub input_tokens: u64,
pub output_tokens: u64,
/// Sum of squared per-turn output tokens, enabling an online sample variance
/// (and therefore a confidence interval) without retaining every turn.
/// `#[serde(default)]` keeps pre-#895 files loadable.
#[serde(default)]
pub sum_sq_output: u64,
/// Whole prompt per turn — billed input plus cache reads and writes (#1905).
/// Compression shrinks the prompt wherever it is billed, so the
/// input-compression comparison uses this, not `input_tokens` alone.
/// `#[serde(default)]` keeps earlier files loadable.
#[serde(default)]
pub prompt_tokens: u64,
/// Sum of squared per-turn `prompt_tokens`, for its confidence interval.
#[serde(default)]
pub sum_sq_prompt: u64,
}
impl CohortUsage {
/// Adds another cohort's running totals — every field, so the sums of
/// squares (and with them the variance / CI) survive a proxy restart.
pub fn merge(&mut self, other: &Self) {
self.requests += other.requests;
self.input_tokens += other.input_tokens;
self.output_tokens += other.output_tokens;
self.sum_sq_output = self.sum_sq_output.saturating_add(other.sum_sq_output);
self.prompt_tokens += other.prompt_tokens;
self.sum_sq_prompt = self.sum_sq_prompt.saturating_add(other.sum_sq_prompt);
}
/// Average output tokens per turn, or `None` with no observations.
#[must_use]
pub fn avg_output(&self) -> Option<f64> {
if self.requests == 0 {
None
} else {
#[allow(clippy::cast_precision_loss)]
Some(self.output_tokens as f64 / self.requests as f64)
}
}
/// Unbiased sample variance of per-turn output tokens, or `None` with < 2
/// observations. Computed from the running sum / sum-of-squares (clamped at
/// 0 to absorb floating-point error on near-constant samples).
#[must_use]
pub fn variance_output(&self) -> Option<f64> {
if self.requests < 2 {
return None;
}
#[allow(clippy::cast_precision_loss)]
let n = self.requests as f64;
#[allow(clippy::cast_precision_loss)]
let sum = self.output_tokens as f64;
#[allow(clippy::cast_precision_loss)]
let sum_sq = self.sum_sq_output as f64;
let var = (sum_sq - sum * sum / n) / (n - 1.0);
Some(var.max(0.0))
}
}
/// On-disk shape of the measured spend totals.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PersistedUsage {
pub ts: u64,
pub models: HashMap<String, ModelUsage>,
/// Output-savings cohort totals (#895). `#[serde(default)]` keeps older
/// `proxy_usage.json` files (written before the holdout existed) loadable.
#[serde(default)]
pub cohorts: HashMap<String, CohortUsage>,
/// The foreground model of the most recent finalized turn and when it was
/// seen (unix seconds), so other processes can attribute work to the model
/// in use now rather than the all-time largest bucket.
#[serde(default)]
pub last_model: Option<LastModel>,
/// The same totals per project key and model (the folder the agent named
/// in each request). Turns that named none stay in `models` only, which
/// remains the device total.
#[serde(default)]
pub projects: HashMap<String, HashMap<String, ModelUsage>>,
}
/// The model the proxy saw last, with the time it saw it.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct LastModel {
pub model: String,
pub at: u64,
}
const PROXY_USAGE_FILE: &str = "proxy_usage.json";
/// Where the measured spend totals live in the data directory.
#[must_use]
pub fn usage_path() -> Option<std::path::PathBuf> {
crate::core::data_dir::lean_ctx_data_dir()
.ok()
.map(|d| d.join(PROXY_USAGE_FILE))
}
/// Cross-process read of the persisted measured spend (dashboard / CLI / ledger).
#[must_use]
pub fn load_persisted() -> Option<PersistedUsage> {
let path = usage_path()?;
let data = std::fs::read_to_string(path).ok()?;
serde_json::from_str(&data).ok()
}
/// The model carrying the most measured tokens (excludes the "unknown" bucket).
/// Used to value savings against the real dominant model when no explicit model
/// is configured.
#[must_use]
pub fn persisted_dominant_model() -> Option<String> {
let persisted = load_persisted()?;
persisted
.models
.iter()
.filter(|(m, _)| m.as_str() != "unknown" && !m.trim().is_empty())
.max_by_key(|(_, u)| u.billable_tokens())
.filter(|(_, u)| u.billable_tokens() > 0)
.map(|(m, _)| m.clone())
}