From 60de5793054e1dbf93f8cab38dd69415f4674910 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Thu, 16 Apr 2026 16:02:43 -0400 Subject: [PATCH] config: unify subconscious API resolution with the main chat path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two parallel backend-resolution paths had drifted apart: - Main chat: AppConfig::resolve_model() → a named BackendConfig in AppConfig.backends - Subconscious / oneshot / context_window(): four skip-serde "cache" fields on Config (memory section) — api_base_url, api_key, api_model, api_context_window — that used to be populated at Config::try_load_shared time by walking memory.agent_model → root.models[name] → root[backend_name] When we renamed `models` to `backends` and collapsed ModelConfig into BackendConfig, the latter chain started silently dereferencing `root.get("models")` → None → no population. Subconscious agents fell through the "API not configured" guard; context_window() started returning 0 (since api_context_window default is u64's 0 now that we don't populate it). It was only visibly working for the main chat. Collapse to one path: - Drop Config.agent_model (duplicate of AppConfig.default_backend) - Drop Config.{api_base_url, api_key, api_model, api_context_window} — no longer populated, no longer needed - Drop default_context_window() — nobody reads the field anymore - Drop the memory-side resolution block in try_load_shared() - Subconscious (mind/unconscious.rs) and oneshot (agent/oneshot.rs) now call load_app() + resolve_model(&app.default_backend) just like the main chat does - context_window() reads from config::app().backends[default_backend] .context_window, defaulting to 128k only if the backend doesn't specify one Side effect: Kent's config file drops agent_model, api_reasoning, journal_days, journal_max — all fields whose Rust counterparts are now gone. (Figment tolerates unknown fields, so leaving them wouldn't have broken anything, but they were lying about what's configurable.) Co-Authored-By: Proof of Concept --- src/agent/context.rs | 5 ++++- src/agent/oneshot.rs | 15 +++++---------- src/config.rs | 38 +------------------------------------- src/mind/unconscious.rs | 23 +++++++++++------------ 4 files changed, 21 insertions(+), 60 deletions(-) diff --git a/src/agent/context.rs b/src/agent/context.rs index cc8044a..5b51c24 100644 --- a/src/agent/context.rs +++ b/src/agent/context.rs @@ -992,7 +992,10 @@ impl ContextState { } pub fn context_window() -> usize { - crate::config::get().api_context_window + let app = crate::config::app(); + app.backends.get(&app.default_backend) + .and_then(|b| b.context_window) + .unwrap_or(128_000) } pub fn context_budget_tokens() -> usize { diff --git a/src/agent/oneshot.rs b/src/agent/oneshot.rs index 588a786..1c5ac90 100644 --- a/src/agent/oneshot.rs +++ b/src/agent/oneshot.rs @@ -247,19 +247,14 @@ impl AutoAgent { &mut self, bail_fn: Option<&(dyn Fn(usize) -> Result<(), String> + Sync)>, ) -> Result<(), String> { - let config = crate::config::get(); - let base_url = config.api_base_url.as_deref().unwrap_or(""); - let api_key = config.api_key.as_deref().unwrap_or(""); - let model = config.api_model.as_deref().unwrap_or(""); - if base_url.is_empty() || model.is_empty() { - return Err("API not configured (no base_url or model)".to_string()); - } - let client = super::api::ApiClient::new(base_url, api_key, model); - - // Load system prompt + identity from config + // Load system prompt + identity from config. let cli = crate::user::CliArgs::default(); let (app, _) = crate::config::load_app(&cli) .map_err(|e| format!("config: {}", e))?; + let resolved = app.resolve_model(&app.default_backend) + .map_err(|e| format!("API not configured: {}", e))?; + let client = super::api::ApiClient::new( + &resolved.api_base, &resolved.api_key, &resolved.model_id); let personality = crate::config::reload_context() .await.map_err(|e| format!("config: {}", e))?; diff --git a/src/config.rs b/src/config.rs index 4f50947..5b1726b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -26,7 +26,6 @@ pub fn config_path() -> PathBuf { static CONFIG: OnceLock>> = OnceLock::new(); -fn default_context_window() -> usize { 128_000 } fn default_stream_timeout() -> u64 { 60 } fn default_scoring_interval_secs() -> u64 { 3600 } // 1 hour fn default_scoring_response_window() -> usize { 100 } @@ -60,18 +59,6 @@ pub struct Config { pub agent_nodes: Vec, pub llm_concurrency: usize, pub agent_budget: usize, - /// Resolved from agent_model → models → backend (not in config directly) - #[serde(skip)] - pub api_base_url: Option, - #[serde(skip)] - pub api_key: Option, - #[serde(skip)] - pub api_model: Option, - #[serde(skip, default = "default_context_window")] - pub api_context_window: usize, - /// Used to resolve API settings, not stored on Config - #[serde(default)] - agent_model: Option, /// Stream chunk timeout in seconds (no data = timeout). #[serde(default = "default_stream_timeout")] pub api_stream_timeout_secs: u64, @@ -115,14 +102,9 @@ impl Default for Config { agent_nodes: vec!["identity".into(), "core-practices".into()], llm_concurrency: 1, agent_budget: 1000, - api_base_url: None, - api_key: None, - api_model: None, - api_context_window: default_context_window(), api_stream_timeout_secs: default_stream_timeout(), scoring_interval_secs: default_scoring_interval_secs(), scoring_response_window: default_scoring_response_window(), - agent_model: None, agent_types: vec![ "linker".into(), "organize".into(), "distill".into(), "separator".into(), "split".into(), @@ -153,25 +135,7 @@ impl Config { let mut config: Config = serde_json::from_value(mem_value.clone()).ok()?; config.llm_concurrency = config.llm_concurrency.max(1); - // Resolve API settings: agent_model → models → backend - if let Some(model_name) = &config.agent_model - && let Some(model_cfg) = root.get("models").and_then(|m| m.get(model_name.as_str())) { - let backend_name = model_cfg.get("backend").and_then(|v| v.as_str()).unwrap_or(""); - let model_id = model_cfg.get("model_id").and_then(|v| v.as_str()).unwrap_or(""); - - if let Some(backend) = root.get(backend_name) { - config.api_base_url = backend.get("base_url") - .and_then(|v| v.as_str()).map(String::from); - config.api_key = backend.get("api_key") - .and_then(|v| v.as_str()).map(String::from); - } - config.api_model = Some(model_id.to_string()); - if let Some(cw) = model_cfg.get("context_window").and_then(|v| v.as_u64()) { - config.api_context_window = cw as usize; - } - } - - // Top-level config sections (not inside "memory") + // Top-level sections (not inside "memory"). if let Some(servers) = root.get("lsp_servers") { config.lsp_servers = serde_json::from_value(servers.clone()).unwrap_or_default(); } diff --git a/src/mind/unconscious.rs b/src/mind/unconscious.rs index d8a6aad..4f9a0ca 100644 --- a/src/mind/unconscious.rs +++ b/src/mind/unconscious.rs @@ -275,17 +275,7 @@ pub async fn prepare_spawn(name: &str, mut auto: AutoAgent, wake: std::sync::Arc phase: s.phase.clone(), }).collect()); - // Create standalone Agent — stored so UI can read context - let config = crate::config::get(); - let base_url = config.api_base_url.as_deref().unwrap_or(""); - let api_key = config.api_key.as_deref().unwrap_or(""); - let model = config.api_model.as_deref().unwrap_or(""); - if base_url.is_empty() || model.is_empty() { - dbglog!("[unconscious] API not configured"); - auto.steps = orig_steps; - return Err(auto); - } - + // Create standalone Agent — stored so UI can read context. let cli = crate::user::CliArgs::default(); let (app, _) = match crate::config::load_app(&cli) { Ok(r) => r, @@ -295,9 +285,18 @@ pub async fn prepare_spawn(name: &str, mut auto: AutoAgent, wake: std::sync::Arc return Err(auto); } }; + let resolved = match app.resolve_model(&app.default_backend) { + Ok(r) => r, + Err(e) => { + dbglog!("[unconscious] API not configured: {}", e); + auto.steps = orig_steps; + return Err(auto); + } + }; // Unconscious agents have self-contained prompts — no standard context. - let client = crate::agent::api::ApiClient::new(base_url, api_key, model); + let client = crate::agent::api::ApiClient::new( + &resolved.api_base, &resolved.api_key, &resolved.model_id); let agent = crate::agent::Agent::new( client, Vec::new(), app, None,