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