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
261static CATALOG: &[CatalogModel] = &[
262 CatalogModel {
268 public_id: "gpt-5.6-sol",
269 cloud: Platform::Aws,
270 upstream_id: "openai.gpt-5.6-sol",
271 client_apis: &[ClientApi::OpenAiResponses],
272 provider_api: ProviderApi::OpenAiResponses,
273 },
274 CatalogModel {
275 public_id: "gpt-5.6-terra",
276 cloud: Platform::Aws,
277 upstream_id: "openai.gpt-5.6-terra",
278 client_apis: &[ClientApi::OpenAiResponses],
279 provider_api: ProviderApi::OpenAiResponses,
280 },
281 CatalogModel {
282 public_id: "gpt-5.6-luna",
283 cloud: Platform::Aws,
284 upstream_id: "openai.gpt-5.6-luna",
285 client_apis: &[ClientApi::OpenAiResponses],
286 provider_api: ProviderApi::OpenAiResponses,
287 },
288 CatalogModel {
289 public_id: "gpt-5.5",
290 cloud: Platform::Aws,
291 upstream_id: "openai.gpt-5.5",
292 client_apis: &[ClientApi::OpenAiResponses],
293 provider_api: ProviderApi::OpenAiResponses,
294 },
295 CatalogModel {
296 public_id: "gpt-5.4",
297 cloud: Platform::Aws,
298 upstream_id: "openai.gpt-5.4",
299 client_apis: &[ClientApi::OpenAiResponses],
300 provider_api: ProviderApi::OpenAiResponses,
301 },
302 CatalogModel {
303 public_id: "gpt-oss-20b",
304 cloud: Platform::Aws,
305 upstream_id: "openai.gpt-oss-20b-1:0",
306 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
307 provider_api: ProviderApi::OpenAi,
308 },
309 CatalogModel {
310 public_id: "gpt-oss-120b",
311 cloud: Platform::Aws,
312 upstream_id: "openai.gpt-oss-120b-1:0",
313 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
314 provider_api: ProviderApi::OpenAi,
315 },
316 CatalogModel {
317 public_id: "gpt-oss-safeguard-20b",
318 cloud: Platform::Aws,
319 upstream_id: "openai.gpt-oss-safeguard-20b",
320 client_apis: &[ClientApi::OpenAiChatCompletions],
321 provider_api: ProviderApi::OpenAi,
322 },
323 CatalogModel {
324 public_id: "gpt-oss-safeguard-120b",
325 cloud: Platform::Aws,
326 upstream_id: "openai.gpt-oss-safeguard-120b",
327 client_apis: &[ClientApi::OpenAiChatCompletions],
328 provider_api: ProviderApi::OpenAi,
329 },
330 CatalogModel {
331 public_id: "deepseek-v3.2",
332 cloud: Platform::Aws,
333 upstream_id: "deepseek.v3.2",
334 client_apis: &[ClientApi::OpenAiChatCompletions],
335 provider_api: ProviderApi::OpenAi,
336 },
337 CatalogModel {
338 public_id: "qwen3-32b",
339 cloud: Platform::Aws,
340 upstream_id: "qwen.qwen3-32b-v1:0",
341 client_apis: &[ClientApi::OpenAiChatCompletions],
342 provider_api: ProviderApi::OpenAi,
343 },
344 CatalogModel {
345 public_id: "qwen3-coder-30b",
346 cloud: Platform::Aws,
347 upstream_id: "qwen.qwen3-coder-30b-a3b-v1:0",
348 client_apis: &[ClientApi::OpenAiChatCompletions],
349 provider_api: ProviderApi::OpenAi,
350 },
351 CatalogModel {
352 public_id: "qwen3-next-80b",
353 cloud: Platform::Aws,
354 upstream_id: "qwen.qwen3-next-80b-a3b",
355 client_apis: &[ClientApi::OpenAiChatCompletions],
356 provider_api: ProviderApi::OpenAi,
357 },
358 CatalogModel {
359 public_id: "qwen3-vl-235b",
360 cloud: Platform::Aws,
361 upstream_id: "qwen.qwen3-vl-235b-a22b",
362 client_apis: &[ClientApi::OpenAiChatCompletions],
363 provider_api: ProviderApi::OpenAi,
364 },
365 CatalogModel {
366 public_id: "mistral-large-3",
367 cloud: Platform::Aws,
368 upstream_id: "mistral.mistral-large-3-675b-instruct",
369 client_apis: &[ClientApi::OpenAiChatCompletions],
370 provider_api: ProviderApi::OpenAi,
371 },
372 CatalogModel {
373 public_id: "devstral-2",
374 cloud: Platform::Aws,
375 upstream_id: "mistral.devstral-2-123b",
376 client_apis: &[ClientApi::OpenAiChatCompletions],
377 provider_api: ProviderApi::OpenAi,
378 },
379 CatalogModel {
380 public_id: "magistral-small",
381 cloud: Platform::Aws,
382 upstream_id: "mistral.magistral-small-2509",
383 client_apis: &[ClientApi::OpenAiChatCompletions],
384 provider_api: ProviderApi::OpenAi,
385 },
386 CatalogModel {
387 public_id: "ministral-3-14b",
388 cloud: Platform::Aws,
389 upstream_id: "mistral.ministral-3-14b-instruct",
390 client_apis: &[ClientApi::OpenAiChatCompletions],
391 provider_api: ProviderApi::OpenAi,
392 },
393 CatalogModel {
394 public_id: "ministral-3-8b",
395 cloud: Platform::Aws,
396 upstream_id: "mistral.ministral-3-8b-instruct",
397 client_apis: &[ClientApi::OpenAiChatCompletions],
398 provider_api: ProviderApi::OpenAi,
399 },
400 CatalogModel {
401 public_id: "ministral-3-3b",
402 cloud: Platform::Aws,
403 upstream_id: "mistral.ministral-3-3b-instruct",
404 client_apis: &[ClientApi::OpenAiChatCompletions],
405 provider_api: ProviderApi::OpenAi,
406 },
407 CatalogModel {
408 public_id: "minimax-m2",
409 cloud: Platform::Aws,
410 upstream_id: "minimax.minimax-m2",
411 client_apis: &[ClientApi::OpenAiChatCompletions],
412 provider_api: ProviderApi::OpenAi,
413 },
414 CatalogModel {
415 public_id: "minimax-m2.1",
416 cloud: Platform::Aws,
417 upstream_id: "minimax.minimax-m2.1",
418 client_apis: &[ClientApi::OpenAiChatCompletions],
419 provider_api: ProviderApi::OpenAi,
420 },
421 CatalogModel {
422 public_id: "minimax-m2.5",
423 cloud: Platform::Aws,
424 upstream_id: "minimax.minimax-m2.5",
425 client_apis: &[ClientApi::OpenAiChatCompletions],
426 provider_api: ProviderApi::OpenAi,
427 },
428 CatalogModel {
429 public_id: "kimi-k2.5",
430 cloud: Platform::Aws,
431 upstream_id: "moonshotai.kimi-k2.5",
432 client_apis: &[ClientApi::OpenAiChatCompletions],
433 provider_api: ProviderApi::OpenAi,
434 },
435 CatalogModel {
436 public_id: "nemotron-nano-9b",
437 cloud: Platform::Aws,
438 upstream_id: "nvidia.nemotron-nano-9b-v2",
439 client_apis: &[ClientApi::OpenAiChatCompletions],
440 provider_api: ProviderApi::OpenAi,
441 },
442 CatalogModel {
443 public_id: "nemotron-nano-12b",
444 cloud: Platform::Aws,
445 upstream_id: "nvidia.nemotron-nano-12b-v2",
446 client_apis: &[ClientApi::OpenAiChatCompletions],
447 provider_api: ProviderApi::OpenAi,
448 },
449 CatalogModel {
450 public_id: "nemotron-nano-3-30b",
451 cloud: Platform::Aws,
452 upstream_id: "nvidia.nemotron-nano-3-30b",
453 client_apis: &[ClientApi::OpenAiChatCompletions],
454 provider_api: ProviderApi::OpenAi,
455 },
456 CatalogModel {
457 public_id: "nemotron-super-3-120b",
458 cloud: Platform::Aws,
459 upstream_id: "nvidia.nemotron-super-3-120b",
460 client_apis: &[ClientApi::OpenAiChatCompletions],
461 provider_api: ProviderApi::OpenAi,
462 },
463 CatalogModel {
464 public_id: "gemma-3-4b",
465 cloud: Platform::Aws,
466 upstream_id: "google.gemma-3-4b-it",
467 client_apis: &[ClientApi::OpenAiChatCompletions],
468 provider_api: ProviderApi::OpenAi,
469 },
470 CatalogModel {
471 public_id: "gemma-3-12b",
472 cloud: Platform::Aws,
473 upstream_id: "google.gemma-3-12b-it",
474 client_apis: &[ClientApi::OpenAiChatCompletions],
475 provider_api: ProviderApi::OpenAi,
476 },
477 CatalogModel {
478 public_id: "gemma-3-27b",
479 cloud: Platform::Aws,
480 upstream_id: "google.gemma-3-27b-it",
481 client_apis: &[ClientApi::OpenAiChatCompletions],
482 provider_api: ProviderApi::OpenAi,
483 },
484 CatalogModel {
485 public_id: "glm-4.7",
486 cloud: Platform::Aws,
487 upstream_id: "zai.glm-4.7",
488 client_apis: &[ClientApi::OpenAiChatCompletions],
489 provider_api: ProviderApi::OpenAi,
490 },
491 CatalogModel {
492 public_id: "glm-4.7-flash",
493 cloud: Platform::Aws,
494 upstream_id: "zai.glm-4.7-flash",
495 client_apis: &[ClientApi::OpenAiChatCompletions],
496 provider_api: ProviderApi::OpenAi,
497 },
498 CatalogModel {
499 public_id: "glm-5",
500 cloud: Platform::Aws,
501 upstream_id: "zai.glm-5",
502 client_apis: &[ClientApi::OpenAiChatCompletions],
503 provider_api: ProviderApi::OpenAi,
504 },
505 CatalogModel {
506 public_id: "palmyra-vision-7b",
507 cloud: Platform::Aws,
508 upstream_id: "writer.palmyra-vision-7b",
509 client_apis: &[ClientApi::OpenAiChatCompletions],
510 provider_api: ProviderApi::OpenAi,
511 },
512 CatalogModel {
519 public_id: "claude-opus-5",
520 cloud: Platform::Aws,
521 upstream_id: "anthropic.claude-opus-5",
522 client_apis: &[ClientApi::AnthropicMessages],
523 provider_api: ProviderApi::Anthropic,
524 },
525 CatalogModel {
526 public_id: "claude-sonnet-5",
527 cloud: Platform::Aws,
528 upstream_id: "anthropic.claude-sonnet-5",
529 client_apis: &[ClientApi::AnthropicMessages],
530 provider_api: ProviderApi::Anthropic,
531 },
532 CatalogModel {
533 public_id: "claude-opus-4.8",
534 cloud: Platform::Aws,
535 upstream_id: "anthropic.claude-opus-4-8",
536 client_apis: &[ClientApi::AnthropicMessages],
537 provider_api: ProviderApi::Anthropic,
538 },
539 CatalogModel {
540 public_id: "claude-opus-4.7",
541 cloud: Platform::Aws,
542 upstream_id: "anthropic.claude-opus-4-7",
543 client_apis: &[ClientApi::AnthropicMessages],
544 provider_api: ProviderApi::Anthropic,
545 },
546 CatalogModel {
547 public_id: "claude-opus-4.6",
548 cloud: Platform::Aws,
549 upstream_id: "anthropic.claude-opus-4-6-v1",
550 client_apis: &[ClientApi::AnthropicMessages],
551 provider_api: ProviderApi::Anthropic,
552 },
553 CatalogModel {
554 public_id: "claude-opus-4.5",
555 cloud: Platform::Aws,
556 upstream_id: "anthropic.claude-opus-4-5-20251101-v1:0",
557 client_apis: &[ClientApi::AnthropicMessages],
558 provider_api: ProviderApi::Anthropic,
559 },
560 CatalogModel {
561 public_id: "claude-sonnet-4.6",
562 cloud: Platform::Aws,
563 upstream_id: "anthropic.claude-sonnet-4-6",
564 client_apis: &[ClientApi::AnthropicMessages],
565 provider_api: ProviderApi::Anthropic,
566 },
567 CatalogModel {
568 public_id: "claude-sonnet-4.5",
569 cloud: Platform::Aws,
570 upstream_id: "anthropic.claude-sonnet-4-5-20250929-v1:0",
571 client_apis: &[ClientApi::AnthropicMessages],
572 provider_api: ProviderApi::Anthropic,
573 },
574 CatalogModel {
575 public_id: "claude-haiku-4.5",
576 cloud: Platform::Aws,
577 upstream_id: "anthropic.claude-haiku-4-5-20251001-v1:0",
578 client_apis: &[ClientApi::AnthropicMessages],
579 provider_api: ProviderApi::Anthropic,
580 },
581 CatalogModel {
582 public_id: "claude-fable-5",
583 cloud: Platform::Aws,
584 upstream_id: "anthropic.claude-fable-5",
585 client_apis: &[ClientApi::AnthropicMessages],
586 provider_api: ProviderApi::Anthropic,
587 },
588 CatalogModel {
591 public_id: "gemini-2.5-pro",
592 cloud: Platform::Gcp,
593 upstream_id: "google/gemini-2.5-pro",
594 client_apis: &[ClientApi::OpenAiChatCompletions],
595 provider_api: ProviderApi::OpenAi,
596 },
597 CatalogModel {
598 public_id: "gemini-2.5-flash",
599 cloud: Platform::Gcp,
600 upstream_id: "google/gemini-2.5-flash",
601 client_apis: &[ClientApi::OpenAiChatCompletions],
602 provider_api: ProviderApi::OpenAi,
603 },
604 CatalogModel {
605 public_id: "gemini-2.5-flash-lite",
606 cloud: Platform::Gcp,
607 upstream_id: "google/gemini-2.5-flash-lite",
608 client_apis: &[ClientApi::OpenAiChatCompletions],
609 provider_api: ProviderApi::OpenAi,
610 },
611 CatalogModel {
612 public_id: "gemini-3.5-flash",
613 cloud: Platform::Gcp,
614 upstream_id: "google/gemini-3.5-flash",
615 client_apis: &[ClientApi::OpenAiChatCompletions],
616 provider_api: ProviderApi::OpenAi,
617 },
618 CatalogModel {
619 public_id: "gemini-3.1-flash-lite",
620 cloud: Platform::Gcp,
621 upstream_id: "google/gemini-3.1-flash-lite",
622 client_apis: &[ClientApi::OpenAiChatCompletions],
623 provider_api: ProviderApi::OpenAi,
624 },
625 CatalogModel {
630 public_id: "claude-opus-5",
631 cloud: Platform::Gcp,
632 upstream_id: "claude-opus-5",
633 client_apis: &[ClientApi::AnthropicMessages],
634 provider_api: ProviderApi::Anthropic,
635 },
636 CatalogModel {
637 public_id: "claude-sonnet-5",
638 cloud: Platform::Gcp,
639 upstream_id: "claude-sonnet-5",
640 client_apis: &[ClientApi::AnthropicMessages],
641 provider_api: ProviderApi::Anthropic,
642 },
643 CatalogModel {
644 public_id: "claude-opus-4.8",
645 cloud: Platform::Gcp,
646 upstream_id: "claude-opus-4-8",
647 client_apis: &[ClientApi::AnthropicMessages],
648 provider_api: ProviderApi::Anthropic,
649 },
650 CatalogModel {
651 public_id: "claude-opus-4.7",
652 cloud: Platform::Gcp,
653 upstream_id: "claude-opus-4-7",
654 client_apis: &[ClientApi::AnthropicMessages],
655 provider_api: ProviderApi::Anthropic,
656 },
657 CatalogModel {
658 public_id: "claude-opus-4.6",
659 cloud: Platform::Gcp,
660 upstream_id: "claude-opus-4-6",
661 client_apis: &[ClientApi::AnthropicMessages],
662 provider_api: ProviderApi::Anthropic,
663 },
664 CatalogModel {
665 public_id: "claude-opus-4.5",
666 cloud: Platform::Gcp,
667 upstream_id: "claude-opus-4-5@20251101",
668 client_apis: &[ClientApi::AnthropicMessages],
669 provider_api: ProviderApi::Anthropic,
670 },
671 CatalogModel {
672 public_id: "claude-sonnet-4.6",
673 cloud: Platform::Gcp,
674 upstream_id: "claude-sonnet-4-6",
675 client_apis: &[ClientApi::AnthropicMessages],
676 provider_api: ProviderApi::Anthropic,
677 },
678 CatalogModel {
679 public_id: "claude-sonnet-4.5",
680 cloud: Platform::Gcp,
681 upstream_id: "claude-sonnet-4-5@20250929",
682 client_apis: &[ClientApi::AnthropicMessages],
683 provider_api: ProviderApi::Anthropic,
684 },
685 CatalogModel {
686 public_id: "claude-haiku-4.5",
687 cloud: Platform::Gcp,
688 upstream_id: "claude-haiku-4-5@20251001",
689 client_apis: &[ClientApi::AnthropicMessages],
690 provider_api: ProviderApi::Anthropic,
691 },
692 CatalogModel {
693 public_id: "claude-fable-5",
694 cloud: Platform::Gcp,
695 upstream_id: "claude-fable-5",
696 client_apis: &[ClientApi::AnthropicMessages],
697 provider_api: ProviderApi::Anthropic,
698 },
699 CatalogModel {
703 public_id: "gpt-4.1",
704 cloud: Platform::Azure,
705 upstream_id: "gpt-4.1",
706 client_apis: &[ClientApi::OpenAiChatCompletions],
707 provider_api: ProviderApi::OpenAi,
708 },
709 CatalogModel {
710 public_id: "gpt-4o-mini",
711 cloud: Platform::Azure,
712 upstream_id: "gpt-4o-mini",
713 client_apis: &[ClientApi::OpenAiChatCompletions],
714 provider_api: ProviderApi::OpenAi,
715 },
716 CatalogModel {
717 public_id: "model-router",
718 cloud: Platform::Azure,
719 upstream_id: "model-router",
720 client_apis: &[ClientApi::OpenAiChatCompletions],
721 provider_api: ProviderApi::OpenAi,
722 },
723 CatalogModel {
731 public_id: "claude-opus-5",
732 cloud: Platform::Azure,
733 upstream_id: "claude-opus-5",
734 client_apis: &[ClientApi::AnthropicMessages],
735 provider_api: ProviderApi::Anthropic,
736 },
737 CatalogModel {
738 public_id: "claude-sonnet-5",
739 cloud: Platform::Azure,
740 upstream_id: "claude-sonnet-5",
741 client_apis: &[ClientApi::AnthropicMessages],
742 provider_api: ProviderApi::Anthropic,
743 },
744 CatalogModel {
745 public_id: "claude-opus-4.8",
746 cloud: Platform::Azure,
747 upstream_id: "claude-opus-4-8",
748 client_apis: &[ClientApi::AnthropicMessages],
749 provider_api: ProviderApi::Anthropic,
750 },
751 CatalogModel {
752 public_id: "claude-opus-4.7",
753 cloud: Platform::Azure,
754 upstream_id: "claude-opus-4-7",
755 client_apis: &[ClientApi::AnthropicMessages],
756 provider_api: ProviderApi::Anthropic,
757 },
758 CatalogModel {
759 public_id: "claude-opus-4.6",
760 cloud: Platform::Azure,
761 upstream_id: "claude-opus-4-6",
762 client_apis: &[ClientApi::AnthropicMessages],
763 provider_api: ProviderApi::Anthropic,
764 },
765 CatalogModel {
766 public_id: "claude-opus-4.5",
767 cloud: Platform::Azure,
768 upstream_id: "claude-opus-4-5",
769 client_apis: &[ClientApi::AnthropicMessages],
770 provider_api: ProviderApi::Anthropic,
771 },
772 CatalogModel {
773 public_id: "claude-sonnet-4.6",
774 cloud: Platform::Azure,
775 upstream_id: "claude-sonnet-4-6",
776 client_apis: &[ClientApi::AnthropicMessages],
777 provider_api: ProviderApi::Anthropic,
778 },
779 CatalogModel {
780 public_id: "claude-sonnet-4.5",
781 cloud: Platform::Azure,
782 upstream_id: "claude-sonnet-4-5",
783 client_apis: &[ClientApi::AnthropicMessages],
784 provider_api: ProviderApi::Anthropic,
785 },
786 CatalogModel {
787 public_id: "claude-haiku-4.5",
788 cloud: Platform::Azure,
789 upstream_id: "claude-haiku-4-5",
790 client_apis: &[ClientApi::AnthropicMessages],
791 provider_api: ProviderApi::Anthropic,
792 },
793 CatalogModel {
794 public_id: "claude-fable-5",
795 cloud: Platform::Azure,
796 upstream_id: "claude-fable-5",
797 client_apis: &[ClientApi::AnthropicMessages],
798 provider_api: ProviderApi::Anthropic,
799 },
800];
801
802pub const AI_CATALOG_REVISION: &str = "2026-08-20.1";
806
807static AZURE_DEPLOYMENTS: &[(&str, &str, &str)] = &[
811 ("gpt-4.1", "gpt-4.1", "2025-04-14"),
812 ("model-router", "model-router", "2025-11-18"),
813];
814
815static DIRECT_ANTHROPIC_MODELS: &[DirectAnthropicModel] = &[
819 DirectAnthropicModel {
820 public_id: "claude-opus-5",
821 upstream_id: "claude-opus-5",
822 },
823 DirectAnthropicModel {
824 public_id: "claude-sonnet-5",
825 upstream_id: "claude-sonnet-5",
826 },
827 DirectAnthropicModel {
828 public_id: "claude-opus-4.8",
829 upstream_id: "claude-opus-4-8",
830 },
831 DirectAnthropicModel {
832 public_id: "claude-opus-4.7",
833 upstream_id: "claude-opus-4-7",
834 },
835 DirectAnthropicModel {
836 public_id: "claude-opus-4.6",
837 upstream_id: "claude-opus-4-6",
838 },
839 DirectAnthropicModel {
840 public_id: "claude-opus-4.5",
841 upstream_id: "claude-opus-4-5",
842 },
843 DirectAnthropicModel {
844 public_id: "claude-sonnet-4.6",
845 upstream_id: "claude-sonnet-4-6",
846 },
847 DirectAnthropicModel {
848 public_id: "claude-sonnet-4.5",
849 upstream_id: "claude-sonnet-4-5-20250929",
850 },
851 DirectAnthropicModel {
852 public_id: "claude-haiku-4.5",
853 upstream_id: "claude-haiku-4-5-20251001",
854 },
855 DirectAnthropicModel {
856 public_id: "claude-fable-5",
857 upstream_id: "claude-fable-5",
858 },
859];
860
861static DIRECT_OPENAI_MODELS: &[DirectOpenAiModel] = &[
871 DirectOpenAiModel {
872 public_id: "gpt-5.6-sol",
873 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
874 },
875 DirectOpenAiModel {
876 public_id: "gpt-5.6-terra",
877 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
878 },
879 DirectOpenAiModel {
880 public_id: "gpt-5.6-luna",
881 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
882 },
883 DirectOpenAiModel {
884 public_id: "gpt-5.5",
885 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
886 },
887 DirectOpenAiModel {
888 public_id: "gpt-5.4",
889 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
890 },
891 DirectOpenAiModel {
892 public_id: "gpt-4.1",
893 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
894 },
895 DirectOpenAiModel {
896 public_id: "gpt-4.1-mini",
897 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
898 },
899 DirectOpenAiModel {
900 public_id: "gpt-4o-mini",
901 client_apis: &[ClientApi::OpenAiChatCompletions, ClientApi::OpenAiResponses],
902 },
903];
904
905static DIRECT_DATABRICKS_MODELS: &[DirectDatabricksModel] = &[
912 DirectDatabricksModel {
913 public_id: "gpt-5.6-sol",
914 upstream: DirectDatabricksUpstream::ServingEndpointChat {
915 endpoint: "databricks-gpt-5-6-sol",
916 },
917 },
918 DirectDatabricksModel {
919 public_id: "gpt-5.6-terra",
920 upstream: DirectDatabricksUpstream::ServingEndpointChat {
921 endpoint: "databricks-gpt-5-6-terra",
922 },
923 },
924 DirectDatabricksModel {
925 public_id: "gpt-5.6-luna",
926 upstream: DirectDatabricksUpstream::ServingEndpointChat {
927 endpoint: "databricks-gpt-5-6-luna",
928 },
929 },
930 DirectDatabricksModel {
931 public_id: "gpt-5.5",
932 upstream: DirectDatabricksUpstream::ServingEndpointChat {
933 endpoint: "databricks-gpt-5-5",
934 },
935 },
936 DirectDatabricksModel {
937 public_id: "gpt-5.4",
938 upstream: DirectDatabricksUpstream::ServingEndpointChat {
939 endpoint: "databricks-gpt-5-4",
940 },
941 },
942 DirectDatabricksModel {
943 public_id: "claude-haiku-4.5",
944 upstream: DirectDatabricksUpstream::ModelServiceMessages {
945 service: "system.ai.claude-haiku-4-5",
946 },
947 },
948 DirectDatabricksModel {
949 public_id: "claude-opus-5",
950 upstream: DirectDatabricksUpstream::ModelServiceMessages {
951 service: "system.ai.claude-opus-5",
952 },
953 },
954 DirectDatabricksModel {
955 public_id: "claude-sonnet-5",
956 upstream: DirectDatabricksUpstream::ModelServiceMessages {
957 service: "system.ai.claude-sonnet-5",
958 },
959 },
960 DirectDatabricksModel {
961 public_id: "claude-sonnet-4.6",
962 upstream: DirectDatabricksUpstream::ModelServiceMessages {
963 service: "system.ai.claude-sonnet-4-6",
964 },
965 },
966 DirectDatabricksModel {
967 public_id: "claude-sonnet-4.5",
968 upstream: DirectDatabricksUpstream::ModelServiceMessages {
969 service: "system.ai.claude-sonnet-4-5",
970 },
971 },
972 DirectDatabricksModel {
973 public_id: "claude-fable-5",
974 upstream: DirectDatabricksUpstream::ModelServiceMessages {
975 service: "system.ai.claude-fable-5",
976 },
977 },
978 DirectDatabricksModel {
979 public_id: "claude-opus-4.8",
980 upstream: DirectDatabricksUpstream::ModelServiceMessages {
981 service: "system.ai.claude-opus-4-8",
982 },
983 },
984 DirectDatabricksModel {
985 public_id: "claude-opus-4.7",
986 upstream: DirectDatabricksUpstream::ModelServiceMessages {
987 service: "system.ai.claude-opus-4-7",
988 },
989 },
990 DirectDatabricksModel {
991 public_id: "claude-opus-4.6",
992 upstream: DirectDatabricksUpstream::ModelServiceMessages {
993 service: "system.ai.claude-opus-4-6",
994 },
995 },
996 DirectDatabricksModel {
997 public_id: "claude-opus-4.5",
998 upstream: DirectDatabricksUpstream::ModelServiceMessages {
999 service: "system.ai.claude-opus-4-5",
1000 },
1001 },
1002 DirectDatabricksModel {
1003 public_id: "gemini-3.5-flash",
1004 upstream: DirectDatabricksUpstream::ModelServiceChat {
1005 service: "system.ai.gemini-3-5-flash",
1006 },
1007 },
1008 DirectDatabricksModel {
1009 public_id: "gemini-3.1-flash-lite",
1010 upstream: DirectDatabricksUpstream::ModelServiceChat {
1011 service: "system.ai.gemini-3-1-flash-lite",
1012 },
1013 },
1014 DirectDatabricksModel {
1015 public_id: "gpt-oss-120b",
1016 upstream: DirectDatabricksUpstream::ServingEndpointChat {
1017 endpoint: "databricks-gpt-oss-120b",
1018 },
1019 },
1020 DirectDatabricksModel {
1021 public_id: "gpt-oss-20b",
1022 upstream: DirectDatabricksUpstream::ServingEndpointChat {
1023 endpoint: "databricks-gpt-oss-20b",
1024 },
1025 },
1026 DirectDatabricksModel {
1027 public_id: "gemma-3-12b",
1028 upstream: DirectDatabricksUpstream::ModelServiceChat {
1029 service: "system.ai.gemma-3-12b",
1030 },
1031 },
1032];
1033
1034#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1036pub struct ResponsesTarget {
1037 pub upstream_id: &'static str,
1039 pub path: &'static str,
1042}
1043
1044static RESPONSES_UPSTREAM: &[(&str, ResponsesTarget)] = &[
1049 (
1050 "gpt-oss-20b",
1051 ResponsesTarget {
1052 upstream_id: "openai.gpt-oss-20b",
1053 path: "/v1/responses",
1054 },
1055 ),
1056 (
1057 "gpt-oss-120b",
1058 ResponsesTarget {
1059 upstream_id: "openai.gpt-oss-120b",
1060 path: "/v1/responses",
1061 },
1062 ),
1063 (
1064 "gpt-5.6-sol",
1065 ResponsesTarget {
1066 upstream_id: "openai.gpt-5.6-sol",
1067 path: "/openai/v1/responses",
1068 },
1069 ),
1070 (
1071 "gpt-5.6-terra",
1072 ResponsesTarget {
1073 upstream_id: "openai.gpt-5.6-terra",
1074 path: "/openai/v1/responses",
1075 },
1076 ),
1077 (
1078 "gpt-5.6-luna",
1079 ResponsesTarget {
1080 upstream_id: "openai.gpt-5.6-luna",
1081 path: "/openai/v1/responses",
1082 },
1083 ),
1084 (
1085 "gpt-5.5",
1086 ResponsesTarget {
1087 upstream_id: "openai.gpt-5.5",
1088 path: "/openai/v1/responses",
1089 },
1090 ),
1091 (
1092 "gpt-5.4",
1093 ResponsesTarget {
1094 upstream_id: "openai.gpt-5.4",
1095 path: "/openai/v1/responses",
1096 },
1097 ),
1098];
1099
1100pub fn responses_target(public_id: &str) -> Option<ResponsesTarget> {
1103 RESPONSES_UPSTREAM
1104 .iter()
1105 .find(|(public, _)| *public == public_id)
1106 .map(|(_, target)| *target)
1107}
1108
1109pub fn models_for(cloud: Platform) -> Vec<&'static CatalogModel> {
1110 CATALOG.iter().filter(|m| m.cloud == cloud).collect()
1111}
1112
1113pub fn direct_anthropic_models() -> Vec<&'static DirectAnthropicModel> {
1114 DIRECT_ANTHROPIC_MODELS.iter().collect()
1115}
1116
1117pub fn resolve_direct_anthropic(public_id: &str) -> Option<&'static DirectAnthropicModel> {
1118 DIRECT_ANTHROPIC_MODELS
1119 .iter()
1120 .find(|model| model.public_id == public_id)
1121}
1122
1123pub fn direct_openai_models() -> Vec<&'static DirectOpenAiModel> {
1124 DIRECT_OPENAI_MODELS.iter().collect()
1125}
1126
1127pub fn resolve_direct_openai(public_id: &str) -> Option<&'static DirectOpenAiModel> {
1128 DIRECT_OPENAI_MODELS
1129 .iter()
1130 .find(|model| model.public_id == public_id)
1131}
1132
1133pub fn direct_databricks_models() -> Vec<&'static DirectDatabricksModel> {
1134 DIRECT_DATABRICKS_MODELS.iter().collect()
1135}
1136
1137pub fn resolve_direct_databricks(public_id: &str) -> Option<&'static DirectDatabricksModel> {
1138 DIRECT_DATABRICKS_MODELS
1139 .iter()
1140 .find(|model| model.public_id == public_id)
1141}
1142
1143pub fn lookup(public_id: &str) -> Option<&'static CatalogModel> {
1148 CATALOG.iter().find(|m| m.public_id == public_id)
1149}
1150
1151fn lookup_for(public_id: &str, cloud: Platform) -> Option<&'static CatalogModel> {
1152 CATALOG
1153 .iter()
1154 .find(|m| m.public_id == public_id && m.cloud == cloud)
1155}
1156
1157pub fn resolve_for(model_id: &str, cloud: Platform) -> Option<&'static CatalogModel> {
1162 lookup_for(model_id, cloud).or_else(|| lookup_for(&canonical_public_id(model_id), cloud))
1163}
1164
1165pub fn resolve(model_id: &str) -> Option<&'static CatalogModel> {
1180 lookup(model_id).or_else(|| lookup(&canonical_public_id(model_id)))
1181}
1182
1183fn canonical_public_id(model_id: &str) -> String {
1184 let mut id = model_id;
1185 if let Some(pos) = id.rfind("anthropic.") {
1186 id = &id[pos + "anthropic.".len()..];
1187 }
1188 id = id.split_once('@').map_or(id, |(base, _)| base);
1190 id = strip_invoke_version(id);
1191 id = strip_release_date(id);
1192 dot_minor_version(id)
1193}
1194
1195fn strip_invoke_version(id: &str) -> &str {
1197 let base = id.split_once(':').map_or(id, |(base, _)| base);
1198 match base.rsplit_once("-v") {
1199 Some((stem, digits))
1200 if !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit()) =>
1201 {
1202 stem
1203 }
1204 _ => base,
1205 }
1206}
1207
1208fn strip_release_date(id: &str) -> &str {
1210 match id.rsplit_once('-') {
1211 Some((stem, date))
1212 if date.len() == 8
1213 && date.starts_with("20")
1214 && date.bytes().all(|b| b.is_ascii_digit()) =>
1215 {
1216 stem
1217 }
1218 _ => id,
1219 }
1220}
1221
1222fn dot_minor_version(id: &str) -> String {
1226 let Some((stem, minor)) = id.rsplit_once('-') else {
1227 return id.to_string();
1228 };
1229 let Some((prefix, major)) = stem.rsplit_once('-') else {
1230 return id.to_string();
1231 };
1232 let both_numeric = !major.is_empty()
1233 && !minor.is_empty()
1234 && major.bytes().all(|b| b.is_ascii_digit())
1235 && minor.bytes().all(|b| b.is_ascii_digit());
1236 if both_numeric {
1237 format!("{prefix}-{major}.{minor}")
1238 } else {
1239 id.to_string()
1240 }
1241}
1242
1243pub fn azure_deployments() -> Vec<(&'static str, &'static str, &'static str)> {
1245 AZURE_DEPLOYMENTS.to_vec()
1246}
1247
1248#[cfg(test)]
1249mod tests {
1250 #[test]
1254 fn public_ids_are_unique_per_cloud() {
1255 let mut seen = std::collections::HashSet::new();
1256 for model in super::CATALOG {
1257 assert!(
1258 seen.insert((model.cloud, model.public_id)),
1259 "public id '{}' appears more than once under {:?}",
1260 model.public_id,
1261 model.cloud
1262 );
1263 }
1264 }
1265
1266 #[test]
1267 fn direct_anthropic_aliases_are_unique_and_round_trip() {
1268 let mut public_ids = std::collections::HashSet::new();
1269 let mut upstream_ids = std::collections::HashSet::new();
1270 for model in DIRECT_ANTHROPIC_MODELS {
1271 assert!(public_ids.insert(model.public_id));
1272 assert!(upstream_ids.insert(model.upstream_id));
1273 assert_eq!(
1274 resolve_direct_anthropic(model.public_id)
1275 .expect("direct model must resolve")
1276 .upstream_id,
1277 model.upstream_id
1278 );
1279 assert_ne!(model.display_name(), model.public_id);
1280 }
1281 assert!(resolve_direct_anthropic("claude-not-real").is_none());
1282 }
1283
1284 #[test]
1285 fn direct_openai_models_are_unique_and_have_qualified_apis() {
1286 let mut public_ids = std::collections::HashSet::new();
1287 for model in DIRECT_OPENAI_MODELS {
1288 assert!(public_ids.insert(model.public_id));
1289 assert!(!model.client_apis.is_empty());
1290 assert_eq!(
1291 resolve_direct_openai(model.public_id)
1292 .expect("direct model must resolve")
1293 .client_apis,
1294 model.client_apis
1295 );
1296 }
1297 assert!(resolve_direct_openai("text-embedding-3-small").is_none());
1298 assert!(resolve_direct_openai("gpt-image-1").is_none());
1299 }
1300
1301 #[test]
1302 fn direct_databricks_models_are_unique_and_resolve_to_provider_ids() {
1303 let mut public_ids = std::collections::HashSet::new();
1304 for model in DIRECT_DATABRICKS_MODELS {
1305 assert!(public_ids.insert(model.public_id));
1306 let upstream_id = model.upstream.model_id();
1307 assert!(
1308 upstream_id.starts_with("databricks-") || upstream_id.starts_with("system.ai.")
1309 );
1310 assert_eq!(
1311 resolve_direct_databricks(model.public_id)
1312 .expect("direct Databricks model must resolve")
1313 .upstream,
1314 model.upstream
1315 );
1316 }
1317 assert!(resolve_direct_databricks("bge-large-en").is_none());
1318 assert!(resolve_direct_databricks("databricks-genie").is_none());
1319 assert_eq!(
1320 resolve_direct_databricks("claude-opus-5")
1321 .expect("Claude Opus 5 must resolve")
1322 .upstream
1323 .model_id(),
1324 "system.ai.claude-opus-5"
1325 );
1326 assert_eq!(
1327 resolve_direct_databricks("gpt-oss-120b")
1328 .expect("GPT-OSS 120B must resolve")
1329 .upstream,
1330 DirectDatabricksUpstream::ServingEndpointChat {
1331 endpoint: "databricks-gpt-oss-120b"
1332 }
1333 );
1334 }
1335
1336 use super::*;
1337
1338 #[test]
1339 fn resolve_accepts_anthropic_native_spellings() {
1340 assert_eq!(
1342 resolve("claude-haiku-4-5").unwrap().public_id,
1343 "claude-haiku-4.5"
1344 );
1345 assert_eq!(
1346 resolve("claude-sonnet-4-5-20250929").unwrap().public_id,
1347 "claude-sonnet-4.5"
1348 );
1349 assert_eq!(
1351 resolve("us.anthropic.claude-haiku-4-5-20251001-v1:0")
1352 .unwrap()
1353 .public_id,
1354 "claude-haiku-4.5"
1355 );
1356 assert_eq!(
1357 resolve("anthropic.claude-opus-4-6-v1").unwrap().public_id,
1358 "claude-opus-4.6"
1359 );
1360 assert_eq!(
1362 resolve("claude-sonnet-5").unwrap().public_id,
1363 "claude-sonnet-5"
1364 );
1365 assert_eq!(
1367 resolve("claude-opus-4.8").unwrap().public_id,
1368 "claude-opus-4.8"
1369 );
1370 assert_eq!(resolve("gpt-oss-20b").unwrap().public_id, "gpt-oss-20b");
1371 assert!(resolve("claude-nonexistent-9-9").is_none());
1373 assert!(resolve("gpt-5").is_none());
1374 }
1375
1376 #[test]
1377 fn aws_has_openai_and_anthropic_with_plain_ids() {
1378 let aws = models_for(Platform::Aws);
1379 assert!(!aws.is_empty());
1380 assert!(aws
1381 .iter()
1382 .any(|m| m.public_id == "gpt-oss-20b" && m.provider_api == ProviderApi::OpenAi));
1383 assert!(
1384 aws.iter().any(|m| m.provider_api == ProviderApi::Anthropic),
1385 "Claude must be included via the Anthropic protocol"
1386 );
1387 assert!(aws.iter().all(|m| !m.upstream_id.starts_with("us.")));
1389 }
1390
1391 #[test]
1392 fn resolve_for_scopes_to_cloud() {
1393 let aws = resolve_for("claude-opus-4.8", Platform::Aws).expect("aws claude");
1396 assert_eq!(aws.upstream_id, "anthropic.claude-opus-4-8");
1397 let gcp = resolve_for("claude-opus-4.8", Platform::Gcp).expect("gcp claude");
1398 assert_eq!(gcp.upstream_id, "claude-opus-4-8");
1399 assert_eq!(gcp.provider_api, ProviderApi::Anthropic);
1400 let dated = resolve_for("claude-haiku-4-5-20251001", Platform::Gcp).expect("dated id");
1403 assert_eq!(dated.upstream_id, "claude-haiku-4-5@20251001");
1404 let vertex = resolve_for("claude-sonnet-4-5@20250929", Platform::Gcp).expect("vertex id");
1407 assert_eq!(vertex.upstream_id, "claude-sonnet-4-5@20250929");
1408 assert!(resolve_for("gemini-2.5-pro", Platform::Aws).is_none());
1410 assert!(resolve_for("gpt-4.1", Platform::Gcp).is_none());
1411 }
1412
1413 #[test]
1414 fn lookup_round_trips() {
1415 let m = lookup("gpt-oss-20b").expect("known model");
1416 assert_eq!(m.cloud, Platform::Aws);
1417 assert_eq!(m.provider_api, ProviderApi::OpenAi);
1418 assert_eq!(m.upstream_id, "openai.gpt-oss-20b-1:0");
1419
1420 let c = lookup("claude-opus-4.8").expect("claude known");
1421 assert_eq!(c.provider_api, ProviderApi::Anthropic);
1422
1423 assert!(lookup("nonexistent-model").is_none());
1424 }
1425
1426 #[test]
1427 fn azure_deployments_map_to_catalog() {
1428 assert!(!azure_deployments().is_empty());
1429 for (deployment, _, _) in azure_deployments() {
1430 assert!(
1431 models_for(Platform::Azure)
1432 .iter()
1433 .any(|m| m.upstream_id == deployment),
1434 "azure deployment {deployment} must map to a catalog model"
1435 );
1436 }
1437 }
1438
1439 #[test]
1440 fn api_kinds_have_stable_wire_names() {
1441 assert_eq!(
1442 serde_json::to_string(&ProviderApi::OpenAi).unwrap(),
1443 "\"openai\""
1444 );
1445 assert_eq!(
1446 serde_json::to_string(&ProviderApi::Anthropic).unwrap(),
1447 "\"anthropic\""
1448 );
1449 assert_eq!(
1450 serde_json::to_string(&ProviderApi::OpenAiResponses).unwrap(),
1451 "\"openairesponses\""
1452 );
1453 assert_eq!(
1454 serde_json::to_string(&ClientApi::OpenAiChatCompletions).unwrap(),
1455 "\"open-ai-chat-completions\""
1456 );
1457 assert_eq!(
1458 serde_json::to_string(&ClientApi::OpenAiResponses).unwrap(),
1459 "\"open-ai-responses\""
1460 );
1461 assert_eq!(
1462 serde_json::to_string(&ClientApi::AnthropicMessages).unwrap(),
1463 "\"anthropic-messages\""
1464 );
1465 }
1466
1467 #[test]
1468 fn client_apis_are_explicit_and_non_empty() {
1469 for model in CATALOG {
1470 assert!(
1471 !model.client_apis.is_empty(),
1472 "'{}' has no supported client API",
1473 model.public_id
1474 );
1475 }
1476
1477 let gpt_oss = resolve_for("gpt-oss-20b", Platform::Aws).unwrap();
1478 assert!(gpt_oss
1479 .client_apis
1480 .contains(&ClientApi::OpenAiChatCompletions));
1481 assert!(gpt_oss.client_apis.contains(&ClientApi::OpenAiResponses));
1482 }
1483
1484 #[test]
1485 fn every_model_has_provider_display_name_and_activation() {
1486 for m in CATALOG {
1487 assert_ne!(
1488 m.provider(),
1489 "unknown",
1490 "no provider mapping for '{}'",
1491 m.public_id
1492 );
1493 assert_ne!(
1494 m.display_name(),
1495 m.public_id,
1496 "no curated display_name for '{}'",
1497 m.public_id
1498 );
1499 let is_claude = m.public_id.starts_with("claude");
1501 match m.activation() {
1502 Activation::OutOfBox => {
1503 assert!(
1504 !is_claude,
1505 "'{}' (Claude) must require a one-time step",
1506 m.public_id
1507 )
1508 }
1509 Activation::RequiresOneTimeStep(summary) => {
1510 assert!(is_claude, "'{}' must be out of the box", m.public_id);
1511 assert!(
1512 !summary.is_empty(),
1513 "'{}' step summary is empty",
1514 m.public_id
1515 );
1516 }
1517 }
1518 }
1519 }
1520
1521 #[test]
1525 fn only_claude_ids_speak_the_anthropic_protocol() {
1526 for m in CATALOG {
1527 assert_eq!(
1528 m.provider_api == ProviderApi::Anthropic,
1529 m.public_id.starts_with("claude"),
1530 "'{}' is {:?} but its id says otherwise",
1531 m.public_id,
1532 m.provider_api
1533 );
1534 }
1535 }
1536}