lean-ctx 3.9.13

Context Runtime for AI Agents with CCP. 71 MCP tools, 10 read modes, 95+ compression patterns, cross-session memory (CCP), persistent AI knowledge with temporal facts + contradiction detection, multi-agent context sharing, LITM-aware positioning, AAAK compact format, adaptive compression with Thompson Sampling bandits. Supports 24+ AI tools. Reduces LLM token consumption by up to 99%.
Documentation
//! Provider-stream economics for gain reporting (#1191).

use serde::{Deserialize, Serialize};

use super::model_pricing::ModelCost;
use crate::core::context_overhead::NATIVE_BASELINE_TOKENS_PER_TURN;

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
pub struct StreamSavings {
    pub first_inject_tokens_saved: u64,
    pub reread_tokens_saved: u64,
    pub input_tokens_saved: u64,
    pub output_tokens_saved: u64,
    pub first_inject_overhead_tokens: u64,
    pub reread_overhead_tokens: u64,
    pub bounce_tokens: u64,
    pub cache_write_usd_saved: f64,
    pub cache_read_usd_saved: f64,
    pub input_usd_saved: f64,
    pub output_usd_saved: f64,
    pub gross_usd_saved: f64,
    pub overhead_usd: f64,
    pub net_usd_saved: f64,
}

impl StreamSavings {
    #[must_use]
    pub fn calculate(
        first_inject_tokens_saved: u64,
        reread_tokens_saved: u64,
        overhead_per_turn: u64,
        turns: u64,
        bounce_tokens: u64,
        cost: ModelCost,
    ) -> Self {
        let overhead_delta = overhead_per_turn.saturating_sub(NATIVE_BASELINE_TOKENS_PER_TURN);
        let first_inject_overhead_tokens = if turns > 0 { overhead_delta } else { 0 };
        let reread_overhead_tokens = overhead_delta.saturating_mul(turns.saturating_sub(1));
        let cache_write_usd_saved = usd(first_inject_tokens_saved, cost.cache_write_per_m);
        let cache_read_usd_saved = usd(reread_tokens_saved, cost.cache_read_per_m);
        let gross_usd_saved = cache_write_usd_saved + cache_read_usd_saved;
        let overhead_usd = usd(
            first_inject_overhead_tokens.saturating_add(bounce_tokens),
            cost.cache_write_per_m,
        ) + usd(reread_overhead_tokens, cost.cache_read_per_m);

        Self {
            first_inject_tokens_saved,
            reread_tokens_saved,
            input_tokens_saved: 0,
            output_tokens_saved: 0,
            first_inject_overhead_tokens,
            reread_overhead_tokens,
            bounce_tokens,
            cache_write_usd_saved,
            cache_read_usd_saved,
            input_usd_saved: 0.0,
            output_usd_saved: 0.0,
            gross_usd_saved,
            overhead_usd,
            net_usd_saved: gross_usd_saved - overhead_usd,
        }
    }
}

fn usd(tokens: u64, rate_per_m: f64) -> f64 {
    tokens as f64 / 1_000_000.0 * rate_per_m
}

#[cfg(test)]
mod tests {
    use super::*;

    const REAL_RATES: ModelCost = ModelCost {
        input_per_m: 5.0,
        output_per_m: 25.0,
        cache_write_per_m: 6.25,
        cache_read_per_m: 0.5,
    };

    #[test]
    fn million_first_inject_tokens_save_cache_write_rate() {
        let s = StreamSavings::calculate(1_000_000, 0, 2_400, 1, 0, REAL_RATES);
        assert_eq!(s.cache_write_usd_saved, 6.25);
        assert_eq!(s.net_usd_saved, 6.25);
    }

    #[test]
    fn million_reread_tokens_save_only_cache_read_rate() {
        let s = StreamSavings::calculate(0, 1_000_000, 2_400, 1, 0, REAL_RATES);
        assert_eq!(s.cache_read_usd_saved, 0.5);
        assert_eq!(s.net_usd_saved, 0.5);
    }

    #[test]
    fn same_token_count_is_twelve_and_half_times_more_valuable_on_first_inject() {
        let s = StreamSavings::calculate(100_000, 100_000, 2_400, 1, 0, REAL_RATES);
        assert_eq!(s.cache_write_usd_saved / s.cache_read_usd_saved, 12.5);
    }

    #[test]
    fn fixed_overhead_splits_into_write_then_reads() {
        let s = StreamSavings::calculate(0, 0, 3_400, 4, 0, REAL_RATES);
        assert_eq!(s.first_inject_overhead_tokens, 1_000);
        assert_eq!(s.reread_overhead_tokens, 3_000);
        assert!((s.overhead_usd - 0.00775).abs() < 1e-12);
    }

    #[test]
    fn native_tool_baseline_is_not_charged_to_lean_ctx() {
        let s = StreamSavings::calculate(10_000, 0, 2_000, 50, 0, REAL_RATES);
        assert_eq!(s.first_inject_overhead_tokens, 0);
        assert_eq!(s.reread_overhead_tokens, 0);
        assert_eq!(s.overhead_usd, 0.0);
    }

    #[test]
    fn no_observed_turn_means_no_guessed_overhead() {
        let s = StreamSavings::calculate(10_000, 0, 9_000, 0, 0, REAL_RATES);
        assert_eq!(s.first_inject_overhead_tokens, 0);
        assert_eq!(s.reread_overhead_tokens, 0);
    }

    #[test]
    fn bounce_waste_can_make_net_savings_negative() {
        let s = StreamSavings::calculate(100, 0, 2_400, 1, 1_000, REAL_RATES);
        assert!(s.net_usd_saved < 0.0);
        assert!((s.overhead_usd - 0.00625).abs() < 1e-12);
    }

    #[test]
    fn output_and_new_input_are_zero_without_measured_evidence() {
        let s = StreamSavings::calculate(1_000, 2_000, 2_400, 2, 0, REAL_RATES);
        assert_eq!(s.input_tokens_saved, 0);
        assert_eq!(s.output_tokens_saved, 0);
        assert_eq!(s.input_usd_saved, 0.0);
        assert_eq!(s.output_usd_saved, 0.0);
    }
}