use crate::models::Provider;
use super::ModelId;
#[cfg(not(docsrs))]
#[allow(dead_code)]
mod capability_generated {
include!(concat!(env!("OUT_DIR"), "/model_capabilities.rs"));
}
#[cfg(docsrs)]
#[allow(dead_code)]
mod capability_generated {
#[derive(Clone, Copy)]
pub struct Pricing {
pub input: Option<f64>,
pub output: Option<f64>,
pub cache_read: Option<f64>,
pub cache_write: Option<f64>,
}
#[derive(Clone, Copy)]
pub struct Entry {
pub provider: &'static str,
pub id: &'static str,
pub display_name: &'static str,
pub description: &'static str,
pub context_window: usize,
pub max_output_tokens: Option<usize>,
pub reasoning: bool,
pub tool_call: bool,
pub vision: bool,
pub input_modalities: &'static [&'static str],
pub caching: bool,
pub structured_output: bool,
pub pricing: Pricing,
}
pub const ENTRIES: &[Entry] = &[];
pub const PROVIDERS: &[&str] = &[];
pub fn metadata_for(_provider: &str, _id: &str) -> Option<Entry> {
None
}
pub fn models_for_provider(_provider: &str) -> Option<&'static [&'static str]> {
None
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ModelPricing {
pub input: Option<f64>,
pub output: Option<f64>,
pub cache_read: Option<f64>,
pub cache_write: Option<f64>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ModelCatalogEntry {
pub provider: &'static str,
pub id: &'static str,
pub display_name: &'static str,
pub description: &'static str,
pub context_window: usize,
pub max_output_tokens: Option<usize>,
pub reasoning: bool,
pub tool_call: bool,
pub vision: bool,
pub input_modalities: &'static [&'static str],
pub caching: bool,
pub structured_output: bool,
pub pricing: ModelPricing,
}
fn catalog_provider_key(provider: &str) -> &str {
if provider.eq_ignore_ascii_case("google") || provider.eq_ignore_ascii_case("gemini") {
"gemini"
} else if provider.eq_ignore_ascii_case("openai") {
"openai"
} else if provider.eq_ignore_ascii_case("anthropic") {
"anthropic"
} else if provider.eq_ignore_ascii_case("deepseek") {
"deepseek"
} else if provider.eq_ignore_ascii_case("openrouter") {
"openrouter"
} else if provider.eq_ignore_ascii_case("ollama") {
"ollama"
} else if provider.eq_ignore_ascii_case("lmstudio") {
"lmstudio"
} else if provider.eq_ignore_ascii_case("moonshot") {
"moonshot"
} else if provider.eq_ignore_ascii_case("zai") {
"zai"
} else if provider.eq_ignore_ascii_case("minimax") {
"minimax"
} else if provider.eq_ignore_ascii_case("huggingface") {
"huggingface"
} else {
provider
}
}
fn capability_provider_key(provider: Provider) -> &'static str {
match provider {
Provider::Gemini => "gemini",
Provider::OpenAI => "openai",
Provider::Anthropic => "anthropic",
Provider::Copilot => "copilot",
Provider::DeepSeek => "deepseek",
Provider::OpenRouter => "openrouter",
Provider::Ollama => "ollama",
Provider::LmStudio => "lmstudio",
Provider::Moonshot => "moonshot",
Provider::ZAI => "zai",
Provider::Minimax => "minimax",
Provider::HuggingFace => "huggingface",
}
}
fn generated_catalog_entry(provider: &str, id: &str) -> Option<ModelCatalogEntry> {
capability_generated::metadata_for(catalog_provider_key(provider), id).map(|entry| {
ModelCatalogEntry {
provider: entry.provider,
id: entry.id,
display_name: entry.display_name,
description: entry.description,
context_window: entry.context_window,
max_output_tokens: entry.max_output_tokens,
reasoning: entry.reasoning,
tool_call: entry.tool_call,
vision: entry.vision,
input_modalities: entry.input_modalities,
caching: entry.caching,
structured_output: entry.structured_output,
pricing: ModelPricing {
input: entry.pricing.input,
output: entry.pricing.output,
cache_read: entry.pricing.cache_read,
cache_write: entry.pricing.cache_write,
},
}
})
}
pub fn model_catalog_entry(provider: &str, id: &str) -> Option<ModelCatalogEntry> {
generated_catalog_entry(provider, id)
}
pub fn supported_models_for_provider(provider: &str) -> Option<&'static [&'static str]> {
capability_generated::models_for_provider(catalog_provider_key(provider))
}
pub fn catalog_provider_keys() -> &'static [&'static str] {
capability_generated::PROVIDERS
}
impl ModelId {
fn generated_capabilities(&self) -> Option<ModelCatalogEntry> {
generated_catalog_entry(capability_provider_key(self.provider()), self.as_str())
}
pub fn preferred_lightweight_variant(&self) -> Option<Self> {
match self {
ModelId::Gemini31ProPreview | ModelId::Gemini31ProPreviewCustomTools => {
Some(ModelId::Gemini31FlashLitePreview)
}
ModelId::GPT54 | ModelId::GPT54Pro => Some(ModelId::GPT54Mini),
ModelId::GPT52
| ModelId::GPT52Codex
| ModelId::GPT53Codex
| ModelId::GPT51Codex
| ModelId::GPT51CodexMax
| ModelId::GPT5
| ModelId::GPT5Codex => Some(ModelId::GPT5Mini),
ModelId::ClaudeOpus46 | ModelId::ClaudeSonnet46 => Some(ModelId::ClaudeHaiku45),
ModelId::CopilotGPT54 => Some(ModelId::CopilotGPT54Mini),
ModelId::CopilotGPT52Codex | ModelId::CopilotGPT51CodexMax => {
Some(ModelId::CopilotGPT54Mini)
}
ModelId::DeepSeekReasoner => Some(ModelId::DeepSeekChat),
ModelId::ZaiGlm51 => Some(ModelId::ZaiGlm5),
ModelId::MinimaxM27 => Some(ModelId::MinimaxM25),
_ => None,
}
}
pub fn non_reasoning_variant(&self) -> Option<Self> {
if let Some(meta) = self.openrouter_metadata() {
if !meta.reasoning {
return None;
}
let vendor = meta.vendor;
let mut candidates: Vec<Self> = Self::openrouter_vendor_groups()
.into_iter()
.find(|(candidate_vendor, _)| *candidate_vendor == vendor)
.map(|(_, models)| {
models
.iter()
.copied()
.filter(|candidate| candidate != self)
.filter(|candidate| {
candidate
.openrouter_metadata()
.map(|other| !other.reasoning)
.unwrap_or(false)
})
.collect()
})
.unwrap_or_default();
if candidates.is_empty() {
return None;
}
candidates.sort_by_key(|candidate| {
candidate
.openrouter_metadata()
.map(|data| (!data.efficient, data.display))
.unwrap_or((true, ""))
});
return candidates.into_iter().next();
}
let direct = match self {
ModelId::Gemini31ProPreview
| ModelId::Gemini31ProPreviewCustomTools
| ModelId::Gemini31FlashLitePreview => Some(ModelId::Gemini3FlashPreview),
ModelId::GPT52
| ModelId::GPT54
| ModelId::GPT54Pro
| ModelId::GPT54Nano
| ModelId::GPT54Mini
| ModelId::GPT5 => Some(ModelId::GPT5Mini),
ModelId::CopilotGPT52Codex | ModelId::CopilotGPT54 => Some(ModelId::CopilotGPT54Mini),
ModelId::DeepSeekReasoner => Some(ModelId::DeepSeekChat),
ModelId::ZaiGlm5 | ModelId::ZaiGlm51 => Some(ModelId::OllamaGlm5Cloud),
ModelId::ClaudeOpus46 | ModelId::ClaudeSonnet46 => Some(ModelId::ClaudeSonnet46),
ModelId::MinimaxM27 | ModelId::MinimaxM25 => None,
_ => None,
};
direct.and_then(|candidate| {
if candidate.supports_reasoning_effort() {
None
} else {
Some(candidate)
}
})
}
pub fn is_flash_variant(&self) -> bool {
matches!(
self,
ModelId::Gemini3FlashPreview
| ModelId::Gemini31FlashLitePreview
| ModelId::OpenRouterStepfunStep35FlashFree
| ModelId::OpenRouterNvidiaNemotron3Super120bA12bFree
| ModelId::OllamaGemini3FlashPreviewCloud
| ModelId::HuggingFaceStep35Flash
)
}
pub fn is_pro_variant(&self) -> bool {
matches!(
self,
ModelId::Gemini31ProPreview
| ModelId::Gemini31ProPreviewCustomTools
| ModelId::OpenRouterGoogleGemini31ProPreview
| ModelId::GPT5
| ModelId::GPT52
| ModelId::GPT52Codex
| ModelId::GPT54
| ModelId::GPT54Pro
| ModelId::GPT53Codex
| ModelId::GPT51Codex
| ModelId::GPT51CodexMax
| ModelId::CopilotGPT52Codex
| ModelId::CopilotGPT51CodexMax
| ModelId::CopilotGPT54
| ModelId::CopilotClaudeSonnet46
| ModelId::GPT5Codex
| ModelId::ClaudeOpus46
| ModelId::ClaudeSonnet46
| ModelId::DeepSeekReasoner
| ModelId::ZaiGlm5
| ModelId::ZaiGlm51
| ModelId::OpenRouterStepfunStep35FlashFree
| ModelId::OpenRouterNvidiaNemotron3Super120bA12bFree
| ModelId::MinimaxM27
| ModelId::MinimaxM25
| ModelId::OllamaGlm5Cloud
| ModelId::OllamaGlm51Cloud
| ModelId::OllamaNemotron3SuperCloud
| ModelId::OllamaMinimaxM25Cloud
| ModelId::HuggingFaceQwen3CoderNextNovita
| ModelId::HuggingFaceQwen35397BA17BTogether
)
}
pub fn is_efficient_variant(&self) -> bool {
if let Some(meta) = self.openrouter_metadata() {
return meta.efficient;
}
matches!(
self,
ModelId::Gemini3FlashPreview
| ModelId::Gemini31FlashLitePreview
| ModelId::GPT5Mini
| ModelId::GPT5Nano
| ModelId::CopilotGPT54Mini
| ModelId::ClaudeHaiku45
| ModelId::DeepSeekChat
| ModelId::HuggingFaceStep35Flash
)
}
pub fn is_top_tier(&self) -> bool {
if let Some(meta) = self.openrouter_metadata() {
return meta.top_tier;
}
matches!(
self,
ModelId::Gemini31ProPreview
| ModelId::Gemini31ProPreviewCustomTools
| ModelId::OpenRouterGoogleGemini31ProPreview
| ModelId::Gemini3FlashPreview
| ModelId::Gemini31FlashLitePreview
| ModelId::GPT5
| ModelId::GPT52
| ModelId::GPT52Codex
| ModelId::GPT54
| ModelId::GPT54Pro
| ModelId::GPT53Codex
| ModelId::GPT51Codex
| ModelId::GPT51CodexMax
| ModelId::GPT5Codex
| ModelId::ClaudeOpus46
| ModelId::ClaudeSonnet46
| ModelId::DeepSeekReasoner
| ModelId::ZaiGlm5
| ModelId::ZaiGlm51
| ModelId::OpenRouterStepfunStep35FlashFree
| ModelId::HuggingFaceQwen3CoderNextNovita
| ModelId::HuggingFaceQwen35397BA17BTogether
)
}
pub fn is_reasoning_variant(&self) -> bool {
if let Some(meta) = self.openrouter_metadata() {
return meta.reasoning;
}
self.provider().supports_reasoning_effort(self.as_str())
}
pub fn supports_tool_calls(&self) -> bool {
if let Some(meta) = self.generated_capabilities() {
return meta.tool_call;
}
if let Some(meta) = self.openrouter_metadata() {
return meta.tool_call;
}
true
}
pub fn input_modalities(&self) -> &'static [&'static str] {
self.generated_capabilities()
.map(|meta| meta.input_modalities)
.unwrap_or(&[])
}
pub fn generation(&self) -> &'static str {
if let Some(meta) = self.openrouter_metadata() {
return meta.generation;
}
match self {
ModelId::Gemini31ProPreview | ModelId::Gemini31ProPreviewCustomTools => "3.1",
ModelId::Gemini31FlashLitePreview => "3.1-lite",
ModelId::Gemini3FlashPreview => "3",
ModelId::GPT52 | ModelId::GPT52Codex => "5.2",
ModelId::GPT54 | ModelId::GPT54Pro | ModelId::GPT54Nano | ModelId::GPT54Mini => "5.4",
ModelId::GPT53Codex => "5.3",
ModelId::GPT51Codex | ModelId::GPT51CodexMax => "5.1",
ModelId::GPT5
| ModelId::GPT5Codex
| ModelId::GPT5Mini
| ModelId::GPT5Nano
| ModelId::OpenAIGptOss20b
| ModelId::OpenAIGptOss120b => "5",
ModelId::ClaudeOpus46 | ModelId::ClaudeSonnet46 => "4.6",
ModelId::ClaudeHaiku45 => "4.5",
ModelId::DeepSeekChat | ModelId::DeepSeekReasoner => "V3.2-Exp",
ModelId::ZaiGlm5 => "5",
ModelId::ZaiGlm51 => "5.1",
ModelId::OllamaGptOss20b => "oss",
ModelId::OllamaGptOss20bCloud => "oss-cloud",
ModelId::OllamaGptOss120bCloud => "oss-cloud",
ModelId::OllamaQwen317b => "oss",
ModelId::OllamaQwen3CoderNext => "qwen3-coder-next:cloud",
ModelId::OllamaDeepseekV32Cloud => "deepseek-v3.2",
ModelId::OllamaQwen3Next80bCloud => "qwen3-next",
ModelId::OllamaMinimaxM2Cloud => "minimax-m2",
ModelId::OllamaMinimaxM27Cloud => "minimax-m2.7",
ModelId::OllamaGlm5Cloud => "glm-5",
ModelId::OllamaGlm51Cloud => "glm-5.1",
ModelId::OllamaMinimaxM25Cloud => "minimax-m2.5",
ModelId::OllamaNemotron3SuperCloud => "nemotron-3",
ModelId::OllamaGemini3FlashPreviewCloud => "gemini-3",
ModelId::MinimaxM27 => "M2.7",
ModelId::MinimaxM25 => "M2.5",
ModelId::MoonshotKimiK25 => "k2.5",
ModelId::HuggingFaceDeepseekV32 => "V3.2-Exp",
ModelId::HuggingFaceOpenAIGptOss20b => "oss",
ModelId::HuggingFaceOpenAIGptOss120b => "oss",
ModelId::HuggingFaceMinimaxM25Novita => "m2.5",
ModelId::HuggingFaceDeepseekV32Novita => "v3.2",
ModelId::HuggingFaceXiaomiMimoV2FlashNovita => "v2-flash",
ModelId::HuggingFaceGlm5Novita => "5",
ModelId::HuggingFaceGlm51ZaiOrg => "5.1",
ModelId::HuggingFaceStep35Flash => "3.5",
ModelId::HuggingFaceQwen3CoderNextNovita | ModelId::OpenRouterQwen3CoderNext => {
"qwen3-coder-next"
}
_ => "unknown",
}
}
pub fn supports_shell_tool(&self) -> bool {
matches!(
self,
ModelId::GPT52
| ModelId::GPT52Codex
| ModelId::GPT54
| ModelId::GPT54Pro
| ModelId::GPT53Codex
| ModelId::GPT51Codex
| ModelId::GPT51CodexMax
| ModelId::GPT5Codex
)
}
pub fn supports_apply_patch_tool(&self) -> bool {
false }
}