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.
This commit is contained in:
sayan-oai
2026-05-19 23:47:48 -07:00
committed by GitHub
Unverified
parent cfa16fcc2e
commit 34aad43684
12 changed files with 291 additions and 14 deletions
@@ -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"
}
]
},
@@ -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"
}
]
},
@@ -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"
}
]
},
@@ -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"
}
]
},
@@ -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"
}
]
},
@@ -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, };
+41 -10
View File
@@ -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",
@@ -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 {
+61 -1
View File
@@ -1314,6 +1314,9 @@ pub enum FunctionCallOutputContentItem {
#[ts(optional)]
detail: Option<ImageDetail>,
},
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::<Vec<_>>();
@@ -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"}"#;
+2 -1
View File
@@ -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::<Vec<_>>()
.join("\n"),
+12 -1
View File
@@ -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::<Vec<_>>();
@@ -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(),
});
}
}
}
@@ -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![