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, DotfileProtectionConfig, ModelConfig, OpenAIConfig, PermissionsConfig, PromptCachingConfig,
13 ProviderOverrideConfig, SandboxConfig, SecurityConfig, SkillsConfig, ToolsConfig,
14};
15use crate::debug::DebugConfig;
16use crate::hooks::HooksConfig;
17use crate::mcp::McpClientConfig;
18use crate::models::{MiMoAuthMethod, Provider};
19use crate::optimization::OptimizationConfig;
20use crate::output_styles::OutputStyleConfig;
21use crate::root::{ChatConfig, PtyConfig, UiConfig};
22use crate::subagents::SubagentRuntimeLimits;
23use crate::telemetry::TelemetryConfig;
24use crate::timeouts::TimeoutsConfig;
25use crate::webmcp::WebmcpConfig;
26
27use crate::loader::syntax_highlighting::SyntaxHighlightingConfig;
28
29#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
31#[derive(Debug, Clone, Deserialize, Serialize, Default)]
32pub struct ProviderConfig {
33 #[serde(default)]
35 pub openai: OpenAIConfig,
36
37 #[serde(default)]
39 pub anthropic: AnthropicConfig,
40
41 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub mimo_auth_method: Option<MiMoAuthMethod>,
44}
45
46#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
52#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
53pub struct FeaturesConfig {
54 #[serde(default)]
58 pub memories: bool,
59}
60
61#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
68#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
69pub struct WorkspaceConfig {
70 #[serde(default)]
74 pub(crate) use_root_config: bool,
75
76 #[serde(default = "default_true")]
78 pub(crate) include_context: bool,
79
80 #[serde(default)]
82 pub(crate) max_context_size: Option<usize>,
83}
84
85impl Default for WorkspaceConfig {
86 fn default() -> Self {
87 Self {
88 use_root_config: false,
89 include_context: true,
90 max_context_size: None,
91 }
92 }
93}
94
95fn default_true() -> bool {
96 true
97}
98
99#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
101#[derive(Debug, Clone, Deserialize, Serialize)]
102pub struct VTCodeConfig {
103 #[serde(default = "default_primary_agent")]
105 pub default_primary_agent: String,
106
107 #[serde(default)]
109 pub features: FeaturesConfig,
110
111 #[serde(default)]
113 pub file_opener: FileOpener,
114
115 #[serde(default)]
117 pub notify: Vec<String>,
118
119 #[serde(default)]
121 pub history: HistoryConfig,
122
123 #[serde(default)]
125 pub tui: TuiConfig,
126
127 #[serde(default)]
129 pub agent: AgentConfig,
130
131 #[serde(default)]
133 pub auth: AuthConfig,
134
135 #[serde(default)]
137 pub tools: ToolsConfig,
138
139 #[serde(default)]
141 pub commands: CommandsConfig,
142
143 #[serde(default)]
145 pub permissions: PermissionsConfig,
146
147 #[serde(skip)]
149 #[cfg_attr(feature = "schema", schemars(skip))]
150 pub runtime_agent_permissions: Option<AgentPermissionsConfig>,
151
152 #[serde(skip)]
157 #[cfg_attr(feature = "schema", schemars(skip))]
158 pub workspace_lifecycle_hooks: Option<crate::hooks::WorkspaceLifecycleHooks>,
159
160 #[serde(default)]
162 pub security: SecurityConfig,
163
164 #[serde(default)]
166 pub sandbox: SandboxConfig,
167
168 #[serde(default)]
170 pub ui: UiConfig,
171
172 #[serde(default)]
174 pub chat: ChatConfig,
175
176 #[serde(default)]
178 pub pty: PtyConfig,
179
180 #[serde(default)]
182 pub debug: DebugConfig,
183
184 #[serde(default)]
186 pub context: ContextFeaturesConfig,
187
188 #[serde(default)]
190 pub telemetry: TelemetryConfig,
191
192 #[serde(default)]
194 pub optimization: OptimizationConfig,
195
196 #[serde(default)]
198 pub syntax_highlighting: SyntaxHighlightingConfig,
199
200 #[serde(default)]
202 pub timeouts: TimeoutsConfig,
203
204 #[serde(default)]
206 pub automation: AutomationConfig,
207
208 #[serde(default)]
210 pub subagents: SubagentRuntimeLimits,
211
212 #[serde(default)]
214 pub prompt_cache: PromptCachingConfig,
215
216 #[serde(default)]
218 pub mcp: McpClientConfig,
219
220 #[serde(default)]
222 pub webmcp: WebmcpConfig,
223
224 #[serde(default)]
226 pub acp: AgentClientProtocolConfig,
227
228 #[serde(default)]
230 pub hooks: HooksConfig,
231
232 #[serde(default)]
234 pub model: ModelConfig,
235
236 #[serde(default)]
238 pub provider: ProviderConfig,
239
240 #[serde(default)]
242 pub skills: SkillsConfig,
243
244 #[serde(default)]
247 pub custom_providers: Vec<CustomProviderConfig>,
248
249 #[serde(default)]
255 pub provider_overrides: BTreeMap<String, ProviderOverrideConfig>,
256
257 #[serde(default)]
263 pub providers_whitelist: Vec<String>,
264
265 #[serde(default)]
267 pub output_style: OutputStyleConfig,
268
269 #[serde(default)]
271 pub dotfile_protection: DotfileProtectionConfig,
272
273 #[serde(default)]
275 pub workspace: WorkspaceConfig,
276}
277
278impl Default for VTCodeConfig {
279 fn default() -> Self {
280 Self {
281 default_primary_agent: default_primary_agent(),
282 features: FeaturesConfig::default(),
283 file_opener: FileOpener::default(),
284 notify: Vec::new(),
285 history: HistoryConfig::default(),
286 tui: TuiConfig::default(),
287 agent: AgentConfig::default(),
288 auth: AuthConfig::default(),
289 tools: ToolsConfig::default(),
290 commands: CommandsConfig::default(),
291 permissions: PermissionsConfig::default(),
292 runtime_agent_permissions: None,
293 workspace_lifecycle_hooks: None,
294 security: SecurityConfig::default(),
295 sandbox: SandboxConfig::default(),
296 ui: UiConfig::default(),
297 chat: ChatConfig::default(),
298 pty: PtyConfig::default(),
299 debug: DebugConfig::default(),
300 context: ContextFeaturesConfig::default(),
301 telemetry: TelemetryConfig::default(),
302 optimization: OptimizationConfig::default(),
303 syntax_highlighting: SyntaxHighlightingConfig::default(),
304 timeouts: TimeoutsConfig::default(),
305 automation: AutomationConfig::default(),
306 subagents: SubagentRuntimeLimits::default(),
307 prompt_cache: PromptCachingConfig::default(),
308 mcp: McpClientConfig::default(),
309 webmcp: WebmcpConfig::default(),
310 acp: AgentClientProtocolConfig::default(),
311 hooks: HooksConfig::default(),
312 model: ModelConfig::default(),
313 provider: ProviderConfig::default(),
314 skills: SkillsConfig::default(),
315 custom_providers: Vec::new(),
316 provider_overrides: BTreeMap::new(),
317 providers_whitelist: Vec::new(),
318 output_style: OutputStyleConfig::default(),
319 dotfile_protection: DotfileProtectionConfig::default(),
320 workspace: WorkspaceConfig::default(),
321 }
322 }
323}
324
325impl VTCodeConfig {
326 pub fn validate(&self) -> Result<()> {
327 self.syntax_highlighting
328 .validate()
329 .context("Invalid syntax_highlighting configuration")?;
330
331 self.context.validate().context("Invalid context configuration")?;
332
333 self.hooks.validate().context("Invalid hooks configuration")?;
334
335 self.timeouts.validate().context("Invalid timeouts configuration")?;
336
337 self.prompt_cache.validate().context("Invalid prompt_cache configuration")?;
338
339 self.webmcp.validate().context("Invalid webmcp configuration")?;
340
341 self.agent
342 .validate_llm_params()
343 .map_err(anyhow::Error::msg)
344 .context("Invalid agent configuration")?;
345
346 self.ui
347 .keyboard_protocol
348 .validate()
349 .context("Invalid keyboard_protocol configuration")?;
350
351 self.pty.validate().context("Invalid pty configuration")?;
352
353 let mut seen_names = std::collections::HashSet::new();
355 for cp in &self.custom_providers {
356 cp.validate()
357 .map_err(|msg| anyhow::anyhow!(msg))
358 .context("Invalid custom_providers configuration")?;
359 if !seen_names.insert(cp.name.to_lowercase()) {
360 anyhow::bail!("custom_providers: duplicate name `{}`", cp.name);
361 }
362 }
363
364 for (provider_name, override_config) in &self.provider_overrides {
366 override_config
367 .validate(provider_name)
368 .map_err(|msg| anyhow::anyhow!(msg))
369 .context("Invalid provider_overrides configuration")?;
370 if Provider::from_str(provider_name).is_err() {
372 anyhow::bail!(
373 "provider_overrides: unknown provider `{provider_name}`; \
374 must be one of: gemini, openai, anthropic, copilot, deepseek, meta, \
375 openrouter, ollama, lmstudio, llamacpp, moonshot, zai, minimax, \
376 mimo, mistral, huggingface, opencodezen, opencodego, qwen, \
377 stepfun, evolink, poolside, nvidia, merge-gateway, vercel"
378 );
379 }
380 }
381
382 for entry in &self.providers_whitelist {
384 let known = Provider::from_str(entry).is_ok() || self.custom_providers.iter().any(|cp| cp.name == *entry);
385 if !known {
386 anyhow::bail!(
387 "providers_whitelist: unknown provider `{entry}`; \
388 must be a built-in provider or a name from custom_providers"
389 );
390 }
391 }
392
393 Ok(())
394 }
395
396 #[must_use]
401 pub fn persistent_memory_enabled(&self) -> bool {
402 self.features.memories && self.agent.persistent_memory.enabled
403 }
404
405 #[must_use]
410 pub fn memories_enabled(&self) -> bool {
411 self.persistent_memory_enabled() && self.agent.persistent_memory.memories.use_memories
412 }
413
414 #[must_use]
416 pub fn should_generate_memories(&self) -> bool {
417 self.persistent_memory_enabled() && self.agent.persistent_memory.memories.generate_memories
418 }
419
420 pub fn custom_provider(&self, name: &str) -> Option<&CustomProviderConfig> {
422 let lower = name.to_lowercase();
423 self.custom_providers.iter().find(|cp| cp.name.to_lowercase() == lower)
424 }
425
426 pub fn configured_api_key_env(&self, name: &str) -> Option<String> {
432 if let Some(custom_provider) = self.custom_provider(name) {
433 return Some(custom_provider.resolved_api_key_env());
434 }
435
436 self.provider_overrides
437 .iter()
438 .find(|(provider, _)| provider.eq_ignore_ascii_case(name))
439 .and_then(|(_, provider_override)| provider_override.api_key_env.clone())
440 .filter(|api_key_env| !api_key_env.trim().is_empty())
441 }
442
443 pub fn provider_display_name(&self, provider_key: &str) -> String {
446 if let Some(cp) = self.custom_provider(provider_key) {
447 cp.display_name.clone()
448 } else if provider_key.eq_ignore_ascii_case("codex") {
449 "Codex".to_string()
450 } else if let Ok(p) = FromStr::from_str(provider_key) {
451 let p: Provider = p;
452 p.label().to_string()
453 } else {
454 provider_key.to_string()
455 }
456 }
457}
458
459fn default_primary_agent() -> String {
460 DEFAULT_PRIMARY_AGENT_NAME.to_string()
461}