use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tracing::debug;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LlmBackendType {
Ollama,
OpenAI,
#[cfg(feature = "embedded-llm")]
Embedded,
}
impl std::fmt::Display for LlmBackendType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LlmBackendType::Ollama => write!(f, "Ollama"),
LlmBackendType::OpenAI => write!(f, "OpenAI-compatible"),
#[cfg(feature = "embedded-llm")]
LlmBackendType::Embedded => write!(f, "Embedded (llama.cpp)"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenAIConfig {
#[serde(default)]
pub base_url: Option<String>,
#[serde(default)]
pub last_model: Option<String>,
}
impl Default for OpenAIConfig {
fn default() -> Self {
Self {
base_url: None,
last_model: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetGetConfig {
#[serde(default)]
pub last_backend: Option<LlmBackendType>,
#[serde(default)]
pub ollama: OllamaConfig,
#[serde(default)]
pub openai: OpenAIConfig,
#[cfg(feature = "embedded-llm")]
#[serde(default)]
pub embedded: EmbeddedLlmConfig,
#[serde(default = "default_models_dir")]
pub models_dir: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OllamaConfig {
#[serde(default = "default_ollama_url")]
pub base_url: String,
#[serde(default)]
pub last_model: Option<String>,
#[serde(default = "default_true")]
pub prefer: bool,
}
impl Default for OllamaConfig {
fn default() -> Self {
Self {
base_url: default_ollama_url(),
last_model: None,
prefer: true,
}
}
}
#[cfg(feature = "embedded-llm")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbeddedLlmConfig {
#[serde(default)]
pub last_model_path: Option<PathBuf>,
#[serde(default = "default_context_size")]
pub context_size: u32,
#[serde(default = "default_max_tokens")]
pub max_tokens: usize,
#[serde(default = "default_gpu_layers")]
pub n_gpu_layers: u32,
#[serde(default)]
pub n_threads: u32,
}
#[cfg(feature = "embedded-llm")]
impl Default for EmbeddedLlmConfig {
fn default() -> Self {
Self {
last_model_path: None,
context_size: default_context_size(),
max_tokens: default_max_tokens(),
n_gpu_layers: default_gpu_layers(),
n_threads: 0,
}
}
}
impl Default for NetGetConfig {
fn default() -> Self {
Self {
last_backend: None,
ollama: OllamaConfig::default(),
openai: OpenAIConfig::default(),
#[cfg(feature = "embedded-llm")]
embedded: EmbeddedLlmConfig::default(),
models_dir: default_models_dir(),
}
}
}
impl NetGetConfig {
pub fn load() -> Result<Self> {
let config_path = Self::config_file_path()?;
if !config_path.exists() {
debug!(
"Config file not found, using defaults: {}",
config_path.display()
);
return Ok(Self::default());
}
let contents = crate::utils::file_io::read_text(&config_path, 1024 * 1024)
.context("Failed to read config file")?;
let config: NetGetConfig =
toml::from_str(&contents).context("Failed to parse config file")?;
debug!("Loaded config from: {}", config_path.display());
Ok(config)
}
pub fn save(&self) -> Result<()> {
let home = dirs::home_dir().context("Cannot find home directory")?;
let config_path =
crate::utils::file_io::ensure_config_directory(&home)?.join("config.toml");
let contents = toml::to_string_pretty(self).context("Failed to serialize config")?;
crate::utils::file_io::write_atomic(&config_path, contents.as_bytes())?;
debug!("Saved config to: {}", config_path.display());
Ok(())
}
pub fn config_file_path() -> Result<PathBuf> {
let home = dirs::home_dir().context("Cannot find home directory")?;
Ok(home.join(".netget").join("config.toml"))
}
pub fn models_directory(&self) -> &Path {
&self.models_dir
}
pub fn ensure_models_dir(&self) -> Result<PathBuf> {
std::fs::create_dir_all(&self.models_dir).context("Failed to create models directory")?;
Ok(self.models_dir.clone())
}
pub fn set_last_backend(&mut self, backend: LlmBackendType) -> Result<()> {
self.last_backend = Some(backend);
self.save()
}
pub fn set_ollama_model(&mut self, model: impl Into<String>) -> Result<()> {
self.ollama.last_model = Some(model.into());
self.last_backend = Some(LlmBackendType::Ollama);
self.save()
}
#[cfg(feature = "embedded-llm")]
pub fn set_embedded_model(&mut self, path: impl Into<PathBuf>) -> Result<()> {
self.embedded.last_model_path = Some(path.into());
self.last_backend = Some(LlmBackendType::Embedded);
self.save()
}
}
fn default_ollama_url() -> String {
"http://localhost:11434".to_string()
}
fn default_models_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".netget")
.join("models")
}
#[allow(dead_code)]
fn default_context_size() -> u32 {
4096
}
#[allow(dead_code)]
fn default_max_tokens() -> usize {
2048
}
#[allow(dead_code)]
fn default_gpu_layers() -> u32 {
u32::MAX }
fn default_true() -> bool {
true
}