Skip to main content

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}