1use serde::{Deserialize, Serialize};
8use std::fmt;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15#[serde(rename_all = "kebab-case")]
16pub enum Model {
17 #[serde(rename = "claude-opus-4-6")]
19 Opus46,
20
21 #[serde(rename = "claude-opus-4-5-20251101")]
23 Opus45,
24
25 #[serde(rename = "claude-sonnet-4-5-20250929")]
27 Sonnet45,
28
29 #[serde(rename = "claude-sonnet-4-20250514")]
31 Sonnet4,
32
33 #[serde(rename = "claude-haiku-4-5-20251001")]
35 Haiku45,
36
37 #[serde(rename = "claude-3-5-haiku-20241022")]
39 Haiku35,
40
41 #[serde(rename = "claude-3-5-sonnet-20241022")]
43 Sonnet35V2,
44
45 #[serde(other)]
47 Unknown,
48}
49
50impl Model {
51 pub fn display_name(&self) -> &'static str {
56 match self {
57 Self::Opus46 => "Opus 4.6",
58 Self::Opus45 => "Opus 4.5",
59 Self::Sonnet45 => "Sonnet 4.5",
60 Self::Sonnet4 => "Sonnet 4",
61 Self::Haiku45 => "Haiku 4.5",
62 Self::Haiku35 => "Haiku 3.5",
63 Self::Sonnet35V2 => "Sonnet 3.5 v2",
64 Self::Unknown => "Unknown",
65 }
66 }
67
68 pub fn context_window_size(&self) -> u32 {
70 match self {
71 Self::Opus46 => 200_000,
72 Self::Opus45 => 200_000,
73 Self::Sonnet45 => 200_000,
74 Self::Sonnet4 => 200_000,
75 Self::Haiku45 => 200_000,
76 Self::Haiku35 => 200_000,
77 Self::Sonnet35V2 => 200_000,
78 Self::Unknown => 200_000, }
80 }
81
82 pub fn input_cost_per_million(&self) -> f64 {
84 match self {
85 Self::Opus46 => 5.00,
86 Self::Opus45 => 15.00,
87 Self::Sonnet45 => 3.00,
88 Self::Sonnet4 => 3.00,
89 Self::Haiku45 => 1.00,
90 Self::Haiku35 => 0.80,
91 Self::Sonnet35V2 => 3.00,
92 Self::Unknown => 3.00, }
94 }
95
96 pub fn output_cost_per_million(&self) -> f64 {
98 match self {
99 Self::Opus46 => 25.00,
100 Self::Opus45 => 75.00,
101 Self::Sonnet45 => 15.00,
102 Self::Sonnet4 => 15.00,
103 Self::Haiku45 => 5.00,
104 Self::Haiku35 => 4.00,
105 Self::Sonnet35V2 => 15.00,
106 Self::Unknown => 15.00, }
108 }
109
110 pub fn from_id(id: &str) -> Self {
115 if id.starts_with("claude-opus-4-6") {
117 Self::Opus46
118 } else if id.starts_with("claude-opus-4-5") {
119 Self::Opus45
120 } else if id.starts_with("claude-sonnet-4-5") {
121 Self::Sonnet45
122 } else if id.starts_with("claude-sonnet-4") {
123 Self::Sonnet4
124 } else if id.starts_with("claude-haiku-4-5") {
125 Self::Haiku45
126 } else if id.starts_with("claude-3-5-haiku") {
127 Self::Haiku35
128 } else if id.starts_with("claude-3-5-sonnet") {
129 Self::Sonnet35V2
130 } else {
131 Self::Unknown
132 }
133 }
134
135 pub fn is_unknown(&self) -> bool {
137 matches!(self, Self::Unknown)
138 }
139}
140
141pub fn derive_display_name(id: &str) -> String {
149 if id.len() > 9 {
151 let potential_date = &id[id.len() - 8..];
152 if potential_date.chars().all(|c| c.is_ascii_digit()) {
153 if let Some(base) = id[..id.len() - 8].strip_suffix('-') {
154 return base.to_string();
155 }
156 }
157 }
158 id.to_string()
159}
160
161impl Default for Model {
162 fn default() -> Self {
163 Self::Unknown
164 }
165}
166
167impl fmt::Display for Model {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 write!(f, "{}", self.display_name())
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
180 fn test_model_parsing_opus45() {
181 let model: Model = serde_json::from_str("\"claude-opus-4-5-20251101\"").unwrap();
182 assert_eq!(model, Model::Opus45);
183 assert_eq!(model.display_name(), "Opus 4.5");
184 }
185
186 #[test]
187 fn test_model_parsing_opus46() {
188 assert_eq!(Model::from_id("claude-opus-4-6"), Model::Opus46);
190 assert_eq!(Model::Opus46.display_name(), "Opus 4.6");
191 }
192
193 #[test]
194 fn test_model_parsing_sonnet45() {
195 assert_eq!(
196 Model::from_id("claude-sonnet-4-5-20250929"),
197 Model::Sonnet45
198 );
199 assert_eq!(Model::Sonnet45.display_name(), "Sonnet 4.5");
200 }
201
202 #[test]
203 fn test_model_parsing_haiku45() {
204 assert_eq!(Model::from_id("claude-haiku-4-5-20251001"), Model::Haiku45);
205 assert_eq!(Model::Haiku45.display_name(), "Haiku 4.5");
206 }
207
208 #[test]
209 fn test_model_unknown_serde() {
210 let model: Model = serde_json::from_str("\"gpt-4o\"").unwrap();
211 assert_eq!(model, Model::Unknown);
212 }
213
214 #[test]
217 fn test_from_id_prefix_exact() {
218 assert_eq!(Model::from_id("claude-opus-4-5-20251101"), Model::Opus45);
219 assert_eq!(Model::from_id("claude-sonnet-4-20250514"), Model::Sonnet4);
220 }
221
222 #[test]
223 fn test_from_id_prefix_with_different_date() {
224 assert_eq!(Model::from_id("claude-opus-4-6-20260301"), Model::Opus46);
226 assert_eq!(
227 Model::from_id("claude-sonnet-4-5-20261201"),
228 Model::Sonnet45
229 );
230 assert_eq!(Model::from_id("claude-haiku-4-5-20260601"), Model::Haiku45);
231 assert_eq!(Model::from_id("claude-opus-4-5-20260101"), Model::Opus45);
232 }
233
234 #[test]
235 fn test_from_id_prefix_no_date() {
236 assert_eq!(Model::from_id("claude-opus-4-6"), Model::Opus46);
238 assert_eq!(Model::from_id("claude-sonnet-4-5"), Model::Sonnet45);
239 }
240
241 #[test]
242 fn test_from_id_sonnet4_not_confused_with_sonnet45() {
243 assert_eq!(
245 Model::from_id("claude-sonnet-4-5-20250929"),
246 Model::Sonnet45
247 );
248 assert_eq!(Model::from_id("claude-sonnet-4-20250514"), Model::Sonnet4);
250 }
251
252 #[test]
255 fn test_from_id_unknown() {
256 assert_eq!(Model::from_id("gpt-4o"), Model::Unknown);
257 assert_eq!(Model::from_id("gemini-1.5-pro"), Model::Unknown);
258 assert_eq!(Model::from_id("llama-3-70b"), Model::Unknown);
259 assert_eq!(Model::from_id("unknown-model"), Model::Unknown);
260 }
261
262 #[test]
263 fn test_is_unknown() {
264 assert!(Model::Unknown.is_unknown());
265 assert!(!Model::Opus46.is_unknown());
266 }
267
268 #[test]
271 fn test_derive_display_name_strips_date() {
272 assert_eq!(
273 derive_display_name("claude-opus-4-7-20260501"),
274 "claude-opus-4-7"
275 );
276 assert_eq!(
277 derive_display_name("claude-sonnet-5-20270101"),
278 "claude-sonnet-5"
279 );
280 }
281
282 #[test]
283 fn test_derive_display_name_no_date() {
284 assert_eq!(derive_display_name("gpt-4o"), "gpt-4o");
285 assert_eq!(derive_display_name("gemini-1.5-pro"), "gemini-1.5-pro");
286 }
287
288 #[test]
289 fn test_derive_display_name_short_ids() {
290 assert_eq!(derive_display_name("gpt-4"), "gpt-4");
291 assert_eq!(derive_display_name("o1"), "o1");
292 }
293}