From 34aad436849963399e903d72bbf140935e9fc66f Mon Sep 17 00:00:00 2001 From: sayan-oai Date: Tue, 19 May 2026 23:47:48 -0700 Subject: [PATCH] add encryptedcontent to functioncalloutput (#23500) add new `EncryptedContent` variant to `FunctionCallOutputContentItem` ahead of standalone websearch. we need to be able to receive and pass encrypted function call output from the new web search endpoint back to responsesapi, as we cannot expose direct search results. --- .../schema/json/ClientRequest.json | 20 ++++++ .../codex_app_server_protocol.schemas.json | 20 ++++++ .../codex_app_server_protocol.v2.schemas.json | 20 ++++++ .../RawResponseItemCompletedNotification.json | 20 ++++++ .../schema/json/v2/ThreadResumeParams.json | 20 ++++++ .../FunctionCallOutputContentItem.ts | 2 +- codex-rs/core/src/context_manager/history.rs | 51 ++++++++++++--- .../core/src/context_manager/history_tests.rs | 20 ++++++ codex-rs/protocol/src/models.rs | 62 ++++++++++++++++++- codex-rs/tools/src/tool_output.rs | 3 +- codex-rs/utils/output-truncation/src/lib.rs | 13 +++- .../output-truncation/src/truncate_tests.rs | 54 ++++++++++++++++ 12 files changed, 291 insertions(+), 14 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index ee68faf00..788e52e82 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -984,6 +984,26 @@ ], "title": "InputImageFunctionCallOutputContentItem", "type": "object" + }, + { + "properties": { + "encrypted_content": { + "type": "string" + }, + "type": { + "enum": [ + "encrypted_content" + ], + "title": "EncryptedContentFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "encrypted_content", + "type" + ], + "title": "EncryptedContentFunctionCallOutputContentItem", + "type": "object" } ] }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 3db4a87ef..f6e5a46c9 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -9242,6 +9242,26 @@ ], "title": "InputImageFunctionCallOutputContentItem", "type": "object" + }, + { + "properties": { + "encrypted_content": { + "type": "string" + }, + "type": { + "enum": [ + "encrypted_content" + ], + "title": "EncryptedContentFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "encrypted_content", + "type" + ], + "title": "EncryptedContentFunctionCallOutputContentItem", + "type": "object" } ] }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 7b62d6d4b..c4e93338a 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -5631,6 +5631,26 @@ ], "title": "InputImageFunctionCallOutputContentItem", "type": "object" + }, + { + "properties": { + "encrypted_content": { + "type": "string" + }, + "type": { + "enum": [ + "encrypted_content" + ], + "title": "EncryptedContentFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "encrypted_content", + "type" + ], + "title": "EncryptedContentFunctionCallOutputContentItem", + "type": "object" } ] }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json index 74420ea57..e69b34228 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json @@ -140,6 +140,26 @@ ], "title": "InputImageFunctionCallOutputContentItem", "type": "object" + }, + { + "properties": { + "encrypted_content": { + "type": "string" + }, + "type": { + "enum": [ + "encrypted_content" + ], + "title": "EncryptedContentFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "encrypted_content", + "type" + ], + "title": "EncryptedContentFunctionCallOutputContentItem", + "type": "object" } ] }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json index 027989479..d4bdeda0e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -199,6 +199,26 @@ ], "title": "InputImageFunctionCallOutputContentItem", "type": "object" + }, + { + "properties": { + "encrypted_content": { + "type": "string" + }, + "type": { + "enum": [ + "encrypted_content" + ], + "title": "EncryptedContentFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "encrypted_content", + "type" + ], + "title": "EncryptedContentFunctionCallOutputContentItem", + "type": "object" } ] }, diff --git a/codex-rs/app-server-protocol/schema/typescript/FunctionCallOutputContentItem.ts b/codex-rs/app-server-protocol/schema/typescript/FunctionCallOutputContentItem.ts index fb2996f1e..cd1890814 100644 --- a/codex-rs/app-server-protocol/schema/typescript/FunctionCallOutputContentItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/FunctionCallOutputContentItem.ts @@ -7,4 +7,4 @@ import type { ImageDetail } from "./ImageDetail"; * Responses API compatible content items that can be returned by a tool call. * This is a subset of ContentItem with the types we support as function call outputs. */ -export type FunctionCallOutputContentItem = { "type": "input_text", text: string, } | { "type": "input_image", image_url: string, detail?: ImageDetail, }; +export type FunctionCallOutputContentItem = { "type": "input_text", text: string, } | { "type": "input_image", image_url: string, detail?: ImageDetail, } | { "type": "encrypted_content", encrypted_content: string, }; diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index cfd929738..6a0553548 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -504,6 +504,10 @@ fn estimate_reasoning_length(encoded_len: usize) -> usize { .saturating_sub(650) } +fn estimate_encrypted_function_output_length(encoded_len: usize) -> usize { + encoded_len.saturating_mul(9).div_ceil(16) +} + fn estimate_item_token_count(item: &ResponseItem) -> i64 { let model_visible_bytes = estimate_response_item_model_visible_bytes(item); approx_tokens_from_byte_count_i64(model_visible_bytes) @@ -547,16 +551,18 @@ pub(crate) fn estimate_response_item_model_visible_bytes(item: &ResponseItem) -> let raw = serde_json::to_string(item) .map(|serialized| i64::try_from(serialized.len()).unwrap_or(i64::MAX)) .unwrap_or_default(); - let (payload_bytes, replacement_bytes) = image_data_url_estimate_adjustment(item); - if payload_bytes == 0 || replacement_bytes == 0 { - raw - } else { - // Replace raw base64 payload bytes with a per-image estimate. - // We intentionally preserve the data URL prefix and JSON - // wrapper bytes already included in `raw`. - raw.saturating_sub(payload_bytes) - .saturating_add(replacement_bytes) - } + let (image_payload_bytes, image_replacement_bytes) = + image_data_url_estimate_adjustment(item); + let (encrypted_payload_bytes, encrypted_replacement_bytes) = + encrypted_function_output_estimate_adjustment(item); + // Replace raw base64 payload bytes with a per-image estimate. + // We intentionally preserve the data URL prefix and JSON + // wrapper bytes already included in `raw`. + let raw = raw + .saturating_sub(image_payload_bytes) + .saturating_add(image_replacement_bytes); + raw.saturating_sub(encrypted_payload_bytes) + .saturating_add(encrypted_replacement_bytes) } } } @@ -678,6 +684,31 @@ fn image_data_url_estimate_adjustment(item: &ResponseItem) -> (i64, i64) { (payload_bytes, replacement_bytes) } +fn encrypted_function_output_estimate_adjustment(item: &ResponseItem) -> (i64, i64) { + let ResponseItem::FunctionCallOutput { output, .. } = item else { + return (0, 0); + }; + let FunctionCallOutputBody::ContentItems(items) = &output.body else { + return (0, 0); + }; + + items.iter().fold((0i64, 0i64), |acc, item| { + let FunctionCallOutputContentItem::EncryptedContent { encrypted_content } = item else { + return acc; + }; + let payload_bytes = acc + .0 + .saturating_add(i64::try_from(encrypted_content.len()).unwrap_or(i64::MAX)); + let replacement_bytes = acc.1.saturating_add( + i64::try_from(estimate_encrypted_function_output_length( + encrypted_content.len(), + )) + .unwrap_or(i64::MAX), + ); + (payload_bytes, replacement_bytes) + }) +} + fn is_model_generated_item(item: &ResponseItem) -> bool { match item { ResponseItem::Message { role, .. } => role == "assistant", diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs index 8ba5d56e3..f63ae6ec1 100644 --- a/codex-rs/core/src/context_manager/history_tests.rs +++ b/codex-rs/core/src/context_manager/history_tests.rs @@ -1754,6 +1754,26 @@ fn non_base64_image_urls_are_unchanged() { ); } +#[test] +fn encrypted_function_output_uses_plaintext_byte_estimate() { + let encrypted_content = "A".repeat(1_868); + let item = ResponseItem::FunctionCallOutput { + call_id: "call-encrypted".to_string(), + output: FunctionCallOutputPayload::from_content_items(vec![ + FunctionCallOutputContentItem::EncryptedContent { + encrypted_content: encrypted_content.clone(), + }, + ]), + }; + + let raw_len = serde_json::to_string(&item).unwrap().len() as i64; + let estimated = estimate_response_item_model_visible_bytes(&item); + let expected = raw_len - encrypted_content.len() as i64 + + estimate_encrypted_function_output_length(encrypted_content.len()) as i64; + + assert_eq!(estimated, expected); +} + #[test] fn data_url_without_base64_marker_is_unchanged() { let item = ResponseItem::Message { diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index d1d2abe75..2927cb1cd 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -1314,6 +1314,9 @@ pub enum FunctionCallOutputContentItem { #[ts(optional)] detail: Option, }, + EncryptedContent { + encrypted_content: String, + }, } /// Converts structured function-call output content into plain text for @@ -1337,7 +1340,8 @@ pub fn function_call_output_content_items_to_text( Some(text.as_str()) } FunctionCallOutputContentItem::InputText { .. } - | FunctionCallOutputContentItem::InputImage { .. } => None, + | FunctionCallOutputContentItem::InputImage { .. } + | FunctionCallOutputContentItem::EncryptedContent { .. } => None, }) .collect::>(); @@ -2063,6 +2067,9 @@ mod tests { image_url: "data:image/png;base64,AAA".to_string(), detail: Some(DEFAULT_IMAGE_DETAIL), }, + FunctionCallOutputContentItem::EncryptedContent { + encrypted_content: "enc_opaque".to_string(), + }, ]; let text = function_call_output_content_items_to_text(&content_items); @@ -2270,6 +2277,35 @@ mod tests { Ok(()) } + #[test] + fn serializes_encrypted_function_output_content_as_array() -> Result<()> { + let item = ResponseInputItem::FunctionCallOutput { + call_id: "call1".into(), + output: FunctionCallOutputPayload::from_content_items(vec![ + FunctionCallOutputContentItem::EncryptedContent { + encrypted_content: "enc_opaque".into(), + }, + ]), + }; + + let json = serde_json::to_value(&item)?; + assert_eq!( + json, + serde_json::json!({ + "type": "function_call_output", + "call_id": "call1", + "output": [ + { + "type": "encrypted_content", + "encrypted_content": "enc_opaque", + } + ], + }) + ); + + Ok(()) + } + #[test] fn preserves_existing_image_data_urls() -> Result<()> { let call_tool_result = CallToolResult { @@ -2394,6 +2430,30 @@ mod tests { Ok(()) } + #[test] + fn deserializes_encrypted_array_payload_into_items() -> Result<()> { + let json = r#"[ + {"type": "encrypted_content", "encrypted_content": "enc_opaque"} + ]"#; + + let payload: FunctionCallOutputPayload = serde_json::from_str(json)?; + let expected_items = vec![FunctionCallOutputContentItem::EncryptedContent { + encrypted_content: "enc_opaque".into(), + }]; + + assert_eq!(payload.success, None); + assert_eq!( + payload.body, + FunctionCallOutputBody::ContentItems(expected_items.clone()) + ); + assert_eq!( + serde_json::to_string(&payload)?, + serde_json::to_string(&expected_items)? + ); + + Ok(()) + } + #[test] fn deserializes_compaction_alias() -> Result<()> { let json = r#"{"type":"compaction_summary","encrypted_content":"abc"}"#; diff --git a/codex-rs/tools/src/tool_output.rs b/codex-rs/tools/src/tool_output.rs index 64a8164da..b61bb80de 100644 --- a/codex-rs/tools/src/tool_output.rs +++ b/codex-rs/tools/src/tool_output.rs @@ -209,7 +209,8 @@ fn content_items_to_code_mode_result(items: &[FunctionCallOutputContentItem]) -> Some(image_url.clone()) } FunctionCallOutputContentItem::InputText { .. } - | FunctionCallOutputContentItem::InputImage { .. } => None, + | FunctionCallOutputContentItem::InputImage { .. } + | FunctionCallOutputContentItem::EncryptedContent { .. } => None, }) .collect::>() .join("\n"), diff --git a/codex-rs/utils/output-truncation/src/lib.rs b/codex-rs/utils/output-truncation/src/lib.rs index 24b1630da..52cd741b7 100644 --- a/codex-rs/utils/output-truncation/src/lib.rs +++ b/codex-rs/utils/output-truncation/src/lib.rs @@ -34,7 +34,8 @@ pub fn formatted_truncate_text_content_items_with_policy( .iter() .filter_map(|item| match item { FunctionCallOutputContentItem::InputText { text } => Some(text.as_str()), - FunctionCallOutputContentItem::InputImage { .. } => None, + FunctionCallOutputContentItem::InputImage { .. } + | FunctionCallOutputContentItem::EncryptedContent { .. } => None, }) .collect::>(); @@ -64,6 +65,11 @@ pub fn formatted_truncate_text_content_items_with_policy( detail: *detail, }) } + FunctionCallOutputContentItem::EncryptedContent { encrypted_content } => { + Some(FunctionCallOutputContentItem::EncryptedContent { + encrypted_content: encrypted_content.clone(), + }) + } FunctionCallOutputContentItem::InputText { .. } => None, })); @@ -117,6 +123,11 @@ pub fn truncate_function_output_items_with_policy( detail: *detail, }); } + FunctionCallOutputContentItem::EncryptedContent { encrypted_content } => { + out.push(FunctionCallOutputContentItem::EncryptedContent { + encrypted_content: encrypted_content.clone(), + }); + } } } diff --git a/codex-rs/utils/output-truncation/src/truncate_tests.rs b/codex-rs/utils/output-truncation/src/truncate_tests.rs index 74acb15ca..baf26a058 100644 --- a/codex-rs/utils/output-truncation/src/truncate_tests.rs +++ b/codex-rs/utils/output-truncation/src/truncate_tests.rs @@ -251,6 +251,60 @@ fn formatted_truncate_text_content_items_with_policy_merges_text_and_appends_ima assert_eq!(original_token_count, Some(4)); } +#[test] +fn formatted_truncate_text_content_items_with_policy_preserves_encrypted_content() { + let items = vec![ + FunctionCallOutputContentItem::InputText { + text: "abcdefgh".to_string(), + }, + FunctionCallOutputContentItem::EncryptedContent { + encrypted_content: "enc_opaque".to_string(), + }, + ]; + + let (output, original_token_count) = + formatted_truncate_text_content_items_with_policy(&items, TruncationPolicy::Bytes(2)); + + assert_eq!( + output, + vec![ + FunctionCallOutputContentItem::InputText { + text: "Total output lines: 1\n\na…6 chars truncated…h".to_string(), + }, + FunctionCallOutputContentItem::EncryptedContent { + encrypted_content: "enc_opaque".to_string(), + }, + ] + ); + assert_eq!(original_token_count, Some(2)); +} + +#[test] +fn truncate_function_output_items_with_policy_preserves_encrypted_content() { + let items = vec![ + FunctionCallOutputContentItem::InputText { + text: "abcdefgh".to_string(), + }, + FunctionCallOutputContentItem::EncryptedContent { + encrypted_content: "enc_opaque".to_string(), + }, + ]; + + let output = truncate_function_output_items_with_policy(&items, TruncationPolicy::Bytes(2)); + + assert_eq!( + output, + vec![ + FunctionCallOutputContentItem::InputText { + text: "a…6 chars truncated…h".to_string(), + }, + FunctionCallOutputContentItem::EncryptedContent { + encrypted_content: "enc_opaque".to_string(), + }, + ] + ); +} + #[test] fn formatted_truncate_text_content_items_with_policy_merges_all_text_for_token_budget() { let items = vec![