1use crate::Platform;
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "lowercase")]
20pub enum Protocol {
21 OpenAi,
23 Anthropic,
25 OpenAiResponses,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum Activation {
36 OutOfBox,
38 RequiresOneTimeStep(&'static str),
40}
41
42#[derive(Debug, Clone)]
46pub struct CatalogModel {
47 pub public_id: &'static str,
48 pub cloud: Platform,
49 pub upstream_id: &'static str,
50 pub protocol: Protocol,
51}
52
53impl CatalogModel {
54 pub fn provider(&self) -> &'static str {
57 let id = self.public_id;
58 if id.starts_with("claude") {
59 "anthropic"
60 } else if id.starts_with("gpt") || id == "model-router" {
61 "openai"
62 } else if id.starts_with("gemini") || id.starts_with("gemma") {
63 "google"
64 } else if id.starts_with("qwen") {
65 "qwen"
66 } else if id.starts_with("deepseek") {
67 "deepseek"
68 } else if id.starts_with("mistral")
69 || id.starts_with("devstral")
70 || id.starts_with("magistral")
71 || id.starts_with("ministral")
72 {
73 "mistral"
74 } else if id.starts_with("minimax") {
75 "minimax"
76 } else if id.starts_with("kimi") {
77 "moonshotai"
78 } else if id.starts_with("nemotron") {
79 "nvidia"
80 } else if id.starts_with("glm") {
81 "zai"
82 } else if id.starts_with("palmyra") {
83 "writer"
84 } else {
85 "unknown"
86 }
87 }
88
89 pub fn display_name(&self) -> &'static str {
92 match self.public_id {
93 "gpt-5.6-sol" => "GPT-5.6 Sol",
94 "gpt-5.6-terra" => "GPT-5.6 Terra",
95 "gpt-5.6-luna" => "GPT-5.6 Luna",
96 "gpt-5.5" => "GPT-5.5",
97 "gpt-5.4" => "GPT-5.4",
98 "gpt-oss-20b" => "GPT-OSS 20B",
99 "gpt-oss-120b" => "GPT-OSS 120B",
100 "gpt-oss-safeguard-20b" => "GPT-OSS Safeguard 20B",
101 "gpt-oss-safeguard-120b" => "GPT-OSS Safeguard 120B",
102 "deepseek-v3.2" => "DeepSeek V3.2",
103 "qwen3-32b" => "Qwen3 32B",
104 "qwen3-coder-30b" => "Qwen3 Coder 30B",
105 "qwen3-coder-next" => "Qwen3 Coder Next",
106 "qwen3-next-80b" => "Qwen3 Next 80B",
107 "qwen3-vl-235b" => "Qwen3 VL 235B",
108 "mistral-large-3" => "Mistral Large 3",
109 "devstral-2" => "Devstral 2",
110 "magistral-small" => "Magistral Small",
111 "ministral-3-14b" => "Ministral 3 14B",
112 "ministral-3-8b" => "Ministral 3 8B",
113 "ministral-3-3b" => "Ministral 3 3B",
114 "minimax-m2" => "MiniMax M2",
115 "minimax-m2.1" => "MiniMax M2.1",
116 "minimax-m2.5" => "MiniMax M2.5",
117 "kimi-k2.5" => "Kimi K2.5",
118 "nemotron-nano-9b" => "Nemotron Nano 9B",
119 "nemotron-nano-12b" => "Nemotron Nano 12B",
120 "nemotron-nano-3-30b" => "Nemotron Nano 3 30B",
121 "nemotron-super-3-120b" => "Nemotron Super 3 120B",
122 "gemma-3-4b" => "Gemma 3 4B",
123 "gemma-3-12b" => "Gemma 3 12B",
124 "gemma-3-27b" => "Gemma 3 27B",
125 "glm-4.7" => "GLM 4.7",
126 "glm-4.7-flash" => "GLM 4.7 Flash",
127 "glm-5" => "GLM 5",
128 "palmyra-vision-7b" => "Palmyra Vision 7B",
129 "claude-opus-5" => "Claude Opus 5",
130 "claude-sonnet-5" => "Claude Sonnet 5",
131 "claude-opus-4.8" => "Claude Opus 4.8",
132 "claude-opus-4.7" => "Claude Opus 4.7",
133 "claude-opus-4.6" => "Claude Opus 4.6",
134 "claude-opus-4.5" => "Claude Opus 4.5",
135 "claude-opus-4.1" => "Claude Opus 4.1",
136 "claude-sonnet-4.6" => "Claude Sonnet 4.6",
137 "claude-sonnet-4.5" => "Claude Sonnet 4.5",
138 "claude-haiku-4.5" => "Claude Haiku 4.5",
139 "claude-fable-5" => "Claude Fable 5",
140 "claude-mythos-5" => "Claude Mythos 5",
141 "claude-sonnet-4" => "Claude Sonnet 4",
142 "claude-3-haiku" => "Claude 3 Haiku",
143 "gemini-2.5-pro" => "Gemini 2.5 Pro",
144 "gemini-2.5-flash" => "Gemini 2.5 Flash",
145 "gemini-2.5-flash-lite" => "Gemini 2.5 Flash Lite",
146 "gemini-3.5-flash" => "Gemini 3.5 Flash",
147 "gemini-3.1-flash-lite" => "Gemini 3.1 Flash Lite",
148 "gpt-4.1" => "GPT-4.1",
149 "gpt-4o-mini" => "GPT-4o mini",
150 "model-router" => "Model Router",
151 other => other,
152 }
153 }
154
155 pub fn activation(&self) -> Activation {
158 if !self.public_id.starts_with("claude") {
159 return Activation::OutOfBox;
160 }
161 match self.cloud {
162 Platform::Aws => Activation::RequiresOneTimeStep(
163 "Submit the one-time Anthropic use-case form in the Bedrock console.",
164 ),
165 Platform::Gcp => Activation::RequiresOneTimeStep(
166 "Enable Claude in Vertex AI Model Garden and accept Anthropic's terms of service, one-time, in the Google Cloud console.",
167 ),
168 Platform::Azure => Activation::RequiresOneTimeStep(
169 "Accept the Marketplace terms and create the Claude deployment in the Microsoft Foundry portal (one-time).",
170 ),
171 _ => Activation::OutOfBox,
172 }
173 }
174}
175
176static CATALOG: &[CatalogModel] = &[
177 CatalogModel { public_id: "gpt-5.6-sol", cloud: Platform::Aws, upstream_id: "openai.gpt-5.6-sol", protocol: Protocol::OpenAiResponses },
184 CatalogModel { public_id: "gpt-5.6-terra", cloud: Platform::Aws, upstream_id: "openai.gpt-5.6-terra", protocol: Protocol::OpenAiResponses },
185 CatalogModel { public_id: "gpt-5.6-luna", cloud: Platform::Aws, upstream_id: "openai.gpt-5.6-luna", protocol: Protocol::OpenAiResponses },
186 CatalogModel { public_id: "gpt-5.5", cloud: Platform::Aws, upstream_id: "openai.gpt-5.5", protocol: Protocol::OpenAiResponses },
187 CatalogModel { public_id: "gpt-5.4", cloud: Platform::Aws, upstream_id: "openai.gpt-5.4", protocol: Protocol::OpenAiResponses },
188 CatalogModel { public_id: "gpt-oss-20b", cloud: Platform::Aws, upstream_id: "openai.gpt-oss-20b-1:0", protocol: Protocol::OpenAi },
189 CatalogModel { public_id: "gpt-oss-120b", cloud: Platform::Aws, upstream_id: "openai.gpt-oss-120b-1:0", protocol: Protocol::OpenAi },
190 CatalogModel { public_id: "gpt-oss-safeguard-20b", cloud: Platform::Aws, upstream_id: "openai.gpt-oss-safeguard-20b", protocol: Protocol::OpenAi },
191 CatalogModel { public_id: "gpt-oss-safeguard-120b", cloud: Platform::Aws, upstream_id: "openai.gpt-oss-safeguard-120b", protocol: Protocol::OpenAi },
192 CatalogModel { public_id: "deepseek-v3.2", cloud: Platform::Aws, upstream_id: "deepseek.v3.2", protocol: Protocol::OpenAi },
193 CatalogModel { public_id: "qwen3-32b", cloud: Platform::Aws, upstream_id: "qwen.qwen3-32b-v1:0", protocol: Protocol::OpenAi },
194 CatalogModel { public_id: "qwen3-coder-30b", cloud: Platform::Aws, upstream_id: "qwen.qwen3-coder-30b-a3b-v1:0", protocol: Protocol::OpenAi },
195 CatalogModel { public_id: "qwen3-coder-next", cloud: Platform::Aws, upstream_id: "qwen.qwen3-coder-next", protocol: Protocol::OpenAi },
196 CatalogModel { public_id: "qwen3-next-80b", cloud: Platform::Aws, upstream_id: "qwen.qwen3-next-80b-a3b", protocol: Protocol::OpenAi },
197 CatalogModel { public_id: "qwen3-vl-235b", cloud: Platform::Aws, upstream_id: "qwen.qwen3-vl-235b-a22b", protocol: Protocol::OpenAi },
198 CatalogModel { public_id: "mistral-large-3", cloud: Platform::Aws, upstream_id: "mistral.mistral-large-3-675b-instruct", protocol: Protocol::OpenAi },
199 CatalogModel { public_id: "devstral-2", cloud: Platform::Aws, upstream_id: "mistral.devstral-2-123b", protocol: Protocol::OpenAi },
200 CatalogModel { public_id: "magistral-small", cloud: Platform::Aws, upstream_id: "mistral.magistral-small-2509", protocol: Protocol::OpenAi },
201 CatalogModel { public_id: "ministral-3-14b", cloud: Platform::Aws, upstream_id: "mistral.ministral-3-14b-instruct", protocol: Protocol::OpenAi },
202 CatalogModel { public_id: "ministral-3-8b", cloud: Platform::Aws, upstream_id: "mistral.ministral-3-8b-instruct", protocol: Protocol::OpenAi },
203 CatalogModel { public_id: "ministral-3-3b", cloud: Platform::Aws, upstream_id: "mistral.ministral-3-3b-instruct", protocol: Protocol::OpenAi },
204 CatalogModel { public_id: "minimax-m2", cloud: Platform::Aws, upstream_id: "minimax.minimax-m2", protocol: Protocol::OpenAi },
205 CatalogModel { public_id: "minimax-m2.1", cloud: Platform::Aws, upstream_id: "minimax.minimax-m2.1", protocol: Protocol::OpenAi },
206 CatalogModel { public_id: "minimax-m2.5", cloud: Platform::Aws, upstream_id: "minimax.minimax-m2.5", protocol: Protocol::OpenAi },
207 CatalogModel { public_id: "kimi-k2.5", cloud: Platform::Aws, upstream_id: "moonshotai.kimi-k2.5", protocol: Protocol::OpenAi },
208 CatalogModel { public_id: "nemotron-nano-9b", cloud: Platform::Aws, upstream_id: "nvidia.nemotron-nano-9b-v2", protocol: Protocol::OpenAi },
209 CatalogModel { public_id: "nemotron-nano-12b", cloud: Platform::Aws, upstream_id: "nvidia.nemotron-nano-12b-v2", protocol: Protocol::OpenAi },
210 CatalogModel { public_id: "nemotron-nano-3-30b", cloud: Platform::Aws, upstream_id: "nvidia.nemotron-nano-3-30b", protocol: Protocol::OpenAi },
211 CatalogModel { public_id: "nemotron-super-3-120b", cloud: Platform::Aws, upstream_id: "nvidia.nemotron-super-3-120b", protocol: Protocol::OpenAi },
212 CatalogModel { public_id: "gemma-3-4b", cloud: Platform::Aws, upstream_id: "google.gemma-3-4b-it", protocol: Protocol::OpenAi },
213 CatalogModel { public_id: "gemma-3-12b", cloud: Platform::Aws, upstream_id: "google.gemma-3-12b-it", protocol: Protocol::OpenAi },
214 CatalogModel { public_id: "gemma-3-27b", cloud: Platform::Aws, upstream_id: "google.gemma-3-27b-it", protocol: Protocol::OpenAi },
215 CatalogModel { public_id: "glm-4.7", cloud: Platform::Aws, upstream_id: "zai.glm-4.7", protocol: Protocol::OpenAi },
216 CatalogModel { public_id: "glm-4.7-flash", cloud: Platform::Aws, upstream_id: "zai.glm-4.7-flash", protocol: Protocol::OpenAi },
217 CatalogModel { public_id: "glm-5", cloud: Platform::Aws, upstream_id: "zai.glm-5", protocol: Protocol::OpenAi },
218 CatalogModel { public_id: "palmyra-vision-7b", cloud: Platform::Aws, upstream_id: "writer.palmyra-vision-7b", protocol: Protocol::OpenAi },
219 CatalogModel { public_id: "claude-opus-5", cloud: Platform::Aws, upstream_id: "anthropic.claude-opus-5", protocol: Protocol::Anthropic },
226 CatalogModel { public_id: "claude-sonnet-5", cloud: Platform::Aws, upstream_id: "anthropic.claude-sonnet-5", protocol: Protocol::Anthropic },
227 CatalogModel { public_id: "claude-opus-4.8", cloud: Platform::Aws, upstream_id: "anthropic.claude-opus-4-8", protocol: Protocol::Anthropic },
228 CatalogModel { public_id: "claude-opus-4.7", cloud: Platform::Aws, upstream_id: "anthropic.claude-opus-4-7", protocol: Protocol::Anthropic },
229 CatalogModel { public_id: "claude-opus-4.6", cloud: Platform::Aws, upstream_id: "anthropic.claude-opus-4-6-v1", protocol: Protocol::Anthropic },
230 CatalogModel { public_id: "claude-opus-4.5", cloud: Platform::Aws, upstream_id: "anthropic.claude-opus-4-5-20251101-v1:0", protocol: Protocol::Anthropic },
231 CatalogModel { public_id: "claude-opus-4.1", cloud: Platform::Aws, upstream_id: "anthropic.claude-opus-4-1-20250805-v1:0", protocol: Protocol::Anthropic },
232 CatalogModel { public_id: "claude-sonnet-4.6", cloud: Platform::Aws, upstream_id: "anthropic.claude-sonnet-4-6", protocol: Protocol::Anthropic },
233 CatalogModel { public_id: "claude-sonnet-4.5", cloud: Platform::Aws, upstream_id: "anthropic.claude-sonnet-4-5-20250929-v1:0", protocol: Protocol::Anthropic },
234 CatalogModel { public_id: "claude-haiku-4.5", cloud: Platform::Aws, upstream_id: "anthropic.claude-haiku-4-5-20251001-v1:0", protocol: Protocol::Anthropic },
235 CatalogModel { public_id: "claude-fable-5", cloud: Platform::Aws, upstream_id: "anthropic.claude-fable-5", protocol: Protocol::Anthropic },
236 CatalogModel { public_id: "claude-mythos-5", cloud: Platform::Aws, upstream_id: "anthropic.claude-mythos-5", protocol: Protocol::Anthropic },
237 CatalogModel { public_id: "claude-sonnet-4", cloud: Platform::Aws, upstream_id: "anthropic.claude-sonnet-4-20250514-v1:0", protocol: Protocol::Anthropic },
238 CatalogModel { public_id: "claude-3-haiku", cloud: Platform::Aws, upstream_id: "anthropic.claude-3-haiku-20240307-v1:0", protocol: Protocol::Anthropic },
239 CatalogModel { public_id: "gemini-2.5-pro", cloud: Platform::Gcp, upstream_id: "google/gemini-2.5-pro", protocol: Protocol::OpenAi },
242 CatalogModel { public_id: "gemini-2.5-flash", cloud: Platform::Gcp, upstream_id: "google/gemini-2.5-flash", protocol: Protocol::OpenAi },
243 CatalogModel { public_id: "gemini-2.5-flash-lite", cloud: Platform::Gcp, upstream_id: "google/gemini-2.5-flash-lite", protocol: Protocol::OpenAi },
244 CatalogModel { public_id: "gemini-3.5-flash", cloud: Platform::Gcp, upstream_id: "google/gemini-3.5-flash", protocol: Protocol::OpenAi },
245 CatalogModel { public_id: "gemini-3.1-flash-lite", cloud: Platform::Gcp, upstream_id: "google/gemini-3.1-flash-lite", protocol: Protocol::OpenAi },
246 CatalogModel { public_id: "claude-sonnet-5", cloud: Platform::Gcp, upstream_id: "claude-sonnet-5", protocol: Protocol::Anthropic },
251 CatalogModel { public_id: "claude-opus-4.8", cloud: Platform::Gcp, upstream_id: "claude-opus-4-8", protocol: Protocol::Anthropic },
252 CatalogModel { public_id: "claude-opus-4.7", cloud: Platform::Gcp, upstream_id: "claude-opus-4-7", protocol: Protocol::Anthropic },
253 CatalogModel { public_id: "claude-opus-4.6", cloud: Platform::Gcp, upstream_id: "claude-opus-4-6", protocol: Protocol::Anthropic },
254 CatalogModel { public_id: "claude-opus-4.5", cloud: Platform::Gcp, upstream_id: "claude-opus-4-5@20251101", protocol: Protocol::Anthropic },
255 CatalogModel { public_id: "claude-sonnet-4.6", cloud: Platform::Gcp, upstream_id: "claude-sonnet-4-6", protocol: Protocol::Anthropic },
256 CatalogModel { public_id: "claude-sonnet-4.5", cloud: Platform::Gcp, upstream_id: "claude-sonnet-4-5@20250929", protocol: Protocol::Anthropic },
257 CatalogModel { public_id: "claude-haiku-4.5", cloud: Platform::Gcp, upstream_id: "claude-haiku-4-5@20251001", protocol: Protocol::Anthropic },
258 CatalogModel { public_id: "claude-fable-5", cloud: Platform::Gcp, upstream_id: "claude-fable-5", protocol: Protocol::Anthropic },
259 CatalogModel { public_id: "gpt-4.1", cloud: Platform::Azure, upstream_id: "gpt-4.1", protocol: Protocol::OpenAi },
263 CatalogModel { public_id: "gpt-4o-mini", cloud: Platform::Azure, upstream_id: "gpt-4o-mini", protocol: Protocol::OpenAi },
264 CatalogModel { public_id: "model-router", cloud: Platform::Azure, upstream_id: "model-router", protocol: Protocol::OpenAi },
265 CatalogModel { public_id: "claude-sonnet-5", cloud: Platform::Azure, upstream_id: "claude-sonnet-5", protocol: Protocol::Anthropic },
274 CatalogModel { public_id: "claude-opus-4.8", cloud: Platform::Azure, upstream_id: "claude-opus-4-8", protocol: Protocol::Anthropic },
275 CatalogModel { public_id: "claude-opus-4.7", cloud: Platform::Azure, upstream_id: "claude-opus-4-7", protocol: Protocol::Anthropic },
276 CatalogModel { public_id: "claude-opus-4.6", cloud: Platform::Azure, upstream_id: "claude-opus-4-6", protocol: Protocol::Anthropic },
277 CatalogModel { public_id: "claude-opus-4.5", cloud: Platform::Azure, upstream_id: "claude-opus-4-5", protocol: Protocol::Anthropic },
278 CatalogModel { public_id: "claude-sonnet-4.6", cloud: Platform::Azure, upstream_id: "claude-sonnet-4-6", protocol: Protocol::Anthropic },
279 CatalogModel { public_id: "claude-sonnet-4.5", cloud: Platform::Azure, upstream_id: "claude-sonnet-4-5", protocol: Protocol::Anthropic },
280 CatalogModel { public_id: "claude-haiku-4.5", cloud: Platform::Azure, upstream_id: "claude-haiku-4-5", protocol: Protocol::Anthropic },
281 CatalogModel { public_id: "claude-fable-5", cloud: Platform::Azure, upstream_id: "claude-fable-5", protocol: Protocol::Anthropic },
282];
283
284static AZURE_DEPLOYMENTS: &[(&str, &str, &str)] = &[
288 ("gpt-4.1", "gpt-4.1", "2025-04-14"),
289 ("gpt-4o-mini", "gpt-4o-mini", "2024-07-18"),
290 ("model-router", "model-router", "2025-11-18"),
291];
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq)]
295pub struct ResponsesTarget {
296 pub upstream_id: &'static str,
298 pub path: &'static str,
301}
302
303static RESPONSES_UPSTREAM: &[(&str, ResponsesTarget)] = &[
308 ("gpt-oss-20b", ResponsesTarget { upstream_id: "openai.gpt-oss-20b", path: "/v1/responses" }),
309 ("gpt-oss-120b", ResponsesTarget { upstream_id: "openai.gpt-oss-120b", path: "/v1/responses" }),
310 ("gpt-5.6-sol", ResponsesTarget { upstream_id: "openai.gpt-5.6-sol", path: "/openai/v1/responses" }),
311 ("gpt-5.6-terra", ResponsesTarget { upstream_id: "openai.gpt-5.6-terra", path: "/openai/v1/responses" }),
312 ("gpt-5.6-luna", ResponsesTarget { upstream_id: "openai.gpt-5.6-luna", path: "/openai/v1/responses" }),
313 ("gpt-5.5", ResponsesTarget { upstream_id: "openai.gpt-5.5", path: "/openai/v1/responses" }),
314 ("gpt-5.4", ResponsesTarget { upstream_id: "openai.gpt-5.4", path: "/openai/v1/responses" }),
315];
316
317pub fn responses_target(public_id: &str) -> Option<ResponsesTarget> {
320 RESPONSES_UPSTREAM
321 .iter()
322 .find(|(public, _)| *public == public_id)
323 .map(|(_, target)| *target)
324}
325
326pub fn models_for(cloud: Platform) -> Vec<&'static CatalogModel> {
327 CATALOG.iter().filter(|m| m.cloud == cloud).collect()
328}
329
330pub fn lookup(public_id: &str) -> Option<&'static CatalogModel> {
335 CATALOG.iter().find(|m| m.public_id == public_id)
336}
337
338fn lookup_for(public_id: &str, cloud: Platform) -> Option<&'static CatalogModel> {
339 CATALOG.iter().find(|m| m.public_id == public_id && m.cloud == cloud)
340}
341
342pub fn resolve_for(model_id: &str, cloud: Platform) -> Option<&'static CatalogModel> {
347 lookup_for(model_id, cloud).or_else(|| lookup_for(&canonical_public_id(model_id), cloud))
348}
349
350pub fn resolve(model_id: &str) -> Option<&'static CatalogModel> {
365 lookup(model_id).or_else(|| lookup(&canonical_public_id(model_id)))
366}
367
368fn canonical_public_id(model_id: &str) -> String {
369 let mut id = model_id;
370 if let Some(pos) = id.rfind("anthropic.") {
371 id = &id[pos + "anthropic.".len()..];
372 }
373 id = id.split_once('@').map_or(id, |(base, _)| base);
375 id = strip_invoke_version(id);
376 id = strip_release_date(id);
377 dot_minor_version(id)
378}
379
380fn strip_invoke_version(id: &str) -> &str {
382 let base = id.split_once(':').map_or(id, |(base, _)| base);
383 match base.rsplit_once("-v") {
384 Some((stem, digits)) if !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit()) => {
385 stem
386 }
387 _ => base,
388 }
389}
390
391fn strip_release_date(id: &str) -> &str {
393 match id.rsplit_once('-') {
394 Some((stem, date))
395 if date.len() == 8 && date.starts_with("20") && date.bytes().all(|b| b.is_ascii_digit()) =>
396 {
397 stem
398 }
399 _ => id,
400 }
401}
402
403fn dot_minor_version(id: &str) -> String {
407 let Some((stem, minor)) = id.rsplit_once('-') else {
408 return id.to_string();
409 };
410 let Some((prefix, major)) = stem.rsplit_once('-') else {
411 return id.to_string();
412 };
413 let both_numeric = !major.is_empty()
414 && !minor.is_empty()
415 && major.bytes().all(|b| b.is_ascii_digit())
416 && minor.bytes().all(|b| b.is_ascii_digit());
417 if both_numeric {
418 format!("{prefix}-{major}.{minor}")
419 } else {
420 id.to_string()
421 }
422}
423
424pub fn azure_deployments() -> Vec<(&'static str, &'static str, &'static str)> {
426 AZURE_DEPLOYMENTS.to_vec()
427}
428
429#[cfg(test)]
430mod tests {
431 #[test]
435 fn public_ids_are_unique_per_cloud() {
436 let mut seen = std::collections::HashSet::new();
437 for model in super::CATALOG {
438 assert!(
439 seen.insert((model.cloud, model.public_id)),
440 "public id '{}' appears more than once under {:?}",
441 model.public_id,
442 model.cloud
443 );
444 }
445 }
446
447 use super::*;
448
449 #[test]
450 fn resolve_accepts_anthropic_native_spellings() {
451 assert_eq!(resolve("claude-haiku-4-5").unwrap().public_id, "claude-haiku-4.5");
453 assert_eq!(
454 resolve("claude-sonnet-4-5-20250929").unwrap().public_id,
455 "claude-sonnet-4.5"
456 );
457 assert_eq!(
459 resolve("us.anthropic.claude-haiku-4-5-20251001-v1:0").unwrap().public_id,
460 "claude-haiku-4.5"
461 );
462 assert_eq!(
463 resolve("anthropic.claude-opus-4-6-v1").unwrap().public_id,
464 "claude-opus-4.6"
465 );
466 assert_eq!(resolve("claude-sonnet-5").unwrap().public_id, "claude-sonnet-5");
468 assert_eq!(resolve("claude-opus-4.8").unwrap().public_id, "claude-opus-4.8");
470 assert_eq!(resolve("gpt-oss-20b").unwrap().public_id, "gpt-oss-20b");
471 assert!(resolve("claude-nonexistent-9-9").is_none());
473 assert!(resolve("gpt-5").is_none());
474 }
475
476 #[test]
477 fn aws_has_openai_and_anthropic_with_plain_ids() {
478 let aws = models_for(Platform::Aws);
479 assert!(!aws.is_empty());
480 assert!(aws
481 .iter()
482 .any(|m| m.public_id == "gpt-oss-20b" && m.protocol == Protocol::OpenAi));
483 assert!(
484 aws.iter().any(|m| m.protocol == Protocol::Anthropic),
485 "Claude must be included via the Anthropic protocol"
486 );
487 assert!(aws.iter().all(|m| !m.upstream_id.starts_with("us.")));
489 }
490
491 #[test]
492 fn resolve_for_scopes_to_cloud() {
493 let aws = resolve_for("claude-opus-4.8", Platform::Aws).expect("aws claude");
496 assert_eq!(aws.upstream_id, "anthropic.claude-opus-4-8");
497 let gcp = resolve_for("claude-opus-4.8", Platform::Gcp).expect("gcp claude");
498 assert_eq!(gcp.upstream_id, "claude-opus-4-8");
499 assert_eq!(gcp.protocol, Protocol::Anthropic);
500 let dated = resolve_for("claude-haiku-4-5-20251001", Platform::Gcp).expect("dated id");
503 assert_eq!(dated.upstream_id, "claude-haiku-4-5@20251001");
504 let vertex = resolve_for("claude-sonnet-4-5@20250929", Platform::Gcp).expect("vertex id");
507 assert_eq!(vertex.upstream_id, "claude-sonnet-4-5@20250929");
508 assert!(resolve_for("gemini-2.5-pro", Platform::Aws).is_none());
510 assert!(resolve_for("gpt-4.1", Platform::Gcp).is_none());
511 }
512
513 #[test]
514 fn lookup_round_trips() {
515 let m = lookup("gpt-oss-20b").expect("known model");
516 assert_eq!(m.cloud, Platform::Aws);
517 assert_eq!(m.protocol, Protocol::OpenAi);
518 assert_eq!(m.upstream_id, "openai.gpt-oss-20b-1:0");
519
520 let c = lookup("claude-opus-4.8").expect("claude known");
521 assert_eq!(c.protocol, Protocol::Anthropic);
522
523 assert!(lookup("nonexistent-model").is_none());
524 }
525
526 #[test]
527 fn azure_deployments_map_to_catalog() {
528 assert!(!azure_deployments().is_empty());
529 for (deployment, _, _) in azure_deployments() {
530 assert!(
531 models_for(Platform::Azure)
532 .iter()
533 .any(|m| m.upstream_id == deployment),
534 "azure deployment {deployment} must map to a catalog model"
535 );
536 }
537 }
538
539 #[test]
540 fn protocol_serializes_lowercase() {
541 assert_eq!(serde_json::to_string(&Protocol::OpenAi).unwrap(), "\"openai\"");
542 assert_eq!(serde_json::to_string(&Protocol::Anthropic).unwrap(), "\"anthropic\"");
543 assert_eq!(
544 serde_json::to_string(&Protocol::OpenAiResponses).unwrap(),
545 "\"openairesponses\""
546 );
547 }
548
549 #[test]
550 fn every_model_has_provider_display_name_and_activation() {
551 for m in CATALOG {
552 assert_ne!(m.provider(), "unknown", "no provider mapping for '{}'", m.public_id);
553 assert_ne!(
554 m.display_name(),
555 m.public_id,
556 "no curated display_name for '{}'",
557 m.public_id
558 );
559 let is_claude = m.public_id.starts_with("claude");
561 match m.activation() {
562 Activation::OutOfBox => {
563 assert!(!is_claude, "'{}' (Claude) must require a one-time step", m.public_id)
564 }
565 Activation::RequiresOneTimeStep(summary) => {
566 assert!(is_claude, "'{}' must be out of the box", m.public_id);
567 assert!(!summary.is_empty(), "'{}' step summary is empty", m.public_id);
568 }
569 }
570 }
571 }
572
573 #[test]
577 fn only_claude_ids_speak_the_anthropic_protocol() {
578 for m in CATALOG {
579 assert_eq!(
580 m.protocol == Protocol::Anthropic,
581 m.public_id.starts_with("claude"),
582 "'{}' is {:?} but its id says otherwise",
583 m.public_id,
584 m.protocol
585 );
586 }
587 }
588}