122 lines
3.9 KiB
Rust
122 lines
3.9 KiB
Rust
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, ConfigError> {
|
|
Self::maybe_from_env()?.ok_or(ConfigError::MissingEnv("DEEPSEEK_API_KEY"))
|
|
}
|
|
|
|
pub fn load(config_path: Option<&Path>) -> Result<Option<Self>, 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<Option<Self>, 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<Self, ConfigError> {
|
|
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<Self, ConfigError> {
|
|
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,
|
|
}
|
|
}
|
|
}
|