1111//! CodeWhale's `reasoning_effort` tiers, sampling-parameter rules for
1212//! models that reject them, and `cache_control` breakpoint placement
1313//! aligned with the prefix-zone model in `prefix_cache.rs`;
14- //! - usage normalization (#2961): `prompt_cache_hit_tokens` comes from
15- //! `cache_read_input_tokens`, `prompt_cache_miss_tokens` is `input_tokens`
16- //! plus `cache_creation_input_tokens`, and the normalized `input_tokens`
17- //! is the sum of all three (total prompt, the DeepSeek convention);
14+ //! - usage normalization (#2961 / #4318): `prompt_cache_hit_tokens` comes from
15+ //! `cache_read_input_tokens`, `prompt_cache_write_tokens` from
16+ //! `cache_creation_input_tokens`, `prompt_cache_miss_tokens` is the raw
17+ //! non-cached `input_tokens`, and the normalized `input_tokens` is the sum
18+ //! of all three (total prompt, the DeepSeek convention);
1819//! - signed-thinking handling: `signature_delta` is captured into
1920//! [`crate::models::Delta::SignatureDelta`] and assistant thinking blocks
2021//! replay verbatim (signature included); unsigned thinking blocks are
@@ -496,8 +497,8 @@ fn convert_anthropic_sse_data(data: &str) -> Option<Result<StreamEvent>> {
496497}
497498
498499/// Map Anthropic's usage payload onto the normalized [`Usage`] convention
499- /// (#2961): hit = cache reads, miss = uncached input + cache writes,
500- /// `input_tokens` = the total prompt across all three.
500+ /// (#2961 / #4318 ): hit = cache reads, write = cache creation, miss = raw
501+ /// uncached input, `input_tokens` = the total prompt across all three.
501502fn parse_anthropic_usage ( usage : & Value ) -> Usage {
502503 let field = |name : & str | {
503504 usage
@@ -517,7 +518,8 @@ fn parse_anthropic_usage(usage: &Value) -> Usage {
517518 . saturating_add ( cache_read) ,
518519 output_tokens : output,
519520 prompt_cache_hit_tokens : Some ( cache_read) ,
520- prompt_cache_miss_tokens : Some ( input_raw. saturating_add ( cache_creation) ) ,
521+ prompt_cache_miss_tokens : Some ( input_raw) ,
522+ prompt_cache_write_tokens : Some ( cache_creation) ,
521523 reasoning_tokens : None ,
522524 reasoning_replay_tokens : None ,
523525 server_tool_use : None ,
@@ -852,7 +854,8 @@ mod tests {
852854 } ;
853855 assert_eq ! ( message. usage. input_tokens, 3 + 2045 + 18000 ) ;
854856 assert_eq ! ( message. usage. prompt_cache_hit_tokens, Some ( 18000 ) ) ;
855- assert_eq ! ( message. usage. prompt_cache_miss_tokens, Some ( 3 + 2045 ) ) ;
857+ assert_eq ! ( message. usage. prompt_cache_miss_tokens, Some ( 3 ) ) ;
858+ assert_eq ! ( message. usage. prompt_cache_write_tokens, Some ( 2045 ) ) ;
856859
857860 assert ! ( matches!(
858861 & decoded[ 1 ] ,
@@ -927,6 +930,21 @@ mod tests {
927930 assert_eq ! ( usage. output_tokens, 5 ) ;
928931 assert_eq ! ( usage. prompt_cache_hit_tokens, Some ( 0 ) ) ;
929932 assert_eq ! ( usage. prompt_cache_miss_tokens, Some ( 10 ) ) ;
933+ assert_eq ! ( usage. prompt_cache_write_tokens, Some ( 0 ) ) ;
934+ }
935+
936+ #[ test]
937+ fn usage_mapping_keeps_cache_write_separate_from_miss ( ) {
938+ let usage = parse_anthropic_usage ( & json ! ( {
939+ "input_tokens" : 3 ,
940+ "cache_creation_input_tokens" : 2045 ,
941+ "cache_read_input_tokens" : 18000 ,
942+ "output_tokens" : 1 ,
943+ } ) ) ;
944+ assert_eq ! ( usage. input_tokens, 3 + 2045 + 18000 ) ;
945+ assert_eq ! ( usage. prompt_cache_hit_tokens, Some ( 18000 ) ) ;
946+ assert_eq ! ( usage. prompt_cache_miss_tokens, Some ( 3 ) ) ;
947+ assert_eq ! ( usage. prompt_cache_write_tokens, Some ( 2045 ) ) ;
930948 }
931949
932950 #[ test]
0 commit comments