diff --git a/.gitignore b/.gitignore index 106a06615a..498293339a 100644 --- a/.gitignore +++ b/.gitignore @@ -135,3 +135,5 @@ src/apps/mobile/android/.kotlin/ # Retired implementation-process notes stay local; durable docs live with their owner. /docs/superpowers/ + +AGENTS.override.md \ No newline at end of file diff --git a/src/crates/assembly/core/src/agentic/insights/collector.rs b/src/crates/assembly/core/src/agentic/insights/collector.rs index 7cab5801d8..6d52850c4a 100644 --- a/src/crates/assembly/core/src/agentic/insights/collector.rs +++ b/src/crates/assembly/core/src/agentic/insights/collector.rs @@ -1175,6 +1175,7 @@ mod tests { first.token_usage = Some(DialogTurnTokenUsageData { input_tokens: 1_200, output_tokens: Some(300), + cached_tokens: None, total_tokens: 1_500, timestamp: 2_000, }); @@ -1182,6 +1183,7 @@ mod tests { second.token_usage = Some(DialogTurnTokenUsageData { input_tokens: 800, output_tokens: None, + cached_tokens: None, total_tokens: 900, timestamp: 4_000, }); diff --git a/src/crates/services/services-core/src/session/types.rs b/src/crates/services/services-core/src/session/types.rs index 1291d0e283..edd5f7e2b1 100644 --- a/src/crates/services/services-core/src/session/types.rs +++ b/src/crates/services/services-core/src/session/types.rs @@ -639,6 +639,17 @@ pub struct DialogTurnTokenUsageData { )] pub output_tokens: Option, + /// Provider-reported cache-hit input tokens, when available. + /// + /// This is optional because providers may omit cache telemetry and older + /// persisted turns predate this field. + #[serde( + default, + skip_serializing_if = "Option::is_none", + alias = "cached_tokens" + )] + pub cached_tokens: Option, + /// Total tokens reported by the provider for this request. #[serde(alias = "total_tokens")] pub total_tokens: u64, @@ -1528,6 +1539,7 @@ mod tests { "tokenUsage": { "inputTokens": 1200, "outputTokens": 320, + "cachedTokens": 900, "totalTokens": 1520, "timestamp": 2 }, @@ -1543,12 +1555,47 @@ mod tests { .expect("token usage should be preserved"); assert_eq!(token_usage.input_tokens, 1200); assert_eq!(token_usage.output_tokens, Some(320)); + assert_eq!(token_usage.cached_tokens, Some(900)); assert_eq!(token_usage.total_tokens, 1520); let serialized = serde_json::to_value(&turn).expect("turn should serialize"); + assert_eq!(serialized["tokenUsage"]["cachedTokens"], 900); assert_eq!(serialized["tokenUsage"]["totalTokens"], 1520); } + #[test] + fn dialog_turn_token_usage_defaults_missing_cache_tokens_for_legacy_payloads() { + let payload = serde_json::json!({ + "inputTokens": 1200, + "outputTokens": 320, + "totalTokens": 1520, + "timestamp": 2 + }); + + let usage: super::DialogTurnTokenUsageData = + serde_json::from_value(payload).expect("legacy token usage should deserialize"); + + assert_eq!(usage.cached_tokens, None); + let serialized = serde_json::to_value(&usage).expect("legacy usage should serialize"); + assert!(!serialized.as_object().unwrap().contains_key("cachedTokens")); + } + + #[test] + fn dialog_turn_token_usage_accepts_legacy_snake_case_cache_tokens() { + let payload = serde_json::json!({ + "input_tokens": 1200, + "output_tokens": 320, + "cached_tokens": 900, + "total_tokens": 1520, + "timestamp": 2 + }); + + let usage: super::DialogTurnTokenUsageData = + serde_json::from_value(payload).expect("legacy cache telemetry should deserialize"); + + assert_eq!(usage.cached_tokens, Some(900)); + } + #[test] fn local_usage_report_turn_is_model_invisible() { assert!(!DialogTurnKind::LocalCommand.is_model_visible());