Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions codex-rs/app-server-protocol/schema/json/ClientRequest.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 41 additions & 10 deletions codex-rs/core/src/context_manager/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -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",
Expand Down
20 changes: 20 additions & 0 deletions codex-rs/core/src/context_manager/history_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
62 changes: 61 additions & 1 deletion codex-rs/protocol/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1314,6 +1314,9 @@ pub enum FunctionCallOutputContentItem {
#[ts(optional)]
detail: Option<ImageDetail>,
},
EncryptedContent {
encrypted_content: String,
},
Comment thread
sayan-oai marked this conversation as resolved.
}

/// Converts structured function-call output content into plain text for
Expand All @@ -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<_>>();

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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"}"#;
Expand Down
3 changes: 2 additions & 1 deletion codex-rs/tools/src/tool_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
13 changes: 12 additions & 1 deletion codex-rs/utils/output-truncation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<_>>();

Expand Down Expand Up @@ -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,
}));

Expand Down Expand Up @@ -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(),
});
Comment on lines +126 to +129
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Badge Bound encrypted function output before replaying it

When a tool returns a large encrypted_content item, this new arm copies the whole blob even though truncate_function_output_items_with_policy is the path used by core/src/context_manager/history.rs to trim function-call outputs before they are kept/replayed in model context. Text output consumes remaining_budget, but encrypted content is now an unbounded model-context item; standalone web search payloads can easily exceed the >1k-token manual-review threshold from .codex/skills/code-review-context/SKILL.md and can push requests over context/API limits. Please apply a hard cap or route these opaque blobs through a bounded/non-context path instead of preserving them unconditionally.

Useful? React with 👍 / 👎.

}
}
}

Expand Down
Loading
Loading