config: merge ModelConfig into BackendConfig, keyed by name

AppConfig had one BackendConfig for credentials and a separate
HashMap<String, ModelConfig> for named model entries. In practice each
named model was always paired with exactly one backend's credentials
— the split bought nothing except an extra struct and the awkward
two-lookup shape in resolve_model (find model → get backend creds →
combine).

Merge them: BackendConfig now carries api_key, base_url, model_id,
and context_window. AppConfig has a single
HashMap<String, BackendConfig> backends map and a default_backend
name. resolve_model is one lookup.

ModelConfig struct deleted. default_model renamed to default_backend.
Config shape changes from

    backend: { api_key, base_url }
    models: { "27b": { model_id, context_window } }
    default_model: "27b"

to

    backends: { "27b": { api_key, base_url, model_id, context_window } }
    default_backend: "27b"

Updated ~/.consciousness/config.json5 to match.

One small side effect: dropped the --api-key / --api-base figment
merge-opts for "backend.*" targets — those would need to know which
backend to target now and there's no sensible default. The CLI flags
still function as post-resolution overrides on the eventual
SessionConfig.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-16 15:49:53 -04:00
parent 2989a6afaa
commit 3e05331608

View file

@ -219,19 +219,19 @@ pub fn reload() -> bool {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig { 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 <name>` on the CLI.
#[serde(default)] #[serde(default)]
pub backend: BackendConfig, pub backends: HashMap<String, BackendConfig>,
#[serde(default)]
pub default_backend: String,
pub debug: bool, pub debug: bool,
pub compaction: CompactionConfig, pub compaction: CompactionConfig,
pub dmn: DmnConfig, pub dmn: DmnConfig,
#[serde(default)] #[serde(default)]
pub learn: LearnConfig, pub learn: LearnConfig,
#[serde(default)] #[serde(default)]
pub models: HashMap<String, ModelConfig>,
#[serde(default = "default_model_name")]
pub default_model: String,
#[serde(default)]
pub mcp_servers: Vec<McpServerConfig>, pub mcp_servers: Vec<McpServerConfig>,
#[serde(default)] #[serde(default)]
pub lsp_servers: Vec<LspServerConfig>, pub lsp_servers: Vec<LspServerConfig>,
@ -257,10 +257,17 @@ pub struct LspServerConfig {
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BackendConfig { pub struct BackendConfig {
/// API key for the backend.
#[serde(default)] #[serde(default)]
pub api_key: String, pub api_key: String,
/// Base URL for the backend's OpenAI-compatible endpoint.
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>, pub base_url: Option<String>,
/// 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<usize>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[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<usize>,
}
impl Default for AppConfig { impl Default for AppConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
backend: BackendConfig::default(), backends: HashMap::new(),
default_backend: String::new(),
debug: false, debug: false,
compaction: CompactionConfig { compaction: CompactionConfig {
hard_threshold_pct: 90, hard_threshold_pct: 90,
@ -318,16 +317,12 @@ impl Default for AppConfig {
}, },
dmn: DmnConfig { max_turns: 20 }, dmn: DmnConfig { max_turns: 20 },
learn: LearnConfig::default(), learn: LearnConfig::default(),
models: HashMap::new(),
default_model: String::new(),
mcp_servers: Vec::new(), mcp_servers: Vec::new(),
lsp_servers: Vec::new(), lsp_servers: Vec::new(),
} }
} }
} }
fn default_model_name() -> String { String::new() }
/// Resolved, ready-to-use agent session config. /// Resolved, ready-to-use agent session config.
pub struct SessionConfig { pub struct SessionConfig {
pub api_base: String, pub api_base: String,
@ -352,17 +347,17 @@ pub struct ResolvedModel {
} }
impl AppConfig { 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<SessionConfig> { pub async fn resolve(&self, cli: &crate::user::CliArgs) -> Result<SessionConfig> {
if self.models.is_empty() { if self.backends.is_empty() {
anyhow::bail!( 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() config_path().display()
); );
} }
let model_name = cli.model.as_deref().unwrap_or(&self.default_model); let name = cli.model.as_deref().unwrap_or(&self.default_backend);
let resolved = self.resolve_model(model_name)?; let resolved = self.resolve_model(name)?;
let personality_nodes = get().personality_nodes.clone(); let personality_nodes = get().personality_nodes.clone();
let context_parts = crate::mind::identity::personality_nodes(&personality_nodes).await; 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<ResolvedModel> { pub fn resolve_model(&self, name: &str) -> Result<ResolvedModel> {
let model = self.models.get(name) let b = self.backends.get(name)
.ok_or_else(|| anyhow::anyhow!( .ok_or_else(|| anyhow::anyhow!(
"Unknown model '{}'. Available: {}", "Unknown backend '{}'. Available: {}",
name, name,
self.model_names().join(", "), self.model_names().join(", "),
))?; ))?;
let api_base = self.backend.base_url.clone() let api_base = b.base_url.clone()
.ok_or_else(|| anyhow::anyhow!( .ok_or_else(|| anyhow::anyhow!(
"backend.base_url not set in {}", "backends.{}.base_url not set in {}",
config_path().display() name, config_path().display()
))?; ))?;
let api_key = self.backend.api_key.clone();
Ok(ResolvedModel { Ok(ResolvedModel {
name: name.to_string(), name: name.to_string(),
api_base, api_base,
api_key, api_key: b.api_key.clone(),
model_id: model.model_id.clone(), model_id: b.model_id.clone(),
context_window: model.context_window, context_window: b.context_window,
}) })
} }
/// List available model names, sorted. /// List available backend names, sorted.
pub fn model_names(&self) -> Vec<String> { pub fn model_names(&self) -> Vec<String> {
let mut names: Vec<_> = self.models.keys().cloned().collect(); let mut names: Vec<_> = self.backends.keys().cloned().collect();
names.sort(); names.sort();
names names
} }
@ -456,8 +450,6 @@ fn build_figment(cli: &crate::user::CliArgs) -> Figment {
let mut f = Figment::from(Serialized::defaults(AppConfig::default())) let mut f = Figment::from(Serialized::defaults(AppConfig::default()))
.merge(Json5File(config_path())); .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"); merge_opt!(f, cli.dmn_max_turns, "dmn.max_turns");
if cli.debug { if cli.debug {
f = f.merge(Serialized::default("debug", true)); f = f.merge(Serialized::default("debug", true));
@ -532,24 +524,26 @@ pub fn show_config(app: &AppConfig, figment: &Figment) {
} }
println!("# Effective configuration\n"); println!("# Effective configuration\n");
println!("backend:"); println!("debug: {} ({})", app.debug, src(figment, "debug"));
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!("\ncompaction:"); println!("\ncompaction:");
println!(" hard_threshold_pct: {} ({})", app.compaction.hard_threshold_pct, src(figment, "compaction.hard_threshold_pct")); 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!(" soft_threshold_pct: {} ({})", app.compaction.soft_threshold_pct, src(figment, "compaction.soft_threshold_pct"));
println!("\ndmn:"); println!("\ndmn:");
println!(" max_turns: {} ({})", app.dmn.max_turns, src(figment, "dmn.max_turns")); println!(" max_turns: {} ({})", app.dmn.max_turns, src(figment, "dmn.max_turns"));
println!("\ndefault_model: {:?}", app.default_model); println!("\ndefault_backend: {:?} ({})", app.default_backend, src(figment, "default_backend"));
if !app.models.is_empty() { if !app.backends.is_empty() {
println!("\nmodels:"); println!("\nbackends:");
for (name, m) in &app.models { let mut names: Vec<_> = app.backends.keys().cloned().collect();
names.sort();
for name in names {
let b = &app.backends[&name];
println!(" {}:", name); println!(" {}:", name);
println!(" model_id: {:?}", m.model_id); println!(" api_key: {} ({})", mask(&b.api_key), src(figment, &format!("backends.{name}.api_key")));
if let Some(cw) = m.context_window { 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); println!(" context_window: {}", cw);
} }
} }