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