Skip to main content

lean_ctx/core/
etpao.rs

1//! ETPAO: Effective Tokens per Accepted Outcome (#1318).
2//!
3//! The canonical efficiency metric that accounts for the full provider
4//! cost structure: fresh input, cached input, output, and reasoning tokens
5//! have different costs and should be weighted accordingly.
6
7use serde::{Deserialize, Serialize};
8
9/// Provider pricing rates (per million tokens).
10#[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/// Token usage record for a single session/task.
30#[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    /// Cost-weighted token count using provider pricing.
40    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    /// Total raw token count (unweighted).
52    pub fn total_raw(&self) -> u64 {
53        self.fresh_input + self.cached_input + self.output + self.reasoning
54    }
55
56    /// Estimated cost in USD.
57    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/// ETPAO comparison between lean-ctx and baseline.
67#[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    /// Compute ETPAO comparison.
81    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    /// True if lean-ctx is cheaper than baseline.
111    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        // fresh: 1M * 1.0 + cached: 1M * 0.1 + output: 100k * 5.0
131        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}