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