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