1#[inline]
28fn model_prices(model: &str) -> (f64, f64) {
29 match model {
30 m if m.contains("fable") => (10.0, 50.0),
31 m if m.contains("opus") => (5.0, 25.0),
32 m if m.contains("sonnet") => (3.0, 15.0),
33 m if m.contains("haiku") => (1.0, 5.0),
34 _ => (3.0, 15.0), }
36}
37
38pub fn calculate_cost(
55 model: &str,
56 input_tokens: u64,
57 output_tokens: u64,
58 cache_read: u64,
59 cache_creation: u64,
60) -> f64 {
61 calculate_cost_split(
62 model,
63 input_tokens,
64 output_tokens,
65 cache_read,
66 cache_creation,
67 0,
68 )
69}
70
71pub fn calculate_cost_split(
77 model: &str,
78 input_tokens: u64,
79 output_tokens: u64,
80 cache_read: u64,
81 cache_write_5m: u64,
82 cache_write_1h: u64,
83) -> f64 {
84 let (input_price, output_price) = model_prices(model);
85 (input_tokens as f64 / 1_000_000.0) * input_price
86 + (cache_read as f64 / 1_000_000.0) * input_price * 0.1
87 + (cache_write_5m as f64 / 1_000_000.0) * input_price * 1.25
88 + (cache_write_1h as f64 / 1_000_000.0) * input_price * 2.0
89 + (output_tokens as f64 / 1_000_000.0) * output_price
90}
91
92#[allow(clippy::too_many_arguments)]
97pub fn calculate_cost_optional_split(
98 model: &str,
99 input_tokens: u64,
100 output_tokens: u64,
101 cache_read: u64,
102 cache_creation: u64,
103 cache_creation_5m: Option<u64>,
104 cache_creation_1h: Option<u64>,
105) -> f64 {
106 match (cache_creation_5m, cache_creation_1h) {
107 (None, None) => calculate_cost(
108 model,
109 input_tokens,
110 output_tokens,
111 cache_read,
112 cache_creation,
113 ),
114 (c5, c1) => calculate_cost_split(
115 model,
116 input_tokens,
117 output_tokens,
118 cache_read,
119 c5.unwrap_or(0),
120 c1.unwrap_or(0),
121 ),
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn fable_pricing() {
131 let cost = calculate_cost("claude-fable-5", 1_000_000, 1_000_000, 0, 0);
133 assert!((cost - 60.0).abs() < 1e-9, "expected $60, got ${cost}");
134 }
135
136 #[test]
137 fn opus_pricing() {
138 let cost = calculate_cost("claude-opus-4-5", 1_000_000, 1_000_000, 0, 0);
140 assert!((cost - 30.0).abs() < 1e-9, "expected $30, got ${cost}");
141 }
142
143 #[test]
144 fn sonnet_pricing() {
145 let cost = calculate_cost("claude-sonnet-4-5", 1_000_000, 1_000_000, 0, 0);
147 assert!((cost - 18.0).abs() < 1e-9, "expected $18, got ${cost}");
148 }
149
150 #[test]
151 fn haiku_pricing() {
152 let cost = calculate_cost("claude-haiku-4-5", 1_000_000, 1_000_000, 0, 0);
154 assert!((cost - 6.0).abs() < 1e-9, "expected $6, got ${cost}");
155 }
156
157 #[test]
158 fn cache_read_bills_at_tenth_input_rate() {
159 let cost = calculate_cost("claude-sonnet-4-5", 0, 0, 1_000_000, 0);
161 assert!((cost - 0.30).abs() < 1e-9, "expected $0.30, got ${cost}");
162 }
163
164 #[test]
165 fn cache_creation_bills_at_125_percent_input_rate() {
166 let cost = calculate_cost("claude-sonnet-4-5", 0, 0, 0, 1_000_000);
168 assert!((cost - 3.75).abs() < 1e-9, "expected $3.75, got ${cost}");
169 }
170
171 #[test]
172 fn cache_write_1h_bills_at_double_input_rate() {
173 let cost = calculate_cost_split("claude-sonnet-4-5", 0, 0, 0, 0, 1_000_000);
175 assert!((cost - 6.0).abs() < 1e-9, "expected $6.00, got ${cost}");
176 }
177
178 #[test]
179 fn split_mixes_5m_and_1h_rates() {
180 let cost = calculate_cost_split("claude-sonnet-4-5", 0, 0, 0, 1_000_000, 1_000_000);
182 assert!((cost - 9.75).abs() < 1e-9, "expected $9.75, got ${cost}");
183 }
184
185 #[test]
186 fn wrapper_equals_split_with_zero_1h() {
187 let cases: &[(&str, u64, u64, u64, u64)] = &[
189 ("claude-sonnet-4-5", 1000, 2000, 3000, 4000),
190 ("claude-opus-4-5", 0, 0, 0, 1_000_000),
191 ("claude-haiku-4-5", 123, 456, 789, 1011),
192 ("gpt-99-turbo", 50, 60, 70, 80),
193 ("claude-fable-5", 0, 0, 0, 0),
194 ];
195 for &(m, i, o, r, w) in cases {
196 let a = calculate_cost(m, i, o, r, w);
197 let b = calculate_cost_split(m, i, o, r, w, 0);
198 assert!((a - b).abs() < 1e-12, "{m}: {a} != {b}");
199 }
200 }
201
202 #[test]
203 fn unknown_model_falls_back_to_sonnet() {
204 let cost_unknown = calculate_cost("gpt-99-turbo", 1_000_000, 0, 0, 0);
205 let cost_sonnet = calculate_cost("claude-sonnet-4-5", 1_000_000, 0, 0, 0);
206 assert!((cost_unknown - cost_sonnet).abs() < 1e-9);
207 }
208
209 #[test]
210 fn zero_usage_is_zero_cost() {
211 assert_eq!(calculate_cost("claude-opus-4-5", 0, 0, 0, 0), 0.0);
212 }
213}