Account for encrypted reasoning for auto compaction (#7113)

- The total token used returned from the api doesn't account for the
reasoning items before the assistant message
- Account for those for auto compaction
- Add the encrypted reasoning effort in the common tests utils
- Add a test to make sure it works as expected
This commit is contained in:
Ahmed Ibrahim
2025-11-21 19:06:45 -08:00
committed by GitHub
parent 529eb4ff2a
commit b519267d05
9 changed files with 236 additions and 30 deletions

View File

@@ -2,6 +2,7 @@ use crate::codex::TurnContext;
use crate::context_manager::normalize;
use crate::truncate::TruncationPolicy;
use crate::truncate::approx_token_count;
use crate::truncate::approx_tokens_from_byte_count;
use crate::truncate::truncate_function_output_items_with_policy;
use crate::truncate::truncate_text;
use codex_protocol::models::FunctionCallOutputPayload;
@@ -119,6 +120,54 @@ impl ContextManager {
);
}
fn get_non_last_reasoning_items_tokens(&self) -> usize {
// get reasoning items excluding all the ones after the last user message
let Some(last_user_index) = self
.items
.iter()
.rposition(|item| matches!(item, ResponseItem::Message { role, .. } if role == "user"))
else {
return 0usize;
};
let total_reasoning_bytes = self
.items
.iter()
.take(last_user_index)
.filter_map(|item| {
if let ResponseItem::Reasoning {
encrypted_content: Some(content),
..
} = item
{
Some(content.len())
} else {
None
}
})
.map(Self::estimate_reasoning_length)
.fold(0usize, usize::saturating_add);
let token_estimate = approx_tokens_from_byte_count(total_reasoning_bytes);
token_estimate as usize
}
fn estimate_reasoning_length(encoded_len: usize) -> usize {
encoded_len
.saturating_mul(3)
.checked_div(4)
.unwrap_or(0)
.saturating_sub(650)
}
pub(crate) fn get_total_token_usage(&self) -> i64 {
self.token_info
.as_ref()
.map(|info| info.last_token_usage.total_tokens)
.unwrap_or(0)
.saturating_add(self.get_non_last_reasoning_items_tokens() as i64)
}
/// This function enforces a couple of invariants on the in-memory history:
/// 1. every call (function/custom) has a corresponding output entry
/// 2. every output has a corresponding call entry

View File

@@ -56,6 +56,17 @@ fn reasoning_msg(text: &str) -> ResponseItem {
}
}
fn reasoning_with_encrypted_content(len: usize) -> ResponseItem {
ResponseItem::Reasoning {
id: String::new(),
summary: vec![ReasoningItemReasoningSummary::SummaryText {
text: "summary".to_string(),
}],
content: None,
encrypted_content: Some("a".repeat(len)),
}
}
fn truncate_exec_output(content: &str) -> String {
truncate::truncate_text(content, TruncationPolicy::Tokens(EXEC_FORMAT_MAX_TOKENS))
}
@@ -112,6 +123,28 @@ fn filters_non_api_messages() {
);
}
#[test]
fn non_last_reasoning_tokens_return_zero_when_no_user_messages() {
let history = create_history_with_items(vec![reasoning_with_encrypted_content(800)]);
assert_eq!(history.get_non_last_reasoning_items_tokens(), 0);
}
#[test]
fn non_last_reasoning_tokens_ignore_entries_after_last_user() {
let history = create_history_with_items(vec![
reasoning_with_encrypted_content(900),
user_msg("first"),
reasoning_with_encrypted_content(1_000),
user_msg("second"),
reasoning_with_encrypted_content(2_000),
]);
// first: (900 * 0.75 - 650) / 4 = 6.25 tokens
// second: (1000 * 0.75 - 650) / 4 = 25 tokens
// first + second = 62.5
assert_eq!(history.get_non_last_reasoning_items_tokens(), 32);
}
#[test]
fn get_history_for_prompt_drops_ghost_commits() {
let items = vec![ResponseItem::GhostSnapshot {