From 2eb396deb54caae38443e589d40934bce423d369 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Wed, 6 May 2026 13:38:43 +0300 Subject: [PATCH] Propagate cache key and service tiers in compact (#21249) ## Why `/responses/compact` should preserve the request-affinity fields that apply to the active auth mode. ChatGPT-auth compact requests need the effective `service_tier`, and compact requests for every auth mode need the stable `prompt_cache_key`, so compaction does not quietly lose routing or cache behavior that normal sampling already has. This follows the request-parity direction from #20719, but keeps the net change focused on the compact payload fields needed here. ## What changed - Add `service_tier` and `prompt_cache_key` to the compact endpoint input payload. - Build the remote compact payload from the existing responses request builder output so `Fast` still maps to `priority` when compact sends a service tier. - Pass the turn service tier into remote compaction, but only include it in compact payloads for ChatGPT-backed auth. - Keep `prompt_cache_key` on compact payloads for all auth modes. - Add request-body diff snapshot coverage in `core/tests/suite/compact_remote.rs` for: - API-key auth reusing `prompt_cache_key` while omitting `service_tier` even when `Fast` is configured. - ChatGPT auth reusing both `service_tier` and `prompt_cache_key`. - Drive the snapshot coverage through five varied turns: plain text, multi-part text, tool-call continuation, image+text input, local-shell continuation, and final-turn reasoning output. ## Verification - Added insta snapshots for compact request-body parity against the last normal `/responses` request after five varied turns. - Not run locally per repo guidance; relying on GitHub CI for test execution. --------- Co-authored-by: Codex --- codex-rs/codex-api/src/common.rs | 4 + codex-rs/core/src/client.rs | 21 +- codex-rs/core/src/compact_remote.rs | 8 +- codex-rs/core/tests/common/BUILD.bazel | 3 + .../core/tests/common/context_snapshot.rs | 109 +++++++ codex-rs/core/tests/suite/compact_remote.rs | 279 ++++++++++++++++++ ...pi_auth_prompt_cache_key_request_diff.snap | 43 +++ ...ce_tier_prompt_cache_key_request_diff.snap | 43 +++ 8 files changed, 502 insertions(+), 8 deletions(-) create mode 100644 codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_api_auth_prompt_cache_key_request_diff.snap create mode 100644 codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_chatgpt_auth_service_tier_prompt_cache_key_request_diff.snap diff --git a/codex-rs/codex-api/src/common.rs b/codex-rs/codex-api/src/common.rs index e2d2ed3c3c..50ac2685b4 100644 --- a/codex-rs/codex-api/src/common.rs +++ b/codex-rs/codex-api/src/common.rs @@ -32,6 +32,10 @@ pub struct CompactionInput<'a> { #[serde(skip_serializing_if = "Option::is_none")] pub reasoning: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub service_tier: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + pub prompt_cache_key: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] pub text: Option, } diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 00feb81a9f..6449237dd6 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -147,6 +147,12 @@ const MEMORIES_SUMMARIZE_ENDPOINT: &str = "/memories/trace_summarize"; pub(crate) const WEBSOCKET_CONNECT_TIMEOUT: Duration = Duration::from_millis(DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS); +pub(crate) struct CompactConversationRequestSettings { + pub(crate) effort: Option, + pub(crate) summary: ReasoningSummaryConfig, + pub(crate) service_tier: Option, +} + /// Session-scoped state shared by all [`ModelClient`] clones. /// /// This is intentionally kept minimal so `ModelClient` does not need to hold a full `Config`. Most @@ -414,12 +420,11 @@ impl ModelClient { /// /// The model selection and telemetry context are passed explicitly to keep `ModelClient` /// session-scoped. - pub async fn compact_conversation_history( + pub(crate) async fn compact_conversation_history( &self, prompt: &Prompt, model_info: &ModelInfo, - effort: Option, - summary: ReasoningSummaryConfig, + settings: CompactConversationRequestSettings, session_telemetry: &SessionTelemetry, compaction_trace: &CompactionTraceContext, ) -> Result> { @@ -442,9 +447,9 @@ impl ModelClient { &client_setup.api_provider, prompt, model_info, - effort, - summary, - /*service_tier*/ None, + settings.effort, + settings.summary, + settings.service_tier, )?; let ResponsesApiRequest { model, @@ -453,6 +458,8 @@ impl ModelClient { tools, parallel_tool_calls, reasoning, + service_tier, + prompt_cache_key, text, .. } = request; @@ -466,6 +473,8 @@ impl ModelClient { tools, parallel_tool_calls, reasoning, + service_tier: service_tier.as_deref(), + prompt_cache_key: prompt_cache_key.as_deref(), text, }; diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs index 1a3f0d4a7a..cf30a623b3 100644 --- a/codex-rs/core/src/compact_remote.rs +++ b/codex-rs/core/src/compact_remote.rs @@ -2,6 +2,7 @@ use std::collections::HashSet; use std::sync::Arc; use crate::Prompt; +use crate::client::CompactConversationRequestSettings; use crate::compact::CompactionAnalyticsAttempt; use crate::compact::InitialContextInjection; use crate::compact::compaction_status_from_result; @@ -170,8 +171,11 @@ async fn run_remote_compact_task_inner_impl( .compact_conversation_history( &prompt, &turn_context.model_info, - turn_context.reasoning_effort, - turn_context.reasoning_summary, + CompactConversationRequestSettings { + effort: turn_context.reasoning_effort, + summary: turn_context.reasoning_summary, + service_tier: turn_context.config.service_tier, + }, &turn_context.session_telemetry, &compaction_trace, ) diff --git a/codex-rs/core/tests/common/BUILD.bazel b/codex-rs/core/tests/common/BUILD.bazel index aec0c17817..983a2012b0 100644 --- a/codex-rs/core/tests/common/BUILD.bazel +++ b/codex-rs/core/tests/common/BUILD.bazel @@ -7,4 +7,7 @@ codex_rust_crate( lib_data_extra = [ "//codex-rs/core:model_availability_nux_fixtures", ], + deps_extra = [ + "@crates//:similar", + ], ) diff --git a/codex-rs/core/tests/common/context_snapshot.rs b/codex-rs/core/tests/common/context_snapshot.rs index cb899969d9..8aaefbbf15 100644 --- a/codex-rs/core/tests/common/context_snapshot.rs +++ b/codex-rs/core/tests/common/context_snapshot.rs @@ -1,5 +1,7 @@ use regex_lite::Regex; use serde_json::Value; +use similar::ChangeTag; +use similar::TextDiff; use std::sync::OnceLock; use crate::responses::ResponsesRequest; @@ -242,6 +244,102 @@ pub fn format_labeled_items_snapshot( format!("Scenario: {scenario}\n\n{sections}") } +/// Render changed JSON lines between two captured `/responses` request bodies. +/// +/// Request-parity tests use this to compare the entire JSON payload while showing only fields that +/// changed, with the same redactions as the other context snapshots. +pub fn format_request_body_diff_snapshot( + scenario: &str, + before_title: &str, + before_request: &ResponsesRequest, + after_title: &str, + after_request: &ResponsesRequest, + options: &ContextSnapshotOptions, +) -> String { + let before = format_request_body_snapshot(before_request, options); + let after = format_request_body_snapshot(after_request, options); + let diff = format_changed_lines_diff(before_title, &before, after_title, &after); + format!("Scenario: {scenario}\n\n{diff}") +} + +fn format_request_body_snapshot( + request: &ResponsesRequest, + options: &ContextSnapshotOptions, +) -> String { + let mut body = request.body_json(); + canonicalize_json_snapshot_value(&mut body, options); + serde_json::to_string_pretty(&body).expect("request body should serialize") +} + +fn canonicalize_json_snapshot_value(value: &mut Value, options: &ContextSnapshotOptions) { + match value { + Value::Array(values) => { + for value in values { + canonicalize_json_snapshot_value(value, options); + } + } + Value::Object(map) => { + // Keep request-body snapshots stable when serde_json preserves insertion order. + let mut entries = std::mem::take(map).into_iter().collect::>(); + entries.sort_by(|(left_key, _), (right_key, _)| left_key.cmp(right_key)); + for (key, mut value) in entries { + canonicalize_json_snapshot_value(&mut value, options); + map.insert(key, value); + } + } + Value::String(text) => { + *text = format_snapshot_json_string(text, options); + } + Value::Null | Value::Bool(_) | Value::Number(_) => {} + } +} + +fn format_snapshot_json_string(text: &str, options: &ContextSnapshotOptions) -> String { + let normalized = match options.render_mode { + ContextSnapshotRenderMode::RedactedText + | ContextSnapshotRenderMode::KindWithTextPrefix { .. } => normalize_snapshot_uuids( + &normalize_snapshot_line_endings(&canonicalize_snapshot_text(text)), + ), + ContextSnapshotRenderMode::FullText => normalize_snapshot_line_endings(text), + ContextSnapshotRenderMode::KindOnly => unreachable!(), + }; + match options.render_mode { + ContextSnapshotRenderMode::KindWithTextPrefix { max_chars } + if normalized.chars().count() > max_chars => + { + let prefix = normalized.chars().take(max_chars).collect::(); + format!("{prefix}...") + } + ContextSnapshotRenderMode::RedactedText + | ContextSnapshotRenderMode::FullText + | ContextSnapshotRenderMode::KindWithTextPrefix { .. } => normalized, + ContextSnapshotRenderMode::KindOnly => unreachable!(), + } +} + +fn format_changed_lines_diff( + before_title: &str, + before: &str, + after_title: &str, + after: &str, +) -> String { + let mut diff = format!("--- {before_title}\n+++ {after_title}\n"); + for change in TextDiff::from_lines(before, after).iter_all_changes() { + match change.tag() { + ChangeTag::Equal => {} + ChangeTag::Delete => { + diff.push('-'); + diff.push_str(change.value()); + } + ChangeTag::Insert => { + diff.push('+'); + diff.push_str(change.value()); + } + } + } + diff +} + fn format_snapshot_text(text: &str, options: &ContextSnapshotOptions) -> String { match options.render_mode { ContextSnapshotRenderMode::RedactedText => { @@ -342,6 +440,17 @@ fn normalize_dynamic_snapshot_paths(text: &str) -> String { .into_owned() } +fn normalize_snapshot_uuids(text: &str) -> String { + static UUID_RE: OnceLock = OnceLock::new(); + let uuid_re = UUID_RE.get_or_init(|| { + Regex::new( + r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b", + ) + .expect("uuid regex should compile") + }); + uuid_re.replace_all(text, "").into_owned() +} + #[cfg(test)] mod tests { use super::ContextSnapshotOptions; diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index c0a2a89394..5e15b54b77 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -7,6 +7,7 @@ use anyhow::Result; use codex_core::compact::SUMMARY_PREFIX; use codex_features::Feature; use codex_login::CodexAuth; +use codex_protocol::config_types::ServiceTier; use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::items::TurnItem; use codex_protocol::models::ContentItem; @@ -105,6 +106,23 @@ fn contains_defer_loading(value: &Value) -> bool { } } +fn canonical_json(value: &Value) -> Value { + match value { + Value::Object(map) => { + let mut entries = map.iter().collect::>(); + entries.sort_by(|(left_key, _), (right_key, _)| left_key.cmp(right_key)); + Value::Object( + entries + .into_iter() + .map(|(key, value)| (key.clone(), canonical_json(value))) + .collect(), + ) + } + Value::Array(values) => Value::Array(values.iter().map(canonical_json).collect()), + Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => value.clone(), + } +} + const PRETURN_CONTEXT_DIFF_CWD: &str = "/tmp/PRETURN_CONTEXT_DIFF_CWD"; const DUMMY_FUNCTION_NAME: &str = "test_tool"; const REMOTE_COMPACT_TURN_COMPLETE_TIMEOUT: Duration = Duration::from_secs(30); @@ -417,6 +435,267 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { Ok(()) } +async fn assert_remote_manual_compact_request_parity( + auth: CodexAuth, + configured_service_tier: Option, + expected_service_tier: Option<&str>, + snapshot_name: &str, + scenario: &str, +) -> Result<()> { + let mut builder = test_codex().with_auth(auth); + if let Some(service_tier) = configured_service_tier { + builder = builder.with_config(move |config| { + config.service_tier = Some(service_tier); + }); + } + let harness = TestCodexHarness::with_builder(builder).await?; + let codex = harness.test().codex.clone(); + let image_url = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=" + .to_string(); + + let responses_mock = responses::mount_sse_sequence( + harness.server(), + vec![ + responses::sse(vec![ + responses::ev_assistant_message("turn-one-assistant", "TURN_ONE_ASSISTANT"), + responses::ev_completed("turn-one-response"), + ]), + responses::sse(vec![ + responses::ev_reasoning_item( + "turn-two-reasoning", + &["TURN_TWO_REASONING"], + &["turn two raw content"], + ), + responses::ev_assistant_message("turn-two-assistant", "TURN_TWO_ASSISTANT"), + responses::ev_completed("turn-two-response"), + ]), + responses::sse(vec![ + responses::ev_function_call("turn-three-call", DUMMY_FUNCTION_NAME, "{}"), + responses::ev_completed("turn-three-call-response"), + ]), + responses::sse(vec![ + responses::ev_assistant_message("turn-three-assistant", "TURN_THREE_ASSISTANT"), + responses::ev_completed("turn-three-final-response"), + ]), + responses::sse(vec![ + responses::ev_local_shell_call( + "turn-four-local-shell", + "completed", + vec!["/bin/echo", "TURN_FOUR_LOCAL_SHELL"], + ), + responses::ev_completed("turn-four-local-shell-response"), + ]), + responses::sse(vec![ + responses::ev_assistant_message("turn-four-assistant", "TURN_FOUR_ASSISTANT"), + responses::ev_completed("turn-four-final-response"), + ]), + responses::sse(vec![ + responses::ev_reasoning_item( + "turn-five-reasoning", + &["TURN_FIVE_REASONING"], + &["turn five raw content"], + ), + responses::ev_assistant_message("turn-five-assistant", "TURN_FIVE_ASSISTANT"), + responses::ev_completed("turn-five-response"), + ]), + ], + ) + .await; + let compact_mock = responses::mount_compact_user_history_with_summary_once( + harness.server(), + "REMOTE_CACHE_TIER_SUMMARY", + ) + .await; + + codex + .submit(Op::UserInput { + environments: None, + items: vec![UserInput::Text { + text: "TURN_ONE_USER".to_string(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + responsesapi_client_metadata: None, + }) + .await?; + wait_for_turn_complete(&codex).await; + + codex + .submit(Op::UserInput { + environments: None, + items: vec![ + UserInput::Text { + text: "TURN_TWO_PREFIX".to_string(), + text_elements: Vec::new(), + }, + UserInput::Text { + text: "TURN_TWO_SUFFIX".to_string(), + text_elements: Vec::new(), + }, + ], + final_output_json_schema: None, + responsesapi_client_metadata: None, + }) + .await?; + wait_for_turn_complete(&codex).await; + + codex + .submit(Op::UserInput { + environments: None, + items: vec![UserInput::Text { + text: "TURN_THREE_TOOL_USER".to_string(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + responsesapi_client_metadata: None, + }) + .await?; + wait_for_turn_complete(&codex).await; + + codex + .submit(Op::UserInput { + environments: None, + items: vec![ + UserInput::Image { image_url }, + UserInput::Text { + text: "TURN_FOUR_IMAGE_USER".to_string(), + text_elements: Vec::new(), + }, + ], + final_output_json_schema: None, + responsesapi_client_metadata: None, + }) + .await?; + wait_for_turn_complete(&codex).await; + + codex + .submit(Op::UserInput { + environments: None, + items: vec![UserInput::Text { + text: "TURN_FIVE_USER".to_string(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + responsesapi_client_metadata: None, + }) + .await?; + wait_for_turn_complete(&codex).await; + + codex.submit(Op::Compact).await?; + wait_for_turn_complete(&codex).await; + + let response_requests = responses_mock.requests(); + assert_eq!( + response_requests.len(), + 7, + "expected five turns with one unsupported tool continuation and one local shell continuation" + ); + assert_eq!( + compact_mock.requests().len(), + 1, + "expected exactly one remote compact request" + ); + let normal_request = response_requests + .last() + .cloned() + .expect("last turn request missing"); + let compact_request = compact_mock.single_request(); + let normal_body = normal_request.body_json(); + let compact_body = compact_request.body_json(); + + let mut expected_compact_body_without_input = normal_body.clone(); + let expected_compact_object = expected_compact_body_without_input + .as_object_mut() + .expect("responses request body should be an object"); + for field in [ + "input", + "client_metadata", + "include", + "store", + "stream", + "tool_choice", + ] { + expected_compact_object.remove(field); + } + if expected_service_tier.is_none() { + expected_compact_object.remove("service_tier"); + } + let mut compact_body_without_input = compact_body.clone(); + compact_body_without_input + .as_object_mut() + .expect("compact request body should be an object") + .remove("input"); + let canonical_compact_body_without_input = canonical_json(&compact_body_without_input); + let canonical_expected_compact_body_without_input = + canonical_json(&expected_compact_body_without_input); + + assert_eq!( + json!({ + "compact_body_without_input": canonical_compact_body_without_input, + "expected_compact_body_without_input": canonical_expected_compact_body_without_input, + "prompt_cache_key_matches_responses": compact_body["prompt_cache_key"] == normal_body["prompt_cache_key"], + "prompt_cache_key_present": compact_body["prompt_cache_key"].is_string(), + "service_tier": compact_body.get("service_tier").and_then(Value::as_str), + }), + json!({ + "compact_body_without_input": canonical_expected_compact_body_without_input, + "expected_compact_body_without_input": canonical_expected_compact_body_without_input, + "prompt_cache_key_matches_responses": true, + "prompt_cache_key_present": true, + "service_tier": expected_service_tier, + }), + "compact requests should carry the same shared request fields as /responses" + ); + + insta::assert_snapshot!( + snapshot_name, + context_snapshot::format_request_body_diff_snapshot( + scenario, + "Last Normal /responses Request", + &normal_request, + "Remote /responses/compact Request", + &compact_request, + &ContextSnapshotOptions::default(), + ) + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn remote_manual_compact_api_auth_reuses_prompt_cache_key() -> Result<()> { + skip_if_no_network!(Ok(())); + + assert_remote_manual_compact_request_parity( + CodexAuth::from_api_key("dummy"), + Some(ServiceTier::Fast), + Some("priority"), + "remote_manual_compact_api_auth_prompt_cache_key_request_diff", + "After five varied API-key-auth turns, remote manual compaction reuses the normal responses service_tier and prompt_cache_key while omitting responses-only fields.", + ) + .await?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn remote_manual_compact_chatgpt_auth_reuses_service_tier_and_prompt_cache_key() -> Result<()> +{ + skip_if_no_network!(Ok(())); + + assert_remote_manual_compact_request_parity( + CodexAuth::create_dummy_chatgpt_auth_for_testing(), + Some(ServiceTier::Fast), + Some("priority"), + "remote_manual_compact_chatgpt_auth_service_tier_prompt_cache_key_request_diff", + "After five varied ChatGPT-auth turns, remote manual compaction reuses the normal responses service_tier and prompt_cache_key while omitting responses-only fields.", + ) + .await?; + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_compact_v2_reuses_context_compaction_for_followups() -> Result<()> { skip_if_no_network!(Ok(())); diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_api_auth_prompt_cache_key_request_diff.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_api_auth_prompt_cache_key_request_diff.snap new file mode 100644 index 0000000000..4527899106 --- /dev/null +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_api_auth_prompt_cache_key_request_diff.snap @@ -0,0 +1,43 @@ +--- +source: core/tests/suite/compact_remote.rs +expression: "context_snapshot::format_request_body_diff_snapshot(scenario,\n\"Last Normal /responses Request\", &normal_request,\n\"Remote /responses/compact Request\", &compact_request,\n&ContextSnapshotOptions::default(),)" +--- +Scenario: After five varied API-key-auth turns, remote manual compaction reuses the normal responses service_tier and prompt_cache_key while omitting responses-only fields. + +--- Last Normal /responses Request ++++ Remote /responses/compact Request +- "client_metadata": { +- "x-codex-installation-id": "" +- }, +- "include": [ +- "reasoning.encrypted_content" +- ], ++ }, ++ { ++ "content": [ ++ { ++ "text": "turn five raw content", ++ "type": "reasoning_text" ++ } ++ ], ++ "encrypted_content": "YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYnR1cm4gZml2ZSByYXcgY29udGVudA==", ++ "summary": [ ++ { ++ "text": "TURN_FIVE_REASONING", ++ "type": "summary_text" ++ } ++ ], ++ "type": "reasoning" ++ }, ++ { ++ "content": [ ++ { ++ "text": "TURN_FIVE_ASSISTANT", ++ "type": "output_text" ++ } ++ ], ++ "role": "assistant", ++ "type": "message" +- "store": false, +- "stream": true, +- "tool_choice": "auto", diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_chatgpt_auth_service_tier_prompt_cache_key_request_diff.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_chatgpt_auth_service_tier_prompt_cache_key_request_diff.snap new file mode 100644 index 0000000000..40baae2674 --- /dev/null +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_chatgpt_auth_service_tier_prompt_cache_key_request_diff.snap @@ -0,0 +1,43 @@ +--- +source: core/tests/suite/compact_remote.rs +expression: "context_snapshot::format_request_body_diff_snapshot(scenario,\n\"Last Normal /responses Request\", &normal_request,\n\"Remote /responses/compact Request\", &compact_request,\n&ContextSnapshotOptions::default(),)" +--- +Scenario: After five varied ChatGPT-auth turns, remote manual compaction reuses the normal responses service_tier and prompt_cache_key while omitting responses-only fields. + +--- Last Normal /responses Request ++++ Remote /responses/compact Request +- "client_metadata": { +- "x-codex-installation-id": "" +- }, +- "include": [ +- "reasoning.encrypted_content" +- ], ++ }, ++ { ++ "content": [ ++ { ++ "text": "turn five raw content", ++ "type": "reasoning_text" ++ } ++ ], ++ "encrypted_content": "YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYnR1cm4gZml2ZSByYXcgY29udGVudA==", ++ "summary": [ ++ { ++ "text": "TURN_FIVE_REASONING", ++ "type": "summary_text" ++ } ++ ], ++ "type": "reasoning" ++ }, ++ { ++ "content": [ ++ { ++ "text": "TURN_FIVE_ASSISTANT", ++ "type": "output_text" ++ } ++ ], ++ "role": "assistant", ++ "type": "message" +- "store": false, +- "stream": true, +- "tool_choice": "auto",