use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub enum Provider {
#[default]
Gemini,
OpenAI,
Anthropic,
OpenRouter,
}
impl Provider {
pub fn default_api_key_env(&self) -> &'static str {
match self {
Provider::Gemini => "GEMINI_API_KEY",
Provider::OpenAI => "OPENAI_API_KEY",
Provider::Anthropic => "ANTHROPIC_API_KEY",
Provider::OpenRouter => "OPENROUTER_API_KEY",
}
}
pub fn all_providers() -> Vec<Provider> {
vec![
Provider::Gemini,
Provider::OpenAI,
Provider::Anthropic,
Provider::OpenRouter,
]
}
}
impl fmt::Display for Provider {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Provider::Gemini => write!(f, "gemini"),
Provider::OpenAI => write!(f, "openai"),
Provider::Anthropic => write!(f, "anthropic"),
Provider::OpenRouter => write!(f, "openrouter"),
}
}
}
impl FromStr for Provider {
type Err = ModelParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"gemini" => Ok(Provider::Gemini),
"openai" => Ok(Provider::OpenAI),
"anthropic" => Ok(Provider::Anthropic),
"openrouter" => Ok(Provider::OpenRouter),
_ => Err(ModelParseError::InvalidProvider(s.to_string())),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ModelId {
Gemini25FlashPreview,
Gemini25Flash,
Gemini25FlashLite,
Gemini25Pro,
GPT5,
GPT5Mini,
GPT5Nano,
CodexMiniLatest,
ClaudeOpus41,
ClaudeSonnet4,
OpenRouterGrokCodeFast1,
OpenRouterQwen3Coder,
OpenRouterDeepSeekChatV31,
OpenRouterOpenAIGPT5,
OpenRouterAnthropicClaudeSonnet4,
}
impl ModelId {
pub fn as_str(&self) -> &'static str {
use crate::config::constants::models;
match self {
ModelId::Gemini25FlashPreview => models::GEMINI_2_5_FLASH_PREVIEW,
ModelId::Gemini25Flash => models::GEMINI_2_5_FLASH,
ModelId::Gemini25FlashLite => models::GEMINI_2_5_FLASH_LITE,
ModelId::Gemini25Pro => models::GEMINI_2_5_PRO,
ModelId::GPT5 => models::GPT_5,
ModelId::GPT5Mini => models::GPT_5_MINI,
ModelId::GPT5Nano => models::GPT_5_NANO,
ModelId::CodexMiniLatest => models::CODEX_MINI_LATEST,
ModelId::ClaudeOpus41 => models::CLAUDE_OPUS_4_1_20250805,
ModelId::ClaudeSonnet4 => models::CLAUDE_SONNET_4_20250514,
ModelId::OpenRouterGrokCodeFast1 => models::OPENROUTER_X_AI_GROK_CODE_FAST_1,
ModelId::OpenRouterQwen3Coder => models::OPENROUTER_QWEN3_CODER,
ModelId::OpenRouterDeepSeekChatV31 => models::OPENROUTER_DEEPSEEK_CHAT_V3_1,
ModelId::OpenRouterOpenAIGPT5 => models::OPENROUTER_OPENAI_GPT_5,
ModelId::OpenRouterAnthropicClaudeSonnet4 => {
models::OPENROUTER_ANTHROPIC_CLAUDE_SONNET_4
}
}
}
pub fn provider(&self) -> Provider {
match self {
ModelId::Gemini25FlashPreview
| ModelId::Gemini25Flash
| ModelId::Gemini25FlashLite
| ModelId::Gemini25Pro => Provider::Gemini,
ModelId::GPT5 | ModelId::GPT5Mini | ModelId::GPT5Nano | ModelId::CodexMiniLatest => {
Provider::OpenAI
}
ModelId::ClaudeOpus41 | ModelId::ClaudeSonnet4 => Provider::Anthropic,
ModelId::OpenRouterGrokCodeFast1
| ModelId::OpenRouterQwen3Coder
| ModelId::OpenRouterDeepSeekChatV31
| ModelId::OpenRouterOpenAIGPT5
| ModelId::OpenRouterAnthropicClaudeSonnet4 => Provider::OpenRouter,
}
}
pub fn display_name(&self) -> &'static str {
match self {
ModelId::Gemini25FlashPreview => "Gemini 2.5 Flash Preview",
ModelId::Gemini25Flash => "Gemini 2.5 Flash",
ModelId::Gemini25FlashLite => "Gemini 2.5 Flash Lite",
ModelId::Gemini25Pro => "Gemini 2.5 Pro",
ModelId::GPT5 => "GPT-5",
ModelId::GPT5Mini => "GPT-5 Mini",
ModelId::GPT5Nano => "GPT-5 Nano",
ModelId::CodexMiniLatest => "Codex Mini Latest",
ModelId::ClaudeOpus41 => "Claude Opus 4.1",
ModelId::ClaudeSonnet4 => "Claude Sonnet 4",
ModelId::OpenRouterGrokCodeFast1 => "Grok Code Fast 1",
ModelId::OpenRouterQwen3Coder => "Qwen3 Coder",
ModelId::OpenRouterDeepSeekChatV31 => "DeepSeek Chat v3.1",
ModelId::OpenRouterOpenAIGPT5 => "OpenAI GPT-5 via OpenRouter",
ModelId::OpenRouterAnthropicClaudeSonnet4 => "Anthropic Claude Sonnet 4 via OpenRouter",
}
}
pub fn description(&self) -> &'static str {
match self {
ModelId::Gemini25FlashPreview => {
"Latest fast Gemini model with advanced multimodal capabilities"
}
ModelId::Gemini25Flash => {
"Legacy alias for Gemini 2.5 Flash Preview (same capabilities)"
}
ModelId::Gemini25FlashLite => {
"Legacy alias for Gemini 2.5 Flash Preview optimized for efficiency"
}
ModelId::Gemini25Pro => "Latest most capable Gemini model with reasoning",
ModelId::GPT5 => "Latest most capable OpenAI model with advanced reasoning",
ModelId::GPT5Mini => "Latest efficient OpenAI model, great for most tasks",
ModelId::GPT5Nano => "Latest most cost-effective OpenAI model",
ModelId::CodexMiniLatest => "Latest Codex model optimized for code generation",
ModelId::ClaudeOpus41 => "Latest most capable Anthropic model with advanced reasoning",
ModelId::ClaudeSonnet4 => "Latest balanced Anthropic model for general tasks",
ModelId::OpenRouterGrokCodeFast1 => "Fast OpenRouter coding model powered by xAI Grok",
ModelId::OpenRouterQwen3Coder => {
"Qwen3-based OpenRouter model tuned for IDE-style coding workflows"
}
ModelId::OpenRouterDeepSeekChatV31 => "Advanced DeepSeek model via OpenRouter",
ModelId::OpenRouterOpenAIGPT5 => "OpenAI GPT-5 model accessed through OpenRouter",
ModelId::OpenRouterAnthropicClaudeSonnet4 => {
"Anthropic Claude Sonnet 4 model accessed through OpenRouter"
}
}
}
pub fn all_models() -> Vec<ModelId> {
vec![
ModelId::Gemini25FlashPreview,
ModelId::Gemini25Flash,
ModelId::Gemini25FlashLite,
ModelId::Gemini25Pro,
ModelId::GPT5,
ModelId::GPT5Mini,
ModelId::GPT5Nano,
ModelId::CodexMiniLatest,
ModelId::ClaudeOpus41,
ModelId::ClaudeSonnet4,
ModelId::OpenRouterGrokCodeFast1,
ModelId::OpenRouterQwen3Coder,
ModelId::OpenRouterDeepSeekChatV31,
ModelId::OpenRouterOpenAIGPT5,
ModelId::OpenRouterAnthropicClaudeSonnet4,
]
}
pub fn models_for_provider(provider: Provider) -> Vec<ModelId> {
Self::all_models()
.into_iter()
.filter(|model| model.provider() == provider)
.collect()
}
pub fn fallback_models() -> Vec<ModelId> {
vec![
ModelId::Gemini25FlashPreview,
ModelId::Gemini25Pro,
ModelId::GPT5,
ModelId::ClaudeOpus41,
ModelId::OpenRouterGrokCodeFast1,
]
}
pub fn default() -> Self {
ModelId::Gemini25FlashPreview
}
pub fn default_orchestrator() -> Self {
ModelId::Gemini25Pro
}
pub fn default_subagent() -> Self {
ModelId::Gemini25FlashPreview
}
pub fn default_orchestrator_for_provider(provider: Provider) -> Self {
match provider {
Provider::Gemini => ModelId::Gemini25Pro,
Provider::OpenAI => ModelId::GPT5,
Provider::Anthropic => ModelId::ClaudeOpus41,
Provider::OpenRouter => ModelId::OpenRouterGrokCodeFast1,
}
}
pub fn default_subagent_for_provider(provider: Provider) -> Self {
match provider {
Provider::Gemini => ModelId::Gemini25FlashPreview,
Provider::OpenAI => ModelId::GPT5Mini,
Provider::Anthropic => ModelId::ClaudeSonnet4,
Provider::OpenRouter => ModelId::OpenRouterGrokCodeFast1,
}
}
pub fn default_single_for_provider(provider: Provider) -> Self {
match provider {
Provider::Gemini => ModelId::Gemini25FlashPreview,
Provider::OpenAI => ModelId::GPT5,
Provider::Anthropic => ModelId::ClaudeOpus41,
Provider::OpenRouter => ModelId::OpenRouterGrokCodeFast1,
}
}
pub fn is_flash_variant(&self) -> bool {
matches!(
self,
ModelId::Gemini25FlashPreview | ModelId::Gemini25Flash | ModelId::Gemini25FlashLite
)
}
pub fn is_pro_variant(&self) -> bool {
matches!(
self,
ModelId::Gemini25Pro | ModelId::GPT5 | ModelId::ClaudeOpus41
)
}
pub fn is_efficient_variant(&self) -> bool {
matches!(
self,
ModelId::Gemini25FlashPreview
| ModelId::Gemini25Flash
| ModelId::Gemini25FlashLite
| ModelId::GPT5Mini
| ModelId::GPT5Nano
| ModelId::OpenRouterGrokCodeFast1
)
}
pub fn is_top_tier(&self) -> bool {
matches!(
self,
ModelId::Gemini25Pro
| ModelId::GPT5
| ModelId::ClaudeOpus41
| ModelId::ClaudeSonnet4
| ModelId::OpenRouterQwen3Coder
)
}
pub fn generation(&self) -> &'static str {
match self {
ModelId::Gemini25FlashPreview
| ModelId::Gemini25Flash
| ModelId::Gemini25FlashLite
| ModelId::Gemini25Pro => "2.5",
ModelId::GPT5 | ModelId::GPT5Mini | ModelId::GPT5Nano | ModelId::CodexMiniLatest => "5",
ModelId::ClaudeSonnet4 => "4",
ModelId::ClaudeOpus41 => "4.1",
ModelId::OpenRouterGrokCodeFast1 | ModelId::OpenRouterQwen3Coder => "marketplace",
ModelId::OpenRouterDeepSeekChatV31
| ModelId::OpenRouterOpenAIGPT5
| ModelId::OpenRouterAnthropicClaudeSonnet4 => "2025-08-07",
}
}
}
impl fmt::Display for ModelId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl FromStr for ModelId {
type Err = ModelParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use crate::config::constants::models;
match s {
s if s == models::GEMINI_2_5_FLASH_PREVIEW => Ok(ModelId::Gemini25FlashPreview),
s if s == models::GEMINI_2_5_FLASH => Ok(ModelId::Gemini25Flash),
s if s == models::GEMINI_2_5_FLASH_LITE => Ok(ModelId::Gemini25FlashLite),
s if s == models::GEMINI_2_5_PRO => Ok(ModelId::Gemini25Pro),
s if s == models::GPT_5 => Ok(ModelId::GPT5),
s if s == models::GPT_5_MINI => Ok(ModelId::GPT5Mini),
s if s == models::GPT_5_NANO => Ok(ModelId::GPT5Nano),
s if s == models::CODEX_MINI_LATEST => Ok(ModelId::CodexMiniLatest),
s if s == models::CLAUDE_OPUS_4_1_20250805 => Ok(ModelId::ClaudeOpus41),
s if s == models::CLAUDE_SONNET_4_20250514 => Ok(ModelId::ClaudeSonnet4),
s if s == models::OPENROUTER_X_AI_GROK_CODE_FAST_1 => {
Ok(ModelId::OpenRouterGrokCodeFast1)
}
s if s == models::OPENROUTER_QWEN3_CODER => Ok(ModelId::OpenRouterQwen3Coder),
s if s == models::OPENROUTER_DEEPSEEK_CHAT_V3_1 => Ok(ModelId::OpenRouterDeepSeekChatV31),
s if s == models::OPENROUTER_OPENAI_GPT_5 => Ok(ModelId::OpenRouterOpenAIGPT5),
s if s == models::OPENROUTER_ANTHROPIC_CLAUDE_SONNET_4 => {
Ok(ModelId::OpenRouterAnthropicClaudeSonnet4)
}
_ => Err(ModelParseError::InvalidModel(s.to_string())),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ModelParseError {
InvalidModel(String),
InvalidProvider(String),
}
impl fmt::Display for ModelParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ModelParseError::InvalidModel(model) => {
write!(
f,
"Invalid model identifier: '{}'. Supported models: {}",
model,
ModelId::all_models()
.iter()
.map(|m| m.as_str())
.collect::<Vec<_>>()
.join(", ")
)
}
ModelParseError::InvalidProvider(provider) => {
write!(
f,
"Invalid provider: '{}'. Supported providers: {}",
provider,
Provider::all_providers()
.iter()
.map(|p| p.to_string())
.collect::<Vec<_>>()
.join(", ")
)
}
}
}
}
impl std::error::Error for ModelParseError {}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::constants::models;
#[test]
fn test_model_string_conversion() {
assert_eq!(
ModelId::Gemini25FlashPreview.as_str(),
models::GEMINI_2_5_FLASH_PREVIEW
);
assert_eq!(ModelId::Gemini25Flash.as_str(), models::GEMINI_2_5_FLASH);
assert_eq!(
ModelId::Gemini25FlashLite.as_str(),
models::GEMINI_2_5_FLASH_LITE
);
assert_eq!(ModelId::Gemini25Pro.as_str(), models::GEMINI_2_5_PRO);
assert_eq!(ModelId::GPT5.as_str(), models::GPT_5);
assert_eq!(ModelId::GPT5Mini.as_str(), models::GPT_5_MINI);
assert_eq!(ModelId::GPT5Nano.as_str(), models::GPT_5_NANO);
assert_eq!(ModelId::CodexMiniLatest.as_str(), models::CODEX_MINI_LATEST);
assert_eq!(
ModelId::ClaudeSonnet4.as_str(),
models::CLAUDE_SONNET_4_20250514
);
assert_eq!(
ModelId::ClaudeOpus41.as_str(),
models::CLAUDE_OPUS_4_1_20250805
);
assert_eq!(
ModelId::OpenRouterGrokCodeFast1.as_str(),
models::OPENROUTER_X_AI_GROK_CODE_FAST_1
);
assert_eq!(
ModelId::OpenRouterQwen3Coder.as_str(),
models::OPENROUTER_QWEN3_CODER
);
assert_eq!(
ModelId::OpenRouterDeepSeekChatV31.as_str(),
models::OPENROUTER_DEEPSEEK_CHAT_V3_1
);
assert_eq!(
ModelId::OpenRouterOpenAIGPT5.as_str(),
models::OPENROUTER_OPENAI_GPT_5
);
assert_eq!(
ModelId::OpenRouterAnthropicClaudeSonnet4.as_str(),
models::OPENROUTER_ANTHROPIC_CLAUDE_SONNET_4
);
}
#[test]
fn test_model_from_string() {
assert_eq!(
models::GEMINI_2_5_FLASH_PREVIEW.parse::<ModelId>().unwrap(),
ModelId::Gemini25FlashPreview
);
assert_eq!(
models::GEMINI_2_5_FLASH.parse::<ModelId>().unwrap(),
ModelId::Gemini25Flash
);
assert_eq!(
models::GEMINI_2_5_FLASH_LITE.parse::<ModelId>().unwrap(),
ModelId::Gemini25FlashLite
);
assert_eq!(
models::GEMINI_2_5_PRO.parse::<ModelId>().unwrap(),
ModelId::Gemini25Pro
);
assert_eq!(models::GPT_5.parse::<ModelId>().unwrap(), ModelId::GPT5);
assert_eq!(
models::GPT_5_MINI.parse::<ModelId>().unwrap(),
ModelId::GPT5Mini
);
assert_eq!(
models::GPT_5_NANO.parse::<ModelId>().unwrap(),
ModelId::GPT5Nano
);
assert_eq!(
models::CODEX_MINI_LATEST.parse::<ModelId>().unwrap(),
ModelId::CodexMiniLatest
);
assert_eq!(
models::CLAUDE_SONNET_4_20250514.parse::<ModelId>().unwrap(),
ModelId::ClaudeSonnet4
);
assert_eq!(
models::OPENROUTER_X_AI_GROK_CODE_FAST_1
.parse::<ModelId>()
.unwrap(),
ModelId::OpenRouterGrokCodeFast1
);
assert_eq!(
models::OPENROUTER_QWEN3_CODER.parse::<ModelId>().unwrap(),
ModelId::OpenRouterQwen3Coder
);
assert_eq!(
models::OPENROUTER_DEEPSEEK_CHAT_V3_1.parse::<ModelId>().unwrap(),
ModelId::OpenRouterDeepSeekChatV31
);
assert_eq!(
models::OPENROUTER_OPENAI_GPT_5.parse::<ModelId>().unwrap(),
ModelId::OpenRouterOpenAIGPT5
);
assert_eq!(
models::OPENROUTER_ANTHROPIC_CLAUDE_SONNET_4
.parse::<ModelId>()
.unwrap(),
ModelId::OpenRouterAnthropicClaudeSonnet4
);
assert!("invalid-model".parse::<ModelId>().is_err());
}
#[test]
fn test_provider_parsing() {
assert_eq!("gemini".parse::<Provider>().unwrap(), Provider::Gemini);
assert_eq!("openai".parse::<Provider>().unwrap(), Provider::OpenAI);
assert_eq!(
"anthropic".parse::<Provider>().unwrap(),
Provider::Anthropic
);
assert_eq!(
"openrouter".parse::<Provider>().unwrap(),
Provider::OpenRouter
);
assert!("invalid-provider".parse::<Provider>().is_err());
}
#[test]
fn test_model_providers() {
assert_eq!(ModelId::Gemini25FlashPreview.provider(), Provider::Gemini);
assert_eq!(ModelId::GPT5.provider(), Provider::OpenAI);
assert_eq!(ModelId::ClaudeSonnet4.provider(), Provider::Anthropic);
assert_eq!(
ModelId::OpenRouterGrokCodeFast1.provider(),
Provider::OpenRouter
);
}
#[test]
fn test_provider_defaults() {
assert_eq!(
ModelId::default_orchestrator_for_provider(Provider::Gemini),
ModelId::Gemini25Pro
);
assert_eq!(
ModelId::default_orchestrator_for_provider(Provider::OpenAI),
ModelId::GPT5
);
assert_eq!(
ModelId::default_orchestrator_for_provider(Provider::Anthropic),
ModelId::ClaudeSonnet4
);
assert_eq!(
ModelId::default_orchestrator_for_provider(Provider::OpenRouter),
ModelId::OpenRouterGrokCodeFast1
);
assert_eq!(
ModelId::default_subagent_for_provider(Provider::Gemini),
ModelId::Gemini25FlashPreview
);
assert_eq!(
ModelId::default_subagent_for_provider(Provider::OpenAI),
ModelId::GPT5Mini
);
assert_eq!(
ModelId::default_subagent_for_provider(Provider::Anthropic),
ModelId::ClaudeSonnet4
);
assert_eq!(
ModelId::default_subagent_for_provider(Provider::OpenRouter),
ModelId::OpenRouterGrokCodeFast1
);
}
#[test]
fn test_model_defaults() {
assert_eq!(ModelId::default(), ModelId::Gemini25FlashPreview);
assert_eq!(ModelId::default_orchestrator(), ModelId::Gemini25Pro);
assert_eq!(ModelId::default_subagent(), ModelId::Gemini25FlashPreview);
}
#[test]
fn test_model_variants() {
assert!(ModelId::Gemini25FlashPreview.is_flash_variant());
assert!(ModelId::Gemini25Flash.is_flash_variant());
assert!(ModelId::Gemini25FlashLite.is_flash_variant());
assert!(!ModelId::GPT5.is_flash_variant());
assert!(ModelId::Gemini25Pro.is_pro_variant());
assert!(ModelId::GPT5.is_pro_variant());
assert!(!ModelId::Gemini25FlashPreview.is_pro_variant());
assert!(ModelId::Gemini25FlashPreview.is_efficient_variant());
assert!(ModelId::Gemini25Flash.is_efficient_variant());
assert!(ModelId::Gemini25FlashLite.is_efficient_variant());
assert!(ModelId::GPT5Mini.is_efficient_variant());
assert!(ModelId::OpenRouterGrokCodeFast1.is_efficient_variant());
assert!(!ModelId::GPT5.is_efficient_variant());
assert!(ModelId::Gemini25Pro.is_top_tier());
assert!(ModelId::GPT5.is_top_tier());
assert!(ModelId::ClaudeSonnet4.is_top_tier());
assert!(ModelId::OpenRouterQwen3Coder.is_top_tier());
assert!(!ModelId::Gemini25FlashPreview.is_top_tier());
}
#[test]
fn test_model_generation() {
assert_eq!(ModelId::Gemini25FlashPreview.generation(), "2.5");
assert_eq!(ModelId::Gemini25Flash.generation(), "2.5");
assert_eq!(ModelId::Gemini25FlashLite.generation(), "2.5");
assert_eq!(ModelId::Gemini25Pro.generation(), "2.5");
assert_eq!(ModelId::GPT5.generation(), "5");
assert_eq!(ModelId::GPT5Mini.generation(), "5");
assert_eq!(ModelId::GPT5Nano.generation(), "5");
assert_eq!(ModelId::CodexMiniLatest.generation(), "5");
assert_eq!(ModelId::ClaudeSonnet4.generation(), "4");
assert_eq!(ModelId::ClaudeOpus41.generation(), "4.1");
assert_eq!(ModelId::OpenRouterGrokCodeFast1.generation(), "marketplace");
assert_eq!(ModelId::OpenRouterQwen3Coder.generation(), "marketplace");
assert_eq!(ModelId::OpenRouterDeepSeekChatV31.generation(), "2025-08-07");
assert_eq!(ModelId::OpenRouterOpenAIGPT5.generation(), "2025-08-07");
assert_eq!(ModelId::OpenRouterAnthropicClaudeSonnet4.generation(), "2025-08-07");
}
#[test]
fn test_models_for_provider() {
let gemini_models = ModelId::models_for_provider(Provider::Gemini);
assert!(gemini_models.contains(&ModelId::Gemini25Pro));
assert!(!gemini_models.contains(&ModelId::GPT5));
let openai_models = ModelId::models_for_provider(Provider::OpenAI);
assert!(openai_models.contains(&ModelId::GPT5));
assert!(!openai_models.contains(&ModelId::Gemini25Pro));
let anthropic_models = ModelId::models_for_provider(Provider::Anthropic);
assert!(anthropic_models.contains(&ModelId::ClaudeSonnet4));
assert!(!anthropic_models.contains(&ModelId::GPT5));
let openrouter_models = ModelId::models_for_provider(Provider::OpenRouter);
assert!(openrouter_models.contains(&ModelId::OpenRouterGrokCodeFast1));
assert!(openrouter_models.contains(&ModelId::OpenRouterQwen3Coder));
assert!(openrouter_models.contains(&ModelId::OpenRouterDeepSeekChatV31));
assert!(openrouter_models.contains(&ModelId::OpenRouterOpenAIGPT5));
assert!(openrouter_models.contains(&ModelId::OpenRouterAnthropicClaudeSonnet4));
}
#[test]
fn test_fallback_models() {
let fallbacks = ModelId::fallback_models();
assert!(!fallbacks.is_empty());
assert!(fallbacks.contains(&ModelId::Gemini25Pro));
assert!(fallbacks.contains(&ModelId::GPT5));
assert!(fallbacks.contains(&ModelId::OpenRouterGrokCodeFast1));
}
}