1use anyhow::{Context, Result};
6use serde::{Deserialize, Serialize};
7use std::path::{Path, PathBuf};
8use tracing::debug;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "lowercase")]
13pub enum LlmBackendType {
14 Ollama,
16 OpenAI,
18 #[cfg(feature = "embedded-llm")]
20 Embedded,
21}
22
23impl std::fmt::Display for LlmBackendType {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 match self {
26 LlmBackendType::Ollama => write!(f, "Ollama"),
27 LlmBackendType::OpenAI => write!(f, "OpenAI-compatible"),
28 #[cfg(feature = "embedded-llm")]
29 LlmBackendType::Embedded => write!(f, "Embedded (llama.cpp)"),
30 }
31 }
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct OpenAIConfig {
37 #[serde(default)]
39 pub base_url: Option<String>,
40
41 #[serde(default)]
43 pub last_model: Option<String>,
44 }
46
47impl Default for OpenAIConfig {
48 fn default() -> Self {
49 Self {
50 base_url: None,
51 last_model: None,
52 }
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct NetGetConfig {
59 #[serde(default)]
61 pub last_backend: Option<LlmBackendType>,
62
63 #[serde(default)]
65 pub ollama: OllamaConfig,
66
67 #[serde(default)]
69 pub openai: OpenAIConfig,
70
71 #[cfg(feature = "embedded-llm")]
73 #[serde(default)]
74 pub embedded: EmbeddedLlmConfig,
75
76 #[serde(default = "default_models_dir")]
78 pub models_dir: PathBuf,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct OllamaConfig {
84 #[serde(default = "default_ollama_url")]
86 pub base_url: String,
87
88 #[serde(default)]
90 pub last_model: Option<String>,
91
92 #[serde(default = "default_true")]
94 pub prefer: bool,
95}
96
97impl Default for OllamaConfig {
98 fn default() -> Self {
99 Self {
100 base_url: default_ollama_url(),
101 last_model: None,
102 prefer: true,
103 }
104 }
105}
106
107#[cfg(feature = "embedded-llm")]
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct EmbeddedLlmConfig {
111 #[serde(default)]
113 pub last_model_path: Option<PathBuf>,
114
115 #[serde(default = "default_context_size")]
117 pub context_size: u32,
118
119 #[serde(default = "default_max_tokens")]
121 pub max_tokens: usize,
122
123 #[serde(default = "default_gpu_layers")]
125 pub n_gpu_layers: u32,
126
127 #[serde(default)]
129 pub n_threads: u32,
130}
131
132#[cfg(feature = "embedded-llm")]
133impl Default for EmbeddedLlmConfig {
134 fn default() -> Self {
135 Self {
136 last_model_path: None,
137 context_size: default_context_size(),
138 max_tokens: default_max_tokens(),
139 n_gpu_layers: default_gpu_layers(),
140 n_threads: 0,
141 }
142 }
143}
144
145impl Default for NetGetConfig {
146 fn default() -> Self {
147 Self {
148 last_backend: None,
149 ollama: OllamaConfig::default(),
150 openai: OpenAIConfig::default(),
151 #[cfg(feature = "embedded-llm")]
152 embedded: EmbeddedLlmConfig::default(),
153 models_dir: default_models_dir(),
154 }
155 }
156}
157
158impl NetGetConfig {
159 pub fn load() -> Result<Self> {
161 let config_path = Self::config_file_path()?;
162
163 if !config_path.exists() {
164 debug!(
165 "Config file not found, using defaults: {}",
166 config_path.display()
167 );
168 return Ok(Self::default());
169 }
170
171 let contents = crate::utils::file_io::read_text(&config_path, 1024 * 1024)
172 .context("Failed to read config file")?;
173
174 let config: NetGetConfig =
175 toml::from_str(&contents).context("Failed to parse config file")?;
176
177 debug!("Loaded config from: {}", config_path.display());
178 Ok(config)
179 }
180
181 pub fn save(&self) -> Result<()> {
183 let home = dirs::home_dir().context("Cannot find home directory")?;
184 let config_path =
185 crate::utils::file_io::ensure_config_directory(&home)?.join("config.toml");
186 let contents = toml::to_string_pretty(self).context("Failed to serialize config")?;
187 crate::utils::file_io::write_atomic(&config_path, contents.as_bytes())?;
188
189 debug!("Saved config to: {}", config_path.display());
190 Ok(())
191 }
192
193 pub fn config_file_path() -> Result<PathBuf> {
195 let home = dirs::home_dir().context("Cannot find home directory")?;
196 Ok(home.join(".netget").join("config.toml"))
197 }
198
199 pub fn models_directory(&self) -> &Path {
201 &self.models_dir
202 }
203
204 pub fn ensure_models_dir(&self) -> Result<PathBuf> {
206 std::fs::create_dir_all(&self.models_dir).context("Failed to create models directory")?;
207 Ok(self.models_dir.clone())
208 }
209
210 pub fn set_last_backend(&mut self, backend: LlmBackendType) -> Result<()> {
212 self.last_backend = Some(backend);
213 self.save()
214 }
215
216 pub fn set_ollama_model(&mut self, model: impl Into<String>) -> Result<()> {
218 self.ollama.last_model = Some(model.into());
219 self.last_backend = Some(LlmBackendType::Ollama);
220 self.save()
221 }
222
223 #[cfg(feature = "embedded-llm")]
225 pub fn set_embedded_model(&mut self, path: impl Into<PathBuf>) -> Result<()> {
226 self.embedded.last_model_path = Some(path.into());
227 self.last_backend = Some(LlmBackendType::Embedded);
228 self.save()
229 }
230}
231
232fn default_ollama_url() -> String {
234 "http://localhost:11434".to_string()
235}
236
237fn default_models_dir() -> PathBuf {
238 dirs::home_dir()
239 .unwrap_or_else(|| PathBuf::from("."))
240 .join(".netget")
241 .join("models")
242}
243
244#[allow(dead_code)]
246fn default_context_size() -> u32 {
247 4096
248}
249
250#[allow(dead_code)]
251fn default_max_tokens() -> usize {
252 2048
253}
254
255#[allow(dead_code)]
256fn default_gpu_layers() -> u32 {
257 u32::MAX }
259
260fn default_true() -> bool {
261 true
262}