Skip to main content

vtcode_core/core/agent/
config.rs

1use std::path::{Path, PathBuf};
2use std::str::FromStr;
3
4use crate::cli::args::Cli;
5pub use crate::config::api_keys::api_key_env_var;
6use crate::config::api_keys::resolve_api_key_env;
7use crate::config::constants::defaults;
8use crate::config::loader::VTCodeConfig;
9use crate::config::models::Provider;
10use crate::config::types::{AgentConfig, ModelSelectionSource};
11use crate::llm::factory::infer_provider;
12use crate::utils::path::canonicalize_workspace;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct RuntimeModelSelection {
16    pub model: String,
17    pub provider: String,
18    pub model_source: ModelSelectionSource,
19}
20
21pub fn resolve_runtime_model_selection(args: &Cli, config: &VTCodeConfig) -> RuntimeModelSelection {
22    let (model, model_source) = if let Some(agent) = args.agent.clone() {
23        (agent, ModelSelectionSource::CliOverride)
24    } else if let Some(model) = args.model.clone() {
25        (model, ModelSelectionSource::CliOverride)
26    } else {
27        (
28            config.agent.default_model.clone(),
29            ModelSelectionSource::WorkspaceConfig,
30        )
31    };
32
33    let provider = resolve_provider(
34        args.provider.clone().or_else(provider_env_override),
35        config.agent.provider.as_str(),
36        &model,
37        model_source,
38    );
39
40    RuntimeModelSelection {
41        model,
42        provider,
43        model_source,
44    }
45}
46
47pub fn build_runtime_agent_config(
48    args: &Cli,
49    config: &VTCodeConfig,
50    workspace: PathBuf,
51    selection: RuntimeModelSelection,
52    api_key: String,
53    theme_selection: String,
54) -> AgentConfig {
55    let workspace = canonicalize_workspace(&workspace);
56    let cli_api_key_env = args.api_key_env.trim();
57    let api_key_env_override = if cli_api_key_env.is_empty()
58        || cli_api_key_env.eq_ignore_ascii_case(defaults::DEFAULT_API_KEY_ENV)
59    {
60        None
61    } else {
62        Some(cli_api_key_env.to_owned())
63    };
64
65    let checkpointing_storage_dir = resolve_checkpointing_storage_dir(
66        &workspace,
67        config.agent.checkpointing.storage_dir.as_deref(),
68    );
69    let RuntimeModelSelection {
70        model,
71        provider,
72        model_source,
73    } = selection;
74    let api_key_env = api_key_env_override
75        .unwrap_or_else(|| resolve_api_key_env(&provider, &config.agent.api_key_env));
76
77    AgentConfig {
78        model,
79        api_key,
80        provider,
81        api_key_env,
82        workspace,
83        verbose: args.verbose,
84        quiet: args.quiet,
85        theme: theme_selection,
86        reasoning_effort: config.agent.reasoning_effort,
87        ui_surface: config.agent.ui_surface,
88        prompt_cache: config.prompt_cache.clone(),
89        model_source,
90        custom_api_keys: config.agent.custom_api_keys.clone(),
91        checkpointing_enabled: config.agent.checkpointing.enabled,
92        checkpointing_storage_dir,
93        checkpointing_max_snapshots: config.agent.checkpointing.max_snapshots,
94        checkpointing_max_age_days: config.agent.checkpointing.max_age_days,
95        max_conversation_turns: config.agent.max_conversation_turns,
96        model_behavior: Some(config.model.clone()),
97        openai_chatgpt_auth: None,
98    }
99}
100
101pub fn resolve_checkpointing_storage_dir(
102    workspace: &Path,
103    storage_dir: Option<&str>,
104) -> Option<PathBuf> {
105    storage_dir.map(PathBuf::from).map(|candidate| {
106        if candidate.is_absolute() {
107            candidate
108        } else {
109            workspace.join(candidate)
110        }
111    })
112}
113
114pub fn provider_label(provider: &str, vt_cfg: Option<&VTCodeConfig>) -> String {
115    if let Some(vt_cfg) = vt_cfg {
116        return vt_cfg.provider_display_name(provider);
117    }
118
119    if provider.eq_ignore_ascii_case("codex") {
120        return "Codex".to_string();
121    }
122
123    Provider::from_str(provider)
124        .map(|resolved| resolved.label().to_string())
125        .unwrap_or_else(|_| provider.to_string())
126}
127
128fn resolve_provider(
129    cli_provider: Option<String>,
130    configured_provider: &str,
131    model: &str,
132    model_source: ModelSelectionSource,
133) -> String {
134    if let Some(provider) = cli_provider {
135        return provider;
136    }
137
138    if matches!(model_source, ModelSelectionSource::CliOverride)
139        && let Some(provider) = infer_provider(None, model)
140    {
141        return provider.to_string();
142    }
143
144    let configured_provider = configured_provider.trim();
145    if !configured_provider.is_empty() {
146        return configured_provider.to_owned();
147    }
148
149    infer_provider(None, model)
150        .map(|provider| provider.to_string())
151        .unwrap_or_else(|| defaults::DEFAULT_PROVIDER.to_owned())
152}
153
154fn provider_env_override() -> Option<String> {
155    std::env::var("VTCODE_PROVIDER")
156        .ok()
157        .or_else(|| std::env::var("provider").ok())
158        .map(|value| value.trim().to_owned())
159        .filter(|value| !value.is_empty())
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use clap::Parser;
166
167    #[test]
168    fn provider_resolution_prefers_configured_provider_for_config_model() {
169        let mut config = VTCodeConfig::default();
170        config.agent.provider = "zai".to_owned();
171        config.agent.default_model =
172            crate::config::constants::models::ollama::MINIMAX_M25_CLOUD.to_owned();
173
174        let args = Cli::parse_from(["vtcode"]);
175        let selection = resolve_runtime_model_selection(&args, &config);
176
177        assert_eq!(selection.provider, "zai");
178        assert_eq!(
179            selection.model_source,
180            ModelSelectionSource::WorkspaceConfig
181        );
182    }
183
184    #[test]
185    fn provider_resolution_infers_from_cli_model_without_cli_provider() {
186        let mut config = VTCodeConfig::default();
187        config.agent.provider = "zai".to_owned();
188
189        let args = Cli::parse_from([
190            "vtcode",
191            "--model",
192            crate::config::constants::models::ollama::MINIMAX_M25_CLOUD,
193        ]);
194        let selection = resolve_runtime_model_selection(&args, &config);
195
196        assert_eq!(selection.provider, "ollama");
197        assert_eq!(selection.model_source, ModelSelectionSource::CliOverride);
198    }
199
200    #[test]
201    fn provider_resolution_uses_cli_provider_when_present() {
202        let mut config = VTCodeConfig::default();
203        config.agent.provider = "zai".to_owned();
204
205        let args = Cli::parse_from([
206            "vtcode",
207            "--model",
208            crate::config::constants::models::ollama::MINIMAX_M25_CLOUD,
209            "--provider",
210            "minimax",
211        ]);
212        let selection = resolve_runtime_model_selection(&args, &config);
213
214        assert_eq!(selection.provider, "minimax");
215    }
216
217    #[test]
218    fn build_runtime_agent_config_uses_provider_default_api_key_env() {
219        let mut config = VTCodeConfig::default();
220        config.agent.api_key_env = defaults::DEFAULT_API_KEY_ENV.to_owned();
221
222        let args = Cli::parse_from(["vtcode", "--provider", "openai"]);
223        let selection = RuntimeModelSelection {
224            model: crate::config::constants::models::openai::GPT_5.to_owned(),
225            provider: "openai".to_owned(),
226            model_source: ModelSelectionSource::CliOverride,
227        };
228
229        let agent_config = build_runtime_agent_config(
230            &args,
231            &config,
232            PathBuf::from("/workspace"),
233            selection,
234            "test-key".to_owned(),
235            "dark".to_owned(),
236        );
237
238        assert_eq!(agent_config.api_key_env, "OPENAI_API_KEY");
239    }
240
241    #[test]
242    fn build_runtime_agent_config_respects_cli_api_key_env_override() {
243        let config = VTCodeConfig::default();
244        let args = Cli::parse_from([
245            "vtcode",
246            "--provider",
247            "openai",
248            "--api-key-env",
249            "CUSTOM_OPENAI_KEY",
250        ]);
251        let selection = RuntimeModelSelection {
252            model: crate::config::constants::models::openai::GPT_5.to_owned(),
253            provider: "openai".to_owned(),
254            model_source: ModelSelectionSource::CliOverride,
255        };
256
257        let agent_config = build_runtime_agent_config(
258            &args,
259            &config,
260            PathBuf::from("/workspace"),
261            selection,
262            "test-key".to_owned(),
263            "dark".to_owned(),
264        );
265
266        assert_eq!(agent_config.api_key_env, "CUSTOM_OPENAI_KEY");
267    }
268
269    #[test]
270    fn provider_label_uses_custom_provider_display_name() {
271        let mut config = VTCodeConfig::default();
272        config
273            .custom_providers
274            .push(vtcode_config::core::CustomProviderConfig {
275                name: "mycorp".to_string(),
276                display_name: "MyCorporateName".to_string(),
277                base_url: "https://llm.example/v1".to_string(),
278                api_key_env: "MYCORP_API_KEY".to_string(),
279                auth: None,
280                model: "gpt-5-mini".to_string(),
281                models: Vec::new(),
282            });
283
284        assert_eq!(provider_label("mycorp", Some(&config)), "MyCorporateName");
285    }
286
287    #[test]
288    fn resolve_checkpointing_storage_dir_preserves_absolute_path() {
289        let resolved = resolve_checkpointing_storage_dir(
290            Path::new("/workspace"),
291            Some("/tmp/vtcode-checkpoints"),
292        );
293
294        assert_eq!(resolved, Some(PathBuf::from("/tmp/vtcode-checkpoints")));
295    }
296
297    #[test]
298    fn build_runtime_agent_config_canonicalizes_relative_workspace() {
299        let temp = tempfile::TempDir::new().expect("temp dir");
300        let original_dir = std::env::current_dir().expect("current dir");
301        std::env::set_current_dir(temp.path()).expect("set current dir");
302
303        let config = VTCodeConfig::default();
304        let args = Cli::parse_from(["vtcode"]);
305        let selection = RuntimeModelSelection {
306            model: crate::config::constants::models::openai::GPT_5.to_owned(),
307            provider: "openai".to_owned(),
308            model_source: ModelSelectionSource::CliOverride,
309        };
310
311        let agent_config = build_runtime_agent_config(
312            &args,
313            &config,
314            PathBuf::from("."),
315            selection,
316            "test-key".to_owned(),
317            "dark".to_owned(),
318        );
319
320        std::env::set_current_dir(original_dir).expect("restore current dir");
321        let expected_workspace = std::fs::canonicalize(temp.path()).expect("canonical workspace");
322
323        assert_eq!(agent_config.workspace, expected_workspace);
324    }
325}