lean_ctx/core/context_kernel/
mcp_receipt.rs1use std::collections::HashMap;
4use std::sync::{Mutex, MutexGuard, OnceLock};
5
6use super::accounting_fix::{PostDeliveryAccounting, compute_honest_accounting};
7
8#[derive(Debug, Clone)]
10pub struct McpReceipt {
11 pub tool: String,
13 pub tokens_in: usize,
15 pub tokens_out: usize,
17 pub kernel_overhead: usize,
19 pub accepted: bool,
21}
22
23#[derive(Debug, Clone, Default)]
25pub struct ToolSavings {
26 pub tool: String,
28 pub calls: usize,
30 pub tokens_in: usize,
32 pub tokens_out: usize,
34 pub kernel_overhead: usize,
36 pub honest_savings_pct: f64,
38}
39
40#[derive(Default)]
41struct McpReceiptStore {
42 receipts: Vec<McpReceipt>,
43 per_tool: HashMap<String, ToolSavings>,
44}
45
46static RECEIPTS: OnceLock<Mutex<McpReceiptStore>> = OnceLock::new();
47
48fn receipt_store() -> &'static Mutex<McpReceiptStore> {
49 RECEIPTS.get_or_init(|| Mutex::new(McpReceiptStore::default()))
50}
51
52fn lock_store() -> MutexGuard<'static, McpReceiptStore> {
53 match receipt_store().lock() {
54 Ok(store) => store,
55 Err(poisoned) => poisoned.into_inner(),
56 }
57}
58
59fn savings_pct(tokens_in: usize, delivered: usize) -> f64 {
60 if tokens_in == 0 {
61 0.0
62 } else {
63 (1.0 - delivered as f64 / tokens_in as f64) * 100.0
64 }
65}
66
67pub fn record_receipt(receipt: McpReceipt) {
69 let mut store = lock_store();
70 let summary = store
71 .per_tool
72 .entry(receipt.tool.clone())
73 .or_insert_with(|| ToolSavings {
74 tool: receipt.tool.clone(),
75 ..ToolSavings::default()
76 });
77 summary.calls = summary.calls.saturating_add(1);
78 summary.tokens_in = summary.tokens_in.saturating_add(receipt.tokens_in);
79 summary.tokens_out = summary.tokens_out.saturating_add(receipt.tokens_out);
80 summary.kernel_overhead = summary
81 .kernel_overhead
82 .saturating_add(receipt.kernel_overhead);
83 summary.honest_savings_pct = savings_pct(
84 summary.tokens_in,
85 summary.tokens_out.saturating_add(summary.kernel_overhead),
86 );
87 store.receipts.push(receipt);
88}
89
90pub fn mcp_accounting() -> PostDeliveryAccounting {
92 let store = lock_store();
93 let mut totals = (0usize, 0usize, 0usize);
94 for receipt in &store.receipts {
95 totals.0 = totals.0.saturating_add(receipt.tokens_in);
96 totals.1 = totals.1.saturating_add(receipt.tokens_out);
97 totals.2 = totals.2.saturating_add(receipt.kernel_overhead);
98 }
99 compute_honest_accounting(totals.0, totals.1, totals.2, 0)
100}
101
102pub fn per_tool_savings() -> Vec<ToolSavings> {
104 let mut summaries: Vec<_> = lock_store().per_tool.values().cloned().collect();
105 summaries.sort_unstable_by_key(|summary| summary.tool.clone());
106 summaries
107}
108
109pub fn savings_report() -> String {
111 per_tool_savings()
112 .iter()
113 .map(|summary| {
114 format!(
115 "{}: {} calls, {:.2}% savings",
116 summary.tool, summary.calls, summary.honest_savings_pct,
117 )
118 })
119 .collect::<Vec<_>>()
120 .join("\n")
121}
122
123pub fn total_kernel_overhead() -> usize {
125 mcp_accounting().kernel_overhead_tokens
126}
127
128pub fn reset_receipts() {
130 *lock_store() = McpReceiptStore::default();
131}
132
133#[cfg(test)]
134mod tests {
135 use super::{
136 McpReceipt, mcp_accounting, per_tool_savings, record_receipt, reset_receipts,
137 savings_report, total_kernel_overhead,
138 };
139 use std::sync::{Mutex, MutexGuard};
140 static TEST_LOCK: Mutex<()> = Mutex::new(());
141 fn isolated_test() -> MutexGuard<'static, ()> {
142 let guard = match TEST_LOCK.lock() {
143 Ok(guard) => guard,
144 Err(poisoned) => poisoned.into_inner(),
145 };
146 reset_receipts();
147 guard
148 }
149 fn receipt(tool: &str) -> McpReceipt {
150 McpReceipt {
151 tool: tool.to_owned(),
152 tokens_in: 100,
153 tokens_out: 40,
154 kernel_overhead: 10,
155 accepted: true,
156 }
157 }
158 #[test]
159 fn record_and_retrieve() {
160 let _guard = isolated_test();
161 record_receipt(receipt("read"));
162 record_receipt(receipt("search"));
163 record_receipt(receipt("read"));
164 assert_eq!(per_tool_savings().len(), 2);
165 }
166 #[test]
167 fn accounting_is_honest() {
168 let _guard = isolated_test();
169 record_receipt(receipt("read"));
170 let accounting = mcp_accounting();
171 assert_eq!(accounting.kernel_overhead_tokens, 10);
172 assert!((accounting.phantom_savings_pct - 0.1).abs() < f64::EPSILON);
173 }
174 #[test]
175 fn per_tool_aggregates() {
176 let _guard = isolated_test();
177 for _ in 0..3 {
178 record_receipt(receipt("read"));
179 }
180 let summaries = per_tool_savings();
181 assert_eq!(summaries[0].calls, 3);
182 assert_eq!(summaries[0].honest_savings_pct, 50.0);
183 }
184 #[test]
185 fn savings_report_formatted() {
186 let _guard = isolated_test();
187 record_receipt(receipt("search"));
188 let report = savings_report();
189 assert!(report.contains("search"));
190 assert!(report.contains("50.00% savings"));
191 }
192 #[test]
193 fn reset_clears_all() {
194 let _guard = isolated_test();
195 record_receipt(receipt("read"));
196 reset_receipts();
197 assert!(per_tool_savings().is_empty());
198 assert_eq!(total_kernel_overhead(), 0);
199 }
200}