1use serde::{Deserialize, Serialize};
19
20pub const CATALOG_AS_OF: &str = "2026-08-23";
22
23pub const GEMINI_DEFAULT: &str = "gemini-3.7-flash";
25pub const OPENAI_DEFAULT: &str = "gpt-5.6-terra";
27pub const ANTHROPIC_DEFAULT: &str = "claude-sonnet-5";
29pub const DEEPSEEK_DEFAULT: &str = "deepseek-v4-flash";
31pub const GROQ_DEFAULT: &str = "openai/gpt-oss-120b";
33pub const OLLAMA_DEFAULT: &str = "qwen3.5";
35pub const OPENROUTER_DEFAULT: &str = "qwen/qwen3.7-max";
37pub const FIREWORKS_DEFAULT: &str = "accounts/fireworks/models/kimi-k2p6";
39pub const TOGETHER_DEFAULT: &str = "MiniMaxAI/MiniMax-M2.7";
41pub const MISTRAL_DEFAULT: &str = "mistral-medium-latest";
43pub const PERPLEXITY_DEFAULT: &str = "sonar-pro";
45pub const CEREBRAS_DEFAULT: &str = "gpt-oss-120b";
47pub const SAMBANOVA_DEFAULT: &str = "gpt-oss-120b";
49pub const XAI_DEFAULT: &str = "grok-4.6";
51pub const MINIMAX_DEFAULT: &str = "MiniMax-M2.7";
53pub const ZHIPU_DEFAULT: &str = "glm-5.2";
55pub const BAIDU_DEFAULT: &str = "ernie-5.1";
57pub const COHERE_DEFAULT: &str = "command-a-plus-05-2026";
59
60pub const OPENAI_REALTIME_DEFAULT: &str = "gpt-realtime-2.1";
62pub const GEMINI_LIVE_DEFAULT: &str = "gemini-3.1-flash-live-preview";
64pub const OPENAI_LIVE_TRANSCRIPTION_DEFAULT: &str = "gpt-live-transcribe";
66pub const GEMINI_TRANSCRIPTION_DEFAULT: &str = GEMINI_DEFAULT;
68pub const DEEPGRAM_DEFAULT: &str = "nova-3";
70pub const CARTESIA_DEFAULT: &str = "sonic-3.5";
72pub const GEMINI_TTS_DEFAULT: &str = "gemini-3.1-flash-tts-preview";
74pub const GEMINI_EMBEDDING_DEFAULT: &str = "gemini-embedding-2";
76pub const OPENAI_EMBEDDING_DEFAULT: &str = "text-embedding-3-small";
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase")]
82pub enum ModelLifecycle {
83 Active,
85 Preview,
87 Deprecated,
89 Retired,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(rename_all = "camelCase")]
96pub enum ModelRole {
97 Balanced,
99 Flagship,
101 Economy,
103 Realtime,
105 Transcription,
107 Speech,
109 Embedding,
111 Image,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
117#[serde(rename_all = "camelCase")]
118pub struct ModelCatalogEntry {
119 pub provider: &'static str,
121 pub model: &'static str,
123 pub role: ModelRole,
125 pub lifecycle: ModelLifecycle,
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub replacement: Option<&'static str>,
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub shutdown_date: Option<&'static str>,
133 pub recommended_default: bool,
135}
136
137impl ModelCatalogEntry {
138 const fn active(provider: &'static str, model: &'static str, role: ModelRole) -> Self {
139 Self {
140 provider,
141 model,
142 role,
143 lifecycle: ModelLifecycle::Active,
144 replacement: None,
145 shutdown_date: None,
146 recommended_default: false,
147 }
148 }
149
150 const fn default(provider: &'static str, model: &'static str, role: ModelRole) -> Self {
151 Self { recommended_default: true, ..Self::active(provider, model, role) }
152 }
153
154 const fn preview(provider: &'static str, model: &'static str, role: ModelRole) -> Self {
155 Self {
156 provider,
157 model,
158 role,
159 lifecycle: ModelLifecycle::Preview,
160 replacement: None,
161 shutdown_date: None,
162 recommended_default: false,
163 }
164 }
165
166 const fn default_preview(provider: &'static str, model: &'static str, role: ModelRole) -> Self {
167 Self { recommended_default: true, ..Self::preview(provider, model, role) }
168 }
169
170 const fn obsolete(
171 provider: &'static str,
172 model: &'static str,
173 lifecycle: ModelLifecycle,
174 replacement: &'static str,
175 shutdown_date: Option<&'static str>,
176 ) -> Self {
177 Self {
178 provider,
179 model,
180 role: ModelRole::Balanced,
181 lifecycle,
182 replacement: Some(replacement),
183 shutdown_date,
184 recommended_default: false,
185 }
186 }
187}
188
189pub const KNOWN_PROVIDERS: &[&str] = &[
191 "gemini",
192 "openai",
193 "anthropic",
194 "deepseek",
195 "groq",
196 "ollama",
197 "openrouter",
198 "fireworks",
199 "together",
200 "mistral",
201 "perplexity",
202 "cerebras",
203 "sambanova",
204 "xai",
205 "minimax",
206 "zhipu",
207 "baidu",
208 "cohere",
209 "azure-ai",
210 "bedrock",
211 "bytedance",
212 "openai-realtime",
213 "gemini-live",
214 "openai-transcription",
215 "gemini-transcription",
216 "deepgram",
217 "cartesia",
218 "gemini-tts",
219 "gemini-embedding",
220 "openai-embedding",
221];
222
223pub const MODEL_CATALOG: &[ModelCatalogEntry] = &[
225 ModelCatalogEntry::default("gemini", GEMINI_DEFAULT, ModelRole::Balanced),
226 ModelCatalogEntry::active("gemini", "gemini-3.6-flash", ModelRole::Balanced),
227 ModelCatalogEntry::active("gemini", "gemini-3.5-flash", ModelRole::Balanced),
228 ModelCatalogEntry::active("gemini", "gemini-3.5-flash-lite", ModelRole::Economy),
229 ModelCatalogEntry::obsolete(
230 "gemini",
231 "gemini-3.1-flash-lite",
232 ModelLifecycle::Deprecated,
233 "gemini-3.5-flash-lite",
234 Some("2027-05-07"),
235 ),
236 ModelCatalogEntry::preview("gemini", "gemini-3.1-pro-preview", ModelRole::Flagship),
237 ModelCatalogEntry::active("gemini", "gemini-3.1-flash-image", ModelRole::Image),
238 ModelCatalogEntry::active("gemini", "gemini-3-pro-image", ModelRole::Image),
239 ModelCatalogEntry::default("gemini-embedding", GEMINI_EMBEDDING_DEFAULT, ModelRole::Embedding),
240 ModelCatalogEntry::default("openai", OPENAI_DEFAULT, ModelRole::Balanced),
241 ModelCatalogEntry::active("openai", "gpt-5.6-sol", ModelRole::Flagship),
242 ModelCatalogEntry::active("openai", "gpt-5.6-luna", ModelRole::Economy),
243 ModelCatalogEntry::active("openai", "gpt-5.6", ModelRole::Flagship),
244 ModelCatalogEntry::default("anthropic", ANTHROPIC_DEFAULT, ModelRole::Balanced),
245 ModelCatalogEntry::active("anthropic", "claude-opus-5", ModelRole::Flagship),
246 ModelCatalogEntry::active("anthropic", "claude-fable-5", ModelRole::Flagship),
247 ModelCatalogEntry::active("anthropic", "claude-haiku-4-5", ModelRole::Economy),
248 ModelCatalogEntry::default("deepseek", DEEPSEEK_DEFAULT, ModelRole::Balanced),
249 ModelCatalogEntry::active("deepseek", "deepseek-v4-pro", ModelRole::Flagship),
250 ModelCatalogEntry::default("groq", GROQ_DEFAULT, ModelRole::Balanced),
251 ModelCatalogEntry::active("groq", "openai/gpt-oss-20b", ModelRole::Economy),
252 ModelCatalogEntry::default("ollama", OLLAMA_DEFAULT, ModelRole::Balanced),
253 ModelCatalogEntry::default("openrouter", OPENROUTER_DEFAULT, ModelRole::Balanced),
254 ModelCatalogEntry::default("fireworks", FIREWORKS_DEFAULT, ModelRole::Balanced),
255 ModelCatalogEntry::active(
256 "fireworks",
257 "accounts/fireworks/models/kimi-k3",
258 ModelRole::Flagship,
259 ),
260 ModelCatalogEntry::default("together", TOGETHER_DEFAULT, ModelRole::Balanced),
261 ModelCatalogEntry::default("mistral", MISTRAL_DEFAULT, ModelRole::Balanced),
262 ModelCatalogEntry::default("perplexity", PERPLEXITY_DEFAULT, ModelRole::Balanced),
263 ModelCatalogEntry::default("cerebras", CEREBRAS_DEFAULT, ModelRole::Balanced),
264 ModelCatalogEntry::default("sambanova", SAMBANOVA_DEFAULT, ModelRole::Balanced),
265 ModelCatalogEntry::default("xai", XAI_DEFAULT, ModelRole::Balanced),
266 ModelCatalogEntry::default("minimax", MINIMAX_DEFAULT, ModelRole::Balanced),
267 ModelCatalogEntry::default("zhipu", ZHIPU_DEFAULT, ModelRole::Balanced),
268 ModelCatalogEntry::default("baidu", BAIDU_DEFAULT, ModelRole::Balanced),
269 ModelCatalogEntry::default("cohere", COHERE_DEFAULT, ModelRole::Balanced),
270 ModelCatalogEntry::default("openai-realtime", OPENAI_REALTIME_DEFAULT, ModelRole::Realtime),
271 ModelCatalogEntry::default_preview("gemini-live", GEMINI_LIVE_DEFAULT, ModelRole::Realtime),
272 ModelCatalogEntry::active(
273 "gemini-live",
274 "gemini-live-2.5-flash-native-audio",
275 ModelRole::Realtime,
276 ),
277 ModelCatalogEntry::default(
278 "openai-transcription",
279 OPENAI_LIVE_TRANSCRIPTION_DEFAULT,
280 ModelRole::Transcription,
281 ),
282 ModelCatalogEntry::default(
283 "gemini-transcription",
284 GEMINI_TRANSCRIPTION_DEFAULT,
285 ModelRole::Transcription,
286 ),
287 ModelCatalogEntry::default("deepgram", DEEPGRAM_DEFAULT, ModelRole::Transcription),
288 ModelCatalogEntry::default("cartesia", CARTESIA_DEFAULT, ModelRole::Speech),
289 ModelCatalogEntry::default_preview("gemini-tts", GEMINI_TTS_DEFAULT, ModelRole::Speech),
290 ModelCatalogEntry::default("openai-embedding", OPENAI_EMBEDDING_DEFAULT, ModelRole::Embedding),
291 ModelCatalogEntry::obsolete(
292 "gemini",
293 "gemini-3.1-flash-lite-preview",
294 ModelLifecycle::Retired,
295 "gemini-3.1-flash-lite",
296 Some("2026-05-25"),
297 ),
298 ModelCatalogEntry::obsolete(
299 "gemini",
300 "gemini-3-flash-preview",
301 ModelLifecycle::Deprecated,
302 "gemini-3.6-flash",
303 None,
304 ),
305 ModelCatalogEntry::obsolete(
306 "gemini",
307 "gemini-3-pro-preview",
308 ModelLifecycle::Retired,
309 "gemini-3.1-pro-preview",
310 Some("2026-03-09"),
311 ),
312 ModelCatalogEntry::obsolete(
313 "gemini",
314 "gemini-3.1-flash-image-preview",
315 ModelLifecycle::Retired,
316 "gemini-3.1-flash-image",
317 Some("2026-06-25"),
318 ),
319 ModelCatalogEntry::obsolete(
320 "gemini",
321 "gemini-3-pro-image-preview",
322 ModelLifecycle::Retired,
323 "gemini-3-pro-image",
324 Some("2026-06-25"),
325 ),
326 ModelCatalogEntry::obsolete(
327 "gemini",
328 "gemini-2.0-flash",
329 ModelLifecycle::Retired,
330 "gemini-3.6-flash",
331 Some("2026-06-01"),
332 ),
333 ModelCatalogEntry::obsolete(
334 "gemini",
335 "gemini-2.0-flash-001",
336 ModelLifecycle::Retired,
337 "gemini-3.6-flash",
338 Some("2026-06-01"),
339 ),
340 ModelCatalogEntry::obsolete(
341 "gemini",
342 "gemini-2.0-flash-lite",
343 ModelLifecycle::Retired,
344 "gemini-3.1-flash-lite",
345 Some("2026-06-01"),
346 ),
347 ModelCatalogEntry::obsolete(
348 "gemini",
349 "gemini-2.0-flash-lite-001",
350 ModelLifecycle::Retired,
351 "gemini-3.1-flash-lite",
352 Some("2026-06-01"),
353 ),
354 ModelCatalogEntry::obsolete(
355 "gemini",
356 "gemini-2.5-flash-image-preview",
357 ModelLifecycle::Retired,
358 "gemini-3.1-flash-image",
359 Some("2026-01-15"),
360 ),
361 ModelCatalogEntry::obsolete(
362 "groq",
363 "llama-3.3-70b-versatile",
364 ModelLifecycle::Deprecated,
365 GROQ_DEFAULT,
366 Some("2026-08-16"),
367 ),
368 ModelCatalogEntry::obsolete(
369 "groq",
370 "llama-3.1-8b-instant",
371 ModelLifecycle::Deprecated,
372 "openai/gpt-oss-20b",
373 Some("2026-08-16"),
374 ),
375 ModelCatalogEntry::obsolete(
376 "groq",
377 "meta-llama/llama-4-scout-17b-16e-instruct",
378 ModelLifecycle::Deprecated,
379 GROQ_DEFAULT,
380 Some("2026-07-17"),
381 ),
382 ModelCatalogEntry::obsolete(
383 "groq",
384 "qwen/qwen3-32b",
385 ModelLifecycle::Deprecated,
386 GROQ_DEFAULT,
387 Some("2026-07-17"),
388 ),
389 ModelCatalogEntry::obsolete(
390 "deepseek",
391 "deepseek-chat",
392 ModelLifecycle::Retired,
393 DEEPSEEK_DEFAULT,
394 Some("2026-07-24"),
395 ),
396 ModelCatalogEntry::obsolete(
397 "deepseek",
398 "deepseek-reasoner",
399 ModelLifecycle::Retired,
400 "deepseek-v4-pro",
401 Some("2026-07-24"),
402 ),
403 ModelCatalogEntry::obsolete(
404 "cerebras",
405 "llama-3.3-70b",
406 ModelLifecycle::Retired,
407 CEREBRAS_DEFAULT,
408 None,
409 ),
410 ModelCatalogEntry::obsolete(
411 "cartesia",
412 "sonic-2",
413 ModelLifecycle::Deprecated,
414 CARTESIA_DEFAULT,
415 None,
416 ),
417 ModelCatalogEntry::obsolete(
418 "gemini-live",
419 "gemini-2.5-flash-native-audio-preview-12-2025",
420 ModelLifecycle::Deprecated,
421 GEMINI_LIVE_DEFAULT,
422 None,
423 ),
424 ModelCatalogEntry::obsolete(
425 "gemini-live",
426 "gemini-live-2.5-flash-preview",
427 ModelLifecycle::Retired,
428 GEMINI_LIVE_DEFAULT,
429 Some("2025-12-09"),
430 ),
431 ModelCatalogEntry::obsolete(
432 "minimax",
433 "minimax-m2.7",
434 ModelLifecycle::Retired,
435 MINIMAX_DEFAULT,
436 None,
437 ),
438 ModelCatalogEntry::obsolete("baidu", "ernie-5", ModelLifecycle::Retired, BAIDU_DEFAULT, None),
439];
440
441pub fn recommended_model(provider: &str) -> Option<&'static str> {
446 match provider {
447 "gemini" => Some(GEMINI_DEFAULT),
448 "openai" => Some(OPENAI_DEFAULT),
449 "anthropic" => Some(ANTHROPIC_DEFAULT),
450 "deepseek" => Some(DEEPSEEK_DEFAULT),
451 "groq" => Some(GROQ_DEFAULT),
452 "ollama" => Some(OLLAMA_DEFAULT),
453 "openrouter" => Some(OPENROUTER_DEFAULT),
454 "fireworks" => Some(FIREWORKS_DEFAULT),
455 "together" => Some(TOGETHER_DEFAULT),
456 "mistral" => Some(MISTRAL_DEFAULT),
457 "perplexity" => Some(PERPLEXITY_DEFAULT),
458 "cerebras" => Some(CEREBRAS_DEFAULT),
459 "sambanova" => Some(SAMBANOVA_DEFAULT),
460 "xai" => Some(XAI_DEFAULT),
461 "minimax" => Some(MINIMAX_DEFAULT),
462 "zhipu" => Some(ZHIPU_DEFAULT),
463 "baidu" => Some(BAIDU_DEFAULT),
464 "cohere" => Some(COHERE_DEFAULT),
465 "openai-realtime" => Some(OPENAI_REALTIME_DEFAULT),
466 "gemini-live" => Some(GEMINI_LIVE_DEFAULT),
467 "openai-transcription" => Some(OPENAI_LIVE_TRANSCRIPTION_DEFAULT),
468 "gemini-transcription" => Some(GEMINI_TRANSCRIPTION_DEFAULT),
469 "deepgram" => Some(DEEPGRAM_DEFAULT),
470 "cartesia" => Some(CARTESIA_DEFAULT),
471 "gemini-tts" => Some(GEMINI_TTS_DEFAULT),
472 "gemini-embedding" => Some(GEMINI_EMBEDDING_DEFAULT),
473 "openai-embedding" => Some(OPENAI_EMBEDDING_DEFAULT),
474 _ => None,
475 }
476}
477
478pub fn requires_explicit_model(provider: &str) -> bool {
480 matches!(provider, "azure-ai" | "bedrock" | "bytedance")
481}
482
483pub fn lookup_model(provider: &str, model: &str) -> Option<&'static ModelCatalogEntry> {
487 let normalized = if provider == "gemini" || provider == "gemini-live" {
488 model.strip_prefix("models/").unwrap_or(model)
489 } else {
490 model
491 };
492 MODEL_CATALOG.iter().find(|entry| entry.provider == provider && entry.model == normalized)
493}
494
495pub fn validate_model_selection(provider: &str, model: &str) -> adk_core::Result<()> {
501 if model.trim().is_empty() {
502 return Err(adk_core::AdkError::new(
503 adk_core::ErrorComponent::Model,
504 adk_core::ErrorCategory::InvalidInput,
505 "model.catalog.empty_model",
506 format!(
507 "model ID for provider '{provider}' is empty; pass an explicit model or deployment ID"
508 ),
509 )
510 .with_provider(provider));
511 }
512 if let Some(entry) = lookup_model(provider, model)
513 && entry.lifecycle == ModelLifecycle::Retired
514 {
515 let replacement = entry.replacement.unwrap_or("a current provider model");
516 return Err(adk_core::AdkError::new(
517 adk_core::ErrorComponent::Model,
518 adk_core::ErrorCategory::InvalidInput,
519 "model.catalog.retired_model",
520 format!(
521 "model '{model}' is retired for provider '{provider}'; use '{replacement}' instead"
522 ),
523 )
524 .with_provider(provider));
525 }
526 Ok(())
527}
528
529pub fn warn_if_obsolete(provider: &str, model: &str) {
536 if let Some(entry) = lookup_model(provider, model)
537 && matches!(entry.lifecycle, ModelLifecycle::Deprecated | ModelLifecycle::Retired)
538 {
539 tracing::warn!(
540 provider,
541 model,
542 lifecycle = ?entry.lifecycle,
543 replacement = entry.replacement,
544 shutdown_date = entry.shutdown_date,
545 "configured model is obsolete"
546 );
547 }
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553 use std::collections::HashSet;
554
555 #[test]
556 fn entries_are_unique() {
557 let mut seen = HashSet::new();
558 for entry in MODEL_CATALOG {
559 assert!(
560 seen.insert((entry.provider, entry.model)),
561 "duplicate catalog entry: {entry:?}"
562 );
563 }
564 }
565
566 #[test]
567 fn every_portable_default_is_active_and_catalogued() {
568 for provider in KNOWN_PROVIDERS {
569 let Some(model) = recommended_model(provider) else {
570 assert!(requires_explicit_model(provider));
571 continue;
572 };
573 let entry = lookup_model(provider, model)
574 .unwrap_or_else(|| panic!("default {provider}/{model} is missing from catalog"));
575 assert!(
576 matches!(entry.lifecycle, ModelLifecycle::Active | ModelLifecycle::Preview),
577 "obsolete default: {entry:?}"
578 );
579 assert!(entry.recommended_default, "default entry is not marked as default: {entry:?}");
580 }
581 }
582
583 #[test]
584 fn unknown_and_private_models_remain_usable() {
585 assert!(validate_model_selection("openai", "ft:gpt-private:team:model").is_ok());
586 assert!(validate_model_selection("azure-ai", "my-production-deployment").is_ok());
587 }
588
589 #[test]
590 fn retired_model_reports_replacement() {
591 let error = validate_model_selection("gemini", "models/gemini-2.0-flash")
592 .expect_err("retired model must be rejected by explicit validation");
593 assert!(error.to_string().contains("gemini-3.6-flash"));
594 }
595
596 #[test]
597 fn live_catalog_distinguishes_vertex_ga_from_retired_studio_model() {
598 let vertex = lookup_model("gemini-live", "models/gemini-live-2.5-flash-native-audio")
599 .expect("Vertex Live GA model should be catalogued");
600 assert_eq!(vertex.lifecycle, ModelLifecycle::Active);
601
602 let error = validate_model_selection("gemini-live", "gemini-live-2.5-flash-preview")
603 .expect_err("retired AI Studio Live model must be rejected");
604 assert!(error.to_string().contains(GEMINI_LIVE_DEFAULT));
605 }
606
607 #[test]
608 fn catalog_is_serializable() {
609 let value = serde_json::to_value(MODEL_CATALOG).expect("catalog should serialize");
610 assert!(value.as_array().is_some_and(|entries| !entries.is_empty()));
611 }
612}