use serde::Deserialize;
use trusty_common::LocalModelConfig;
fn default_openrouter_model() -> String {
"anthropic/claude-haiku-4.5".to_string()
}
#[derive(Deserialize, Default, Clone)]
struct UserConfigFile {
#[serde(default)]
openrouter: OpenRouterSection,
#[serde(default)]
local_model: LocalModelSection,
}
#[derive(Deserialize, Default, Clone)]
struct OpenRouterSection {
#[serde(default)]
api_key: String,
#[serde(default)]
model: String,
}
#[derive(Deserialize, Clone)]
struct LocalModelSection {
#[serde(default = "default_local_enabled")]
enabled: bool,
#[serde(default = "default_local_base_url")]
base_url: String,
#[serde(default = "default_local_model")]
model: String,
}
fn default_local_enabled() -> bool {
true
}
fn default_local_base_url() -> String {
"http://localhost:11434".to_string()
}
fn default_local_model() -> String {
"llama3.2".to_string()
}
impl Default for LocalModelSection {
fn default() -> Self {
Self {
enabled: default_local_enabled(),
base_url: default_local_base_url(),
model: default_local_model(),
}
}
}
#[derive(Clone, Debug)]
pub struct LoadedUserConfig {
pub openrouter_api_key: String,
pub openrouter_model: String,
pub local_model: LocalModelConfig,
}
impl Default for LoadedUserConfig {
fn default() -> Self {
Self {
openrouter_api_key: std::env::var("OPENROUTER_API_KEY").unwrap_or_default(),
openrouter_model: default_openrouter_model(),
local_model: LocalModelConfig::default(),
}
}
}
pub fn load_user_config() -> LoadedUserConfig {
let Some(home) = dirs::home_dir() else {
return LoadedUserConfig::default();
};
let path = home.join(".trusty-search").join("config.toml");
if !path.exists() {
return LoadedUserConfig::default();
}
let raw = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(e) => {
tracing::warn!("could not read {}: {e}; using defaults", path.display());
return LoadedUserConfig::default();
}
};
let parsed: UserConfigFile = match toml::from_str(&raw) {
Ok(v) => v,
Err(e) => {
tracing::warn!("could not parse {}: {e}; using defaults", path.display());
return LoadedUserConfig::default();
}
};
let env_key = std::env::var("OPENROUTER_API_KEY").unwrap_or_default();
let openrouter_api_key = if !env_key.is_empty() {
env_key
} else {
parsed.openrouter.api_key
};
let openrouter_model = if parsed.openrouter.model.is_empty() {
default_openrouter_model()
} else {
parsed.openrouter.model
};
LoadedUserConfig {
openrouter_api_key,
openrouter_model,
local_model: LocalModelConfig {
enabled: parsed.local_model.enabled,
base_url: parsed.local_model.base_url,
model: parsed.local_model.model,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_local_model_section() {
let src = r#"
[local_model]
enabled = true
base_url = "http://localhost:1234"
model = "qwen2.5-coder"
[openrouter]
model = "anthropic/claude-3-5-sonnet"
"#;
let parsed: UserConfigFile = toml::from_str(src).unwrap();
assert!(parsed.local_model.enabled);
assert_eq!(parsed.local_model.base_url, "http://localhost:1234");
assert_eq!(parsed.local_model.model, "qwen2.5-coder");
assert_eq!(parsed.openrouter.model, "anthropic/claude-3-5-sonnet");
}
#[test]
fn local_model_defaults_apply_when_section_absent() {
let parsed: UserConfigFile = toml::from_str("").unwrap();
assert!(parsed.local_model.enabled);
assert_eq!(parsed.local_model.base_url, "http://localhost:11434");
assert_eq!(parsed.local_model.model, "llama3.2");
}
}