//! Pure AppConfig entity — provider registry, model roles, and default model //! selections. //! //! # Architecture //! This is a pure data structure with **no I/O logic**. Load/save //! responsibilities live in [`AppConfigRepository`](super::repository::AppConfigRepository). #![allow( clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap )] use std::collections::HashMap; use serde::{Deserialize, Serialize}; /// Top-level application config: registered providers, named model roles, /// and which provider/model to use by default. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppConfig { pub providers: HashMap, pub model_roles: HashMap, pub default_provider: String, pub default_model: String, pub default_context_window: u32, } /// Connection details for a single LLM provider. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProviderConfig { pub api_base: String, pub api_key_env: Option, pub default_model: Option, pub default_api_key: Option, } /// A named role (e.g. "default") mapping to a specific provider/model and /// its generation parameters. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ModelRole { pub provider: String, pub model: String, pub max_tokens: Option, pub context_window: Option, pub temperature: Option, } impl Default for AppConfig { fn default() -> Self { let mut providers = HashMap::new(); providers.insert( "zen".to_string(), ProviderConfig { api_base: "https://opencode.ai/zen/v1".to_string(), api_key_env: Some("API_KEY".to_string()), default_model: Some("deepseek-v4-flash-free".to_string()), default_api_key: None, }, ); providers.insert( "router".to_string(), ProviderConfig { api_base: "https://9router.asepharyana.my.id/v1".to_string(), api_key_env: Some("ROUTER_API_KEY".to_string()), default_model: Some("claude-opus-4-8".to_string()), default_api_key: None, }, ); let mut model_roles = HashMap::new(); model_roles.insert( "default".to_string(), ModelRole { provider: "zen".to_string(), model: "deepseek-v4-flash-free".to_string(), max_tokens: None, context_window: None, temperature: Some(0.7), }, ); Self { providers, model_roles, default_provider: "zen".to_string(), default_model: "deepseek-v4-flash-free".to_string(), default_context_window: 256_000, } } }