diff --git a/Cargo.lock b/Cargo.lock index 24b90d2..0711e40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4862,7 +4862,7 @@ dependencies = [ [[package]] name = "zesdex-api" -version = "1.19.1" +version = "1.19.2" dependencies = [ "anyhow", "argon2", @@ -4885,7 +4885,7 @@ dependencies = [ [[package]] name = "zesdex-application" -version = "1.19.1" +version = "1.19.2" dependencies = [ "anyhow", "base64", @@ -4902,7 +4902,7 @@ dependencies = [ [[package]] name = "zesdex-bootstrap" -version = "1.19.1" +version = "1.19.2" dependencies = [ "anyhow", "chrono", @@ -4919,7 +4919,7 @@ dependencies = [ [[package]] name = "zesdex-daemon" -version = "1.19.1" +version = "1.19.2" dependencies = [ "anyhow", "base64", @@ -4943,7 +4943,7 @@ dependencies = [ [[package]] name = "zesdex-domain" -version = "1.19.1" +version = "1.19.2" dependencies = [ "anyhow", "base64", @@ -4959,7 +4959,7 @@ dependencies = [ [[package]] name = "zesdex-gateway" -version = "1.19.1" +version = "1.19.2" dependencies = [ "anyhow", "axum", @@ -4986,7 +4986,7 @@ dependencies = [ [[package]] name = "zesdex-grpc" -version = "1.19.1" +version = "1.19.2" dependencies = [ "anyhow", "axum", @@ -5003,7 +5003,7 @@ dependencies = [ [[package]] name = "zesdex-infrastructure" -version = "1.19.1" +version = "1.19.2" dependencies = [ "anyhow", "argon2", @@ -5051,7 +5051,7 @@ dependencies = [ [[package]] name = "zesdex-tui" -version = "1.19.1" +version = "1.19.2" dependencies = [ "anyhow", "base64", @@ -5077,7 +5077,7 @@ dependencies = [ [[package]] name = "zesdex-web" -version = "1.19.1" +version = "1.19.2" dependencies = [ "anyhow", "axum", @@ -5097,7 +5097,7 @@ dependencies = [ [[package]] name = "zesdex-ws" -version = "1.19.1" +version = "1.19.2" dependencies = [ "anyhow", "axum", diff --git a/apps/domain/src/cms/mod.rs b/apps/domain/src/cms/mod.rs index 9ce6fc6..a0865f3 100644 --- a/apps/domain/src/cms/mod.rs +++ b/apps/domain/src/cms/mod.rs @@ -47,6 +47,7 @@ pub use repository::SettingsRepository; pub use service::ConversationService; pub use service::MemoryService; pub use service::SettingsService; +pub use settings::resolve_effective_model; pub use settings::InternetMode; pub use settings::Settings; pub use settings::SettingsFlags; diff --git a/apps/domain/src/cms/settings.rs b/apps/domain/src/cms/settings.rs index 2cf2e2f..ef686ac 100644 --- a/apps/domain/src/cms/settings.rs +++ b/apps/domain/src/cms/settings.rs @@ -23,6 +23,8 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; +use super::app_config::AppConfig; + /// Controls how much network access the agent is permitted during a session. /// /// ## Variants @@ -112,3 +114,86 @@ impl Default for Settings { } } } + +/// Pick the effective model name for the main agent. +/// +/// When `settings.provider` is `"claude"` (auto-detected from +/// `~/.claude/settings.json`), the provider's `default_model` (or the +/// app-level `default_model`) wins over a possibly-stale persisted +/// `settings.model`. Otherwise the user's explicit `settings.model` is used. +/// +/// Why: the user's custom Claude endpoint (URL + API key from +/// `~/.claude/settings.json`) implies Opus as the model; a stale +/// `settings.json` (e.g. "deepseek-v4-flash-free") must not override it. +pub fn resolve_effective_model(settings: &Settings, app_config: &AppConfig) -> String { + if settings.provider == "claude" { + if let Some(m) = app_config + .providers + .get("claude") + .and_then(|p| p.default_model.clone()) + { + return m; + } + return app_config.default_model.clone(); + } + settings.model.clone() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cms::app_config::AppConfig; + + fn claude_app_config() -> AppConfig { + let mut cfg = AppConfig::default(); + cfg.providers.insert( + "claude".to_string(), + crate::cms::ProviderConfig { + api_base: "https://9router.example/v1".to_string(), + api_key_env: Some("ANTHROPIC_API_KEY".to_string()), + default_model: Some("claude-opus-4-8".to_string()), + default_api_key: Some("sk-test".to_string()), + }, + ); + cfg.default_provider = "claude".to_string(); + cfg.default_model = "claude-opus-4-8".to_string(); + cfg + } + + #[test] + fn claude_provider_uses_opus_model_over_stale_settings_model() { + let settings = Settings { + provider: "claude".to_string(), + model: "deepseek-v4-flash-free".to_string(), // stale persisted + ..Settings::default() + }; + + let model = resolve_effective_model(&settings, &claude_app_config()); + assert_eq!(model, "claude-opus-4-8"); + } + + #[test] + fn non_claude_provider_uses_settings_model() { + let settings = Settings { + provider: "zen".to_string(), + model: "my-model".to_string(), + ..Settings::default() + }; + + let model = resolve_effective_model(&settings, &AppConfig::default()); + assert_eq!(model, "my-model"); + } + + #[test] + fn claude_falls_back_to_app_default() { + let settings = Settings { + provider: "claude".to_string(), + model: String::new(), + ..Settings::default() + }; + + let cfg = AppConfig::default(); + let model = resolve_effective_model(&settings, &cfg); + assert_eq!(model, cfg.default_model); + } +} diff --git a/apps/interfaces/daemon/src/handler.rs b/apps/interfaces/daemon/src/handler.rs index 1450c06..f9b8217 100644 --- a/apps/interfaces/daemon/src/handler.rs +++ b/apps/interfaces/daemon/src/handler.rs @@ -316,13 +316,13 @@ fn handle_submit_input(state: &mut AppStateRest, text: String) { in_flight: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)), abort: state.abort_flag.clone(), api_key: api_key.clone(), - model: state.settings.model.clone(), + model: zesdex_domain::cms::resolve_effective_model(&state.settings, &state.app_config), api_base: provider_cfg.as_ref().map(|cfg| cfg.api_base.clone()), }; let client = std::sync::Arc::new(zesdex_infrastructure::llm::provider::LlmClient::new( api_key, - state.settings.model.clone(), + zesdex_domain::cms::resolve_effective_model(&state.settings, &state.app_config), provider_cfg.map(|cfg| cfg.api_base.clone()), )); @@ -465,7 +465,7 @@ fn handle_compact(state: &mut AppStateRest) { .get(provider_name) .cloned() .unwrap_or_default(); - let model = state.settings.model.clone(); + let model = zesdex_domain::cms::resolve_effective_model(&state.settings, &state.app_config); let api_base = provider_cfg.map(|cfg| cfg.api_base.clone()); let client = zesdex_infrastructure::llm::provider::LlmClient::new(api_key, model, api_base); diff --git a/apps/interfaces/tui/src/turn.rs b/apps/interfaces/tui/src/turn.rs index d4d8965..fddb205 100644 --- a/apps/interfaces/tui/src/turn.rs +++ b/apps/interfaces/tui/src/turn.rs @@ -80,7 +80,7 @@ pub fn spawn_agent_turn(state: &mut AppStateRest, text: String) { // ── Resolve provider configuration ───────────────────────────────── let provider_name = &state.settings.provider; let api_key = resolve_api_key(state, provider_name); - let model = state.settings.model.clone(); + let model = zesdex_domain::cms::resolve_effective_model(&state.settings, &state.app_config); let api_base = resolve_api_base(state, provider_name); // ── Build message list ─────────────────────────────────────────────