Skip to main content

vtcode_core/models_manager/model_presets/
mod.rs

1//! Model presets and built-in model configurations.
2//!
3//! This module provides pre-configured model presets for all supported providers,
4//! following the pattern from OpenAI Codex's models_manager.
5//!
6//! Per-provider preset definitions live in the `presets` subdirectory; this
7//! module owns the shared types and the two public aggregators
8//! ([`builtin_model_presets`] and [`presets_for_provider`]).
9
10use serde::{Deserialize, Serialize};
11
12use crate::config::models::Provider;
13use crate::config::types::ReasoningEffortLevel;
14
15mod presets;
16
17/// Reasoning effort preset with description
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct ReasoningEffortPreset {
20    /// The effort level
21    pub effort: ReasoningEffortLevel,
22    /// Human-readable description
23    pub description: String,
24}
25
26/// Model upgrade information
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct ModelUpgrade {
29    /// Target model ID to upgrade to
30    pub id: String,
31    /// Optional reasoning effort mapping
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub reasoning_effort_mapping: Option<String>,
34    /// Configuration key for migration
35    pub migration_config_key: String,
36    /// Link to model documentation
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub model_link: Option<String>,
39    /// Upgrade notification copy
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub upgrade_copy: Option<String>,
42}
43
44/// Remote model information received from provider APIs
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct ModelInfo {
47    /// Unique model identifier/slug
48    pub slug: String,
49    /// Display name for UI
50    pub display_name: String,
51    /// Model description
52    pub description: String,
53    /// Provider this model belongs to
54    pub provider: Provider,
55    /// Default reasoning level
56    #[serde(default)]
57    pub default_reasoning_level: ReasoningEffortLevel,
58    /// Supported reasoning levels
59    #[serde(default)]
60    pub supported_reasoning_levels: Vec<ReasoningEffortPreset>,
61    /// Context window size
62    #[serde(default)]
63    pub context_window: Option<i64>,
64    /// Whether this model supports tool use
65    #[serde(default = "default_true")]
66    pub supports_tool_use: bool,
67    /// Whether this model supports streaming
68    #[serde(default = "default_true")]
69    pub supports_streaming: bool,
70    /// Whether this model accepts image inputs
71    #[serde(default)]
72    pub supports_vision: bool,
73    /// Whether this model supports schema-constrained structured output
74    #[serde(default)]
75    pub supports_structured_output: bool,
76    /// Whether this model supports reasoning/thinking
77    #[serde(default)]
78    pub supports_reasoning: bool,
79    /// Maximum generated output tokens, when published by the provider
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub max_output_tokens: Option<i64>,
82    /// Priority for sorting (lower = higher priority)
83    #[serde(default)]
84    pub priority: i32,
85    /// Visibility in picker
86    #[serde(default = "default_visibility")]
87    pub visibility: String,
88    /// Whether supported in API mode
89    #[serde(default = "default_true")]
90    pub supported_in_api: bool,
91    /// Upgrade path if available
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    pub upgrade: Option<ModelUpgrade>,
94}
95
96fn default_true() -> bool {
97    true
98}
99
100fn default_visibility() -> String {
101    "list".to_string()
102}
103
104/// A preset configuration for a model shown in the picker
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
106pub struct ModelPreset {
107    /// Unique identifier for the preset
108    pub id: String,
109    /// Actual model slug to use in API calls
110    pub model: String,
111    /// Display name for UI
112    pub display_name: String,
113    /// Model description
114    pub description: String,
115    /// Provider
116    pub provider: Provider,
117    /// Default reasoning effort
118    pub default_reasoning_effort: ReasoningEffortLevel,
119    /// Supported reasoning efforts
120    pub supported_reasoning_efforts: Vec<ReasoningEffortPreset>,
121    /// Whether this is the default model
122    #[serde(default)]
123    pub is_default: bool,
124    /// Upgrade path
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub upgrade: Option<ModelUpgrade>,
127    /// Whether to show in picker
128    #[serde(default = "default_true")]
129    pub show_in_picker: bool,
130    /// Whether supported in API mode
131    #[serde(default = "default_true")]
132    pub supported_in_api: bool,
133    /// Context window size
134    #[serde(default)]
135    pub context_window: Option<i64>,
136}
137
138impl From<ModelInfo> for ModelPreset {
139    fn from(info: ModelInfo) -> Self {
140        Self {
141            id: info.slug.clone(),
142            model: info.slug,
143            display_name: info.display_name,
144            description: info.description,
145            provider: info.provider,
146            default_reasoning_effort: info.default_reasoning_level,
147            supported_reasoning_efforts: info.supported_reasoning_levels,
148            is_default: false,
149            upgrade: info.upgrade,
150            show_in_picker: info.visibility == "list",
151            supported_in_api: info.supported_in_api,
152            context_window: info.context_window,
153        }
154    }
155}
156
157/// Get built-in model presets for the given provider
158pub fn builtin_model_presets() -> Vec<ModelPreset> {
159    let mut presets = Vec::new();
160
161    // Gemini presets
162    presets.extend(presets::gemini_presets());
163
164    // OpenAI presets
165    presets.extend(presets::openai_presets());
166
167    // Anthropic presets
168    presets.extend(presets::anthropic_presets());
169
170    // Copilot presets
171    presets.extend(presets::copilot_presets());
172
173    // DeepSeek presets
174    presets.extend(presets::deepseek_presets());
175
176    // Official Meta AI presets
177    presets.extend(presets::meta_presets());
178
179    // Z.AI presets
180    presets.extend(presets::zai_presets());
181
182    // LM Studio presets
183    presets.extend(presets::lmstudio_presets());
184
185    // llama.cpp presets
186    presets.extend(presets::llamacpp_presets());
187
188    // MiniMax presets
189    presets.extend(presets::minimax_presets());
190
191    // OpenCode Zen presets
192    presets.extend(presets::opencode_zen_presets());
193
194    // OpenCode Go presets
195    presets.extend(presets::opencode_go_presets());
196
197    // Poolside presets
198    presets.extend(presets::poolside_presets());
199
200    // StepFun presets
201    presets.extend(presets::stepfun_presets());
202
203    // xAI presets
204    presets.extend(presets::xai_presets());
205
206    // NVIDIA NIM presets
207    presets.extend(presets::nvidia_presets());
208
209    // Merge Gateway presets
210    presets.extend(presets::merge_gateway_presets());
211
212    // Evolink presets
213    presets.extend(presets::evolink_presets());
214
215    // Vercel AI Gateway presets
216    presets.extend(presets::vercel_presets());
217
218    presets
219}
220
221/// Get presets for a specific provider
222pub fn presets_for_provider(provider: Provider) -> Vec<ModelPreset> {
223    match provider {
224        Provider::Gemini => presets::gemini_presets(),
225        Provider::OpenAI => presets::openai_presets(),
226        Provider::Anthropic => presets::anthropic_presets(),
227        Provider::Copilot => presets::copilot_presets(),
228        Provider::DeepSeek => presets::deepseek_presets(),
229        Provider::Meta => presets::meta_presets(),
230        Provider::ZAI => presets::zai_presets(),
231        Provider::Minimax => presets::minimax_presets(),
232        Provider::OpenRouter => presets::openrouter_presets(),
233        Provider::Ollama => presets::ollama_presets(),
234        Provider::OllamaCloud => presets::ollama_presets(),
235        Provider::LmStudio => presets::lmstudio_presets(),
236        Provider::LlamaCpp => presets::llamacpp_presets(),
237        Provider::Moonshot => presets::moonshot_presets(),
238        Provider::Mistral => presets::mistral_presets(),
239        Provider::HuggingFace => presets::huggingface_presets(),
240        Provider::OpenCodeZen => presets::opencode_zen_presets(),
241        Provider::OpenCodeGo => presets::opencode_go_presets(),
242        Provider::MiMo => presets::mimo_presets(),
243        Provider::Qwen => presets::qwen_presets(),
244        Provider::StepFun => presets::stepfun_presets(),
245        Provider::Evolink => presets::evolink_presets(),
246        Provider::Poolside => presets::poolside_presets(),
247        Provider::XAI => presets::xai_presets(),
248        Provider::NVIDIA => presets::nvidia_presets(),
249        Provider::MergeGateway => presets::merge_gateway_presets(),
250        Provider::Vercel => presets::vercel_presets(),
251    }
252}
253
254pub fn all_model_presets() -> Vec<ModelPreset> {
255    builtin_model_presets()
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn only_one_default_per_provider() {
264        let presets = builtin_model_presets();
265        let providers: Vec<Provider> = Provider::all_providers();
266
267        for provider in providers {
268            let default_count = presets.iter().filter(|p| p.provider == provider && p.is_default).count();
269            assert!(default_count <= 1, "Provider {provider:?} has {default_count} defaults, expected 0 or 1");
270        }
271    }
272
273    #[test]
274    fn gemini_presets_exist() {
275        let presets = presets::gemini_presets();
276        assert!(!presets.is_empty());
277        assert!(presets.iter().any(|p| p.id.contains("gemini")));
278    }
279
280    #[test]
281    fn model_info_converts_to_preset() {
282        let info = ModelInfo {
283            slug: "test-model".to_string(),
284            display_name: "Test Model".to_string(),
285            description: "A test model".to_string(),
286            provider: Provider::Gemini,
287            default_reasoning_level: ReasoningEffortLevel::Medium,
288            supported_reasoning_levels: vec![],
289            context_window: Some(128_000),
290            supports_tool_use: true,
291            supports_streaming: true,
292            supports_vision: false,
293            supports_structured_output: false,
294            supports_reasoning: false,
295            max_output_tokens: None,
296            priority: 0,
297            visibility: "list".to_string(),
298            supported_in_api: true,
299            upgrade: None,
300        };
301
302        let preset: ModelPreset = info.into();
303        assert_eq!(preset.id, "test-model");
304        assert_eq!(preset.model, "test-model");
305        assert!(preset.show_in_picker);
306    }
307
308    #[test]
309    fn openai_codex_presets_default_to_high_reasoning() {
310        let codex = presets::openai_presets()
311            .into_iter()
312            .find(|preset| preset.id == "gpt-5-codex")
313            .expect("gpt-5.3-codex preset");
314
315        assert_eq!(codex.default_reasoning_effort, ReasoningEffortLevel::High);
316    }
317
318    #[test]
319    fn mistral_presets_include_large_4_without_changing_default() {
320        let presets = presets::mistral_presets();
321        let large_4 = presets
322            .iter()
323            .find(|preset| preset.model == "mistral-large-4")
324            .expect("mistral large 4 preset");
325        assert_eq!(large_4.provider, Provider::Mistral);
326        assert!(!large_4.is_default);
327        assert!(large_4.show_in_picker);
328        assert_eq!(large_4.context_window, Some(1_000_000));
329
330        let default = presets.iter().find(|preset| preset.is_default).expect("mistral default preset");
331        assert_eq!(default.model, "mistral-large-2512");
332    }
333
334    #[test]
335    fn moonshot_presets_exist_and_default_to_kimi_k3() {
336        let presets = presets::moonshot_presets();
337        assert_eq!(presets.len(), 1);
338
339        let default = presets
340            .iter()
341            .find(|preset| preset.is_default)
342            .expect("moonshot default preset");
343        assert_eq!(default.id, "kimi-k3");
344        assert_eq!(default.provider, Provider::Moonshot);
345    }
346
347    #[test]
348    fn xai_presets_exist_and_default_to_grok46() {
349        let presets = presets::xai_presets();
350        assert!(presets.iter().any(|preset| preset.model == "grok-4.6"));
351        assert!(presets.iter().any(|preset| preset.model == "grok-4.7"));
352
353        let default = presets.iter().find(|preset| preset.is_default).expect("xAI default preset");
354        assert_eq!(default.model, "grok-4.7");
355        assert_eq!(default.context_window, Some(500_000));
356        assert!(
357            default
358                .supported_reasoning_efforts
359                .iter()
360                .any(|effort| effort.effort == ReasoningEffortLevel::XHigh)
361        );
362        assert!(
363            presets
364                .iter()
365                .all(|preset| preset.provider == Provider::XAI && preset.show_in_picker)
366        );
367    }
368
369    #[test]
370    fn nvidia_presets_expose_curated_models_and_default() {
371        let presets = presets::nvidia_presets();
372        assert_eq!(presets.len(), 3);
373        let default = presets.iter().find(|preset| preset.is_default).expect("NVIDIA default preset");
374        assert_eq!(default.model, "nvidia/nemotron-3-ultra-550b-a55b");
375        assert_eq!(default.context_window, Some(1_000_000));
376        assert!(
377            presets
378                .iter()
379                .all(|preset| preset.provider == Provider::NVIDIA && preset.show_in_picker)
380        );
381    }
382
383    #[test]
384    fn merge_gateway_presets_expose_curated_routes_and_default() {
385        let presets = presets::merge_gateway_presets();
386        assert_eq!(presets.len(), 26);
387        let default = presets
388            .iter()
389            .find(|preset| preset.is_default)
390            .expect("Merge Gateway default preset");
391        assert_eq!(default.model, "default_routing");
392        assert_eq!(default.context_window, Some(128_000));
393        assert!(
394            presets
395                .iter()
396                .all(|preset| preset.provider == Provider::MergeGateway && preset.show_in_picker)
397        );
398
399        use crate::config::constants::models;
400        let reasoning_models: Vec<&str> = presets
401            .iter()
402            .filter(|preset| !preset.supported_reasoning_efforts.is_empty())
403            .map(|preset| preset.model.as_str())
404            .collect();
405        assert_eq!(reasoning_models, models::merge_gateway::REASONING_MODELS.to_vec());
406        assert!(
407            presets
408                .iter()
409                .filter(|preset| !preset.supported_reasoning_efforts.is_empty())
410                .all(|preset| preset.default_reasoning_effort == ReasoningEffortLevel::Medium)
411        );
412    }
413
414    #[test]
415    fn meta_presets_keep_standard_default_and_contributor_opt_in() {
416        let presets = presets::meta_presets();
417        assert_eq!(presets.len(), 3);
418
419        let default = presets.iter().find(|preset| preset.is_default).expect("Meta default preset");
420        assert_eq!(default.model, "muse-spark-1.3");
421        assert!(presets.iter().any(|preset| preset.model == "muse-spark-1.3-contributor"));
422        assert!(
423            presets
424                .iter()
425                .all(|preset| preset.provider == Provider::Meta && preset.show_in_picker)
426        );
427    }
428
429    #[test]
430    fn openrouter_presets_include_meta_muse_models() {
431        let presets = presets::openrouter_presets();
432
433        let glimmer = presets
434            .iter()
435            .find(|preset| preset.model == "meta/muse-glimmer-30b")
436            .expect("OpenRouter Glimmer preset");
437        assert_eq!(glimmer.context_window, Some(131_072));
438    }
439}