1use axum::{
2 body::Body,
3 extract::State,
4 http::{Request, StatusCode},
5 response::Response,
6};
7use serde_json::Value;
8
9use super::ProxyState;
10use super::compress_shared::{self, ToolKind};
11use super::forward;
12use super::tool_kind::{self, ToolResultKind};
13use super::{cache_safety, prefix_cache_stats, prefix_replay, prose, sticky_tools};
14
15std::thread_local! {
16 static HEADROOM_REQUEST: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
18}
19
20pub(super) fn set_headroom_request(val: bool) {
22 HEADROOM_REQUEST.set(val);
23}
24use crate::core::config::{HistoryMode, ProseRole};
25
26pub async fn handler(
27 State(state): State<ProxyState>,
28 req: Request<Body>,
29) -> Result<Response, StatusCode> {
30 let upstream = state.anthropic_upstream();
31 forward::forward_request(
32 State(state),
33 req,
34 &upstream,
35 "/v1/messages",
36 compress_request_body,
37 "Anthropic",
38 &[],
39 )
40 .await
41}
42
43pub(super) fn compress_request_body(
44 parsed: Value,
45 original_size: usize,
46) -> (Vec<u8>, usize, usize) {
47 let mut doc = parsed;
48 let mut modified = false;
49
50 let cfg = crate::core::config::Config::load();
53 let config_headroom = cfg.proxy.is_headroom_compat();
54 if config_headroom {
55 prefix_cache_stats::record_headroom_compat();
56 }
57 let _headroom_compat = config_headroom || HEADROOM_REQUEST.get();
62 let system_aggr = cfg.proxy.resolved_role_aggressiveness(ProseRole::System);
63 let user_aggr = cfg.proxy.resolved_role_aggressiveness(ProseRole::User);
64 let live_compress = cfg.proxy.live_compresses();
65 let mode = cfg.proxy.resolved_history_mode();
66 let inject_breakpoint = cfg.proxy.cache_breakpoint_enabled();
71 let align_volatile = cfg.proxy.cache_aligner_enabled();
76 let relocate_volatile = cfg.proxy.cache_align_relocate_enabled();
81 let cache_economics = cfg.proxy.cache_policy_enabled();
88 let arm = super::holdout::assign(
93 &super::holdout::anthropic_key(&doc),
94 cfg.proxy.output_holdout_fraction(),
95 );
96 if cfg.proxy.ccr_inband_enabled() {
102 modified |= super::ccr::splice_inband_in_place(&mut doc);
103 }
104 if arm == super::holdout::Arm::Treatment {
109 if let Some(effort) = cfg.proxy.resolved_effort() {
110 modified |= super::effort::apply_anthropic(&mut doc, effort);
111 }
112 if cfg.proxy.verbosity_steer_enabled() {
116 modified |= super::verbosity::apply_anthropic(&mut doc);
117 }
118 }
119 if !live_compress
124 && mode == HistoryMode::Off
125 && system_aggr.is_none()
126 && user_aggr.is_none()
127 && !modified
128 && !inject_breakpoint
129 && !align_volatile
130 && !relocate_volatile
131 && !cache_economics
132 {
133 let out = serde_json::to_vec(&doc).unwrap_or_default();
134 return (out, original_size, original_size);
135 }
136 let mut prose_segments: u64 = 0;
137
138 let cached = doc
143 .get("messages")
144 .and_then(|m| m.as_array())
145 .map_or(0, |m| super::history_prune::cached_prefix_len(m));
146
147 if cache_economics
158 && let Some(m) = doc.get("messages").and_then(|m| m.as_array())
159 && let Some(outcome) = super::cache_attribution::record_request(m, cached)
160 {
161 match outcome {
162 super::cache_attribution::CacheOutcome::WarmReuse => {
163 prefix_cache_stats::record_hit();
164 }
165 super::cache_attribution::CacheOutcome::ColdStart => {}
166 _ => {
167 prefix_cache_stats::record_miss();
168 }
169 }
170 }
171 let repack = cfg.proxy.repacks_cold_prefix()
176 && doc
177 .get("messages")
178 .and_then(|m| m.as_array())
179 .is_some_and(|m| {
180 super::cold_prefix::repack_decision(m, cached)
181 && (!cache_economics
182 || super::cache_policy::worth_repacking(doc.get("system"), m, cached))
183 });
184 let protect = if repack { 0 } else { cached };
187
188 let model_name = doc
193 .get("model")
194 .and_then(Value::as_str)
195 .unwrap_or("default")
196 .to_owned();
197 if let Some(a) = system_aggr
198 && protect == 0
199 && let Some(system) = doc.get_mut("system")
200 && (repack || !prose::value_has_cache_control(system))
201 {
202 let should_compress = if repack || cached == 0 {
203 true
204 } else {
205 let sys_tokens = prose::estimate_tokens(system) as u64;
206 let estimated_after = (sys_tokens as f64 * (1.0 - a)).max(0.0) as u64;
207 let reuse_rate = super::cache_attribution::estimated_reuse_rate();
208 let model_cost = super::cache_policy::model_cost_for(&model_name);
209 let gate = super::cache_policy::should_mutate_frozen(
210 sys_tokens,
211 estimated_after,
212 reuse_rate,
213 &model_cost,
214 );
215 matches!(gate, super::cache_policy::MutationDecision::Mutate { .. })
216 };
217 if should_compress {
218 let n = prose::compress_system_value(system, a);
219 if n > 0 {
220 prose_segments += u64::from(n);
221 modified = true;
222 }
223 }
224 }
225
226 if let Some(messages) = doc.get_mut("messages").and_then(|m| m.as_array_mut()) {
227 let tool_names = tool_kind::anthropic_tool_names(messages);
230
231 let boundary = super::history_prune::prune_boundary(mode, messages.len());
235 modified |=
241 super::history_prune::prune_history_range(messages, protect, boundary, &tool_names);
242
243 for msg in messages.iter_mut() {
244 let role = msg.get("role").and_then(|r| r.as_str()).unwrap_or("");
245 if role != "user" {
246 continue;
247 }
248
249 if let Some(content) = msg.get_mut("content").and_then(|c| c.as_array_mut()) {
250 for block in content.iter_mut() {
251 if compress_shared::classify_tool_kind(block) != ToolKind::ToolResult {
252 continue;
253 }
254
255 let name = block
256 .get("tool_use_id")
257 .and_then(|v| v.as_str())
258 .and_then(|id| tool_names.get(id))
259 .map(String::as_str);
260 let kind = compress_shared::tool_result_kind(name);
261
262 let excluded =
265 name.is_some_and(|n| cfg.proxy.is_tool_live_compress_excluded(n));
266 if live_compress
267 && !excluded
268 && let Some(inner_content) = block.get_mut("content")
269 {
270 modified |= compress_content_field(inner_content, name, kind);
271 }
272 }
273 }
274 }
275
276 if let Some(a) = user_aggr {
281 let end = boundary.min(messages.len());
282 let start = protect.min(end);
283 for msg in &mut messages[start..end] {
284 if msg.get("role").and_then(|r| r.as_str()) == Some("user")
285 && let Some(content) = msg.get_mut("content").and_then(|c| c.as_array_mut())
286 {
287 prose_segments += u64::from(prose::compress_text_blocks(content, a));
288 }
289 }
290 }
291 }
292
293 if prose_segments > 0 {
294 modified = true;
295 }
296 if align_volatile
302 && cached == 0
303 && let Some(system) = doc.get("system")
304 && !prose::value_has_cache_control(system)
305 && let Some(text) = super::cache_aligner::system_text(system)
306 {
307 let scan = super::cache_aligner::scan_volatile(&text);
308 cache_safety::record_volatile_system(scan.fields as u64);
309 }
310 if relocate_volatile
319 && arm == super::holdout::Arm::Treatment
320 && cached == 0
321 && doc
322 .get("system")
323 .is_some_and(|s| !prose::value_has_cache_control(s))
324 {
325 let relocated = super::cache_aligner::apply_anthropic_relocate(&mut doc);
326 if relocated > 0 {
327 modified = true;
328 cache_safety::record_volatile_relocated(relocated as u64);
329 }
330 }
331 if inject_breakpoint
340 && cached == 0
341 && doc
342 .get("system")
343 .is_some_and(|s| !prose::value_has_cache_control(s))
344 && super::cache_breakpoint::inject_anthropic_system(&mut doc)
345 {
346 modified = true;
347 cache_safety::record_breakpoint_injected();
348 }
349 if repack {
354 cache_safety::record_cold_repack();
355 }
356 cache_safety::record(prose_segments, true);
357
358 let system_val = doc.get("system");
361 let messages_for_id = doc.get("messages").and_then(Value::as_array);
362 if let Some(msgs) = messages_for_id {
363 let conv_id = prefix_replay::conversation_id(system_val, msgs);
364 if sticky_tools::ensure_tool_present(conv_id, &mut doc) {
365 modified = true;
366 prefix_cache_stats::record_sticky_injection();
367 }
368 }
369
370 prefix_cache_stats::record_frozen_count(cached as u64);
371
372 let system_val_replay = doc.get("system");
375 let msgs_replay = doc.get("messages").and_then(Value::as_array);
376 let out = if let Some(msgs) = msgs_replay {
377 let conv_id = prefix_replay::conversation_id(system_val_replay, msgs);
378 if let Some(delta) = prefix_replay::detect_append_only(conv_id, msgs) {
379 let delta_msgs = &msgs[delta.delta_start..];
380 if let Some(replayed) = prefix_replay::overlay_prefix(&delta.prefix_bytes, delta_msgs) {
381 prefix_cache_stats::record_replay_hit();
382 let original_bytes = serde_json::to_vec(&doc).unwrap_or_default();
383 prefix_cache_stats::record_delta(
384 original_bytes.len() as u64,
385 replayed.len() as u64,
386 );
387 replayed
388 } else {
389 prefix_cache_stats::record_replay_miss();
390 serde_json::to_vec(&doc).unwrap_or_default()
391 }
392 } else {
393 prefix_cache_stats::record_replay_miss();
394 serde_json::to_vec(&doc).unwrap_or_default()
395 }
396 } else {
397 serde_json::to_vec(&doc).unwrap_or_default()
398 };
399 let compressed_size = if modified { out.len() } else { original_size };
400 (out, original_size, compressed_size)
401}
402
403fn compress_content_field(
406 content: &mut Value,
407 tool_name: Option<&str>,
408 kind: ToolResultKind,
409) -> bool {
410 match content {
411 Value::String(s) => super::tool_output::compress_text(s, tool_name, kind),
412 Value::Array(arr) => {
413 let mut modified = false;
414 for item in arr.iter_mut() {
415 if compress_shared::should_compress_content(compress_shared::classify_tool_kind(
416 item,
417 )) && let Some(Value::String(text)) = item.get_mut("text")
418 {
419 modified |= super::tool_output::compress_text(text, tool_name, kind);
420 }
421 }
422 modified
423 }
424 _ => false,
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::super::compress::compress_tool_result;
431 use super::*;
432
433 fn source_file_body() -> Vec<u8> {
434 let code = (0..60)
435 .map(|i| format!(" let binding_{i} = compute_value_{i}(context, options);"))
436 .collect::<Vec<_>>()
437 .join("\n");
438 let body = serde_json::json!({
439 "model": "claude-opus-4-8",
440 "messages": [
441 {
442 "role": "assistant",
443 "content": [{"type": "tool_use", "id": "toolu_1", "name": "Read", "input": {"file_path": "src/app.rs"}}]
444 },
445 {
446 "role": "user",
447 "content": [{"type": "tool_result", "tool_use_id": "toolu_1", "content": code}]
448 }
449 ]
450 });
451 serde_json::to_vec(&body).unwrap()
452 }
453
454 #[test]
455 fn read_tool_result_is_never_truncated() {
456 let bytes = source_file_body();
457 let body: Value = serde_json::from_slice(&bytes).unwrap();
458 let (out, _orig, _comp) = compress_request_body(body, bytes.len());
459 let parsed: Value = serde_json::from_slice(&out).unwrap();
460 let content = parsed["messages"][1]["content"][0]["content"]
461 .as_str()
462 .unwrap();
463 assert!(
464 content.contains("binding_59"),
465 "the full source body must survive — refactors need it intact"
466 );
467 assert!(!content.contains("lines omitted"));
468 }
469
470 fn forge_log_body(tool_name: &str) -> Value {
471 let mut log = String::new();
475 for i in 0..90 {
476 log.push_str(&format!(
477 "INFO processing item {i}: ok, latency={i}ms, queue depth normal, retries 0\n"
478 ));
479 }
480 serde_json::json!({
481 "messages": [
482 {"role": "assistant", "content": [{"type": "tool_use", "id": "f1", "name": tool_name, "input": {}}]},
483 {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "f1", "content": log}]}
484 ]
485 })
486 }
487
488 #[test]
489 fn forge_shell_tool_result_compresses() {
490 let body = forge_log_body("forge_shell");
493 let bytes = serde_json::to_vec(&body).unwrap();
494 let (_out, orig, comp) = compress_request_body(body, bytes.len());
495 assert!(comp < orig, "foreign shell output must be compressed");
496 }
497
498 #[test]
499 fn foreign_read_tool_protects_source() {
500 let code = (0..60)
503 .map(|i| format!(" let binding_{i} = compute_value_{i}(context, options);"))
504 .collect::<Vec<_>>()
505 .join("\n");
506 let body = serde_json::json!({
507 "messages": [
508 {"role": "assistant", "content": [{"type": "tool_use", "id": "r1", "name": "forge_read", "input": {"path": "src/app.rs"}}]},
509 {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "r1", "content": code}]}
510 ]
511 });
512 let bytes = serde_json::to_vec(&body).unwrap();
513 let (out, _orig, _comp) = compress_request_body(body, bytes.len());
514 let parsed: Value = serde_json::from_slice(&out).unwrap();
515 let content = parsed["messages"][1]["content"][0]["content"]
516 .as_str()
517 .unwrap();
518 assert!(
519 content.contains("binding_59"),
520 "source body must survive intact"
521 );
522 }
523
524 #[test]
525 fn compress_request_body_is_deterministic() {
526 let _lock = crate::core::data_dir::test_env_lock();
529 let bytes = serde_json::to_vec(&forge_log_body("Bash")).unwrap();
532 let a = compress_request_body(serde_json::from_slice(&bytes).unwrap(), bytes.len()).0;
533 let b = compress_request_body(serde_json::from_slice(&bytes).unwrap(), bytes.len()).0;
534 assert_eq!(a, b, "identical input must yield byte-identical output");
535 }
536
537 fn big_log() -> String {
539 (0..200)
540 .map(|i| format!("[info] processed item {i:04} ok, latency {i}ms, queue normal"))
541 .collect::<Vec<_>>()
542 .join("\n")
543 }
544
545 #[test]
546 fn inband_ccr_emit_echo_splice_round_trip() {
547 let _iso = crate::core::data_dir::isolated_data_dir();
551 crate::test_env::remove_var("LEAN_CTX_PROXY_CCR_INBAND");
552 crate::core::config::Config::update_global(|c| {
553 c.proxy.ccr_inband = Some(true);
554 })
555 .unwrap();
556
557 let log = big_log();
559 let emit = serde_json::json!({
560 "messages": [
561 {"role": "assistant", "content": [{"type": "tool_use", "id": "t1", "name": "bash", "input": {}}]},
562 {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "t1", "content": log}]}
563 ]
564 });
565 let bytes = serde_json::to_vec(&emit).unwrap();
566 let (out, _o, _c) = compress_request_body(emit, bytes.len());
567 let emitted: Value = serde_json::from_slice(&out).unwrap();
568 let stub = emitted["messages"][1]["content"][0]["content"]
569 .as_str()
570 .unwrap();
571 assert!(
572 stub.contains("<lc_expand:"),
573 "in-band stub must advertise an echo-able marker: {stub}"
574 );
575 assert!(
576 !stub.contains("/tee/proxy_"),
577 "in-band stub must not leak the unreachable local tee path: {stub}"
578 );
579
580 let start = stub.find("<lc_expand:").unwrap();
582 let end = stub[start..].find('>').unwrap() + start + 1;
583 let marker = &stub[start..end];
584
585 let echo = serde_json::json!({
588 "messages": [
589 {"role": "user", "content": [{"type": "text", "text": "look again"}]},
590 {"role": "assistant", "content": format!("revisiting that output: {marker}")}
591 ]
592 });
593 let bytes = serde_json::to_vec(&echo).unwrap();
594 let (out, _o, _c) = compress_request_body(echo, bytes.len());
595 let spliced: Value = serde_json::from_slice(&out).unwrap();
596 let assistant = spliced["messages"][1]["content"].as_str().unwrap();
597 assert!(
598 assistant.contains("processed item 0007 ok")
599 && assistant.contains("processed item 0199 ok"),
600 "the verbatim original must be spliced back in full: {assistant}"
601 );
602 assert!(
603 !assistant.contains("<lc_expand:"),
604 "the marker must be consumed by the splice"
605 );
606 }
607
608 #[test]
609 fn inband_marker_less_turn_is_byte_identical_on_or_off() {
610 let _iso = crate::core::data_dir::isolated_data_dir();
615 crate::test_env::remove_var("LEAN_CTX_PROXY_CCR_INBAND");
616 let body = serde_json::json!({
617 "messages": [
618 {"role": "user", "content": [{"type": "text", "text": "hello there"}]},
619 {"role": "assistant", "content": "hi — how can I help?"}
620 ]
621 });
622 let bytes = serde_json::to_vec(&body).unwrap();
623
624 crate::core::config::Config::update_global(|c| c.proxy.ccr_inband = Some(false)).unwrap();
625 let off = compress_request_body(body.clone(), bytes.len()).0;
626 crate::core::config::Config::update_global(|c| c.proxy.ccr_inband = Some(true)).unwrap();
627 let on = compress_request_body(body, bytes.len()).0;
628
629 assert_eq!(
630 off, on,
631 "a marker-less request must be byte-identical whether in-band is on or off"
632 );
633 }
634
635 fn big_prose() -> String {
637 let p = "You are a careful, senior software engineer. You always explain your \
638 reasoning before making changes, you prefer small reviewable diffs, and \
639 you never introduce mock data or placeholders into production code. ";
640 [p; 6].join("\n")
641 }
642
643 #[test]
644 fn system_prose_compressed_and_assistant_untouched() {
645 let _iso = crate::core::data_dir::isolated_data_dir();
646 crate::core::config::Config::update_global(|c| {
647 c.proxy.role_aggressiveness.system = Some(0.6);
648 c.proxy.role_aggressiveness.user = Some(0.6);
649 })
650 .unwrap();
651
652 let prose = big_prose();
653 let assistant_text = big_prose();
654 let body = serde_json::json!({
655 "model": "claude-opus-4-8",
656 "system": prose,
657 "messages": [
658 {"role": "user", "content": [{"type": "text", "text": prose}]},
659 {"role": "assistant", "content": assistant_text},
660 ]
661 });
662 let bytes = serde_json::to_vec(&body).unwrap();
663 let (out, _orig, _comp) = compress_request_body(body, bytes.len());
664 let parsed: Value = serde_json::from_slice(&out).unwrap();
665
666 assert!(
667 parsed["system"].as_str().unwrap().len() < prose.len(),
668 "system prose must be compressed when enabled"
669 );
670 assert_eq!(
671 parsed["messages"][1]["content"].as_str().unwrap(),
672 assistant_text,
673 "assistant turns must pass through verbatim (#710)"
674 );
675 }
676
677 #[test]
678 fn user_prose_compressed_only_in_frozen_region() {
679 let _iso = crate::core::data_dir::isolated_data_dir();
680 crate::core::config::Config::update_global(|c| {
681 c.proxy.role_aggressiveness.user = Some(0.7);
682 })
683 .unwrap();
684
685 let prose = big_prose();
686 let mut messages = Vec::new();
688 for i in 0..30 {
689 let role = if i % 2 == 0 { "user" } else { "assistant" };
690 messages.push(serde_json::json!({
691 "role": role,
692 "content": [{"type": "text", "text": prose}]
693 }));
694 }
695 let body = serde_json::json!({ "messages": messages });
696 let bytes = serde_json::to_vec(&body).unwrap();
697 let (out, _o, _c) = compress_request_body(body, bytes.len());
698 let parsed: Value = serde_json::from_slice(&out).unwrap();
699
700 let frozen_user = parsed["messages"][0]["content"][0]["text"]
701 .as_str()
702 .unwrap();
703 assert!(
704 frozen_user.len() < prose.len(),
705 "user prose in the frozen region must be compressed"
706 );
707 assert_eq!(
708 parsed["messages"][1]["content"][0]["text"]
709 .as_str()
710 .unwrap(),
711 prose,
712 "assistant prose is never compressed"
713 );
714 let live_tail_user = parsed["messages"][28]["content"][0]["text"]
715 .as_str()
716 .unwrap();
717 assert_eq!(
718 live_tail_user, prose,
719 "user prose in the live tail (>= boundary) must be preserved for quality"
720 );
721 }
722
723 #[test]
724 fn client_cached_prefix_disables_system_prose() {
725 let _iso = crate::core::data_dir::isolated_data_dir();
726 crate::core::config::Config::update_global(|c| {
727 c.proxy.role_aggressiveness.system = Some(0.9);
728 })
729 .unwrap();
730
731 let prose = big_prose();
732 let body = serde_json::json!({
733 "system": prose,
734 "messages": [
735 {"role": "user", "content": [
736 {"type": "text", "text": "hi", "cache_control": {"type": "ephemeral"}}
737 ]},
738 {"role": "assistant", "content": "ok"}
739 ]
740 });
741 let bytes = serde_json::to_vec(&body).unwrap();
742 let (out, _o, _c) = compress_request_body(body, bytes.len());
743 let parsed: Value = serde_json::from_slice(&out).unwrap();
744 assert_eq!(
745 parsed["system"].as_str().unwrap(),
746 prose,
747 "system must stay verbatim when the client caches a message prefix (#448)"
748 );
749 }
750
751 #[test]
752 fn prose_compression_is_deterministic() {
753 let _iso = crate::core::data_dir::isolated_data_dir();
754 crate::core::config::Config::update_global(|c| {
755 c.proxy.role_aggressiveness.system = Some(0.6);
756 })
757 .unwrap();
758 let prose = big_prose();
759 let mk = || serde_json::json!({"system": prose, "messages": [{"role": "user", "content": "hi"}]});
760 let (a, b) = (mk(), mk());
761 let la = serde_json::to_vec(&a).unwrap().len();
762 let lb = serde_json::to_vec(&b).unwrap().len();
763 assert_eq!(
764 compress_request_body(a, la).0,
765 compress_request_body(b, lb).0,
766 "prose compression must be byte-identical for identical input (#498)"
767 );
768 }
769
770 #[test]
771 fn bash_tool_result_still_compresses() {
772 let log = {
773 let mut s = String::from(
774 "$ git status\nOn branch main\nYour branch is up to date with 'origin/main'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n",
775 );
776 for i in 0..90 {
777 s.push_str(&format!("\tmodified: src/module_{i}/file_{i}.rs\n"));
778 }
779 s.push_str("\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\n");
780 s
781 };
782 let body = serde_json::json!({
783 "messages": [
784 {"role": "assistant", "content": [{"type": "tool_use", "id": "t1", "name": "Bash", "input": {}}]},
785 {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "t1", "content": log}]}
786 ]
787 });
788 let bytes = serde_json::to_vec(&body).unwrap();
789 let (_out, orig, comp) = compress_request_body(body, bytes.len());
790 assert!(comp < orig, "shell output must still be compressed");
791 }
792
793 #[test]
794 fn json_envelope_tool_result_is_compressed() {
795 let _iso = crate::core::data_dir::isolated_data_dir();
796 let log = long_git_status();
797 let expected = compress_tool_result(&log, Some("Bash"));
798 let envelope = serde_json::to_string(&serde_json::json!({
799 "content": [{"type": "text", "text": log}],
800 "isError": false,
801 }))
802 .unwrap();
803 let body = serde_json::json!({
804 "messages": [
805 {"role": "assistant", "content": [{
806 "type": "tool_use",
807 "id": "t1",
808 "name": "Bash",
809 "input": {}
810 }]},
811 {"role": "user", "content": [{
812 "type": "tool_result",
813 "tool_use_id": "t1",
814 "content": envelope
815 }]}
816 ]
817 });
818 let bytes = serde_json::to_vec(&body).unwrap();
819 let (out, orig, comp) = compress_request_body(body, bytes.len());
820
821 assert!(comp < orig, "JSON envelope tool result should shrink");
822 let parsed: Value = serde_json::from_slice(&out).unwrap();
823 let content = parsed["messages"][1]["content"][0]["content"]
824 .as_str()
825 .unwrap();
826 let envelope: Value = serde_json::from_str(content).unwrap();
827 assert_eq!(envelope["content"][0]["text"].as_str().unwrap(), expected);
828 }
829
830 fn long_git_status() -> String {
831 let mut s = String::from(
832 "$ git status\nOn branch main\nYour branch is up to date with 'origin/main'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n",
833 );
834 for i in 0..80 {
835 s.push_str(&format!("\tmodified: src/module_{i}/file_{i}.rs\n"));
836 }
837 s.push_str("\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\n");
838 s
839 }
840
841 fn cached_prefix_body(first_text: &str, prose: &str) -> (Vec<Value>, Value) {
851 let messages = vec![
852 serde_json::json!({"role": "user", "content": [
853 {"type": "text", "text": first_text, "cache_control": {"type": "ephemeral"}}
854 ]}),
855 serde_json::json!({"role": "assistant", "content": "ok"}),
856 ];
857 let body = serde_json::json!({ "system": prose, "messages": messages.clone() });
858 (messages, body)
859 }
860
861 #[test]
862 fn cold_prefix_repack_rewrites_protected_system_prose_when_enabled() {
863 let _iso = crate::core::data_dir::isolated_data_dir();
864 crate::test_env::remove_var("LEAN_CTX_PROXY_COLD_PREFIX_REPACK");
865 crate::core::config::Config::update_global(|c| {
866 c.proxy.role_aggressiveness.system = Some(0.9);
867 c.proxy.cold_prefix_repack = Some(true);
868 })
869 .unwrap();
870
871 let prose = big_prose().repeat(6);
876 let (messages, body) = cached_prefix_body("cold-repack-enabled-session", &prose);
877 super::super::cold_prefix::test_seed_last_touch(&messages, 3 * 60 * 60);
879
880 let bytes = serde_json::to_vec(&body).unwrap();
881 let (out, _o, _c) = compress_request_body(body, bytes.len());
882 let parsed: Value = serde_json::from_slice(&out).unwrap();
883 assert!(
884 parsed["system"].as_str().unwrap().len() < prose.len(),
885 "a predicted-cold prefix must let the proxy repack the otherwise-protected system prose"
886 );
887 }
888
889 #[test]
890 fn cold_prefix_repack_skipped_for_subcacheable_prefix_by_default() {
891 let _iso = crate::core::data_dir::isolated_data_dir();
897 crate::test_env::remove_var("LEAN_CTX_PROXY_COLD_PREFIX_REPACK");
898 crate::test_env::remove_var("LEAN_CTX_PROXY_CACHE_POLICY");
899 crate::core::config::Config::update_global(|c| {
900 c.proxy.role_aggressiveness.system = Some(0.9);
901 c.proxy.cold_prefix_repack = Some(true);
902 })
903 .unwrap();
904
905 let prose = big_prose();
906 let (messages, body) = cached_prefix_body("cold-repack-subcacheable-session", &prose);
907 super::super::cold_prefix::test_seed_last_touch(&messages, 3 * 60 * 60);
908
909 let bytes = serde_json::to_vec(&body).unwrap();
910 let (out, _o, _c) = compress_request_body(body, bytes.len());
911 let parsed: Value = serde_json::from_slice(&out).unwrap();
912 assert_eq!(
913 parsed["system"].as_str().unwrap(),
914 prose,
915 "the net-cost gate must skip repacking a sub-cacheable prefix (premium default)"
916 );
917 }
918
919 #[test]
920 fn cold_prefix_repack_off_by_default_keeps_prefix_protected() {
921 let _iso = crate::core::data_dir::isolated_data_dir();
922 crate::test_env::remove_var("LEAN_CTX_PROXY_COLD_PREFIX_REPACK");
923 crate::core::config::Config::update_global(|c| {
924 c.proxy.role_aggressiveness.system = Some(0.9);
925 c.proxy.cold_prefix_repack = Some(false);
926 })
927 .unwrap();
928
929 let prose = big_prose();
930 let (messages, body) = cached_prefix_body("cold-repack-disabled-session", &prose);
931 super::super::cold_prefix::test_seed_last_touch(&messages, 24 * 60 * 60);
933
934 let bytes = serde_json::to_vec(&body).unwrap();
935 let (out, _o, _c) = compress_request_body(body, bytes.len());
936 let parsed: Value = serde_json::from_slice(&out).unwrap();
937 assert_eq!(
938 parsed["system"].as_str().unwrap(),
939 prose,
940 "with repack off the cached prefix stays byte-stable regardless of idle time (#448)"
941 );
942 }
943
944 #[test]
945 fn cold_prefix_repack_protects_warm_prefix_even_when_enabled() {
946 let _iso = crate::core::data_dir::isolated_data_dir();
947 crate::test_env::remove_var("LEAN_CTX_PROXY_COLD_PREFIX_REPACK");
948 crate::core::config::Config::update_global(|c| {
949 c.proxy.role_aggressiveness.system = Some(0.9);
950 c.proxy.cold_prefix_repack = Some(true);
951 })
952 .unwrap();
953
954 let prose = big_prose();
955 let (messages, body) = cached_prefix_body("cold-repack-warm-session", &prose);
956 super::super::cold_prefix::test_seed_last_touch(&messages, 60);
958
959 let bytes = serde_json::to_vec(&body).unwrap();
960 let (out, _o, _c) = compress_request_body(body, bytes.len());
961 let parsed: Value = serde_json::from_slice(&out).unwrap();
962 assert_eq!(
963 parsed["system"].as_str().unwrap(),
964 prose,
965 "a warm prefix must stay protected even with repack enabled — only LARGE gaps trigger"
966 );
967 }
968
969 #[test]
970 fn cache_policy_attribution_is_measurement_only() {
971 let _iso = crate::core::data_dir::isolated_data_dir();
975 crate::test_env::remove_var("LEAN_CTX_PROXY_CACHE_POLICY");
976 crate::test_env::remove_var("LEAN_CTX_PROXY_COLD_PREFIX_REPACK");
977
978 let prose = big_prose();
979 let (_messages, body) = cached_prefix_body("cache-policy-measurement-session", &prose);
980 let bytes = serde_json::to_vec(&body).unwrap();
981
982 crate::core::config::Config::update_global(|c| {
983 c.proxy.cache_policy = Some(false);
984 })
985 .unwrap();
986 let (off, _o, _c) = compress_request_body(body.clone(), bytes.len());
987
988 crate::core::config::Config::update_global(|c| {
989 c.proxy.cache_policy = Some(true);
990 })
991 .unwrap();
992 let (on, _o, _c) = compress_request_body(body, bytes.len());
993
994 assert_eq!(
995 off, on,
996 "miss attribution is measurement-only: the wire bytes must not change"
997 );
998 }
999
1000 #[test]
1001 fn effort_control_dials_adaptive_thinking_only() {
1002 let _iso = crate::core::data_dir::isolated_data_dir();
1005 crate::test_env::remove_var("LEAN_CTX_PROXY_EFFORT");
1006 crate::core::config::Config::update_global(|c| {
1007 c.proxy.effort = Some("medium".into());
1008 })
1009 .unwrap();
1010
1011 let adaptive = serde_json::json!({
1012 "model": "claude-opus-4-8",
1013 "thinking": {"type": "adaptive"},
1014 "messages": [{"role": "user", "content": "hi"}]
1015 });
1016 let bytes = serde_json::to_vec(&adaptive).unwrap();
1017 let (out, _o, _c) = compress_request_body(adaptive, bytes.len());
1018 assert_eq!(
1019 serde_json::from_slice::<Value>(&out).unwrap()["output_config"]["effort"],
1020 "medium"
1021 );
1022
1023 let plain = serde_json::json!({
1026 "model": "claude-opus-4-8",
1027 "messages": [{"role": "user", "content": "hi"}]
1028 });
1029 let bytes = serde_json::to_vec(&plain).unwrap();
1030 let (out, _o, _c) = compress_request_body(plain, bytes.len());
1031 assert!(
1032 serde_json::from_slice::<Value>(&out)
1033 .unwrap()
1034 .get("output_config")
1035 .is_none()
1036 );
1037 }
1038
1039 #[test]
1040 fn verbosity_steer_applies_to_treatment_skips_control() {
1041 let _iso = crate::core::data_dir::isolated_data_dir();
1044 crate::test_env::remove_var("LEAN_CTX_PROXY_VERBOSITY_STEER");
1045 crate::test_env::remove_var("LEAN_CTX_PROXY_OUTPUT_HOLDOUT");
1046 crate::test_env::remove_var("LEAN_CTX_PROXY_EFFORT");
1047
1048 let req = serde_json::json!({
1049 "model": "claude-opus-4-8",
1050 "messages": [{"role": "user", "content": "Summarize the design."}]
1051 });
1052 let bytes = serde_json::to_vec(&req).unwrap();
1053
1054 crate::core::config::Config::update_global(|c| {
1056 c.proxy.verbosity_steer = Some(true);
1057 c.proxy.output_holdout = Some(0.0);
1058 })
1059 .unwrap();
1060 let (out, _o, _c) = compress_request_body(req.clone(), bytes.len());
1061 let v: Value = serde_json::from_slice(&out).unwrap();
1062 assert!(
1063 v["messages"][0]["content"]
1064 .as_str()
1065 .unwrap()
1066 .contains(crate::proxy::verbosity::STEER),
1067 "treatment arm must receive the verbosity steer"
1068 );
1069
1070 crate::core::config::Config::update_global(|c| {
1072 c.proxy.output_holdout = Some(1.0);
1073 })
1074 .unwrap();
1075 let (out2, _o, _c) = compress_request_body(req, bytes.len());
1076 let v2: Value = serde_json::from_slice(&out2).unwrap();
1077 assert!(
1078 !v2["messages"][0]["content"]
1079 .as_str()
1080 .unwrap()
1081 .contains(crate::proxy::verbosity::STEER),
1082 "control arm must NOT be steered (measurement baseline)"
1083 );
1084 }
1085
1086 fn cacheable_system() -> String {
1089 "You are a careful, senior software engineer who writes maintainable code. ".repeat(400)
1090 }
1091
1092 #[test]
1093 fn cache_breakpoint_injected_on_unanchored_system_when_opt_in() {
1094 let _iso = crate::core::data_dir::isolated_data_dir();
1097 crate::test_env::remove_var("LEAN_CTX_PROXY_CACHE_BREAKPOINT");
1098 let body = serde_json::json!({
1099 "model": "claude-opus-4-8",
1100 "system": cacheable_system(),
1101 "messages": [{"role": "user", "content": "Refactor the parser."}]
1102 });
1103 let bytes = serde_json::to_vec(&body).unwrap();
1104
1105 crate::core::config::Config::update_global(|c| c.proxy.cache_breakpoint = Some(true))
1106 .unwrap();
1107 let (out, _o, _c) = compress_request_body(body, bytes.len());
1108 let v: Value = serde_json::from_slice(&out).unwrap();
1109
1110 assert_eq!(
1111 v["system"][0]["cache_control"]["type"], "ephemeral",
1112 "an unanchored system prompt must receive one ephemeral breakpoint"
1113 );
1114 assert!(
1115 v["system"][0]["text"]
1116 .as_str()
1117 .unwrap()
1118 .contains("senior software engineer"),
1119 "the system text must be preserved verbatim under the marker"
1120 );
1121 }
1122
1123 #[test]
1124 fn cache_breakpoint_off_by_default_is_byte_unchanged() {
1125 let _iso = crate::core::data_dir::isolated_data_dir();
1128 crate::test_env::remove_var("LEAN_CTX_PROXY_CACHE_BREAKPOINT");
1129 let body = serde_json::json!({
1130 "model": "claude-opus-4-8",
1131 "system": cacheable_system(),
1132 "messages": [{"role": "user", "content": "Refactor the parser."}]
1133 });
1134 let bytes = serde_json::to_vec(&body).unwrap();
1135 let (out, _o, _c) = compress_request_body(body, bytes.len());
1136 assert_eq!(
1137 out, bytes,
1138 "default-off must leave the request byte-identical"
1139 );
1140 }
1141
1142 #[test]
1143 fn cache_breakpoint_respects_client_anchor() {
1144 let _iso = crate::core::data_dir::isolated_data_dir();
1147 crate::test_env::remove_var("LEAN_CTX_PROXY_CACHE_BREAKPOINT");
1148 let body = serde_json::json!({
1149 "model": "claude-opus-4-8",
1150 "system": cacheable_system(),
1151 "messages": [{
1152 "role": "user",
1153 "content": [{
1154 "type": "text",
1155 "text": "hello",
1156 "cache_control": {"type": "ephemeral"}
1157 }]
1158 }]
1159 });
1160 let bytes = serde_json::to_vec(&body).unwrap();
1161 crate::core::config::Config::update_global(|c| c.proxy.cache_breakpoint = Some(true))
1162 .unwrap();
1163 let (out, _o, _c) = compress_request_body(body, bytes.len());
1164 let v: Value = serde_json::from_slice(&out).unwrap();
1165 assert!(
1166 v["system"].is_string(),
1167 "with a client anchor present, system must be left untouched (no second breakpoint)"
1168 );
1169 }
1170
1171 #[test]
1172 fn cache_aligner_measures_without_mutating_body() {
1173 let _iso = crate::core::data_dir::isolated_data_dir();
1177 crate::test_env::remove_var("LEAN_CTX_PROXY_CACHE_ALIGNER");
1178 crate::test_env::remove_var("LEAN_CTX_PROXY_CACHE_BREAKPOINT");
1179 let body = serde_json::json!({
1180 "model": "claude-opus-4-8",
1181 "system": "Today is 2026-06-22. Session 550e8400-e29b-41d4-a716-446655440000.",
1182 "messages": [{"role": "user", "content": "Hello."}]
1183 });
1184 let bytes = serde_json::to_vec(&body).unwrap();
1185 crate::core::config::Config::update_global(|c| c.proxy.cache_aligner = Some(true)).unwrap();
1186 let (out, _o, _c) = compress_request_body(body, bytes.len());
1187 assert_eq!(
1188 out, bytes,
1189 "cache-aligner telemetry must never mutate the request body"
1190 );
1191 }
1192
1193 fn cacheable_system_with_date() -> String {
1196 format!("Today is 2026-06-27. {}", cacheable_system())
1197 }
1198
1199 fn clear_relocate_env() {
1200 crate::test_env::remove_var("LEAN_CTX_PROXY_CACHE_ALIGN_RELOCATE");
1201 crate::test_env::remove_var("LEAN_CTX_PROXY_CACHE_BREAKPOINT");
1202 crate::test_env::remove_var("LEAN_CTX_PROXY_CACHE_ALIGNER");
1203 crate::test_env::remove_var("LEAN_CTX_PROXY_OUTPUT_HOLDOUT");
1204 }
1205
1206 #[test]
1207 fn cache_align_relocate_moves_volatiles_to_tail_when_opt_in() {
1208 let _iso = crate::core::data_dir::isolated_data_dir();
1212 clear_relocate_env();
1213 let body = serde_json::json!({
1214 "model": "claude-opus-4-8",
1215 "system": cacheable_system_with_date(),
1216 "messages": [{"role": "user", "content": "Refactor the parser."}]
1217 });
1218 let bytes = serde_json::to_vec(&body).unwrap();
1219 crate::core::config::Config::update_global(|c| {
1220 c.proxy.cache_align_relocate = Some(true);
1221 c.proxy.output_holdout = Some(0.0);
1222 })
1223 .unwrap();
1224 let (out, _o, _c) = compress_request_body(body, bytes.len());
1225 let v: Value = serde_json::from_slice(&out).unwrap();
1226
1227 assert!(v["system"].is_array(), "system reshaped into a block array");
1228 assert_eq!(v["system"][0]["cache_control"]["type"], "ephemeral");
1229 assert!(
1230 !v["system"][0]["text"]
1231 .as_str()
1232 .unwrap()
1233 .contains("2026-06-27"),
1234 "the volatile date must leave the cacheable prefix"
1235 );
1236 assert!(
1237 v["system"][1].get("cache_control").is_none(),
1238 "the relocated tail block stays uncached"
1239 );
1240 assert!(
1241 v["system"][1]["text"]
1242 .as_str()
1243 .unwrap()
1244 .contains("2026-06-27"),
1245 "the date must be re-stated in the tail"
1246 );
1247 }
1248
1249 #[test]
1250 fn cache_align_relocate_off_by_default_is_byte_unchanged() {
1251 let _iso = crate::core::data_dir::isolated_data_dir();
1254 clear_relocate_env();
1255 let body = serde_json::json!({
1256 "model": "claude-opus-4-8",
1257 "system": cacheable_system_with_date(),
1258 "messages": [{"role": "user", "content": "Refactor the parser."}]
1259 });
1260 let bytes = serde_json::to_vec(&body).unwrap();
1261 let (out, _o, _c) = compress_request_body(body, bytes.len());
1262 assert_eq!(
1263 out, bytes,
1264 "default-off must leave the request byte-identical"
1265 );
1266 }
1267
1268 #[test]
1269 fn cache_align_relocate_skips_control_arm() {
1270 let _iso = crate::core::data_dir::isolated_data_dir();
1273 clear_relocate_env();
1274 let body = serde_json::json!({
1275 "model": "claude-opus-4-8",
1276 "system": cacheable_system_with_date(),
1277 "messages": [{"role": "user", "content": "Refactor the parser."}]
1278 });
1279 let bytes = serde_json::to_vec(&body).unwrap();
1280 crate::core::config::Config::update_global(|c| {
1281 c.proxy.cache_align_relocate = Some(true);
1282 c.proxy.output_holdout = Some(1.0);
1283 })
1284 .unwrap();
1285 let (out, _o, _c) = compress_request_body(body, bytes.len());
1286 let v: Value = serde_json::from_slice(&out).unwrap();
1287 assert!(
1288 v["system"].is_string(),
1289 "control arm must not be relocated (measurement baseline)"
1290 );
1291 }
1292
1293 #[test]
1294 fn cache_align_relocate_respects_client_anchor() {
1295 let _iso = crate::core::data_dir::isolated_data_dir();
1298 clear_relocate_env();
1299 let body = serde_json::json!({
1300 "model": "claude-opus-4-8",
1301 "system": cacheable_system_with_date(),
1302 "messages": [{
1303 "role": "user",
1304 "content": [{
1305 "type": "text",
1306 "text": "hello",
1307 "cache_control": {"type": "ephemeral"}
1308 }]
1309 }]
1310 });
1311 let bytes = serde_json::to_vec(&body).unwrap();
1312 crate::core::config::Config::update_global(|c| {
1313 c.proxy.cache_align_relocate = Some(true);
1314 c.proxy.output_holdout = Some(0.0);
1315 })
1316 .unwrap();
1317 let (out, _o, _c) = compress_request_body(body, bytes.len());
1318 let v: Value = serde_json::from_slice(&out).unwrap();
1319 assert!(
1320 v["system"].is_string(),
1321 "with a client anchor present, system must be left untouched"
1322 );
1323 }
1324
1325 #[test]
1326 fn cache_align_relocate_composes_with_breakpoint_to_one_anchor() {
1327 let _iso = crate::core::data_dir::isolated_data_dir();
1331 clear_relocate_env();
1332 let body = serde_json::json!({
1333 "model": "claude-opus-4-8",
1334 "system": cacheable_system_with_date(),
1335 "messages": [{"role": "user", "content": "Refactor the parser."}]
1336 });
1337 let bytes = serde_json::to_vec(&body).unwrap();
1338 crate::core::config::Config::update_global(|c| {
1339 c.proxy.cache_align_relocate = Some(true);
1340 c.proxy.cache_breakpoint = Some(true);
1341 c.proxy.output_holdout = Some(0.0);
1342 })
1343 .unwrap();
1344 let (out, _o, _c) = compress_request_body(body, bytes.len());
1345 let v: Value = serde_json::from_slice(&out).unwrap();
1346 let blocks = v["system"].as_array().expect("system is a block array");
1347 assert_eq!(blocks.len(), 2, "stable block + volatile tail");
1348 assert_eq!(blocks[0]["cache_control"]["type"], "ephemeral");
1349 assert!(
1350 blocks[1].get("cache_control").is_none(),
1351 "no second breakpoint — the tail stays uncached"
1352 );
1353 }
1354}