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