1use crate::Platform;
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
19#[serde(rename_all = "kebab-case")]
20pub enum ClientApi {
21 OpenAiChatCompletions,
22 OpenAiResponses,
23 AnthropicMessages,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
31#[serde(rename_all = "lowercase")]
32pub enum ProviderApi {
33 OpenAi,
35 Anthropic,
37 OpenAiResponses,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum Activation {
48 OutOfBox,
50 RequiresOneTimeStep(&'static str),
52}
53
54#[derive(Debug, Clone)]
58pub struct CatalogModel {
59 pub public_id: &'static str,
60 pub cloud: Platform,
61 pub upstream_id: &'static str,
62 pub client_apis: &'static [ClientApi],
63 pub provider_api: ProviderApi,
64}
65
66#[derive(Debug, Clone, Copy)]
69pub struct DirectAnthropicModel {
70 pub public_id: &'static str,
71 pub upstream_id: &'static str,
72}
73
74#[derive(Debug, Clone, Copy)]
81pub struct DirectOpenAiModel {
82 pub public_id: &'static str,
83 pub client_apis: &'static [ClientApi],
84}
85
86#[derive(Debug, Clone, Copy)]
88pub struct DirectDatabricksModel {
89 pub public_id: &'static str,
90 pub upstream_id: &'static str,
91 pub client_apis: &'static [ClientApi],
92}
93
94impl DirectAnthropicModel {
95 pub fn display_name(&self) -> &'static str {
96 resolve(self.public_id)
97 .map(CatalogModel::display_name)
98 .unwrap_or(self.public_id)
99 }
100}
101
102impl CatalogModel {
103 pub fn provider(&self) -> &'static str {
106 let id = self.public_id;
107 if id.starts_with("claude") {
108 "anthropic"
109 } else if id.starts_with("gpt") || id == "model-router" {
110 "openai"
111 } else if id.starts_with("gemini") || id.starts_with("gemma") {
112 "google"
113 } else if id.starts_with("qwen") {
114 "qwen"
115 } else if id.starts_with("deepseek") {
116 "deepseek"
117 } else if id.starts_with("mistral")
118 || id.starts_with("devstral")
119 || id.starts_with("magistral")
120 || id.starts_with("ministral")
121 {
122 "mistral"
123 } else if id.starts_with("minimax") {
124 "minimax"
125 } else if id.starts_with("kimi") {
126 "moonshotai"
127 } else if id.starts_with("nemotron") {
128 "nvidia"
129 } else if id.starts_with("glm") {
130 "zai"
131 } else if id.starts_with("palmyra") {
132 "writer"
133 } else {
134 "unknown"
135 }
136 }
137
138 pub fn display_name(&self) -> &'static str {
141 match self.public_id {
142 "gpt-5.6-sol" => "GPT-5.6 Sol",
143 "gpt-5.6-terra" => "GPT-5.6 Terra",
144 "gpt-5.6-luna" => "GPT-5.6 Luna",
145 "gpt-5.5" => "GPT-5.5",
146 "gpt-5.4" => "GPT-5.4",
147 "gpt-oss-20b" => "GPT-OSS 20B",
148 "gpt-oss-120b" => "GPT-OSS 120B",
149 "gpt-oss-safeguard-20b" => "GPT-OSS Safeguard 20B",
150 "gpt-oss-safeguard-120b" => "GPT-OSS Safeguard 120B",
151 "deepseek-v3.2" => "DeepSeek V3.2",
152 "qwen3-32b" => "Qwen3 32B",
153 "qwen3-coder-30b" => "Qwen3 Coder 30B",
154 "qwen3-next-80b" => "Qwen3 Next 80B",
155 "qwen3-vl-235b" => "Qwen3 VL 235B",
156 "mistral-large-3" => "Mistral Large 3",
157 "devstral-2" => "Devstral 2",
158 "magistral-small" => "Magistral Small",
159 "ministral-3-14b" => "Ministral 3 14B",
160 "ministral-3-8b" => "Ministral 3 8B",
161 "ministral-3-3b" => "Ministral 3 3B",
162 "minimax-m2" => "MiniMax M2",
163 "minimax-m2.1" => "MiniMax M2.1",
164 "minimax-m2.5" => "MiniMax M2.5",
165 "kimi-k2.5" => "Kimi K2.5",
166 "nemotron-nano-9b" => "Nemotron Nano 9B",
167 "nemotron-nano-12b" => "Nemotron Nano 12B",
168 "nemotron-nano-3-30b" => "Nemotron Nano 3 30B",
169 "nemotron-super-3-120b" => "Nemotron Super 3 120B",
170 "gemma-3-4b" => "Gemma 3 4B",
171 "gemma-3-12b" => "Gemma 3 12B",
172 "gemma-3-27b" => "Gemma 3 27B",
173 "glm-4.7" => "GLM 4.7",
174 "glm-4.7-flash" => "GLM 4.7 Flash",
175 "glm-5" => "GLM 5",
176 "palmyra-vision-7b" => "Palmyra Vision 7B",
177 "claude-opus-5" => "Claude Opus 5",
178 "claude-sonnet-5" => "Claude Sonnet 5",
179 "claude-opus-4.8" => "Claude Opus 4.8",
180 "claude-opus-4.7" => "Claude Opus 4.7",
181 "claude-opus-4.6" => "Claude Opus 4.6",
182 "claude-opus-4.5" => "Claude Opus 4.5",
183 "claude-sonnet-4.6" => "Claude Sonnet 4.6",
184 "claude-sonnet-4.5" => "Claude Sonnet 4.5",
185 "claude-haiku-4.5" => "Claude Haiku 4.5",
186 "claude-fable-5" => "Claude Fable 5",
187 "gemini-2.5-pro" => "Gemini 2.5 Pro",
188 "gemini-2.5-flash" => "Gemini 2.5 Flash",
189 "gemini-2.5-flash-lite" => "Gemini 2.5 Flash Lite",
190 "gemini-3.5-flash" => "Gemini 3.5 Flash",
191 "gemini-3.1-flash-lite" => "Gemini 3.1 Flash Lite",
192 "gpt-4.1" => "GPT-4.1",
193 "gpt-4o-mini" => "GPT-4o mini",
194 "model-router" => "Model Router",
195 other => other,
196 }
197 }
198
199 pub fn activation(&self) -> Activation {
202 if !self.public_id.starts_with("claude") {
203 return Activation::OutOfBox;
204 }
205 match self.cloud {
206 Platform::Aws => Activation::RequiresOneTimeStep(
207 "Submit the one-time Anthropic use-case form in the Bedrock console.",
208 ),
209 Platform::Gcp => Activation::RequiresOneTimeStep(
210 "Enable Claude in Vertex AI Model Garden and accept Anthropic's terms of service, one-time, in the Google Cloud console.",
211 ),
212 Platform::Azure => Activation::RequiresOneTimeStep(
213 "Accept the Marketplace terms and create the Claude deployment in the Microsoft Foundry portal (one-time).",
214 ),
215 _ => Activation::OutOfBox,
216 }
217 }
218}
219
220static CATALOG: &[CatalogModel] = &[
221 CatalogModel {
227 public_id: "gpt-5.6-sol",
228 cloud: Platform::Aws,
229 upstream_id: "openai.gpt-5.6-sol",
230 client_apis: &[ClientApi::OpenAiResponses],
231 provider_api: ProviderApi::OpenAiResponses,
232 },
233 CatalogModel {
234 public_id: "gpt-5.6-terra",
235 cloud: Platform::Aws,
236 upstream_id: "openai.gpt-5.6-terra",
237 client_apis: &[ClientApi::OpenAiResponses],
238 provider_api: ProviderApi::OpenAiResponses,
239 },
240 CatalogModel {
241 public_id: "gpt-5.6-luna",
242 cloud: Platform::Aws,
243 upstream_id: "openai.gpt-5.6-luna",
244 client_apis: &[ClientApi::OpenAiResponses],
245 provider_api: ProviderApi::OpenAiResponses,
246 },
247 CatalogModel {
248 public_id: "gpt-5.5",
249 cloud: Platform::Aws,
250 upstream_id: "openai.gpt-5.5",
251 client_apis: &[ClientApi::OpenAiResponses],
252 provider_api: ProviderApi::OpenAiResponses,
253 },
254 CatalogModel {
255 public_id: "gpt-5.4",
256 cloud: Platform::Aws,
257 upstream_id: "openai.gpt-5.4",
258 client_apis: &[ClientApi::OpenAiResponses],
259 provider_api: ProviderApi::OpenAiResponses,
260 },
261 CatalogModel {
262 public_id: "gpt-oss-20b",
263 cloud: Platform::Aws,
264 upstream_id: "openai.gpt-oss-20b-1:0",
265 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
266 provider_api: ProviderApi::OpenAi,
267 },
268 CatalogModel {
269 public_id: "gpt-oss-120b",
270 cloud: Platform::Aws,
271 upstream_id: "openai.gpt-oss-120b-1:0",
272 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
273 provider_api: ProviderApi::OpenAi,
274 },
275 CatalogModel {
276 public_id: "gpt-oss-safeguard-20b",
277 cloud: Platform::Aws,
278 upstream_id: "openai.gpt-oss-safeguard-20b",
279 client_apis: &[ClientApi::OpenAiChatCompletions],
280 provider_api: ProviderApi::OpenAi,
281 },
282 CatalogModel {
283 public_id: "gpt-oss-safeguard-120b",
284 cloud: Platform::Aws,
285 upstream_id: "openai.gpt-oss-safeguard-120b",
286 client_apis: &[ClientApi::OpenAiChatCompletions],
287 provider_api: ProviderApi::OpenAi,
288 },
289 CatalogModel {
290 public_id: "deepseek-v3.2",
291 cloud: Platform::Aws,
292 upstream_id: "deepseek.v3.2",
293 client_apis: &[ClientApi::OpenAiChatCompletions],
294 provider_api: ProviderApi::OpenAi,
295 },
296 CatalogModel {
297 public_id: "qwen3-32b",
298 cloud: Platform::Aws,
299 upstream_id: "qwen.qwen3-32b-v1:0",
300 client_apis: &[ClientApi::OpenAiChatCompletions],
301 provider_api: ProviderApi::OpenAi,
302 },
303 CatalogModel {
304 public_id: "qwen3-coder-30b",
305 cloud: Platform::Aws,
306 upstream_id: "qwen.qwen3-coder-30b-a3b-v1:0",
307 client_apis: &[ClientApi::OpenAiChatCompletions],
308 provider_api: ProviderApi::OpenAi,
309 },
310 CatalogModel {
311 public_id: "qwen3-next-80b",
312 cloud: Platform::Aws,
313 upstream_id: "qwen.qwen3-next-80b-a3b",
314 client_apis: &[ClientApi::OpenAiChatCompletions],
315 provider_api: ProviderApi::OpenAi,
316 },
317 CatalogModel {
318 public_id: "qwen3-vl-235b",
319 cloud: Platform::Aws,
320 upstream_id: "qwen.qwen3-vl-235b-a22b",
321 client_apis: &[ClientApi::OpenAiChatCompletions],
322 provider_api: ProviderApi::OpenAi,
323 },
324 CatalogModel {
325 public_id: "mistral-large-3",
326 cloud: Platform::Aws,
327 upstream_id: "mistral.mistral-large-3-675b-instruct",
328 client_apis: &[ClientApi::OpenAiChatCompletions],
329 provider_api: ProviderApi::OpenAi,
330 },
331 CatalogModel {
332 public_id: "devstral-2",
333 cloud: Platform::Aws,
334 upstream_id: "mistral.devstral-2-123b",
335 client_apis: &[ClientApi::OpenAiChatCompletions],
336 provider_api: ProviderApi::OpenAi,
337 },
338 CatalogModel {
339 public_id: "magistral-small",
340 cloud: Platform::Aws,
341 upstream_id: "mistral.magistral-small-2509",
342 client_apis: &[ClientApi::OpenAiChatCompletions],
343 provider_api: ProviderApi::OpenAi,
344 },
345 CatalogModel {
346 public_id: "ministral-3-14b",
347 cloud: Platform::Aws,
348 upstream_id: "mistral.ministral-3-14b-instruct",
349 client_apis: &[ClientApi::OpenAiChatCompletions],
350 provider_api: ProviderApi::OpenAi,
351 },
352 CatalogModel {
353 public_id: "ministral-3-8b",
354 cloud: Platform::Aws,
355 upstream_id: "mistral.ministral-3-8b-instruct",
356 client_apis: &[ClientApi::OpenAiChatCompletions],
357 provider_api: ProviderApi::OpenAi,
358 },
359 CatalogModel {
360 public_id: "ministral-3-3b",
361 cloud: Platform::Aws,
362 upstream_id: "mistral.ministral-3-3b-instruct",
363 client_apis: &[ClientApi::OpenAiChatCompletions],
364 provider_api: ProviderApi::OpenAi,
365 },
366 CatalogModel {
367 public_id: "minimax-m2",
368 cloud: Platform::Aws,
369 upstream_id: "minimax.minimax-m2",
370 client_apis: &[ClientApi::OpenAiChatCompletions],
371 provider_api: ProviderApi::OpenAi,
372 },
373 CatalogModel {
374 public_id: "minimax-m2.1",
375 cloud: Platform::Aws,
376 upstream_id: "minimax.minimax-m2.1",
377 client_apis: &[ClientApi::OpenAiChatCompletions],
378 provider_api: ProviderApi::OpenAi,
379 },
380 CatalogModel {
381 public_id: "minimax-m2.5",
382 cloud: Platform::Aws,
383 upstream_id: "minimax.minimax-m2.5",
384 client_apis: &[ClientApi::OpenAiChatCompletions],
385 provider_api: ProviderApi::OpenAi,
386 },
387 CatalogModel {
388 public_id: "kimi-k2.5",
389 cloud: Platform::Aws,
390 upstream_id: "moonshotai.kimi-k2.5",
391 client_apis: &[ClientApi::OpenAiChatCompletions],
392 provider_api: ProviderApi::OpenAi,
393 },
394 CatalogModel {
395 public_id: "nemotron-nano-9b",
396 cloud: Platform::Aws,
397 upstream_id: "nvidia.nemotron-nano-9b-v2",
398 client_apis: &[ClientApi::OpenAiChatCompletions],
399 provider_api: ProviderApi::OpenAi,
400 },
401 CatalogModel {
402 public_id: "nemotron-nano-12b",
403 cloud: Platform::Aws,
404 upstream_id: "nvidia.nemotron-nano-12b-v2",
405 client_apis: &[ClientApi::OpenAiChatCompletions],
406 provider_api: ProviderApi::OpenAi,
407 },
408 CatalogModel {
409 public_id: "nemotron-nano-3-30b",
410 cloud: Platform::Aws,
411 upstream_id: "nvidia.nemotron-nano-3-30b",
412 client_apis: &[ClientApi::OpenAiChatCompletions],
413 provider_api: ProviderApi::OpenAi,
414 },
415 CatalogModel {
416 public_id: "nemotron-super-3-120b",
417 cloud: Platform::Aws,
418 upstream_id: "nvidia.nemotron-super-3-120b",
419 client_apis: &[ClientApi::OpenAiChatCompletions],
420 provider_api: ProviderApi::OpenAi,
421 },
422 CatalogModel {
423 public_id: "gemma-3-4b",
424 cloud: Platform::Aws,
425 upstream_id: "google.gemma-3-4b-it",
426 client_apis: &[ClientApi::OpenAiChatCompletions],
427 provider_api: ProviderApi::OpenAi,
428 },
429 CatalogModel {
430 public_id: "gemma-3-12b",
431 cloud: Platform::Aws,
432 upstream_id: "google.gemma-3-12b-it",
433 client_apis: &[ClientApi::OpenAiChatCompletions],
434 provider_api: ProviderApi::OpenAi,
435 },
436 CatalogModel {
437 public_id: "gemma-3-27b",
438 cloud: Platform::Aws,
439 upstream_id: "google.gemma-3-27b-it",
440 client_apis: &[ClientApi::OpenAiChatCompletions],
441 provider_api: ProviderApi::OpenAi,
442 },
443 CatalogModel {
444 public_id: "glm-4.7",
445 cloud: Platform::Aws,
446 upstream_id: "zai.glm-4.7",
447 client_apis: &[ClientApi::OpenAiChatCompletions],
448 provider_api: ProviderApi::OpenAi,
449 },
450 CatalogModel {
451 public_id: "glm-4.7-flash",
452 cloud: Platform::Aws,
453 upstream_id: "zai.glm-4.7-flash",
454 client_apis: &[ClientApi::OpenAiChatCompletions],
455 provider_api: ProviderApi::OpenAi,
456 },
457 CatalogModel {
458 public_id: "glm-5",
459 cloud: Platform::Aws,
460 upstream_id: "zai.glm-5",
461 client_apis: &[ClientApi::OpenAiChatCompletions],
462 provider_api: ProviderApi::OpenAi,
463 },
464 CatalogModel {
465 public_id: "palmyra-vision-7b",
466 cloud: Platform::Aws,
467 upstream_id: "writer.palmyra-vision-7b",
468 client_apis: &[ClientApi::OpenAiChatCompletions],
469 provider_api: ProviderApi::OpenAi,
470 },
471 CatalogModel {
478 public_id: "claude-opus-5",
479 cloud: Platform::Aws,
480 upstream_id: "anthropic.claude-opus-5",
481 client_apis: &[ClientApi::AnthropicMessages],
482 provider_api: ProviderApi::Anthropic,
483 },
484 CatalogModel {
485 public_id: "claude-sonnet-5",
486 cloud: Platform::Aws,
487 upstream_id: "anthropic.claude-sonnet-5",
488 client_apis: &[ClientApi::AnthropicMessages],
489 provider_api: ProviderApi::Anthropic,
490 },
491 CatalogModel {
492 public_id: "claude-opus-4.8",
493 cloud: Platform::Aws,
494 upstream_id: "anthropic.claude-opus-4-8",
495 client_apis: &[ClientApi::AnthropicMessages],
496 provider_api: ProviderApi::Anthropic,
497 },
498 CatalogModel {
499 public_id: "claude-opus-4.7",
500 cloud: Platform::Aws,
501 upstream_id: "anthropic.claude-opus-4-7",
502 client_apis: &[ClientApi::AnthropicMessages],
503 provider_api: ProviderApi::Anthropic,
504 },
505 CatalogModel {
506 public_id: "claude-opus-4.6",
507 cloud: Platform::Aws,
508 upstream_id: "anthropic.claude-opus-4-6-v1",
509 client_apis: &[ClientApi::AnthropicMessages],
510 provider_api: ProviderApi::Anthropic,
511 },
512 CatalogModel {
513 public_id: "claude-opus-4.5",
514 cloud: Platform::Aws,
515 upstream_id: "anthropic.claude-opus-4-5-20251101-v1:0",
516 client_apis: &[ClientApi::AnthropicMessages],
517 provider_api: ProviderApi::Anthropic,
518 },
519 CatalogModel {
520 public_id: "claude-sonnet-4.6",
521 cloud: Platform::Aws,
522 upstream_id: "anthropic.claude-sonnet-4-6",
523 client_apis: &[ClientApi::AnthropicMessages],
524 provider_api: ProviderApi::Anthropic,
525 },
526 CatalogModel {
527 public_id: "claude-sonnet-4.5",
528 cloud: Platform::Aws,
529 upstream_id: "anthropic.claude-sonnet-4-5-20250929-v1:0",
530 client_apis: &[ClientApi::AnthropicMessages],
531 provider_api: ProviderApi::Anthropic,
532 },
533 CatalogModel {
534 public_id: "claude-haiku-4.5",
535 cloud: Platform::Aws,
536 upstream_id: "anthropic.claude-haiku-4-5-20251001-v1:0",
537 client_apis: &[ClientApi::AnthropicMessages],
538 provider_api: ProviderApi::Anthropic,
539 },
540 CatalogModel {
541 public_id: "claude-fable-5",
542 cloud: Platform::Aws,
543 upstream_id: "anthropic.claude-fable-5",
544 client_apis: &[ClientApi::AnthropicMessages],
545 provider_api: ProviderApi::Anthropic,
546 },
547 CatalogModel {
550 public_id: "gemini-2.5-pro",
551 cloud: Platform::Gcp,
552 upstream_id: "google/gemini-2.5-pro",
553 client_apis: &[ClientApi::OpenAiChatCompletions],
554 provider_api: ProviderApi::OpenAi,
555 },
556 CatalogModel {
557 public_id: "gemini-2.5-flash",
558 cloud: Platform::Gcp,
559 upstream_id: "google/gemini-2.5-flash",
560 client_apis: &[ClientApi::OpenAiChatCompletions],
561 provider_api: ProviderApi::OpenAi,
562 },
563 CatalogModel {
564 public_id: "gemini-2.5-flash-lite",
565 cloud: Platform::Gcp,
566 upstream_id: "google/gemini-2.5-flash-lite",
567 client_apis: &[ClientApi::OpenAiChatCompletions],
568 provider_api: ProviderApi::OpenAi,
569 },
570 CatalogModel {
571 public_id: "gemini-3.5-flash",
572 cloud: Platform::Gcp,
573 upstream_id: "google/gemini-3.5-flash",
574 client_apis: &[ClientApi::OpenAiChatCompletions],
575 provider_api: ProviderApi::OpenAi,
576 },
577 CatalogModel {
578 public_id: "gemini-3.1-flash-lite",
579 cloud: Platform::Gcp,
580 upstream_id: "google/gemini-3.1-flash-lite",
581 client_apis: &[ClientApi::OpenAiChatCompletions],
582 provider_api: ProviderApi::OpenAi,
583 },
584 CatalogModel {
589 public_id: "claude-opus-5",
590 cloud: Platform::Gcp,
591 upstream_id: "claude-opus-5",
592 client_apis: &[ClientApi::AnthropicMessages],
593 provider_api: ProviderApi::Anthropic,
594 },
595 CatalogModel {
596 public_id: "claude-sonnet-5",
597 cloud: Platform::Gcp,
598 upstream_id: "claude-sonnet-5",
599 client_apis: &[ClientApi::AnthropicMessages],
600 provider_api: ProviderApi::Anthropic,
601 },
602 CatalogModel {
603 public_id: "claude-opus-4.8",
604 cloud: Platform::Gcp,
605 upstream_id: "claude-opus-4-8",
606 client_apis: &[ClientApi::AnthropicMessages],
607 provider_api: ProviderApi::Anthropic,
608 },
609 CatalogModel {
610 public_id: "claude-opus-4.7",
611 cloud: Platform::Gcp,
612 upstream_id: "claude-opus-4-7",
613 client_apis: &[ClientApi::AnthropicMessages],
614 provider_api: ProviderApi::Anthropic,
615 },
616 CatalogModel {
617 public_id: "claude-opus-4.6",
618 cloud: Platform::Gcp,
619 upstream_id: "claude-opus-4-6",
620 client_apis: &[ClientApi::AnthropicMessages],
621 provider_api: ProviderApi::Anthropic,
622 },
623 CatalogModel {
624 public_id: "claude-opus-4.5",
625 cloud: Platform::Gcp,
626 upstream_id: "claude-opus-4-5@20251101",
627 client_apis: &[ClientApi::AnthropicMessages],
628 provider_api: ProviderApi::Anthropic,
629 },
630 CatalogModel {
631 public_id: "claude-sonnet-4.6",
632 cloud: Platform::Gcp,
633 upstream_id: "claude-sonnet-4-6",
634 client_apis: &[ClientApi::AnthropicMessages],
635 provider_api: ProviderApi::Anthropic,
636 },
637 CatalogModel {
638 public_id: "claude-sonnet-4.5",
639 cloud: Platform::Gcp,
640 upstream_id: "claude-sonnet-4-5@20250929",
641 client_apis: &[ClientApi::AnthropicMessages],
642 provider_api: ProviderApi::Anthropic,
643 },
644 CatalogModel {
645 public_id: "claude-haiku-4.5",
646 cloud: Platform::Gcp,
647 upstream_id: "claude-haiku-4-5@20251001",
648 client_apis: &[ClientApi::AnthropicMessages],
649 provider_api: ProviderApi::Anthropic,
650 },
651 CatalogModel {
652 public_id: "claude-fable-5",
653 cloud: Platform::Gcp,
654 upstream_id: "claude-fable-5",
655 client_apis: &[ClientApi::AnthropicMessages],
656 provider_api: ProviderApi::Anthropic,
657 },
658 CatalogModel {
662 public_id: "gpt-4.1",
663 cloud: Platform::Azure,
664 upstream_id: "gpt-4.1",
665 client_apis: &[ClientApi::OpenAiChatCompletions],
666 provider_api: ProviderApi::OpenAi,
667 },
668 CatalogModel {
669 public_id: "gpt-4o-mini",
670 cloud: Platform::Azure,
671 upstream_id: "gpt-4o-mini",
672 client_apis: &[ClientApi::OpenAiChatCompletions],
673 provider_api: ProviderApi::OpenAi,
674 },
675 CatalogModel {
676 public_id: "model-router",
677 cloud: Platform::Azure,
678 upstream_id: "model-router",
679 client_apis: &[ClientApi::OpenAiChatCompletions],
680 provider_api: ProviderApi::OpenAi,
681 },
682 CatalogModel {
690 public_id: "claude-opus-5",
691 cloud: Platform::Azure,
692 upstream_id: "claude-opus-5",
693 client_apis: &[ClientApi::AnthropicMessages],
694 provider_api: ProviderApi::Anthropic,
695 },
696 CatalogModel {
697 public_id: "claude-sonnet-5",
698 cloud: Platform::Azure,
699 upstream_id: "claude-sonnet-5",
700 client_apis: &[ClientApi::AnthropicMessages],
701 provider_api: ProviderApi::Anthropic,
702 },
703 CatalogModel {
704 public_id: "claude-opus-4.8",
705 cloud: Platform::Azure,
706 upstream_id: "claude-opus-4-8",
707 client_apis: &[ClientApi::AnthropicMessages],
708 provider_api: ProviderApi::Anthropic,
709 },
710 CatalogModel {
711 public_id: "claude-opus-4.7",
712 cloud: Platform::Azure,
713 upstream_id: "claude-opus-4-7",
714 client_apis: &[ClientApi::AnthropicMessages],
715 provider_api: ProviderApi::Anthropic,
716 },
717 CatalogModel {
718 public_id: "claude-opus-4.6",
719 cloud: Platform::Azure,
720 upstream_id: "claude-opus-4-6",
721 client_apis: &[ClientApi::AnthropicMessages],
722 provider_api: ProviderApi::Anthropic,
723 },
724 CatalogModel {
725 public_id: "claude-opus-4.5",
726 cloud: Platform::Azure,
727 upstream_id: "claude-opus-4-5",
728 client_apis: &[ClientApi::AnthropicMessages],
729 provider_api: ProviderApi::Anthropic,
730 },
731 CatalogModel {
732 public_id: "claude-sonnet-4.6",
733 cloud: Platform::Azure,
734 upstream_id: "claude-sonnet-4-6",
735 client_apis: &[ClientApi::AnthropicMessages],
736 provider_api: ProviderApi::Anthropic,
737 },
738 CatalogModel {
739 public_id: "claude-sonnet-4.5",
740 cloud: Platform::Azure,
741 upstream_id: "claude-sonnet-4-5",
742 client_apis: &[ClientApi::AnthropicMessages],
743 provider_api: ProviderApi::Anthropic,
744 },
745 CatalogModel {
746 public_id: "claude-haiku-4.5",
747 cloud: Platform::Azure,
748 upstream_id: "claude-haiku-4-5",
749 client_apis: &[ClientApi::AnthropicMessages],
750 provider_api: ProviderApi::Anthropic,
751 },
752 CatalogModel {
753 public_id: "claude-fable-5",
754 cloud: Platform::Azure,
755 upstream_id: "claude-fable-5",
756 client_apis: &[ClientApi::AnthropicMessages],
757 provider_api: ProviderApi::Anthropic,
758 },
759];
760
761pub const AI_CATALOG_REVISION: &str = "2026-08-09.1";
765
766static AZURE_DEPLOYMENTS: &[(&str, &str, &str)] = &[
770 ("gpt-4.1", "gpt-4.1", "2025-04-14"),
771 ("model-router", "model-router", "2025-11-18"),
772];
773
774static DIRECT_ANTHROPIC_MODELS: &[DirectAnthropicModel] = &[
778 DirectAnthropicModel {
779 public_id: "claude-opus-5",
780 upstream_id: "claude-opus-5",
781 },
782 DirectAnthropicModel {
783 public_id: "claude-sonnet-5",
784 upstream_id: "claude-sonnet-5",
785 },
786 DirectAnthropicModel {
787 public_id: "claude-opus-4.8",
788 upstream_id: "claude-opus-4-8",
789 },
790 DirectAnthropicModel {
791 public_id: "claude-opus-4.7",
792 upstream_id: "claude-opus-4-7",
793 },
794 DirectAnthropicModel {
795 public_id: "claude-opus-4.6",
796 upstream_id: "claude-opus-4-6",
797 },
798 DirectAnthropicModel {
799 public_id: "claude-opus-4.5",
800 upstream_id: "claude-opus-4-5",
801 },
802 DirectAnthropicModel {
803 public_id: "claude-sonnet-4.6",
804 upstream_id: "claude-sonnet-4-6",
805 },
806 DirectAnthropicModel {
807 public_id: "claude-sonnet-4.5",
808 upstream_id: "claude-sonnet-4-5-20250929",
809 },
810 DirectAnthropicModel {
811 public_id: "claude-haiku-4.5",
812 upstream_id: "claude-haiku-4-5-20251001",
813 },
814 DirectAnthropicModel {
815 public_id: "claude-fable-5",
816 upstream_id: "claude-fable-5",
817 },
818];
819
820static DIRECT_OPENAI_MODELS: &[DirectOpenAiModel] = &[
830 DirectOpenAiModel {
831 public_id: "gpt-5.6-sol",
832 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
833 },
834 DirectOpenAiModel {
835 public_id: "gpt-5.6-terra",
836 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
837 },
838 DirectOpenAiModel {
839 public_id: "gpt-5.6-luna",
840 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
841 },
842 DirectOpenAiModel {
843 public_id: "gpt-5.5",
844 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
845 },
846 DirectOpenAiModel {
847 public_id: "gpt-5.4",
848 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
849 },
850 DirectOpenAiModel {
851 public_id: "gpt-4.1",
852 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
853 },
854 DirectOpenAiModel {
855 public_id: "gpt-4.1-mini",
856 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
857 },
858 DirectOpenAiModel {
859 public_id: "gpt-4o-mini",
860 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
861 },
862];
863
864static DIRECT_DATABRICKS_MODELS: &[DirectDatabricksModel] = &[
871 DirectDatabricksModel {
872 public_id: "gpt-5.6-sol",
873 upstream_id: "databricks-gpt-5-6-sol",
874 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
875 },
876 DirectDatabricksModel {
877 public_id: "gpt-5.6-terra",
878 upstream_id: "databricks-gpt-5-6-terra",
879 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
880 },
881 DirectDatabricksModel {
882 public_id: "gpt-5.6-luna",
883 upstream_id: "databricks-gpt-5-6-luna",
884 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
885 },
886 DirectDatabricksModel {
887 public_id: "gpt-5.5",
888 upstream_id: "databricks-gpt-5-5",
889 client_apis: &[ClientApi::OpenAiResponses],
890 },
891 DirectDatabricksModel {
892 public_id: "gpt-5.4",
893 upstream_id: "databricks-gpt-5-4",
894 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
895 },
896 DirectDatabricksModel {
897 public_id: "claude-haiku-4.5",
898 upstream_id: "databricks-claude-haiku-4-5",
899 client_apis: &[
900 ClientApi::OpenAiChatCompletions,
901 ClientApi::OpenAiResponses,
902 ClientApi::AnthropicMessages,
903 ],
904 },
905 DirectDatabricksModel {
906 public_id: "claude-opus-5",
907 upstream_id: "system.ai.claude-opus-5",
908 client_apis: &[
909 ClientApi::OpenAiChatCompletions,
910 ClientApi::OpenAiResponses,
911 ClientApi::AnthropicMessages,
912 ],
913 },
914 DirectDatabricksModel {
915 public_id: "claude-sonnet-5",
916 upstream_id: "databricks-claude-sonnet-5",
917 client_apis: &[
918 ClientApi::OpenAiChatCompletions,
919 ClientApi::OpenAiResponses,
920 ClientApi::AnthropicMessages,
921 ],
922 },
923 DirectDatabricksModel {
924 public_id: "claude-sonnet-4.6",
925 upstream_id: "databricks-claude-sonnet-4-6",
926 client_apis: &[
927 ClientApi::OpenAiChatCompletions,
928 ClientApi::OpenAiResponses,
929 ClientApi::AnthropicMessages,
930 ],
931 },
932 DirectDatabricksModel {
933 public_id: "claude-sonnet-4.5",
934 upstream_id: "databricks-claude-sonnet-4-5",
935 client_apis: &[
936 ClientApi::OpenAiChatCompletions,
937 ClientApi::OpenAiResponses,
938 ClientApi::AnthropicMessages,
939 ],
940 },
941 DirectDatabricksModel {
942 public_id: "claude-fable-5",
943 upstream_id: "databricks-claude-fable-5",
944 client_apis: &[
945 ClientApi::OpenAiChatCompletions,
946 ClientApi::OpenAiResponses,
947 ClientApi::AnthropicMessages,
948 ],
949 },
950 DirectDatabricksModel {
951 public_id: "claude-opus-4.8",
952 upstream_id: "databricks-claude-opus-4-8",
953 client_apis: &[
954 ClientApi::OpenAiChatCompletions,
955 ClientApi::OpenAiResponses,
956 ClientApi::AnthropicMessages,
957 ],
958 },
959 DirectDatabricksModel {
960 public_id: "claude-opus-4.7",
961 upstream_id: "databricks-claude-opus-4-7",
962 client_apis: &[
963 ClientApi::OpenAiChatCompletions,
964 ClientApi::OpenAiResponses,
965 ClientApi::AnthropicMessages,
966 ],
967 },
968 DirectDatabricksModel {
969 public_id: "claude-opus-4.6",
970 upstream_id: "databricks-claude-opus-4-6",
971 client_apis: &[
972 ClientApi::OpenAiChatCompletions,
973 ClientApi::OpenAiResponses,
974 ClientApi::AnthropicMessages,
975 ],
976 },
977 DirectDatabricksModel {
978 public_id: "claude-opus-4.5",
979 upstream_id: "databricks-claude-opus-4-5",
980 client_apis: &[
981 ClientApi::OpenAiChatCompletions,
982 ClientApi::OpenAiResponses,
983 ClientApi::AnthropicMessages,
984 ],
985 },
986 DirectDatabricksModel {
987 public_id: "gemini-3.5-flash",
988 upstream_id: "databricks-gemini-3-5-flash",
989 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
990 },
991 DirectDatabricksModel {
992 public_id: "gemini-3.1-flash-lite",
993 upstream_id: "databricks-gemini-3-1-flash-lite",
994 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
995 },
996 DirectDatabricksModel {
997 public_id: "gpt-oss-120b",
998 upstream_id: "databricks-gpt-oss-120b",
999 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
1000 },
1001 DirectDatabricksModel {
1002 public_id: "gpt-oss-20b",
1003 upstream_id: "databricks-gpt-oss-20b",
1004 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
1005 },
1006 DirectDatabricksModel {
1007 public_id: "gemma-3-12b",
1008 upstream_id: "databricks-gemma-3-12b",
1009 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
1010 },
1011];
1012
1013#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1015pub struct ResponsesTarget {
1016 pub upstream_id: &'static str,
1018 pub path: &'static str,
1021}
1022
1023static RESPONSES_UPSTREAM: &[(&str, ResponsesTarget)] = &[
1028 (
1029 "gpt-oss-20b",
1030 ResponsesTarget {
1031 upstream_id: "openai.gpt-oss-20b",
1032 path: "/v1/responses",
1033 },
1034 ),
1035 (
1036 "gpt-oss-120b",
1037 ResponsesTarget {
1038 upstream_id: "openai.gpt-oss-120b",
1039 path: "/v1/responses",
1040 },
1041 ),
1042 (
1043 "gpt-5.6-sol",
1044 ResponsesTarget {
1045 upstream_id: "openai.gpt-5.6-sol",
1046 path: "/openai/v1/responses",
1047 },
1048 ),
1049 (
1050 "gpt-5.6-terra",
1051 ResponsesTarget {
1052 upstream_id: "openai.gpt-5.6-terra",
1053 path: "/openai/v1/responses",
1054 },
1055 ),
1056 (
1057 "gpt-5.6-luna",
1058 ResponsesTarget {
1059 upstream_id: "openai.gpt-5.6-luna",
1060 path: "/openai/v1/responses",
1061 },
1062 ),
1063 (
1064 "gpt-5.5",
1065 ResponsesTarget {
1066 upstream_id: "openai.gpt-5.5",
1067 path: "/openai/v1/responses",
1068 },
1069 ),
1070 (
1071 "gpt-5.4",
1072 ResponsesTarget {
1073 upstream_id: "openai.gpt-5.4",
1074 path: "/openai/v1/responses",
1075 },
1076 ),
1077];
1078
1079pub fn responses_target(public_id: &str) -> Option<ResponsesTarget> {
1082 RESPONSES_UPSTREAM
1083 .iter()
1084 .find(|(public, _)| *public == public_id)
1085 .map(|(_, target)| *target)
1086}
1087
1088pub fn models_for(cloud: Platform) -> Vec<&'static CatalogModel> {
1089 CATALOG.iter().filter(|m| m.cloud == cloud).collect()
1090}
1091
1092pub fn direct_anthropic_models() -> Vec<&'static DirectAnthropicModel> {
1093 DIRECT_ANTHROPIC_MODELS.iter().collect()
1094}
1095
1096pub fn resolve_direct_anthropic(public_id: &str) -> Option<&'static DirectAnthropicModel> {
1097 DIRECT_ANTHROPIC_MODELS
1098 .iter()
1099 .find(|model| model.public_id == public_id)
1100}
1101
1102pub fn direct_openai_models() -> Vec<&'static DirectOpenAiModel> {
1103 DIRECT_OPENAI_MODELS.iter().collect()
1104}
1105
1106pub fn resolve_direct_openai(public_id: &str) -> Option<&'static DirectOpenAiModel> {
1107 DIRECT_OPENAI_MODELS
1108 .iter()
1109 .find(|model| model.public_id == public_id)
1110}
1111
1112pub fn direct_databricks_models() -> Vec<&'static DirectDatabricksModel> {
1113 DIRECT_DATABRICKS_MODELS.iter().collect()
1114}
1115
1116pub fn resolve_direct_databricks(public_id: &str) -> Option<&'static DirectDatabricksModel> {
1117 DIRECT_DATABRICKS_MODELS
1118 .iter()
1119 .find(|model| model.public_id == public_id)
1120}
1121
1122pub fn lookup(public_id: &str) -> Option<&'static CatalogModel> {
1127 CATALOG.iter().find(|m| m.public_id == public_id)
1128}
1129
1130fn lookup_for(public_id: &str, cloud: Platform) -> Option<&'static CatalogModel> {
1131 CATALOG
1132 .iter()
1133 .find(|m| m.public_id == public_id && m.cloud == cloud)
1134}
1135
1136pub fn resolve_for(model_id: &str, cloud: Platform) -> Option<&'static CatalogModel> {
1141 lookup_for(model_id, cloud).or_else(|| lookup_for(&canonical_public_id(model_id), cloud))
1142}
1143
1144pub fn resolve(model_id: &str) -> Option<&'static CatalogModel> {
1159 lookup(model_id).or_else(|| lookup(&canonical_public_id(model_id)))
1160}
1161
1162fn canonical_public_id(model_id: &str) -> String {
1163 let mut id = model_id;
1164 if let Some(pos) = id.rfind("anthropic.") {
1165 id = &id[pos + "anthropic.".len()..];
1166 }
1167 id = id.split_once('@').map_or(id, |(base, _)| base);
1169 id = strip_invoke_version(id);
1170 id = strip_release_date(id);
1171 dot_minor_version(id)
1172}
1173
1174fn strip_invoke_version(id: &str) -> &str {
1176 let base = id.split_once(':').map_or(id, |(base, _)| base);
1177 match base.rsplit_once("-v") {
1178 Some((stem, digits))
1179 if !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit()) =>
1180 {
1181 stem
1182 }
1183 _ => base,
1184 }
1185}
1186
1187fn strip_release_date(id: &str) -> &str {
1189 match id.rsplit_once('-') {
1190 Some((stem, date))
1191 if date.len() == 8
1192 && date.starts_with("20")
1193 && date.bytes().all(|b| b.is_ascii_digit()) =>
1194 {
1195 stem
1196 }
1197 _ => id,
1198 }
1199}
1200
1201fn dot_minor_version(id: &str) -> String {
1205 let Some((stem, minor)) = id.rsplit_once('-') else {
1206 return id.to_string();
1207 };
1208 let Some((prefix, major)) = stem.rsplit_once('-') else {
1209 return id.to_string();
1210 };
1211 let both_numeric = !major.is_empty()
1212 && !minor.is_empty()
1213 && major.bytes().all(|b| b.is_ascii_digit())
1214 && minor.bytes().all(|b| b.is_ascii_digit());
1215 if both_numeric {
1216 format!("{prefix}-{major}.{minor}")
1217 } else {
1218 id.to_string()
1219 }
1220}
1221
1222pub fn azure_deployments() -> Vec<(&'static str, &'static str, &'static str)> {
1224 AZURE_DEPLOYMENTS.to_vec()
1225}
1226
1227#[cfg(test)]
1228mod tests {
1229 #[test]
1233 fn public_ids_are_unique_per_cloud() {
1234 let mut seen = std::collections::HashSet::new();
1235 for model in super::CATALOG {
1236 assert!(
1237 seen.insert((model.cloud, model.public_id)),
1238 "public id '{}' appears more than once under {:?}",
1239 model.public_id,
1240 model.cloud
1241 );
1242 }
1243 }
1244
1245 #[test]
1246 fn direct_anthropic_aliases_are_unique_and_round_trip() {
1247 let mut public_ids = std::collections::HashSet::new();
1248 let mut upstream_ids = std::collections::HashSet::new();
1249 for model in DIRECT_ANTHROPIC_MODELS {
1250 assert!(public_ids.insert(model.public_id));
1251 assert!(upstream_ids.insert(model.upstream_id));
1252 assert_eq!(
1253 resolve_direct_anthropic(model.public_id)
1254 .expect("direct model must resolve")
1255 .upstream_id,
1256 model.upstream_id
1257 );
1258 assert_ne!(model.display_name(), model.public_id);
1259 }
1260 assert!(resolve_direct_anthropic("claude-not-real").is_none());
1261 }
1262
1263 #[test]
1264 fn direct_openai_models_are_unique_and_have_qualified_apis() {
1265 let mut public_ids = std::collections::HashSet::new();
1266 for model in DIRECT_OPENAI_MODELS {
1267 assert!(public_ids.insert(model.public_id));
1268 assert!(!model.client_apis.is_empty());
1269 assert_eq!(
1270 resolve_direct_openai(model.public_id)
1271 .expect("direct model must resolve")
1272 .client_apis,
1273 model.client_apis
1274 );
1275 }
1276 assert!(resolve_direct_openai("text-embedding-3-small").is_none());
1277 assert!(resolve_direct_openai("gpt-image-1").is_none());
1278 }
1279
1280 #[test]
1281 fn direct_databricks_models_are_unique_and_resolve_to_provider_ids() {
1282 let mut public_ids = std::collections::HashSet::new();
1283 for model in DIRECT_DATABRICKS_MODELS {
1284 assert!(public_ids.insert(model.public_id));
1285 assert!(
1286 model.upstream_id.starts_with("databricks-")
1287 || model.upstream_id.starts_with("system.ai.")
1288 );
1289 assert!(!model.client_apis.is_empty());
1290 assert_eq!(
1291 resolve_direct_databricks(model.public_id)
1292 .expect("direct Databricks model must resolve")
1293 .upstream_id,
1294 model.upstream_id
1295 );
1296 }
1297 assert!(resolve_direct_databricks("bge-large-en").is_none());
1298 assert!(resolve_direct_databricks("databricks-genie").is_none());
1299 assert_eq!(
1300 resolve_direct_databricks("claude-opus-5")
1301 .expect("Claude Opus 5 must resolve")
1302 .upstream_id,
1303 "system.ai.claude-opus-5"
1304 );
1305 assert_eq!(
1306 resolve_direct_databricks("gpt-5.5")
1307 .expect("GPT-5.5 must resolve")
1308 .client_apis,
1309 &[ClientApi::OpenAiResponses]
1310 );
1311 }
1312
1313 use super::*;
1314
1315 #[test]
1316 fn resolve_accepts_anthropic_native_spellings() {
1317 assert_eq!(
1319 resolve("claude-haiku-4-5").unwrap().public_id,
1320 "claude-haiku-4.5"
1321 );
1322 assert_eq!(
1323 resolve("claude-sonnet-4-5-20250929").unwrap().public_id,
1324 "claude-sonnet-4.5"
1325 );
1326 assert_eq!(
1328 resolve("us.anthropic.claude-haiku-4-5-20251001-v1:0")
1329 .unwrap()
1330 .public_id,
1331 "claude-haiku-4.5"
1332 );
1333 assert_eq!(
1334 resolve("anthropic.claude-opus-4-6-v1").unwrap().public_id,
1335 "claude-opus-4.6"
1336 );
1337 assert_eq!(
1339 resolve("claude-sonnet-5").unwrap().public_id,
1340 "claude-sonnet-5"
1341 );
1342 assert_eq!(
1344 resolve("claude-opus-4.8").unwrap().public_id,
1345 "claude-opus-4.8"
1346 );
1347 assert_eq!(resolve("gpt-oss-20b").unwrap().public_id, "gpt-oss-20b");
1348 assert!(resolve("claude-nonexistent-9-9").is_none());
1350 assert!(resolve("gpt-5").is_none());
1351 }
1352
1353 #[test]
1354 fn aws_has_openai_and_anthropic_with_plain_ids() {
1355 let aws = models_for(Platform::Aws);
1356 assert!(!aws.is_empty());
1357 assert!(aws
1358 .iter()
1359 .any(|m| m.public_id == "gpt-oss-20b" && m.provider_api == ProviderApi::OpenAi));
1360 assert!(
1361 aws.iter().any(|m| m.provider_api == ProviderApi::Anthropic),
1362 "Claude must be included via the Anthropic protocol"
1363 );
1364 assert!(aws.iter().all(|m| !m.upstream_id.starts_with("us.")));
1366 }
1367
1368 #[test]
1369 fn resolve_for_scopes_to_cloud() {
1370 let aws = resolve_for("claude-opus-4.8", Platform::Aws).expect("aws claude");
1373 assert_eq!(aws.upstream_id, "anthropic.claude-opus-4-8");
1374 let gcp = resolve_for("claude-opus-4.8", Platform::Gcp).expect("gcp claude");
1375 assert_eq!(gcp.upstream_id, "claude-opus-4-8");
1376 assert_eq!(gcp.provider_api, ProviderApi::Anthropic);
1377 let dated = resolve_for("claude-haiku-4-5-20251001", Platform::Gcp).expect("dated id");
1380 assert_eq!(dated.upstream_id, "claude-haiku-4-5@20251001");
1381 let vertex = resolve_for("claude-sonnet-4-5@20250929", Platform::Gcp).expect("vertex id");
1384 assert_eq!(vertex.upstream_id, "claude-sonnet-4-5@20250929");
1385 assert!(resolve_for("gemini-2.5-pro", Platform::Aws).is_none());
1387 assert!(resolve_for("gpt-4.1", Platform::Gcp).is_none());
1388 }
1389
1390 #[test]
1391 fn lookup_round_trips() {
1392 let m = lookup("gpt-oss-20b").expect("known model");
1393 assert_eq!(m.cloud, Platform::Aws);
1394 assert_eq!(m.provider_api, ProviderApi::OpenAi);
1395 assert_eq!(m.upstream_id, "openai.gpt-oss-20b-1:0");
1396
1397 let c = lookup("claude-opus-4.8").expect("claude known");
1398 assert_eq!(c.provider_api, ProviderApi::Anthropic);
1399
1400 assert!(lookup("nonexistent-model").is_none());
1401 }
1402
1403 #[test]
1404 fn azure_deployments_map_to_catalog() {
1405 assert!(!azure_deployments().is_empty());
1406 for (deployment, _, _) in azure_deployments() {
1407 assert!(
1408 models_for(Platform::Azure)
1409 .iter()
1410 .any(|m| m.upstream_id == deployment),
1411 "azure deployment {deployment} must map to a catalog model"
1412 );
1413 }
1414 }
1415
1416 #[test]
1417 fn api_kinds_have_stable_wire_names() {
1418 assert_eq!(
1419 serde_json::to_string(&ProviderApi::OpenAi).unwrap(),
1420 "\"openai\""
1421 );
1422 assert_eq!(
1423 serde_json::to_string(&ProviderApi::Anthropic).unwrap(),
1424 "\"anthropic\""
1425 );
1426 assert_eq!(
1427 serde_json::to_string(&ProviderApi::OpenAiResponses).unwrap(),
1428 "\"openairesponses\""
1429 );
1430 assert_eq!(
1431 serde_json::to_string(&ClientApi::OpenAiChatCompletions).unwrap(),
1432 "\"open-ai-chat-completions\""
1433 );
1434 assert_eq!(
1435 serde_json::to_string(&ClientApi::OpenAiResponses).unwrap(),
1436 "\"open-ai-responses\""
1437 );
1438 assert_eq!(
1439 serde_json::to_string(&ClientApi::AnthropicMessages).unwrap(),
1440 "\"anthropic-messages\""
1441 );
1442 }
1443
1444 #[test]
1445 fn client_apis_are_explicit_and_non_empty() {
1446 for model in CATALOG {
1447 assert!(
1448 !model.client_apis.is_empty(),
1449 "'{}' has no supported client API",
1450 model.public_id
1451 );
1452 }
1453
1454 let gpt_oss = resolve_for("gpt-oss-20b", Platform::Aws).unwrap();
1455 assert!(gpt_oss
1456 .client_apis
1457 .contains(&ClientApi::OpenAiChatCompletions));
1458 assert!(gpt_oss.client_apis.contains(&ClientApi::OpenAiResponses));
1459 }
1460
1461 #[test]
1462 fn every_model_has_provider_display_name_and_activation() {
1463 for m in CATALOG {
1464 assert_ne!(
1465 m.provider(),
1466 "unknown",
1467 "no provider mapping for '{}'",
1468 m.public_id
1469 );
1470 assert_ne!(
1471 m.display_name(),
1472 m.public_id,
1473 "no curated display_name for '{}'",
1474 m.public_id
1475 );
1476 let is_claude = m.public_id.starts_with("claude");
1478 match m.activation() {
1479 Activation::OutOfBox => {
1480 assert!(
1481 !is_claude,
1482 "'{}' (Claude) must require a one-time step",
1483 m.public_id
1484 )
1485 }
1486 Activation::RequiresOneTimeStep(summary) => {
1487 assert!(is_claude, "'{}' must be out of the box", m.public_id);
1488 assert!(
1489 !summary.is_empty(),
1490 "'{}' step summary is empty",
1491 m.public_id
1492 );
1493 }
1494 }
1495 }
1496 }
1497
1498 #[test]
1502 fn only_claude_ids_speak_the_anthropic_protocol() {
1503 for m in CATALOG {
1504 assert_eq!(
1505 m.provider_api == ProviderApi::Anthropic,
1506 m.public_id.starts_with("claude"),
1507 "'{}' is {:?} but its id says otherwise",
1508 m.public_id,
1509 m.provider_api
1510 );
1511 }
1512 }
1513}