1use axum::{Json, http::StatusCode, response::IntoResponse};
35use serde::{Deserialize, Serialize};
36use serde_json::Value;
37
38use crate::core::protocol::strip_trailing_savings_footer;
39use crate::core::tokens::count_tokens;
40
41use super::compress::compress_tool_result_gateway;
42
43const TOKENIZER: &str = "o200k_base";
46
47#[derive(Debug, Deserialize)]
48pub struct CompressRequest {
49 pub messages: Vec<Value>,
50 #[serde(default)]
53 pub model: Option<String>,
54}
55
56#[derive(Debug, Serialize)]
57pub struct CompressStats {
58 pub original_tokens: usize,
59 pub compressed_tokens: usize,
60 pub saved_tokens: usize,
61 pub saved_pct: f64,
63 pub tokenizer: &'static str,
64 #[serde(skip_serializing_if = "Option::is_none")]
65 pub model: Option<String>,
66}
67
68#[derive(Debug, Serialize)]
69pub struct CompressResponse {
70 pub messages: Vec<Value>,
71 pub stats: CompressStats,
72 pub tokens_before: usize,
77 pub tokens_after: usize,
78 pub compression_ratio: f64,
81}
82
83#[derive(Default)]
84struct Totals {
85 original: usize,
86 compressed: usize,
87}
88
89pub async fn handler(Json(req): Json<CompressRequest>) -> impl IntoResponse {
91 (StatusCode::OK, Json(compress_messages(req)))
92}
93
94pub fn compress_messages(req: CompressRequest) -> CompressResponse {
97 let mut messages = req.messages;
98 let mut totals = Totals::default();
99 for msg in &mut messages {
100 compress_message(msg, &mut totals);
101 }
102
103 let saved = totals.original.saturating_sub(totals.compressed);
104 let saved_pct = if totals.original > 0 {
105 ((saved as f64 / totals.original as f64) * 1000.0).round() / 10.0
106 } else {
107 0.0
108 };
109 let compression_ratio = if totals.original > 0 {
110 ((totals.compressed as f64 / totals.original as f64) * 100.0).round() / 100.0
111 } else {
112 1.0
113 };
114
115 CompressResponse {
116 messages,
117 stats: CompressStats {
118 original_tokens: totals.original,
119 compressed_tokens: totals.compressed,
120 saved_tokens: saved,
121 saved_pct,
122 tokenizer: TOKENIZER,
123 model: req.model,
124 },
125 tokens_before: totals.original,
126 tokens_after: totals.compressed,
127 compression_ratio,
128 }
129}
130
131fn compress_message(msg: &mut Value, totals: &mut Totals) {
132 let name = msg.get("name").and_then(Value::as_str).map(str::to_string);
135 if let Some(content) = msg.get_mut("content") {
136 compress_content(content, name.as_deref(), totals);
137 }
138}
139
140fn compress_content(content: &mut Value, name: Option<&str>, totals: &mut Totals) {
141 match content {
142 Value::String(s) => squeeze_in_place(s, name, totals),
143 Value::Array(blocks) => {
144 for block in blocks.iter_mut() {
145 compress_block(block, name, totals);
146 }
147 }
148 _ => {}
149 }
150}
151
152fn compress_block(block: &mut Value, name: Option<&str>, totals: &mut Totals) {
153 let Some(obj) = block.as_object_mut() else {
154 return;
155 };
156 match obj.get("type").and_then(Value::as_str) {
157 Some("text") => {
159 if let Some(Value::String(s)) = obj.get_mut("text") {
160 squeeze_in_place(s, name, totals);
161 }
162 }
163 Some("tool_result") => {
166 if let Some(inner) = obj.get_mut("content") {
167 compress_content(inner, name, totals);
168 }
169 }
170 _ => {}
172 }
173}
174
175fn squeeze_in_place(s: &mut String, name: Option<&str>, totals: &mut Totals) {
176 let before = count_tokens(s);
177 let compressed = compress_tool_result_gateway(s, name);
181 let clean = strip_trailing_savings_footer(&compressed);
182 let after = count_tokens(clean);
183 totals.original += before;
184 totals.compressed += after;
185 if clean != s {
186 *s = clean.to_string();
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use serde_json::json;
194
195 fn dedupable_prose() -> String {
198 let para = "Rust is a multi-paradigm systems programming language that \
199 emphasizes performance, type safety, and fearless concurrency, \
200 achieving memory safety without a garbage collector at runtime.";
201 format!("{}\n", [para; 8].join("\n\n"))
202 }
203
204 fn run(messages: Vec<Value>, model: Option<&str>) -> CompressResponse {
205 compress_messages(CompressRequest {
206 messages,
207 model: model.map(str::to_string),
208 })
209 }
210
211 #[test]
212 fn string_content_is_compressed_and_stats_reported() {
213 let _lock = crate::core::data_dir::test_env_lock();
214 let resp = run(
215 vec![json!({"role": "user", "content": dedupable_prose()})],
216 Some("claude-sonnet-4"),
217 );
218 let out = resp.messages[0]["content"].as_str().unwrap();
219 assert_eq!(
220 out.matches("fearless concurrency").count(),
221 1,
222 "duplicate paragraphs must be deduped"
223 );
224 assert!(resp.stats.saved_tokens > 0, "stats must reflect savings");
225 assert!(resp.stats.compressed_tokens < resp.stats.original_tokens);
226 assert_eq!(resp.stats.tokenizer, "o200k_base");
227 assert_eq!(resp.stats.model.as_deref(), Some("claude-sonnet-4"));
228 }
229
230 #[test]
231 fn litellm_guardrail_fields_present_and_consistent() {
232 let _lock = crate::core::data_dir::test_env_lock();
233 let resp = run(
237 vec![json!({"role": "user", "content": dedupable_prose()})],
238 None,
239 );
240 assert_eq!(resp.tokens_before, resp.stats.original_tokens);
241 assert_eq!(resp.tokens_after, resp.stats.compressed_tokens);
242 assert!(resp.compression_ratio > 0.0 && resp.compression_ratio < 1.0);
243
244 let wire = serde_json::to_value(&resp).unwrap();
245 assert!(wire["tokens_before"].is_u64());
246 assert!(wire["tokens_after"].is_u64());
247 assert!(wire["compression_ratio"].is_f64());
248
249 let empty = run(vec![json!({"role": "user", "content": "hi"})], None);
251 assert_eq!(empty.compression_ratio, 1.0);
252 }
253
254 #[test]
255 fn message_bodies_stay_footer_free() {
256 let _lock = crate::core::data_dir::test_env_lock();
257 let resp = run(
258 vec![json!({"role": "user", "content": dedupable_prose()})],
259 None,
260 );
261 let out = resp.messages[0]["content"].as_str().unwrap();
262 assert!(!out.contains('\u{2500}'), "no box-drawing footer in body");
263 assert!(!out.contains("[lean-ctx:"), "no savings footer in body");
264 assert!(resp.stats.model.is_none());
265 }
266
267 #[test]
272 fn lossy_rewrite_carries_litellm_retrieval_marker() {
273 let _lock = crate::core::data_dir::test_env_lock();
274 let original = dedupable_prose();
275 let resp = run(
276 vec![json!({"role": "user", "content": original.clone()})],
277 None,
278 );
279 let out = resp.messages[0]["content"].as_str().unwrap();
280
281 let litellm_regex = regex::Regex::new(r"hash=([a-f0-9]{24})").unwrap();
282 let hash = litellm_regex
283 .captures(out)
284 .unwrap_or_else(|| panic!("lossy body must carry the hash= marker: {out}"))
285 .get(1)
286 .unwrap()
287 .as_str();
288 let recovered = super::super::ccr::retrieve_litellm(hash)
289 .expect("marker hash must resolve via /v1/retrieve");
290 assert!(
291 recovered.contains("fearless concurrency"),
292 "retrieve returns the verbatim pre-compression original"
293 );
294 assert_eq!(
295 recovered.matches("fearless concurrency").count(),
296 8,
297 "all deduped paragraphs are recoverable"
298 );
299 }
300
301 #[test]
302 fn output_is_deterministic() {
303 let _lock = crate::core::data_dir::test_env_lock();
304 let msgs = vec![
305 json!({"role": "system", "content": "You are a helpful assistant."}),
306 json!({"role": "user", "content": dedupable_prose()}),
307 ];
308 let a = serde_json::to_string(&run(msgs.clone(), Some("gpt-4o"))).unwrap();
309 let b = serde_json::to_string(&run(msgs, Some("gpt-4o"))).unwrap();
310 assert_eq!(a, b, "same input must yield byte-identical output");
311 }
312
313 #[test]
314 fn short_content_is_untouched() {
315 let resp = run(vec![json!({"role": "user", "content": "hi there"})], None);
316 assert_eq!(resp.messages[0]["content"], "hi there");
317 assert_eq!(resp.stats.saved_tokens, 0);
318 assert_eq!(resp.stats.saved_pct, 0.0);
319 }
320
321 #[test]
322 fn anthropic_blocks_text_compressed_image_passthrough() {
323 let _lock = crate::core::data_dir::test_env_lock();
324 let resp = run(
325 vec![json!({
326 "role": "user",
327 "content": [
328 {"type": "text", "text": dedupable_prose()},
329 {"type": "image", "source": {"type": "base64", "data": "AAAA"}},
330 ],
331 })],
332 None,
333 );
334 let blocks = resp.messages[0]["content"].as_array().unwrap();
335 assert_eq!(
336 blocks[0]["text"]
337 .as_str()
338 .unwrap()
339 .matches("fearless concurrency")
340 .count(),
341 1
342 );
343 assert_eq!(blocks[1]["source"]["data"], "AAAA");
345 }
346
347 #[test]
348 fn anthropic_tool_result_block_is_compressed() {
349 let _lock = crate::core::data_dir::test_env_lock();
350 let resp = run(
351 vec![json!({
352 "role": "user",
353 "content": [{
354 "type": "tool_result",
355 "tool_use_id": "toolu_123",
356 "content": dedupable_prose(),
357 }],
358 })],
359 None,
360 );
361 let block = &resp.messages[0]["content"][0];
362 assert_eq!(block["tool_use_id"], "toolu_123", "ids preserved");
363 assert_eq!(
364 block["content"]
365 .as_str()
366 .unwrap()
367 .matches("fearless concurrency")
368 .count(),
369 1
370 );
371 assert!(resp.stats.saved_tokens > 0);
372 }
373
374 #[test]
375 fn lean_ctx_tool_output_passes_through_verbatim() {
376 let prose = dedupable_prose();
378 let resp = run(
379 vec![json!({"role": "tool", "name": "ctx_read", "content": prose.clone()})],
380 None,
381 );
382 assert_eq!(resp.messages[0]["content"].as_str().unwrap(), prose);
383 assert_eq!(
384 resp.stats.saved_tokens, 0,
385 "ctx_* output is not re-compressed"
386 );
387 }
388
389 #[test]
390 fn non_string_content_is_ignored() {
391 let resp = run(vec![json!({"role": "assistant", "tool_calls": []})], None);
393 assert_eq!(resp.stats.original_tokens, 0);
394 assert_eq!(resp.messages.len(), 1);
395 }
396
397 #[test]
402 fn determinism_regression_full_conversation_498() {
403 let _lock = crate::core::data_dir::test_env_lock();
404 let conversation = || {
405 vec![
406 json!({"role": "system", "content": "You are a helpful assistant."}),
407 json!({"role": "user", "content": dedupable_prose()}),
408 json!({
409 "role": "user",
410 "content": [
411 {"type": "text", "text": dedupable_prose()},
412 {"type": "image", "source": {"type": "base64", "data": "AAAA"}},
413 {"type": "tool_result", "tool_use_id": "toolu_1", "content": dedupable_prose()},
414 ],
415 }),
416 json!({"role": "tool", "name": "ctx_read", "content": dedupable_prose()}),
417 ]
418 };
419
420 let baseline =
421 serde_json::to_string(&run(conversation(), Some("claude-sonnet-4"))).unwrap();
422 for _ in 0..4 {
423 let again =
424 serde_json::to_string(&run(conversation(), Some("claude-sonnet-4"))).unwrap();
425 assert_eq!(again, baseline, "/v1/compress output must be byte-stable");
426 }
427
428 assert!(!baseline.contains("[lean-ctx:"));
430 assert!(!baseline.contains('\u{2500}'));
431 }
432
433 #[test]
438 #[ignore = "benchmark; run explicitly with --ignored --nocapture"]
439 fn bench_real_corpus_o200k() {
440 use std::path::Path;
441 use std::time::Instant;
442
443 let corpus = Path::new(env!("CARGO_MANIFEST_DIR")).join("../docs/reference");
444 let mut messages = Vec::new();
445 if let Ok(entries) = std::fs::read_dir(&corpus) {
446 let mut paths: Vec<_> = entries
447 .flatten()
448 .map(|e| e.path())
449 .filter(|p| p.extension().and_then(|s| s.to_str()) == Some("md"))
450 .collect();
451 paths.sort();
452 for path in paths {
453 if let Ok(text) = std::fs::read_to_string(&path) {
454 messages.push(json!({"role": "user", "content": text}));
455 }
456 }
457 }
458 assert!(!messages.is_empty(), "no corpus files found at {corpus:?}");
459
460 let files = messages.len();
461 let started = Instant::now();
462 let resp = run(messages, Some("gpt-4o"));
463 let latency_ms = started.elapsed().as_secs_f64() * 1000.0;
464
465 let report = json!({
466 "corpus": corpus.to_string_lossy(),
467 "files": files,
468 "tokenizer": resp.stats.tokenizer,
469 "original_tokens": resp.stats.original_tokens,
470 "compressed_tokens": resp.stats.compressed_tokens,
471 "tokens_saved": resp.stats.saved_tokens,
472 "saved_pct": resp.stats.saved_pct,
473 "latency_ms": (latency_ms * 100.0).round() / 100.0,
474 });
475 println!("{}", serde_json::to_string_pretty(&report).unwrap());
476 }
477}