1#![doc = include_str!("../docs/catalog.md")]
2
3use crate::providers::local::discovery::discover_local_models;
4
5mod bedrock;
6
7pub use bedrock::BedrockModel;
8
9include!(concat!(env!("OUT_DIR"), "/generated.rs"));
10
11pub fn available_models() -> Vec<LlmModel> {
13 LlmModel::all()
14 .iter()
15 .filter(|m| m.required_env_var().is_none_or(|var| std::env::var(var).is_ok()))
16 .cloned()
17 .collect()
18}
19
20pub async fn get_local_models() -> Vec<LlmModel> {
22 let mut models = available_models();
23 let local = discover_local_models().await;
24 models.extend(local);
25 models
26}
27
28#[cfg(test)]
29mod tests {
30 use super::*;
31
32 #[test]
33 fn display_fromstr_roundtrip_all_catalog_models() {
34 for model in LlmModel::all() {
35 let s = model.to_string();
36 let parsed: LlmModel = s.parse().unwrap_or_else(|e| panic!("Failed to parse '{s}' back to LlmModel: {e}"));
37 assert_eq!(&parsed, model, "roundtrip failed for '{s}'");
38 }
39 }
40
41 #[test]
42 fn display_fromstr_roundtrip_dynamic_providers() {
43 let cases = [LlmModel::Ollama("llama3.2".to_string()), LlmModel::LlamaCpp("my-model".to_string())];
44 for model in &cases {
45 let s = model.to_string();
46 let parsed: LlmModel = s.parse().unwrap();
47 assert_eq!(&parsed, model);
48 }
49 }
50
51 #[test]
52 fn codex_gpt55_uses_subscription_context_window() {
53 let model: LlmModel = "codex:gpt-5.5".parse().unwrap();
54 assert_eq!(model.context_window(), Some(272_000));
55 }
56
57 #[test]
58 fn openai_gpt55_keeps_api_context_window() {
59 let model: LlmModel = "openai:gpt-5.5".parse().unwrap();
60 assert_eq!(model.context_window(), Some(1_050_000));
61 }
62
63 #[test]
64 fn bedrock_foundation_model_parses() {
65 let model: LlmModel = "bedrock:anthropic.claude-sonnet-4-5-20250929-v1:0".parse().unwrap();
66
67 assert_eq!(model.to_string(), "bedrock:anthropic.claude-sonnet-4-5-20250929-v1:0");
68 assert_eq!(model.context_window(), Some(200_000));
69 }
70
71 #[test]
72 fn bedrock_prompt_caching_support_comes_from_catalog() {
73 let claude: LlmModel = "bedrock:anthropic.claude-sonnet-4-5-20250929-v1:0".parse().unwrap();
74 let nova: LlmModel = "bedrock:amazon.nova-lite-v1:0".parse().unwrap();
75 let profile: LlmModel = "bedrock:us.anthropic.claude-future-model-v99:0".parse().unwrap();
76
77 assert!(claude.supports_prompt_caching());
78 assert!(nova.supports_prompt_caching());
79 assert!(!profile.supports_prompt_caching());
80 }
81
82 #[test]
83 fn provider_display_name_returns_human_readable() {
84 let anthropic: LlmModel = "anthropic:claude-opus-4-6".parse().unwrap();
85 assert_eq!(anthropic.provider_display_name(), "Anthropic");
86
87 let bedrock: LlmModel = "bedrock:anthropic.claude-haiku-4-5-20251001-v1:0".parse().unwrap();
88 assert_eq!(bedrock.provider_display_name(), "AWS Bedrock");
89
90 let zai: LlmModel = "zai:glm-4.5".parse().unwrap();
91 assert_eq!(zai.provider_display_name(), "ZAI");
92
93 let ollama = LlmModel::Ollama("llama3.2".to_string());
94 assert_eq!(ollama.provider_display_name(), "Ollama");
95 }
96
97 #[test]
98 fn oauth_provider_id_is_codex_only_for_codex_provider() {
99 let codex: LlmModel = "codex:gpt-5.5".parse().unwrap();
100 assert_eq!(codex.oauth_provider_id(), Some("codex"));
101
102 for non_oauth in
103 ["anthropic:claude-opus-4-6", "openai:gpt-5.5", "bedrock:anthropic.claude-sonnet-4-5-20250929-v1:0"]
104 {
105 let model: LlmModel = non_oauth.parse().unwrap();
106 assert_eq!(model.oauth_provider_id(), None, "{non_oauth} should not have OAuth");
107 }
108 assert_eq!(LlmModel::Ollama("foo".into()).oauth_provider_id(), None);
109 }
110
111 #[test]
112 fn required_env_var_matches_provider() {
113 let cases = [
114 ("anthropic:claude-opus-4-6", Some("ANTHROPIC_API_KEY")),
115 ("openai:gpt-5.5", Some("OPENAI_API_KEY")),
116 ("deepseek:deepseek-chat", Some("DEEPSEEK_API_KEY")),
117 ("gemini:gemini-2.5-pro", Some("GEMINI_API_KEY")),
118 ("openrouter:anthropic/claude-opus-4.6", Some("OPENROUTER_API_KEY")),
119 ("zai:glm-4.5", Some("ZAI_API_KEY")),
120 ("codex:gpt-5.5", None),
121 ("bedrock:anthropic.claude-sonnet-4-5-20250929-v1:0", None),
122 ];
123 for (input, expected) in cases {
124 let model: LlmModel = input.parse().unwrap();
125 assert_eq!(model.required_env_var(), expected, "{input}");
126 }
127 assert_eq!(LlmModel::Ollama("foo".into()).required_env_var(), None);
128 assert_eq!(LlmModel::LlamaCpp("foo".into()).required_env_var(), None);
129 }
130
131 #[test]
132 fn codex_reasoning_models_include_xhigh_level() {
133 let codex: LlmModel = "codex:gpt-5.5".parse().unwrap();
134 assert_eq!(
135 codex.reasoning_levels(),
136 &[ReasoningEffort::Low, ReasoningEffort::Medium, ReasoningEffort::High, ReasoningEffort::Xhigh]
137 );
138 assert!(codex.supports_reasoning());
139 }
140
141 #[test]
142 fn anthropic_reasoning_models_use_three_levels() {
143 let claude: LlmModel = "anthropic:claude-opus-4-6".parse().unwrap();
144 assert_eq!(claude.reasoning_levels(), &[ReasoningEffort::Low, ReasoningEffort::Medium, ReasoningEffort::High]);
145 }
146
147 #[test]
148 fn dynamic_provider_models_have_no_reasoning_levels() {
149 assert!(LlmModel::Ollama("llama3.2".into()).reasoning_levels().is_empty());
150 assert!(!LlmModel::Ollama("llama3.2".into()).supports_reasoning());
151 }
152
153 #[test]
154 fn supports_reasoning_matches_reasoning_levels_emptiness() {
155 for model in LlmModel::all() {
156 assert_eq!(model.supports_reasoning(), !model.reasoning_levels().is_empty(), "{model}");
157 }
158 }
159
160 #[test]
161 fn dynamic_providers_have_no_context_window() {
162 assert_eq!(LlmModel::Ollama("foo".into()).context_window(), None);
163 assert_eq!(LlmModel::LlamaCpp("foo".into()).context_window(), None);
164 }
165
166 #[test]
167 fn bedrock_profile_fallback_parses_arbitrary_id() {
168 let profile: LlmModel = "bedrock:us.anthropic.future-model-v99:0".parse().unwrap();
169 assert_eq!(profile.context_window(), None);
170 assert!(profile.reasoning_levels().is_empty());
171 assert!(!profile.supports_prompt_caching());
172 }
173}