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, prose};
14use crate::core::config::{HistoryMode, ProseRole};
15
16pub async fn handler(
30 State(state): State<ProxyState>,
31 req: Request<Body>,
32) -> Result<Response, StatusCode> {
33 let upstream = state.openai_upstream();
34 forward::forward_request(
35 State(state),
36 req,
37 &upstream,
38 "/v1/responses",
39 compress_request_body,
40 "OpenAI",
41 &[],
42 )
43 .await
44}
45
46pub async fn ws_handler(
53 State(state): State<ProxyState>,
54 headers: axum::http::HeaderMap,
55 ws: axum::extract::ws::WebSocketUpgrade,
56) -> Response {
57 super::openai_responses_ws::upgrade(state, ws, &headers)
58}
59
60pub(super) fn compress_request_body(
61 parsed: Value,
62 original_size: usize,
63) -> (Vec<u8>, usize, usize) {
64 let mut doc = parsed;
65 let cfg = crate::core::config::Config::load();
66 let system_aggr = cfg.proxy.resolved_role_aggressiveness(ProseRole::System);
67 let user_aggr = cfg.proxy.resolved_role_aggressiveness(ProseRole::User);
68 let live_compress = cfg.proxy.live_compresses();
69 let mode = cfg.proxy.resolved_history_mode();
70 let mut modified = false;
76 let arm = super::holdout::assign(
80 &super::holdout::openai_responses_key(&doc),
81 cfg.proxy.output_holdout_fraction(),
82 );
83 if cfg.proxy.ccr_inband_enabled() {
84 modified |= super::ccr::splice_inband_in_place(&mut doc);
85 }
86 if arm == super::holdout::Arm::Treatment {
91 if let Some(effort) = cfg.proxy.resolved_effort() {
92 modified |= super::effort::apply_openai_responses(&mut doc, effort);
93 }
94 if cfg.proxy.verbosity_steer_enabled() {
96 modified |= super::verbosity::apply_openai_responses(&mut doc);
97 }
98 }
99 if !live_compress
103 && mode == HistoryMode::Off
104 && system_aggr.is_none()
105 && user_aggr.is_none()
106 && !modified
107 {
108 let out = serde_json::to_vec(&doc).unwrap_or_default();
109 return (out, original_size, original_size);
110 }
111 let mut prose_segments: u64 = 0;
112 if let Some(a) = system_aggr {
113 prose_segments += u64::from(prose::compress_string_field(&mut doc, "instructions", a));
114 }
115 modified |= prune_responses_input(&mut doc);
120 modified |= compress_responses_input(&mut doc);
121 if let Some(a) = user_aggr {
122 prose_segments += u64::from(compress_responses_user_prose(&mut doc, mode, a));
123 }
124 if prose_segments > 0 {
125 modified = true;
126 }
127 cache_safety::record(prose_segments, true);
128 let out = serde_json::to_vec(&doc).unwrap_or_default();
129 let compressed_size = if modified { out.len() } else { original_size };
130 (out, original_size, compressed_size)
131}
132
133fn compress_responses_user_prose(doc: &mut Value, mode: HistoryMode, aggressiveness: f64) -> u32 {
134 let Some(input) = doc.get_mut("input").and_then(|i| i.as_array_mut()) else {
135 return 0;
136 };
137 let boundary = super::history_prune::prune_boundary(mode, input.len());
138 if boundary == 0 {
139 return 0;
140 }
141
142 let mut segments = 0;
143 for item in input.iter_mut().take(boundary) {
144 let item_type = item
145 .get("type")
146 .and_then(|t| t.as_str())
147 .unwrap_or("message");
148 let role = item.get("role").and_then(|r| r.as_str());
149 if item_type == "message" && role == Some("user") {
150 segments += prose::compress_message_content(item, aggressiveness);
151 }
152 }
153 segments
154}
155
156pub(super) fn prune_responses_input(doc: &mut Value) -> bool {
171 let mode = crate::core::config::Config::load()
172 .proxy
173 .resolved_history_mode();
174 let Some(input) = doc.get_mut("input").and_then(|i| i.as_array_mut()) else {
175 return false;
176 };
177 let boundary = super::history_prune::prune_boundary(mode, input.len());
178 if boundary == 0 {
179 return false;
180 }
181 let tool_names = tool_kind::responses_tool_names(input);
182 let mut modified = false;
183 for item in input.iter_mut().take(boundary) {
184 if compress_shared::classify_tool_kind(item) != ToolKind::ToolResult {
185 continue;
186 }
187 let tool_name = item
188 .get("call_id")
189 .and_then(|v| v.as_str())
190 .and_then(|id| tool_names.get(id))
191 .map(String::as_str);
192 let kind = compress_shared::tool_result_kind(tool_name);
193 if let Some(output) = item.get_mut("output") {
194 modified |= prune_output_field(output, kind);
195 }
196 }
197 modified
198}
199
200fn prune_output_field(output: &mut Value, kind: ToolResultKind) -> bool {
204 super::tool_output::prune_value(output, kind)
205}
206
207pub(super) fn compress_responses_input(doc: &mut Value) -> bool {
219 let cfg = crate::core::config::Config::load();
222 if !cfg.proxy.live_compresses() {
223 return false;
224 }
225 let mut modified = false;
226 if let Some(input) = doc.get_mut("input").and_then(|i| i.as_array_mut()) {
227 let tool_names = tool_kind::responses_tool_names(input);
228 for item in input.iter_mut() {
229 if compress_shared::classify_tool_kind(item) != ToolKind::ToolResult {
230 continue;
231 }
232 let name = item
233 .get("call_id")
234 .and_then(|v| v.as_str())
235 .and_then(|id| tool_names.get(id))
236 .map(String::as_str);
237 if name.is_some_and(|n| cfg.proxy.is_tool_live_compress_excluded(n)) {
240 continue;
241 }
242 let kind = compress_shared::tool_result_kind(name);
243 if let Some(output) = item.get_mut("output") {
244 modified |= compress_output_field(output, name, kind);
245 }
246 }
247 }
248 modified
249}
250
251fn compress_output_field(
258 output: &mut Value,
259 tool_name: Option<&str>,
260 kind: ToolResultKind,
261) -> bool {
262 super::tool_output::compress_value(output, tool_name, kind)
263}
264
265#[cfg(test)]
266mod tests {
267 use super::super::compress::compress_tool_result;
268 use super::*;
269
270 fn long_git_status() -> String {
273 let mut s = String::from(
274 "$ 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",
275 );
276 for i in 0..80 {
277 s.push_str(&format!("\tmodified: src/module_{i}/file_{i}.rs\n"));
278 }
279 s.push_str("\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\n");
280 s
281 }
282
283 fn big_prose() -> String {
284 let p = "You are a careful, senior software engineer. You always explain your \
285 reasoning before making changes, you prefer small reviewable diffs, and \
286 you never introduce mock data or placeholders into production code. ";
287 [p; 6].join("\n")
288 }
289
290 fn long_tool_json() -> String {
291 let rows = (0..32)
292 .map(|i| {
293 serde_json::json!({
294 "path": format!("/Users/alex/work/app/src/module_{i}.rs"),
295 "regex": r"src/[a-z_]+\.rs:\d+",
296 "error": format!("error[E0{i:03}]: expected exact diagnostic text"),
297 })
298 })
299 .collect::<Vec<_>>();
300 serde_json::to_string(&serde_json::json!({ "results": rows })).unwrap()
301 }
302
303 #[test]
304 fn shell_json_envelope_text_is_compressed() {
305 let _lock = crate::core::data_dir::test_env_lock();
306 let raw = long_git_status();
307 let expected = compress_tool_result(&raw, Some("Bash"));
308 let envelope = serde_json::to_string(&serde_json::json!({
309 "content": [{"type": "text", "text": raw}],
310 "isError": false,
311 }))
312 .unwrap();
313
314 let body = serde_json::json!({
315 "model": "gpt-5",
316 "input": [
317 {"type": "function_call", "call_id": "call_1", "name": "Bash", "arguments": "{}"},
318 {"type": "function_call_output", "call_id": "call_1", "output": envelope}
319 ]
320 });
321 let bytes = serde_json::to_vec(&body).unwrap();
322 let (out, orig, comp) = compress_request_body(body, bytes.len());
323
324 assert!(comp < orig);
325 let parsed: Value = serde_json::from_slice(&out).unwrap();
326 let output = parsed["input"][1]["output"].as_str().unwrap();
327 let envelope: Value = serde_json::from_str(output).unwrap();
328 assert_eq!(envelope["content"][0]["text"].as_str().unwrap(), expected);
329 }
330
331 #[test]
332 fn shell_json_envelope_non_text_field_is_compressed() {
333 let _lock = crate::core::data_dir::test_env_lock();
334 let raw = long_git_status();
335 let expected = compress_tool_result(&raw, Some("Bash"));
336 let envelope = serde_json::to_string(&serde_json::json!({
339 "stdout": raw,
340 "exit_code": 0,
341 }))
342 .unwrap();
343
344 let body = serde_json::json!({
345 "model": "gpt-5",
346 "input": [
347 {"type": "function_call", "call_id": "call_1", "name": "Bash", "arguments": "{}"},
348 {"type": "function_call_output", "call_id": "call_1", "output": envelope}
349 ]
350 });
351 let bytes = serde_json::to_vec(&body).unwrap();
352 let (out, orig, comp) = compress_request_body(body, bytes.len());
353
354 assert!(comp < orig, "non-text shell field should be compressed");
355 let parsed: Value = serde_json::from_slice(&out).unwrap();
356 let output = parsed["input"][1]["output"].as_str().unwrap();
357 let envelope: Value = serde_json::from_str(output).unwrap();
358 assert_eq!(envelope["stdout"].as_str().unwrap(), expected);
359 assert_eq!(envelope["exit_code"].as_i64().unwrap(), 0);
360 }
361
362 #[test]
363 fn old_shell_json_envelope_text_is_pruned() {
364 let _iso = crate::core::data_dir::isolated_data_dir();
365 let raw = long_git_status();
366 let envelope = serde_json::to_string(&serde_json::json!({
367 "content": [{"type": "text", "text": raw}],
368 "isError": false,
369 }))
370 .unwrap();
371 let mut output = Value::String(envelope);
372
373 assert!(prune_output_field(&mut output, ToolResultKind::Shell));
374 let envelope: Value = serde_json::from_str(output.as_str().unwrap()).unwrap();
375 assert!(
376 envelope["content"][0]["text"].as_str().unwrap().len() < raw.len(),
377 "nested text payload should be pruned"
378 );
379 }
380
381 #[test]
382 fn string_output_mirrors_engine_and_shrinks() {
383 let _lock = crate::core::data_dir::test_env_lock();
386 let raw = long_git_status();
387 let expected = compress_tool_result(&raw, None);
388 assert!(
389 expected.len() < raw.len(),
390 "fixture must be compressible by the shared engine"
391 );
392
393 let body = serde_json::json!({
394 "model": "gpt-5",
395 "input": [
396 {"type": "function_call_output", "call_id": "call_1", "output": raw}
397 ]
398 });
399 let bytes = serde_json::to_vec(&body).unwrap();
400 let (out, orig, comp) = compress_request_body(body, bytes.len());
401
402 assert!(comp < orig, "compressed body must be smaller");
403 let parsed: Value = serde_json::from_slice(&out).unwrap();
404 assert_eq!(
405 parsed["input"][0]["output"].as_str().unwrap(),
406 expected,
407 "output must be exactly what the shared compressor produces"
408 );
409 }
410
411 #[test]
412 fn array_output_text_is_compressed() {
413 let _lock = crate::core::data_dir::test_env_lock();
416 let raw = long_git_status();
417 let expected = compress_tool_result(&raw, None);
418
419 let body = serde_json::json!({
420 "input": [
421 {
422 "type": "function_call_output",
423 "call_id": "call_1",
424 "output": [{"type": "input_text", "text": raw}]
425 }
426 ]
427 });
428 let bytes = serde_json::to_vec(&body).unwrap();
429 let (out, orig, comp) = compress_request_body(body, bytes.len());
430
431 assert!(comp < orig);
432 let parsed: Value = serde_json::from_slice(&out).unwrap();
433 assert_eq!(
434 parsed["input"][0]["output"][0]["text"].as_str().unwrap(),
435 expected
436 );
437 }
438
439 #[test]
440 fn non_tool_output_items_are_untouched() {
441 let _iso = crate::core::data_dir::isolated_data_dir();
442 let body = serde_json::json!({
443 "input": [
444 {"type": "message", "role": "user", "content": long_git_status()},
445 {"type": "function_call", "call_id": "c", "name": "x", "arguments": "{}"}
446 ]
447 });
448 let bytes = serde_json::to_vec(&body).unwrap();
449 let (out, orig, comp) = compress_request_body(body.clone(), bytes.len());
450
451 assert_eq!(comp, orig, "no function_call_output → passthrough");
452 let reparsed: Value = serde_json::from_slice(&out).unwrap();
453 assert_eq!(reparsed, body);
454 }
455
456 #[test]
457 fn plain_string_input_passthrough() {
458 let _iso = crate::core::data_dir::isolated_data_dir();
459 let body = serde_json::json!({"model": "gpt-5", "input": "hello world"});
460 let bytes = serde_json::to_vec(&body).unwrap();
461 let (out, orig, comp) = compress_request_body(body.clone(), bytes.len());
462 assert_eq!(comp, orig);
463 let reparsed: Value = serde_json::from_slice(&out).unwrap();
464 assert_eq!(reparsed, body);
465 }
466
467 #[test]
468 fn no_input_field_passthrough() {
469 let _iso = crate::core::data_dir::isolated_data_dir();
470 let body = serde_json::json!({"model": "gpt-5", "previous_response_id": "resp_abc"});
471 let bytes = serde_json::to_vec(&body).unwrap();
472 let (out, orig, comp) = compress_request_body(body.clone(), bytes.len());
473 assert_eq!(comp, orig);
474 let reparsed: Value = serde_json::from_slice(&out).unwrap();
475 assert_eq!(reparsed, body);
476 }
477
478 #[test]
479 fn chatgpt_responses_eval_fixture_keeps_exact_payloads_and_pairing() {
480 let _iso = crate::core::data_dir::isolated_data_dir();
481 crate::core::config::Config::update_global(|c| {
482 c.proxy.role_aggressiveness.user = Some(0.8);
483 })
484 .unwrap();
485
486 let command_input = "$ cargo test --lib proxy::openai_responses\nerror[E0425]: cannot find value `x` in this scope\nsrc/proxy/openai_responses.rs:12:9";
487 let body = serde_json::json!({"model": "gpt-5", "input": command_input});
488 let bytes = serde_json::to_vec(&body).unwrap();
489 let (out, orig, comp) = compress_request_body(body.clone(), bytes.len());
490 assert_eq!(comp, orig, "top-level exact command string must stay raw");
491 assert_eq!(serde_json::from_slice::<Value>(&out).unwrap(), body);
492
493 let old_json = long_tool_json();
494 let recent_json = long_tool_json();
495 let old_shell = long_git_status();
496 let input_text_block = "```rust\nfn main() { panic!(\"exact\"); }\n```\nRegex: src/[a-z_]+\\.rs:\\d+\nPath: /Users/alex/work/app/src/main.rs\nError: error[E0425]: cannot find value `x` in this scope";
497 let mut input = vec![
498 serde_json::json!({"type": "reasoning", "id": "rs_1", "summary": []}),
499 serde_json::json!({"type": "function_call", "call_id": "json_old", "name": "submit_tool_json", "arguments": "{\"strict\":true}"}),
500 serde_json::json!({"type": "function_call_output", "call_id": "json_old", "output": old_json}),
501 serde_json::json!({"type": "function_call", "call_id": "shell_old", "name": "Bash", "arguments": "{\"cmd\":\"git status\"}"}),
502 serde_json::json!({"type": "function_call_output", "call_id": "shell_old", "output": old_shell}),
503 serde_json::json!({"type": "message", "role": "user", "content": [{"type": "input_text", "text": input_text_block}]}),
504 ];
505 while input.len() < 22 {
506 input.push(serde_json::json!({
507 "type": "message",
508 "role": "user",
509 "content": format!("filler {}", input.len()),
510 }));
511 }
512 input.push(serde_json::json!({"type": "function_call", "call_id": "json_recent", "name": "submit_tool_json", "arguments": "{\"strict\":true}"}));
513 input.push(serde_json::json!({"type": "function_call_output", "call_id": "json_recent", "output": recent_json}));
514
515 let body = serde_json::json!({"model": "gpt-5", "input": input});
516 let item_count = body["input"].as_array().unwrap().len();
517 let bytes = serde_json::to_vec(&body).unwrap();
518 let (out, orig, comp) = compress_request_body(body, bytes.len());
519 assert!(comp < orig, "old shell output should still provide savings");
520
521 let parsed: Value = serde_json::from_slice(&out).unwrap();
522 let input = parsed["input"].as_array().unwrap();
523 assert_eq!(input.len(), item_count, "no Responses item may be dropped");
524 assert_eq!(input[0]["type"], "reasoning");
525 assert_eq!(input[1]["type"], "function_call");
526 assert_eq!(input[2]["type"], "function_call_output");
527 assert_eq!(input[2]["output"].as_str().unwrap(), old_json);
528 assert_ne!(input[4]["output"].as_str().unwrap(), old_shell);
529 assert_eq!(
530 input[5]["content"][0]["text"].as_str().unwrap(),
531 input_text_block
532 );
533 assert_eq!(input[22]["type"], "function_call");
534 assert_eq!(input[23]["type"], "function_call_output");
535 assert_eq!(input[23]["output"].as_str().unwrap(), recent_json);
536 assert_eq!(input[1]["call_id"], input[2]["call_id"]);
537 assert_eq!(input[22]["call_id"], input[23]["call_id"]);
538 }
539
540 #[test]
541 fn responses_instructions_prose_compressed_and_assistant_untouched() {
542 let _iso = crate::core::data_dir::isolated_data_dir();
543 crate::core::config::Config::update_global(|c| {
544 c.proxy.role_aggressiveness.system = Some(0.6);
545 })
546 .unwrap();
547
548 let prose = big_prose();
549 let body = serde_json::json!({
550 "model": "gpt-5",
551 "instructions": prose,
552 "input": [
553 {"type": "message", "role": "user", "content": "hi"},
554 {"type": "message", "role": "assistant", "content": prose},
555 ]
556 });
557 let bytes = serde_json::to_vec(&body).unwrap();
558 let (out, orig, comp) = compress_request_body(body, bytes.len());
559 assert!(comp < orig, "enabled instructions prose must save bytes");
560 let parsed: Value = serde_json::from_slice(&out).unwrap();
561
562 assert!(
563 parsed["instructions"].as_str().unwrap().len() < prose.len(),
564 "Responses instructions must be compressed when enabled"
565 );
566 assert_eq!(
567 parsed["input"][1]["content"].as_str().unwrap(),
568 prose,
569 "assistant turns must pass through verbatim (#710)"
570 );
571 }
572
573 #[test]
574 fn responses_user_prose_compressed_only_in_frozen_region() {
575 let _iso = crate::core::data_dir::isolated_data_dir();
576 crate::core::config::Config::update_global(|c| {
577 c.proxy.role_aggressiveness.user = Some(0.7);
578 })
579 .unwrap();
580
581 let prose = big_prose();
582 let mut input = Vec::new();
584 for i in 0..30 {
585 let role = if i % 2 == 0 { "user" } else { "assistant" };
586 input.push(serde_json::json!({
587 "type": "message",
588 "role": role,
589 "content": prose,
590 }));
591 }
592 let body = serde_json::json!({"model": "gpt-5", "input": input});
593 let bytes = serde_json::to_vec(&body).unwrap();
594 let (out, orig, comp) = compress_request_body(body, bytes.len());
595 assert!(comp < orig, "old user prose must save bytes");
596 let parsed: Value = serde_json::from_slice(&out).unwrap();
597
598 let frozen_user = parsed["input"][0]["content"].as_str().unwrap();
599 assert!(
600 frozen_user.len() < prose.len(),
601 "old user prose should compress"
602 );
603 assert_eq!(
604 parsed["input"][1]["content"].as_str().unwrap(),
605 prose,
606 "assistant prose must stay verbatim"
607 );
608 assert_eq!(
609 parsed["input"][16]["content"].as_str().unwrap(),
610 prose,
611 "live-tail user prose must stay verbatim"
612 );
613 }
614
615 #[test]
616 fn responses_prose_compression_is_deterministic() {
617 let _iso = crate::core::data_dir::isolated_data_dir();
618 crate::core::config::Config::update_global(|c| {
619 c.proxy.role_aggressiveness.system = Some(0.6);
620 })
621 .unwrap();
622
623 let prose = big_prose();
624 let mk = || {
625 serde_json::json!({
626 "model": "gpt-5",
627 "instructions": prose,
628 "input": [{"type": "message", "role": "user", "content": "hi"}],
629 })
630 };
631 let (a, b) = (mk(), mk());
632 let (la, lb) = (
633 serde_json::to_vec(&a).unwrap().len(),
634 serde_json::to_vec(&b).unwrap().len(),
635 );
636 assert_eq!(
637 compress_request_body(a, la).0,
638 compress_request_body(b, lb).0,
639 "identical input must yield byte-identical output (#498)"
640 );
641 }
642
643 #[test]
644 fn short_output_unchanged() {
645 let _iso = crate::core::data_dir::isolated_data_dir();
646 let body = serde_json::json!({
647 "input": [
648 {"type": "function_call_output", "call_id": "c", "output": "ok"}
649 ]
650 });
651 let bytes = serde_json::to_vec(&body).unwrap();
652 let (out, orig, comp) = compress_request_body(body.clone(), bytes.len());
653 assert_eq!(comp, orig);
654 let reparsed: Value = serde_json::from_slice(&out).unwrap();
655 assert_eq!(reparsed, body);
656 }
657
658 fn responses_read_turns(pairs: usize) -> Vec<Value> {
661 let code = (0..40)
662 .map(|i| format!(" let v{i} = compute_{i}(ctx, opts);"))
663 .collect::<Vec<_>>()
664 .join("\n");
665 let mut input = Vec::new();
666 for t in 0..pairs {
667 input.push(serde_json::json!({
668 "type": "function_call", "call_id": format!("c{t}"),
669 "name": "read_file", "arguments": "{}"
670 }));
671 input.push(serde_json::json!({
672 "type": "function_call_output", "call_id": format!("c{t}"),
673 "output": format!("{code}\n// turn {t}")
674 }));
675 }
676 input
677 }
678
679 #[test]
680 fn cache_aware_prune_stubs_old_reads_keeps_recent_and_pairing() {
681 let _iso = crate::core::data_dir::isolated_data_dir();
683 let body = serde_json::json!({"model": "gpt-5", "input": responses_read_turns(14)});
685 let item_count = body["input"].as_array().unwrap().len();
686 let bytes = serde_json::to_vec(&body).unwrap();
687 let (out, orig, comp) = compress_request_body(body, bytes.len());
688 assert!(comp < orig, "old reads must be pruned for savings");
689
690 let parsed: Value = serde_json::from_slice(&out).unwrap();
691 let input = parsed["input"].as_array().unwrap();
692 assert_eq!(input.len(), item_count, "no items may be removed (pairing)");
694 for (i, item) in input.iter().enumerate() {
695 let expect = if i.is_multiple_of(2) {
696 "function_call"
697 } else {
698 "function_call_output"
699 };
700 assert_eq!(item["type"], expect, "item {i} type/order changed");
701 }
702 let old = input[1]["output"].as_str().unwrap();
704 assert!(
705 old.contains("Re-read the file"),
706 "old read should be stubbed, got: {old}"
707 );
708 let recent = input[27]["output"].as_str().unwrap();
710 assert!(
711 recent.contains("v39"),
712 "recent read must be protected, got: {recent}"
713 );
714 }
715
716 #[test]
717 fn responses_compression_is_deterministic() {
718 let _iso = crate::core::data_dir::isolated_data_dir();
721 let mk = || serde_json::json!({"model": "gpt-5", "input": responses_read_turns(14)});
722 let (a, b) = (mk(), mk());
723 let (la, lb) = (
724 serde_json::to_vec(&a).unwrap().len(),
725 serde_json::to_vec(&b).unwrap().len(),
726 );
727 let (out_a, _, _) = compress_request_body(a, la);
728 let (out_b, _, _) = compress_request_body(b, lb);
729 assert_eq!(out_a, out_b, "identical input must yield identical bytes");
730 }
731
732 #[test]
733 fn cache_aware_responses_prefix_is_byte_stable_across_turns() {
734 let _iso = crate::core::data_dir::isolated_data_dir();
738 let mut prev: Vec<String> = Vec::new();
739 let mut prev_boundary = 0;
740 for pairs in 1..=20 {
741 let input = responses_read_turns(pairs);
742 let len = input.len();
743 let body = serde_json::json!({"model": "gpt-5", "input": input});
744 let bytes = serde_json::to_vec(&body).unwrap();
745 let (out, _, _) = compress_request_body(body, bytes.len());
746 let parsed: Value = serde_json::from_slice(&out).unwrap();
747 let items: Vec<String> = parsed["input"]
748 .as_array()
749 .unwrap()
750 .iter()
751 .map(Value::to_string)
752 .collect();
753 for i in 0..prev_boundary {
754 assert_eq!(
755 prev[i], items[i],
756 "Responses item {i} changed at turn {pairs} — prompt cache prefix broken"
757 );
758 }
759 prev = items;
760 prev_boundary = crate::proxy::history_prune::prune_boundary(
761 crate::core::config::HistoryMode::CacheAware,
762 len,
763 );
764 }
765 }
766
767 #[test]
768 fn effort_control_sets_nested_reasoning_effort() {
769 let _iso = crate::core::data_dir::isolated_data_dir();
771 crate::test_env::remove_var("LEAN_CTX_PROXY_EFFORT");
772 crate::core::config::Config::update_global(|c| {
773 c.proxy.effort = Some("low".into());
774 })
775 .unwrap();
776 let body = serde_json::json!({"model": "gpt-5.5", "input": []});
777 let bytes = serde_json::to_vec(&body).unwrap();
778 let (out, _o, _c) = compress_request_body(body, bytes.len());
779 assert_eq!(
780 serde_json::from_slice::<Value>(&out).unwrap()["reasoning"]["effort"],
781 "low"
782 );
783 }
784}