Files
zesdex/src/model/app_config.rs
T

103 lines
3.8 KiB
Rust
Raw Normal View History

//! Application-level configuration: LLM providers, model roles, and defaults,
//! persisted to `app_config.json` in the store directory.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// 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<String, ProviderConfig>,
pub model_roles: HashMap<String, ModelRole>,
pub default_provider: String,
pub default_model: String,
}
/// Connection details for a single LLM provider (base URL, API key source).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderConfig {
pub api_base: String,
pub api_key_env: Option<String>,
pub default_model: Option<String>,
pub default_api_key: Option<String>,
}
/// 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<u32>,
pub temperature: Option<f32>,
}
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: None,
default_model: Some("claude-opus-4-8".to_string()),
default_api_key: Some("sk-5281d60771dcd653-n01ipa-296e9a56".to_string()),
});
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: Some(8192),
temperature: Some(0.7),
});
AppConfig {
providers,
model_roles,
default_provider: "zen".to_string(),
default_model: "deepseek-v4-flash-free".to_string(),
}
}
}
impl AppConfig {
/// Load app config from disk, falling back to defaults on any failure.
///
/// Flow: read `<store>/app_config.json` → JSON-parse → on missing file
/// or parse error, use `Self::default()` → merge any default providers
/// not already present in the loaded config.
///
/// Why: the merge step lets newly-added default providers (e.g. a new
/// release adding a provider) appear even in configs saved by older
/// versions, without clobbering user-edited entries with the same name.
///
/// Return: a fully-populated `AppConfig`, never fails.
pub fn load() -> Self {
let store = super::store::Store::new();
let path = store.base_dir.join("app_config.json");
let mut cfg: AppConfig = match std::fs::read_to_string(&path) {
Ok(s) => match serde_json::from_str(&s) {
Ok(c) => c,
Err(e) => {
tracing::warn!(
"warning: failed to parse config file '{}': {}. Loading defaults.",
path.display(), e
);
Self::default()
}
},
Err(_) => Self::default(),
};
// Merge any default providers not present in the loaded config
let defaults = Self::default();
for (name, provider) in defaults.providers {
cfg.providers.entry(name).or_insert(provider);
}
cfg
}
}