1#[cfg(feature = "http-server")]
36use axum::{
37 Json,
38 http::StatusCode,
39 response::{IntoResponse, Response},
40};
41use serde::{Deserialize, Serialize};
42use serde_json::Value;
43
44#[cfg(feature = "http-server")]
45use super::admitted::{self, GatewayRefusal, Payload};
46use super::live_zone;
47use lean_ctx::core::adaptive_policy::select_policy;
48#[cfg(feature = "http-server")]
49use lean_ctx::core::context_admission::egress::{EgressTarget, TrustedDestinationBinding};
50use lean_ctx::core::protocol::strip_trailing_savings_footer;
51use lean_ctx::core::tokens::{TokenizerFamily, count_tokens_for, detect_tokenizer};
52
53use super::compress::compress_tool_result_gateway_with_policy;
54
55#[derive(Debug, Deserialize)]
56pub struct CompressRequest {
57 pub messages: Vec<Value>,
58 #[serde(default)]
61 pub model: Option<String>,
62}
63
64#[derive(Debug, Serialize)]
65pub struct CompressStats {
66 pub original_tokens: usize,
67 pub compressed_tokens: usize,
68 pub saved_tokens: usize,
69 pub saved_pct: f64,
71 pub tokenizer: String,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub model: Option<String>,
74}
75
76#[derive(Debug, Serialize)]
77pub struct CompressResponse {
78 pub messages: Vec<Value>,
79 pub stats: CompressStats,
80 pub tokens_before: usize,
85 pub tokens_after: usize,
86 pub compression_ratio: f64,
89}
90
91#[derive(Default)]
92struct Totals {
93 original: usize,
94 compressed: usize,
95}
96
97#[cfg(feature = "http-server")]
102const COMPRESS_DESTINATION: &str = "compress-api";
103
104#[cfg(feature = "http-server")]
111pub async fn handler(Json(req): Json<CompressRequest>) -> Response {
112 match admitted_compress(req).await {
113 Ok(response) => response,
114 Err(refusal) => refusal.response("OpenAI"),
115 }
116}
117
118#[cfg(feature = "http-server")]
119async fn admitted_compress(req: CompressRequest) -> Result<Response, GatewayRefusal> {
120 let CompressRequest { messages, model } = req;
121 let mut document = Value::Array(messages);
122 let original_bytes = document.to_string().len();
123 let target = EgressTarget {
124 provider: COMPRESS_DESTINATION,
125 model: model.as_deref(),
126 upstream_base: "",
127 };
128 let mut admission = admitted::admit(
129 Payload::Document(&document),
130 &target,
131 TrustedDestinationBinding::default(),
132 )?;
133 if admission.is_refused() {
134 return Err(admitted::record_refusal(admission, original_bytes, None).await);
135 }
136 if let Some(admitted_document) = admission.take_rewritten() {
137 document = admitted_document;
138 }
139 let Value::Array(messages) = document else {
140 return Err(GatewayRefusal::unavailable("egress.encode_failed"));
141 };
142 let (compressed, kept_whole) =
148 super::compress::observing(super::compress::Reader::GatewayCaller, || {
149 compress_messages(CompressRequest { messages, model })
150 });
151 let reply = serde_json::to_vec(&compressed)
152 .map_err(|_| GatewayRefusal::unavailable("egress.encode_failed"))?;
153 let admission = admission.note_kept_whole(&kept_whole);
155 admitted::bind_delivery(admission, &reply, original_bytes).await?;
156 let mut response = (
157 StatusCode::OK,
158 [(axum::http::header::CONTENT_TYPE, "application/json")],
159 reply,
160 )
161 .into_response();
162 if let Some(codes) = kept_whole.codes()
163 && let Ok(value) = axum::http::HeaderValue::from_str(&codes)
164 {
165 response
166 .headers_mut()
167 .insert(admitted::REASON_CODES_HEADER, value);
168 }
169 Ok(response)
170}
171
172pub fn compress_messages(req: CompressRequest) -> CompressResponse {
175 let family = req
176 .model
177 .as_deref()
178 .map(detect_tokenizer)
179 .unwrap_or_default();
180 let mut messages = req.messages;
181 let mut totals = Totals::default();
182 let last_user_content = messages
183 .iter()
184 .rev()
185 .find(|message| message.get("role").and_then(Value::as_str) == Some("user"))
186 .and_then(|message| message.get("content").and_then(Value::as_str));
187 let task_class = super::pre_optimize::classify_task(last_user_content);
188
189 let policy = lean_ctx::core::adaptive_policy::best_policy_for(task_class);
190 let live_split = live_zone::detect_live_zone(&messages);
191 for msg in &mut messages[live_split.boundary_turn..] {
192 compress_message(msg, &mut totals, family, policy);
193 }
194 let _live_stats = live_zone::compress_live_only(&mut messages, family, select_policy("chat"));
195
196 let saved = totals.original.saturating_sub(totals.compressed);
197 let saved_pct = if totals.original > 0 {
198 ((saved as f64 / totals.original as f64) * 1000.0).round() / 10.0
199 } else {
200 0.0
201 };
202 let compression_ratio = if totals.original > 0 {
203 ((totals.compressed as f64 / totals.original as f64) * 100.0).round() / 100.0
204 } else {
205 1.0
206 };
207
208 CompressResponse {
209 messages,
210 stats: CompressStats {
211 original_tokens: totals.original,
212 compressed_tokens: totals.compressed,
213 saved_tokens: saved,
214 saved_pct,
215 tokenizer: family.to_string(),
216 model: req.model,
217 },
218 tokens_before: totals.original,
219 tokens_after: totals.compressed,
220 compression_ratio,
221 }
222}
223
224fn compress_message(
225 msg: &mut Value,
226 totals: &mut Totals,
227 family: TokenizerFamily,
228 policy: lean_ctx::core::adaptive_policy::CompressionPolicy,
229) {
230 let name = msg.get("name").and_then(Value::as_str).map(str::to_string);
233 if let Some(content) = msg.get_mut("content") {
234 compress_content(content, name.as_deref(), totals, family, policy);
235 }
236}
237
238fn compress_content(
239 content: &mut Value,
240 name: Option<&str>,
241 totals: &mut Totals,
242 family: TokenizerFamily,
243 policy: lean_ctx::core::adaptive_policy::CompressionPolicy,
244) {
245 match content {
246 Value::String(s) => squeeze_in_place(s, name, totals, family, policy),
247 Value::Array(blocks) => {
248 for block in blocks.iter_mut() {
249 compress_block(block, name, totals, family, policy);
250 }
251 }
252 _ => {}
253 }
254}
255
256fn compress_block(
257 block: &mut Value,
258 name: Option<&str>,
259 totals: &mut Totals,
260 family: TokenizerFamily,
261 policy: lean_ctx::core::adaptive_policy::CompressionPolicy,
262) {
263 let Some(obj) = block.as_object_mut() else {
264 return;
265 };
266 match obj.get("type").and_then(Value::as_str) {
267 Some("text") => {
269 if let Some(Value::String(s)) = obj.get_mut("text") {
270 squeeze_in_place(s, name, totals, family, policy);
271 }
272 }
273 Some("tool_result") => {
276 if let Some(inner) = obj.get_mut("content") {
277 compress_content(inner, name, totals, family, policy);
278 }
279 }
280 _ => {}
282 }
283}
284
285fn squeeze_in_place(
286 s: &mut String,
287 name: Option<&str>,
288 totals: &mut Totals,
289 family: TokenizerFamily,
290 policy: lean_ctx::core::adaptive_policy::CompressionPolicy,
291) {
292 let before = count_tokens_for(s, family);
293 let compressed = compress_tool_result_gateway_with_policy(s, name, family, policy);
297 let clean = strip_trailing_savings_footer(&compressed);
298 let after = count_tokens_for(clean, family);
299 totals.original += before;
300 totals.compressed += after;
301 if clean != s {
302 *s = clean.to_string();
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use serde_json::json;
310
311 fn dedupable_prose() -> String {
314 let para = "Rust is a multi-paradigm systems programming language that \
315 emphasizes performance, type safety, and fearless concurrency, \
316 achieving memory safety without a garbage collector at runtime.";
317 format!("{}\n", [para; 8].join("\n\n"))
318 }
319
320 fn run(messages: Vec<Value>, model: Option<&str>) -> CompressResponse {
321 compress_messages(CompressRequest {
322 messages,
323 model: model.map(str::to_string),
324 })
325 }
326
327 #[test]
328 fn string_content_is_compressed_and_stats_reported() {
329 let _lock = lean_ctx::core::data_dir::test_env_lock();
330 let _community = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
331 let resp = run(
332 vec![json!({"role": "user", "content": dedupable_prose()})],
333 Some("claude-sonnet-4"),
334 );
335 let out = resp.messages[0]["content"].as_str().unwrap();
336 assert_eq!(
337 out.matches("fearless concurrency").count(),
338 1,
339 "duplicate paragraphs must be deduped"
340 );
341 assert!(resp.stats.saved_tokens > 0, "stats must reflect savings");
342 assert!(resp.stats.compressed_tokens < resp.stats.original_tokens);
343 assert_eq!(resp.stats.tokenizer, "cl100k_base");
344 assert_eq!(resp.stats.model.as_deref(), Some("claude-sonnet-4"));
345 }
346
347 #[cfg(feature = "http-server")]
348 #[tokio::test]
353 async fn handler_admits_messages_before_compressing_or_replying() {
354 let _data = lean_ctx::core::data_dir::isolated_data_dir();
355 let _community = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
356 let key = concat!("AK", "IAIOSFODNN7EXAMPLE");
357 let request = |text: String| {
358 Json(CompressRequest {
359 messages: vec![json!({"role": "user", "content": text})],
360 model: None,
361 })
362 };
363
364 let response = handler(request(format!("deploy with {key} today"))).await;
365 assert_eq!(response.status(), StatusCode::OK);
366 let reply = axum::body::to_bytes(response.into_body(), usize::MAX)
367 .await
368 .expect("reply body");
369 let reply = String::from_utf8(reply.to_vec()).expect("JSON reply");
370 assert!(!reply.contains(key), "the reply carries the value: {reply}");
371 assert!(reply.contains("deploy with"), "{reply}");
372
373 let withheld = handler(request(
376 "Classification: RESTRICTED\nmerger terms for the board".to_owned(),
377 ))
378 .await;
379 assert_eq!(withheld.status(), StatusCode::OK);
380 let body = axum::body::to_bytes(withheld.into_body(), usize::MAX)
381 .await
382 .expect("reply body");
383 let body = String::from_utf8_lossy(&body);
384 assert!(!body.contains("merger terms"), "{body}");
385 assert!(body.contains("destination.remote_restricted"), "{body}");
386 assert!(body.contains("lean-ctx gateway why w-"), "{body}");
387 }
388
389 #[test]
390 fn model_selects_tokenizer_family_for_gateway_compression() {
391 let resp = run(
392 vec![json!({"role": "user", "content": dedupable_prose()})],
393 Some("claude-sonnet-4"),
394 );
395
396 assert_eq!(resp.stats.model.as_deref(), Some("claude-sonnet-4"));
397 assert_eq!(resp.stats.tokenizer, "cl100k_base");
398 }
399
400 #[test]
401 fn litellm_guardrail_fields_present_and_consistent() {
402 let _lock = lean_ctx::core::data_dir::test_env_lock();
403 let _community = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
404 let resp = run(
408 vec![json!({"role": "user", "content": dedupable_prose()})],
409 None,
410 );
411 assert_eq!(resp.tokens_before, resp.stats.original_tokens);
412 assert_eq!(resp.tokens_after, resp.stats.compressed_tokens);
413 assert!(resp.compression_ratio > 0.0 && resp.compression_ratio < 1.0);
414
415 let wire = serde_json::to_value(&resp).unwrap();
416 assert!(wire["tokens_before"].is_u64());
417 assert!(wire["tokens_after"].is_u64());
418 assert!(wire["compression_ratio"].is_f64());
419
420 let empty = run(vec![json!({"role": "user", "content": "hi"})], None);
422 assert_eq!(empty.compression_ratio, 1.0);
423 }
424
425 #[test]
426 fn message_bodies_stay_footer_free() {
427 let _lock = lean_ctx::core::data_dir::test_env_lock();
428 let resp = run(
429 vec![json!({"role": "user", "content": dedupable_prose()})],
430 None,
431 );
432 let out = resp.messages[0]["content"].as_str().unwrap();
433 assert!(!out.contains('\u{2500}'), "no box-drawing footer in body");
434 assert!(!out.contains("[lean-ctx:"), "no savings footer in body");
435 assert!(resp.stats.model.is_none());
436 }
437
438 #[cfg(feature = "http-server")]
439 #[test]
444 fn lossy_rewrite_carries_litellm_retrieval_marker() {
445 let _lock = lean_ctx::core::data_dir::test_env_lock();
446 let _community = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
447 let original = dedupable_prose();
448 let resp = run(
449 vec![json!({"role": "user", "content": original.clone()})],
450 None,
451 );
452 let out = resp.messages[0]["content"].as_str().unwrap();
453
454 let litellm_regex = regex::Regex::new(r"hash=([a-f0-9]{24})").unwrap();
455 let hash = litellm_regex
456 .captures(out)
457 .unwrap_or_else(|| panic!("lossy body must carry the hash= marker: {out}"))
458 .get(1)
459 .unwrap()
460 .as_str();
461 let recovered = super::super::ccr::retrieve_litellm(hash)
462 .expect("marker hash must resolve via /v1/retrieve");
463 assert!(
464 recovered.contains("fearless concurrency"),
465 "retrieve returns the verbatim pre-compression original"
466 );
467 assert_eq!(
468 recovered.matches("fearless concurrency").count(),
469 8,
470 "all deduped paragraphs are recoverable"
471 );
472 }
473
474 #[test]
475 fn output_is_deterministic() {
476 let _lock = lean_ctx::core::data_dir::test_env_lock();
477 let _community = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
478 let msgs = vec![
479 json!({"role": "system", "content": "You are a helpful assistant."}),
480 json!({"role": "user", "content": dedupable_prose()}),
481 ];
482 let a = serde_json::to_string(&run(msgs.clone(), Some("gpt-4o"))).unwrap();
483 let b = serde_json::to_string(&run(msgs, Some("gpt-4o"))).unwrap();
484 assert_eq!(a, b, "same input must yield byte-identical output");
485 }
486
487 #[test]
488 fn short_content_is_untouched() {
489 let resp = run(vec![json!({"role": "user", "content": "hi there"})], None);
490 assert_eq!(resp.messages[0]["content"], "hi there");
491 assert_eq!(resp.stats.saved_tokens, 0);
492 assert_eq!(resp.stats.saved_pct, 0.0);
493 }
494
495 #[test]
496 fn anthropic_blocks_text_compressed_image_passthrough() {
497 let _lock = lean_ctx::core::data_dir::test_env_lock();
498 let _community = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
499 let resp = run(
500 vec![json!({
501 "role": "user",
502 "content": [
503 {"type": "text", "text": dedupable_prose()},
504 {"type": "image", "source": {"type": "base64", "data": "AAAA"}},
505 ],
506 })],
507 None,
508 );
509 let blocks = resp.messages[0]["content"].as_array().unwrap();
510 assert_eq!(
511 blocks[0]["text"]
512 .as_str()
513 .unwrap()
514 .matches("fearless concurrency")
515 .count(),
516 1
517 );
518 assert_eq!(blocks[1]["source"]["data"], "AAAA");
520 }
521
522 #[test]
523 fn anthropic_tool_result_block_is_compressed() {
524 let _lock = lean_ctx::core::data_dir::test_env_lock();
525 let _community = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
526 let resp = run(
527 vec![json!({
528 "role": "user",
529 "content": [{
530 "type": "tool_result",
531 "tool_use_id": "toolu_123",
532 "content": dedupable_prose(),
533 }],
534 })],
535 None,
536 );
537 let block = &resp.messages[0]["content"][0];
538 assert_eq!(block["tool_use_id"], "toolu_123", "ids preserved");
539 assert_eq!(
540 block["content"]
541 .as_str()
542 .unwrap()
543 .matches("fearless concurrency")
544 .count(),
545 1
546 );
547 assert!(resp.stats.saved_tokens > 0);
548 }
549
550 #[test]
551 fn lean_ctx_tool_output_passes_through_verbatim() {
552 let prose = dedupable_prose();
554 let resp = run(
555 vec![json!({"role": "tool", "name": "ctx_read", "content": prose.clone()})],
556 None,
557 );
558 assert_eq!(resp.messages[0]["content"].as_str().unwrap(), prose);
559 assert_eq!(
560 resp.stats.saved_tokens, 0,
561 "ctx_* output is not re-compressed"
562 );
563 }
564
565 #[test]
566 fn non_string_content_is_ignored() {
567 let resp = run(vec![json!({"role": "assistant", "tool_calls": []})], None);
569 assert_eq!(resp.stats.original_tokens, 0);
570 assert_eq!(resp.messages.len(), 1);
571 }
572
573 #[test]
578 fn determinism_regression_full_conversation_498() {
579 let _lock = lean_ctx::core::data_dir::test_env_lock();
580 let _community = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
581 let conversation = || {
582 vec![
583 json!({"role": "system", "content": "You are a helpful assistant."}),
584 json!({"role": "user", "content": dedupable_prose()}),
585 json!({
586 "role": "user",
587 "content": [
588 {"type": "text", "text": dedupable_prose()},
589 {"type": "image", "source": {"type": "base64", "data": "AAAA"}},
590 {"type": "tool_result", "tool_use_id": "toolu_1", "content": dedupable_prose()},
591 ],
592 }),
593 json!({"role": "tool", "name": "ctx_read", "content": dedupable_prose()}),
594 ]
595 };
596
597 let baseline =
598 serde_json::to_string(&run(conversation(), Some("claude-sonnet-4"))).unwrap();
599 for _ in 0..4 {
600 let again =
601 serde_json::to_string(&run(conversation(), Some("claude-sonnet-4"))).unwrap();
602 assert_eq!(again, baseline, "/v1/compress output must be byte-stable");
603 }
604
605 assert!(!baseline.contains("[lean-ctx:"));
607 assert!(!baseline.contains('\u{2500}'));
608 }
609
610 #[cfg(feature = "http-server")]
611 async fn reply_for(text: &str) -> (Option<String>, String) {
614 let response = handler(Json(CompressRequest {
615 messages: vec![json!({"role": "tool", "name": "bash", "content": text})],
616 model: None,
617 }))
618 .await;
619 assert_eq!(response.status(), StatusCode::OK);
620 assert!(
621 !response.headers().contains_key(admitted::REFUSAL_HEADER),
622 "a delivered reply is not a refusal"
623 );
624 let codes = response
625 .headers()
626 .get(admitted::REASON_CODES_HEADER)
627 .and_then(|value| value.to_str().ok())
628 .map(str::to_owned);
629 let body = axum::body::to_bytes(response.into_body(), usize::MAX)
630 .await
631 .expect("reply body");
632 let body: Value = serde_json::from_slice(&body).expect("JSON reply");
633 let content = body["messages"][0]["content"]
634 .as_str()
635 .expect("string content")
636 .to_owned();
637 (codes, content)
638 }
639
640 #[cfg(feature = "http-server")]
641 #[tokio::test]
649 async fn protected_reply_compresses_only_with_a_bound_copy_and_names_why_not() {
650 let _data = lean_ctx::core::data_dir::isolated_data_dir();
651 let _community = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
652 let directory = tempfile::tempdir().expect("project");
653 let root = directory.path().canonicalize().expect("canonical root");
654 let config = lean_ctx::core::paths::config_dir_read_only().expect("config directory");
655 std::fs::create_dir_all(&config).expect("config directory");
656 std::fs::write(
657 config.join("config.toml"),
658 format!(
659 "[context_gateway]\nmode = \"governed\"\n\n[proxy]\nproject_root = '{}'\n",
660 root.display()
661 ),
662 )
663 .expect("gateway config");
664 let log = (1..=400)
665 .map(|i| format!("[info] processed item {i:04} ok"))
666 .collect::<Vec<_>>()
667 .join("\n");
668 let source = root.join("build.log");
669 std::fs::write(&source, &log).expect("source");
670 let delivered_text = super::super::request_project::in_project(&root, || {
672 let path = source.to_str().expect("utf-8 path");
673 let admitted_text = lean_ctx::core::rooted_read::read_file_for_tool_rooted(
674 path,
675 root.to_str().expect("utf-8 root"),
676 "ctx_read",
677 )
678 .expect("admitted read");
679 let authority = lean_ctx::core::archive::authority::ArchiveAuthority::file(
680 &root,
681 path,
682 "ctx_read",
683 &admitted_text,
684 )
685 .expect("protection is active: the read is bound");
686 assert!(lean_ctx::core::archive::delivered::record_lineage(
687 &lean_ctx::core::archive::delivered::digest(&admitted_text),
688 &lean_ctx::core::archive::delivered::Lineage::of_files(authority)
689 ));
690 admitted_text
691 });
692
693 let (codes, sent) = reply_for(&delivered_text).await;
694 assert_eq!(codes, None, "nothing stayed whole");
695 assert!(sent.len() < delivered_text.len(), "compressed: {sent}");
696 let (_, rest) = sent
697 .rsplit_once("GET /v1/references/")
698 .expect("a stub naming the route");
699 let id: String = rest.chars().take(64).collect();
700 assert!(
701 sent.ends_with(&format!(
702 "[lean-ctx CCR: full original archived — GET /v1/references/{id} on this \
703 gateway returns it while its sources are admitted]"
704 )),
705 "{sent}"
706 );
707 assert!(!sent.contains("hash="), "not a tee marker: {sent}");
708 assert_eq!(
709 super::super::compress::resolve_recovery(&id),
710 Some(Ok(delivered_text.clone())),
711 "the id resolves to the exact original"
712 );
713 let (_, again) = reply_for(&delivered_text).await;
714 assert_eq!(again, sent, "byte-stable");
715
716 let unknown = format!("{delivered_text}\n[info] a line the Engine never delivered");
717 let (codes, whole) = reply_for(&unknown).await;
718 assert_eq!(whole, unknown, "no lineage, no copy: the original");
719 assert_eq!(codes.as_deref(), Some("recovery.unknown_lineage"));
720
721 std::fs::remove_file(&source).expect("delete the source");
722 let (codes, whole) = reply_for(&delivered_text).await;
723 assert_eq!(whole, delivered_text);
724 assert_eq!(codes.as_deref(), Some("recovery.source_no_longer_admitted"));
725 }
726
727 #[test]
732 #[ignore = "benchmark; run explicitly with --ignored --nocapture"]
733 fn bench_real_corpus_o200k() {
734 use std::path::Path;
735 use std::time::Instant;
736
737 let corpus = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../docs/reference");
738 let mut messages = Vec::new();
739 if let Ok(entries) = std::fs::read_dir(&corpus) {
740 let mut paths: Vec<_> = entries
741 .flatten()
742 .map(|e| e.path())
743 .filter(|p| p.extension().and_then(|s| s.to_str()) == Some("md"))
744 .collect();
745 paths.sort();
746 for path in paths {
747 if let Ok(text) = std::fs::read_to_string(&path) {
748 messages.push(json!({"role": "user", "content": text}));
749 }
750 }
751 }
752 assert!(!messages.is_empty(), "no corpus files found at {corpus:?}");
753
754 let files = messages.len();
755 let started = Instant::now();
756 let resp = run(messages, Some("gpt-4o"));
757 let latency_ms = started.elapsed().as_secs_f64() * 1000.0;
758
759 let report = json!({
760 "corpus": corpus.to_string_lossy(),
761 "files": files,
762 "tokenizer": resp.stats.tokenizer,
763 "original_tokens": resp.stats.original_tokens,
764 "compressed_tokens": resp.stats.compressed_tokens,
765 "tokens_saved": resp.stats.saved_tokens,
766 "saved_pct": resp.stats.saved_pct,
767 "latency_ms": (latency_ms * 100.0).round() / 100.0,
768 });
769 println!("{}", serde_json::to_string_pretty(&report).unwrap());
770 }
771}