1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct TokenPricing {
12 pub fresh_input: f64,
13 pub cached_input: f64,
14 pub output: f64,
15 pub reasoning: f64,
16}
17
18impl Default for TokenPricing {
19 fn default() -> Self {
20 Self {
21 fresh_input: 3.00,
22 cached_input: 0.30,
23 output: 15.00,
24 reasoning: 15.00,
25 }
26 }
27}
28
29#[derive(Debug, Clone, Default, Serialize, Deserialize)]
31pub struct TokenUsage {
32 pub fresh_input: u64,
33 pub cached_input: u64,
34 pub output: u64,
35 pub reasoning: u64,
36}
37
38impl TokenUsage {
39 pub fn weighted_tokens(&self, pricing: &TokenPricing) -> f64 {
41 let normalize = pricing.fresh_input;
42 if normalize == 0.0 {
43 return 0.0;
44 }
45 self.fresh_input as f64
46 + (self.cached_input as f64 * pricing.cached_input / normalize)
47 + (self.output as f64 * pricing.output / normalize)
48 + (self.reasoning as f64 * pricing.reasoning / normalize)
49 }
50
51 pub fn total_raw(&self) -> u64 {
53 self.fresh_input + self.cached_input + self.output + self.reasoning
54 }
55
56 pub fn cost_usd(&self, pricing: &TokenPricing) -> f64 {
58 (self.fresh_input as f64 * pricing.fresh_input
59 + self.cached_input as f64 * pricing.cached_input
60 + self.output as f64 * pricing.output
61 + self.reasoning as f64 * pricing.reasoning)
62 / 1_000_000.0
63 }
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct EtpaoReport {
69 pub leanctx_usage: TokenUsage,
70 pub baseline_usage: TokenUsage,
71 pub leanctx_weighted: f64,
72 pub baseline_weighted: f64,
73 pub delta_pct: f64,
74 pub leanctx_cost_usd: f64,
75 pub baseline_cost_usd: f64,
76 pub cost_delta_pct: f64,
77}
78
79impl EtpaoReport {
80 pub fn compute(leanctx: TokenUsage, baseline: TokenUsage, pricing: &TokenPricing) -> Self {
82 let lw = leanctx.weighted_tokens(pricing);
83 let bw = baseline.weighted_tokens(pricing);
84 let delta_pct = if bw > 0.0 {
85 ((lw - bw) / bw) * 100.0
86 } else {
87 0.0
88 };
89
90 let lc = leanctx.cost_usd(pricing);
91 let bc = baseline.cost_usd(pricing);
92 let cost_delta = if bc > 0.0 {
93 ((lc - bc) / bc) * 100.0
94 } else {
95 0.0
96 };
97
98 Self {
99 leanctx_usage: leanctx,
100 baseline_usage: baseline,
101 leanctx_weighted: lw,
102 baseline_weighted: bw,
103 delta_pct,
104 leanctx_cost_usd: lc,
105 baseline_cost_usd: bc,
106 cost_delta_pct: cost_delta,
107 }
108 }
109
110 pub fn is_cost_efficient(&self) -> bool {
112 self.cost_delta_pct < 0.0
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn weighted_tokens_accounts_for_pricing() {
122 let usage = TokenUsage {
123 fresh_input: 1_000_000,
124 cached_input: 1_000_000,
125 output: 100_000,
126 reasoning: 0,
127 };
128 let pricing = TokenPricing::default();
129 let weighted = usage.weighted_tokens(&pricing);
130 assert!((weighted - 1_600_000.0).abs() < 1.0);
132 }
133
134 #[test]
135 fn cost_usd_calculation() {
136 let usage = TokenUsage {
137 fresh_input: 1_000_000,
138 cached_input: 0,
139 output: 0,
140 reasoning: 0,
141 };
142 let cost = usage.cost_usd(&TokenPricing::default());
143 assert!((cost - 3.00).abs() < 0.01);
144 }
145
146 #[test]
147 fn etpao_report_negative_delta_means_savings() {
148 let leanctx = TokenUsage {
149 fresh_input: 500_000,
150 cached_input: 500_000,
151 output: 50_000,
152 reasoning: 0,
153 };
154 let baseline = TokenUsage {
155 fresh_input: 1_000_000,
156 cached_input: 0,
157 output: 50_000,
158 reasoning: 0,
159 };
160 let report = EtpaoReport::compute(leanctx, baseline, &TokenPricing::default());
161 assert!(report.is_cost_efficient());
162 assert!(report.delta_pct < 0.0);
163 }
164
165 #[test]
166 fn etpao_report_positive_delta_means_overhead() {
167 let leanctx = TokenUsage {
168 fresh_input: 1_200_000,
169 cached_input: 0,
170 output: 50_000,
171 reasoning: 0,
172 };
173 let baseline = TokenUsage {
174 fresh_input: 1_000_000,
175 cached_input: 0,
176 output: 50_000,
177 reasoning: 0,
178 };
179 let report = EtpaoReport::compute(leanctx, baseline, &TokenPricing::default());
180 assert!(!report.is_cost_efficient());
181 assert!(report.delta_pct > 0.0);
182 }
183}