ccboard_core/pricing.rs
1//! Pricing calculations for Claude models based on official Anthropic pricing
2//!
3//! This module implements accurate pricing based on official Anthropic rates as of January 2025.
4//! Pricing varies significantly between models and token types:
5//!
6//! - **Input tokens**: Regular input tokens (not cached)
7//! - **Output tokens**: Generated tokens (typically 5x more expensive than input)
8//! - **Cache write tokens**: Tokens written to cache (25% of input price)
9//! - **Cache read tokens**: Tokens read from cache (10% of input price)
10//!
11//! # Examples
12//!
13//! ```
14//! use ccboard_core::pricing::calculate_cost;
15//!
16//! // Calculate cost for Opus-4 with 1M input + 1M output
17//! let cost = calculate_cost("opus-4", 1_000_000, 1_000_000, 0, 0);
18//! assert_eq!(cost, 90.0); // $15 input + $75 output = $90
19//!
20//! // Calculate cost with cache
21//! let cost = calculate_cost("opus-4", 1_000_000, 0, 1_000_000, 10_000_000);
22//! // $15 input + $3.75 cache_write + $15 cache_read = $33.75
23//! assert_eq!(cost, 33.75);
24//! ```
25
26use once_cell::sync::Lazy;
27use std::collections::HashMap;
28
29/// Pricing structure for a Claude model
30///
31/// All prices are per million tokens (M). Cache multipliers are applied to input price.
32#[derive(Debug, Clone)]
33pub struct ModelPricing {
34 /// Price per million input tokens ($/M)
35 pub input_price_per_million: f64,
36 /// Price per million output tokens ($/M)
37 pub output_price_per_million: f64,
38 /// Cache read multiplier (0.1 = 10% of input price)
39 pub cache_read_multiplier: f64,
40 /// Cache write multiplier (0.25 = 25% of input price)
41 pub cache_write_multiplier: f64,
42}
43
44impl ModelPricing {
45 /// Default average pricing for unknown models
46 ///
47 /// Uses a weighted average across common models (sonnet-4 weight: 70%, opus-4: 20%, haiku-4: 10%)
48 /// to provide reasonable estimates when model is unknown or unrecognized.
49 pub fn default_average() -> Self {
50 Self {
51 input_price_per_million: 3.5, // Weighted average
52 output_price_per_million: 17.5, // Weighted average
53 cache_read_multiplier: 0.1,
54 cache_write_multiplier: 0.25,
55 }
56 }
57}
58
59/// Official Claude pricing table (as of January 2025)
60///
61/// Source: https://www.anthropic.com/api#pricing
62///
63/// This table includes both full model IDs and common aliases for convenience.
64/// All cache pricing follows Anthropic's standard multipliers:
65/// - Cache read: 10% of input price
66/// - Cache write: 25% of input price
67static PRICING_TABLE: Lazy<HashMap<&'static str, ModelPricing>> = Lazy::new(|| {
68 let mut m = HashMap::new();
69
70 // Claude Opus 4.5 - Most powerful model
71 let opus_pricing = ModelPricing {
72 input_price_per_million: 15.0,
73 output_price_per_million: 75.0,
74 cache_read_multiplier: 0.1,
75 cache_write_multiplier: 0.25,
76 };
77 m.insert("claude-opus-4-5-20251101", opus_pricing.clone());
78 m.insert("opus-4", opus_pricing.clone());
79 m.insert("claude-opus-4", opus_pricing);
80
81 // Claude Sonnet 4.5 - Balanced model (most commonly used)
82 let sonnet_pricing = ModelPricing {
83 input_price_per_million: 3.0,
84 output_price_per_million: 15.0,
85 cache_read_multiplier: 0.1,
86 cache_write_multiplier: 0.25,
87 };
88 m.insert("claude-sonnet-4-5-20250929", sonnet_pricing.clone());
89 m.insert("sonnet-4", sonnet_pricing.clone());
90 m.insert("claude-sonnet-4", sonnet_pricing);
91
92 // Claude Haiku 4.5 - Fastest, most economical model
93 let haiku_pricing = ModelPricing {
94 input_price_per_million: 1.0,
95 output_price_per_million: 5.0,
96 cache_read_multiplier: 0.1,
97 cache_write_multiplier: 0.25,
98 };
99 m.insert("claude-haiku-4-5-20251001", haiku_pricing.clone());
100 m.insert("haiku-4", haiku_pricing.clone());
101 m.insert("claude-haiku-4", haiku_pricing);
102
103 m
104});
105
106/// Get pricing for a specific model
107///
108/// Returns the pricing structure for the given model ID. If the model is not recognized,
109/// returns a default weighted average pricing based on typical usage patterns.
110///
111/// # Examples
112///
113/// ```
114/// use ccboard_core::pricing::get_pricing;
115///
116/// let pricing = get_pricing("opus-4");
117/// assert_eq!(pricing.input_price_per_million, 15.0);
118///
119/// let pricing = get_pricing("unknown-model");
120/// assert_eq!(pricing.input_price_per_million, 3.5); // Default average
121/// ```
122pub fn get_pricing(model: &str) -> ModelPricing {
123 PRICING_TABLE
124 .get(model)
125 .cloned()
126 .unwrap_or_else(ModelPricing::default_average)
127}
128
129/// Calculate cost for token usage with a specific model
130///
131/// This is the main pricing calculation function. It applies official Anthropic pricing
132/// rates for each token type and sums them to produce the total cost.
133///
134/// # Pricing Formula
135///
136/// ```text
137/// Input cost = (input / 1M) × input_price
138/// Output cost = (output / 1M) × output_price
139/// Cache create cost = (cache_create / 1M) × input_price × 0.25
140/// Cache read cost = (cache_read / 1M) × input_price × 0.1
141/// Total = sum of all
142/// ```
143///
144/// # Arguments
145///
146/// * `model` - Model ID (e.g., "opus-4", "sonnet-4", "claude-haiku-4-5-20251001")
147/// * `input` - Regular input tokens (not cached)
148/// * `output` - Generated output tokens
149/// * `cache_create` - Tokens written to cache (also called cache_write_tokens)
150/// * `cache_read` - Tokens read from cache
151///
152/// # Returns
153///
154/// Total cost in USD
155///
156/// # Examples
157///
158/// ```
159/// use ccboard_core::pricing::calculate_cost;
160///
161/// // Opus-4: 1M input + 1M output
162/// let cost = calculate_cost("opus-4", 1_000_000, 1_000_000, 0, 0);
163/// assert_eq!(cost, 90.0); // $15 input + $75 output
164///
165/// // Sonnet-4: 500K input + 100K output
166/// let cost = calculate_cost("sonnet-4", 500_000, 100_000, 0, 0);
167/// assert_eq!(cost, 3.0); // $1.5 input + $1.5 output
168///
169/// // Opus-4 with cache: 1M input + 1M cache_create + 10M cache_read
170/// let cost = calculate_cost("opus-4", 1_000_000, 0, 1_000_000, 10_000_000);
171/// assert_eq!(cost, 33.75); // $15 + $3.75 + $15
172/// ```
173pub fn calculate_cost(
174 model: &str,
175 input: u64,
176 output: u64,
177 cache_create: u64,
178 cache_read: u64,
179) -> f64 {
180 let pricing = get_pricing(model);
181
182 let input_cost = (input as f64 / 1_000_000.0) * pricing.input_price_per_million;
183 let output_cost = (output as f64 / 1_000_000.0) * pricing.output_price_per_million;
184 let cache_create_cost = (cache_create as f64 / 1_000_000.0)
185 * pricing.input_price_per_million
186 * pricing.cache_write_multiplier;
187 let cache_read_cost = (cache_read as f64 / 1_000_000.0)
188 * pricing.input_price_per_million
189 * pricing.cache_read_multiplier;
190
191 input_cost + output_cost + cache_create_cost + cache_read_cost
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn test_opus_pricing() {
200 let pricing = get_pricing("opus-4");
201 assert_eq!(pricing.input_price_per_million, 15.0);
202 assert_eq!(pricing.output_price_per_million, 75.0);
203 assert_eq!(pricing.cache_read_multiplier, 0.1);
204 assert_eq!(pricing.cache_write_multiplier, 0.25);
205 }
206
207 #[test]
208 fn test_sonnet_pricing() {
209 let pricing = get_pricing("sonnet-4");
210 assert_eq!(pricing.input_price_per_million, 3.0);
211 assert_eq!(pricing.output_price_per_million, 15.0);
212 }
213
214 #[test]
215 fn test_haiku_pricing() {
216 let pricing = get_pricing("haiku-4");
217 assert_eq!(pricing.input_price_per_million, 1.0);
218 assert_eq!(pricing.output_price_per_million, 5.0);
219 }
220
221 #[test]
222 fn test_full_model_id() {
223 let pricing = get_pricing("claude-sonnet-4-5-20250929");
224 assert_eq!(pricing.input_price_per_million, 3.0);
225 }
226
227 #[test]
228 fn test_unknown_model_fallback() {
229 let pricing = get_pricing("unknown-model-xyz");
230 assert_eq!(pricing.input_price_per_million, 3.5); // Default average
231 }
232
233 #[test]
234 fn test_cost_calculation_opus_basic() {
235 // Opus-4: 1M input + 1M output = $15 + $75 = $90
236 let cost = calculate_cost("opus-4", 1_000_000, 1_000_000, 0, 0);
237 assert_eq!(cost, 90.0);
238 }
239
240 #[test]
241 fn test_cost_calculation_sonnet_basic() {
242 // Sonnet-4: 1M input + 1M output = $3 + $15 = $18
243 let cost = calculate_cost("sonnet-4", 1_000_000, 1_000_000, 0, 0);
244 assert_eq!(cost, 18.0);
245 }
246
247 #[test]
248 fn test_cost_calculation_haiku_basic() {
249 // Haiku-4: 1M input + 1M output = $1 + $5 = $6
250 let cost = calculate_cost("haiku-4", 1_000_000, 1_000_000, 0, 0);
251 assert_eq!(cost, 6.0);
252 }
253
254 #[test]
255 fn test_cost_calculation_with_cache() {
256 // Opus-4 with cache: 1M input + 1M cache_create + 10M cache_read
257 // $15 input + ($15 × 0.25) cache_create + ($15 × 0.1 × 10) cache_read
258 // = $15 + $3.75 + $15 = $33.75
259 let cost = calculate_cost("opus-4", 1_000_000, 0, 1_000_000, 10_000_000);
260 assert_eq!(cost, 33.75);
261 }
262
263 #[test]
264 fn test_cost_calculation_zero_tokens() {
265 let cost = calculate_cost("opus-4", 0, 0, 0, 0);
266 assert_eq!(cost, 0.0);
267 }
268
269 #[test]
270 fn test_cost_calculation_small_numbers() {
271 // Sonnet-4: 10K tokens input only = $0.03
272 let cost = calculate_cost("sonnet-4", 10_000, 0, 0, 0);
273 assert_eq!(cost, 0.03);
274 }
275
276 #[test]
277 fn test_cost_calculation_mixed_tokens() {
278 // Sonnet-4: 500K input + 100K output + 50K cache_create + 1M cache_read
279 // Input: (500K / 1M) × $3 = $1.50
280 // Output: (100K / 1M) × $15 = $1.50
281 // Cache create: (50K / 1M) × $3 × 0.25 = $0.0375
282 // Cache read: (1M / 1M) × $3 × 0.1 = $0.30
283 // Total = $3.3375
284 let cost = calculate_cost("sonnet-4", 500_000, 100_000, 50_000, 1_000_000);
285 let expected = 1.5 + 1.5 + 0.0375 + 0.3;
286 assert!((cost - expected).abs() < 0.0001);
287 }
288
289 #[test]
290 fn test_total_tokens_includes_cache_read() {
291 // Verify that total_tokens calculation aligns with pricing
292 let input = 1000u64;
293 let output = 500u64;
294 let cache_create = 100u64;
295 let cache_read = 50000u64;
296
297 let total = input + output + cache_create + cache_read;
298 assert_eq!(total, 51600);
299
300 // Cost should be calculated from all token types
301 let cost = calculate_cost("sonnet-4", input, output, cache_create, cache_read);
302 assert!(cost > 0.0);
303 }
304}