use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::str::FromStr;
use crate::acp::AgentClientProtocolConfig;
use crate::codex::{FileOpener, HistoryConfig, TuiConfig};
use crate::constants::defaults::DEFAULT_PRIMARY_AGENT_NAME;
use crate::context::ContextFeaturesConfig;
use crate::core::{
AgentConfig, AgentPermissionsConfig, AnthropicConfig, AuthConfig, AutomationConfig, CommandsConfig,
CustomProviderConfig, DotfileProtectionConfig, ModelConfig, OpenAIConfig, PermissionsConfig, PromptCachingConfig,
ProviderOverrideConfig, SandboxConfig, SecurityConfig, SkillsConfig, ToolsConfig,
};
use crate::debug::DebugConfig;
use crate::hooks::HooksConfig;
use crate::mcp::McpClientConfig;
use crate::models::{MiMoAuthMethod, Provider};
use crate::optimization::OptimizationConfig;
use crate::output_styles::OutputStyleConfig;
use crate::root::{ChatConfig, PtyConfig, UiConfig};
use crate::subagents::SubagentRuntimeLimits;
use crate::telemetry::TelemetryConfig;
use crate::timeouts::TimeoutsConfig;
use crate::webmcp::WebmcpConfig;
use crate::loader::syntax_highlighting::SyntaxHighlightingConfig;
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct ProviderConfig {
#[serde(default)]
pub openai: OpenAIConfig,
#[serde(default)]
pub anthropic: AnthropicConfig,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mimo_auth_method: Option<MiMoAuthMethod>,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
pub struct FeaturesConfig {
#[serde(default)]
pub memories: bool,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct WorkspaceConfig {
#[serde(default)]
pub(crate) use_root_config: bool,
#[serde(default = "default_true")]
pub(crate) include_context: bool,
#[serde(default)]
pub(crate) max_context_size: Option<usize>,
}
impl Default for WorkspaceConfig {
fn default() -> Self {
Self {
use_root_config: false,
include_context: true,
max_context_size: None,
}
}
}
fn default_true() -> bool {
true
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct VTCodeConfig {
#[serde(default = "default_primary_agent")]
pub default_primary_agent: String,
#[serde(default)]
pub features: FeaturesConfig,
#[serde(default)]
pub file_opener: FileOpener,
#[serde(default)]
pub notify: Vec<String>,
#[serde(default)]
pub history: HistoryConfig,
#[serde(default)]
pub tui: TuiConfig,
#[serde(default)]
pub agent: AgentConfig,
#[serde(default)]
pub auth: AuthConfig,
#[serde(default)]
pub tools: ToolsConfig,
#[serde(default)]
pub commands: CommandsConfig,
#[serde(default)]
pub permissions: PermissionsConfig,
#[serde(skip)]
#[cfg_attr(feature = "schema", schemars(skip))]
pub runtime_agent_permissions: Option<AgentPermissionsConfig>,
#[serde(skip)]
#[cfg_attr(feature = "schema", schemars(skip))]
pub workspace_lifecycle_hooks: Option<crate::hooks::WorkspaceLifecycleHooks>,
#[serde(default)]
pub security: SecurityConfig,
#[serde(default)]
pub sandbox: SandboxConfig,
#[serde(default)]
pub ui: UiConfig,
#[serde(default)]
pub chat: ChatConfig,
#[serde(default)]
pub pty: PtyConfig,
#[serde(default)]
pub debug: DebugConfig,
#[serde(default)]
pub context: ContextFeaturesConfig,
#[serde(default)]
pub telemetry: TelemetryConfig,
#[serde(default)]
pub optimization: OptimizationConfig,
#[serde(default)]
pub syntax_highlighting: SyntaxHighlightingConfig,
#[serde(default)]
pub timeouts: TimeoutsConfig,
#[serde(default)]
pub automation: AutomationConfig,
#[serde(default)]
pub subagents: SubagentRuntimeLimits,
#[serde(default)]
pub prompt_cache: PromptCachingConfig,
#[serde(default)]
pub mcp: McpClientConfig,
#[serde(default)]
pub webmcp: WebmcpConfig,
#[serde(default)]
pub acp: AgentClientProtocolConfig,
#[serde(default)]
pub hooks: HooksConfig,
#[serde(default)]
pub model: ModelConfig,
#[serde(default)]
pub provider: ProviderConfig,
#[serde(default)]
pub skills: SkillsConfig,
#[serde(default)]
pub custom_providers: Vec<CustomProviderConfig>,
#[serde(default)]
pub provider_overrides: BTreeMap<String, ProviderOverrideConfig>,
#[serde(default)]
pub providers_whitelist: Vec<String>,
#[serde(default)]
pub output_style: OutputStyleConfig,
#[serde(default)]
pub dotfile_protection: DotfileProtectionConfig,
#[serde(default)]
pub workspace: WorkspaceConfig,
}
impl Default for VTCodeConfig {
fn default() -> Self {
Self {
default_primary_agent: default_primary_agent(),
features: FeaturesConfig::default(),
file_opener: FileOpener::default(),
notify: Vec::new(),
history: HistoryConfig::default(),
tui: TuiConfig::default(),
agent: AgentConfig::default(),
auth: AuthConfig::default(),
tools: ToolsConfig::default(),
commands: CommandsConfig::default(),
permissions: PermissionsConfig::default(),
runtime_agent_permissions: None,
workspace_lifecycle_hooks: None,
security: SecurityConfig::default(),
sandbox: SandboxConfig::default(),
ui: UiConfig::default(),
chat: ChatConfig::default(),
pty: PtyConfig::default(),
debug: DebugConfig::default(),
context: ContextFeaturesConfig::default(),
telemetry: TelemetryConfig::default(),
optimization: OptimizationConfig::default(),
syntax_highlighting: SyntaxHighlightingConfig::default(),
timeouts: TimeoutsConfig::default(),
automation: AutomationConfig::default(),
subagents: SubagentRuntimeLimits::default(),
prompt_cache: PromptCachingConfig::default(),
mcp: McpClientConfig::default(),
webmcp: WebmcpConfig::default(),
acp: AgentClientProtocolConfig::default(),
hooks: HooksConfig::default(),
model: ModelConfig::default(),
provider: ProviderConfig::default(),
skills: SkillsConfig::default(),
custom_providers: Vec::new(),
provider_overrides: BTreeMap::new(),
providers_whitelist: Vec::new(),
output_style: OutputStyleConfig::default(),
dotfile_protection: DotfileProtectionConfig::default(),
workspace: WorkspaceConfig::default(),
}
}
}
impl VTCodeConfig {
pub fn validate(&self) -> Result<()> {
self.syntax_highlighting
.validate()
.context("Invalid syntax_highlighting configuration")?;
self.context.validate().context("Invalid context configuration")?;
self.hooks.validate().context("Invalid hooks configuration")?;
self.timeouts.validate().context("Invalid timeouts configuration")?;
self.prompt_cache.validate().context("Invalid prompt_cache configuration")?;
self.webmcp.validate().context("Invalid webmcp configuration")?;
self.agent
.validate_llm_params()
.map_err(anyhow::Error::msg)
.context("Invalid agent configuration")?;
self.ui
.keyboard_protocol
.validate()
.context("Invalid keyboard_protocol configuration")?;
self.pty.validate().context("Invalid pty configuration")?;
let mut seen_names = std::collections::HashSet::new();
for cp in &self.custom_providers {
cp.validate()
.map_err(|msg| anyhow::anyhow!(msg))
.context("Invalid custom_providers configuration")?;
if !seen_names.insert(cp.name.to_lowercase()) {
anyhow::bail!("custom_providers: duplicate name `{}`", cp.name);
}
}
for (provider_name, override_config) in &self.provider_overrides {
override_config
.validate(provider_name)
.map_err(|msg| anyhow::anyhow!(msg))
.context("Invalid provider_overrides configuration")?;
if Provider::from_str(provider_name).is_err() {
anyhow::bail!(
"provider_overrides: unknown provider `{provider_name}`; \
must be one of: gemini, openai, anthropic, copilot, deepseek, meta, \
openrouter, ollama, lmstudio, llamacpp, moonshot, zai, minimax, \
mimo, mistral, huggingface, opencodezen, opencodego, qwen, \
stepfun, evolink, poolside, nvidia, merge-gateway, vercel"
);
}
}
for entry in &self.providers_whitelist {
let known = Provider::from_str(entry).is_ok() || self.custom_providers.iter().any(|cp| cp.name == *entry);
if !known {
anyhow::bail!(
"providers_whitelist: unknown provider `{entry}`; \
must be a built-in provider or a name from custom_providers"
);
}
}
Ok(())
}
#[must_use]
pub fn persistent_memory_enabled(&self) -> bool {
self.features.memories && self.agent.persistent_memory.enabled
}
#[must_use]
pub fn memories_enabled(&self) -> bool {
self.persistent_memory_enabled() && self.agent.persistent_memory.memories.use_memories
}
#[must_use]
pub fn should_generate_memories(&self) -> bool {
self.persistent_memory_enabled() && self.agent.persistent_memory.memories.generate_memories
}
pub fn custom_provider(&self, name: &str) -> Option<&CustomProviderConfig> {
let lower = name.to_lowercase();
self.custom_providers.iter().find(|cp| cp.name.to_lowercase() == lower)
}
pub fn configured_api_key_env(&self, name: &str) -> Option<String> {
if let Some(custom_provider) = self.custom_provider(name) {
return Some(custom_provider.resolved_api_key_env());
}
self.provider_overrides
.iter()
.find(|(provider, _)| provider.eq_ignore_ascii_case(name))
.and_then(|(_, provider_override)| provider_override.api_key_env.clone())
.filter(|api_key_env| !api_key_env.trim().is_empty())
}
pub fn provider_display_name(&self, provider_key: &str) -> String {
if let Some(cp) = self.custom_provider(provider_key) {
cp.display_name.clone()
} else if provider_key.eq_ignore_ascii_case("codex") {
"Codex".to_string()
} else if let Ok(p) = FromStr::from_str(provider_key) {
let p: Provider = p;
p.label().to_string()
} else {
provider_key.to_string()
}
}
}
fn default_primary_agent() -> String {
DEFAULT_PRIMARY_AGENT_NAME.to_string()
}