mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"),
|
||||
}],
|
||||
|
||||
Reference in New Issue
Block a user