use std::path::{Path, PathBuf}; use serde::Deserialize; use thiserror::Error; const DEFAULT_DEEPSEEK_BASE_URL: &str = "https://api.deepseek.com"; const DEFAULT_DEEPSEEK_MODEL: &str = "deepseek-chat"; #[derive(Debug, Clone, PartialEq, Eq)] pub struct DeepSeekSettings { pub api_key: String, pub base_url: String, pub model: String, } impl DeepSeekSettings { pub fn from_env() -> Result { Self::maybe_from_env()?.ok_or(ConfigError::MissingEnv("DEEPSEEK_API_KEY")) } pub fn load(config_path: Option<&Path>) -> Result, ConfigError> { if let Some(path) = config_path { if path.exists() { return Self::from_config_path(path).map(Some); } } Self::maybe_from_env() } fn maybe_from_env() -> Result, ConfigError> { let api_key = match std::env::var("DEEPSEEK_API_KEY") { Ok(value) => value, Err(std::env::VarError::NotPresent) => return Ok(None), Err(std::env::VarError::NotUnicode(_)) => { return Err(ConfigError::InvalidEnv("DEEPSEEK_API_KEY")) } }; let base_url = std::env::var("DEEPSEEK_BASE_URL") .unwrap_or_else(|_| DEFAULT_DEEPSEEK_BASE_URL.to_string()); let model = std::env::var("DEEPSEEK_MODEL").unwrap_or_else(|_| DEFAULT_DEEPSEEK_MODEL.to_string()); Ok(Some(Self::new(api_key, base_url, model)?)) } fn from_config_path(path: &Path) -> Result { let raw = std::fs::read_to_string(path) .map_err(|err| ConfigError::ConfigRead(path.to_path_buf(), err.to_string()))?; let config: RawDeepSeekSettings = serde_json::from_str(&raw) .map_err(|err| ConfigError::ConfigParse(path.to_path_buf(), err.to_string()))?; Self::new(config.api_key, config.base_url, config.model) .map_err(|err| err.with_path(path)) } fn new(api_key: String, base_url: String, model: String) -> Result { let api_key = api_key.trim().to_string(); let base_url = if base_url.trim().is_empty() { DEFAULT_DEEPSEEK_BASE_URL.to_string() } else { base_url.trim().to_string() }; let model = if model.trim().is_empty() { DEFAULT_DEEPSEEK_MODEL.to_string() } else { model.trim().to_string() }; if api_key.is_empty() { return Err(ConfigError::EmptyValue("DEEPSEEK_API_KEY")); } if base_url.is_empty() { return Err(ConfigError::EmptyValue("DEEPSEEK_BASE_URL")); } if model.is_empty() { return Err(ConfigError::EmptyValue("DEEPSEEK_MODEL")); } Ok(Self { api_key, base_url, model, }) } } #[derive(Debug, Deserialize)] struct RawDeepSeekSettings { #[serde(rename = "apiKey", default)] api_key: String, #[serde(rename = "baseUrl", default)] base_url: String, #[serde(default)] model: String, } #[derive(Debug, Error, Clone, PartialEq, Eq)] pub enum ConfigError { #[error("missing environment variable: {0}")] MissingEnv(&'static str), #[error("environment variable must not be empty: {0}")] EmptyValue(&'static str), #[error("invalid non-utf8 environment variable: {0}")] InvalidEnv(&'static str), #[error("failed to read DeepSeek config file {0}: {1}")] ConfigRead(PathBuf, String), #[error("invalid DeepSeek config JSON in {0}: {1}")] ConfigParse(PathBuf, String), #[error("DeepSeek config value must not be empty: {0} ({1})")] ConfigValueEmpty(&'static str, PathBuf), } impl ConfigError { fn with_path(self, path: &Path) -> Self { match self { Self::EmptyValue(field) => Self::ConfigValueEmpty(field, path.to_path_buf()), other => other, } } }