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