feat: Suppor mid-turn auto compressions so sufficiently large histories don't create deadlocks for max input tokens

This commit is contained in:
Dark-Alex-17 committed 2026-09-17 17:12:18 -06:00
1 parent 56ed4664cd
commit e002bd6727
9 files changed
+1199 -109

No files matched your search

+9
View File
@@ -420,6 +420,15 @@ impl TokenUsage {
&& self.cache_read_input_tokens.is_none()
}
/// Prompt-side tokens the API reported for one request: fresh input plus
/// cache writes and cache reads. The buckets are disjoint (see
/// [`Self::cost_usd`]), so the terms sum without adjustment.
pub fn total_prompt_tokens(&self) -> u64 {
self.input_tokens.unwrap_or(0)
+ self.cache_creation_input_tokens.unwrap_or(0)
+ self.cache_read_input_tokens.unwrap_or(0)
}
pub fn accumulate(&mut self, other: &TokenUsage) {
fn add(acc: &mut Option<u64>, value: Option<u64>) {
if let Some(value) = value {
+30 -6
View File
@@ -869,17 +869,22 @@ fn reasoning_thinking_block(item: &Value) -> Option<ThinkingBlock> {
}
/// OpenAI reports cached input under `input_tokens_details` and has no
/// cache-creation concept, so that field stays `None`.
/// cache-creation concept, so that field stays `None`. The reported
/// `input_tokens` total already includes cached reads, so cached tokens are
/// subtracted here to keep the buckets disjoint like other providers.
fn openai_parse_responses_usage(usage: &Value) -> Option<TokenUsage> {
if !usage.is_object() {
return None;
}
let cached_tokens = usage["input_tokens_details"]["cached_tokens"].as_u64();
Some(TokenUsage {
input_tokens: usage["input_tokens"].as_u64(),
input_tokens: usage["input_tokens"]
.as_u64()
.map(|v| v.saturating_sub(cached_tokens.unwrap_or(0))),
output_tokens: usage["output_tokens"].as_u64(),
cache_creation_input_tokens: None,
cache_read_input_tokens: usage["input_tokens_details"]["cached_tokens"].as_u64(),
cache_read_input_tokens: cached_tokens,
})
}
@@ -1267,12 +1272,31 @@ mod tests {
let output = openai_extract_responses(&data).unwrap();
let usage = output.usage.unwrap();
assert_eq!(usage.input_tokens, Some(100));
assert_eq!(usage.input_tokens, Some(40));
assert_eq!(usage.output_tokens, Some(20));
assert_eq!(usage.cache_creation_input_tokens, None);
assert_eq!(usage.cache_read_input_tokens, Some(60));
}
#[test]
fn extract_responses_usage_without_cached_details_keeps_input_intact() {
let data = json!({
"output": [
{ "type": "message", "content": [{ "type": "output_text", "text": "answer" }] },
],
"usage": {
"input_tokens": 100,
"output_tokens": 20,
}
});
let output = openai_extract_responses(&data).unwrap();
let usage = output.usage.unwrap();
assert_eq!(usage.input_tokens, Some(100));
assert_eq!(usage.cache_read_input_tokens, None);
}
#[test]
fn extract_responses_without_usage_leaves_it_none() {
let data = json!({
@@ -1357,7 +1381,7 @@ mod tests {
assert!(done);
let (_, _, _, usage) = handler.take();
let usage = usage.unwrap();
assert_eq!(usage.input_tokens, Some(100));
assert_eq!(usage.input_tokens, Some(40));
assert_eq!(usage.output_tokens, Some(20));
assert_eq!(usage.cache_creation_input_tokens, None);
assert_eq!(usage.cache_read_input_tokens, Some(60));
@@ -1467,7 +1491,7 @@ mod tests {
let (text, _, _, usage) = handler.take();
assert_eq!(text, "partial answer");
let usage = usage.expect("usage should be recorded for truncated turns");
assert_eq!(usage.input_tokens, Some(50));
assert_eq!(usage.input_tokens, Some(25));
assert_eq!(usage.output_tokens, Some(10));
assert_eq!(usage.cache_read_input_tokens, Some(25));
}