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(model, input_tokens, output_tokens, cache_read, cache_creation, 0)
62}
63
64pub fn calculate_cost_split(
70 model: &str,
71 input_tokens: u64,
72 output_tokens: u64,
73 cache_read: u64,
74 cache_write_5m: u64,
75 cache_write_1h: u64,
76) -> f64 {
77 let (input_price, output_price) = model_prices(model);
78 (input_tokens as f64 / 1_000_000.0) * input_price
79 + (cache_read as f64 / 1_000_000.0) * input_price * 0.1
80 + (cache_write_5m as f64 / 1_000_000.0) * input_price * 1.25
81 + (cache_write_1h as f64 / 1_000_000.0) * input_price * 2.0
82 + (output_tokens as f64 / 1_000_000.0) * output_price
83}
84
85#[allow(clippy::too_many_arguments)]
90pub fn calculate_cost_optional_split(
91 model: &str,
92 input_tokens: u64,
93 output_tokens: u64,
94 cache_read: u64,
95 cache_creation: u64,
96 cache_creation_5m: Option<u64>,
97 cache_creation_1h: Option<u64>,
98) -> f64 {
99 match (cache_creation_5m, cache_creation_1h) {
100 (None, None) => calculate_cost(model, input_tokens, output_tokens, cache_read, cache_creation),
101 (c5, c1) => calculate_cost_split(
102 model, input_tokens, output_tokens, cache_read,
103 c5.unwrap_or(0), c1.unwrap_or(0),
104 ),
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn fable_pricing() {
114 let cost = calculate_cost("claude-fable-5", 1_000_000, 1_000_000, 0, 0);
116 assert!((cost - 60.0).abs() < 1e-9, "expected $60, got ${cost}");
117 }
118
119 #[test]
120 fn opus_pricing() {
121 let cost = calculate_cost("claude-opus-4-5", 1_000_000, 1_000_000, 0, 0);
123 assert!((cost - 30.0).abs() < 1e-9, "expected $30, got ${cost}");
124 }
125
126 #[test]
127 fn sonnet_pricing() {
128 let cost = calculate_cost("claude-sonnet-4-5", 1_000_000, 1_000_000, 0, 0);
130 assert!((cost - 18.0).abs() < 1e-9, "expected $18, got ${cost}");
131 }
132
133 #[test]
134 fn haiku_pricing() {
135 let cost = calculate_cost("claude-haiku-4-5", 1_000_000, 1_000_000, 0, 0);
137 assert!((cost - 6.0).abs() < 1e-9, "expected $6, got ${cost}");
138 }
139
140 #[test]
141 fn cache_read_bills_at_tenth_input_rate() {
142 let cost = calculate_cost("claude-sonnet-4-5", 0, 0, 1_000_000, 0);
144 assert!((cost - 0.30).abs() < 1e-9, "expected $0.30, got ${cost}");
145 }
146
147 #[test]
148 fn cache_creation_bills_at_125_percent_input_rate() {
149 let cost = calculate_cost("claude-sonnet-4-5", 0, 0, 0, 1_000_000);
151 assert!((cost - 3.75).abs() < 1e-9, "expected $3.75, got ${cost}");
152 }
153
154 #[test]
155 fn cache_write_1h_bills_at_double_input_rate() {
156 let cost = calculate_cost_split("claude-sonnet-4-5", 0, 0, 0, 0, 1_000_000);
158 assert!((cost - 6.0).abs() < 1e-9, "expected $6.00, got ${cost}");
159 }
160
161 #[test]
162 fn split_mixes_5m_and_1h_rates() {
163 let cost = calculate_cost_split("claude-sonnet-4-5", 0, 0, 0, 1_000_000, 1_000_000);
165 assert!((cost - 9.75).abs() < 1e-9, "expected $9.75, got ${cost}");
166 }
167
168 #[test]
169 fn wrapper_equals_split_with_zero_1h() {
170 let cases: &[(&str, u64, u64, u64, u64)] = &[
172 ("claude-sonnet-4-5", 1000, 2000, 3000, 4000),
173 ("claude-opus-4-5", 0, 0, 0, 1_000_000),
174 ("claude-haiku-4-5", 123, 456, 789, 1011),
175 ("gpt-99-turbo", 50, 60, 70, 80),
176 ("claude-fable-5", 0, 0, 0, 0),
177 ];
178 for &(m, i, o, r, w) in cases {
179 let a = calculate_cost(m, i, o, r, w);
180 let b = calculate_cost_split(m, i, o, r, w, 0);
181 assert!((a - b).abs() < 1e-12, "{m}: {a} != {b}");
182 }
183 }
184
185 #[test]
186 fn unknown_model_falls_back_to_sonnet() {
187 let cost_unknown = calculate_cost("gpt-99-turbo", 1_000_000, 0, 0, 0);
188 let cost_sonnet = calculate_cost("claude-sonnet-4-5", 1_000_000, 0, 0, 0);
189 assert!((cost_unknown - cost_sonnet).abs() < 1e-9);
190 }
191
192 #[test]
193 fn zero_usage_is_zero_cost() {
194 assert_eq!(calculate_cost("claude-opus-4-5", 0, 0, 0, 0), 0.0);
195 }
196}