mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
Account for JSON overhead when truncating MCP tool results (#50470)
## Why Truncating the preview alone can leave the serialized MCP tool result above its byte budget because JSON escaping and the result wrapper add bytes. ## What changed Measure the complete serialized replacement and progressively reduce the preview budget until the result fits or the preview is empty. ## Testing Strengthen the regression test with quotes, backslashes, and newlines. Assert that the serialized result fits within the 1,024-byte budget and that truncating it again returns the existing result unchanged. GitOrigin-RevId: ebd97b1a6ca71343941a96e472cb313028ac680c
This commit is contained in:
@@ -40,8 +40,8 @@ pub fn truncate_text(content: &str, policy: TruncationPolicy) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces an oversized serialized MCP result with a text preview while preserving its error
|
||||
/// status and supported bounded metadata.
|
||||
/// Replaces an oversized serialized MCP result with a bounded text preview while preserving its
|
||||
/// error status and supported bounded metadata.
|
||||
pub fn truncate_mcp_tool_result(
|
||||
result: &CallToolResult,
|
||||
max_bytes: usize,
|
||||
@@ -53,15 +53,37 @@ pub fn truncate_mcp_tool_result(
|
||||
return Cow::Borrowed(result);
|
||||
}
|
||||
|
||||
Cow::Owned(CallToolResult {
|
||||
content: vec![serde_json::json!({
|
||||
"type": "text",
|
||||
"text": truncate_text(&serialized, TruncationPolicy::Bytes(max_bytes)),
|
||||
})],
|
||||
structured_content: None,
|
||||
is_error: result.is_error,
|
||||
meta: None,
|
||||
})
|
||||
let meta = None;
|
||||
|
||||
let mut preview_budget = max_bytes;
|
||||
loop {
|
||||
let preview = if preview_budget == 0 {
|
||||
String::new()
|
||||
} else {
|
||||
truncate_text(&serialized, TruncationPolicy::Bytes(preview_budget))
|
||||
};
|
||||
let truncated = CallToolResult {
|
||||
content: vec![serde_json::json!({
|
||||
"type": "text",
|
||||
"text": preview,
|
||||
})],
|
||||
structured_content: None,
|
||||
is_error: result.is_error,
|
||||
meta: meta.clone(),
|
||||
};
|
||||
let Ok(truncated_serialized) = serde_json::to_string(&truncated) else {
|
||||
return Cow::Borrowed(result);
|
||||
};
|
||||
let truncated_len = truncated_serialized.len();
|
||||
if truncated_len <= max_bytes || preview_budget == 0 {
|
||||
return Cow::Owned(truncated);
|
||||
}
|
||||
|
||||
// The preview is serialized as a JSON string, so escaping can make it exceed its raw byte
|
||||
// budget. Scale the next budget by the observed size of the complete replacement.
|
||||
preview_budget = (preview_budget.saturating_mul(max_bytes) / truncated_len)
|
||||
.min(preview_budget.saturating_sub(1));
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies the existing byte/token budget without changing success metadata or media ordering.
|
||||
|
||||
@@ -32,10 +32,11 @@ fn truncate_mcp_tool_result_borrows_small_result() {
|
||||
|
||||
#[test]
|
||||
fn truncate_mcp_tool_result_compacts_large_result() {
|
||||
let max_bytes = 1024;
|
||||
let result = CallToolResult {
|
||||
content: vec![serde_json::json!({
|
||||
"type": "text",
|
||||
"text": format!("head\n{}\ntail", "x".repeat(5_000)),
|
||||
"text": format!("head\n{}\ntail", "\"\\\n".repeat(5_000)),
|
||||
})],
|
||||
structured_content: Some(serde_json::json!({"x": "y".repeat(5_000)})),
|
||||
is_error: Some(true),
|
||||
@@ -44,7 +45,7 @@ fn truncate_mcp_tool_result_compacts_large_result() {
|
||||
})),
|
||||
};
|
||||
|
||||
let truncated = truncate_mcp_tool_result(&result, /*max_bytes*/ 1024).into_owned();
|
||||
let truncated = truncate_mcp_tool_result(&result, max_bytes).into_owned();
|
||||
|
||||
let preview = truncated.content[0]["text"]
|
||||
.as_str()
|
||||
@@ -61,10 +62,14 @@ fn truncate_mcp_tool_result_compacts_large_result() {
|
||||
meta: None,
|
||||
}
|
||||
);
|
||||
assert!(preview.len() < 2 * 1024);
|
||||
assert!(serde_json::to_string(&truncated).unwrap().len() <= max_bytes);
|
||||
assert!(preview.contains("head"));
|
||||
assert!(preview.contains("chars truncated"));
|
||||
assert!(preview.contains("meta-tail"));
|
||||
|
||||
let truncated_again = truncate_mcp_tool_result(&truncated, max_bytes);
|
||||
assert!(matches!(truncated_again, std::borrow::Cow::Borrowed(_)));
|
||||
assert_eq!(truncated_again.as_ref(), &truncated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user