diff --git a/src/config.rs b/src/config.rs index 1d5c2c3..39c69ab 100644 --- a/src/config.rs +++ b/src/config.rs @@ -219,19 +219,19 @@ pub fn reload() -> bool { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppConfig { - /// Credentials for the single model backend. + /// Named model endpoints — credentials, base URL, and model id bundled + /// into one entry per backend. Keyed by name, selected by + /// `default_backend` or by `--model ` on the CLI. #[serde(default)] - pub backend: BackendConfig, + pub backends: HashMap, + #[serde(default)] + pub default_backend: String, pub debug: bool, pub compaction: CompactionConfig, pub dmn: DmnConfig, #[serde(default)] pub learn: LearnConfig, #[serde(default)] - pub models: HashMap, - #[serde(default = "default_model_name")] - pub default_model: String, - #[serde(default)] pub mcp_servers: Vec, #[serde(default)] pub lsp_servers: Vec, @@ -257,10 +257,17 @@ pub struct LspServerConfig { #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct BackendConfig { + /// API key for the backend. #[serde(default)] pub api_key: String, + /// Base URL for the backend's OpenAI-compatible endpoint. #[serde(default, skip_serializing_if = "Option::is_none")] pub base_url: Option, + /// Model identifier sent to the API. + pub model_id: String, + /// Context window size in tokens. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub context_window: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -298,19 +305,11 @@ impl Default for LearnConfig { } } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ModelConfig { - /// Model identifier sent to the API. - pub model_id: String, - /// Context window size in tokens. - #[serde(default)] - pub context_window: Option, -} - impl Default for AppConfig { fn default() -> Self { Self { - backend: BackendConfig::default(), + backends: HashMap::new(), + default_backend: String::new(), debug: false, compaction: CompactionConfig { hard_threshold_pct: 90, @@ -318,16 +317,12 @@ impl Default for AppConfig { }, dmn: DmnConfig { max_turns: 20 }, learn: LearnConfig::default(), - models: HashMap::new(), - default_model: String::new(), mcp_servers: Vec::new(), lsp_servers: Vec::new(), } } } -fn default_model_name() -> String { String::new() } - /// Resolved, ready-to-use agent session config. pub struct SessionConfig { pub api_base: String, @@ -352,17 +347,17 @@ pub struct ResolvedModel { } impl AppConfig { - /// Resolve the active model and assemble prompts into a SessionConfig. + /// Resolve the active backend and assemble prompts into a SessionConfig. pub async fn resolve(&self, cli: &crate::user::CliArgs) -> Result { - if self.models.is_empty() { + if self.backends.is_empty() { anyhow::bail!( - "no models configured in {}. Add a `models` section with at least one entry.", + "no backends configured in {}. Add a `backends` section with at least one entry.", config_path().display() ); } - let model_name = cli.model.as_deref().unwrap_or(&self.default_model); - let resolved = self.resolve_model(model_name)?; + let name = cli.model.as_deref().unwrap_or(&self.default_backend); + let resolved = self.resolve_model(name)?; let personality_nodes = get().personality_nodes.clone(); let context_parts = crate::mind::identity::personality_nodes(&personality_nodes).await; @@ -387,34 +382,33 @@ impl AppConfig { }) } - /// Look up a named model and resolve its credentials from the backend config. + /// Look up a named backend and resolve its credentials. pub fn resolve_model(&self, name: &str) -> Result { - let model = self.models.get(name) + let b = self.backends.get(name) .ok_or_else(|| anyhow::anyhow!( - "Unknown model '{}'. Available: {}", + "Unknown backend '{}'. Available: {}", name, self.model_names().join(", "), ))?; - let api_base = self.backend.base_url.clone() + let api_base = b.base_url.clone() .ok_or_else(|| anyhow::anyhow!( - "backend.base_url not set in {}", - config_path().display() + "backends.{}.base_url not set in {}", + name, config_path().display() ))?; - let api_key = self.backend.api_key.clone(); Ok(ResolvedModel { name: name.to_string(), api_base, - api_key, - model_id: model.model_id.clone(), - context_window: model.context_window, + api_key: b.api_key.clone(), + model_id: b.model_id.clone(), + context_window: b.context_window, }) } - /// List available model names, sorted. + /// List available backend names, sorted. pub fn model_names(&self) -> Vec { - let mut names: Vec<_> = self.models.keys().cloned().collect(); + let mut names: Vec<_> = self.backends.keys().cloned().collect(); names.sort(); names } @@ -456,8 +450,6 @@ fn build_figment(cli: &crate::user::CliArgs) -> Figment { let mut f = Figment::from(Serialized::defaults(AppConfig::default())) .merge(Json5File(config_path())); - merge_opt!(f, cli.api_key, "backend.api_key"); - merge_opt!(f, cli.api_base, "backend.base_url"); merge_opt!(f, cli.dmn_max_turns, "dmn.max_turns"); if cli.debug { f = f.merge(Serialized::default("debug", true)); @@ -532,24 +524,26 @@ pub fn show_config(app: &AppConfig, figment: &Figment) { } println!("# Effective configuration\n"); - println!("backend:"); - println!(" api_key: {} ({})", mask(&app.backend.api_key), src(figment, "backend.api_key")); - if let Some(ref url) = app.backend.base_url { - println!(" base_url: {:?} ({})", url, src(figment, "backend.base_url")); - } - println!("\ndebug: {} ({})", app.debug, src(figment, "debug")); + println!("debug: {} ({})", app.debug, src(figment, "debug")); println!("\ncompaction:"); println!(" hard_threshold_pct: {} ({})", app.compaction.hard_threshold_pct, src(figment, "compaction.hard_threshold_pct")); println!(" soft_threshold_pct: {} ({})", app.compaction.soft_threshold_pct, src(figment, "compaction.soft_threshold_pct")); println!("\ndmn:"); println!(" max_turns: {} ({})", app.dmn.max_turns, src(figment, "dmn.max_turns")); - println!("\ndefault_model: {:?}", app.default_model); - if !app.models.is_empty() { - println!("\nmodels:"); - for (name, m) in &app.models { + println!("\ndefault_backend: {:?} ({})", app.default_backend, src(figment, "default_backend")); + if !app.backends.is_empty() { + println!("\nbackends:"); + let mut names: Vec<_> = app.backends.keys().cloned().collect(); + names.sort(); + for name in names { + let b = &app.backends[&name]; println!(" {}:", name); - println!(" model_id: {:?}", m.model_id); - if let Some(cw) = m.context_window { + println!(" api_key: {} ({})", mask(&b.api_key), src(figment, &format!("backends.{name}.api_key"))); + if let Some(ref url) = b.base_url { + println!(" base_url: {:?} ({})", url, src(figment, &format!("backends.{name}.base_url"))); + } + println!(" model_id: {:?}", b.model_id); + if let Some(cw) = b.context_window { println!(" context_window: {}", cw); } }