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