Skip to main content

claude_codes/
models.rs

1//! Convenience enum for the models the Claude CLI exposes.
2//!
3//! Variants are keyed by human-friendly model names; [`ClaudeModel::cli_arg`]
4//! returns the exact string to pass to `claude --model` (and therefore to
5//! `ClaudeCliBuilder::model`, which accepts the enum directly via
6//! `Into<String>`).
7//!
8//! The model table was extracted from the Claude CLI 2.1.205 binary's model
9//! registry. Aliases (`sonnet`, `opus`, …) float to the newest model of that
10//! family on the CLI side; pinned variants name one concrete model. Unknown
11//! or future model strings round-trip through [`ClaudeModel::Custom`].
12
13use serde::{Deserialize, Deserializer, Serialize, Serializer};
14use std::fmt;
15
16/// A model selector accepted by `claude --model`.
17#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18pub enum ClaudeModel {
19    /// Newest Sonnet-family model (floating alias `sonnet`).
20    Sonnet,
21    /// Newest Opus-family model (floating alias `opus`).
22    Opus,
23    /// Newest Haiku-family model (floating alias `haiku`).
24    Haiku,
25    /// Newest Fable-family model (floating alias `fable`).
26    Fable,
27    /// The most capable model available to the account (alias `best`).
28    Best,
29    /// Opus for plan mode, Sonnet otherwise (alias `opusplan`).
30    OpusPlan,
31    /// Newest Sonnet with the 1M-token context beta (alias `sonnet[1m]`).
32    Sonnet1m,
33    /// Newest Opus with the 1M-token context beta (alias `opus[1m]`).
34    Opus1m,
35    /// Newest Fable with the 1M-token context beta (alias `fable[1m]`).
36    Fable1m,
37    /// Fable 5 (`claude-fable-5`).
38    Fable5,
39    /// Mythos 5 (`claude-mythos-5`).
40    Mythos5,
41    /// Opus 4.8 (`claude-opus-4-8`).
42    Opus48,
43    /// Opus 4.7 (`claude-opus-4-7`).
44    Opus47,
45    /// Opus 4.6 (`claude-opus-4-6`).
46    Opus46,
47    /// Opus 4.5 (`claude-opus-4-5`).
48    Opus45,
49    /// Opus 4.1 (`claude-opus-4-1`).
50    Opus41,
51    /// Opus 4 (`claude-opus-4-0`).
52    Opus40,
53    /// Sonnet 5 (`claude-sonnet-5`).
54    Sonnet5,
55    /// Sonnet 4.6 (`claude-sonnet-4-6`).
56    Sonnet46,
57    /// Sonnet 4.5 (`claude-sonnet-4-5`).
58    Sonnet45,
59    /// Sonnet 4 (`claude-sonnet-4-0`).
60    Sonnet40,
61    /// Sonnet 3.7 (`claude-3-7-sonnet`).
62    Sonnet37,
63    /// Sonnet 3.5 (`claude-3-5-sonnet`).
64    Sonnet35,
65    /// Haiku 4.5 (`claude-haiku-4-5`).
66    Haiku45,
67    /// Haiku 3.5 (`claude-3-5-haiku`).
68    Haiku35,
69    /// A model string not yet known to this version of the crate. Passed to
70    /// the CLI verbatim.
71    Custom(String),
72}
73
74impl ClaudeModel {
75    /// The string to pass to `claude --model` for this model.
76    pub fn cli_arg(&self) -> &str {
77        match self {
78            Self::Sonnet => "sonnet",
79            Self::Opus => "opus",
80            Self::Haiku => "haiku",
81            Self::Fable => "fable",
82            Self::Best => "best",
83            Self::OpusPlan => "opusplan",
84            Self::Sonnet1m => "sonnet[1m]",
85            Self::Opus1m => "opus[1m]",
86            Self::Fable1m => "fable[1m]",
87            Self::Fable5 => "claude-fable-5",
88            Self::Mythos5 => "claude-mythos-5",
89            Self::Opus48 => "claude-opus-4-8",
90            Self::Opus47 => "claude-opus-4-7",
91            Self::Opus46 => "claude-opus-4-6",
92            Self::Opus45 => "claude-opus-4-5",
93            Self::Opus41 => "claude-opus-4-1",
94            Self::Opus40 => "claude-opus-4-0",
95            Self::Sonnet5 => "claude-sonnet-5",
96            Self::Sonnet46 => "claude-sonnet-4-6",
97            Self::Sonnet45 => "claude-sonnet-4-5",
98            Self::Sonnet40 => "claude-sonnet-4-0",
99            Self::Sonnet37 => "claude-3-7-sonnet",
100            Self::Sonnet35 => "claude-3-5-sonnet",
101            Self::Haiku45 => "claude-haiku-4-5",
102            Self::Haiku35 => "claude-3-5-haiku",
103            Self::Custom(s) => s.as_str(),
104        }
105    }
106
107    /// Alias for [`cli_arg`](Self::cli_arg), matching the crate's string-enum
108    /// convention.
109    pub fn as_str(&self) -> &str {
110        self.cli_arg()
111    }
112
113    /// Human-friendly display name, matching what the CLI's own UI renders.
114    pub fn display_name(&self) -> &str {
115        match self {
116            Self::Sonnet => "Sonnet (latest)",
117            Self::Opus => "Opus (latest)",
118            Self::Haiku => "Haiku (latest)",
119            Self::Fable => "Fable (latest)",
120            Self::Best => "Best available",
121            Self::OpusPlan => "Opus (plan mode) / Sonnet",
122            Self::Sonnet1m => "Sonnet (latest, 1M context)",
123            Self::Opus1m => "Opus (latest, 1M context)",
124            Self::Fable1m => "Fable (latest, 1M context)",
125            Self::Fable5 => "Fable 5",
126            Self::Mythos5 => "Mythos 5",
127            Self::Opus48 => "Opus 4.8",
128            Self::Opus47 => "Opus 4.7",
129            Self::Opus46 => "Opus 4.6",
130            Self::Opus45 => "Opus 4.5",
131            Self::Opus41 => "Opus 4.1",
132            Self::Opus40 => "Opus 4",
133            Self::Sonnet5 => "Sonnet 5",
134            Self::Sonnet46 => "Sonnet 4.6",
135            Self::Sonnet45 => "Sonnet 4.5",
136            Self::Sonnet40 => "Sonnet 4",
137            Self::Sonnet37 => "Sonnet 3.7",
138            Self::Sonnet35 => "Sonnet 3.5",
139            Self::Haiku45 => "Haiku 4.5",
140            Self::Haiku35 => "Haiku 3.5",
141            Self::Custom(s) => s.as_str(),
142        }
143    }
144
145    /// True for the floating aliases (`sonnet`, `opus`, …) that the CLI
146    /// resolves to the newest model of the family at session start.
147    pub fn is_alias(&self) -> bool {
148        matches!(
149            self,
150            Self::Sonnet
151                | Self::Opus
152                | Self::Haiku
153                | Self::Fable
154                | Self::Best
155                | Self::OpusPlan
156                | Self::Sonnet1m
157                | Self::Opus1m
158                | Self::Fable1m
159        )
160    }
161
162    /// Every model known to this version of the crate, aliases first.
163    pub fn known() -> &'static [ClaudeModel] {
164        &[
165            Self::Sonnet,
166            Self::Opus,
167            Self::Haiku,
168            Self::Fable,
169            Self::Best,
170            Self::OpusPlan,
171            Self::Sonnet1m,
172            Self::Opus1m,
173            Self::Fable1m,
174            Self::Fable5,
175            Self::Mythos5,
176            Self::Opus48,
177            Self::Opus47,
178            Self::Opus46,
179            Self::Opus45,
180            Self::Opus41,
181            Self::Opus40,
182            Self::Sonnet5,
183            Self::Sonnet46,
184            Self::Sonnet45,
185            Self::Sonnet40,
186            Self::Sonnet37,
187            Self::Sonnet35,
188            Self::Haiku45,
189            Self::Haiku35,
190        ]
191    }
192}
193
194impl fmt::Display for ClaudeModel {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        f.write_str(self.cli_arg())
197    }
198}
199
200impl From<&str> for ClaudeModel {
201    fn from(s: &str) -> Self {
202        match s {
203            "sonnet" => Self::Sonnet,
204            "opus" => Self::Opus,
205            "haiku" => Self::Haiku,
206            "fable" => Self::Fable,
207            "best" => Self::Best,
208            "opusplan" => Self::OpusPlan,
209            "sonnet[1m]" => Self::Sonnet1m,
210            "opus[1m]" => Self::Opus1m,
211            "fable[1m]" => Self::Fable1m,
212            "claude-fable-5" => Self::Fable5,
213            "claude-mythos-5" => Self::Mythos5,
214            "claude-opus-4-8" => Self::Opus48,
215            "claude-opus-4-7" => Self::Opus47,
216            "claude-opus-4-6" => Self::Opus46,
217            "claude-opus-4-5" => Self::Opus45,
218            "claude-opus-4-1" => Self::Opus41,
219            "claude-opus-4-0" => Self::Opus40,
220            "claude-sonnet-5" => Self::Sonnet5,
221            "claude-sonnet-4-6" => Self::Sonnet46,
222            "claude-sonnet-4-5" => Self::Sonnet45,
223            "claude-sonnet-4-0" => Self::Sonnet40,
224            "claude-3-7-sonnet" => Self::Sonnet37,
225            "claude-3-5-sonnet" => Self::Sonnet35,
226            "claude-haiku-4-5" => Self::Haiku45,
227            "claude-3-5-haiku" => Self::Haiku35,
228            other => Self::Custom(other.to_string()),
229        }
230    }
231}
232
233impl From<ClaudeModel> for String {
234    fn from(model: ClaudeModel) -> Self {
235        model.cli_arg().to_string()
236    }
237}
238
239impl Serialize for ClaudeModel {
240    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
241        serializer.serialize_str(self.cli_arg())
242    }
243}
244
245impl<'de> Deserialize<'de> for ClaudeModel {
246    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
247        let s = String::deserialize(deserializer)?;
248        Ok(Self::from(s.as_str()))
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::ClaudeModel;
255
256    #[test]
257    fn test_cli_arg_round_trip() {
258        for model in ClaudeModel::known() {
259            assert_eq!(&ClaudeModel::from(model.cli_arg()), model);
260        }
261        assert_eq!(
262            ClaudeModel::from("claude-nova-7"),
263            ClaudeModel::Custom("claude-nova-7".to_string())
264        );
265        assert_eq!(
266            ClaudeModel::from("claude-nova-7").cli_arg(),
267            "claude-nova-7"
268        );
269    }
270
271    #[test]
272    fn test_into_string_matches_cli_arg() {
273        let s: String = ClaudeModel::Sonnet5.into();
274        assert_eq!(s, "claude-sonnet-5");
275        let s: String = ClaudeModel::Opus1m.into();
276        assert_eq!(s, "opus[1m]");
277    }
278
279    #[test]
280    fn test_display_names() {
281        assert_eq!(ClaudeModel::Fable5.display_name(), "Fable 5");
282        assert_eq!(ClaudeModel::Opus48.display_name(), "Opus 4.8");
283        assert_eq!(ClaudeModel::Sonnet.display_name(), "Sonnet (latest)");
284        assert!(ClaudeModel::Sonnet.is_alias());
285        assert!(!ClaudeModel::Sonnet5.is_alias());
286    }
287
288    #[test]
289    fn test_serde_round_trip() {
290        let json = serde_json::to_string(&ClaudeModel::Haiku45).unwrap();
291        assert_eq!(json, "\"claude-haiku-4-5\"");
292        let back: ClaudeModel = serde_json::from_str(&json).unwrap();
293        assert_eq!(back, ClaudeModel::Haiku45);
294    }
295}