1use std::path::PathBuf;
31
32use serde::{Deserialize, Serialize};
33
34#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(rename_all = "snake_case")]
41pub enum TuneSource {
42 #[default]
44 Empirical,
45 OllamaShow,
47 Manual,
49 Community,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct TuningProfile {
56 pub model: String,
58
59 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub context_window: Option<u32>,
62
63 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub safe_context: Option<u32>,
66
67 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub mid_loop_trim_threshold: Option<usize>,
70
71 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub max_tool_rounds: Option<usize>,
74
75 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub estimate_ratio: Option<f32>,
82
83 #[serde(default)]
85 pub tune_source: TuneSource,
86
87 #[serde(default = "default_confidence")]
89 pub confidence: String,
90
91 #[serde(default)]
93 pub data_points: u32,
94
95 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub notes: Option<String>,
98}
99
100fn default_confidence() -> String {
101 "none".to_string()
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct FormatMeta {
107 pub version: String,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub generated_by: Option<String>,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub generated_at: Option<String>,
115}
116
117impl Default for FormatMeta {
118 fn default() -> Self {
119 Self {
120 version: "1".to_string(),
121 generated_by: None,
122 generated_at: None,
123 }
124 }
125}
126
127#[derive(Debug, Clone, Default, Serialize, Deserialize)]
129pub struct CommunityTunings {
130 #[serde(default)]
131 pub format: FormatMeta,
132 #[serde(default)]
134 pub profiles: Vec<TuningProfile>,
135}
136
137pub fn community_tunings_path() -> Option<PathBuf> {
143 crate::config::Config::user_config_path().map(|p| p.with_file_name("community-tunings.toml"))
144}
145
146pub fn load_community_tunings() -> CommunityTunings {
149 let Some(path) = community_tunings_path() else {
150 return CommunityTunings::default();
151 };
152 let Ok(data) = std::fs::read_to_string(&path) else {
153 return CommunityTunings::default();
154 };
155 toml::from_str(&data).unwrap_or_default()
156}
157
158pub fn save_community_tunings(tunings: &CommunityTunings) -> std::io::Result<()> {
160 let path = community_tunings_path()
161 .ok_or_else(|| std::io::Error::other("cannot determine ~/.newt path"))?;
162 let data = toml::to_string_pretty(tunings).map_err(|e| std::io::Error::other(e.to_string()))?;
163 std::fs::write(path, data)
164}
165
166impl CommunityTunings {
168 pub fn find(&self, model: &str) -> Option<&TuningProfile> {
169 self.profiles.iter().find(|p| p.model == model)
170 }
171
172 pub fn merge(&mut self, other: Self) {
175 for incoming in other.profiles {
176 if let Some(existing) = self.profiles.iter_mut().find(|p| p.model == incoming.model) {
177 if confidence_rank(&incoming.confidence) >= confidence_rank(&existing.confidence) {
178 *existing = incoming;
179 }
180 } else {
181 self.profiles.push(incoming);
182 }
183 }
184 }
185}
186
187fn confidence_rank(c: &str) -> u8 {
188 match c {
189 "high" => 3,
190 "medium" => 2,
191 "low" => 1,
192 _ => 0,
193 }
194}
195
196#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn community_tunings_roundtrips_toml() {
206 let mut ct = CommunityTunings::default();
207 ct.format.version = "1".to_string();
208 ct.format.generated_by = Some("newt/0.6.6".to_string());
209 ct.profiles.push(TuningProfile {
210 model: "nemotron3:33b".to_string(),
211 context_window: Some(32768),
212 safe_context: Some(24576),
213 mid_loop_trim_threshold: Some(12),
214 max_tool_rounds: Some(20),
215 estimate_ratio: Some(1.29),
217 tune_source: TuneSource::Empirical,
218 confidence: "high".to_string(),
219 data_points: 15,
220 notes: Some("confirmed safe".to_string()),
221 });
222 let serialized = toml::to_string_pretty(&ct).unwrap();
223 let back: CommunityTunings = toml::from_str(&serialized).unwrap();
224 assert_eq!(back.profiles.len(), 1);
225 assert_eq!(back.profiles[0].model, "nemotron3:33b");
226 assert_eq!(back.profiles[0].safe_context, Some(24576));
227 assert_eq!(back.profiles[0].confidence, "high");
228 assert_eq!(back.profiles[0].estimate_ratio, Some(1.29));
229 }
230
231 #[test]
234 fn estimate_ratio_is_optional_in_v1_files() {
235 let toml_text = r#"
236[format]
237version = "1"
238
239[[profiles]]
240model = "old:7b"
241safe_context = 4096
242confidence = "low"
243"#;
244 let ct: CommunityTunings = toml::from_str(toml_text).unwrap();
245 assert_eq!(ct.format.version, "1");
246 assert_eq!(ct.profiles[0].estimate_ratio, None);
247 let out = toml::to_string_pretty(&ct).unwrap();
249 assert!(!out.contains("estimate_ratio"), "{out}");
250 }
251
252 #[test]
253 fn community_tunings_find_returns_correct_profile() {
254 let ct = CommunityTunings {
255 profiles: vec![
256 TuningProfile {
257 model: "a:7b".to_string(),
258 context_window: None,
259 safe_context: None,
260 mid_loop_trim_threshold: None,
261 max_tool_rounds: None,
262 estimate_ratio: None,
263 tune_source: TuneSource::Manual,
264 confidence: "medium".to_string(),
265 data_points: 0,
266 notes: None,
267 },
268 TuningProfile {
269 model: "b:13b".to_string(),
270 context_window: Some(8192),
271 safe_context: Some(6553),
272 mid_loop_trim_threshold: None,
273 max_tool_rounds: None,
274 estimate_ratio: None,
275 tune_source: TuneSource::OllamaShow,
276 confidence: "low".to_string(),
277 data_points: 1,
278 notes: None,
279 },
280 ],
281 ..CommunityTunings::default()
282 };
283 assert!(ct.find("a:7b").is_some());
284 assert_eq!(ct.find("b:13b").unwrap().context_window, Some(8192));
285 assert!(ct.find("c:30b").is_none());
286 }
287
288 #[test]
289 fn merge_replaces_lower_confidence_with_higher() {
290 let mut base = CommunityTunings {
291 profiles: vec![TuningProfile {
292 model: "m:7b".to_string(),
293 safe_context: Some(4096),
294 confidence: "low".to_string(),
295 context_window: None,
296 mid_loop_trim_threshold: None,
297 max_tool_rounds: None,
298 estimate_ratio: None,
299 tune_source: TuneSource::Community,
300 data_points: 1,
301 notes: None,
302 }],
303 ..CommunityTunings::default()
304 };
305 let incoming = CommunityTunings {
306 profiles: vec![TuningProfile {
307 model: "m:7b".to_string(),
308 safe_context: Some(8192),
309 confidence: "high".to_string(),
310 context_window: None,
311 mid_loop_trim_threshold: None,
312 max_tool_rounds: None,
313 estimate_ratio: None,
314 tune_source: TuneSource::Empirical,
315 data_points: 10,
316 notes: None,
317 }],
318 ..CommunityTunings::default()
319 };
320 base.merge(incoming);
321 assert_eq!(base.profiles.len(), 1);
322 assert_eq!(base.profiles[0].safe_context, Some(8192));
323 assert_eq!(base.profiles[0].data_points, 10);
324 }
325
326 #[test]
327 fn merge_does_not_replace_higher_confidence_with_lower() {
328 let mut base = CommunityTunings {
329 profiles: vec![TuningProfile {
330 model: "m:7b".to_string(),
331 safe_context: Some(8192),
332 confidence: "high".to_string(),
333 context_window: None,
334 mid_loop_trim_threshold: None,
335 max_tool_rounds: None,
336 estimate_ratio: None,
337 tune_source: TuneSource::Empirical,
338 data_points: 10,
339 notes: None,
340 }],
341 ..CommunityTunings::default()
342 };
343 let incoming = CommunityTunings {
344 profiles: vec![TuningProfile {
345 model: "m:7b".to_string(),
346 safe_context: Some(2048),
347 confidence: "low".to_string(),
348 context_window: None,
349 mid_loop_trim_threshold: None,
350 max_tool_rounds: None,
351 estimate_ratio: None,
352 tune_source: TuneSource::Community,
353 data_points: 1,
354 notes: None,
355 }],
356 ..CommunityTunings::default()
357 };
358 base.merge(incoming);
359 assert_eq!(base.profiles[0].safe_context, Some(8192));
361 assert_eq!(base.profiles[0].data_points, 10);
362 }
363
364 #[test]
365 fn confidence_rank_orders_correctly() {
366 assert!(confidence_rank("high") > confidence_rank("medium"));
367 assert!(confidence_rank("medium") > confidence_rank("low"));
368 assert!(confidence_rank("low") > confidence_rank("none"));
369 assert_eq!(confidence_rank("none"), 0);
370 }
371
372 #[test]
373 fn empty_document_parses_without_error() {
374 let ct: CommunityTunings = toml::from_str("").unwrap();
375 assert!(ct.profiles.is_empty());
376 }
377}