Skip to main content

lean_ctx/core/gain/
evidence.rs

1// SPDX-License-Identifier: Apache-2.0
2//! How strong is a gain number? Economic evidence levels for gain reporting.
3//!
4//! Tool-output token reduction is not a provider bill reduction: the provider bill
5//! depends on turns, cache writes and reads, retries and the whole trajectory. A gain
6//! figure therefore carries the evidence it rests on, and a figure the data path
7//! cannot observe is reported as unknown — never filled in from the gross number.
8
9use serde::{Deserialize, Serialize};
10
11/// Evidence behind an economic figure, ordered from weakest to strongest.
12#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum EconomicEvidence {
15    /// Raw vs. transformed token counts computed locally; no traffic observed.
16    #[default]
17    LocalEstimate,
18    /// lean-ctx saw the tool traffic it transformed (hooks, MCP tools) but not the
19    /// provider request path — the integrated mode of subscription clients.
20    ObservedToolTraffic,
21    /// The provider's own counter priced the original request shape. A counterfactual
22    /// for one request, not a bill for an alternative trajectory.
23    ProviderCountedInput,
24    /// The proxy carried the treatment requests and saw their provider usage; the
25    /// savings side is still an estimate against an unobserved baseline.
26    ProviderMeasuredUsage,
27    /// Paired control/treatment provider cost. The only level that supports an
28    /// end-to-end bill-saving claim.
29    PairedControl,
30}
31
32impl EconomicEvidence {
33    pub fn label(self) -> &'static str {
34        match self {
35            Self::LocalEstimate => "local estimate",
36            Self::ObservedToolTraffic => "observed tool traffic (provider path not visible)",
37            Self::ProviderCountedInput => "provider-counted input (counterfactual)",
38            Self::ProviderMeasuredUsage => "provider-measured usage (savings estimated)",
39            Self::PairedControl => "paired control/treatment",
40        }
41    }
42
43    /// Whether a net provider-bill figure can be computed at all: only when the
44    /// provider request path was observed.
45    pub fn bill_impact_observable(self) -> bool {
46        self >= Self::ProviderMeasuredUsage
47    }
48}
49
50/// The economic side of a gain summary, with unobservable figures left as `None`.
51#[derive(Debug, Clone, Copy, PartialEq)]
52pub struct EconomicView {
53    pub evidence: EconomicEvidence,
54    pub net_bill_impact_tokens: Option<i64>,
55    pub net_bill_impact_usd: Option<f64>,
56    pub roi: Option<f64>,
57}
58
59/// Classify what was observed and keep only the figures the data path supports.
60///
61/// `provider_turns` is the number of provider requests the proxy carried; `0` means
62/// the proxy is not in the request path (for example a Claude Code subscription,
63/// where only hooks and MCP tools pass through lean-ctx). Then the net bill impact
64/// and ROI are unknown — not the gross savings, and not zero.
65pub fn economic_view(
66    observed_commands: u64,
67    provider_turns: u64,
68    net_tokens_if_observed: i64,
69    net_usd_if_observed: f64,
70    tool_spend_usd: f64,
71) -> EconomicView {
72    let evidence = if provider_turns > 0 {
73        EconomicEvidence::ProviderMeasuredUsage
74    } else if observed_commands > 0 {
75        EconomicEvidence::ObservedToolTraffic
76    } else {
77        EconomicEvidence::LocalEstimate
78    };
79    let observable = evidence.bill_impact_observable();
80    EconomicView {
81        evidence,
82        net_bill_impact_tokens: observable.then_some(net_tokens_if_observed),
83        net_bill_impact_usd: observable.then_some(net_usd_if_observed),
84        roi: (observable && tool_spend_usd > 0.0).then(|| net_usd_if_observed / tool_spend_usd),
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn integrated_mode_never_reports_a_bill_impact_or_roi() {
94        // Claude Code subscription: thousands of observed tool calls, no provider turn.
95        let v = economic_view(4_200, 0, 1_500_000, 18.75, 3.0);
96        assert_eq!(v.evidence, EconomicEvidence::ObservedToolTraffic);
97        assert_eq!(
98            v.net_bill_impact_tokens, None,
99            "net must not fall back to gross"
100        );
101        assert_eq!(v.net_bill_impact_usd, None);
102        assert_eq!(v.roi, None);
103    }
104
105    #[test]
106    fn gateway_mode_reports_an_estimated_net_and_roi() {
107        let v = economic_view(4_200, 900, -12_000, -0.4, 2.0);
108        assert_eq!(v.evidence, EconomicEvidence::ProviderMeasuredUsage);
109        assert_eq!(
110            v.net_bill_impact_tokens,
111            Some(-12_000),
112            "a negative net is reported"
113        );
114        assert_eq!(v.roi, Some(-0.2));
115        assert_ne!(
116            v.evidence,
117            EconomicEvidence::PairedControl,
118            "measured usage is still not a paired bill claim"
119        );
120    }
121
122    #[test]
123    fn nothing_observed_is_a_local_estimate() {
124        let v = economic_view(0, 0, 0, 0.0, 0.0);
125        assert_eq!(v.evidence, EconomicEvidence::LocalEstimate);
126        assert_eq!(v.net_bill_impact_usd, None);
127    }
128}