1use std::{collections::VecDeque, str::FromStr};
2
3use chrono::NaiveDate;
4use derive_more::derive::Display;
5use serde::{Deserialize, Serialize};
6
7pub mod error;
8pub mod llm;
9
10pub mod openai {
11 pub use async_openai::*;
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize, Display)]
16pub enum OpenAIModel {
17 #[display("gpt-4o")]
18 GPT4O,
19 #[display("gpt-4o-mini")]
20 GPT4OMINI,
21 #[display("o1")]
22 O1,
23 #[display("o1-mini")]
24 O1MINI,
25 #[display("gpt-3.5-turbo")]
26 GPT35TURBO,
27 #[display("gpt-4")]
28 GPT4,
29 #[display("gpt-4-turbo")]
30 GPT4TURBO,
31 #[display("gpt-5-mini")]
32 GPT5MINI,
33 #[display("gpt-5-nano")]
34 GPT5NANO,
35 #[display("gpt-5.2")]
36 GPT52,
37 #[display("gpt-5")]
38 GPT5,
39 #[display("gpt-5.1")]
40 GPT51,
41 #[display("gpt-5-pro")]
42 GPT5PRO,
43 #[display("gpt-4.1")]
44 GPT41,
45 #[display("gpt-4.1-mini")]
46 GPT41MINI,
47 #[display("gpt-4.1-nano")]
48 GPT41NANO,
49 #[display("o3")]
50 O3,
51 #[display("o4-mini")]
52 O4MINI,
53 #[display("o3-mini")]
54 O3MINI,
55 #[display("o3-pro")]
56 O3PRO,
57 #[display("gemini-3-pro-preview")]
58 GEMINI3PRO,
59 #[display("gemini-3-flash-preview")]
60 GEMINI3FLASH,
61 #[display("gemini-2.5-pro")]
62 GEMINI25PRO,
63 #[display("gemini-2.5-flash")]
64 GEMINI25FLASH,
65 #[display("{_0}")]
66 Other(String, PricingInfo),
67}
68
69impl FromStr for OpenAIModel {
70 type Err = String;
71 fn from_str(s: &str) -> Result<Self, Self::Err> {
72 match s {
73 "gpt-4o" | "gpt4o" => Ok(Self::GPT4O),
74 "gpt-4" | "gpt" => Ok(Self::GPT4),
75 "gpt-4-turbo" | "gpt4turbo" => Ok(Self::GPT4TURBO),
76 "gpt-4o-mini" | "gpt4omini" => Ok(Self::GPT4OMINI),
77 "o1" => Ok(Self::O1),
78 "o1-mini" => Ok(Self::O1MINI),
79 "gpt-3.5-turbo" | "gpt3.5turbo" => Ok(Self::GPT35TURBO),
80 "gpt-5.2" => Ok(Self::GPT52),
81 "gpt-5-mini" | "gpt-5mini" => Ok(Self::GPT5MINI),
82 "gpt-5" => Ok(Self::GPT5),
83 "gpt-5-nano" | "gpt-5nano" => Ok(Self::GPT5NANO),
84 "gpt-5.1" => Ok(Self::GPT51),
85 "gpt-5-pro" | "gpt5pro" => Ok(Self::GPT5PRO),
86 "gpt-4.1" | "gpt41" => Ok(Self::GPT41),
87 "gpt-4.1-mini" | "gpt41mini" => Ok(Self::GPT41MINI),
88 "gpt-4.1-nano" | "gpt41nano" => Ok(Self::GPT41NANO),
89 "o3" => Ok(Self::O3),
90 "o4-mini" | "o4mini" => Ok(Self::O4MINI),
91 "o3-mini" | "o3mini" => Ok(Self::O3MINI),
92 "o3-pro" | "o3pro" => Ok(Self::O3PRO),
93 "gemini-3-pro-preview" | "gemini-3-pro" => Ok(Self::GEMINI3PRO),
94 "gemini-3-flash-preview" | "gemini-3-flash" => Ok(Self::GEMINI3FLASH),
95 "gemini-2.5-pro" => Ok(Self::GEMINI25PRO),
96 "gemini-2.5-flash" => Ok(Self::GEMINI25FLASH),
97 _ => {
98 if !s.contains(",") {
99 return Ok(Self::Other(
100 s.to_string(),
101 PricingInfo {
102 input_tokens: 0.0f64,
103 output_tokens: 0.0f64,
104 cached_input_tokens: None,
105 },
106 ));
107 }
108 let mut tks = s
109 .split(",")
110 .map(|t| t.to_string())
111 .collect::<VecDeque<String>>();
112
113 if tks.len() >= 2 {
114 let model = tks.pop_front().unwrap();
115 let tks = tks
116 .into_iter()
117 .map(|t| f64::from_str(&t))
118 .collect::<Result<Vec<f64>, _>>()
119 .map_err(|e| e.to_string())?;
120
121 let pricing = if tks.len() == 2 {
122 PricingInfo {
123 input_tokens: tks[0],
124 output_tokens: tks[1],
125 cached_input_tokens: None,
126 }
127 } else if tks.len() == 3 {
128 PricingInfo {
129 input_tokens: tks[0],
130 output_tokens: tks[1],
131 cached_input_tokens: Some(tks[2]),
132 }
133 } else {
134 return Err("fail to parse pricing".to_string());
135 };
136
137 Ok(Self::Other(model, pricing))
138 } else {
139 Err("unreconigized model".to_string())
140 }
141 }
142 }
143 }
144}
145
146#[derive(Copy, Debug, Clone, Serialize, Deserialize)]
149pub struct PricingInfo {
150 pub input_tokens: f64,
151 pub output_tokens: f64,
152 pub cached_input_tokens: Option<f64>,
153}
154
155impl FromStr for PricingInfo {
156 type Err = String;
157
158 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
159 let tks = s
160 .split(",")
161 .map(f64::from_str)
162 .collect::<Result<Vec<f64>, _>>()
163 .map_err(|e| e.to_string())?;
164
165 if tks.len() == 2 {
166 Ok(PricingInfo {
167 input_tokens: tks[0],
168 output_tokens: tks[1],
169 cached_input_tokens: None,
170 })
171 } else if tks.len() == 3 {
172 Ok(PricingInfo {
173 input_tokens: tks[0],
174 output_tokens: tks[1],
175 cached_input_tokens: Some(tks[2]),
176 })
177 } else {
178 Err("fail to parse pricing".to_string())
179 }
180 }
181}
182
183#[derive(Copy, Debug, Clone)]
185pub struct ModelInfo {
186 pub context_window: u64,
188 pub max_output_tokens: u64,
190 pub knowledge_cutoff: NaiveDate,
192}
193
194impl OpenAIModel {
195 pub fn pricing(&self) -> PricingInfo {
196 match self {
197 Self::GPT4O => PricingInfo {
198 input_tokens: 2.5,
199 output_tokens: 10.00,
200 cached_input_tokens: Some(1.25),
201 },
202 Self::GPT4OMINI => PricingInfo {
203 input_tokens: 0.15,
204 cached_input_tokens: Some(0.075),
205 output_tokens: 0.6,
206 },
207 Self::O1 => PricingInfo {
208 input_tokens: 15.00,
209 cached_input_tokens: Some(7.5),
210 output_tokens: 60.00,
211 },
212 Self::O1MINI => PricingInfo {
213 input_tokens: 1.10,
214 cached_input_tokens: Some(0.55),
215 output_tokens: 4.40,
216 },
217 Self::GPT35TURBO => PricingInfo {
218 input_tokens: 0.50,
219 cached_input_tokens: None,
220 output_tokens: 1.50,
221 },
222 Self::GPT4 => PricingInfo {
223 input_tokens: 30.0,
224 output_tokens: 60.0,
225 cached_input_tokens: None,
226 },
227 Self::GPT4TURBO => PricingInfo {
228 input_tokens: 10.0,
229 output_tokens: 30.0,
230 cached_input_tokens: None,
231 },
232 Self::GPT52 => PricingInfo {
233 input_tokens: 1.75,
234 output_tokens: 14.00,
235 cached_input_tokens: Some(0.175),
236 },
237 Self::GPT5MINI => PricingInfo {
238 input_tokens: 0.25,
239 output_tokens: 2.00,
240 cached_input_tokens: Some(0.025),
241 },
242 Self::GPT5NANO => PricingInfo {
243 input_tokens: 0.05,
244 output_tokens: 0.40,
245 cached_input_tokens: Some(0.005),
246 },
247 Self::GPT5 => PricingInfo {
248 input_tokens: 1.25,
249 output_tokens: 10.0,
250 cached_input_tokens: Some(0.125),
251 },
252 Self::GPT51 => PricingInfo {
253 input_tokens: 1.25,
254 output_tokens: 10.00,
255 cached_input_tokens: Some(0.125),
256 },
257 Self::GPT5PRO => PricingInfo {
258 input_tokens: 15.00,
259 output_tokens: 120.00,
260 cached_input_tokens: None,
261 },
262 Self::GPT41 => PricingInfo {
263 input_tokens: 2.00,
264 output_tokens: 8.00,
265 cached_input_tokens: Some(0.50),
266 },
267 Self::GPT41MINI => PricingInfo {
268 input_tokens: 0.40,
269 output_tokens: 1.60,
270 cached_input_tokens: Some(0.10),
271 },
272 Self::GPT41NANO => PricingInfo {
273 input_tokens: 0.10,
274 output_tokens: 0.40,
275 cached_input_tokens: Some(0.025),
276 },
277 Self::O3 => PricingInfo {
278 input_tokens: 2.00,
279 output_tokens: 8.00,
280 cached_input_tokens: Some(0.50),
281 },
282 Self::O4MINI => PricingInfo {
283 input_tokens: 1.10,
284 output_tokens: 4.40,
285 cached_input_tokens: Some(0.275),
286 },
287 Self::O3MINI => PricingInfo {
288 input_tokens: 1.10,
289 output_tokens: 4.40,
290 cached_input_tokens: Some(0.55),
291 },
292 Self::O3PRO => PricingInfo {
293 input_tokens: 20.00,
294 output_tokens: 80.00,
295 cached_input_tokens: None,
296 },
297 Self::GEMINI3PRO => PricingInfo {
298 input_tokens: 2.00, output_tokens: 12.00, cached_input_tokens: None,
301 },
302 Self::GEMINI3FLASH => PricingInfo {
303 input_tokens: 0.50,
304 output_tokens: 3.0,
305 cached_input_tokens: None,
306 },
307 Self::GEMINI25PRO => PricingInfo {
308 input_tokens: 1.25, output_tokens: 10.00, cached_input_tokens: None,
311 },
312 Self::GEMINI25FLASH => PricingInfo {
313 input_tokens: 0.30,
314 output_tokens: 2.50,
315 cached_input_tokens: None,
316 },
317 Self::Other(_, pricing) => *pricing,
318 }
319 }
320
321 pub fn batch_pricing(&self) -> Option<PricingInfo> {
322 match self {
323 Self::GPT4O => Some(PricingInfo {
324 input_tokens: 1.25,
325 output_tokens: 5.00,
326 cached_input_tokens: None,
327 }),
328 Self::GPT4OMINI => Some(PricingInfo {
329 input_tokens: 0.075,
330 output_tokens: 0.30,
331 cached_input_tokens: None,
332 }),
333 Self::GPT41 => Some(PricingInfo {
334 input_tokens: 1.00,
335 output_tokens: 4.00,
336 cached_input_tokens: Some(0.25),
337 }),
338 Self::GPT41MINI => Some(PricingInfo {
339 input_tokens: 0.20,
340 output_tokens: 0.80,
341 cached_input_tokens: Some(0.05),
342 }),
343 Self::GPT41NANO => Some(PricingInfo {
344 input_tokens: 0.05,
345 output_tokens: 0.20,
346 cached_input_tokens: Some(0.0125),
347 }),
348 Self::O3 => Some(PricingInfo {
349 input_tokens: 1.00,
350 output_tokens: 4.00,
351 cached_input_tokens: Some(0.25),
352 }),
353 Self::O4MINI => Some(PricingInfo {
354 input_tokens: 0.55,
355 output_tokens: 2.20,
356 cached_input_tokens: Some(0.1375),
357 }),
358 Self::O3MINI => Some(PricingInfo {
359 input_tokens: 0.55,
360 output_tokens: 2.20,
361 cached_input_tokens: Some(0.275),
362 }),
363 _ => None,
364 }
365 }
366
367 pub fn info(&self) -> Option<ModelInfo> {
369 match self {
370 Self::GPT52 => Some(ModelInfo {
371 context_window: 400_000,
372 max_output_tokens: 128_000,
373 knowledge_cutoff: NaiveDate::from_ymd_opt(2025, 8, 31).unwrap(),
374 }),
375 Self::GPT51 => Some(ModelInfo {
376 context_window: 400_000,
377 max_output_tokens: 128_000,
378 knowledge_cutoff: NaiveDate::from_ymd_opt(2024, 9, 30).unwrap(),
379 }),
380 Self::GPT5 => Some(ModelInfo {
381 context_window: 400_000,
382 max_output_tokens: 128_000,
383 knowledge_cutoff: NaiveDate::from_ymd_opt(2024, 9, 30).unwrap(),
384 }),
385 Self::GPT5MINI => Some(ModelInfo {
386 context_window: 400_000,
387 max_output_tokens: 128_000,
388 knowledge_cutoff: NaiveDate::from_ymd_opt(2024, 5, 31).unwrap(),
389 }),
390 Self::GPT5NANO => Some(ModelInfo {
391 context_window: 400_000,
392 max_output_tokens: 128_000,
393 knowledge_cutoff: NaiveDate::from_ymd_opt(2024, 5, 31).unwrap(),
394 }),
395 Self::GPT5PRO => Some(ModelInfo {
396 context_window: 400_000,
397 max_output_tokens: 272_000,
398 knowledge_cutoff: NaiveDate::from_ymd_opt(2024, 9, 30).unwrap(),
399 }),
400 Self::GPT41 => Some(ModelInfo {
401 context_window: 1_047_576,
402 max_output_tokens: 32_768,
403 knowledge_cutoff: NaiveDate::from_ymd_opt(2024, 6, 1).unwrap(),
404 }),
405 Self::GPT41MINI => Some(ModelInfo {
406 context_window: 1_047_576,
407 max_output_tokens: 32_768,
408 knowledge_cutoff: NaiveDate::from_ymd_opt(2024, 6, 1).unwrap(),
409 }),
410 Self::GPT41NANO => Some(ModelInfo {
411 context_window: 1_047_576,
412 max_output_tokens: 32_768,
413 knowledge_cutoff: NaiveDate::from_ymd_opt(2024, 6, 1).unwrap(),
414 }),
415 Self::O3 => Some(ModelInfo {
416 context_window: 200_000,
417 max_output_tokens: 100_000,
418 knowledge_cutoff: NaiveDate::from_ymd_opt(2024, 6, 1).unwrap(),
419 }),
420 Self::O4MINI => Some(ModelInfo {
421 context_window: 200_000,
422 max_output_tokens: 100_000,
423 knowledge_cutoff: NaiveDate::from_ymd_opt(2024, 6, 1).unwrap(),
424 }),
425 Self::O3MINI => Some(ModelInfo {
426 context_window: 200_000,
427 max_output_tokens: 100_000,
428 knowledge_cutoff: NaiveDate::from_ymd_opt(2023, 10, 1).unwrap(),
429 }),
430 Self::O3PRO => Some(ModelInfo {
431 context_window: 200_000,
432 max_output_tokens: 100_000,
433 knowledge_cutoff: NaiveDate::from_ymd_opt(2024, 6, 1).unwrap(),
434 }),
435 Self::O1 => Some(ModelInfo {
436 context_window: 200_000,
437 max_output_tokens: 100_000,
438 knowledge_cutoff: NaiveDate::from_ymd_opt(2023, 10, 1).unwrap(),
439 }),
440 Self::O1MINI => Some(ModelInfo {
441 context_window: 128_000,
442 max_output_tokens: 65_536,
443 knowledge_cutoff: NaiveDate::from_ymd_opt(2023, 10, 1).unwrap(),
444 }),
445 Self::GPT4O => Some(ModelInfo {
446 context_window: 128_000,
447 max_output_tokens: 16_384,
448 knowledge_cutoff: NaiveDate::from_ymd_opt(2023, 10, 1).unwrap(),
449 }),
450 Self::GPT4OMINI => Some(ModelInfo {
451 context_window: 128_000,
452 max_output_tokens: 16_384,
453 knowledge_cutoff: NaiveDate::from_ymd_opt(2023, 10, 1).unwrap(),
454 }),
455 Self::GPT4 => Some(ModelInfo {
456 context_window: 8_192,
457 max_output_tokens: 8_192,
458 knowledge_cutoff: NaiveDate::from_ymd_opt(2023, 12, 1).unwrap(),
459 }),
460 Self::GPT4TURBO => Some(ModelInfo {
461 context_window: 128_000,
462 max_output_tokens: 4_096,
463 knowledge_cutoff: NaiveDate::from_ymd_opt(2023, 12, 1).unwrap(),
464 }),
465 Self::GPT35TURBO => Some(ModelInfo {
466 context_window: 16_385,
467 max_output_tokens: 4_096,
468 knowledge_cutoff: NaiveDate::from_ymd_opt(2021, 9, 1).unwrap(),
469 }),
470 _ => None,
471 }
472 }
473}