Skip to content

Commit 24ad4aa

Browse files
authored
Merge pull request #783 from nonoqing/yuyiqing/dev
token usage
2 parents 5bcb8ea + 5ba684d commit 24ad4aa

5 files changed

Lines changed: 120 additions & 1 deletion

File tree

src/crates/agent-stream/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,7 @@ impl StreamProcessor {
609609
total_token_count: response_usage.total_token_count,
610610
reasoning_token_count: response_usage.reasoning_token_count,
611611
cached_content_token_count: response_usage.cached_content_token_count,
612+
cache_creation_token_count: response_usage.cache_creation_token_count,
612613
});
613614
debug!(
614615
"Received token usage stats: input={}, output={}, total={}",

src/crates/ai-adapters/src/client/response_aggregator.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,5 +166,6 @@ pub(crate) fn unified_usage_to_gemini_usage(
166166
total_token_count: usage.total_token_count,
167167
reasoning_token_count: usage.reasoning_token_count,
168168
cached_content_token_count: usage.cached_content_token_count,
169+
cache_creation_token_count: usage.cache_creation_token_count,
169170
}
170171
}

src/crates/ai-adapters/src/stream/types/responses.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,33 @@ pub fn parse_responses_output_item(
129129

130130
#[cfg(test)]
131131
mod tests {
132-
use super::{parse_responses_output_item, ResponsesCompleted, ResponsesStreamEvent};
132+
use super::{parse_responses_output_item, ResponsesCompleted, ResponsesStreamEvent, ResponsesUsage};
133+
use crate::stream::types::unified::UnifiedTokenUsage;
133134
use serde_json::json;
134135

136+
#[test]
137+
fn responses_cached_tokens_maps_to_cached_content() {
138+
let raw = r#"{
139+
"input_tokens": 200,
140+
"input_tokens_details": { "cached_tokens": 80 },
141+
"output_tokens": 40,
142+
"total_tokens": 240
143+
}"#;
144+
let usage: ResponsesUsage = serde_json::from_str(raw).expect("valid responses usage");
145+
let unified: UnifiedTokenUsage = usage.into();
146+
assert_eq!(unified.cached_content_token_count, Some(80));
147+
assert_eq!(unified.cache_creation_token_count, None);
148+
}
149+
150+
#[test]
151+
fn responses_absent_cache_stays_none() {
152+
let raw = r#"{ "input_tokens": 200, "output_tokens": 40, "total_tokens": 240 }"#;
153+
let usage: ResponsesUsage = serde_json::from_str(raw).expect("valid responses usage");
154+
let unified: UnifiedTokenUsage = usage.into();
155+
assert_eq!(unified.cached_content_token_count, None);
156+
assert_eq!(unified.cache_creation_token_count, None);
157+
}
158+
135159
#[test]
136160
fn parses_output_text_message_item() {
137161
let response = parse_responses_output_item(

src/crates/ai-adapters/src/types/ai.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ pub struct GeminiUsage {
3232
#[serde(rename = "cachedContentTokenCount")]
3333
#[serde(skip_serializing_if = "Option::is_none")]
3434
pub cached_content_token_count: Option<u32>,
35+
#[serde(rename = "cacheCreationTokenCount")]
36+
#[serde(default, skip_serializing_if = "Option::is_none")]
37+
pub cache_creation_token_count: Option<u32>,
3538
}
3639

3740
/// Structured message codes for localized connection test messaging.
@@ -69,3 +72,40 @@ pub struct RemoteModelInfo {
6972
#[serde(skip_serializing_if = "Option::is_none")]
7073
pub display_name: Option<String>,
7174
}
75+
76+
#[cfg(test)]
77+
mod tests {
78+
use super::GeminiUsage;
79+
80+
#[test]
81+
fn gemini_usage_roundtrips_cache_creation_field() {
82+
let usage = GeminiUsage {
83+
prompt_token_count: 100,
84+
candidates_token_count: 20,
85+
total_token_count: 120,
86+
reasoning_token_count: None,
87+
cached_content_token_count: Some(30),
88+
cache_creation_token_count: Some(20),
89+
};
90+
let json = serde_json::to_string(&usage).expect("serialize");
91+
assert!(json.contains("\"cacheCreationTokenCount\":20"));
92+
93+
let parsed: GeminiUsage = serde_json::from_str(&json).expect("deserialize");
94+
assert_eq!(parsed.cache_creation_token_count, Some(20));
95+
}
96+
97+
#[test]
98+
fn gemini_usage_legacy_payload_parses_with_new_field_absent() {
99+
// Records persisted before this plan don't have cacheCreationTokenCount;
100+
// they must still parse, with the new field defaulting to None.
101+
let raw = r#"{
102+
"promptTokenCount": 10,
103+
"candidatesTokenCount": 5,
104+
"totalTokenCount": 15,
105+
"cachedContentTokenCount": 3
106+
}"#;
107+
let parsed: GeminiUsage = serde_json::from_str(raw).expect("legacy payload");
108+
assert_eq!(parsed.cached_content_token_count, Some(3));
109+
assert_eq!(parsed.cache_creation_token_count, None);
110+
}
111+
}

src/crates/core/src/agentic/execution/round_executor.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1356,6 +1356,13 @@ fn token_details_from_usage(
13561356
serde_json::json!(cached_tokens),
13571357
);
13581358
}
1359+
// Cache writes (Anthropic only at the moment). Disjoint from reads.
1360+
if let Some(creation_tokens) = usage.cache_creation_token_count {
1361+
details.insert(
1362+
"cacheCreationTokenCount".to_string(),
1363+
serde_json::json!(creation_tokens),
1364+
);
1365+
}
13591366

13601367
(!details.is_empty()).then_some(serde_json::Value::Object(details))
13611368
}
@@ -1973,4 +1980,50 @@ mod tests {
19731980
use super::detect_placeholder_patterns;
19741981
assert!(detect_placeholder_patterns("").is_none());
19751982
}
1983+
1984+
#[test]
1985+
fn token_details_emits_both_cache_keys_when_present() {
1986+
use crate::util::types::ai::GeminiUsage;
1987+
let usage = GeminiUsage {
1988+
prompt_token_count: 100,
1989+
candidates_token_count: 20,
1990+
total_token_count: 120,
1991+
reasoning_token_count: None,
1992+
cached_content_token_count: Some(30),
1993+
cache_creation_token_count: Some(20),
1994+
};
1995+
let details = super::token_details_from_usage(&usage).expect("details");
1996+
assert_eq!(details.get("cachedContentTokenCount").and_then(|v| v.as_u64()), Some(30));
1997+
assert_eq!(details.get("cacheCreationTokenCount").and_then(|v| v.as_u64()), Some(20));
1998+
}
1999+
2000+
#[test]
2001+
fn token_details_emits_only_read_when_creation_absent() {
2002+
use crate::util::types::ai::GeminiUsage;
2003+
let usage = GeminiUsage {
2004+
prompt_token_count: 100,
2005+
candidates_token_count: 20,
2006+
total_token_count: 120,
2007+
reasoning_token_count: None,
2008+
cached_content_token_count: Some(30),
2009+
cache_creation_token_count: None,
2010+
};
2011+
let details = super::token_details_from_usage(&usage).expect("details");
2012+
assert_eq!(details.get("cachedContentTokenCount").and_then(|v| v.as_u64()), Some(30));
2013+
assert!(details.get("cacheCreationTokenCount").is_none());
2014+
}
2015+
2016+
#[test]
2017+
fn token_details_is_none_when_no_cache_info() {
2018+
use crate::util::types::ai::GeminiUsage;
2019+
let usage = GeminiUsage {
2020+
prompt_token_count: 100,
2021+
candidates_token_count: 20,
2022+
total_token_count: 120,
2023+
reasoning_token_count: None,
2024+
cached_content_token_count: None,
2025+
cache_creation_token_count: None,
2026+
};
2027+
assert!(super::token_details_from_usage(&usage).is_none());
2028+
}
19762029
}

0 commit comments

Comments
 (0)