1#![doc = include_str!("../docs/catalog.md")]
2
3use crate::providers::local::discovery::discover_local_models;
4use crate::reasoning::ReasoningControl;
5use crate::{ReasoningDisabledSupport, ReasoningEffort};
6use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
7use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
8use std::borrow::Cow;
9use std::fmt;
10use std::str::FromStr;
11use std::sync::LazyLock;
12
13mod bedrock;
14mod model_spec;
15pub mod transport;
16
17pub use crate::usage::ModelPricing;
18pub use bedrock::BedrockModel;
19pub use model_spec::{ModelSpec, ModelSpecError, ReasoningEffortError, validate_reasoning_effort};
20pub use transport::ModelTransport;
21
22include!(concat!(env!("OUT_DIR"), "/generated.rs"));
23
24pub fn available_models() -> Vec<LlmModel> {
26 LlmModel::all()
27 .iter()
28 .filter(|m| m.required_env_var().is_none_or(|var| std::env::var(var).is_ok()))
29 .cloned()
30 .collect()
31}
32
33pub async fn get_local_models() -> Vec<LlmModel> {
35 let mut models = available_models();
36 let local = discover_local_models().await;
37 models.extend(local);
38 models
39}
40
41impl LlmModel {
42 pub(crate) fn parse_for(provider: Provider, model_id: &str) -> Result<Self, String> {
43 match provider {
44 Provider::Ollama => Ok(Self::Ollama(model_id.to_string())),
45 Provider::LlamaCpp => Ok(Self::LlamaCpp(model_id.to_string())),
46 Provider::Bedrock => model_id.parse().map(Self::Bedrock),
47 provider => Self::all()
48 .iter()
49 .find(|model| model.provider_enum() == provider && model.model_id() == model_id)
50 .cloned()
51 .ok_or_else(|| format!("Unknown {provider} model: '{model_id}'")),
52 }
53 }
54
55 pub fn model_id(&self) -> Cow<'static, str> {
57 match self.source() {
58 ModelSource::Catalog(entry) => Cow::Borrowed(entry.id),
59 ModelSource::Runtime(id) => Cow::Owned(id.to_string()),
60 }
61 }
62
63 pub fn display_name(&self) -> Cow<'static, str> {
65 match self.source() {
66 ModelSource::Catalog(entry) => Cow::Borrowed(entry.display_name),
67 ModelSource::Runtime(id) => Cow::Owned(format!("{} {id}", self.provider_display_name())),
68 }
69 }
70
71 pub fn provider(&self) -> &'static str {
73 self.provider_enum().parser_name()
74 }
75
76 pub fn provider_display_name(&self) -> &'static str {
78 self.provider_enum().display_name()
79 }
80
81 pub fn context_window(&self) -> Option<u32> {
83 self.entry().map(|entry| entry.context_window)
84 }
85
86 pub fn required_env_var(&self) -> Option<&'static str> {
88 self.provider_enum().required_env_var()
89 }
90
91 pub fn oauth_provider_id(&self) -> Option<&'static str> {
93 self.provider_enum().oauth_provider_id()
94 }
95
96 pub fn reasoning_levels(&self) -> &'static [ReasoningEffort] {
98 self.entry().map_or(&[], |entry| entry.reasoning_levels)
99 }
100
101 pub fn reasoning_disabled_support(&self) -> ReasoningDisabledSupport {
102 self.entry().map_or(ReasoningDisabledSupport::Unsupported, |entry| entry.disabled_support)
103 }
104
105 pub fn supports_reasoning_off(&self) -> bool {
107 self.reasoning_levels().contains(&ReasoningEffort::Disabled)
108 }
109
110 pub fn supports_reasoning_off_transport(&self) -> bool {
112 if !self.supports_reasoning_off() {
113 return false;
114 }
115 match self.provider_enum().info().reasoning_off {
116 ReasoningOff::Always => true,
117 ReasoningOff::Effort => self.reasoning_disabled_support() == ReasoningDisabledSupport::Effort,
118 ReasoningOff::ModelTransport => self.transport().is_some(),
119 ReasoningOff::Never => false,
120 }
121 }
122
123 pub fn effective_reasoning_levels(&self) -> Vec<ReasoningEffort> {
125 self.reasoning_levels()
126 .iter()
127 .copied()
128 .filter(|effort| *effort != ReasoningEffort::Disabled || self.supports_reasoning_off_transport())
129 .collect()
130 }
131
132 pub fn validate_reasoning_effort(&self, effort: ReasoningEffort) -> Result<(), ReasoningEffortError> {
133 let supported = self.effective_reasoning_levels();
134 if effort == ReasoningEffort::Default || supported.contains(&effort) {
135 Ok(())
136 } else {
137 Err(ReasoningEffortError::Unsupported { model: self.to_string(), effort, supported })
138 }
139 }
140
141 pub fn supports_reasoning(&self) -> bool {
143 self.reasoning_levels().iter().any(|effort| effort.is_enabled())
144 }
145
146 pub fn supports_prompt_caching(&self) -> bool {
148 self.entry().is_some_and(|entry| entry.supports_prompt_caching)
149 }
150
151 pub(crate) fn reasoning_control(&self) -> ReasoningControl {
152 self.entry().map(|entry| entry.reasoning_control).unwrap_or_default()
153 }
154
155 pub fn pricing(&self) -> Option<ModelPricing> {
156 self.entry().and_then(|entry| entry.pricing)
157 }
158
159 pub fn supports_image(&self) -> bool {
161 self.entry().is_some_and(|entry| entry.supports_image)
162 }
163
164 pub fn supports_audio(&self) -> bool {
166 self.entry().is_some_and(|entry| entry.supports_audio)
167 }
168
169 pub fn transport(&self) -> Option<ModelTransport> {
172 self.entry().and_then(|entry| entry.transport)
173 }
174
175 fn entry(&self) -> Option<&'static CatalogEntry> {
176 match self.source() {
177 ModelSource::Catalog(entry) => Some(entry),
178 ModelSource::Runtime(_) => None,
179 }
180 }
181}
182
183impl fmt::Display for LlmModel {
184 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185 write!(f, "{}:{}", self.provider(), self.model_id())
186 }
187}
188
189impl FromStr for LlmModel {
190 type Err = String;
191
192 fn from_str(s: &str) -> Result<Self, Self::Err> {
194 let (provider, model_id) = s.split_once(':').unwrap_or((s, ""));
195 Self::parse_for(provider.parse()?, model_id)
196 }
197}
198
199impl Serialize for LlmModel {
200 fn serialize<T: Serializer>(&self, serializer: T) -> Result<T::Ok, T::Error> {
201 serializer.collect_str(self)
202 }
203}
204
205impl<'de> Deserialize<'de> for LlmModel {
206 fn deserialize<T: Deserializer<'de>>(deserializer: T) -> Result<Self, T::Error> {
207 String::deserialize(deserializer)?.parse().map_err(de::Error::custom)
208 }
209}
210
211impl JsonSchema for LlmModel {
212 fn schema_name() -> Cow<'static, str> {
213 "LlmModel".into()
214 }
215
216 fn json_schema(_: &mut SchemaGenerator) -> Schema {
217 json_schema!({ "type": "string", "description": "A `provider:model` identifier." })
218 }
219}
220
221impl Provider {
222 pub fn parser_name(self) -> &'static str {
224 self.info().parser_name
225 }
226
227 pub fn genai_provider_name(self) -> &'static str {
229 self.info().genai_provider_name
230 }
231
232 pub fn display_name(self) -> &'static str {
234 self.info().display_name
235 }
236
237 pub fn required_env_var(self) -> Option<&'static str> {
239 self.info().required_env_var
240 }
241
242 pub fn env_vars(self) -> Vec<&'static str> {
243 let overrides: &[&'static str] = match self {
244 Self::Ollama => &["OLLAMA_HOST"],
245 Self::LlamaCpp => &["LLAMA_CPP_HOST"],
246 Self::Bedrock => AWS_ENV_VARS,
247 _ => &[],
248 };
249 self.required_env_var().into_iter().chain(overrides.iter().copied()).collect()
250 }
251
252 pub fn oauth_provider_id(self) -> Option<&'static str> {
254 self.info().oauth_provider_id
255 }
256
257 pub fn is_local(self) -> bool {
260 self.info().is_local
261 }
262
263 fn info(self) -> &'static ProviderInfo {
264 &Self::INFO[self as usize]
265 }
266}
267
268impl fmt::Display for Provider {
269 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270 f.write_str(self.parser_name())
271 }
272}
273
274impl FromStr for Provider {
275 type Err = String;
276
277 fn from_str(s: &str) -> Result<Self, Self::Err> {
278 Self::ALL
279 .iter()
280 .copied()
281 .find(|provider| provider.parser_name() == s)
282 .ok_or_else(|| format!("Unknown provider: '{s}'"))
283 }
284}
285
286const AWS_ENV_VARS: &[&str] = &[
287 "AWS_ACCESS_KEY_ID",
288 "AWS_BEARER_TOKEN_BEDROCK",
289 "AWS_DEFAULT_REGION",
290 "AWS_PROFILE",
291 "AWS_REGION",
292 "AWS_SECRET_ACCESS_KEY",
293 "AWS_SESSION_TOKEN",
294];
295
296enum ModelSource<'a> {
298 Catalog(&'static CatalogEntry),
300 Runtime(&'a str),
302}
303
304struct CatalogEntry {
306 id: &'static str,
307 display_name: &'static str,
308 context_window: u32,
309 reasoning_levels: &'static [ReasoningEffort],
310 disabled_support: ReasoningDisabledSupport,
311 supports_prompt_caching: bool,
312 reasoning_control: ReasoningControl,
313 pricing: Option<ModelPricing>,
314 supports_image: bool,
315 supports_audio: bool,
316 transport: Option<ModelTransport>,
317}
318
319struct ProviderInfo {
321 parser_name: &'static str,
322 genai_provider_name: &'static str,
323 display_name: &'static str,
324 required_env_var: Option<&'static str>,
325 oauth_provider_id: Option<&'static str>,
326 is_local: bool,
327 reasoning_off: ReasoningOff,
328}
329
330enum ReasoningOff {
332 Always,
334 Effort,
336 ModelTransport,
338 Never,
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 #[test]
347 fn display_fromstr_roundtrip_all_catalog_models() {
348 for model in LlmModel::all() {
349 let s = model.to_string();
350 let parsed: LlmModel = s.parse().unwrap_or_else(|e| panic!("Failed to parse '{s}' back to LlmModel: {e}"));
351 assert_eq!(&parsed, model, "roundtrip failed for '{s}'");
352 }
353 }
354
355 #[test]
356 fn display_fromstr_roundtrip_dynamic_providers() {
357 let cases = [LlmModel::Ollama("llama3.2".to_string()), LlmModel::LlamaCpp("my-model".to_string())];
358 for model in &cases {
359 let s = model.to_string();
360 let parsed: LlmModel = s.parse().unwrap();
361 assert_eq!(&parsed, model);
362 }
363 }
364
365 #[test]
366 fn model_pricing_comes_from_provider_catalog() {
367 let model: LlmModel = "anthropic:claude-sonnet-4-5".parse().unwrap();
368
369 assert_eq!(
370 model.pricing(),
371 Some(ModelPricing {
372 input_per_million: 3.0,
373 output_per_million: 15.0,
374 cache_read_per_million: Some(0.3),
375 cache_write_per_million: Some(3.75),
376 })
377 );
378 }
379
380 #[test]
381 fn models_without_usage_pricing_do_not_inherit_unrelated_prices() {
382 let codex: LlmModel = "codex:gpt-5.5".parse().unwrap();
383 let local: LlmModel = "ollama:local-model".parse().unwrap();
384 let bedrock_profile: LlmModel = "bedrock:us.anthropic.claude-future-model-v99:0".parse().unwrap();
385
386 assert_eq!(codex.pricing(), None);
387 assert_eq!(local.pricing(), None);
388 assert_eq!(bedrock_profile.pricing(), None);
389 }
390
391 #[test]
392 fn codex_gpt56_subscription_models_have_expected_metadata() {
393 let expected_levels = &[
394 ReasoningEffort::Low,
395 ReasoningEffort::Medium,
396 ReasoningEffort::High,
397 ReasoningEffort::Xhigh,
398 ReasoningEffort::Max,
399 ];
400 for id in ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"] {
401 let model: LlmModel = format!("codex:{id}").parse().unwrap();
402 assert_eq!(model.model_id(), id);
403 assert_eq!(model.context_window(), Some(272_000));
404 assert_eq!(model.reasoning_levels(), expected_levels);
405 }
406
407 assert!("codex:gpt-5.6".parse::<LlmModel>().is_err());
408 assert!("codex:gpt-5.1-codex".parse::<LlmModel>().is_err());
409 assert!("openai:gpt-5.6".parse::<LlmModel>().is_ok());
410 }
411
412 #[test]
413 fn codex_gpt6_models_are_available_with_subscription_metadata() {
414 let expected_levels = &[
415 ReasoningEffort::Low,
416 ReasoningEffort::Medium,
417 ReasoningEffort::High,
418 ReasoningEffort::Xhigh,
419 ReasoningEffort::Max,
420 ];
421 for id in ["gpt-6.1-sol", "gpt-6-sol", "gpt-6-astra", "gpt-6-luna"] {
422 let model: LlmModel = format!("codex:{id}").parse().unwrap();
423 assert_eq!(model.model_id(), id);
424 assert_eq!(model.context_window(), Some(272_000));
425 assert_eq!(model.reasoning_levels(), expected_levels);
426 assert_eq!(model.oauth_provider_id(), Some("codex"));
427 }
428 }
429
430 #[test]
431 fn codex_gpt55_uses_subscription_context_window() {
432 let model: LlmModel = "codex:gpt-5.5".parse().unwrap();
433 assert_eq!(model.context_window(), Some(272_000));
434 }
435
436 #[test]
437 fn openai_gpt55_keeps_api_context_window() {
438 let model: LlmModel = "openai:gpt-5.5".parse().unwrap();
439 assert_eq!(model.context_window(), Some(1_050_000));
440 }
441
442 #[test]
443 fn bedrock_foundation_model_parses() {
444 let model: LlmModel = "bedrock:anthropic.claude-sonnet-4-5-20250929-v1:0".parse().unwrap();
445
446 assert_eq!(model.to_string(), "bedrock:anthropic.claude-sonnet-4-5-20250929-v1:0");
447 assert_eq!(model.context_window(), Some(200_000));
448 }
449
450 #[test]
451 fn bedrock_prompt_caching_support_comes_from_catalog() {
452 let claude: LlmModel = "bedrock:anthropic.claude-sonnet-4-5-20250929-v1:0".parse().unwrap();
453 let nova: LlmModel = "bedrock:amazon.nova-lite-v1:0".parse().unwrap();
454 let profile: LlmModel = "bedrock:us.anthropic.claude-future-model-v99:0".parse().unwrap();
455
456 assert!(claude.supports_prompt_caching());
457 assert!(nova.supports_prompt_caching());
458 assert!(!profile.supports_prompt_caching());
459 }
460
461 #[test]
462 fn provider_display_name_returns_human_readable() {
463 let anthropic: LlmModel = "anthropic:claude-opus-4-6".parse().unwrap();
464 assert_eq!(anthropic.provider_display_name(), "Anthropic");
465
466 let bedrock: LlmModel = "bedrock:anthropic.claude-haiku-4-5-20251001-v1:0".parse().unwrap();
467 assert_eq!(bedrock.provider_display_name(), "AWS Bedrock");
468
469 let zai: LlmModel = "zai:glm-4.5".parse().unwrap();
470 assert_eq!(zai.provider_display_name(), "ZAI");
471
472 let ollama = LlmModel::Ollama("llama3.2".to_string());
473 assert_eq!(ollama.provider_display_name(), "Ollama");
474 }
475
476 #[test]
477 fn oauth_provider_id_is_codex_only_for_codex_provider() {
478 let codex: LlmModel = "codex:gpt-5.5".parse().unwrap();
479 assert_eq!(codex.oauth_provider_id(), Some("codex"));
480
481 for non_oauth in
482 ["anthropic:claude-opus-4-6", "openai:gpt-5.5", "bedrock:anthropic.claude-sonnet-4-5-20250929-v1:0"]
483 {
484 let model: LlmModel = non_oauth.parse().unwrap();
485 assert_eq!(model.oauth_provider_id(), None, "{non_oauth} should not have OAuth");
486 }
487 assert_eq!(LlmModel::Ollama("foo".into()).oauth_provider_id(), None);
488 }
489
490 #[test]
491 fn required_env_var_matches_provider() {
492 let cases = [
493 ("anthropic:claude-opus-4-6", Some("ANTHROPIC_API_KEY")),
494 ("openai:gpt-5.5", Some("OPENAI_API_KEY")),
495 ("deepseek:deepseek-v4-flash", Some("DEEPSEEK_API_KEY")),
496 ("gemini:gemini-2.5-pro", Some("GEMINI_API_KEY")),
497 ("openrouter:anthropic/claude-opus-4.6", Some("OPENROUTER_API_KEY")),
498 ("zai:glm-4.5", Some("ZAI_API_KEY")),
499 ("xiaomi:mimo-v2.6-pro", Some("XIAOMI_API_KEY")),
500 ("codex:gpt-5.5", None),
501 ("bedrock:anthropic.claude-sonnet-4-5-20250929-v1:0", None),
502 ];
503 for (input, expected) in cases {
504 let model: LlmModel = input.parse().unwrap();
505 assert_eq!(model.required_env_var(), expected, "{input}");
506 }
507 assert_eq!(LlmModel::Ollama("foo".into()).required_env_var(), None);
508 assert_eq!(LlmModel::LlamaCpp("foo".into()).required_env_var(), None);
509 }
510
511 #[test]
512 fn xiaomi_models_parse_with_catalog_metadata() {
513 let model: LlmModel = "xiaomi:mimo-v2.6-pro".parse().unwrap();
514 assert_eq!(model.model_id(), "mimo-v2.6-pro");
515 assert_eq!(model.context_window(), Some(1_048_576));
516 assert_eq!(model.provider_display_name(), "Xiaomi");
517 assert!(model.supports_reasoning());
518 }
519
520 #[test]
521 fn codex_reasoning_models_include_xhigh_level() {
522 let codex: LlmModel = "codex:gpt-5.5".parse().unwrap();
523 assert_eq!(
524 codex.reasoning_levels(),
525 &[ReasoningEffort::Low, ReasoningEffort::Medium, ReasoningEffort::High, ReasoningEffort::Xhigh]
526 );
527 assert!(codex.supports_reasoning());
528 }
529
530 #[test]
531 fn anthropic_reasoning_models_use_catalog_levels() {
532 let claude: LlmModel = "anthropic:claude-opus-4-6".parse().unwrap();
533 assert_eq!(
534 claude.reasoning_levels(),
535 &[ReasoningEffort::Low, ReasoningEffort::Medium, ReasoningEffort::High, ReasoningEffort::Max,]
536 );
537 }
538
539 #[test]
540 fn dynamic_provider_models_have_no_reasoning_levels() {
541 assert_eq!(LlmModel::Ollama("llama3.2".into()).reasoning_levels(), []);
542 assert!(!LlmModel::Ollama("llama3.2".into()).supports_reasoning());
543 }
544
545 #[test]
546 fn supports_reasoning_matches_reasoning_levels_emptiness() {
547 for model in LlmModel::all() {
548 assert_eq!(model.supports_reasoning(), !model.reasoning_levels().is_empty(), "{model}");
549 }
550 }
551
552 #[test]
553 fn dynamic_providers_have_no_context_window() {
554 assert_eq!(LlmModel::Ollama("foo".into()).context_window(), None);
555 assert_eq!(LlmModel::LlamaCpp("foo".into()).context_window(), None);
556 }
557
558 #[test]
559 fn bedrock_responses_models_carry_their_endpoint_and_wire_shape() {
560 let cases = [
561 ("openai.gpt-6.1-sol", "https://bedrock-mantle.${AWS_REGION}.api.aws/openai/v1"),
562 ("us.openai.gpt-6.1-sol", "https://bedrock-runtime.${AWS_REGION}.amazonaws.com/openai/v1"),
563 ("openai.gpt-5.6-luna", "https://bedrock-mantle.${AWS_REGION}.api.aws/openai/v1"),
564 ("openai.gpt-5.6-sol", "https://bedrock-mantle.${AWS_REGION}.api.aws/openai/v1"),
565 ("openai.gpt-5.6-terra", "https://bedrock-mantle.${AWS_REGION}.api.aws/openai/v1"),
566 ("openai.gpt-6-astra", "https://bedrock-mantle.${AWS_REGION}.api.aws/openai/v1"),
567 ("us.openai.gpt-6-luna", "https://bedrock-runtime.${AWS_REGION}.amazonaws.com/openai/v1"),
568 ("us.openai.gpt-6-sol", "https://bedrock-runtime.${AWS_REGION}.amazonaws.com/openai/v1"),
569 ("us.openai.gpt-6-astra", "https://bedrock-runtime.${AWS_REGION}.amazonaws.com/openai/v1"),
570 ("openai.gpt-5.5", "https://bedrock-mantle.${AWS_REGION}.api.aws/openai/v1"),
571 ("openai.gpt-5.4", "https://bedrock-mantle.${AWS_REGION}.api.aws/openai/v1"),
572 ("xai.grok-4.3", "https://bedrock-mantle.${AWS_REGION}.api.aws/openai/v1"),
573 ("openai.gpt-oss-120b", "https://bedrock-mantle.${AWS_REGION}.api.aws/v1"),
574 ("openai.gpt-oss-20b", "https://bedrock-mantle.${AWS_REGION}.api.aws/v1"),
575 ];
576
577 for (id, expected_api) in cases {
578 let model: LlmModel = format!("bedrock:{id}").parse().unwrap();
579 assert_eq!(
580 model.transport(),
581 Some(ModelTransport::OpenAiResponses { base_url_template: expected_api }),
582 "{id}"
583 );
584 }
585 }
586
587 #[test]
588 fn bedrock_claude_models_use_the_messages_transport() {
589 for model in bedrock_claude_models() {
590 assert_eq!(model.transport(), Some(ModelTransport::AnthropicMessages), "{model}");
591 }
592 }
593
594 #[test]
595 fn bedrock_claude_models_turn_reasoning_off_when_the_catalog_allows_it() {
596 for model in bedrock_claude_models() {
597 assert_eq!(model.supports_reasoning_off_transport(), model.supports_reasoning_off(), "{model}");
598 }
599 }
600
601 #[test]
602 fn models_outside_the_catalog_default_to_effort_reasoning_control() {
603 let profile: LlmModel = "bedrock:us.anthropic.claude-future-model-v99:0".parse().unwrap();
604
605 assert_eq!(profile.reasoning_control(), ReasoningControl::Effort);
606 assert_eq!(LlmModel::Ollama("llama3.2".into()).reasoning_control(), ReasoningControl::Effort);
607 }
608
609 #[test]
610 fn converse_models_and_profiles_declare_no_transport_override() {
611 for id in [
612 "amazon.nova-lite-v1:0",
613 "us.anthropic.claude-future-model-v99:0",
614 "global.openai.gpt-6-luna",
615 "global.openai.gpt-6-sol",
616 "global.openai.gpt-6.1-sol",
617 "global.openai.gpt-6-astra",
618 ] {
619 let model: LlmModel = format!("bedrock:{id}").parse().unwrap();
620 assert_eq!(model.transport(), None, "{id}");
621 }
622 assert_eq!(LlmModel::Ollama("llama3.2".into()).transport(), None);
623 assert_eq!("anthropic:claude-opus-4-6".parse::<LlmModel>().unwrap().transport(), None);
624 }
625
626 #[test]
627 fn every_declared_transport_template_resolves() {
628 for model in LlmModel::all() {
629 let Some(ModelTransport::OpenAiResponses { base_url_template }) = model.transport() else {
630 continue;
631 };
632 let resolved = utils::variables::Vars::new()
633 .with_env_lookup(|_| Some("test-region".to_string()))
634 .expand(base_url_template)
635 .unwrap_or_else(|e| panic!("{model} has an unresolvable endpoint template: {e}"));
636 assert!(resolved.starts_with("https://"), "{model} resolved to '{resolved}'");
637 assert!(!resolved.contains("${"), "{model} left a placeholder in '{resolved}'");
638 }
639 }
640
641 #[test]
642 fn bedrock_profile_fallback_parses_arbitrary_id() {
643 let profile: LlmModel = "bedrock:us.anthropic.future-model-v99:0".parse().unwrap();
644 assert_eq!(profile.context_window(), None);
645 assert_eq!(profile.reasoning_levels(), []);
646 assert!(!profile.supports_prompt_caching());
647 }
648
649 fn bedrock_claude_models() -> Vec<&'static LlmModel> {
650 let models: Vec<_> = LlmModel::all()
651 .iter()
652 .filter(|model| {
653 model.provider_enum() == Provider::Bedrock && model.model_id().contains("anthropic.claude-")
654 })
655 .collect();
656 assert!(!models.is_empty(), "the catalog must list Bedrock Claude models");
657 models
658 }
659}