synaps_cli/core/
models.rs1pub const KNOWN_MODELS: &[(&str, &str)] = &[
5 ("claude-sonnet-4-6", "Sonnet 4.6 — balanced (default)"),
6 ("claude-fable-5", "Fable 5 — latest"),
7 ("claude-opus-4-7", "Opus 4.7 — most capable"),
8 ("claude-opus-4-6", "Opus 4.6 — previous flagship"),
9 ("claude-haiku-4-5-20251001", "Haiku 4.5 — fast"),
10];
11
12pub fn default_model() -> &'static str {
13 KNOWN_MODELS[0].0
14}
15
16pub fn model_supports_adaptive_thinking(model: &str) -> bool {
28 let m = model.to_ascii_lowercase();
29 if m.contains("opus-4-7") || m.contains("opus-4-8") || m.contains("opus-4-9") {
34 return true;
35 }
36 if m.contains("sonnet-4-7") || m.contains("sonnet-4-8") || m.contains("sonnet-4-9") {
37 return true;
38 }
39 if m.contains("opus-5") || m.contains("sonnet-5") || m.contains("haiku-5") || m.contains("fable-5") {
41 return true;
42 }
43 false
44}
45
46pub fn effort_for_thinking_level(level: &str) -> Option<&'static str> {
53 match level {
54 "low" => Some("low"),
55 "medium" | "med" => Some("medium"),
56 "high" => Some("high"),
57 "xhigh" => Some("xhigh"),
58 "adaptive" => None, _ => Some("high"), }
61}
62
63pub fn thinking_level_for_budget(budget: u32) -> &'static str {
70 match budget {
71 0 => "adaptive",
72 1..=2048 => "low",
73 2049..=4096 => "medium",
74 4097..=16384 => "high",
75 _ => "xhigh",
76 }
77}
78
79pub const DEFAULT_LEGACY_ADAPTIVE_FALLBACK: u32 = 16384;
82
83pub fn model_supports_1m(model: &str) -> bool {
91 let m = model.to_ascii_lowercase();
92 m.contains("opus-4-6") || m.contains("opus-4-7")
94 || m.contains("opus-4-8") || m.contains("opus-4-9")
95 || m.contains("sonnet-4")
96 || m.contains("opus-5") || m.contains("sonnet-5")
97 || m.contains("fable-5")
98}
99
100pub fn context_window_for_model(_model: &str) -> u64 {
114 200_000
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn default_model_is_first_entry() {
123 assert_eq!(default_model(), KNOWN_MODELS[0].0);
124 }
125
126 #[test]
127 fn known_models_has_expected_ids() {
128 let ids: Vec<&str> = KNOWN_MODELS.iter().map(|(id, _)| *id).collect();
129 assert!(ids.contains(&"claude-opus-4-7"));
130 assert!(ids.contains(&"claude-sonnet-4-6"));
131 assert!(ids.contains(&"claude-haiku-4-5-20251001"));
132 }
133
134 #[test]
135 fn descriptions_are_non_empty() {
136 for (_, desc) in KNOWN_MODELS {
137 assert!(!desc.is_empty(), "empty description");
138 }
139 }
140
141 #[test]
142 fn context_window_default_is_200k_for_all_models() {
143 assert_eq!(context_window_for_model("claude-opus-4-7"), 200_000);
145 assert_eq!(context_window_for_model("claude-opus-4-6"), 200_000);
146 assert_eq!(context_window_for_model("claude-sonnet-4-6"), 200_000);
147 assert_eq!(context_window_for_model("claude-opus-4-5-20251101"), 200_000);
148 assert_eq!(context_window_for_model("claude-haiku-4-5-20251001"), 200_000);
149 }
150
151 #[test]
152 fn model_supports_1m_for_opus_46_and_sonnet_4() {
153 assert!(model_supports_1m("claude-opus-4-6"));
154 assert!(model_supports_1m("claude-opus-4-7"));
155 assert!(model_supports_1m("claude-sonnet-4-6"));
156 assert!(model_supports_1m("claude-sonnet-4-5-20250929"));
157 }
158
159 #[test]
160 fn model_supports_1m_rejects_older_and_haiku() {
161 assert!(!model_supports_1m("claude-opus-4-5"));
162 assert!(!model_supports_1m("claude-haiku-4-5-20251001"));
163 assert!(!model_supports_1m("claude-opus-3-5"));
164 }
165
166 #[test]
167 fn model_supports_1m_assumed_for_5x() {
168 assert!(model_supports_1m("claude-opus-5-0"));
169 assert!(model_supports_1m("claude-sonnet-5-1"));
170 assert!(model_supports_1m("claude-fable-5"));
171 }
172
173 #[test]
174 fn model_supports_1m_case_insensitive() {
175 assert!(model_supports_1m("CLAUDE-OPUS-4-6"));
176 }
177
178 #[test]
179 fn context_window_haiku_is_200k() {
180 assert_eq!(context_window_for_model("claude-haiku-4-5-20251001"), 200_000);
181 }
182
183 #[test]
184 fn context_window_opus3_is_200k() {
185 assert_eq!(context_window_for_model("claude-opus-3-5-20250101"), 200_000);
186 }
187
188 #[test]
189 fn context_window_unknown_defaults_200k() {
190 assert_eq!(context_window_for_model("some-future-model"), 200_000);
191 assert_eq!(context_window_for_model(""), 200_000);
192 }
193
194 #[test]
195 fn adaptive_thinking_required_for_opus_4_7_plus() {
196 assert!(model_supports_adaptive_thinking("claude-opus-4-7"));
197 assert!(model_supports_adaptive_thinking("claude-opus-4-8"));
198 }
199
200 #[test]
201 fn adaptive_thinking_not_for_opus_4_6() {
202 assert!(!model_supports_adaptive_thinking("claude-opus-4-6"));
204 }
205
206 #[test]
207 fn adaptive_thinking_not_for_sonnet_4_6() {
208 assert!(!model_supports_adaptive_thinking("claude-sonnet-4-6"));
209 }
210
211 #[test]
212 fn adaptive_thinking_required_for_sonnet_4_7_plus() {
213 assert!(model_supports_adaptive_thinking("claude-sonnet-4-7"));
214 }
215
216 #[test]
217 fn adaptive_thinking_not_for_older_models() {
218 assert!(!model_supports_adaptive_thinking("claude-opus-4-5"));
219 assert!(!model_supports_adaptive_thinking("claude-sonnet-4-5"));
220 assert!(!model_supports_adaptive_thinking("claude-haiku-4-5-20251001"));
221 assert!(!model_supports_adaptive_thinking("claude-opus-3-5"));
222 }
223
224 #[test]
225 fn adaptive_thinking_assumed_for_5x() {
226 assert!(model_supports_adaptive_thinking("claude-opus-5-0"));
227 assert!(model_supports_adaptive_thinking("claude-sonnet-5-1"));
228 }
229
230 #[test]
231 fn adaptive_thinking_case_insensitive() {
232 assert!(model_supports_adaptive_thinking("CLAUDE-OPUS-4-7"));
233 }
234}