use serde::{Deserialize, Serialize};
use crate::config::models::Provider;
use crate::config::types::ReasoningEffortLevel;
mod presets;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReasoningEffortPreset {
pub effort: ReasoningEffortLevel,
pub description: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModelUpgrade {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_effort_mapping: Option<String>,
pub migration_config_key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_link: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub upgrade_copy: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModelInfo {
pub slug: String,
pub display_name: String,
pub description: String,
pub provider: Provider,
#[serde(default)]
pub default_reasoning_level: ReasoningEffortLevel,
#[serde(default)]
pub supported_reasoning_levels: Vec<ReasoningEffortPreset>,
#[serde(default)]
pub context_window: Option<i64>,
#[serde(default = "default_true")]
pub supports_tool_use: bool,
#[serde(default = "default_true")]
pub supports_streaming: bool,
#[serde(default)]
pub supports_reasoning: bool,
#[serde(default)]
pub priority: i32,
#[serde(default = "default_visibility")]
pub visibility: String,
#[serde(default = "default_true")]
pub supported_in_api: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub upgrade: Option<ModelUpgrade>,
}
fn default_true() -> bool {
true
}
fn default_visibility() -> String {
"list".to_string()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModelPreset {
pub id: String,
pub model: String,
pub display_name: String,
pub description: String,
pub provider: Provider,
pub default_reasoning_effort: ReasoningEffortLevel,
pub supported_reasoning_efforts: Vec<ReasoningEffortPreset>,
#[serde(default)]
pub is_default: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub upgrade: Option<ModelUpgrade>,
#[serde(default = "default_true")]
pub show_in_picker: bool,
#[serde(default = "default_true")]
pub supported_in_api: bool,
#[serde(default)]
pub context_window: Option<i64>,
}
impl From<ModelInfo> for ModelPreset {
fn from(info: ModelInfo) -> Self {
Self {
id: info.slug.clone(),
model: info.slug,
display_name: info.display_name,
description: info.description,
provider: info.provider,
default_reasoning_effort: info.default_reasoning_level,
supported_reasoning_efforts: info.supported_reasoning_levels,
is_default: false,
upgrade: info.upgrade,
show_in_picker: info.visibility == "list",
supported_in_api: info.supported_in_api,
context_window: info.context_window,
}
}
}
pub fn builtin_model_presets() -> Vec<ModelPreset> {
let mut presets = Vec::new();
presets.extend(presets::gemini_presets());
presets.extend(presets::openai_presets());
presets.extend(presets::anthropic_presets());
presets.extend(presets::copilot_presets());
presets.extend(presets::deepseek_presets());
presets.extend(presets::zai_presets());
presets.extend(presets::lmstudio_presets());
presets.extend(presets::llamacpp_presets());
presets.extend(presets::minimax_presets());
presets.extend(presets::opencode_zen_presets());
presets.extend(presets::opencode_go_presets());
presets.extend(presets::poolside_presets());
presets.extend(presets::stepfun_presets());
presets.extend(presets::evolink_presets());
presets
}
pub fn presets_for_provider(provider: Provider) -> Vec<ModelPreset> {
match provider {
Provider::Gemini => presets::gemini_presets(),
Provider::OpenAI => presets::openai_presets(),
Provider::Anthropic => presets::anthropic_presets(),
Provider::Copilot => presets::copilot_presets(),
Provider::DeepSeek => presets::deepseek_presets(),
Provider::ZAI => presets::zai_presets(),
Provider::Minimax => presets::minimax_presets(),
Provider::OpenRouter => presets::openrouter_presets(),
Provider::Ollama => presets::ollama_presets(),
Provider::LmStudio => presets::lmstudio_presets(),
Provider::LlamaCpp => presets::llamacpp_presets(),
Provider::Moonshot => presets::moonshot_presets(),
Provider::Mistral => presets::mistral_presets(),
Provider::HuggingFace => presets::huggingface_presets(),
Provider::OpenCodeZen => presets::opencode_zen_presets(),
Provider::OpenCodeGo => presets::opencode_go_presets(),
Provider::MiMo => presets::mimo_presets(),
Provider::Qwen => presets::qwen_presets(),
Provider::StepFun => presets::stepfun_presets(),
Provider::Evolink => presets::evolink_presets(),
Provider::Poolside => presets::poolside_presets(),
}
}
pub fn all_model_presets() -> Vec<ModelPreset> {
builtin_model_presets()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_one_default_per_provider() {
let presets = builtin_model_presets();
let providers: Vec<Provider> = Provider::all_providers();
for provider in providers {
let default_count = presets.iter().filter(|p| p.provider == provider && p.is_default).count();
assert!(default_count <= 1, "Provider {provider:?} has {default_count} defaults, expected 0 or 1");
}
}
#[test]
fn gemini_presets_exist() {
let presets = presets::gemini_presets();
assert!(!presets.is_empty());
assert!(presets.iter().any(|p| p.id.contains("gemini")));
}
#[test]
fn model_info_converts_to_preset() {
let info = ModelInfo {
slug: "test-model".to_string(),
display_name: "Test Model".to_string(),
description: "A test model".to_string(),
provider: Provider::Gemini,
default_reasoning_level: ReasoningEffortLevel::Medium,
supported_reasoning_levels: vec![],
context_window: Some(128_000),
supports_tool_use: true,
supports_streaming: true,
supports_reasoning: false,
priority: 0,
visibility: "list".to_string(),
supported_in_api: true,
upgrade: None,
};
let preset: ModelPreset = info.into();
assert_eq!(preset.id, "test-model");
assert_eq!(preset.model, "test-model");
assert!(preset.show_in_picker);
}
#[test]
fn openai_codex_presets_default_to_high_reasoning() {
let codex = presets::openai_presets()
.into_iter()
.find(|preset| preset.id == "gpt-5.3-codex")
.expect("gpt-5.3-codex preset");
assert_eq!(codex.default_reasoning_effort, ReasoningEffortLevel::High);
}
#[test]
fn moonshot_presets_exist_and_default_to_kimi_k3() {
let presets = presets::moonshot_presets();
assert_eq!(presets.len(), 3);
let default = presets
.iter()
.find(|preset| preset.is_default)
.expect("moonshot default preset");
assert_eq!(default.id, "kimi-k3");
assert_eq!(default.provider, Provider::Moonshot);
}
}