agent_core/core/
models.rs1pub 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 fn context_window_for_model(_model: &str) -> u64 {
161 200_000
162}
163
164#[cfg(test)]
165mod tests {
166 #[test]
167 fn tool_search_support_follows_anthropic_compatibility_table() {
168 for m in [
169 "claude-opus-5-5",
170 "anthropic/claude-sonnet-5-5",
171 "claude-fable-5-1",
172 "claude-opus-4-5-20251101",
173 "claude-sonnet-4-5-20250929",
174 "claude-haiku-4-5-20251001",
175 "claude-opus-4-8",
176 ] {
177 assert!(super::model_supports_tool_search(m), "{m}");
178 }
179 for m in [
180 "claude-opus-4-1-20250805",
181 "claude-opus-4-20250514",
182 "claude-sonnet-4-20250514",
183 "claude-3-7-sonnet-latest",
184 "gpt-5.6-sol",
185 "groq/llama-3.3-70b-versatile",
186 ] {
187 assert!(!super::model_supports_tool_search(m), "{m}");
188 }
189 }
190
191 use super::*;
192
193 #[test]
194 fn default_model_is_first_entry() {
195 assert_eq!(default_model(), KNOWN_MODELS[0].0);
196 }
197
198 #[test]
199 fn known_models_has_expected_ids() {
200 let ids: Vec<&str> = KNOWN_MODELS.iter().map(|(id, _)| *id).collect();
201 assert!(ids.contains(&"claude-fable-5-1"));
202 assert!(ids.contains(&"claude-fable-5"));
203 assert!(ids.contains(&"claude-opus-5-5"));
204 assert!(ids.contains(&"claude-opus-4-7"));
205 assert!(ids.contains(&"claude-sonnet-4-6"));
206 assert!(ids.contains(&"claude-haiku-4-5-20251001"));
207 }
208
209 #[test]
210 fn descriptions_are_non_empty() {
211 for (_, desc) in KNOWN_MODELS {
212 assert!(!desc.is_empty(), "empty description");
213 }
214 }
215
216 #[test]
217 fn budget_for_thinking_level_round_trips() {
218 for budget in [0u32, 2048, 4096, 16384, 32768] {
219 let level = thinking_level_for_budget(budget);
220 assert_eq!(
221 budget_for_thinking_level(level),
222 Some(budget),
223 "level {}",
224 level
225 );
226 }
227 assert_eq!(budget_for_thinking_level("off"), Some(0));
228 assert_eq!(budget_for_thinking_level("custom(8192)"), None);
229 }
230
231 #[test]
232 fn context_window_default_is_200k_for_all_models() {
233 assert_eq!(context_window_for_model("claude-opus-4-7"), 200_000);
235 assert_eq!(context_window_for_model("claude-opus-4-6"), 200_000);
236 assert_eq!(context_window_for_model("claude-sonnet-4-6"), 200_000);
237 assert_eq!(
238 context_window_for_model("claude-opus-4-5-20251101"),
239 200_000
240 );
241 assert_eq!(
242 context_window_for_model("claude-haiku-4-5-20251001"),
243 200_000
244 );
245 }
246
247 #[test]
248 fn model_supports_1m_for_opus_46_and_sonnet_4() {
249 assert!(model_supports_1m("claude-opus-4-6"));
250 assert!(model_supports_1m("claude-opus-4-7"));
251 assert!(model_supports_1m("claude-sonnet-4-6"));
252 assert!(model_supports_1m("claude-sonnet-4-5-20250929"));
253 }
254
255 #[test]
256 fn model_supports_1m_rejects_older_and_haiku() {
257 assert!(!model_supports_1m("claude-opus-4-5"));
258 assert!(!model_supports_1m("claude-haiku-4-5-20251001"));
259 assert!(!model_supports_1m("claude-opus-3-5"));
260 }
261
262 #[test]
263 fn model_supports_1m_assumed_for_5x() {
264 assert!(model_supports_1m("claude-opus-5-0"));
265 assert!(model_supports_1m("claude-sonnet-5-1"));
266 assert!(model_supports_1m("claude-fable-5"));
267 }
268
269 #[test]
270 fn model_supports_1m_case_insensitive() {
271 assert!(model_supports_1m("CLAUDE-OPUS-4-6"));
272 }
273
274 #[test]
275 fn context_window_haiku_is_200k() {
276 assert_eq!(
277 context_window_for_model("claude-haiku-4-5-20251001"),
278 200_000
279 );
280 }
281
282 #[test]
283 fn context_window_opus3_is_200k() {
284 assert_eq!(
285 context_window_for_model("claude-opus-3-5-20250101"),
286 200_000
287 );
288 }
289
290 #[test]
291 fn context_window_unknown_defaults_200k() {
292 assert_eq!(context_window_for_model("some-future-model"), 200_000);
293 assert_eq!(context_window_for_model(""), 200_000);
294 }
295
296 #[test]
297 fn adaptive_thinking_required_for_opus_4_7_plus() {
298 assert!(model_supports_adaptive_thinking("claude-opus-4-7"));
299 assert!(model_supports_adaptive_thinking("claude-opus-4-8"));
300 }
301
302 #[test]
303 fn adaptive_thinking_not_for_opus_4_6() {
304 assert!(!model_supports_adaptive_thinking("claude-opus-4-6"));
306 }
307
308 #[test]
309 fn adaptive_thinking_not_for_sonnet_4_6() {
310 assert!(!model_supports_adaptive_thinking("claude-sonnet-4-6"));
311 }
312
313 #[test]
314 fn adaptive_thinking_required_for_sonnet_4_7_plus() {
315 assert!(model_supports_adaptive_thinking("claude-sonnet-4-7"));
316 }
317
318 #[test]
319 fn adaptive_thinking_not_for_older_models() {
320 assert!(!model_supports_adaptive_thinking("claude-opus-4-5"));
321 assert!(!model_supports_adaptive_thinking("claude-sonnet-4-5"));
322 assert!(!model_supports_adaptive_thinking(
323 "claude-haiku-4-5-20251001"
324 ));
325 assert!(!model_supports_adaptive_thinking("claude-opus-3-5"));
326 }
327
328 #[test]
329 fn adaptive_thinking_assumed_for_5x() {
330 assert!(model_supports_adaptive_thinking("claude-opus-5-0"));
331 assert!(model_supports_adaptive_thinking("claude-sonnet-5-1"));
332 }
333
334 #[test]
335 fn opus_5_5_is_a_known_adaptive_1m_capable_model() {
336 assert!(KNOWN_MODELS.iter().any(|(id, _)| *id == "claude-opus-5-5"));
338 assert!(!KNOWN_MODELS.iter().any(|(id, _)| *id == "claude-opus-5.5"));
339 assert!(model_supports_adaptive_thinking("claude-opus-5-5"));
340 assert!(model_supports_1m("claude-opus-5-5"));
341 assert_ne!(default_model(), "claude-opus-5-5");
343 }
344
345 #[test]
346 fn adaptive_thinking_case_insensitive() {
347 assert!(model_supports_adaptive_thinking("CLAUDE-OPUS-4-7"));
348 }
349}