1use serde::{Deserialize, Deserializer, Serialize, Serializer};
14use std::fmt;
15
16#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18pub enum ClaudeModel {
19 Sonnet,
22 Opus,
25 Haiku,
29 Fable,
33 Best,
36 OpusPlan,
38 Sonnet1m,
40 Opus1m,
42 Fable1m,
44 Fable52,
47 Fable51,
49 Fable5,
51 Mythos52,
54 Mythos51,
56 Mythos5,
58 Opus55,
61 Opus5,
63 Opus48,
65 Opus47,
67 Opus46,
69 Opus45,
71 Opus41,
73 Opus40,
75 Sonnet55,
78 Sonnet5,
80 Sonnet46,
82 Sonnet45,
84 Sonnet40,
86 Sonnet37,
88 Sonnet35,
90 Haiku55,
93 Haiku45,
95 Haiku35,
97 Custom(String),
100}
101
102impl ClaudeModel {
103 pub fn cli_arg(&self) -> &str {
105 match self {
106 Self::Sonnet => "sonnet",
107 Self::Opus => "opus",
108 Self::Haiku => "haiku",
109 Self::Fable => "fable",
110 Self::Best => "best",
111 Self::OpusPlan => "opusplan",
112 Self::Sonnet1m => "sonnet[1m]",
113 Self::Opus1m => "opus[1m]",
114 Self::Fable1m => "fable[1m]",
115 Self::Fable52 => "claude-fable-5-2",
116 Self::Fable51 => "claude-fable-5-1",
117 Self::Fable5 => "claude-fable-5",
118 Self::Mythos52 => "claude-mythos-5-2",
119 Self::Mythos51 => "claude-mythos-5-1",
120 Self::Mythos5 => "claude-mythos-5",
121 Self::Opus55 => "claude-opus-5-5",
122 Self::Opus5 => "claude-opus-5",
123 Self::Opus48 => "claude-opus-4-8",
124 Self::Opus47 => "claude-opus-4-7",
125 Self::Opus46 => "claude-opus-4-6",
126 Self::Opus45 => "claude-opus-4-5",
127 Self::Opus41 => "claude-opus-4-1",
128 Self::Opus40 => "claude-opus-4-0",
129 Self::Sonnet55 => "claude-sonnet-5-5",
130 Self::Sonnet5 => "claude-sonnet-5",
131 Self::Sonnet46 => "claude-sonnet-4-6",
132 Self::Sonnet45 => "claude-sonnet-4-5",
133 Self::Sonnet40 => "claude-sonnet-4-0",
134 Self::Sonnet37 => "claude-3-7-sonnet",
135 Self::Sonnet35 => "claude-3-5-sonnet",
136 Self::Haiku55 => "claude-haiku-5-5",
137 Self::Haiku45 => "claude-haiku-4-5",
138 Self::Haiku35 => "claude-3-5-haiku",
139 Self::Custom(s) => s.as_str(),
140 }
141 }
142
143 pub fn as_str(&self) -> &str {
146 self.cli_arg()
147 }
148
149 pub fn display_name(&self) -> &str {
151 match self {
152 Self::Sonnet => "Sonnet (latest)",
153 Self::Opus => "Opus (latest)",
154 Self::Haiku => "Haiku (latest)",
155 Self::Fable => "Fable (latest)",
156 Self::Best => "Best available",
157 Self::OpusPlan => "Opus (plan mode) / Sonnet",
158 Self::Sonnet1m => "Sonnet (latest, 1M context)",
159 Self::Opus1m => "Opus (latest, 1M context)",
160 Self::Fable1m => "Fable (latest, 1M context)",
161 Self::Fable52 => "Fable 5.2",
162 Self::Fable51 => "Fable 5.1",
163 Self::Fable5 => "Fable 5",
164 Self::Mythos52 => "Mythos 5.2",
165 Self::Mythos51 => "Mythos 5.1",
166 Self::Mythos5 => "Mythos 5",
167 Self::Opus55 => "Opus 5.5",
168 Self::Opus5 => "Opus 5",
169 Self::Opus48 => "Opus 4.8",
170 Self::Opus47 => "Opus 4.7",
171 Self::Opus46 => "Opus 4.6",
172 Self::Opus45 => "Opus 4.5",
173 Self::Opus41 => "Opus 4.1",
174 Self::Opus40 => "Opus 4",
175 Self::Sonnet55 => "Sonnet 5.5",
176 Self::Sonnet5 => "Sonnet 5",
177 Self::Sonnet46 => "Sonnet 4.6",
178 Self::Sonnet45 => "Sonnet 4.5",
179 Self::Sonnet40 => "Sonnet 4",
180 Self::Sonnet37 => "Sonnet 3.7",
181 Self::Sonnet35 => "Sonnet 3.5",
182 Self::Haiku55 => "Haiku 5.5",
183 Self::Haiku45 => "Haiku 4.5",
184 Self::Haiku35 => "Haiku 3.5",
185 Self::Custom(s) => s.as_str(),
186 }
187 }
188
189 pub fn is_alias(&self) -> bool {
192 matches!(
193 self,
194 Self::Sonnet
195 | Self::Opus
196 | Self::Haiku
197 | Self::Fable
198 | Self::Best
199 | Self::OpusPlan
200 | Self::Sonnet1m
201 | Self::Opus1m
202 | Self::Fable1m
203 )
204 }
205
206 pub fn known() -> &'static [ClaudeModel] {
208 &[
209 Self::Sonnet,
210 Self::Opus,
211 Self::Haiku,
212 Self::Fable,
213 Self::Best,
214 Self::OpusPlan,
215 Self::Sonnet1m,
216 Self::Opus1m,
217 Self::Fable1m,
218 Self::Fable52,
219 Self::Fable51,
220 Self::Fable5,
221 Self::Mythos52,
222 Self::Mythos51,
223 Self::Mythos5,
224 Self::Opus55,
225 Self::Opus5,
226 Self::Opus48,
227 Self::Opus47,
228 Self::Opus46,
229 Self::Opus45,
230 Self::Opus41,
231 Self::Opus40,
232 Self::Sonnet55,
233 Self::Sonnet5,
234 Self::Sonnet46,
235 Self::Sonnet45,
236 Self::Sonnet40,
237 Self::Sonnet37,
238 Self::Sonnet35,
239 Self::Haiku55,
240 Self::Haiku45,
241 Self::Haiku35,
242 ]
243 }
244}
245
246impl fmt::Display for ClaudeModel {
247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248 f.write_str(self.cli_arg())
249 }
250}
251
252impl From<&str> for ClaudeModel {
253 fn from(s: &str) -> Self {
254 match s {
255 "sonnet" => Self::Sonnet,
256 "opus" => Self::Opus,
257 "haiku" => Self::Haiku,
258 "fable" => Self::Fable,
259 "best" => Self::Best,
260 "opusplan" => Self::OpusPlan,
261 "sonnet[1m]" => Self::Sonnet1m,
262 "opus[1m]" => Self::Opus1m,
263 "fable[1m]" => Self::Fable1m,
264 "claude-fable-5-2" => Self::Fable52,
265 "claude-fable-5-1" => Self::Fable51,
266 "claude-fable-5" => Self::Fable5,
267 "claude-mythos-5-2" => Self::Mythos52,
268 "claude-mythos-5-1" => Self::Mythos51,
269 "claude-mythos-5" => Self::Mythos5,
270 "claude-opus-5-5" => Self::Opus55,
271 "claude-opus-5" => Self::Opus5,
272 "claude-opus-4-8" => Self::Opus48,
273 "claude-opus-4-7" => Self::Opus47,
274 "claude-opus-4-6" => Self::Opus46,
275 "claude-opus-4-5" => Self::Opus45,
276 "claude-opus-4-1" => Self::Opus41,
277 "claude-opus-4-0" => Self::Opus40,
278 "claude-sonnet-5-5" => Self::Sonnet55,
279 "claude-sonnet-5" => Self::Sonnet5,
280 "claude-sonnet-4-6" => Self::Sonnet46,
281 "claude-sonnet-4-5" => Self::Sonnet45,
282 "claude-sonnet-4-0" => Self::Sonnet40,
283 "claude-3-7-sonnet" => Self::Sonnet37,
284 "claude-3-5-sonnet" => Self::Sonnet35,
285 "claude-haiku-5-5" => Self::Haiku55,
286 "claude-haiku-4-5" => Self::Haiku45,
287 "claude-3-5-haiku" => Self::Haiku35,
288 other => Self::Custom(other.to_string()),
289 }
290 }
291}
292
293impl From<ClaudeModel> for String {
294 fn from(model: ClaudeModel) -> Self {
295 model.cli_arg().to_string()
296 }
297}
298
299impl Serialize for ClaudeModel {
300 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
301 serializer.serialize_str(self.cli_arg())
302 }
303}
304
305impl<'de> Deserialize<'de> for ClaudeModel {
306 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
307 let s = String::deserialize(deserializer)?;
308 Ok(Self::from(s.as_str()))
309 }
310}
311
312#[derive(Debug, Clone, PartialEq, Eq, Hash)]
321pub enum EffortLevel {
322 Low,
323 Medium,
324 High,
325 Xhigh,
328 Max,
329 Unknown(String),
331}
332
333impl EffortLevel {
334 pub fn as_str(&self) -> &str {
336 match self {
337 Self::Low => "low",
338 Self::Medium => "medium",
339 Self::High => "high",
340 Self::Xhigh => "xhigh",
341 Self::Max => "max",
342 Self::Unknown(s) => s.as_str(),
343 }
344 }
345
346 pub fn known() -> &'static [EffortLevel] {
348 &[Self::Low, Self::Medium, Self::High, Self::Xhigh, Self::Max]
349 }
350}
351
352impl fmt::Display for EffortLevel {
353 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
354 f.write_str(self.as_str())
355 }
356}
357
358impl From<&str> for EffortLevel {
359 fn from(s: &str) -> Self {
360 match s {
361 "low" => Self::Low,
362 "medium" => Self::Medium,
363 "high" => Self::High,
364 "xhigh" => Self::Xhigh,
365 "max" => Self::Max,
366 other => Self::Unknown(other.to_string()),
367 }
368 }
369}
370
371impl From<EffortLevel> for String {
372 fn from(level: EffortLevel) -> Self {
373 level.as_str().to_string()
374 }
375}
376
377impl Serialize for EffortLevel {
378 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
379 serializer.serialize_str(self.as_str())
380 }
381}
382
383impl<'de> Deserialize<'de> for EffortLevel {
384 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
385 let s = String::deserialize(deserializer)?;
386 Ok(Self::from(s.as_str()))
387 }
388}
389
390#[cfg(test)]
391mod effort_tests {
392 use super::EffortLevel;
393
394 #[test]
395 fn effort_level_round_trips_known_and_unknown() {
396 for level in EffortLevel::known() {
397 assert_eq!(&EffortLevel::from(level.as_str()), level);
398 let json = serde_json::to_string(level).unwrap();
399 assert_eq!(&serde_json::from_str::<EffortLevel>(&json).unwrap(), level);
400 }
401 let future: EffortLevel = serde_json::from_str(r#""ultra""#).unwrap();
402 assert_eq!(future, EffortLevel::Unknown("ultra".to_string()));
403 assert_eq!(serde_json::to_string(&future).unwrap(), r#""ultra""#);
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use super::ClaudeModel;
410
411 #[test]
412 fn test_cli_arg_round_trip() {
413 for model in ClaudeModel::known() {
414 assert_eq!(&ClaudeModel::from(model.cli_arg()), model);
415 }
416 assert_eq!(
417 ClaudeModel::from("claude-nova-7"),
418 ClaudeModel::Custom("claude-nova-7".to_string())
419 );
420 assert_eq!(
421 ClaudeModel::from("claude-nova-7").cli_arg(),
422 "claude-nova-7"
423 );
424 }
425
426 #[test]
427 fn test_into_string_matches_cli_arg() {
428 let s: String = ClaudeModel::Sonnet5.into();
429 assert_eq!(s, "claude-sonnet-5");
430 let s: String = ClaudeModel::Opus1m.into();
431 assert_eq!(s, "opus[1m]");
432 }
433
434 #[test]
435 fn test_display_names() {
436 assert_eq!(ClaudeModel::Opus5.display_name(), "Opus 5");
437 assert_eq!(ClaudeModel::Opus5.cli_arg(), "claude-opus-5");
438 assert_eq!(ClaudeModel::Fable5.display_name(), "Fable 5");
439 assert_eq!(ClaudeModel::Opus48.display_name(), "Opus 4.8");
440 assert_eq!(ClaudeModel::Sonnet.display_name(), "Sonnet (latest)");
441 assert!(ClaudeModel::Sonnet.is_alias());
442 assert!(!ClaudeModel::Sonnet5.is_alias());
443 }
444
445 #[test]
446 fn test_serde_round_trip() {
447 let json = serde_json::to_string(&ClaudeModel::Haiku45).unwrap();
448 assert_eq!(json, "\"claude-haiku-4-5\"");
449 let back: ClaudeModel = serde_json::from_str(&json).unwrap();
450 assert_eq!(back, ClaudeModel::Haiku45);
451
452 let json = serde_json::to_string(&ClaudeModel::Haiku55).unwrap();
453 assert_eq!(json, "\"claude-haiku-5-5\"");
454 let back: ClaudeModel = serde_json::from_str(&json).unwrap();
455 assert_eq!(back, ClaudeModel::Haiku55);
456 assert_eq!(ClaudeModel::Haiku55.display_name(), "Haiku 5.5");
457 assert!(!ClaudeModel::Haiku55.is_alias());
458 }
459}