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