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 _iso = crate::core::data_dir::isolated_data_dir();
363 let raw = long_git_status();
364 let envelope = serde_json::to_string(&serde_json::json!({
365 "content": [{"type": "text", "text": raw}],
366 "isError": false,
367 }))
368 .unwrap();
369 let mut output = Value::String(envelope);
370
371 assert!(prune_output_field(&mut output, ToolResultKind::Shell));
372 let envelope: Value = serde_json::from_str(output.as_str().unwrap()).unwrap();
373 assert!(
374 envelope["content"][0]["text"].as_str().unwrap().len() < raw.len(),
375 "nested text payload should be pruned"
376 );
377 }
378
379 #[test]
380 fn string_output_mirrors_engine_and_shrinks() {
381 let _lock = crate::core::data_dir::test_env_lock();
384 let raw = long_git_status();
385 let expected = compress_tool_result(&raw, None);
386 assert!(
387 expected.len() < raw.len(),
388 "fixture must be compressible by the shared engine"
389 );
390
391 let body = serde_json::json!({
392 "model": "gpt-5",
393 "input": [
394 {"type": "function_call_output", "call_id": "call_1", "output": raw}
395 ]
396 });
397 let bytes = serde_json::to_vec(&body).unwrap();
398 let (out, orig, comp) = compress_request_body(body, bytes.len());
399
400 assert!(comp < orig, "compressed body must be smaller");
401 let parsed: Value = serde_json::from_slice(&out).unwrap();
402 assert_eq!(
403 parsed["input"][0]["output"].as_str().unwrap(),
404 expected,
405 "output must be exactly what the shared compressor produces"
406 );
407 }
408
409 #[test]
410 fn array_output_text_is_compressed() {
411 let _lock = crate::core::data_dir::test_env_lock();
414 let raw = long_git_status();
415 let expected = compress_tool_result(&raw, None);
416
417 let body = serde_json::json!({
418 "input": [
419 {
420 "type": "function_call_output",
421 "call_id": "call_1",
422 "output": [{"type": "input_text", "text": raw}]
423 }
424 ]
425 });
426 let bytes = serde_json::to_vec(&body).unwrap();
427 let (out, orig, comp) = compress_request_body(body, bytes.len());
428
429 assert!(comp < orig);
430 let parsed: Value = serde_json::from_slice(&out).unwrap();
431 assert_eq!(
432 parsed["input"][0]["output"][0]["text"].as_str().unwrap(),
433 expected
434 );
435 }
436
437 #[test]
438 fn non_tool_output_items_are_untouched() {
439 let _iso = crate::core::data_dir::isolated_data_dir();
440 let body = serde_json::json!({
441 "input": [
442 {"type": "message", "role": "user", "content": long_git_status()},
443 {"type": "function_call", "call_id": "c", "name": "x", "arguments": "{}"}
444 ]
445 });
446 let bytes = serde_json::to_vec(&body).unwrap();
447 let (out, orig, comp) = compress_request_body(body.clone(), bytes.len());
448
449 assert_eq!(comp, orig, "no function_call_output → passthrough");
450 let reparsed: Value = serde_json::from_slice(&out).unwrap();
451 assert_eq!(reparsed, body);
452 }
453
454 #[test]
455 fn plain_string_input_passthrough() {
456 let _iso = crate::core::data_dir::isolated_data_dir();
457 let body = serde_json::json!({"model": "gpt-5", "input": "hello world"});
458 let bytes = serde_json::to_vec(&body).unwrap();
459 let (out, orig, comp) = compress_request_body(body.clone(), bytes.len());
460 assert_eq!(comp, orig);
461 let reparsed: Value = serde_json::from_slice(&out).unwrap();
462 assert_eq!(reparsed, body);
463 }
464
465 #[test]
466 fn no_input_field_passthrough() {
467 let _iso = crate::core::data_dir::isolated_data_dir();
468 let body = serde_json::json!({"model": "gpt-5", "previous_response_id": "resp_abc"});
469 let bytes = serde_json::to_vec(&body).unwrap();
470 let (out, orig, comp) = compress_request_body(body.clone(), bytes.len());
471 assert_eq!(comp, orig);
472 let reparsed: Value = serde_json::from_slice(&out).unwrap();
473 assert_eq!(reparsed, body);
474 }
475
476 #[test]
477 fn chatgpt_responses_eval_fixture_keeps_exact_payloads_and_pairing() {
478 let _iso = crate::core::data_dir::isolated_data_dir();
479 crate::core::config::Config::update_global(|c| {
480 c.proxy.role_aggressiveness.user = Some(0.8);
481 })
482 .unwrap();
483
484 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";
485 let body = serde_json::json!({"model": "gpt-5", "input": command_input});
486 let bytes = serde_json::to_vec(&body).unwrap();
487 let (out, orig, comp) = compress_request_body(body.clone(), bytes.len());
488 assert_eq!(comp, orig, "top-level exact command string must stay raw");
489 assert_eq!(serde_json::from_slice::<Value>(&out).unwrap(), body);
490
491 let old_json = long_tool_json();
492 let recent_json = long_tool_json();
493 let old_shell = long_git_status();
494 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";
495 let mut input = vec![
496 serde_json::json!({"type": "reasoning", "id": "rs_1", "summary": []}),
497 serde_json::json!({"type": "function_call", "call_id": "json_old", "name": "submit_tool_json", "arguments": "{\"strict\":true}"}),
498 serde_json::json!({"type": "function_call_output", "call_id": "json_old", "output": old_json}),
499 serde_json::json!({"type": "function_call", "call_id": "shell_old", "name": "Bash", "arguments": "{\"cmd\":\"git status\"}"}),
500 serde_json::json!({"type": "function_call_output", "call_id": "shell_old", "output": old_shell}),
501 serde_json::json!({"type": "message", "role": "user", "content": [{"type": "input_text", "text": input_text_block}]}),
502 ];
503 while input.len() < 22 {
504 input.push(serde_json::json!({
505 "type": "message",
506 "role": "user",
507 "content": format!("filler {}", input.len()),
508 }));
509 }
510 input.push(serde_json::json!({"type": "function_call", "call_id": "json_recent", "name": "submit_tool_json", "arguments": "{\"strict\":true}"}));
511 input.push(serde_json::json!({"type": "function_call_output", "call_id": "json_recent", "output": recent_json}));
512
513 let body = serde_json::json!({"model": "gpt-5", "input": input});
514 let item_count = body["input"].as_array().unwrap().len();
515 let bytes = serde_json::to_vec(&body).unwrap();
516 let (out, orig, comp) = compress_request_body(body, bytes.len());
517 assert!(comp < orig, "old shell output should still provide savings");
518
519 let parsed: Value = serde_json::from_slice(&out).unwrap();
520 let input = parsed["input"].as_array().unwrap();
521 assert_eq!(input.len(), item_count, "no Responses item may be dropped");
522 assert_eq!(input[0]["type"], "reasoning");
523 assert_eq!(input[1]["type"], "function_call");
524 assert_eq!(input[2]["type"], "function_call_output");
525 assert_eq!(input[2]["output"].as_str().unwrap(), old_json);
526 assert_ne!(input[4]["output"].as_str().unwrap(), old_shell);
527 assert_eq!(
528 input[5]["content"][0]["text"].as_str().unwrap(),
529 input_text_block
530 );
531 assert_eq!(input[22]["type"], "function_call");
532 assert_eq!(input[23]["type"], "function_call_output");
533 assert_eq!(input[23]["output"].as_str().unwrap(), recent_json);
534 assert_eq!(input[1]["call_id"], input[2]["call_id"]);
535 assert_eq!(input[22]["call_id"], input[23]["call_id"]);
536 }
537
538 #[test]
539 fn responses_instructions_prose_compressed_and_assistant_untouched() {
540 let _iso = crate::core::data_dir::isolated_data_dir();
541 crate::core::config::Config::update_global(|c| {
542 c.proxy.role_aggressiveness.system = Some(0.6);
543 })
544 .unwrap();
545
546 let prose = big_prose();
547 let body = serde_json::json!({
548 "model": "gpt-5",
549 "instructions": prose,
550 "input": [
551 {"type": "message", "role": "user", "content": "hi"},
552 {"type": "message", "role": "assistant", "content": prose},
553 ]
554 });
555 let bytes = serde_json::to_vec(&body).unwrap();
556 let (out, orig, comp) = compress_request_body(body, bytes.len());
557 assert!(comp < orig, "enabled instructions prose must save bytes");
558 let parsed: Value = serde_json::from_slice(&out).unwrap();
559
560 assert!(
561 parsed["instructions"].as_str().unwrap().len() < prose.len(),
562 "Responses instructions must be compressed when enabled"
563 );
564 assert_eq!(
565 parsed["input"][1]["content"].as_str().unwrap(),
566 prose,
567 "assistant turns must pass through verbatim (#710)"
568 );
569 }
570
571 #[test]
572 fn responses_user_prose_compressed_only_in_frozen_region() {
573 let _iso = crate::core::data_dir::isolated_data_dir();
574 crate::core::config::Config::update_global(|c| {
575 c.proxy.role_aggressiveness.user = Some(0.7);
576 })
577 .unwrap();
578
579 let prose = big_prose();
580 let mut input = Vec::new();
582 for i in 0..30 {
583 let role = if i % 2 == 0 { "user" } else { "assistant" };
584 input.push(serde_json::json!({
585 "type": "message",
586 "role": role,
587 "content": prose,
588 }));
589 }
590 let body = serde_json::json!({"model": "gpt-5", "input": input});
591 let bytes = serde_json::to_vec(&body).unwrap();
592 let (out, orig, comp) = compress_request_body(body, bytes.len());
593 assert!(comp < orig, "old user prose must save bytes");
594 let parsed: Value = serde_json::from_slice(&out).unwrap();
595
596 let frozen_user = parsed["input"][0]["content"].as_str().unwrap();
597 assert!(
598 frozen_user.len() < prose.len(),
599 "old user prose should compress"
600 );
601 assert_eq!(
602 parsed["input"][1]["content"].as_str().unwrap(),
603 prose,
604 "assistant prose must stay verbatim"
605 );
606 assert_eq!(
607 parsed["input"][16]["content"].as_str().unwrap(),
608 prose,
609 "live-tail user prose must stay verbatim"
610 );
611 }
612
613 #[test]
614 fn responses_prose_compression_is_deterministic() {
615 let _iso = crate::core::data_dir::isolated_data_dir();
616 crate::core::config::Config::update_global(|c| {
617 c.proxy.role_aggressiveness.system = Some(0.6);
618 })
619 .unwrap();
620
621 let prose = big_prose();
622 let mk = || {
623 serde_json::json!({
624 "model": "gpt-5",
625 "instructions": prose,
626 "input": [{"type": "message", "role": "user", "content": "hi"}],
627 })
628 };
629 let (a, b) = (mk(), mk());
630 let (la, lb) = (
631 serde_json::to_vec(&a).unwrap().len(),
632 serde_json::to_vec(&b).unwrap().len(),
633 );
634 assert_eq!(
635 compress_request_body(a, la).0,
636 compress_request_body(b, lb).0,
637 "identical input must yield byte-identical output (#498)"
638 );
639 }
640
641 #[test]
642 fn short_output_unchanged() {
643 let _iso = crate::core::data_dir::isolated_data_dir();
644 let body = serde_json::json!({
645 "input": [
646 {"type": "function_call_output", "call_id": "c", "output": "ok"}
647 ]
648 });
649 let bytes = serde_json::to_vec(&body).unwrap();
650 let (out, orig, comp) = compress_request_body(body.clone(), bytes.len());
651 assert_eq!(comp, orig);
652 let reparsed: Value = serde_json::from_slice(&out).unwrap();
653 assert_eq!(reparsed, body);
654 }
655
656 fn responses_read_turns(pairs: usize) -> Vec<Value> {
659 let code = (0..40)
660 .map(|i| format!(" let v{i} = compute_{i}(ctx, opts);"))
661 .collect::<Vec<_>>()
662 .join("\n");
663 let mut input = Vec::new();
664 for t in 0..pairs {
665 input.push(serde_json::json!({
666 "type": "function_call", "call_id": format!("c{t}"),
667 "name": "read_file", "arguments": "{}"
668 }));
669 input.push(serde_json::json!({
670 "type": "function_call_output", "call_id": format!("c{t}"),
671 "output": format!("{code}\n// turn {t}")
672 }));
673 }
674 input
675 }
676
677 #[test]
678 fn cache_aware_prune_stubs_old_reads_keeps_recent_and_pairing() {
679 let _iso = crate::core::data_dir::isolated_data_dir();
681 let body = serde_json::json!({"model": "gpt-5", "input": responses_read_turns(14)});
683 let item_count = body["input"].as_array().unwrap().len();
684 let bytes = serde_json::to_vec(&body).unwrap();
685 let (out, orig, comp) = compress_request_body(body, bytes.len());
686 assert!(comp < orig, "old reads must be pruned for savings");
687
688 let parsed: Value = serde_json::from_slice(&out).unwrap();
689 let input = parsed["input"].as_array().unwrap();
690 assert_eq!(input.len(), item_count, "no items may be removed (pairing)");
692 for (i, item) in input.iter().enumerate() {
693 let expect = if i.is_multiple_of(2) {
694 "function_call"
695 } else {
696 "function_call_output"
697 };
698 assert_eq!(item["type"], expect, "item {i} type/order changed");
699 }
700 let old = input[1]["output"].as_str().unwrap();
702 assert!(
703 old.contains("Re-read the file"),
704 "old read should be stubbed, got: {old}"
705 );
706 let recent = input[27]["output"].as_str().unwrap();
708 assert!(
709 recent.contains("v39"),
710 "recent read must be protected, got: {recent}"
711 );
712 }
713
714 #[test]
715 fn responses_compression_is_deterministic() {
716 let _iso = crate::core::data_dir::isolated_data_dir();
719 let mk = || serde_json::json!({"model": "gpt-5", "input": responses_read_turns(14)});
720 let (a, b) = (mk(), mk());
721 let (la, lb) = (
722 serde_json::to_vec(&a).unwrap().len(),
723 serde_json::to_vec(&b).unwrap().len(),
724 );
725 let (out_a, _, _) = compress_request_body(a, la);
726 let (out_b, _, _) = compress_request_body(b, lb);
727 assert_eq!(out_a, out_b, "identical input must yield identical bytes");
728 }
729
730 #[test]
731 fn cache_aware_responses_prefix_is_byte_stable_across_turns() {
732 let _iso = crate::core::data_dir::isolated_data_dir();
736 let mut prev: Vec<String> = Vec::new();
737 let mut prev_boundary = 0;
738 for pairs in 1..=20 {
739 let input = responses_read_turns(pairs);
740 let len = input.len();
741 let body = serde_json::json!({"model": "gpt-5", "input": input});
742 let bytes = serde_json::to_vec(&body).unwrap();
743 let (out, _, _) = compress_request_body(body, bytes.len());
744 let parsed: Value = serde_json::from_slice(&out).unwrap();
745 let items: Vec<String> = parsed["input"]
746 .as_array()
747 .unwrap()
748 .iter()
749 .map(Value::to_string)
750 .collect();
751 for i in 0..prev_boundary {
752 assert_eq!(
753 prev[i], items[i],
754 "Responses item {i} changed at turn {pairs} — prompt cache prefix broken"
755 );
756 }
757 prev = items;
758 prev_boundary = crate::proxy::history_prune::prune_boundary(
759 crate::core::config::HistoryMode::CacheAware,
760 len,
761 );
762 }
763 }
764
765 #[test]
766 fn effort_control_sets_nested_reasoning_effort() {
767 let _iso = crate::core::data_dir::isolated_data_dir();
769 crate::test_env::remove_var("LEAN_CTX_PROXY_EFFORT");
770 crate::core::config::Config::update_global(|c| {
771 c.proxy.effort = Some("low".into());
772 })
773 .unwrap();
774 let body = serde_json::json!({"model": "gpt-5.5", "input": []});
775 let bytes = serde_json::to_vec(&body).unwrap();
776 let (out, _o, _c) = compress_request_body(body, bytes.len());
777 assert_eq!(
778 serde_json::from_slice::<Value>(&out).unwrap()["reasoning"]["effort"],
779 "low"
780 );
781 }
782}