use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
#[derive(Debug)]
pub enum TunerError {
ReadConfigDir(String),
ParseTOML(PathBuf, String),
UserConfigExists(PathBuf),
ModelNotFound(String),
}
impl std::fmt::Display for TunerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ReadConfigDir(path) => write!(f, "Cannot read config dir: {}", path),
Self::ParseTOML(path, msg) => write!(f, "Failed to parse {}: {}", path.display(), msg),
Self::UserConfigExists(path) => {
write!(
f,
"User config exists at {}, refusing to overwrite",
path.display()
)
}
Self::ModelNotFound(model) => write!(f, "Model not found: {}", model),
}
}
}
impl std::error::Error for TunerError {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModelTuning {
pub model: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub num_ctx: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_window: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub real_context_discovery: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mid_loop_trim_threshold: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mid_loop_trim_tokens: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_tool_rounds: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workflow_grace_rounds: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub narration_nudge_cap: Option<usize>,
}
pub fn load_model_config(model_name: &str) -> Result<toml::Value, TunerError> {
let home = dirs::home_dir()
.ok_or_else(|| TunerError::ReadConfigDir("Cannot determine home directory".to_string()))?;
let config_dir = home.join(".newt").join("model_tuning");
let user_config_path = config_dir.join(format!("{}.toml", model_name));
if user_config_path.exists() {
return load_toml(&user_config_path)
.map_err(|e| TunerError::ParseTOML(user_config_path, e.to_string()));
}
let default_path = config_dir.join(format!("{}.default.toml", model_name));
if default_path.exists() {
return load_toml(&default_path)
.map_err(|e| TunerError::ParseTOML(default_path, e.to_string()));
}
Err(TunerError::ModelNotFound(model_name.to_string()))
}
pub fn init_model_tuning() -> Result<(), TunerError> {
let home = dirs::home_dir()
.ok_or_else(|| TunerError::ReadConfigDir("Cannot determine home directory".to_string()))?;
let config_dir = home.join(".newt").join("model_tuning");
if config_dir.exists() {
return Ok(());
}
std::fs::create_dir_all(&config_dir).map_err(|e| {
TunerError::ReadConfigDir(format!("Cannot create {}: {}", config_dir.display(), e))
})?;
Ok(())
}
pub fn model_tuning_dir() -> Result<PathBuf, TunerError> {
let home = dirs::home_dir()
.ok_or_else(|| TunerError::ReadConfigDir("Cannot determine home directory".to_string()))?;
Ok(home.join(".newt").join("model_tuning"))
}
fn load_toml(path: &Path) -> Result<toml::Value, String> {
let content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
toml::from_str(&content).map_err(|e| e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_model_tuning_dir() {
let dir = model_tuning_dir().expect("Should return config dir");
assert!(dir.ends_with(".newt/model_tuning"));
}
}