Skip to main content

lean_ctx/core/
energy.rs

1//! Electricity-footprint estimate for saved tokens.
2//!
3//! This mirrors the website `/metrics` methodology (same `J_PER_TOKEN`) so a user's local
4//! "energy saved" figure and the community scoreboard always reconcile.
5//!
6//! ~0.4 J per saved token is a deliberately conservative midpoint of measured modern
7//! inference (Llama-3-70B FP8 on H100 + vLLM ≈ 0.39 J/token, John Snow Labs "Tokens per
8//! Joule", 2025; query-level estimates such as Epoch AI's ~0.3 Wh per GPT-4o query imply
9//! more). lean-ctx mostly removes cheaper prefill/context tokens, so we never overstate.
10//! Real figures vary by model and hardware — this is always surfaced as an estimate.
11
12/// Joules of inference compute avoided per token that lean-ctx kept out of the context.
13pub const J_PER_TOKEN: f64 = 0.4;
14
15/// Watt-hours in a full smartphone charge — the relatable yardstick used in the UI.
16pub const WH_PER_PHONE_CHARGE: f64 = 12.0;
17
18/// Grams of CO₂-equivalent per kWh of grid electricity. ~475 g/kWh is the global
19/// average grid carbon intensity (IEA, ~2023) — a transparent, conservative
20/// midpoint. Users on cleaner grids can override via `LEAN_CTX_GRID_CO2_G_PER_KWH`
21/// so the local footprint reflects their actual electricity mix.
22pub const G_CO2_PER_KWH: f64 = 475.0;
23
24/// Energy (Wh) saved for a given number of saved tokens. `Wh = tokens · J/token / 3600`.
25#[must_use]
26pub fn wh_for_tokens(tokens_saved: u64) -> f64 {
27    tokens_saved as f64 * J_PER_TOKEN / 3600.0
28}
29
30/// Equivalent number of full smartphone charges for a given saved-token count.
31#[must_use]
32pub fn phone_charges(tokens_saved: u64) -> f64 {
33    wh_for_tokens(tokens_saved) / WH_PER_PHONE_CHARGE
34}
35
36/// Effective grid carbon intensity (g CO₂/kWh), honoring the
37/// `LEAN_CTX_GRID_CO2_G_PER_KWH` override when it is a positive, finite number.
38#[must_use]
39pub fn grid_co2_g_per_kwh() -> f64 {
40    std::env::var("LEAN_CTX_GRID_CO2_G_PER_KWH")
41        .ok()
42        .and_then(|v| v.trim().parse::<f64>().ok())
43        .filter(|v| v.is_finite() && *v > 0.0)
44        .unwrap_or(G_CO2_PER_KWH)
45}
46
47/// Grams of CO₂-equivalent avoided for a given number of saved tokens.
48/// `g = Wh / 1000 · gridIntensity`.
49#[must_use]
50pub fn co2_grams_for_tokens(tokens_saved: u64) -> f64 {
51    wh_for_tokens(tokens_saved) / 1_000.0 * grid_co2_g_per_kwh()
52}
53
54/// Human-readable CO₂ mass with adaptive units (`g` / `kg` / `t`).
55#[must_use]
56pub fn format_co2(grams: f64) -> String {
57    if !grams.is_finite() || grams <= 0.0 {
58        "0 g".to_string()
59    } else if grams >= 1_000_000.0 {
60        format!("{:.1} t", grams / 1_000_000.0)
61    } else if grams >= 1_000.0 {
62        format!("{:.1} kg", grams / 1_000.0)
63    } else {
64        format!("{grams:.0} g")
65    }
66}
67
68/// Convenience: formatted CO₂ string straight from a saved-token count.
69#[must_use]
70pub fn format_co2_for_tokens(tokens_saved: u64) -> String {
71    format_co2(co2_grams_for_tokens(tokens_saved))
72}
73
74/// Human-readable energy with adaptive units (`Wh` / `kWh` / `MWh`).
75#[must_use]
76pub fn format_wh(wh: f64) -> String {
77    if !wh.is_finite() || wh <= 0.0 {
78        "0 Wh".to_string()
79    } else if wh >= 1_000_000.0 {
80        format!("{:.1} MWh", wh / 1_000_000.0)
81    } else if wh >= 1_000.0 {
82        format!("{:.1} kWh", wh / 1_000.0)
83    } else {
84        format!("{wh:.0} Wh")
85    }
86}
87
88/// Convenience: formatted energy string straight from a saved-token count.
89#[must_use]
90pub fn format_for_tokens(tokens_saved: u64) -> String {
91    format_wh(wh_for_tokens(tokens_saved))
92}
93
94/// Rounded phone-charge equivalent as a display string (e.g. `"≈ 117 phone charges"`),
95/// or `None` when the saving is too small to round to at least one charge.
96#[must_use]
97pub fn phone_charges_hint(tokens_saved: u64) -> Option<String> {
98    let charges = phone_charges(tokens_saved);
99    if charges < 0.5 {
100        return None;
101    }
102    Some(format!("≈ {} phone charges", charges.round() as u64))
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn wh_scales_linearly_with_tokens() {
111        // 9000 tokens · 0.4 J / 3600 = exactly 1 Wh.
112        assert!((wh_for_tokens(9_000) - 1.0).abs() < 1e-9);
113        assert!((wh_for_tokens(0)).abs() < 1e-9);
114    }
115
116    #[test]
117    fn format_picks_adaptive_units() {
118        assert_eq!(format_wh(0.0), "0 Wh");
119        assert_eq!(format_wh(-5.0), "0 Wh");
120        assert_eq!(format_wh(42.4), "42 Wh");
121        assert_eq!(format_wh(1_500.0), "1.5 kWh");
122        assert_eq!(format_wh(2_500_000.0), "2.5 MWh");
123    }
124
125    #[test]
126    fn format_for_tokens_matches_methodology() {
127        // 12.8M tokens ≈ 1422 Wh ≈ 1.4 kWh (the figure a real user would see).
128        assert_eq!(format_for_tokens(12_800_000), "1.4 kWh");
129    }
130
131    #[test]
132    fn phone_charge_hint_suppressed_when_tiny() {
133        assert!(phone_charges_hint(0).is_none());
134        // 12 Wh = 1 charge needs 108k tokens; far below that → no hint.
135        assert!(phone_charges_hint(1_000).is_none());
136        assert!(phone_charges_hint(12_800_000).is_some());
137    }
138
139    #[test]
140    fn co2_scales_with_energy_and_grid_intensity() {
141        // 9000 tokens = 1 Wh = 0.001 kWh · 475 g/kWh = 0.475 g.
142        let g = co2_grams_for_tokens(9_000);
143        assert!((g - 0.475).abs() < 1e-6, "got {g}");
144        assert!(co2_grams_for_tokens(0).abs() < 1e-12);
145        // Linear in tokens.
146        assert!((co2_grams_for_tokens(18_000) - 2.0 * g).abs() < 1e-6);
147    }
148
149    #[test]
150    fn format_co2_picks_adaptive_units() {
151        assert_eq!(format_co2(0.0), "0 g");
152        assert_eq!(format_co2(-3.0), "0 g");
153        assert_eq!(format_co2(42.4), "42 g");
154        assert_eq!(format_co2(1_500.0), "1.5 kg");
155        assert_eq!(format_co2(2_500_000.0), "2.5 t");
156    }
157
158    #[test]
159    fn grid_intensity_default_when_no_override() {
160        // The override is read from the environment; without it we use the constant.
161        // (Env mutation is covered indirectly; here we assert the documented default.)
162        crate::test_env::remove_var("LEAN_CTX_GRID_CO2_G_PER_KWH");
163        assert!((grid_co2_g_per_kwh() - G_CO2_PER_KWH).abs() < 1e-9);
164    }
165}