Files
codex/codex-rs/core/tests/common/context_snapshot.rs
Ahmed Ibrahim 5d6f23a27b 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 <noreply@openai.com>
2026-05-06 13:38:43 +03:00

712 lines
26 KiB
Rust

use regex_lite::Regex;
use serde_json::Value;
use similar::ChangeTag;
use similar::TextDiff;
use std::sync::OnceLock;
use crate::responses::ResponsesRequest;
use codex_protocol::protocol::APPS_INSTRUCTIONS_OPEN_TAG;
use codex_protocol::protocol::PLUGINS_INSTRUCTIONS_OPEN_TAG;
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ContextSnapshotRenderMode {
#[default]
RedactedText,
FullText,
KindOnly,
KindWithTextPrefix {
max_chars: usize,
},
}
#[derive(Debug, Clone)]
pub struct ContextSnapshotOptions {
render_mode: ContextSnapshotRenderMode,
strip_capability_instructions: bool,
strip_agents_md_user_context: bool,
}
impl Default for ContextSnapshotOptions {
fn default() -> Self {
Self {
render_mode: ContextSnapshotRenderMode::RedactedText,
strip_capability_instructions: false,
strip_agents_md_user_context: false,
}
}
}
impl ContextSnapshotOptions {
pub fn render_mode(mut self, render_mode: ContextSnapshotRenderMode) -> Self {
self.render_mode = render_mode;
self
}
pub fn strip_capability_instructions(mut self) -> Self {
self.strip_capability_instructions = true;
self
}
pub fn strip_agents_md_user_context(mut self) -> Self {
self.strip_agents_md_user_context = true;
self
}
}
pub fn format_request_input_snapshot(
request: &ResponsesRequest,
options: &ContextSnapshotOptions,
) -> String {
let items = request.input();
format_response_items_snapshot(items.as_slice(), options)
}
pub fn format_response_items_snapshot(items: &[Value], options: &ContextSnapshotOptions) -> String {
items
.iter()
.enumerate()
.map(|(idx, item)| {
let Some(item_type) = item.get("type").and_then(Value::as_str) else {
return format!("{idx:02}:<MISSING_TYPE>");
};
if options.render_mode == ContextSnapshotRenderMode::KindOnly {
return if item_type == "message" {
let role = item.get("role").and_then(Value::as_str).unwrap_or("unknown");
format!("{idx:02}:message/{role}")
} else {
format!("{idx:02}:{item_type}")
};
}
match item_type {
"message" => {
let role = item.get("role").and_then(Value::as_str).unwrap_or("unknown");
let rendered_parts = item
.get("content")
.and_then(Value::as_array)
.map(|content| {
content
.iter()
.filter_map(|entry| {
if let Some(text) = entry.get("text").and_then(Value::as_str) {
if options.strip_capability_instructions
&& role == "developer"
&& is_capability_instruction_text(text)
{
return None;
}
if options.strip_agents_md_user_context
&& role == "user"
&& text.starts_with("# AGENTS.md instructions for ")
{
return None;
}
return Some(format_snapshot_text(text, options));
}
let Some(content_type) =
entry.get("type").and_then(Value::as_str)
else {
return Some("<UNKNOWN_CONTENT_ITEM>".to_string());
};
let Some(content_object) = entry.as_object() else {
return Some(format!("<{content_type}>"));
};
let mut extra_keys = content_object
.keys()
.filter(|key| *key != "type" && *key != "text")
.cloned()
.collect::<Vec<String>>();
extra_keys.sort();
Some(if extra_keys.is_empty() {
format!("<{content_type}>")
} else {
format!("<{content_type}:{}>", extra_keys.join(","))
})
})
.collect::<Vec<String>>()
})
.unwrap_or_default();
let role = if rendered_parts.len() > 1 {
format!("{role}[{}]", rendered_parts.len())
} else {
role.to_string()
};
if rendered_parts.is_empty() {
return format!("{idx:02}:message/{role}:<NO_TEXT>");
}
if rendered_parts.len() == 1 {
return format!("{idx:02}:message/{role}:{}", rendered_parts[0]);
}
let parts = rendered_parts
.iter()
.enumerate()
.map(|(part_idx, part)| format!(" [{:02}] {part}", part_idx + 1))
.collect::<Vec<String>>()
.join("\n");
format!("{idx:02}:message/{role}:\n{parts}")
}
"function_call" => {
let name = item.get("name").and_then(Value::as_str).unwrap_or("unknown");
format!("{idx:02}:function_call/{name}")
}
"function_call_output" => {
let output = item
.get("output")
.and_then(Value::as_str)
.map(|output| format_snapshot_text(output, options))
.unwrap_or_else(|| "<NON_STRING_OUTPUT>".to_string());
format!("{idx:02}:function_call_output:{output}")
}
"local_shell_call" => {
let command = item
.get("action")
.and_then(|action| action.get("command"))
.and_then(Value::as_array)
.map(|parts| {
parts
.iter()
.filter_map(Value::as_str)
.collect::<Vec<&str>>()
.join(" ")
})
.map(|command| format_snapshot_text(&command, options))
.filter(|cmd| !cmd.is_empty())
.unwrap_or_else(|| "<NO_COMMAND>".to_string());
format!("{idx:02}:local_shell_call:{command}")
}
"reasoning" => {
let summary_text = item
.get("summary")
.and_then(Value::as_array)
.and_then(|summary| summary.first())
.and_then(|entry| entry.get("text"))
.and_then(Value::as_str)
.map(|text| format_snapshot_text(text, options))
.unwrap_or_else(|| "<NO_SUMMARY>".to_string());
let has_encrypted_content = item
.get("encrypted_content")
.and_then(Value::as_str)
.is_some_and(|value| !value.is_empty());
format!(
"{idx:02}:reasoning:summary={summary_text}:encrypted={has_encrypted_content}"
)
}
"compaction" => {
let has_encrypted_content = item
.get("encrypted_content")
.and_then(Value::as_str)
.is_some_and(|value| !value.is_empty());
format!("{idx:02}:compaction:encrypted={has_encrypted_content}")
}
other => format!("{idx:02}:{other}"),
}
})
.collect::<Vec<String>>()
.join("\n")
}
pub fn format_labeled_requests_snapshot(
scenario: &str,
sections: &[(&str, &ResponsesRequest)],
options: &ContextSnapshotOptions,
) -> String {
let sections = sections
.iter()
.map(|(title, request)| {
format!(
"## {title}\n{}",
format_request_input_snapshot(request, options)
)
})
.collect::<Vec<String>>()
.join("\n\n");
format!("Scenario: {scenario}\n\n{sections}")
}
pub fn format_labeled_items_snapshot(
scenario: &str,
sections: &[(&str, &[Value])],
options: &ContextSnapshotOptions,
) -> String {
let sections = sections
.iter()
.map(|(title, items)| {
format!(
"## {title}\n{}",
format_response_items_snapshot(items, options)
)
})
.collect::<Vec<String>>()
.join("\n\n");
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::<Vec<_>>();
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::<String>();
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 => {
normalize_snapshot_line_endings(&canonicalize_snapshot_text(text)).replace('\n', "\\n")
}
ContextSnapshotRenderMode::FullText => {
normalize_snapshot_line_endings(text).replace('\n', "\\n")
}
ContextSnapshotRenderMode::KindWithTextPrefix { max_chars } => {
let normalized = normalize_snapshot_line_endings(&canonicalize_snapshot_text(text))
.replace('\n', "\\n");
if normalized.chars().count() <= max_chars {
normalized
} else {
let prefix = normalized.chars().take(max_chars).collect::<String>();
format!("{prefix}...")
}
}
ContextSnapshotRenderMode::KindOnly => unreachable!(),
}
}
fn normalize_snapshot_line_endings(text: &str) -> String {
text.replace("\r\n", "\n").replace('\r', "\n")
}
fn canonicalize_snapshot_text(text: &str) -> String {
if text.starts_with("<permissions instructions>") {
return "<PERMISSIONS_INSTRUCTIONS>".to_string();
}
if text.starts_with(APPS_INSTRUCTIONS_OPEN_TAG) {
return "<APPS_INSTRUCTIONS>".to_string();
}
if text.starts_with(SKILLS_INSTRUCTIONS_OPEN_TAG) {
return "<SKILLS_INSTRUCTIONS>".to_string();
}
if text.starts_with(PLUGINS_INSTRUCTIONS_OPEN_TAG) {
return "<PLUGINS_INSTRUCTIONS>".to_string();
}
if text.starts_with("# AGENTS.md instructions for ") {
return "<AGENTS_MD>".to_string();
}
if text.starts_with("<environment_context>") {
let subagent_count = text
.split_once("<subagents>")
.and_then(|(_, rest)| rest.split_once("</subagents>"))
.map(|(subagents, _)| {
subagents
.lines()
.filter(|line| line.trim_start().starts_with("- "))
.count()
})
.unwrap_or(0);
let subagents_suffix = if subagent_count > 0 {
format!(":subagents={subagent_count}")
} else {
String::new()
};
if let (Some(cwd_start), Some(cwd_end)) = (text.find("<cwd>"), text.find("</cwd>")) {
let cwd = &text[cwd_start + "<cwd>".len()..cwd_end];
return if cwd.ends_with("PRETURN_CONTEXT_DIFF_CWD") {
format!("<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD{subagents_suffix}>")
} else {
format!("<ENVIRONMENT_CONTEXT:cwd=<CWD>{subagents_suffix}>")
};
}
return if subagent_count > 0 {
format!("<ENVIRONMENT_CONTEXT{subagents_suffix}>")
} else {
"<ENVIRONMENT_CONTEXT>".to_string()
};
}
if text.starts_with("You are performing a CONTEXT CHECKPOINT COMPACTION.") {
return "<SUMMARIZATION_PROMPT>".to_string();
}
if text.starts_with("Another language model started to solve this problem")
&& let Some((_, summary)) = text.split_once('\n')
{
return format!("<COMPACTION_SUMMARY>\n{summary}");
}
normalize_dynamic_snapshot_paths(text)
}
fn is_capability_instruction_text(text: &str) -> bool {
text.starts_with(APPS_INSTRUCTIONS_OPEN_TAG)
|| text.starts_with(SKILLS_INSTRUCTIONS_OPEN_TAG)
|| text.starts_with(PLUGINS_INSTRUCTIONS_OPEN_TAG)
}
fn normalize_dynamic_snapshot_paths(text: &str) -> String {
static SYSTEM_SKILL_PATH_RE: OnceLock<Regex> = OnceLock::new();
let system_skill_path_re = SYSTEM_SKILL_PATH_RE.get_or_init(|| {
Regex::new(r"/[^)\n]*/skills/\.system/([^/\n]+)/SKILL\.md")
.expect("system skill path regex should compile")
});
system_skill_path_re
.replace_all(text, "<SYSTEM_SKILLS_ROOT>/$1/SKILL.md")
.into_owned()
}
fn normalize_snapshot_uuids(text: &str) -> String {
static UUID_RE: OnceLock<Regex> = 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, "<UUID>").into_owned()
}
#[cfg(test)]
mod tests {
use super::ContextSnapshotOptions;
use super::ContextSnapshotRenderMode;
use super::format_response_items_snapshot;
use pretty_assertions::assert_eq;
use serde_json::json;
#[test]
fn full_text_mode_preserves_unredacted_text() {
let items = vec![json!({
"type": "message",
"role": "user",
"content": [{
"type": "input_text",
"text": "# AGENTS.md instructions for /tmp/example\n\n<INSTRUCTIONS>\nbody\n</INSTRUCTIONS>"
}]
})];
let rendered = format_response_items_snapshot(
&items,
&ContextSnapshotOptions::default().render_mode(ContextSnapshotRenderMode::FullText),
);
assert_eq!(
rendered,
r"00:message/user:# AGENTS.md instructions for /tmp/example\n\n<INSTRUCTIONS>\nbody\n</INSTRUCTIONS>"
);
}
#[test]
fn full_text_mode_normalizes_crlf_line_endings() {
let items = vec![json!({
"type": "message",
"role": "user",
"content": [{
"type": "input_text",
"text": "line one\r\n\r\nline two"
}]
})];
let rendered = format_response_items_snapshot(
&items,
&ContextSnapshotOptions::default().render_mode(ContextSnapshotRenderMode::FullText),
);
assert_eq!(rendered, r"00:message/user:line one\n\nline two");
}
#[test]
fn redacted_text_mode_keeps_canonical_placeholders() {
let items = vec![json!({
"type": "message",
"role": "user",
"content": [{
"type": "input_text",
"text": "# AGENTS.md instructions for /tmp/example\n\n<INSTRUCTIONS>\nbody\n</INSTRUCTIONS>"
}]
})];
let rendered = format_response_items_snapshot(
&items,
&ContextSnapshotOptions::default().render_mode(ContextSnapshotRenderMode::RedactedText),
);
assert_eq!(rendered, "00:message/user:<AGENTS_MD>");
}
#[test]
fn redacted_text_mode_keeps_capability_instruction_placeholders() {
let items = vec![json!({
"type": "message",
"role": "developer",
"content": [
{
"type": "input_text",
"text": "<apps_instructions>\n## Apps\nbody\n</apps_instructions>"
},
{
"type": "input_text",
"text": "<skills_instructions>\n## Skills\nbody\n</skills_instructions>"
},
{
"type": "input_text",
"text": "<plugins_instructions>\n## Plugins\nbody\n</plugins_instructions>"
}
]
})];
let rendered = format_response_items_snapshot(
&items,
&ContextSnapshotOptions::default().render_mode(ContextSnapshotRenderMode::RedactedText),
);
assert_eq!(
rendered,
"00:message/developer[3]:\n [01] <APPS_INSTRUCTIONS>\n [02] <SKILLS_INSTRUCTIONS>\n [03] <PLUGINS_INSTRUCTIONS>"
);
}
#[test]
fn strip_capability_instructions_omits_capability_parts_from_developer_messages() {
let items = vec![json!({
"type": "message",
"role": "developer",
"content": [
{ "type": "input_text", "text": "<permissions instructions>\n...</permissions instructions>" },
{ "type": "input_text", "text": "<skills_instructions>\n## Skills\n...</skills_instructions>" },
{ "type": "input_text", "text": "<plugins_instructions>\n## Plugins\n...</plugins_instructions>" }
]
})];
let rendered = format_response_items_snapshot(
&items,
&ContextSnapshotOptions::default()
.render_mode(ContextSnapshotRenderMode::RedactedText)
.strip_capability_instructions(),
);
assert_eq!(rendered, "00:message/developer:<PERMISSIONS_INSTRUCTIONS>");
}
#[test]
fn strip_agents_md_user_context_omits_agents_fragment_from_user_messages() {
let items = vec![json!({
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "# AGENTS.md instructions for /tmp/example\n\n<INSTRUCTIONS>\n- test\n</INSTRUCTIONS>"
},
{
"type": "input_text",
"text": "<environment_context>\n <cwd>/tmp/example</cwd>\n</environment_context>"
}
]
})];
let rendered = format_response_items_snapshot(
&items,
&ContextSnapshotOptions::default()
.render_mode(ContextSnapshotRenderMode::RedactedText)
.strip_agents_md_user_context(),
);
assert_eq!(rendered, "00:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>");
}
#[test]
fn redacted_text_mode_normalizes_environment_context_with_subagents() {
let items = vec![json!({
"type": "message",
"role": "user",
"content": [{
"type": "input_text",
"text": "<environment_context>\n <cwd>/tmp/example</cwd>\n <shell>bash</shell>\n <subagents>\n - agent-1: atlas\n - agent-2\n </subagents>\n</environment_context>"
}]
})];
let rendered = format_response_items_snapshot(
&items,
&ContextSnapshotOptions::default().render_mode(ContextSnapshotRenderMode::RedactedText),
);
assert_eq!(
rendered,
"00:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>:subagents=2>"
);
}
#[test]
fn kind_with_text_prefix_mode_normalizes_crlf_line_endings() {
let items = vec![json!({
"type": "message",
"role": "developer",
"content": [{
"type": "input_text",
"text": "<realtime_conversation>\r\nRealtime conversation started.\r\n\r\nYou are..."
}]
})];
let rendered = format_response_items_snapshot(
&items,
&ContextSnapshotOptions::default()
.render_mode(ContextSnapshotRenderMode::KindWithTextPrefix { max_chars: 64 }),
);
assert_eq!(
rendered,
r"00:message/developer:<realtime_conversation>\nRealtime conversation started.\n\nYou a..."
);
}
#[test]
fn image_only_message_is_rendered_as_non_text_span() {
let items = vec![json!({
"type": "message",
"role": "user",
"content": [{
"type": "input_image",
"image_url": "data:image/png;base64,AAAA"
}]
})];
let rendered = format_response_items_snapshot(&items, &ContextSnapshotOptions::default());
assert_eq!(rendered, "00:message/user:<input_image:image_url>");
}
#[test]
fn mixed_text_and_image_message_keeps_image_span() {
let items = vec![json!({
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "<image>"
},
{
"type": "input_image",
"image_url": "data:image/png;base64,AAAA"
},
{
"type": "input_text",
"text": "</image>"
}
]
})];
let rendered = format_response_items_snapshot(&items, &ContextSnapshotOptions::default());
assert_eq!(
rendered,
"00:message/user[3]:\n [01] <image>\n [02] <input_image:image_url>\n [03] </image>"
);
}
#[test]
fn redacted_text_mode_normalizes_system_skill_temp_paths() {
let items = vec![json!({
"type": "message",
"role": "developer",
"content": [{
"type": "input_text",
"text": "## Skills\n- openai-docs: helper (file: /private/var/folders/yk/p4jp9nzs79s5q84csslkgqtm0000gn/T/.tmpAnGVww/skills/.system/openai-docs/SKILL.md)"
}]
})];
let rendered = format_response_items_snapshot(&items, &ContextSnapshotOptions::default());
assert_eq!(
rendered,
"00:message/developer:## Skills\\n- openai-docs: helper (file: <SYSTEM_SKILLS_ROOT>/openai-docs/SKILL.md)"
);
}
}