1pub const KNOWN_MODELS: &[(&str, &str)] = &[
5 ("claude-sonnet-4-6", "Sonnet 4.6 — balanced (default)"),
6 ("claude-fable-5-2", "Fable 5.2 — most capable"),
7 ("claude-fable-5-1", "Fable 5.1"),
8 ("claude-fable-5", "Fable 5"),
9 ("claude-opus-5-5", "Opus 5.5 — most capable Opus"),
10 ("claude-opus-4-7", "Opus 4.7"),
11 ("claude-opus-4-6", "Opus 4.6 — previous flagship"),
12 ("claude-haiku-4-5-20251001", "Haiku 4.5 — fast"),
13];
14
15pub fn default_model() -> &'static str {
16 KNOWN_MODELS[0].0
17}
18
19pub fn model_supports_tool_search(model: &str) -> bool {
36 let m = model.to_ascii_lowercase();
37 let m = m.strip_prefix("anthropic/").unwrap_or(&m);
38 if !m.starts_with("claude-") {
39 return false;
40 }
41 const UNSUPPORTED: &[&str] = &[
42 "claude-3",
43 "claude-opus-4-0",
44 "claude-opus-4-1",
45 "claude-opus-4-2025",
46 "claude-sonnet-4-0",
47 "claude-sonnet-4-2025",
48 "claude-instant",
49 ];
50 !UNSUPPORTED.iter().any(|p| m.starts_with(p))
51}
52
53pub fn model_supports_adaptive_thinking(model: &str) -> bool {
54 let m = model.to_ascii_lowercase();
55 if m.contains("opus-4-7") || m.contains("opus-4-8") || m.contains("opus-4-9") {
60 return true;
61 }
62 if m.contains("sonnet-4-7") || m.contains("sonnet-4-8") || m.contains("sonnet-4-9") {
63 return true;
64 }
65 if m.contains("opus-5")
67 || m.contains("sonnet-5")
68 || m.contains("haiku-5")
69 || m.contains("fable-5")
70 {
71 return true;
72 }
73 false
74}
75
76pub fn effort_for_thinking_level(level: &str) -> Option<&'static str> {
83 match level {
84 "low" => Some("low"),
85 "medium" | "med" => Some("medium"),
86 "high" => Some("high"),
87 "xhigh" => Some("xhigh"),
88 "adaptive" => None, _ => Some("high"), }
91}
92
93pub fn thinking_level_for_budget(budget: u32) -> &'static str {
100 match budget {
101 0 => "adaptive",
102 1..=2048 => "low",
103 2049..=4096 => "medium",
104 4097..=16384 => "high",
105 _ => "xhigh",
106 }
107}
108
109pub fn budget_for_thinking_level(level: &str) -> Option<u32> {
114 match level {
115 "adaptive" | "off" => Some(0),
116 "low" => Some(2048),
117 "medium" => Some(4096),
118 "high" => Some(16384),
119 "xhigh" => Some(32768),
120 _ => None,
121 }
122}
123
124pub const DEFAULT_LEGACY_ADAPTIVE_FALLBACK: u32 = 16384;
127
128pub fn model_supports_1m(model: &str) -> bool {
136 let m = model.to_ascii_lowercase();
137 m.contains("opus-4-6")
139 || m.contains("opus-4-7")
140 || m.contains("opus-4-8")
141 || m.contains("opus-4-9")
142 || m.contains("sonnet-4")
143 || m.contains("opus-5")
144 || m.contains("sonnet-5")
145 || m.contains("fable-5")
146}
147
148pub const EXTENDED_CONTEXT_WINDOW: u64 = 1_000_000;
150
151pub fn model_rejects_output_overflow(model: &str) -> bool {
166 let m = model.to_ascii_lowercase();
167 let m = m.strip_prefix("anthropic/").unwrap_or(&m);
168 const BEFORE_4_5: &[&str] = &[
169 "claude-2",
170 "claude-instant",
171 "claude-3",
172 "claude-opus-4-0",
173 "claude-opus-4-1",
174 "claude-opus-4-2025",
175 "claude-sonnet-4-0",
176 "claude-sonnet-4-2025",
177 ];
178 matches!(m, "claude-opus-4" | "claude-sonnet-4") || BEFORE_4_5.iter().any(|p| m.starts_with(p))
179}
180
181pub fn context_window_for_model(_model: &str) -> u64 {
195 200_000
196}
197
198#[cfg(test)]
199mod tests {
200 #[test]
201 fn tool_search_support_follows_anthropic_compatibility_table() {
202 for m in [
203 "claude-opus-5-5",
204 "anthropic/claude-sonnet-5-5",
205 "claude-fable-5-1",
206 "claude-fable-5-2",
207 "claude-opus-4-5-20251101",
208 "claude-sonnet-4-5-20250929",
209 "claude-haiku-4-5-20251001",
210 "claude-opus-4-8",
211 ] {
212 assert!(super::model_supports_tool_search(m), "{m}");
213 }
214 for m in [
215 "claude-opus-4-1-20250805",
216 "claude-opus-4-20250514",
217 "claude-sonnet-4-20250514",
218 "claude-3-7-sonnet-latest",
219 "gpt-5.6-sol",
220 "groq/llama-3.3-70b-versatile",
221 ] {
222 assert!(!super::model_supports_tool_search(m), "{m}");
223 }
224 }
225
226 use super::*;
227
228 #[test]
229 fn default_model_is_first_entry() {
230 assert_eq!(default_model(), KNOWN_MODELS[0].0);
231 }
232
233 #[test]
234 fn known_models_has_expected_ids() {
235 let ids: Vec<&str> = KNOWN_MODELS.iter().map(|(id, _)| *id).collect();
236 assert!(ids.contains(&"claude-fable-5-2"));
237 assert!(ids.contains(&"claude-fable-5-1"));
238 assert!(ids.contains(&"claude-fable-5"));
239 assert!(ids.contains(&"claude-opus-5-5"));
240 assert!(ids.contains(&"claude-opus-4-7"));
241 assert!(ids.contains(&"claude-sonnet-4-6"));
242 assert!(ids.contains(&"claude-haiku-4-5-20251001"));
243 }
244
245 #[test]
246 fn descriptions_are_non_empty() {
247 for (_, desc) in KNOWN_MODELS {
248 assert!(!desc.is_empty(), "empty description");
249 }
250 }
251
252 #[test]
253 fn budget_for_thinking_level_round_trips() {
254 for budget in [0u32, 2048, 4096, 16384, 32768] {
255 let level = thinking_level_for_budget(budget);
256 assert_eq!(
257 budget_for_thinking_level(level),
258 Some(budget),
259 "level {}",
260 level
261 );
262 }
263 assert_eq!(budget_for_thinking_level("off"), Some(0));
264 assert_eq!(budget_for_thinking_level("custom(8192)"), None);
265 }
266
267 #[test]
268 fn only_models_before_claude_4_5_reject_output_overflow() {
269 for m in [
270 "claude-opus-4-1-20250805",
271 "claude-opus-4-20250514",
272 "claude-opus-4-0",
273 "claude-sonnet-4-20250514",
274 "anthropic/claude-sonnet-4-0",
275 "claude-sonnet-4",
276 "claude-3-7-sonnet-latest",
277 "claude-3-5-haiku-20241022",
278 "CLAUDE-3-OPUS-20240229",
279 "claude-2.1",
280 "claude-instant-1.2",
281 ] {
282 assert!(model_rejects_output_overflow(m), "{m}");
283 }
284 for m in [
285 "claude-haiku-4-5-20251001",
286 "claude-sonnet-4-5-20250929",
287 "claude-opus-4-5-20251101",
288 "claude-sonnet-4-6",
289 "claude-opus-4-7",
290 "anthropic/claude-opus-5-5",
291 "claude-fable-5-1",
292 "claude-sonnet-9-0",
293 "gpt-5.5",
294 "google-gemini/gemini-2.5-pro",
295 ] {
296 assert!(!model_rejects_output_overflow(m), "{m}");
297 }
298 }
299
300 #[test]
301 fn context_window_default_is_200k_for_all_models() {
302 assert_eq!(context_window_for_model("claude-opus-4-7"), 200_000);
304 assert_eq!(context_window_for_model("claude-opus-4-6"), 200_000);
305 assert_eq!(context_window_for_model("claude-sonnet-4-6"), 200_000);
306 assert_eq!(
307 context_window_for_model("claude-opus-4-5-20251101"),
308 200_000
309 );
310 assert_eq!(
311 context_window_for_model("claude-haiku-4-5-20251001"),
312 200_000
313 );
314 }
315
316 #[test]
317 fn model_supports_1m_for_opus_46_and_sonnet_4() {
318 assert!(model_supports_1m("claude-opus-4-6"));
319 assert!(model_supports_1m("claude-opus-4-7"));
320 assert!(model_supports_1m("claude-sonnet-4-6"));
321 assert!(model_supports_1m("claude-sonnet-4-5-20250929"));
322 }
323
324 #[test]
325 fn model_supports_1m_rejects_older_and_haiku() {
326 assert!(!model_supports_1m("claude-opus-4-5"));
327 assert!(!model_supports_1m("claude-haiku-4-5-20251001"));
328 assert!(!model_supports_1m("claude-opus-3-5"));
329 }
330
331 #[test]
332 fn model_supports_1m_assumed_for_5x() {
333 assert!(model_supports_1m("claude-opus-5-0"));
334 assert!(model_supports_1m("claude-sonnet-5-1"));
335 assert!(model_supports_1m("claude-fable-5"));
336 }
337
338 #[test]
339 fn model_supports_1m_case_insensitive() {
340 assert!(model_supports_1m("CLAUDE-OPUS-4-6"));
341 }
342
343 #[test]
344 fn context_window_haiku_is_200k() {
345 assert_eq!(
346 context_window_for_model("claude-haiku-4-5-20251001"),
347 200_000
348 );
349 }
350
351 #[test]
352 fn context_window_opus3_is_200k() {
353 assert_eq!(
354 context_window_for_model("claude-opus-3-5-20250101"),
355 200_000
356 );
357 }
358
359 #[test]
360 fn context_window_unknown_defaults_200k() {
361 assert_eq!(context_window_for_model("some-future-model"), 200_000);
362 assert_eq!(context_window_for_model(""), 200_000);
363 }
364
365 #[test]
366 fn adaptive_thinking_required_for_opus_4_7_plus() {
367 assert!(model_supports_adaptive_thinking("claude-opus-4-7"));
368 assert!(model_supports_adaptive_thinking("claude-opus-4-8"));
369 }
370
371 #[test]
372 fn adaptive_thinking_not_for_opus_4_6() {
373 assert!(!model_supports_adaptive_thinking("claude-opus-4-6"));
375 }
376
377 #[test]
378 fn adaptive_thinking_not_for_sonnet_4_6() {
379 assert!(!model_supports_adaptive_thinking("claude-sonnet-4-6"));
380 }
381
382 #[test]
383 fn adaptive_thinking_required_for_sonnet_4_7_plus() {
384 assert!(model_supports_adaptive_thinking("claude-sonnet-4-7"));
385 }
386
387 #[test]
388 fn adaptive_thinking_not_for_older_models() {
389 assert!(!model_supports_adaptive_thinking("claude-opus-4-5"));
390 assert!(!model_supports_adaptive_thinking("claude-sonnet-4-5"));
391 assert!(!model_supports_adaptive_thinking(
392 "claude-haiku-4-5-20251001"
393 ));
394 assert!(!model_supports_adaptive_thinking("claude-opus-3-5"));
395 }
396
397 #[test]
398 fn adaptive_thinking_assumed_for_5x() {
399 assert!(model_supports_adaptive_thinking("claude-opus-5-0"));
400 assert!(model_supports_adaptive_thinking("claude-sonnet-5-1"));
401 }
402
403 #[test]
404 fn opus_5_5_is_a_known_adaptive_1m_capable_model() {
405 assert!(KNOWN_MODELS.iter().any(|(id, _)| *id == "claude-opus-5-5"));
407 assert!(!KNOWN_MODELS.iter().any(|(id, _)| *id == "claude-opus-5.5"));
408 assert!(model_supports_adaptive_thinking("claude-opus-5-5"));
409 assert!(model_supports_1m("claude-opus-5-5"));
410 assert_ne!(default_model(), "claude-opus-5-5");
412 }
413
414 #[test]
415 fn adaptive_thinking_case_insensitive() {
416 assert!(model_supports_adaptive_thinking("CLAUDE-OPUS-4-7"));
417 }
418}