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}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Activation {
33 OutOfBox,
35 RequiresOneTimeStep(&'static str),
37}
38
39#[derive(Debug, Clone)]
43pub struct CatalogModel {
44 pub public_id: &'static str,
45 pub cloud: Platform,
46 pub upstream_id: &'static str,
47 pub protocol: Protocol,
48}
49
50impl CatalogModel {
51 pub fn provider(&self) -> &'static str {
54 let id = self.public_id;
55 if id.starts_with("claude") {
56 "anthropic"
57 } else if id.starts_with("gpt") || id == "model-router" {
58 "openai"
59 } else if id.starts_with("gemini") || id.starts_with("gemma") {
60 "google"
61 } else if id.starts_with("qwen") {
62 "qwen"
63 } else if id.starts_with("deepseek") {
64 "deepseek"
65 } else if id.starts_with("mistral")
66 || id.starts_with("devstral")
67 || id.starts_with("magistral")
68 || id.starts_with("ministral")
69 {
70 "mistral"
71 } else if id.starts_with("minimax") {
72 "minimax"
73 } else if id.starts_with("kimi") {
74 "moonshotai"
75 } else if id.starts_with("nemotron") {
76 "nvidia"
77 } else if id.starts_with("glm") {
78 "zai"
79 } else if id.starts_with("palmyra") {
80 "writer"
81 } else {
82 "unknown"
83 }
84 }
85
86 pub fn display_name(&self) -> &'static str {
89 match self.public_id {
90 "gpt-oss-20b" => "GPT-OSS 20B",
91 "gpt-oss-120b" => "GPT-OSS 120B",
92 "gpt-oss-safeguard-20b" => "GPT-OSS Safeguard 20B",
93 "gpt-oss-safeguard-120b" => "GPT-OSS Safeguard 120B",
94 "deepseek-v3.2" => "DeepSeek V3.2",
95 "qwen3-32b" => "Qwen3 32B",
96 "qwen3-coder-30b" => "Qwen3 Coder 30B",
97 "qwen3-coder-next" => "Qwen3 Coder Next",
98 "qwen3-next-80b" => "Qwen3 Next 80B",
99 "qwen3-vl-235b" => "Qwen3 VL 235B",
100 "mistral-large-3" => "Mistral Large 3",
101 "devstral-2" => "Devstral 2",
102 "magistral-small" => "Magistral Small",
103 "ministral-3-14b" => "Ministral 3 14B",
104 "ministral-3-8b" => "Ministral 3 8B",
105 "ministral-3-3b" => "Ministral 3 3B",
106 "minimax-m2" => "MiniMax M2",
107 "minimax-m2.1" => "MiniMax M2.1",
108 "minimax-m2.5" => "MiniMax M2.5",
109 "kimi-k2.5" => "Kimi K2.5",
110 "nemotron-nano-9b" => "Nemotron Nano 9B",
111 "nemotron-nano-12b" => "Nemotron Nano 12B",
112 "nemotron-nano-3-30b" => "Nemotron Nano 3 30B",
113 "nemotron-super-3-120b" => "Nemotron Super 3 120B",
114 "gemma-3-4b" => "Gemma 3 4B",
115 "gemma-3-12b" => "Gemma 3 12B",
116 "gemma-3-27b" => "Gemma 3 27B",
117 "glm-4.7" => "GLM 4.7",
118 "glm-4.7-flash" => "GLM 4.7 Flash",
119 "glm-5" => "GLM 5",
120 "palmyra-vision-7b" => "Palmyra Vision 7B",
121 "claude-sonnet-5" => "Claude Sonnet 5",
122 "claude-opus-4.8" => "Claude Opus 4.8",
123 "claude-opus-4.7" => "Claude Opus 4.7",
124 "claude-opus-4.6" => "Claude Opus 4.6",
125 "claude-opus-4.5" => "Claude Opus 4.5",
126 "claude-opus-4.1" => "Claude Opus 4.1",
127 "claude-sonnet-4.6" => "Claude Sonnet 4.6",
128 "claude-sonnet-4.5" => "Claude Sonnet 4.5",
129 "claude-haiku-4.5" => "Claude Haiku 4.5",
130 "claude-fable-5" => "Claude Fable 5",
131 "claude-mythos-5" => "Claude Mythos 5",
132 "gemini-2.5-pro" => "Gemini 2.5 Pro",
133 "gemini-2.5-flash" => "Gemini 2.5 Flash",
134 "gemini-2.5-flash-lite" => "Gemini 2.5 Flash Lite",
135 "gemini-3.5-flash" => "Gemini 3.5 Flash",
136 "gemini-3.1-flash-lite" => "Gemini 3.1 Flash Lite",
137 "gpt-4.1" => "GPT-4.1",
138 "gpt-4o-mini" => "GPT-4o mini",
139 "model-router" => "Model Router",
140 other => other,
141 }
142 }
143
144 pub fn activation(&self) -> Activation {
147 if !self.public_id.starts_with("claude") {
148 return Activation::OutOfBox;
149 }
150 match self.cloud {
151 Platform::Aws => Activation::RequiresOneTimeStep(
152 "Submit the one-time Anthropic use-case form in the Bedrock console.",
153 ),
154 Platform::Gcp => Activation::RequiresOneTimeStep(
155 "Enable Claude in Vertex AI Model Garden and accept Anthropic's terms of service, one-time, in the Google Cloud console.",
156 ),
157 Platform::Azure => Activation::RequiresOneTimeStep(
158 "Accept the Marketplace terms and create the Claude deployment in the Microsoft Foundry portal (one-time).",
159 ),
160 _ => Activation::OutOfBox,
161 }
162 }
163}
164
165static CATALOG: &[CatalogModel] = &[
166 CatalogModel { public_id: "gpt-oss-20b", cloud: Platform::Aws, upstream_id: "openai.gpt-oss-20b-1:0", protocol: Protocol::OpenAi },
170 CatalogModel { public_id: "gpt-oss-120b", cloud: Platform::Aws, upstream_id: "openai.gpt-oss-120b-1:0", protocol: Protocol::OpenAi },
171 CatalogModel { public_id: "gpt-oss-safeguard-20b", cloud: Platform::Aws, upstream_id: "openai.gpt-oss-safeguard-20b", protocol: Protocol::OpenAi },
172 CatalogModel { public_id: "gpt-oss-safeguard-120b", cloud: Platform::Aws, upstream_id: "openai.gpt-oss-safeguard-120b", protocol: Protocol::OpenAi },
173 CatalogModel { public_id: "deepseek-v3.2", cloud: Platform::Aws, upstream_id: "deepseek.v3.2", protocol: Protocol::OpenAi },
174 CatalogModel { public_id: "qwen3-32b", cloud: Platform::Aws, upstream_id: "qwen.qwen3-32b-v1:0", protocol: Protocol::OpenAi },
175 CatalogModel { public_id: "qwen3-coder-30b", cloud: Platform::Aws, upstream_id: "qwen.qwen3-coder-30b-a3b-v1:0", protocol: Protocol::OpenAi },
176 CatalogModel { public_id: "qwen3-coder-next", cloud: Platform::Aws, upstream_id: "qwen.qwen3-coder-next", protocol: Protocol::OpenAi },
177 CatalogModel { public_id: "qwen3-next-80b", cloud: Platform::Aws, upstream_id: "qwen.qwen3-next-80b-a3b", protocol: Protocol::OpenAi },
178 CatalogModel { public_id: "qwen3-vl-235b", cloud: Platform::Aws, upstream_id: "qwen.qwen3-vl-235b-a22b", protocol: Protocol::OpenAi },
179 CatalogModel { public_id: "mistral-large-3", cloud: Platform::Aws, upstream_id: "mistral.mistral-large-3-675b-instruct", protocol: Protocol::OpenAi },
180 CatalogModel { public_id: "devstral-2", cloud: Platform::Aws, upstream_id: "mistral.devstral-2-123b", protocol: Protocol::OpenAi },
181 CatalogModel { public_id: "magistral-small", cloud: Platform::Aws, upstream_id: "mistral.magistral-small-2509", protocol: Protocol::OpenAi },
182 CatalogModel { public_id: "ministral-3-14b", cloud: Platform::Aws, upstream_id: "mistral.ministral-3-14b-instruct", protocol: Protocol::OpenAi },
183 CatalogModel { public_id: "ministral-3-8b", cloud: Platform::Aws, upstream_id: "mistral.ministral-3-8b-instruct", protocol: Protocol::OpenAi },
184 CatalogModel { public_id: "ministral-3-3b", cloud: Platform::Aws, upstream_id: "mistral.ministral-3-3b-instruct", protocol: Protocol::OpenAi },
185 CatalogModel { public_id: "minimax-m2", cloud: Platform::Aws, upstream_id: "minimax.minimax-m2", protocol: Protocol::OpenAi },
186 CatalogModel { public_id: "minimax-m2.1", cloud: Platform::Aws, upstream_id: "minimax.minimax-m2.1", protocol: Protocol::OpenAi },
187 CatalogModel { public_id: "minimax-m2.5", cloud: Platform::Aws, upstream_id: "minimax.minimax-m2.5", protocol: Protocol::OpenAi },
188 CatalogModel { public_id: "kimi-k2.5", cloud: Platform::Aws, upstream_id: "moonshotai.kimi-k2.5", protocol: Protocol::OpenAi },
189 CatalogModel { public_id: "nemotron-nano-9b", cloud: Platform::Aws, upstream_id: "nvidia.nemotron-nano-9b-v2", protocol: Protocol::OpenAi },
190 CatalogModel { public_id: "nemotron-nano-12b", cloud: Platform::Aws, upstream_id: "nvidia.nemotron-nano-12b-v2", protocol: Protocol::OpenAi },
191 CatalogModel { public_id: "nemotron-nano-3-30b", cloud: Platform::Aws, upstream_id: "nvidia.nemotron-nano-3-30b", protocol: Protocol::OpenAi },
192 CatalogModel { public_id: "nemotron-super-3-120b", cloud: Platform::Aws, upstream_id: "nvidia.nemotron-super-3-120b", protocol: Protocol::OpenAi },
193 CatalogModel { public_id: "gemma-3-4b", cloud: Platform::Aws, upstream_id: "google.gemma-3-4b-it", protocol: Protocol::OpenAi },
194 CatalogModel { public_id: "gemma-3-12b", cloud: Platform::Aws, upstream_id: "google.gemma-3-12b-it", protocol: Protocol::OpenAi },
195 CatalogModel { public_id: "gemma-3-27b", cloud: Platform::Aws, upstream_id: "google.gemma-3-27b-it", protocol: Protocol::OpenAi },
196 CatalogModel { public_id: "glm-4.7", cloud: Platform::Aws, upstream_id: "zai.glm-4.7", protocol: Protocol::OpenAi },
197 CatalogModel { public_id: "glm-4.7-flash", cloud: Platform::Aws, upstream_id: "zai.glm-4.7-flash", protocol: Protocol::OpenAi },
198 CatalogModel { public_id: "glm-5", cloud: Platform::Aws, upstream_id: "zai.glm-5", protocol: Protocol::OpenAi },
199 CatalogModel { public_id: "palmyra-vision-7b", cloud: Platform::Aws, upstream_id: "writer.palmyra-vision-7b", protocol: Protocol::OpenAi },
200 CatalogModel { public_id: "claude-sonnet-5", cloud: Platform::Aws, upstream_id: "anthropic.claude-sonnet-5", protocol: Protocol::Anthropic },
207 CatalogModel { public_id: "claude-opus-4.8", cloud: Platform::Aws, upstream_id: "anthropic.claude-opus-4-8", protocol: Protocol::Anthropic },
208 CatalogModel { public_id: "claude-opus-4.7", cloud: Platform::Aws, upstream_id: "anthropic.claude-opus-4-7", protocol: Protocol::Anthropic },
209 CatalogModel { public_id: "claude-opus-4.6", cloud: Platform::Aws, upstream_id: "anthropic.claude-opus-4-6-v1", protocol: Protocol::Anthropic },
210 CatalogModel { public_id: "claude-opus-4.5", cloud: Platform::Aws, upstream_id: "anthropic.claude-opus-4-5-20251101-v1:0", protocol: Protocol::Anthropic },
211 CatalogModel { public_id: "claude-opus-4.1", cloud: Platform::Aws, upstream_id: "anthropic.claude-opus-4-1-20250805-v1:0", protocol: Protocol::Anthropic },
212 CatalogModel { public_id: "claude-sonnet-4.6", cloud: Platform::Aws, upstream_id: "anthropic.claude-sonnet-4-6", protocol: Protocol::Anthropic },
213 CatalogModel { public_id: "claude-sonnet-4.5", cloud: Platform::Aws, upstream_id: "anthropic.claude-sonnet-4-5-20250929-v1:0", protocol: Protocol::Anthropic },
214 CatalogModel { public_id: "claude-haiku-4.5", cloud: Platform::Aws, upstream_id: "anthropic.claude-haiku-4-5-20251001-v1:0", protocol: Protocol::Anthropic },
215 CatalogModel { public_id: "claude-fable-5", cloud: Platform::Aws, upstream_id: "anthropic.claude-fable-5", protocol: Protocol::Anthropic },
216 CatalogModel { public_id: "claude-mythos-5", cloud: Platform::Aws, upstream_id: "anthropic.claude-mythos-5", protocol: Protocol::Anthropic },
217 CatalogModel { public_id: "gemini-2.5-pro", cloud: Platform::Gcp, upstream_id: "google/gemini-2.5-pro", protocol: Protocol::OpenAi },
220 CatalogModel { public_id: "gemini-2.5-flash", cloud: Platform::Gcp, upstream_id: "google/gemini-2.5-flash", protocol: Protocol::OpenAi },
221 CatalogModel { public_id: "gemini-2.5-flash-lite", cloud: Platform::Gcp, upstream_id: "google/gemini-2.5-flash-lite", protocol: Protocol::OpenAi },
222 CatalogModel { public_id: "gemini-3.5-flash", cloud: Platform::Gcp, upstream_id: "google/gemini-3.5-flash", protocol: Protocol::OpenAi },
223 CatalogModel { public_id: "gemini-3.1-flash-lite", cloud: Platform::Gcp, upstream_id: "google/gemini-3.1-flash-lite", protocol: Protocol::OpenAi },
224 CatalogModel { public_id: "claude-sonnet-5", cloud: Platform::Gcp, upstream_id: "claude-sonnet-5", protocol: Protocol::Anthropic },
229 CatalogModel { public_id: "claude-opus-4.8", cloud: Platform::Gcp, upstream_id: "claude-opus-4-8", protocol: Protocol::Anthropic },
230 CatalogModel { public_id: "claude-opus-4.7", cloud: Platform::Gcp, upstream_id: "claude-opus-4-7", protocol: Protocol::Anthropic },
231 CatalogModel { public_id: "claude-opus-4.6", cloud: Platform::Gcp, upstream_id: "claude-opus-4-6", protocol: Protocol::Anthropic },
232 CatalogModel { public_id: "claude-opus-4.5", cloud: Platform::Gcp, upstream_id: "claude-opus-4-5@20251101", protocol: Protocol::Anthropic },
233 CatalogModel { public_id: "claude-sonnet-4.6", cloud: Platform::Gcp, upstream_id: "claude-sonnet-4-6", protocol: Protocol::Anthropic },
234 CatalogModel { public_id: "claude-sonnet-4.5", cloud: Platform::Gcp, upstream_id: "claude-sonnet-4-5@20250929", protocol: Protocol::Anthropic },
235 CatalogModel { public_id: "claude-haiku-4.5", cloud: Platform::Gcp, upstream_id: "claude-haiku-4-5@20251001", protocol: Protocol::Anthropic },
236 CatalogModel { public_id: "claude-fable-5", cloud: Platform::Gcp, upstream_id: "claude-fable-5", protocol: Protocol::Anthropic },
237 CatalogModel { public_id: "gpt-4.1", cloud: Platform::Azure, upstream_id: "gpt-4.1", protocol: Protocol::OpenAi },
241 CatalogModel { public_id: "gpt-4o-mini", cloud: Platform::Azure, upstream_id: "gpt-4o-mini", protocol: Protocol::OpenAi },
242 CatalogModel { public_id: "model-router", cloud: Platform::Azure, upstream_id: "model-router", protocol: Protocol::OpenAi },
243 CatalogModel { public_id: "claude-sonnet-5", cloud: Platform::Azure, upstream_id: "claude-sonnet-5", protocol: Protocol::Anthropic },
252 CatalogModel { public_id: "claude-opus-4.8", cloud: Platform::Azure, upstream_id: "claude-opus-4-8", protocol: Protocol::Anthropic },
253 CatalogModel { public_id: "claude-opus-4.7", cloud: Platform::Azure, upstream_id: "claude-opus-4-7", protocol: Protocol::Anthropic },
254 CatalogModel { public_id: "claude-opus-4.6", cloud: Platform::Azure, upstream_id: "claude-opus-4-6", protocol: Protocol::Anthropic },
255 CatalogModel { public_id: "claude-opus-4.5", cloud: Platform::Azure, upstream_id: "claude-opus-4-5", protocol: Protocol::Anthropic },
256 CatalogModel { public_id: "claude-sonnet-4.6", cloud: Platform::Azure, upstream_id: "claude-sonnet-4-6", protocol: Protocol::Anthropic },
257 CatalogModel { public_id: "claude-sonnet-4.5", cloud: Platform::Azure, upstream_id: "claude-sonnet-4-5", protocol: Protocol::Anthropic },
258 CatalogModel { public_id: "claude-haiku-4.5", cloud: Platform::Azure, upstream_id: "claude-haiku-4-5", protocol: Protocol::Anthropic },
259 CatalogModel { public_id: "claude-fable-5", cloud: Platform::Azure, upstream_id: "claude-fable-5", protocol: Protocol::Anthropic },
260];
261
262static AZURE_DEPLOYMENTS: &[(&str, &str, &str)] = &[
266 ("gpt-4.1", "gpt-4.1", "2025-04-14"),
267 ("gpt-4o-mini", "gpt-4o-mini", "2024-07-18"),
268 ("model-router", "model-router", "2025-11-18"),
269];
270
271static RESPONSES_UPSTREAM: &[(&str, &str)] = &[
277 ("gpt-oss-20b", "openai.gpt-oss-20b"),
278 ("gpt-oss-120b", "openai.gpt-oss-120b"),
279];
280
281pub fn responses_upstream_id(public_id: &str) -> Option<&'static str> {
284 RESPONSES_UPSTREAM
285 .iter()
286 .find(|(public, _)| *public == public_id)
287 .map(|(_, upstream)| *upstream)
288}
289
290pub fn models_for(cloud: Platform) -> Vec<&'static CatalogModel> {
291 CATALOG.iter().filter(|m| m.cloud == cloud).collect()
292}
293
294pub fn lookup(public_id: &str) -> Option<&'static CatalogModel> {
299 CATALOG.iter().find(|m| m.public_id == public_id)
300}
301
302fn lookup_for(public_id: &str, cloud: Platform) -> Option<&'static CatalogModel> {
303 CATALOG.iter().find(|m| m.public_id == public_id && m.cloud == cloud)
304}
305
306pub fn resolve_for(model_id: &str, cloud: Platform) -> Option<&'static CatalogModel> {
311 lookup_for(model_id, cloud).or_else(|| lookup_for(&canonical_public_id(model_id), cloud))
312}
313
314pub fn resolve(model_id: &str) -> Option<&'static CatalogModel> {
329 lookup(model_id).or_else(|| lookup(&canonical_public_id(model_id)))
330}
331
332fn canonical_public_id(model_id: &str) -> String {
333 let mut id = model_id;
334 if let Some(pos) = id.rfind("anthropic.") {
335 id = &id[pos + "anthropic.".len()..];
336 }
337 id = id.split_once('@').map_or(id, |(base, _)| base);
339 id = strip_invoke_version(id);
340 id = strip_release_date(id);
341 dot_minor_version(id)
342}
343
344fn strip_invoke_version(id: &str) -> &str {
346 let base = id.split_once(':').map_or(id, |(base, _)| base);
347 match base.rsplit_once("-v") {
348 Some((stem, digits)) if !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit()) => {
349 stem
350 }
351 _ => base,
352 }
353}
354
355fn strip_release_date(id: &str) -> &str {
357 match id.rsplit_once('-') {
358 Some((stem, date))
359 if date.len() == 8 && date.starts_with("20") && date.bytes().all(|b| b.is_ascii_digit()) =>
360 {
361 stem
362 }
363 _ => id,
364 }
365}
366
367fn dot_minor_version(id: &str) -> String {
371 let Some((stem, minor)) = id.rsplit_once('-') else {
372 return id.to_string();
373 };
374 let Some((prefix, major)) = stem.rsplit_once('-') else {
375 return id.to_string();
376 };
377 let both_numeric = !major.is_empty()
378 && !minor.is_empty()
379 && major.bytes().all(|b| b.is_ascii_digit())
380 && minor.bytes().all(|b| b.is_ascii_digit());
381 if both_numeric {
382 format!("{prefix}-{major}.{minor}")
383 } else {
384 id.to_string()
385 }
386}
387
388pub fn azure_deployments() -> Vec<(&'static str, &'static str, &'static str)> {
390 AZURE_DEPLOYMENTS.to_vec()
391}
392
393#[cfg(test)]
394mod tests {
395 #[test]
399 fn public_ids_are_unique_per_cloud() {
400 let mut seen = std::collections::HashSet::new();
401 for model in super::CATALOG {
402 assert!(
403 seen.insert((model.cloud, model.public_id)),
404 "public id '{}' appears more than once under {:?}",
405 model.public_id,
406 model.cloud
407 );
408 }
409 }
410
411 use super::*;
412
413 #[test]
414 fn resolve_accepts_anthropic_native_spellings() {
415 assert_eq!(resolve("claude-haiku-4-5").unwrap().public_id, "claude-haiku-4.5");
417 assert_eq!(
418 resolve("claude-sonnet-4-5-20250929").unwrap().public_id,
419 "claude-sonnet-4.5"
420 );
421 assert_eq!(
423 resolve("us.anthropic.claude-haiku-4-5-20251001-v1:0").unwrap().public_id,
424 "claude-haiku-4.5"
425 );
426 assert_eq!(
427 resolve("anthropic.claude-opus-4-6-v1").unwrap().public_id,
428 "claude-opus-4.6"
429 );
430 assert_eq!(resolve("claude-sonnet-5").unwrap().public_id, "claude-sonnet-5");
432 assert_eq!(resolve("claude-opus-4.8").unwrap().public_id, "claude-opus-4.8");
434 assert_eq!(resolve("gpt-oss-20b").unwrap().public_id, "gpt-oss-20b");
435 assert!(resolve("claude-nonexistent-9-9").is_none());
437 assert!(resolve("gpt-5").is_none());
438 }
439
440 #[test]
441 fn aws_has_openai_and_anthropic_with_plain_ids() {
442 let aws = models_for(Platform::Aws);
443 assert!(!aws.is_empty());
444 assert!(aws
445 .iter()
446 .any(|m| m.public_id == "gpt-oss-20b" && m.protocol == Protocol::OpenAi));
447 assert!(
448 aws.iter().any(|m| m.protocol == Protocol::Anthropic),
449 "Claude must be included via the Anthropic protocol"
450 );
451 assert!(aws.iter().all(|m| !m.upstream_id.starts_with("us.")));
453 }
454
455 #[test]
456 fn resolve_for_scopes_to_cloud() {
457 let aws = resolve_for("claude-opus-4.8", Platform::Aws).expect("aws claude");
460 assert_eq!(aws.upstream_id, "anthropic.claude-opus-4-8");
461 let gcp = resolve_for("claude-opus-4.8", Platform::Gcp).expect("gcp claude");
462 assert_eq!(gcp.upstream_id, "claude-opus-4-8");
463 assert_eq!(gcp.protocol, Protocol::Anthropic);
464 let dated = resolve_for("claude-haiku-4-5-20251001", Platform::Gcp).expect("dated id");
467 assert_eq!(dated.upstream_id, "claude-haiku-4-5@20251001");
468 let vertex = resolve_for("claude-sonnet-4-5@20250929", Platform::Gcp).expect("vertex id");
471 assert_eq!(vertex.upstream_id, "claude-sonnet-4-5@20250929");
472 assert!(resolve_for("gemini-2.5-pro", Platform::Aws).is_none());
474 assert!(resolve_for("gpt-4.1", Platform::Gcp).is_none());
475 }
476
477 #[test]
478 fn lookup_round_trips() {
479 let m = lookup("gpt-oss-20b").expect("known model");
480 assert_eq!(m.cloud, Platform::Aws);
481 assert_eq!(m.protocol, Protocol::OpenAi);
482 assert_eq!(m.upstream_id, "openai.gpt-oss-20b-1:0");
483
484 let c = lookup("claude-opus-4.8").expect("claude known");
485 assert_eq!(c.protocol, Protocol::Anthropic);
486
487 assert!(lookup("nonexistent-model").is_none());
488 }
489
490 #[test]
491 fn azure_deployments_map_to_catalog() {
492 assert!(!azure_deployments().is_empty());
493 for (deployment, _, _) in azure_deployments() {
494 assert!(
495 models_for(Platform::Azure)
496 .iter()
497 .any(|m| m.upstream_id == deployment),
498 "azure deployment {deployment} must map to a catalog model"
499 );
500 }
501 }
502
503 #[test]
504 fn protocol_serializes_lowercase() {
505 assert_eq!(serde_json::to_string(&Protocol::OpenAi).unwrap(), "\"openai\"");
506 assert_eq!(serde_json::to_string(&Protocol::Anthropic).unwrap(), "\"anthropic\"");
507 }
508
509 #[test]
510 fn every_model_has_provider_display_name_and_activation() {
511 for m in CATALOG {
512 assert_ne!(m.provider(), "unknown", "no provider mapping for '{}'", m.public_id);
513 assert_ne!(
514 m.display_name(),
515 m.public_id,
516 "no curated display_name for '{}'",
517 m.public_id
518 );
519 let is_claude = m.public_id.starts_with("claude");
521 match m.activation() {
522 Activation::OutOfBox => {
523 assert!(!is_claude, "'{}' (Claude) must require a one-time step", m.public_id)
524 }
525 Activation::RequiresOneTimeStep(summary) => {
526 assert!(is_claude, "'{}' must be out of the box", m.public_id);
527 assert!(!summary.is_empty(), "'{}' step summary is empty", m.public_id);
528 }
529 }
530 }
531 }
532}