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/// Current version of the ETPAO calculation formula.
10/// Increment when formula logic changes to maintain reproducibility.
11pub const ETPAO_FORMULA_VERSION: &str = "1.0.0";
12
13/// Scalar ETPAO value stored in execution-ledger projections.
14pub type EtpaoResult = f64;
15
16/// Provider pricing rates (per million tokens).
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct TokenPricing {
19    pub fresh_input: f64,
20    pub cached_input: f64,
21    pub output: f64,
22    pub reasoning: f64,
23}
24
25impl Default for TokenPricing {
26    fn default() -> Self {
27        Self {
28            fresh_input: 3.00,
29            cached_input: 0.30,
30            output: 15.00,
31            reasoning: 15.00,
32        }
33    }
34}
35
36/// Token usage record for a single session/task.
37#[derive(Debug, Clone, Default, Serialize, Deserialize)]
38pub struct TokenUsage {
39    pub fresh_input: u64,
40    pub cached_input: u64,
41    pub output: u64,
42    pub reasoning: u64,
43}
44
45impl TokenUsage {
46    /// Cost-weighted token count using provider pricing.
47    pub fn weighted_tokens(&self, pricing: &TokenPricing) -> f64 {
48        let normalize = pricing.fresh_input;
49        if normalize == 0.0 {
50            return 0.0;
51        }
52        self.fresh_input as f64
53            + (self.cached_input as f64 * pricing.cached_input / normalize)
54            + (self.output as f64 * pricing.output / normalize)
55            + (self.reasoning as f64 * pricing.reasoning / normalize)
56    }
57
58    /// Total raw token count (unweighted).
59    pub fn total_raw(&self) -> u64 {
60        self.fresh_input + self.cached_input + self.output + self.reasoning
61    }
62
63    /// Estimated cost in USD.
64    pub fn cost_usd(&self, pricing: &TokenPricing) -> f64 {
65        (self.fresh_input as f64 * pricing.fresh_input
66            + self.cached_input as f64 * pricing.cached_input
67            + self.output as f64 * pricing.output
68            + self.reasoning as f64 * pricing.reasoning)
69            / 1_000_000.0
70    }
71}
72
73/// ETPAO comparison between lean-ctx and baseline.
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct EtpaoReport {
76    pub leanctx_usage: TokenUsage,
77    pub baseline_usage: TokenUsage,
78    pub leanctx_weighted: f64,
79    pub baseline_weighted: f64,
80    pub delta_pct: f64,
81    pub leanctx_cost_usd: f64,
82    pub baseline_cost_usd: f64,
83    pub cost_delta_pct: f64,
84}
85
86impl EtpaoReport {
87    /// Compute ETPAO comparison.
88    pub fn compute(leanctx: TokenUsage, baseline: TokenUsage, pricing: &TokenPricing) -> Self {
89        let lw = leanctx.weighted_tokens(pricing);
90        let bw = baseline.weighted_tokens(pricing);
91        let delta_pct = if bw > 0.0 {
92            ((lw - bw) / bw) * 100.0
93        } else {
94            0.0
95        };
96
97        let lc = leanctx.cost_usd(pricing);
98        let bc = baseline.cost_usd(pricing);
99        let cost_delta = if bc > 0.0 {
100            ((lc - bc) / bc) * 100.0
101        } else {
102            0.0
103        };
104
105        Self {
106            leanctx_usage: leanctx,
107            baseline_usage: baseline,
108            leanctx_weighted: lw,
109            baseline_weighted: bw,
110            delta_pct,
111            leanctx_cost_usd: lc,
112            baseline_cost_usd: bc,
113            cost_delta_pct: cost_delta,
114        }
115    }
116
117    /// True if lean-ctx is cheaper than baseline.
118    pub fn is_cost_efficient(&self) -> bool {
119        self.cost_delta_pct < 0.0
120    }
121}
122
123#[cfg(test)]
124pub mod tests {
125    use super::*;
126
127    #[test]
128    fn weighted_tokens_accounts_for_pricing() {
129        let usage = TokenUsage {
130            fresh_input: 1_000_000,
131            cached_input: 1_000_000,
132            output: 100_000,
133            reasoning: 0,
134        };
135        let pricing = TokenPricing::default();
136        let weighted = usage.weighted_tokens(&pricing);
137        // fresh: 1M * 1.0 + cached: 1M * 0.1 + output: 100k * 5.0
138        assert!((weighted - 1_600_000.0).abs() < 1.0);
139    }
140
141    #[test]
142    fn cost_usd_calculation() {
143        let usage = TokenUsage {
144            fresh_input: 1_000_000,
145            cached_input: 0,
146            output: 0,
147            reasoning: 0,
148        };
149        let cost = usage.cost_usd(&TokenPricing::default());
150        assert!((cost - 3.00).abs() < 0.01);
151    }
152
153    #[test]
154    fn etpao_report_negative_delta_means_savings() {
155        let leanctx = TokenUsage {
156            fresh_input: 500_000,
157            cached_input: 500_000,
158            output: 50_000,
159            reasoning: 0,
160        };
161        let baseline = TokenUsage {
162            fresh_input: 1_000_000,
163            cached_input: 0,
164            output: 50_000,
165            reasoning: 0,
166        };
167        let report = EtpaoReport::compute(leanctx, baseline, &TokenPricing::default());
168        assert!(report.is_cost_efficient());
169        assert!(report.delta_pct < 0.0);
170    }
171
172    #[test]
173    fn etpao_report_positive_delta_means_overhead() {
174        let leanctx = TokenUsage {
175            fresh_input: 1_200_000,
176            cached_input: 0,
177            output: 50_000,
178            reasoning: 0,
179        };
180        let baseline = TokenUsage {
181            fresh_input: 1_000_000,
182            cached_input: 0,
183            output: 50_000,
184            reasoning: 0,
185        };
186        let report = EtpaoReport::compute(leanctx, baseline, &TokenPricing::default());
187        assert!(!report.is_cost_efficient());
188        assert!(report.delta_pct > 0.0);
189    }
190}