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,
27 Fable,
29 Best,
31 OpusPlan,
33 Sonnet1m,
35 Opus1m,
37 Fable1m,
39 Fable51,
41 Fable5,
43 Mythos51,
45 Mythos5,
47 Opus55,
50 Opus5,
52 Opus48,
54 Opus47,
56 Opus46,
58 Opus45,
60 Opus41,
62 Opus40,
64 Sonnet55,
67 Sonnet5,
69 Sonnet46,
71 Sonnet45,
73 Sonnet40,
75 Sonnet37,
77 Sonnet35,
79 Haiku45,
81 Haiku35,
83 Custom(String),
86}
87
88impl ClaudeModel {
89 pub fn cli_arg(&self) -> &str {
91 match self {
92 Self::Sonnet => "sonnet",
93 Self::Opus => "opus",
94 Self::Haiku => "haiku",
95 Self::Fable => "fable",
96 Self::Best => "best",
97 Self::OpusPlan => "opusplan",
98 Self::Sonnet1m => "sonnet[1m]",
99 Self::Opus1m => "opus[1m]",
100 Self::Fable1m => "fable[1m]",
101 Self::Fable51 => "claude-fable-5-1",
102 Self::Fable5 => "claude-fable-5",
103 Self::Mythos51 => "claude-mythos-5-1",
104 Self::Mythos5 => "claude-mythos-5",
105 Self::Opus55 => "claude-opus-5-5",
106 Self::Opus5 => "claude-opus-5",
107 Self::Opus48 => "claude-opus-4-8",
108 Self::Opus47 => "claude-opus-4-7",
109 Self::Opus46 => "claude-opus-4-6",
110 Self::Opus45 => "claude-opus-4-5",
111 Self::Opus41 => "claude-opus-4-1",
112 Self::Opus40 => "claude-opus-4-0",
113 Self::Sonnet55 => "claude-sonnet-5-5",
114 Self::Sonnet5 => "claude-sonnet-5",
115 Self::Sonnet46 => "claude-sonnet-4-6",
116 Self::Sonnet45 => "claude-sonnet-4-5",
117 Self::Sonnet40 => "claude-sonnet-4-0",
118 Self::Sonnet37 => "claude-3-7-sonnet",
119 Self::Sonnet35 => "claude-3-5-sonnet",
120 Self::Haiku45 => "claude-haiku-4-5",
121 Self::Haiku35 => "claude-3-5-haiku",
122 Self::Custom(s) => s.as_str(),
123 }
124 }
125
126 pub fn as_str(&self) -> &str {
129 self.cli_arg()
130 }
131
132 pub fn display_name(&self) -> &str {
134 match self {
135 Self::Sonnet => "Sonnet (latest)",
136 Self::Opus => "Opus (latest)",
137 Self::Haiku => "Haiku (latest)",
138 Self::Fable => "Fable (latest)",
139 Self::Best => "Best available",
140 Self::OpusPlan => "Opus (plan mode) / Sonnet",
141 Self::Sonnet1m => "Sonnet (latest, 1M context)",
142 Self::Opus1m => "Opus (latest, 1M context)",
143 Self::Fable1m => "Fable (latest, 1M context)",
144 Self::Fable51 => "Fable 5.1",
145 Self::Fable5 => "Fable 5",
146 Self::Mythos51 => "Mythos 5.1",
147 Self::Mythos5 => "Mythos 5",
148 Self::Opus55 => "Opus 5.5",
149 Self::Opus5 => "Opus 5",
150 Self::Opus48 => "Opus 4.8",
151 Self::Opus47 => "Opus 4.7",
152 Self::Opus46 => "Opus 4.6",
153 Self::Opus45 => "Opus 4.5",
154 Self::Opus41 => "Opus 4.1",
155 Self::Opus40 => "Opus 4",
156 Self::Sonnet55 => "Sonnet 5.5",
157 Self::Sonnet5 => "Sonnet 5",
158 Self::Sonnet46 => "Sonnet 4.6",
159 Self::Sonnet45 => "Sonnet 4.5",
160 Self::Sonnet40 => "Sonnet 4",
161 Self::Sonnet37 => "Sonnet 3.7",
162 Self::Sonnet35 => "Sonnet 3.5",
163 Self::Haiku45 => "Haiku 4.5",
164 Self::Haiku35 => "Haiku 3.5",
165 Self::Custom(s) => s.as_str(),
166 }
167 }
168
169 pub fn is_alias(&self) -> bool {
172 matches!(
173 self,
174 Self::Sonnet
175 | Self::Opus
176 | Self::Haiku
177 | Self::Fable
178 | Self::Best
179 | Self::OpusPlan
180 | Self::Sonnet1m
181 | Self::Opus1m
182 | Self::Fable1m
183 )
184 }
185
186 pub fn known() -> &'static [ClaudeModel] {
188 &[
189 Self::Sonnet,
190 Self::Opus,
191 Self::Haiku,
192 Self::Fable,
193 Self::Best,
194 Self::OpusPlan,
195 Self::Sonnet1m,
196 Self::Opus1m,
197 Self::Fable1m,
198 Self::Fable51,
199 Self::Fable5,
200 Self::Mythos51,
201 Self::Mythos5,
202 Self::Opus55,
203 Self::Opus5,
204 Self::Opus48,
205 Self::Opus47,
206 Self::Opus46,
207 Self::Opus45,
208 Self::Opus41,
209 Self::Opus40,
210 Self::Sonnet55,
211 Self::Sonnet5,
212 Self::Sonnet46,
213 Self::Sonnet45,
214 Self::Sonnet40,
215 Self::Sonnet37,
216 Self::Sonnet35,
217 Self::Haiku45,
218 Self::Haiku35,
219 ]
220 }
221}
222
223impl fmt::Display for ClaudeModel {
224 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225 f.write_str(self.cli_arg())
226 }
227}
228
229impl From<&str> for ClaudeModel {
230 fn from(s: &str) -> Self {
231 match s {
232 "sonnet" => Self::Sonnet,
233 "opus" => Self::Opus,
234 "haiku" => Self::Haiku,
235 "fable" => Self::Fable,
236 "best" => Self::Best,
237 "opusplan" => Self::OpusPlan,
238 "sonnet[1m]" => Self::Sonnet1m,
239 "opus[1m]" => Self::Opus1m,
240 "fable[1m]" => Self::Fable1m,
241 "claude-fable-5-1" => Self::Fable51,
242 "claude-fable-5" => Self::Fable5,
243 "claude-mythos-5-1" => Self::Mythos51,
244 "claude-mythos-5" => Self::Mythos5,
245 "claude-opus-5-5" => Self::Opus55,
246 "claude-opus-5" => Self::Opus5,
247 "claude-opus-4-8" => Self::Opus48,
248 "claude-opus-4-7" => Self::Opus47,
249 "claude-opus-4-6" => Self::Opus46,
250 "claude-opus-4-5" => Self::Opus45,
251 "claude-opus-4-1" => Self::Opus41,
252 "claude-opus-4-0" => Self::Opus40,
253 "claude-sonnet-5-5" => Self::Sonnet55,
254 "claude-sonnet-5" => Self::Sonnet5,
255 "claude-sonnet-4-6" => Self::Sonnet46,
256 "claude-sonnet-4-5" => Self::Sonnet45,
257 "claude-sonnet-4-0" => Self::Sonnet40,
258 "claude-3-7-sonnet" => Self::Sonnet37,
259 "claude-3-5-sonnet" => Self::Sonnet35,
260 "claude-haiku-4-5" => Self::Haiku45,
261 "claude-3-5-haiku" => Self::Haiku35,
262 other => Self::Custom(other.to_string()),
263 }
264 }
265}
266
267impl From<ClaudeModel> for String {
268 fn from(model: ClaudeModel) -> Self {
269 model.cli_arg().to_string()
270 }
271}
272
273impl Serialize for ClaudeModel {
274 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
275 serializer.serialize_str(self.cli_arg())
276 }
277}
278
279impl<'de> Deserialize<'de> for ClaudeModel {
280 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
281 let s = String::deserialize(deserializer)?;
282 Ok(Self::from(s.as_str()))
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::ClaudeModel;
289
290 #[test]
291 fn test_cli_arg_round_trip() {
292 for model in ClaudeModel::known() {
293 assert_eq!(&ClaudeModel::from(model.cli_arg()), model);
294 }
295 assert_eq!(
296 ClaudeModel::from("claude-nova-7"),
297 ClaudeModel::Custom("claude-nova-7".to_string())
298 );
299 assert_eq!(
300 ClaudeModel::from("claude-nova-7").cli_arg(),
301 "claude-nova-7"
302 );
303 }
304
305 #[test]
306 fn test_into_string_matches_cli_arg() {
307 let s: String = ClaudeModel::Sonnet5.into();
308 assert_eq!(s, "claude-sonnet-5");
309 let s: String = ClaudeModel::Opus1m.into();
310 assert_eq!(s, "opus[1m]");
311 }
312
313 #[test]
314 fn test_display_names() {
315 assert_eq!(ClaudeModel::Opus5.display_name(), "Opus 5");
316 assert_eq!(ClaudeModel::Opus5.cli_arg(), "claude-opus-5");
317 assert_eq!(ClaudeModel::Fable5.display_name(), "Fable 5");
318 assert_eq!(ClaudeModel::Opus48.display_name(), "Opus 4.8");
319 assert_eq!(ClaudeModel::Sonnet.display_name(), "Sonnet (latest)");
320 assert!(ClaudeModel::Sonnet.is_alias());
321 assert!(!ClaudeModel::Sonnet5.is_alias());
322 }
323
324 #[test]
325 fn test_serde_round_trip() {
326 let json = serde_json::to_string(&ClaudeModel::Haiku45).unwrap();
327 assert_eq!(json, "\"claude-haiku-4-5\"");
328 let back: ClaudeModel = serde_json::from_str(&json).unwrap();
329 assert_eq!(back, ClaudeModel::Haiku45);
330 }
331}