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