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::env_helpers::default_true;
18use crate::hooks::HooksConfig;
19use crate::mcp::McpClientConfig;
20use crate::models::{MiMoAuthMethod, Provider};
21use crate::optimization::OptimizationConfig;
22use crate::output_styles::OutputStyleConfig;
23use crate::root::{ChatConfig, PtyConfig, UiConfig};
24use crate::subagents::SubagentRuntimeLimits;
25use crate::telemetry::TelemetryConfig;
26use crate::timeouts::TimeoutsConfig;
27use crate::webmcp::WebmcpConfig;
28
29use crate::loader::syntax_highlighting::SyntaxHighlightingConfig;
30
31#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
33#[derive(Debug, Clone, Deserialize, Serialize, Default)]
34pub struct ProviderConfig {
35 #[serde(default)]
37 pub openai: OpenAIConfig,
38
39 #[serde(default)]
41 pub anthropic: AnthropicConfig,
42
43 #[serde(default)]
45 pub openrouter: OpenRouterConfig,
46
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub mimo_auth_method: Option<MiMoAuthMethod>,
50}
51
52#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
58#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
59pub struct FeaturesConfig {
60 #[serde(default)]
64 pub memories: bool,
65}
66
67#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
74#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
75pub struct WorkspaceConfig {
76 #[serde(default)]
80 pub(crate) use_root_config: bool,
81
82 #[serde(default = "default_true")]
84 pub(crate) include_context: bool,
85
86 #[serde(default)]
88 pub(crate) max_context_size: Option<usize>,
89}
90
91impl Default for WorkspaceConfig {
92 fn default() -> Self {
93 Self {
94 use_root_config: false,
95 include_context: true,
96 max_context_size: None,
97 }
98 }
99}
100
101#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
103#[derive(Debug, Clone, Deserialize, Serialize)]
104pub struct VTCodeConfig {
105 #[serde(default = "default_primary_agent")]
107 pub default_primary_agent: String,
108
109 #[serde(default)]
111 pub features: FeaturesConfig,
112
113 #[serde(default)]
115 pub file_opener: FileOpener,
116
117 #[serde(default)]
119 pub notify: Vec<String>,
120
121 #[serde(default)]
123 pub history: HistoryConfig,
124
125 #[serde(default)]
127 pub tui: TuiConfig,
128
129 #[serde(default)]
131 pub desktop: DesktopConfig,
132
133 #[serde(default)]
135 pub agent: AgentConfig,
136
137 #[serde(default)]
139 pub auth: AuthConfig,
140
141 #[serde(default)]
143 pub tools: ToolsConfig,
144
145 #[serde(default)]
147 pub commands: CommandsConfig,
148
149 #[serde(default)]
151 pub permissions: PermissionsConfig,
152
153 #[serde(skip)]
155 #[cfg_attr(feature = "schema", schemars(skip))]
156 pub runtime_agent_permissions: Option<AgentPermissionsConfig>,
157
158 #[serde(skip)]
163 #[cfg_attr(feature = "schema", schemars(skip))]
164 pub workspace_lifecycle_hooks: Option<crate::hooks::WorkspaceLifecycleHooks>,
165
166 #[serde(default)]
168 pub security: SecurityConfig,
169
170 #[serde(default)]
172 pub sandbox: SandboxConfig,
173
174 #[serde(default)]
176 pub ui: UiConfig,
177
178 #[serde(default)]
180 pub chat: ChatConfig,
181
182 #[serde(default)]
184 pub pty: PtyConfig,
185
186 #[serde(default)]
188 pub debug: DebugConfig,
189
190 #[serde(default)]
192 pub context: ContextFeaturesConfig,
193
194 #[serde(default)]
196 pub telemetry: TelemetryConfig,
197
198 #[serde(default)]
200 pub optimization: OptimizationConfig,
201
202 #[serde(default)]
204 pub syntax_highlighting: SyntaxHighlightingConfig,
205
206 #[serde(default)]
208 pub timeouts: TimeoutsConfig,
209
210 #[serde(default)]
212 pub automation: AutomationConfig,
213
214 #[serde(default)]
216 pub subagents: SubagentRuntimeLimits,
217
218 #[serde(default)]
220 pub prompt_cache: PromptCachingConfig,
221
222 #[serde(default)]
224 pub mcp: McpClientConfig,
225
226 #[serde(default)]
228 pub webmcp: WebmcpConfig,
229
230 #[serde(default)]
232 pub acp: AgentClientProtocolConfig,
233
234 #[serde(default)]
236 pub hooks: HooksConfig,
237
238 #[serde(default)]
240 pub model: ModelConfig,
241
242 #[serde(default)]
244 pub provider: ProviderConfig,
245
246 #[serde(default)]
248 pub skills: SkillsConfig,
249
250 #[serde(default)]
253 pub custom_providers: Vec<CustomProviderConfig>,
254
255 #[serde(default)]
261 pub provider_overrides: BTreeMap<String, ProviderOverrideConfig>,
262
263 #[serde(default)]
269 pub providers_whitelist: Vec<String>,
270
271 #[serde(default)]
273 pub output_style: OutputStyleConfig,
274
275 #[serde(default)]
277 pub dotfile_protection: DotfileProtectionConfig,
278
279 #[serde(default)]
281 pub workspace: WorkspaceConfig,
282}
283
284impl Default for VTCodeConfig {
285 fn default() -> Self {
286 Self {
287 default_primary_agent: default_primary_agent(),
288 features: FeaturesConfig::default(),
289 file_opener: FileOpener::default(),
290 notify: Vec::new(),
291 history: HistoryConfig::default(),
292 tui: TuiConfig::default(),
293 desktop: DesktopConfig::default(),
294 agent: AgentConfig::default(),
295 auth: AuthConfig::default(),
296 tools: ToolsConfig::default(),
297 commands: CommandsConfig::default(),
298 permissions: PermissionsConfig::default(),
299 runtime_agent_permissions: None,
300 workspace_lifecycle_hooks: None,
301 security: SecurityConfig::default(),
302 sandbox: SandboxConfig::default(),
303 ui: UiConfig::default(),
304 chat: ChatConfig::default(),
305 pty: PtyConfig::default(),
306 debug: DebugConfig::default(),
307 context: ContextFeaturesConfig::default(),
308 telemetry: TelemetryConfig::default(),
309 optimization: OptimizationConfig::default(),
310 syntax_highlighting: SyntaxHighlightingConfig::default(),
311 timeouts: TimeoutsConfig::default(),
312 automation: AutomationConfig::default(),
313 subagents: SubagentRuntimeLimits::default(),
314 prompt_cache: PromptCachingConfig::default(),
315 mcp: McpClientConfig::default(),
316 webmcp: WebmcpConfig::default(),
317 acp: AgentClientProtocolConfig::default(),
318 hooks: HooksConfig::default(),
319 model: ModelConfig::default(),
320 provider: ProviderConfig::default(),
321 skills: SkillsConfig::default(),
322 custom_providers: Vec::new(),
323 provider_overrides: BTreeMap::new(),
324 providers_whitelist: Vec::new(),
325 output_style: OutputStyleConfig::default(),
326 dotfile_protection: DotfileProtectionConfig::default(),
327 workspace: WorkspaceConfig::default(),
328 }
329 }
330}
331
332impl VTCodeConfig {
333 pub fn validate(&self) -> Result<()> {
334 self.syntax_highlighting
335 .validate()
336 .context("Invalid syntax_highlighting configuration")?;
337
338 self.context.validate().context("Invalid context configuration")?;
339
340 self.hooks.validate().context("Invalid hooks configuration")?;
341
342 self.timeouts.validate().context("Invalid timeouts configuration")?;
343
344 self.prompt_cache.validate().context("Invalid prompt_cache configuration")?;
345
346 self.webmcp.validate().context("Invalid webmcp configuration")?;
347
348 self.agent
349 .validate_llm_params()
350 .map_err(anyhow::Error::msg)
351 .context("Invalid agent configuration")?;
352
353 self.ui
354 .keyboard_protocol
355 .validate()
356 .context("Invalid keyboard_protocol configuration")?;
357
358 self.pty.validate().context("Invalid pty configuration")?;
359
360 let mut seen_names = std::collections::HashSet::new();
362 for cp in &self.custom_providers {
363 cp.validate()
364 .map_err(|msg| anyhow::anyhow!(msg))
365 .context("Invalid custom_providers configuration")?;
366 if !seen_names.insert(cp.name.to_lowercase()) {
367 anyhow::bail!("custom_providers: duplicate name `{}`", cp.name);
368 }
369 }
370
371 for (provider_name, override_config) in &self.provider_overrides {
373 override_config
374 .validate(provider_name)
375 .map_err(|msg| anyhow::anyhow!(msg))
376 .context("Invalid provider_overrides configuration")?;
377 if Provider::from_str(provider_name).is_err() {
379 anyhow::bail!(
380 "provider_overrides: unknown provider `{provider_name}`; \
381 must be one of: gemini, openai, anthropic, copilot, deepseek, meta, \
382 openrouter, ollama, lmstudio, llamacpp, moonshot, zai, minimax, \
383 mimo, mistral, huggingface, opencodezen, opencodego, qwen, \
384 stepfun, evolink, poolside, nvidia, merge-gateway, vercel"
385 );
386 }
387 }
388
389 for entry in &self.providers_whitelist {
391 let known = Provider::from_str(entry).is_ok() || self.custom_providers.iter().any(|cp| cp.name == *entry);
392 if !known {
393 anyhow::bail!(
394 "providers_whitelist: unknown provider `{entry}`; \
395 must be a built-in provider or a name from custom_providers"
396 );
397 }
398 }
399
400 Ok(())
401 }
402
403 #[must_use]
408 pub fn persistent_memory_enabled(&self) -> bool {
409 self.features.memories && self.agent.persistent_memory.enabled
410 }
411
412 #[must_use]
417 pub fn memories_enabled(&self) -> bool {
418 self.persistent_memory_enabled() && self.agent.persistent_memory.memories.use_memories
419 }
420
421 #[must_use]
423 pub fn should_generate_memories(&self) -> bool {
424 self.persistent_memory_enabled() && self.agent.persistent_memory.memories.generate_memories
425 }
426
427 pub fn custom_provider(&self, name: &str) -> Option<&CustomProviderConfig> {
429 let lower = name.to_lowercase();
430 self.custom_providers.iter().find(|cp| cp.name.to_lowercase() == lower)
431 }
432
433 pub fn configured_api_key_env(&self, name: &str) -> Option<String> {
439 if let Some(custom_provider) = self.custom_provider(name) {
440 return Some(custom_provider.resolved_api_key_env());
441 }
442
443 self.provider_overrides
444 .iter()
445 .find(|(provider, _)| provider.eq_ignore_ascii_case(name))
446 .and_then(|(_, provider_override)| provider_override.api_key_env.clone())
447 .filter(|api_key_env| !api_key_env.trim().is_empty())
448 }
449
450 pub fn provider_display_name(&self, provider_key: &str) -> String {
453 if let Some(cp) = self.custom_provider(provider_key) {
454 cp.display_name.clone()
455 } else if provider_key.eq_ignore_ascii_case("codex") {
456 "Codex".to_string()
457 } else if let Ok(p) = FromStr::from_str(provider_key) {
458 let p: Provider = p;
459 p.label().to_string()
460 } else {
461 provider_key.to_string()
462 }
463 }
464}
465
466fn default_primary_agent() -> String {
467 DEFAULT_PRIMARY_AGENT_NAME.to_string()
468}