1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4use std::str::FromStr;
5
6use crate::acp::AgentClientProtocolConfig;
7use crate::codex::{FileOpener, HistoryConfig, TuiConfig};
8use crate::constants::defaults::DEFAULT_PRIMARY_AGENT_NAME;
9use crate::context::ContextFeaturesConfig;
10use crate::core::{
11 AgentConfig, AgentPermissionsConfig, AnthropicConfig, AuthConfig, AutomationConfig, CommandsConfig,
12 CustomProviderConfig, DesktopConfig, DotfileProtectionConfig, ModelConfig, OpenAIConfig, OpenRouterConfig,
13 PermissionsConfig, PromptCachingConfig, ProviderOverrideConfig, SandboxConfig, SecurityConfig, SkillsConfig,
14 ToolsConfig,
15};
16use crate::debug::DebugConfig;
17use crate::hooks::HooksConfig;
18use crate::mcp::McpClientConfig;
19use crate::models::{MiMoAuthMethod, Provider};
20use crate::optimization::OptimizationConfig;
21use crate::output_styles::OutputStyleConfig;
22use crate::root::{ChatConfig, PtyConfig, UiConfig};
23use crate::subagents::SubagentRuntimeLimits;
24use crate::telemetry::TelemetryConfig;
25use crate::timeouts::TimeoutsConfig;
26use crate::webmcp::WebmcpConfig;
27
28use crate::loader::syntax_highlighting::SyntaxHighlightingConfig;
29
30#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
32#[derive(Debug, Clone, Deserialize, Serialize, Default)]
33pub struct ProviderConfig {
34 #[serde(default)]
36 pub openai: OpenAIConfig,
37
38 #[serde(default)]
40 pub anthropic: AnthropicConfig,
41
42 #[serde(default)]
44 pub openrouter: OpenRouterConfig,
45
46 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub mimo_auth_method: Option<MiMoAuthMethod>,
49}
50
51#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
57#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
58pub struct FeaturesConfig {
59 #[serde(default)]
63 pub memories: bool,
64}
65
66#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
73#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
74pub struct WorkspaceConfig {
75 #[serde(default)]
79 pub(crate) use_root_config: bool,
80
81 #[serde(default = "default_true")]
83 pub(crate) include_context: bool,
84
85 #[serde(default)]
87 pub(crate) max_context_size: Option<usize>,
88}
89
90impl Default for WorkspaceConfig {
91 fn default() -> Self {
92 Self {
93 use_root_config: false,
94 include_context: true,
95 max_context_size: None,
96 }
97 }
98}
99
100fn default_true() -> bool {
101 true
102}
103
104#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
106#[derive(Debug, Clone, Deserialize, Serialize)]
107pub struct VTCodeConfig {
108 #[serde(default = "default_primary_agent")]
110 pub default_primary_agent: String,
111
112 #[serde(default)]
114 pub features: FeaturesConfig,
115
116 #[serde(default)]
118 pub file_opener: FileOpener,
119
120 #[serde(default)]
122 pub notify: Vec<String>,
123
124 #[serde(default)]
126 pub history: HistoryConfig,
127
128 #[serde(default)]
130 pub tui: TuiConfig,
131
132 #[serde(default)]
134 pub desktop: DesktopConfig,
135
136 #[serde(default)]
138 pub agent: AgentConfig,
139
140 #[serde(default)]
142 pub auth: AuthConfig,
143
144 #[serde(default)]
146 pub tools: ToolsConfig,
147
148 #[serde(default)]
150 pub commands: CommandsConfig,
151
152 #[serde(default)]
154 pub permissions: PermissionsConfig,
155
156 #[serde(skip)]
158 #[cfg_attr(feature = "schema", schemars(skip))]
159 pub runtime_agent_permissions: Option<AgentPermissionsConfig>,
160
161 #[serde(skip)]
166 #[cfg_attr(feature = "schema", schemars(skip))]
167 pub workspace_lifecycle_hooks: Option<crate::hooks::WorkspaceLifecycleHooks>,
168
169 #[serde(default)]
171 pub security: SecurityConfig,
172
173 #[serde(default)]
175 pub sandbox: SandboxConfig,
176
177 #[serde(default)]
179 pub ui: UiConfig,
180
181 #[serde(default)]
183 pub chat: ChatConfig,
184
185 #[serde(default)]
187 pub pty: PtyConfig,
188
189 #[serde(default)]
191 pub debug: DebugConfig,
192
193 #[serde(default)]
195 pub context: ContextFeaturesConfig,
196
197 #[serde(default)]
199 pub telemetry: TelemetryConfig,
200
201 #[serde(default)]
203 pub optimization: OptimizationConfig,
204
205 #[serde(default)]
207 pub syntax_highlighting: SyntaxHighlightingConfig,
208
209 #[serde(default)]
211 pub timeouts: TimeoutsConfig,
212
213 #[serde(default)]
215 pub automation: AutomationConfig,
216
217 #[serde(default)]
219 pub subagents: SubagentRuntimeLimits,
220
221 #[serde(default)]
223 pub prompt_cache: PromptCachingConfig,
224
225 #[serde(default)]
227 pub mcp: McpClientConfig,
228
229 #[serde(default)]
231 pub webmcp: WebmcpConfig,
232
233 #[serde(default)]
235 pub acp: AgentClientProtocolConfig,
236
237 #[serde(default)]
239 pub hooks: HooksConfig,
240
241 #[serde(default)]
243 pub model: ModelConfig,
244
245 #[serde(default)]
247 pub provider: ProviderConfig,
248
249 #[serde(default)]
251 pub skills: SkillsConfig,
252
253 #[serde(default)]
256 pub custom_providers: Vec<CustomProviderConfig>,
257
258 #[serde(default)]
264 pub provider_overrides: BTreeMap<String, ProviderOverrideConfig>,
265
266 #[serde(default)]
272 pub providers_whitelist: Vec<String>,
273
274 #[serde(default)]
276 pub output_style: OutputStyleConfig,
277
278 #[serde(default)]
280 pub dotfile_protection: DotfileProtectionConfig,
281
282 #[serde(default)]
284 pub workspace: WorkspaceConfig,
285}
286
287impl Default for VTCodeConfig {
288 fn default() -> Self {
289 Self {
290 default_primary_agent: default_primary_agent(),
291 features: FeaturesConfig::default(),
292 file_opener: FileOpener::default(),
293 notify: Vec::new(),
294 history: HistoryConfig::default(),
295 tui: TuiConfig::default(),
296 desktop: DesktopConfig::default(),
297 agent: AgentConfig::default(),
298 auth: AuthConfig::default(),
299 tools: ToolsConfig::default(),
300 commands: CommandsConfig::default(),
301 permissions: PermissionsConfig::default(),
302 runtime_agent_permissions: None,
303 workspace_lifecycle_hooks: None,
304 security: SecurityConfig::default(),
305 sandbox: SandboxConfig::default(),
306 ui: UiConfig::default(),
307 chat: ChatConfig::default(),
308 pty: PtyConfig::default(),
309 debug: DebugConfig::default(),
310 context: ContextFeaturesConfig::default(),
311 telemetry: TelemetryConfig::default(),
312 optimization: OptimizationConfig::default(),
313 syntax_highlighting: SyntaxHighlightingConfig::default(),
314 timeouts: TimeoutsConfig::default(),
315 automation: AutomationConfig::default(),
316 subagents: SubagentRuntimeLimits::default(),
317 prompt_cache: PromptCachingConfig::default(),
318 mcp: McpClientConfig::default(),
319 webmcp: WebmcpConfig::default(),
320 acp: AgentClientProtocolConfig::default(),
321 hooks: HooksConfig::default(),
322 model: ModelConfig::default(),
323 provider: ProviderConfig::default(),
324 skills: SkillsConfig::default(),
325 custom_providers: Vec::new(),
326 provider_overrides: BTreeMap::new(),
327 providers_whitelist: Vec::new(),
328 output_style: OutputStyleConfig::default(),
329 dotfile_protection: DotfileProtectionConfig::default(),
330 workspace: WorkspaceConfig::default(),
331 }
332 }
333}
334
335impl VTCodeConfig {
336 pub fn validate(&self) -> Result<()> {
337 self.syntax_highlighting
338 .validate()
339 .context("Invalid syntax_highlighting configuration")?;
340
341 self.context.validate().context("Invalid context configuration")?;
342
343 self.hooks.validate().context("Invalid hooks configuration")?;
344
345 self.timeouts.validate().context("Invalid timeouts configuration")?;
346
347 self.prompt_cache.validate().context("Invalid prompt_cache configuration")?;
348
349 self.webmcp.validate().context("Invalid webmcp configuration")?;
350
351 self.agent
352 .validate_llm_params()
353 .map_err(anyhow::Error::msg)
354 .context("Invalid agent configuration")?;
355
356 self.ui
357 .keyboard_protocol
358 .validate()
359 .context("Invalid keyboard_protocol configuration")?;
360
361 self.pty.validate().context("Invalid pty configuration")?;
362
363 let mut seen_names = std::collections::HashSet::new();
365 for cp in &self.custom_providers {
366 cp.validate()
367 .map_err(|msg| anyhow::anyhow!(msg))
368 .context("Invalid custom_providers configuration")?;
369 if !seen_names.insert(cp.name.to_lowercase()) {
370 anyhow::bail!("custom_providers: duplicate name `{}`", cp.name);
371 }
372 }
373
374 for (provider_name, override_config) in &self.provider_overrides {
376 override_config
377 .validate(provider_name)
378 .map_err(|msg| anyhow::anyhow!(msg))
379 .context("Invalid provider_overrides configuration")?;
380 if Provider::from_str(provider_name).is_err() {
382 anyhow::bail!(
383 "provider_overrides: unknown provider `{provider_name}`; \
384 must be one of: gemini, openai, anthropic, copilot, deepseek, meta, \
385 openrouter, ollama, lmstudio, llamacpp, moonshot, zai, minimax, \
386 mimo, mistral, huggingface, opencodezen, opencodego, qwen, \
387 stepfun, evolink, poolside, nvidia, merge-gateway, vercel"
388 );
389 }
390 }
391
392 for entry in &self.providers_whitelist {
394 let known = Provider::from_str(entry).is_ok() || self.custom_providers.iter().any(|cp| cp.name == *entry);
395 if !known {
396 anyhow::bail!(
397 "providers_whitelist: unknown provider `{entry}`; \
398 must be a built-in provider or a name from custom_providers"
399 );
400 }
401 }
402
403 Ok(())
404 }
405
406 #[must_use]
411 pub fn persistent_memory_enabled(&self) -> bool {
412 self.features.memories && self.agent.persistent_memory.enabled
413 }
414
415 #[must_use]
420 pub fn memories_enabled(&self) -> bool {
421 self.persistent_memory_enabled() && self.agent.persistent_memory.memories.use_memories
422 }
423
424 #[must_use]
426 pub fn should_generate_memories(&self) -> bool {
427 self.persistent_memory_enabled() && self.agent.persistent_memory.memories.generate_memories
428 }
429
430 pub fn custom_provider(&self, name: &str) -> Option<&CustomProviderConfig> {
432 let lower = name.to_lowercase();
433 self.custom_providers.iter().find(|cp| cp.name.to_lowercase() == lower)
434 }
435
436 pub fn configured_api_key_env(&self, name: &str) -> Option<String> {
442 if let Some(custom_provider) = self.custom_provider(name) {
443 return Some(custom_provider.resolved_api_key_env());
444 }
445
446 self.provider_overrides
447 .iter()
448 .find(|(provider, _)| provider.eq_ignore_ascii_case(name))
449 .and_then(|(_, provider_override)| provider_override.api_key_env.clone())
450 .filter(|api_key_env| !api_key_env.trim().is_empty())
451 }
452
453 pub fn provider_display_name(&self, provider_key: &str) -> String {
456 if let Some(cp) = self.custom_provider(provider_key) {
457 cp.display_name.clone()
458 } else if provider_key.eq_ignore_ascii_case("codex") {
459 "Codex".to_string()
460 } else if let Ok(p) = FromStr::from_str(provider_key) {
461 let p: Provider = p;
462 p.label().to_string()
463 } else {
464 provider_key.to_string()
465 }
466 }
467}
468
469fn default_primary_agent() -> String {
470 DEFAULT_PRIMARY_AGENT_NAME.to_string()
471}