@@ -15,19 +15,36 @@ pub struct ModelPricing {
1515 pub input_per_million : f64 ,
1616 /// USD per 1M output (completion) tokens.
1717 pub output_per_million : f64 ,
18+ /// USD per 1M cached read input tokens (typically 10% of input price).
19+ /// If None, no cache-read pricing is applied (cached tokens are free).
20+ pub cache_read_input_per_million : Option < f64 > ,
1821}
1922
2023impl ModelPricing {
2124 pub const fn new ( input_per_million : f64 , output_per_million : f64 ) -> Self {
22- Self { input_per_million, output_per_million }
25+ Self { input_per_million, output_per_million, cache_read_input_per_million : None }
26+ }
27+
28+ pub const fn with_cache (
29+ input_per_million : f64 ,
30+ output_per_million : f64 ,
31+ cache_read_input_per_million : f64 ,
32+ ) -> Self {
33+ Self { input_per_million, output_per_million, cache_read_input_per_million : Some ( cache_read_input_per_million) }
2334 }
2435
2536 /// Compute the USD cost for the given token counts.
37+ /// Cached read tokens are charged at cache_read_input_per_million rate if available.
2638 #[ inline]
27- pub fn cost ( & self , prompt_tokens : u64 , completion_tokens : u64 ) -> f64 {
39+ pub fn cost ( & self , prompt_tokens : u64 , completion_tokens : u64 , cache_read_tokens : u64 ) -> f64 {
2840 let input_cost = ( prompt_tokens as f64 / 1_000_000.0 ) * self . input_per_million ;
2941 let output_cost = ( completion_tokens as f64 / 1_000_000.0 ) * self . output_per_million ;
30- input_cost + output_cost
42+ let cache_cost = if let Some ( cache_price) = self . cache_read_input_per_million {
43+ ( cache_read_tokens as f64 / 1_000_000.0 ) * cache_price
44+ } else {
45+ 0.0
46+ } ;
47+ input_cost + output_cost + cache_cost
3148 }
3249}
3350
@@ -61,15 +78,25 @@ static PRICING_TABLE: LazyLock<Vec<PricingEntry>> = LazyLock::new(|| {
6178 PricingEntry { prefixes: & [ "gpt-4-turbo" ] , pricing: ModelPricing :: new( 10.00 , 30.00 ) } ,
6279 PricingEntry { prefixes: & [ "gpt-4" ] , pricing: ModelPricing :: new( 30.00 , 60.00 ) } ,
6380 // ── Anthropic ─────────────────────────────────────────────
64- PricingEntry { prefixes: & [ "claude-opus-4" ] , pricing: ModelPricing :: new( 15.00 , 75.00 ) } ,
65- PricingEntry { prefixes: & [ "claude-sonnet-4" ] , pricing: ModelPricing :: new( 3.00 , 15.00 ) } ,
66- PricingEntry { prefixes: & [ "claude-3.7-sonnet" , "claude-3-7-sonnet" ] , pricing: ModelPricing :: new( 3.00 , 15.00 ) } ,
67- PricingEntry { prefixes: & [ "claude-3.5-haiku" , "claude-3-5-haiku" ] , pricing: ModelPricing :: new( 0.80 , 4.00 ) } ,
68- PricingEntry { prefixes: & [ "claude-3.5-sonnet" , "claude-3-5-sonnet" ] , pricing: ModelPricing :: new( 3.00 , 15.00 ) } ,
69- PricingEntry { prefixes: & [ "claude-3-opus" ] , pricing: ModelPricing :: new( 15.00 , 75.00 ) } ,
70- PricingEntry { prefixes: & [ "claude-3-sonnet" ] , pricing: ModelPricing :: new( 3.00 , 15.00 ) } ,
71- PricingEntry { prefixes: & [ "claude-3-haiku" ] , pricing: ModelPricing :: new( 0.25 , 1.25 ) } ,
72- PricingEntry { prefixes: & [ "claude" ] , pricing: ModelPricing :: new( 3.00 , 15.00 ) } ,
81+ // Anthropic models support prompt caching at 90% discount (10% of input price).
82+ PricingEntry { prefixes: & [ "claude-opus-4" ] , pricing: ModelPricing :: with_cache( 15.00 , 75.00 , 1.50 ) } ,
83+ PricingEntry { prefixes: & [ "claude-sonnet-4" ] , pricing: ModelPricing :: with_cache( 3.00 , 15.00 , 0.30 ) } ,
84+ PricingEntry {
85+ prefixes: & [ "claude-3.7-sonnet" , "claude-3-7-sonnet" ] ,
86+ pricing: ModelPricing :: with_cache( 3.00 , 15.00 , 0.30 ) ,
87+ } ,
88+ PricingEntry {
89+ prefixes: & [ "claude-3.5-haiku" , "claude-3-5-haiku" ] ,
90+ pricing: ModelPricing :: with_cache( 0.80 , 4.00 , 0.08 ) ,
91+ } ,
92+ PricingEntry {
93+ prefixes: & [ "claude-3.5-sonnet" , "claude-3-5-sonnet" ] ,
94+ pricing: ModelPricing :: with_cache( 3.00 , 15.00 , 0.30 ) ,
95+ } ,
96+ PricingEntry { prefixes: & [ "claude-3-opus" ] , pricing: ModelPricing :: with_cache( 15.00 , 75.00 , 1.50 ) } ,
97+ PricingEntry { prefixes: & [ "claude-3-sonnet" ] , pricing: ModelPricing :: with_cache( 3.00 , 15.00 , 0.30 ) } ,
98+ PricingEntry { prefixes: & [ "claude-3-haiku" ] , pricing: ModelPricing :: with_cache( 0.25 , 1.25 , 0.025 ) } ,
99+ PricingEntry { prefixes: & [ "claude" ] , pricing: ModelPricing :: with_cache( 3.00 , 15.00 , 0.30 ) } ,
73100 // ── Google / Gemini ───────────────────────────────────────
74101 PricingEntry { prefixes: & [ "gemini-2.5-pro" ] , pricing: ModelPricing :: new( 1.25 , 10.00 ) } ,
75102 PricingEntry { prefixes: & [ "gemini-2.5-flash" ] , pricing: ModelPricing :: new( 0.15 , 0.60 ) } ,
@@ -135,25 +162,44 @@ mod tests {
135162 fn model_pricing_cost_calculation ( ) {
136163 let pricing = ModelPricing :: new ( 2.50 , 10.00 ) ;
137164 // 1M input tokens → $2.50
138- assert ! ( ( pricing. cost( 1_000_000 , 0 ) - 2.50 ) . abs( ) < 1e-9 ) ;
165+ assert ! ( ( pricing. cost( 1_000_000 , 0 , 0 ) - 2.50 ) . abs( ) < 1e-9 ) ;
139166 // 1M output tokens → $10.00
140- assert ! ( ( pricing. cost( 0 , 1_000_000 ) - 10.00 ) . abs( ) < 1e-9 ) ;
141- // 100k input + 50k output
142- let cost = pricing. cost ( 100_000 , 50_000 ) ;
167+ assert ! ( ( pricing. cost( 0 , 1_000_000 , 0 ) - 10.00 ) . abs( ) < 1e-9 ) ;
168+ // 100k input + 50k output + no cache
169+ let cost = pricing. cost ( 100_000 , 50_000 , 0 ) ;
143170 assert ! ( ( cost - 0.75 ) . abs( ) < 1e-9 ) ;
144171 }
145172
173+ #[ test]
174+ fn model_pricing_with_cache_read_tokens ( ) {
175+ let pricing = ModelPricing :: with_cache ( 3.00 , 15.00 , 0.30 ) ;
176+ // 100k input + 50k output + 25k cache read
177+ // Cost: (100k/1M)*3.0 + (50k/1M)*15.0 + (25k/1M)*0.30 = 0.3 + 0.75 + 0.0075 = 1.0575
178+ let cost = pricing. cost ( 100_000 , 50_000 , 25_000 ) ;
179+ assert ! ( ( cost - 1.0575 ) . abs( ) < 1e-9 ) ;
180+ }
181+
182+ #[ test]
183+ fn model_pricing_without_cache_pricing_ignores_cache_tokens ( ) {
184+ let pricing = ModelPricing :: new ( 2.50 , 10.00 ) ;
185+ // Even with cache tokens, cost should not include them
186+ let cost = pricing. cost ( 100_000 , 50_000 , 25_000 ) ;
187+ let cost_without_cache = pricing. cost ( 100_000 , 50_000 , 0 ) ;
188+ assert ! ( ( cost - cost_without_cache) . abs( ) < 1e-9 ) ;
189+ }
190+
146191 #[ test]
147192 fn model_pricing_zero_tokens ( ) {
148193 let pricing = ModelPricing :: new ( 2.50 , 10.00 ) ;
149- assert ! ( ( pricing. cost( 0 , 0 ) - 0.0 ) . abs( ) < 1e-9 ) ;
194+ assert ! ( ( pricing. cost( 0 , 0 , 0 ) - 0.0 ) . abs( ) < 1e-9 ) ;
150195 }
151196
152197 #[ test]
153198 fn lookup_openai_gpt4o ( ) {
154199 let pricing = lookup ( "gpt-4o" ) . unwrap ( ) ;
155200 assert ! ( ( pricing. input_per_million - 2.50 ) . abs( ) < 1e-9 ) ;
156201 assert ! ( ( pricing. output_per_million - 10.00 ) . abs( ) < 1e-9 ) ;
202+ assert ! ( pricing. cache_read_input_per_million. is_none( ) ) ;
157203 }
158204
159205 #[ test]
@@ -167,12 +213,14 @@ mod tests {
167213 let pricing = lookup ( "claude-sonnet-4-20250514" ) . unwrap ( ) ;
168214 assert ! ( ( pricing. input_per_million - 3.00 ) . abs( ) < 1e-9 ) ;
169215 assert ! ( ( pricing. output_per_million - 15.00 ) . abs( ) < 1e-9 ) ;
216+ assert_eq ! ( pricing. cache_read_input_per_million, Some ( 0.30 ) ) ;
170217 }
171218
172219 #[ test]
173220 fn lookup_anthropic_claude_35_sonnet ( ) {
174221 let pricing = lookup ( "claude-3-5-sonnet-20241022" ) . unwrap ( ) ;
175222 assert ! ( ( pricing. input_per_million - 3.00 ) . abs( ) < 1e-9 ) ;
223+ assert_eq ! ( pricing. cache_read_input_per_million, Some ( 0.30 ) ) ;
176224 }
177225
178226 #[ test]
@@ -194,6 +242,7 @@ mod tests {
194242 // Provider prefixes (openrouter/, etc.) should be stripped implicitly by contains()
195243 let pricing = lookup ( "openrouter/anthropic/claude-sonnet-4" ) . unwrap ( ) ;
196244 assert ! ( ( pricing. input_per_million - 3.00 ) . abs( ) < 1e-9 ) ;
245+ assert_eq ! ( pricing. cache_read_input_per_million, Some ( 0.30 ) ) ;
197246 }
198247
199248 #[ test]
@@ -230,4 +279,11 @@ mod tests {
230279 let pricing = lookup ( "qwen2.5-coder-32b-instruct" ) . unwrap ( ) ;
231280 assert ! ( ( pricing. input_per_million - 0.14 ) . abs( ) < 1e-9 ) ;
232281 }
282+
283+ #[ test]
284+ fn lookup_anthropic_claude_opus_4_has_cache_pricing ( ) {
285+ let pricing = lookup ( "claude-opus-4" ) . unwrap ( ) ;
286+ assert ! ( ( pricing. input_per_million - 15.00 ) . abs( ) < 1e-9 ) ;
287+ assert_eq ! ( pricing. cache_read_input_per_million, Some ( 1.50 ) ) ;
288+ }
233289}
0 commit comments