Add opt-in output token replay for OpenAI requests (#52742)

## What changed

- Add the disabled-by-default `output_token_replay` feature to request `output.encrypted_content` from OpenAI providers.
- Preserve encrypted message and tool-call output, message and function-call status, and output-text `annotations` and `logprobs` through history and replay. Update the generated protocol schemas and SDK types.
- Strip output ciphertext from requests to other providers while retaining it in stored history, including after replay is disabled. Remove encrypted content from memory extraction inputs.

## Testing

Add integration coverage for the feature/provider combinations, metadata replay across follow-up requests, and history preservation when resuming with replay disabled.

GitOrigin-RevId: 77729a021b6771933402aebe74ece8eb39c057e2
This commit is contained in:
pakrym-oai
2026-10-10 01:25:00 +00:00
committed by copyberry
parent e4490eff65
commit 5ef96ab278
119 changed files with 1602 additions and 91 deletions
+2
View File
@@ -687,6 +687,8 @@ pub fn build_hook_prompt_message(fragments: &[HookPromptFragment]) -> Option<Res
}
Some(ResponseItem::Message {
status: None,
encrypted_content: None,
id: Some(ResponseItemId::new("msg")),
role: "user".to_string(),
content,
+100
View File
@@ -34,6 +34,7 @@ use codex_utils_path_uri::PathUri;
mod configuration_update;
mod executed_tool_calls;
mod item_metadata;
pub use crate::local_media::MAX_PROMPT_AUDIO_INPUT_BYTES;
@@ -892,6 +893,17 @@ pub enum ContentItem {
},
OutputText {
text: String,
// Preserve provider metadata, including empty arrays, for sampled-output replay.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
#[ts(type = "unknown[]")]
#[debug(skip)]
annotations: Option<Vec<serde_json::Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
#[ts(type = "unknown[]")]
#[debug(skip)]
logprobs: Option<Vec<serde_json::Value>>,
},
}
@@ -1022,6 +1034,12 @@ pub enum ResponseItem {
tools: Vec<serde_json::Value>,
},
Message {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
encrypted_content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
status: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
id: Option<ResponseItemId>,
@@ -1062,6 +1080,9 @@ pub enum ResponseItem {
internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough>,
},
LocalShellCall {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
encrypted_content: Option<String>,
/// Legacy id field retained for compatibility with older payloads.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
@@ -1075,6 +1096,12 @@ pub enum ResponseItem {
internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough>,
},
FunctionCall {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
encrypted_content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
status: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
id: Option<ResponseItemId>,
@@ -1095,6 +1122,9 @@ pub enum ResponseItem {
internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough>,
},
ToolSearchCall {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
encrypted_content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
id: Option<ResponseItemId>,
@@ -1135,6 +1165,9 @@ pub enum ResponseItem {
internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough>,
},
CustomToolCall {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
encrypted_content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
id: Option<ResponseItemId>,
@@ -1192,6 +1225,9 @@ pub enum ResponseItem {
// "action": {"type":"search","query":"weather: San Francisco, CA"}
// }
WebSearchCall {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
encrypted_content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
id: Option<ResponseItemId>,
@@ -1215,6 +1251,9 @@ pub enum ResponseItem {
// "result":"..."
// }
ImageGenerationCall {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
encrypted_content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
id: Option<ResponseItemId>,
@@ -1259,6 +1298,50 @@ pub enum ResponseItem {
}
impl ResponseItem {
/// Clears the top-level `encrypted_content` fields.
pub fn clear_encrypted_content(&mut self) {
match self {
Self::Message {
encrypted_content, ..
}
| Self::Reasoning {
encrypted_content, ..
}
| Self::LocalShellCall {
encrypted_content, ..
}
| Self::FunctionCall {
encrypted_content, ..
}
| Self::ToolSearchCall {
encrypted_content, ..
}
| Self::CustomToolCall {
encrypted_content, ..
}
| Self::WebSearchCall {
encrypted_content, ..
}
| Self::ImageGenerationCall {
encrypted_content, ..
}
| Self::ContextCompaction {
encrypted_content, ..
} => *encrypted_content = None,
Self::Compaction {
encrypted_content, ..
} => encrypted_content.clear(),
Self::AdditionalTools { .. }
| Self::AgentMessage { .. }
| Self::FunctionCallOutput { .. }
| Self::CustomToolCallOutput { .. }
| Self::ToolSearchOutput { .. }
| Self::ConfigurationUpdate { .. }
| Self::CompactionTrigger { .. }
| Self::Other => {}
}
}
/// Returns the wire type name for this response item.
pub fn item_type(&self) -> &'static str {
match self {
@@ -1900,6 +1983,8 @@ impl From<ResponseInputItem> for ResponseItem {
content,
phase,
} => Self::Message {
status: None,
encrypted_content: None,
role,
content,
id: None,
@@ -2555,6 +2640,8 @@ mod tests {
let input = ResponseInputItem::Message {
role: "assistant".to_string(),
content: vec![ContentItem::OutputText {
annotations: None,
logprobs: None,
text: "answer text".to_string(),
}],
phase: phase.clone(),
@@ -2562,9 +2649,13 @@ mod tests {
let wire = serde_json::to_value(input)?;
let item = ResponseItem::from(serde_json::from_value::<ResponseInputItem>(wire)?);
let expected = ResponseItem::Message {
status: None,
encrypted_content: None,
id: None,
role: "assistant".to_string(),
content: vec![ContentItem::OutputText {
annotations: None,
logprobs: None,
text: "answer text".to_string(),
}],
phase,
@@ -2678,6 +2769,8 @@ mod tests {
internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough>,
) -> ResponseItem {
ResponseItem::Message {
status: None,
encrypted_content: None,
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
@@ -2796,6 +2889,7 @@ mod tests {
assert_eq!(
item,
ResponseItem::ImageGenerationCall {
encrypted_content: None,
id: Some(ResponseItemId::with_suffix("ig", "123")),
status: "completed".to_string(),
revised_prompt: Some("A small blue square".to_string()),
@@ -2818,6 +2912,7 @@ mod tests {
assert_eq!(
item,
ResponseItem::ImageGenerationCall {
encrypted_content: None,
id: Some(ResponseItemId::with_suffix("ig", "123")),
status: "completed".to_string(),
revised_prompt: None,
@@ -3244,6 +3339,8 @@ mod tests {
assert_eq!(
item,
ResponseItem::FunctionCall {
status: None,
encrypted_content: None,
id: None,
name: "mcp__codex_apps__gmail_get_recent_emails".to_string(),
namespace: Some("mcp__codex_apps__gmail".to_string()),
@@ -3917,6 +4014,7 @@ mod tests {
for (json_literal, expected_id, expected_action, expected_status) in cases {
let parsed: ResponseItem = serde_json::from_str(json_literal)?;
let expected = ResponseItem::WebSearchCall {
encrypted_content: None,
id: expected_id.clone(),
status: expected_status.clone(),
action: expected_action.clone(),
@@ -4159,6 +4257,7 @@ mod tests {
assert_eq!(
parsed,
ResponseItem::ToolSearchCall {
encrypted_content: None,
id: None,
call_id: Some("search-1".to_string()),
status: None,
@@ -4276,6 +4375,7 @@ mod tests {
assert_eq!(
parsed_call,
ResponseItem::ToolSearchCall {
encrypted_content: None,
id: None,
call_id: None,
status: Some("completed".to_string()),
@@ -59,6 +59,8 @@ fn malformed_content_kinds_do_not_prevent_loading_response_item() -> Result<()>
fn response_item(metadata: InternalChatMessageMetadataPassthrough) -> ResponseItem {
ResponseItem::Message {
status: None,
encrypted_content: None,
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
+5 -1
View File
@@ -976,6 +976,8 @@ impl InterAgentCommunication {
ResponseInputItem::Message {
role: "assistant".to_string(),
content: vec![ContentItem::OutputText {
annotations: None,
logprobs: None,
text: serde_json::to_string(&communication).unwrap_or_default(),
}],
phase: Some(MessagePhase::Commentary),
@@ -1023,7 +1025,7 @@ impl InterAgentCommunication {
pub fn from_message_content(content: &[ContentItem]) -> Option<Self> {
match content {
[ContentItem::InputText { text }] | [ContentItem::OutputText { text }] => {
[ContentItem::InputText { text }] | [ContentItem::OutputText { text, .. }] => {
serde_json::from_str(text).ok()
}
_ => None,
@@ -4832,6 +4834,8 @@ mod tests {
ResponseInputItem::Message {
role: "assistant".to_string(),
content: vec![ContentItem::OutputText {
annotations: None,
logprobs: None,
text: serde_json::to_string(&serialized_communication)
.expect("serialize communication"),
}],