Skip to main content

agent_proxy_rust_core/
compression.rs

1//! Compression statistics tracking across three layers.
2//!
3//! # Layers
4//!
5//! - **tokenless** (external hook/rewrite): reported via `TOKENLESS_TOKENS` env var
6//! - **compress middleware** (agent-proxy): `SchemaCompressor` + `ResponseCompressor`
7//! - **upstream** (API response): usage fields extracted by cost module
8//!
9//! # Data flow
10//!
11//! ```text
12//! tokenless env var → parse → CompressionStats → ctx.extensions
13//!   → compress middleware appends schema/response stats
14//!   → cost module reads to compute saved_cost
15//! ```
16
17/// Complete multi-layer compression statistics.
18#[derive(Debug, Clone, Default)]
19pub struct CompressionStats {
20    // ── Layer 1: tokenless (from TOKENLESS_TOKENS env var) ──
21    /// Request token count before tokenless compression.
22    pub tokenless_pre: u64,
23    /// Request token count after tokenless compression.
24    pub tokenless_post: u64,
25    /// Additional tokens saved by tokenless experimental mode.
26    pub tokenless_experimental: u64,
27
28    // ── Layer 2: compress middleware ──
29    /// Request body token count before agent-proxy schema compression.
30    pub proxy_req_pre: u64,
31    /// Request body token count after agent-proxy schema compression.
32    pub proxy_req_post: u64,
33    /// Response body token count before agent-proxy response compression.
34    pub proxy_res_pre: u64,
35    /// Response body token count after agent-proxy response compression.
36    pub proxy_res_post: u64,
37
38    // ── External: RTK (from x-rtk-tokens env var) ──
39    /// Tokens saved by RTK rewrite before the request reaches agent-proxy.
40    pub rtk_saved: u64,
41}
42
43impl CompressionStats {
44    /// Total tokens saved at the tokenless layer.
45    #[must_use]
46    pub fn tokenless_saved(&self) -> u64 {
47        self.tokenless_pre.saturating_sub(self.tokenless_post)
48    }
49
50    /// Tokens saved by agent-proxy schema compression.
51    #[must_use]
52    pub fn proxy_schema_saved(&self) -> u64 {
53        self.proxy_req_pre.saturating_sub(self.proxy_req_post)
54    }
55
56    /// Tokens saved by agent-proxy response compression.
57    #[must_use]
58    pub fn proxy_response_saved(&self) -> u64 {
59        self.proxy_res_pre.saturating_sub(self.proxy_res_post)
60    }
61
62    /// Total tokens saved across all layers.
63    #[must_use]
64    pub fn total_saved(&self) -> u64 {
65        self.tokenless_saved()
66            + self.tokenless_experimental
67            + self.proxy_schema_saved()
68            + self.proxy_response_saved()
69            + self.rtk_saved
70    }
71}
72
73/// Parses the `TOKENLESS_TOKENS` environment variable into [`CompressionStats`].
74///
75/// Expected format: `{"pre":800,"post":300,"experimental":150}`
76/// Returns [`CompressionStats::default()`] if the env var is not set or invalid.
77#[must_use]
78pub fn read_tokenless_stats() -> CompressionStats {
79    let Ok(value) = std::env::var("TOKENLESS_TOKENS") else {
80        return CompressionStats::default();
81    };
82    if value.is_empty() {
83        return CompressionStats::default();
84    }
85    parse_tokenless_json(&value).unwrap_or_default()
86}
87
88fn parse_tokenless_json(json: &str) -> Option<CompressionStats> {
89    #[derive(serde::Deserialize)]
90    struct TokenlessHeader {
91        pre: Option<u64>,
92        post: Option<u64>,
93        experimental: Option<u64>,
94    }
95    let h: TokenlessHeader = serde_json::from_str(json).ok()?;
96    Some(CompressionStats {
97        tokenless_pre: h.pre.unwrap_or(0),
98        tokenless_post: h.post.unwrap_or(0),
99        tokenless_experimental: h.experimental.unwrap_or(0),
100        ..Default::default()
101    })
102}
103
104// ---------------------------------------------------------------------------
105// Tests
106// ---------------------------------------------------------------------------
107
108#[cfg(test)]
109#[allow(clippy::unwrap_used)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn test_default_is_zero() {
115        let s = CompressionStats::default();
116        assert_eq!(s.tokenless_pre, 0);
117        assert_eq!(s.total_saved(), 0);
118    }
119
120    #[test]
121    fn test_tokenless_saved() {
122        let s = CompressionStats {
123            tokenless_pre: 800,
124            tokenless_post: 300,
125            ..Default::default()
126        };
127        assert_eq!(s.tokenless_saved(), 500);
128    }
129
130    #[test]
131    fn test_saturating_sub_prevents_underflow() {
132        let s = CompressionStats {
133            tokenless_pre: 100,
134            tokenless_post: 200,
135            ..Default::default()
136        };
137        assert_eq!(s.tokenless_saved(), 0);
138    }
139
140    #[test]
141    fn test_total_saved_sums_all_layers() {
142        let s = CompressionStats {
143            tokenless_pre: 800,
144            tokenless_post: 300,         // saved 500
145            tokenless_experimental: 150, // extra 150
146            proxy_req_pre: 300,
147            proxy_req_post: 220, // saved 80
148            proxy_res_pre: 1200,
149            proxy_res_post: 800, // saved 400
150            rtk_saved: 0,
151        };
152        // 500 + 150 + 80 + 400 + 0 = 1130
153        assert_eq!(s.total_saved(), 1130);
154    }
155
156    #[test]
157    fn test_parse_valid_json() {
158        let stats = parse_tokenless_json(r#"{"pre":800,"post":300,"experimental":150}"#).unwrap();
159        assert_eq!(stats.tokenless_pre, 800);
160        assert_eq!(stats.tokenless_post, 300);
161        assert_eq!(stats.tokenless_experimental, 150);
162    }
163
164    #[test]
165    fn test_parse_partial_json() {
166        let stats = parse_tokenless_json(r#"{"pre":500}"#).unwrap();
167        assert_eq!(stats.tokenless_pre, 500);
168        assert_eq!(stats.tokenless_post, 0);
169        assert_eq!(stats.tokenless_experimental, 0);
170    }
171
172    #[test]
173    fn test_parse_invalid_json_returns_none() {
174        assert!(parse_tokenless_json("not json").is_none());
175        assert!(parse_tokenless_json("").is_none());
176    }
177
178    #[test]
179    fn test_parse_missing_all_fields_returns_default() {
180        let stats = parse_tokenless_json(r"{}").unwrap();
181        assert_eq!(stats.tokenless_pre, 0);
182    }
183
184    #[test]
185    fn test_proxy_schema_saved() {
186        let s = CompressionStats {
187            proxy_req_pre: 500,
188            proxy_req_post: 350,
189            ..Default::default()
190        };
191        assert_eq!(s.proxy_schema_saved(), 150);
192    }
193
194    #[test]
195    fn test_proxy_response_saved() {
196        let s = CompressionStats {
197            proxy_res_pre: 2000,
198            proxy_res_post: 1200,
199            ..Default::default()
200        };
201        assert_eq!(s.proxy_response_saved(), 800);
202    }
203}