//! Pure domain entity for application configuration. //! //! Defines `AppConfig`, `ProviderConfig`, and `ModelRole` — the data //! structures that describe which LLM providers are registered, which //! model roles exist, and which provider/model is the default. //! //! # Architecture //! These are pure data structures with **no I/O logic**. Load/save //! responsibilities live in `AppConfigRepository` (domain::repository). //! //! ## Data Flow //! 1. `AppConfig` is deserialised from `app_config.json` at startup //! 2. The HTTP handler layer calls `SettingsService::update_provider()` //! to mutate the provider map //! 3. The modified `AppConfig` is serialised back to `app_config.json` use std::collections::HashMap; use serde::{Deserialize, Serialize}; /// Top-level application configuration. /// /// Holds the registry of configured LLM providers, named model roles /// (logical profiles mapping to a provider+model pair), and the default /// provider/model selection. /// /// ## Fields /// - `providers` — map of provider name → connection details /// - `model_roles` — map of role name → provider/model/temperature /// - `default_provider` — the provider to use when none is specified /// - `default_model` — the model to use when none is specified /// - `default_context_window` — fallback context window size in tokens #[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 endpoint. /// /// ## Fields /// - `api_base` — base URL for the provider API /// - `api_key_env` — optional environment variable name holding the API key /// - `default_model` — optional default model name for this provider /// - `default_api_key` — optional inline API key (less secure than env var) #[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 model role mapping to a specific provider/model with parameters. /// /// Roles allow the UI to present logical profiles (e.g. "fast", "reasoning") /// that abstract over concrete provider+model strings. /// /// ## Fields /// - `provider` — which provider serves this role /// - `model` — which model to use for this role /// - `max_tokens` — optional maximum output token limit /// - `context_window` — optional context window override /// - `temperature` — optional generation temperature #[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, } /// Returns the default AppConfig with built-in "zen" and "router" providers. impl Default for AppConfig { /// Construct an AppConfig with the default "zen" and "router" providers. /// /// ## Defaults /// - Zen provider: `deepseek-v4-flash-free` model /// - Router provider: `claude-opus-5` model /// - Default role: "default" → zen / deepseek-v4-flash-free, temp 0.7 /// - `default_context_window`: 256,000 tokens 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-5".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, } } }