Estimate history tokens from content instead of serialized envelopes (#45094)

## Why

Serialized response items include message IDs, metadata, and JSON escaping that inflate token estimates without adding model-visible content.

## What changed

- Estimate each response item from its content, retaining JSON syntax for structured tool payloads.
- Apply image estimates to all image inputs, including non-base64 URLs, and count audio and encrypted content through their modality-specific estimates.
- Exclude plaintext reasoning and bookkeeping-only items from replay accounting.

## Testing

Update unit expectations for text, images, audio, and encrypted content. Add a remote compaction regression test showing that equal-length text produces identical token usage estimates despite different message IDs, metadata, and JSON escaping, while preserving the submitted messages.

GitOrigin-RevId: 1c43d5abbcc1668b4ab413901a2b4b6511ca5892
This commit is contained in:
Won Park authored and copyberry committed 2026-09-12 18:41:47 +00:00
1 parent 8d3c6cc13d
commit b04a2c2645
3 files changed
+241 -196

No files matched your search

@@ -14,6 +14,78 @@ fn compact_response() -> String {
])
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn remote_compact_v2_token_estimate_ignores_message_bookkeeping_and_json_escaping()
-> Result<()> {
skip_if_no_network!(Ok(()));
let harness = TestCodexHarness::with_auto_env_builder(
test_codex().with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()),
)
.await?;
let escaped_text = "a \"quoted\" line\nand\\path";
let plain_text = "x".repeat(escaped_text.len());
let mut estimates = Vec::new();
for (id, text, metadata) in [
(
"msg_1",
plain_text.as_str(),
json!({
"turn_id": "turn-1",
"create_time": 1,
"content_item_kinds": ["unknown"],
}),
),
(
"msg_019f0000-0000-7000-8000-000000000001",
escaped_text,
json!({
"turn_id": "019f0000-0000-7000-8000-000000000002",
"create_time": 1_700_000_000.123,
"content_item_kinds": ["user.text"],
}),
),
] {
let codex = harness
.test()
.thread_manager
.start_thread(StartThreadOptions {
environments: Some(vec![
harness.test().executor_environment().selection().clone(),
]),
..StartThreadOptions::new(harness.test().config.clone())
})
.await?
.thread;
let message = json!({
"type": "message",
"id": id,
"role": "user",
"content": [{"type": "input_text", "text": text}],
"internal_chat_message_metadata_passthrough": metadata,
});
codex
.inject_response_items(vec![serde_json::from_value(message.clone())?])
.await?;
let mock = mount_sse_once(harness.server(), compact_response()).await;
codex.submit(Op::Compact).await?;
wait_for_turn_complete(&codex).await;
estimates.push(codex.token_usage_info().await.context("token usage")?);
assert_eq!(
mock.single_request()
.input()
.into_iter()
.find(|item| item["id"] == id),
Some(message),
);
codex.shutdown_and_wait().await?;
}
assert_eq!(estimates[0], estimates[1]);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn remote_compact_v2_trims_tool_search_output_to_empty_tools_array() -> Result<()> {
skip_if_no_network!(Ok(()));