136 lines
4.6 KiB
Rust
136 lines
4.6 KiB
Rust
//! JSON file–backed `AppConfigRepository` with Claude credential auto-detection.
|
||
|
||
use std::path::Path;
|
||
|
||
use serde::{Deserialize, Serialize};
|
||
use zesdex_domain::cms::{AppConfig, AppConfigRepository, ModelRole, ProviderConfig, RepositoryError};
|
||
|
||
use crate::utils::write_json_atomic;
|
||
|
||
/// File-based `AppConfigRepository` that reads/writes `app_config.json`.
|
||
#[derive(Debug, Clone, Default)]
|
||
pub struct JsonAppConfigRepository;
|
||
|
||
impl JsonAppConfigRepository {
|
||
pub fn new() -> Self {
|
||
Self
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
struct ClaudeEnv {
|
||
#[serde(alias = "ANTHROPIC_BASE_URL")]
|
||
anthropic_base_url: Option<String>,
|
||
#[serde(alias = "ANTHROPIC_API_KEY")]
|
||
anthropic_api_key: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
struct ClaudeSettings {
|
||
env: Option<ClaudeEnv>,
|
||
#[serde(alias = "customModel", alias = "model")]
|
||
custom_model: Option<String>,
|
||
}
|
||
|
||
fn claude_settings_from_file() -> Option<ClaudeSettings> {
|
||
let path = dirs::home_dir()?.join(".claude").join("settings.json");
|
||
let content = std::fs::read_to_string(&path).ok()?;
|
||
serde_json::from_str(&content).ok()
|
||
}
|
||
|
||
fn detect_claude_settings_provider() -> Option<(ProviderConfig, Option<String>)> {
|
||
let settings = claude_settings_from_file();
|
||
|
||
let file_creds = settings.as_ref().and_then(|s| {
|
||
let env = s.env.as_ref()?;
|
||
Some((env.anthropic_base_url.clone()?, env.anthropic_api_key.clone()?))
|
||
});
|
||
|
||
let env_creds = || -> Option<(String, String)> {
|
||
let base_url = std::env::var("ANTHROPIC_BASE_URL").ok()?;
|
||
let key = std::env::var("ANTHROPIC_API_KEY").ok()?;
|
||
Some((base_url, key))
|
||
};
|
||
|
||
let custom_model = settings.and_then(|s| s.custom_model);
|
||
|
||
let (base_url, key) = file_creds.or_else(env_creds)?;
|
||
|
||
Some((
|
||
ProviderConfig {
|
||
api_base: base_url,
|
||
api_key_env: Some("ANTHROPIC_API_KEY".to_string()),
|
||
default_model: custom_model.clone(),
|
||
default_api_key: Some(key),
|
||
},
|
||
custom_model
|
||
))
|
||
}
|
||
|
||
impl AppConfigRepository for JsonAppConfigRepository {
|
||
fn load(&self, base_dir: &Path) -> Result<AppConfig, RepositoryError> {
|
||
let path = base_dir.join("app_config.json");
|
||
let mut cfg: AppConfig = match std::fs::read_to_string(&path) {
|
||
Ok(s) => serde_json::from_str(&s)?,
|
||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||
AppConfig::default()
|
||
}
|
||
Err(e) => return Err(RepositoryError::Io(e)),
|
||
};
|
||
|
||
let defaults = AppConfig::default();
|
||
for (name, provider) in defaults.providers {
|
||
cfg.providers.entry(name).or_insert(provider);
|
||
}
|
||
|
||
if let Some((claude_provider, custom_model)) = detect_claude_settings_provider() {
|
||
cfg.providers
|
||
.entry("claude".to_string())
|
||
.or_insert(claude_provider);
|
||
|
||
let claude_models: [(&str, &str); 3] = [
|
||
("claude-opus-4-8", "claude-opus-4-8"),
|
||
("claude-sonnet-5", "claude-sonnet-5"),
|
||
("claude-haiku-4-5", "claude-haiku-4-5-20251001"),
|
||
];
|
||
for (role_name, model_name) in &claude_models {
|
||
cfg.model_roles
|
||
.entry(role_name.to_string())
|
||
.or_insert(ModelRole {
|
||
provider: "claude".to_string(),
|
||
model: model_name.to_string(),
|
||
max_tokens: Some(8192),
|
||
context_window: Some(200_000),
|
||
temperature: Some(0.7),
|
||
});
|
||
}
|
||
|
||
if let Some(custom) = &custom_model {
|
||
cfg.model_roles
|
||
.entry(custom.clone())
|
||
.or_insert(ModelRole {
|
||
provider: "claude".to_string(),
|
||
model: custom.clone(),
|
||
max_tokens: Some(8192),
|
||
context_window: Some(200_000),
|
||
temperature: Some(0.7),
|
||
});
|
||
}
|
||
|
||
if cfg.default_provider == defaults.default_provider {
|
||
cfg.default_provider = "claude".to_string();
|
||
cfg.default_model = custom_model.unwrap_or_else(|| "claude-opus-4-8".to_string());
|
||
}
|
||
}
|
||
|
||
Ok(cfg)
|
||
}
|
||
|
||
fn save(&self, base_dir: &Path, config: &AppConfig) -> Result<(), RepositoryError> {
|
||
std::fs::create_dir_all(base_dir)?;
|
||
let path = base_dir.join("app_config.json");
|
||
write_json_atomic(&path, config, None)?;
|
||
Ok(())
|
||
}
|
||
}
|