1mod model_config;
2mod presets;
3mod profiles;
4mod store;
5
6use std::collections::HashSet;
7use std::path::PathBuf;
8
9use chrono::Local;
10use serde::{Deserialize, Serialize};
11
12#[allow(unused_imports)]
13pub use model_config::{display_from_key, key_from_display, ModelConfigStore};
14
15pub use profiles::ProfileStore;
16
17use crate::models::{
18 Backend, CacheType, CacheTypeK, CacheTypeV, Mirostat, NumMode, RopeScaling, Samplers, SplitMode,
19};
20use crate::tui::app::ActivePanel;
21pub use presets::PresetStore;
22
23pub const DEFAULT_SYSTEM_PROMPT: &str = "You are an expert software developer. Write clean, well-documented code. Explain your reasoning and suggest improvements.";
25
26pub fn config_base_dir() -> PathBuf {
31 if let Some(d) = dirs::config_dir() {
32 return d;
33 }
34 if let Some(home) = dirs::home_dir() {
35 return home.join(".config");
36 }
37 PathBuf::from(".").join(".llm-manager")
38}
39
40pub fn physical_cores() -> u32 {
43 let content = match std::fs::read_to_string("/proc/cpuinfo") {
44 Ok(c) => c,
45 Err(_) => {
46 return std::thread::available_parallelism()
47 .map(|p| p.get() as u32)
48 .unwrap_or(1);
49 }
50 };
51 let mut seen = HashSet::new();
52 let mut cur_phys: Option<&str> = None;
53 let mut cur_core: Option<&str> = None;
54 for line in content.lines() {
55 if let Some((key, val)) = line.split_once(':') {
56 let key = key.trim();
57 let val = val.trim();
58 match key {
59 "physical id" => cur_phys = Some(val),
60 "core id" => cur_core = Some(val),
61 _ => {}
62 }
63 if let (Some(phys), Some(core)) = (cur_phys, cur_core) {
64 seen.insert((phys, core));
65 }
66 }
67 }
68 seen.len() as u32
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct RpcWorker {
74 #[serde(default)]
75 pub selected: bool,
76 #[serde(default)]
77 pub name: String,
78 pub ip: String,
79 #[serde(default = "default_rpc_port")]
80 pub port: u16,
81}
82
83fn default_rpc_port() -> u16 {
84 50052
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct Config {
90 pub models_dirs: Vec<PathBuf>,
91 pub llama_server: PathBuf,
92 pub default: DefaultParams,
93 #[serde(default, skip)]
95 pub model_overrides: ModelConfigStore,
96 #[serde(default, skip)]
98 pub profiles: ProfileStore,
99 #[serde(default, skip)]
101 pub system_prompt_presets: PresetStore,
102 #[serde(default)]
104 pub rpc_workers: Vec<RpcWorker>,
105 #[serde(default = "default_search_limit")]
107 pub search_limit: u32,
108 #[serde(default)]
110 pub active_panel: crate::tui::app::ActivePanel,
111 #[serde(default = "default_left_pct")]
113 pub left_pct: u16,
114 #[serde(default = "default_language")]
116 pub language: String,
117 #[serde(default = "default_onboarding")]
119 pub onboarding_complete: bool,
120}
121
122fn default_language() -> String {
123 "en".to_string()
124}
125
126fn default_left_pct() -> u16 {
127 55
128}
129
130fn default_onboarding() -> bool {
131 false
132}
133
134fn default_search_limit() -> u32 {
135 50
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
140pub struct Profile {
141 pub name: String,
142 pub description: String,
144 #[serde(default)]
146 pub settings: ModelOverride,
147}
148
149impl Profile {
150 pub fn apply(&self, mut base: crate::models::ModelSettings) -> crate::models::ModelSettings {
152 self.settings.apply(&mut base);
153 base
154 }
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct SystemPromptPreset {
160 pub name: String,
161 pub description: String,
162 pub content: String,
163}
164
165pub fn builtin_system_prompt_presets() -> Vec<SystemPromptPreset> {
167 vec![
168 SystemPromptPreset {
169 name: "General".into(),
170 description: "General-purpose assistant".into(),
171 content: "You are a helpful assistant.".into(),
172 },
173 SystemPromptPreset {
174 name: "Coder".into(),
175 description: "Expert software developer".into(),
176 content: "You are an expert software developer. Write clean, well-documented code. Explain your reasoning and suggest improvements.".into(),
177 },
178 SystemPromptPreset {
179 name: "Thinker".into(),
180 description: "Analytical and thoughtful".into(),
181 content: "You are a thoughtful and analytical AI assistant. Think carefully before answering. Provide well-reasoned responses with clear explanations.".into(),
182 },
183 SystemPromptPreset {
184 name: "Mathematician".into(),
185 description: "Expert in mathematics".into(),
186 content: "You are an expert in mathematics. Provide clear, step-by-step solutions to mathematical problems. Show your reasoning and explain key concepts.".into(),
187 },
188 ]
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
192pub struct ModelOverride {
193 pub context_length: Option<u32>,
195 pub batch_size: Option<u32>,
196 pub ubatch_size: Option<u32>,
197 pub cache_type_k: Option<CacheTypeK>,
198 pub cache_type_v: Option<CacheTypeV>,
199 pub keep: Option<i32>,
200 pub swa_full: Option<bool>,
201 pub mlock: Option<bool>,
202 pub mmap: Option<bool>,
203 pub numa: Option<NumMode>,
204 pub uniform_cache: Option<bool>,
205 pub system_prompt: Option<String>,
206 pub system_prompt_preset_name: Option<String>,
207 pub max_concurrent_predictions: Option<u32>,
208 pub threads: Option<u32>,
209 pub threads_batch: Option<u32>,
210 pub parallel: Option<u32>,
211
212 pub gpu_layers: Option<i32>,
214 pub split_mode: Option<SplitMode>,
215 pub tensor_split: Option<String>,
216 pub main_gpu: Option<i32>,
217 pub fit: Option<bool>,
218 pub lora: Option<PathBuf>,
219 pub lora_scaled: Option<(PathBuf, f32)>,
220 pub rpc: Option<String>,
221 pub embedding: Option<bool>,
222 pub kv_cache_offload: Option<bool>,
223 pub flash_attn: Option<bool>,
224 pub jinja: Option<bool>,
225 pub chat_template: Option<String>,
226 pub chat_template_kwargs: Option<String>,
227 pub expert_count: Option<i32>,
228 pub gpu_layers_mode: Option<crate::models::GpuLayersMode>,
229
230 pub seed: Option<i32>,
232 pub temperature: Option<f32>,
233 pub top_k: Option<i32>,
234 pub top_p: Option<f32>,
235 pub min_p: Option<f32>,
236 pub typical_p: Option<f32>,
237 pub mirostat: Option<Mirostat>,
238 pub mirostat_lr: Option<f32>,
239 pub mirostat_ent: Option<f32>,
240 pub ignore_eos: Option<bool>,
241 pub samplers: Option<Samplers>,
242
243 pub repeat_penalty: Option<f32>,
245 pub repeat_last_n: Option<i32>,
246 pub presence_penalty: Option<f32>,
247 pub frequency_penalty: Option<f32>,
248 pub dry_multiplier: Option<f32>,
249 pub dry_base: Option<f32>,
250 pub dry_allowed_length: Option<i32>,
251 pub dry_penalty_last_n: Option<i32>,
252
253 pub rope_scaling: Option<RopeScaling>,
255 pub rope_scale: Option<f32>,
256 pub rope_freq_base: Option<f32>,
257 pub rope_freq_scale: Option<f32>,
258 pub rope_yarn_enabled: Option<bool>,
259
260 pub cache_prompt: Option<bool>,
262 pub cache_reuse: Option<u32>,
263 pub webui: Option<bool>,
264
265 pub max_tokens: Option<u32>,
267 pub cache_type: Option<CacheType>,
268 pub llama_cpp_version_cpu: Option<String>,
269 pub llama_cpp_version_vulkan: Option<String>,
270 pub llama_cpp_version_rocm: Option<String>,
271 pub llama_cpp_version_rocm_lemonade: Option<String>,
272 pub llama_cpp_version_cuda: Option<String>,
273 pub spec_type: Option<String>,
274 pub draft_tokens: Option<u32>,
275 pub tags: Option<Vec<String>>,
276}
277
278macro_rules! apply_scalar {
280 ($self:ident, $base:ident, $($field:ident),+ $(,)?) => {
281 $(
282 $base.$field = $self.$field.unwrap_or($base.$field);
283 )+
284 };
285}
286
287macro_rules! apply_clone {
289 ($self:ident, $base:ident, $($field:ident),+ $(,)?) => {
290 $(
291 if let Some(v) = &$self.$field {
292 $base.$field = v.clone();
293 }
294 )+
295 };
296}
297
298macro_rules! apply_option {
300 ($self:ident, $base:ident, $($field:ident),+ $(,)?) => {
301 $(
302 if let Some(v) = &$self.$field {
303 $base.$field = Some(v.clone());
304 }
305 )+
306 };
307}
308
309impl ModelOverride {
310 pub fn from_settings(s: &crate::models::ModelSettings) -> Self {
311 Self {
312 context_length: Some(s.context_length),
313 batch_size: Some(s.batch_size),
314 ubatch_size: Some(s.ubatch_size),
315 cache_type_k: s.cache_type_k,
316 cache_type_v: s.cache_type_v,
317 keep: Some(s.keep),
318 swa_full: Some(s.swa_full),
319 mlock: Some(s.mlock),
320 mmap: Some(s.mmap),
321 numa: Some(s.numa),
322 uniform_cache: Some(s.uniform_cache),
323 system_prompt: Some(s.system_prompt.clone()),
324 system_prompt_preset_name: Some(s.system_prompt_preset_name.clone()),
325 max_concurrent_predictions: s.max_concurrent_predictions,
326 threads: Some(s.threads),
327 threads_batch: Some(s.threads_batch),
328 parallel: Some(s.parallel),
329 gpu_layers: Some(match s.gpu_layers_mode {
330 crate::models::GpuLayersMode::Auto => 0,
331 crate::models::GpuLayersMode::Specific(n) => n as i32,
332 crate::models::GpuLayersMode::All => -1,
333 }),
334 gpu_layers_mode: Some(s.gpu_layers_mode),
335 split_mode: Some(s.split_mode),
336 tensor_split: Some(s.tensor_split.clone()),
337 main_gpu: Some(s.main_gpu),
338 fit: Some(s.fit),
339 lora: s.lora.clone(),
340 lora_scaled: s.lora_scaled.clone(),
341 rpc: Some(s.rpc.clone()),
342 embedding: Some(s.embedding),
343 kv_cache_offload: Some(s.kv_cache_offload),
344 flash_attn: Some(s.flash_attn),
345 jinja: Some(s.jinja),
346 chat_template: s.chat_template.clone(),
347 chat_template_kwargs: s.chat_template_kwargs.clone(),
348 expert_count: Some(s.expert_count),
349 seed: Some(s.seed),
350 temperature: Some(s.temperature),
351 top_k: Some(s.top_k),
352 top_p: Some(s.top_p),
353 min_p: Some(s.min_p),
354 typical_p: Some(s.typical_p),
355 mirostat: Some(s.mirostat),
356 mirostat_lr: Some(s.mirostat_lr),
357 mirostat_ent: Some(s.mirostat_ent),
358 ignore_eos: Some(s.ignore_eos),
359 samplers: Some(s.samplers.clone()),
360 repeat_penalty: Some(s.repeat_penalty),
361 repeat_last_n: Some(s.repeat_last_n),
362 presence_penalty: s.presence_penalty,
363 frequency_penalty: s.frequency_penalty,
364 dry_multiplier: Some(s.dry_multiplier),
365 dry_base: Some(s.dry_base),
366 dry_allowed_length: Some(s.dry_allowed_length),
367 dry_penalty_last_n: Some(s.dry_penalty_last_n),
368 rope_scaling: Some(s.rope_scaling),
369 rope_scale: Some(s.rope_scale),
370 rope_freq_base: Some(s.rope_freq_base),
371 rope_freq_scale: Some(s.rope_freq_scale),
372 rope_yarn_enabled: Some(s.rope_yarn_enabled),
373 cache_prompt: Some(s.cache_prompt),
374 cache_reuse: Some(s.cache_reuse),
375 webui: Some(s.webui),
376 max_tokens: s.max_tokens,
377 cache_type: Some(s.cache_type),
378 llama_cpp_version_cpu: s.llama_cpp_version_cpu.clone(),
379 llama_cpp_version_vulkan: s.llama_cpp_version_vulkan.clone(),
380 llama_cpp_version_rocm: s.llama_cpp_version_rocm.clone(),
381 llama_cpp_version_rocm_lemonade: s.llama_cpp_version_rocm_lemonade.clone(),
382 llama_cpp_version_cuda: s.llama_cpp_version_cuda.clone(),
383 spec_type: Some(s.spec_type.clone()),
384 draft_tokens: Some(s.draft_tokens),
385 tags: Some(s.tags.clone()),
386 }
387 }
388
389 pub fn apply(&self, base: &mut crate::models::ModelSettings) {
391 apply_scalar!(
396 self,
397 base,
398 context_length,
399 batch_size,
400 ubatch_size,
401 keep,
402 swa_full,
403 mlock,
404 mmap,
405 numa,
406 uniform_cache,
407 kv_cache_offload,
408 threads,
409 threads_batch,
410 parallel,
411 split_mode,
412 main_gpu,
413 fit,
414 embedding,
415 flash_attn,
416 jinja,
417 expert_count,
418 seed,
419 temperature,
420 top_k,
421 top_p,
422 min_p,
423 typical_p,
424 mirostat,
425 mirostat_lr,
426 mirostat_ent,
427 ignore_eos,
428 repeat_penalty,
429 repeat_last_n,
430 dry_multiplier,
431 dry_base,
432 dry_allowed_length,
433 dry_penalty_last_n,
434 rope_scaling,
435 rope_scale,
436 rope_freq_base,
437 rope_freq_scale,
438 rope_yarn_enabled,
439 cache_prompt,
440 cache_reuse,
441 webui,
442 cache_type,
443 draft_tokens,
444 gpu_layers_mode,
445 );
446
447 apply_clone!(
449 self,
450 base,
451 system_prompt,
452 system_prompt_preset_name,
453 tensor_split,
454 rpc,
455 samplers,
456 spec_type,
457 tags,
458 );
459
460 apply_option!(
462 self,
463 base,
464 lora,
465 lora_scaled,
466 chat_template,
467 chat_template_kwargs,
468 llama_cpp_version_cpu,
469 llama_cpp_version_vulkan,
470 llama_cpp_version_rocm,
471 llama_cpp_version_rocm_lemonade,
472 llama_cpp_version_cuda,
473 );
474
475 base.cache_type_k = self.cache_type_k;
477 base.cache_type_v = self.cache_type_v;
478 base.presence_penalty = self.presence_penalty;
479 base.frequency_penalty = self.frequency_penalty;
480 base.max_tokens = self.max_tokens;
481
482 base.max_concurrent_predictions = self
484 .max_concurrent_predictions
485 .or(base.max_concurrent_predictions);
486
487 if let Some(n) = self.gpu_layers {
490 base.gpu_layers_mode = match n {
491 n if n < 0 => crate::models::GpuLayersMode::All,
492 n => crate::models::GpuLayersMode::Specific(n as u32),
493 };
494 }
495
496 }
512}
513
514pub fn builtin_profiles() -> Vec<Profile> {
516 vec![
517 Profile {
518 name: "Qwen".into(),
519 description: "Optimized for Qwen models (dense)".into(),
520 settings: ModelOverride {
521 context_length: Some(131072),
522 temperature: Some(0.7),
523 top_k: Some(20),
524 top_p: Some(0.95),
525 max_tokens: Some(4096),
526 presence_penalty: Some(0.0),
527 uniform_cache: Some(true),
528 jinja: Some(true),
529 ..Default::default()
530 },
531 },
532 Profile {
533 name: "Qwen-MoE".into(),
534 description: "Optimized for Qwen MoE models (35B-A3B)".into(),
535 settings: ModelOverride {
536 context_length: Some(131072),
537 temperature: Some(0.8),
538 top_k: Some(20),
539 top_p: Some(0.95),
540 max_tokens: Some(4096),
541 presence_penalty: Some(1.5),
542 uniform_cache: Some(true),
543 jinja: Some(true),
544 ..Default::default()
545 },
546 },
547 Profile {
548 name: "Qwen-Coding".into(),
549 description: "Optimized for Qwen models in coding mode".into(),
550 settings: ModelOverride {
551 context_length: Some(131072),
552 temperature: Some(0.6),
553 top_k: Some(20),
554 top_p: Some(0.95),
555 max_tokens: Some(4096),
556 presence_penalty: Some(0.0),
557 uniform_cache: Some(true),
558 jinja: Some(true),
559 ..Default::default()
560 },
561 },
562 Profile {
563 name: "Gemma".into(),
564 description: "Optimized for Gemma 2/4 models".into(),
565 settings: ModelOverride {
566 context_length: Some(131072),
567 min_p: Some(0.1),
568 temperature: Some(1.0),
569 top_k: Some(65),
570 top_p: Some(0.95),
571 max_tokens: Some(4096),
572 uniform_cache: Some(true),
573 jinja: Some(true),
574 ..Default::default()
575 },
576 },
577 Profile {
578 name: "Llama".into(),
579 description: "Optimized for Llama 3.1/3.3 models".into(),
580 settings: ModelOverride {
581 context_length: Some(131072),
582 temperature: Some(0.7),
583 top_p: Some(0.9),
584 repeat_penalty: Some(1.1),
585 max_tokens: Some(4096),
586 uniform_cache: Some(true),
587 jinja: Some(true),
588 ..Default::default()
589 },
590 },
591 Profile {
592 name: "Mistral".into(),
593 description: "Optimized for Mistral 7B/NeMo models".into(),
594 settings: ModelOverride {
595 context_length: Some(131072),
596 temperature: Some(0.7),
597 top_k: Some(50),
598 top_p: Some(0.9),
599 max_tokens: Some(4096),
600 uniform_cache: Some(true),
601 jinja: Some(true),
602 ..Default::default()
603 },
604 },
605 Profile {
606 name: "Phi".into(),
607 description: "Optimized for Phi 3.5 Mini models".into(),
608 settings: ModelOverride {
609 context_length: Some(131072),
610 temperature: Some(0.7),
611 top_k: Some(50),
612 top_p: Some(0.9),
613 repeat_penalty: Some(1.1),
614 max_tokens: Some(4096),
615 uniform_cache: Some(true),
616 ..Default::default()
617 },
618 },
619 ]
620}
621
622#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
623#[serde(default)]
624pub struct DefaultParams {
625 #[serde(default)]
627 pub context_length: u32,
628 #[serde(default)]
629 pub threads: u32,
630 #[serde(default)]
631 pub threads_batch: u32,
632 #[serde(default)]
633 pub batch_size: u32,
634 #[serde(default)]
635 pub ubatch_size: u32,
636 #[serde(default = "default_cache_type_k")]
637 pub cache_type_k: Option<CacheTypeK>,
638 #[serde(default = "default_cache_type_v")]
639 pub cache_type_v: Option<CacheTypeV>,
640 #[serde(default)]
641 pub keep: i32,
642 #[serde(default)]
643 pub swa_full: bool,
644 #[serde(default)]
645 pub mlock: bool,
646 #[serde(default)]
647 pub mmap: bool,
648 #[serde(default)]
649 pub numa: NumMode,
650 #[serde(default)]
651 pub uniform_cache: bool,
652 #[serde(default)]
653 pub kv_cache_offload: bool,
654 #[serde(default)]
655 pub parallel: u32,
656 #[serde(default)]
657 pub max_concurrent_predictions: Option<u32>,
658 #[serde(default)]
659 pub system_prompt: String,
660 #[serde(default = "default_system_prompt_preset_name")]
661 pub system_prompt_preset_name: String,
662 #[serde(default)]
664 pub gpu_layers: i32,
665 #[serde(default = "default_gpu_layers_mode")]
666 pub gpu_layers_mode: crate::models::GpuLayersMode,
667 #[serde(default)]
668 pub split_mode: SplitMode,
669 #[serde(default)]
670 pub tensor_split: String,
671 #[serde(default)]
672 pub main_gpu: i32,
673 #[serde(default)]
674 pub fit: bool,
675 #[serde(default)]
676 pub lora: Option<PathBuf>,
677 #[serde(default)]
678 pub lora_scaled: Option<(PathBuf, f32)>,
679 #[serde(default)]
680 pub rpc: String,
681 #[serde(default)]
682 pub embedding: bool,
683 #[serde(default)]
684 pub flash_attn: bool,
685 #[serde(default)]
686 pub jinja: bool,
687 #[serde(default)]
688 pub chat_template: Option<String>,
689 #[serde(default)]
690 pub chat_template_kwargs: Option<String>,
691 #[serde(default)]
692 pub expert_count: i32,
693
694 #[serde(default)]
696 pub seed: i32,
697 #[serde(default)]
698 pub temperature: f32,
699 #[serde(default)]
700 pub top_k: i32,
701 #[serde(default)]
702 pub top_p: f32,
703 #[serde(default)]
704 pub min_p: f32,
705 #[serde(default)]
706 pub typical_p: f32,
707 #[serde(default)]
708 pub mirostat: Mirostat,
709 #[serde(default)]
710 pub mirostat_lr: f32,
711 #[serde(default)]
712 pub mirostat_ent: f32,
713 #[serde(default)]
714 pub ignore_eos: bool,
715 #[serde(default)]
716 pub samplers: Samplers,
717
718 #[serde(default)]
720 pub repeat_penalty: f32,
721 #[serde(default)]
722 pub repeat_last_n: i32,
723 #[serde(default = "default_presence_penalty")]
724 pub presence_penalty: Option<f32>,
725 #[serde(default = "default_frequency_penalty")]
726 pub frequency_penalty: Option<f32>,
727 #[serde(default)]
728 pub dry_multiplier: f32,
729 #[serde(default)]
730 pub dry_base: f32,
731 #[serde(default)]
732 pub dry_allowed_length: i32,
733 #[serde(default)]
734 pub dry_penalty_last_n: i32,
735
736 #[serde(default)]
738 pub rope_scaling: RopeScaling,
739 #[serde(default)]
740 pub rope_scale: f32,
741 #[serde(default)]
742 pub rope_freq_base: f32,
743 #[serde(default)]
744 pub rope_freq_scale: f32,
745 #[serde(default)]
746 pub rope_yarn_enabled: bool,
747
748 #[serde(default)]
750 pub host: String,
751 #[serde(default)]
752 pub port: u16,
753 #[serde(default)]
754 pub timeout: u32,
755 #[serde(default = "default_cache_prompt")]
756 pub cache_prompt: bool,
757 #[serde(default)]
758 pub cache_reuse: u32,
759 #[serde(default)]
760 pub webui: bool,
761 #[serde(default)]
762 pub ws_server_enabled: bool,
763 #[serde(default = "default_ws_server_port")]
764 pub ws_server_port: u16,
765 #[serde(default)]
766 pub ws_server_auth_key: Option<String>,
767 #[serde(default = "default_ws_server_tls_enabled")]
768 pub ws_server_tls_enabled: bool,
769 #[serde(default)]
770 pub ws_server_tls_cert: Option<String>,
771 #[serde(default)]
772 pub ws_server_tls_key: Option<String>,
773 #[serde(default)]
774 pub router_max_models: u32,
775 #[serde(default)]
776 pub server_mode: crate::models::ServerMode,
777
778 #[serde(default = "default_max_tokens")]
780 pub max_tokens: Option<u32>,
781 #[serde(default)]
782 pub cache_type: CacheType,
783 #[serde(default)]
784 pub backend: Backend,
785 #[serde(default)]
787 pub platform: Option<String>,
788 #[serde(default)]
789 pub llama_cpp_version_cpu: Option<String>,
790 #[serde(default)]
791 pub llama_cpp_version_vulkan: Option<String>,
792 #[serde(default)]
793 pub llama_cpp_version_rocm: Option<String>,
794 #[serde(default)]
795 pub llama_cpp_version_rocm_lemonade: Option<String>,
796 #[serde(default)]
797 pub llama_cpp_version_cuda: Option<String>,
798
799 #[serde(default)]
801 pub api_endpoint_enabled: bool,
802 #[serde(default = "default_api_endpoint_port")]
803 pub api_endpoint_port: u16,
804 #[serde(default)]
805 pub spec_type: String,
806 #[serde(default)]
807 pub draft_tokens: u32,
808 #[serde(default)]
809 pub tags: Vec<String>,
810}
811
812fn default_api_endpoint_port() -> u16 {
813 49222
814}
815
816fn default_system_prompt_preset_name() -> String {
817 "General".to_string()
818}
819
820fn default_cache_type_k() -> Option<CacheTypeK> {
821 None
822}
823fn default_cache_type_v() -> Option<CacheTypeV> {
824 None
825}
826fn default_presence_penalty() -> Option<f32> {
827 None
828}
829fn default_frequency_penalty() -> Option<f32> {
830 None
831}
832fn default_max_tokens() -> Option<u32> {
833 None
834}
835fn default_cache_prompt() -> bool {
836 true
837}
838fn default_ws_server_port() -> u16 {
839 49223
840}
841
842fn default_ws_server_tls_enabled() -> bool {
843 true
844}
845
846fn default_gpu_layers_mode() -> crate::models::GpuLayersMode {
847 crate::models::GpuLayersMode::Auto
848}
849
850impl Default for DefaultParams {
851 fn default() -> Self {
852 Self {
853 context_length: 131072,
855 threads: physical_cores(),
856 threads_batch: 8,
857 batch_size: 512,
858 ubatch_size: 512,
859 cache_type_k: None,
860 cache_type_v: None,
861 keep: 0,
862 swa_full: false,
863 mlock: false,
864 mmap: true,
865 numa: NumMode::None,
866 uniform_cache: true,
867 kv_cache_offload: true,
868 parallel: 1,
869 max_concurrent_predictions: None,
870 system_prompt: DEFAULT_SYSTEM_PROMPT.to_string(),
871 system_prompt_preset_name: "Coder".to_string(),
872
873 gpu_layers: -1,
875 gpu_layers_mode: crate::models::GpuLayersMode::Auto,
876 split_mode: SplitMode::Layer,
877 tensor_split: String::new(),
878 main_gpu: 0,
879 fit: true,
880 lora: None,
881 lora_scaled: None,
882 rpc: String::new(),
883 embedding: false,
884 flash_attn: true,
885 jinja: true,
886 chat_template: None,
887 chat_template_kwargs: None,
888 expert_count: -1,
889
890 seed: -1,
892 temperature: 0.8,
893 top_k: 40,
894 top_p: 0.95,
895 min_p: 0.0,
896 typical_p: 1.0,
897 mirostat: Mirostat::Off,
898 mirostat_lr: 0.1,
899 mirostat_ent: 5.0,
900 ignore_eos: false,
901 samplers: Samplers::default(),
902
903 repeat_penalty: 1.1,
905 repeat_last_n: 64,
906 presence_penalty: None,
907 frequency_penalty: None,
908 dry_multiplier: 0.0,
909 dry_base: 1.75,
910 dry_allowed_length: 2,
911 dry_penalty_last_n: -1,
912
913 rope_scaling: RopeScaling::None,
915 rope_scale: 1.0,
916 rope_freq_base: 0.0,
917 rope_freq_scale: 1.0,
918 rope_yarn_enabled: false,
919
920 host: "127.0.0.1".to_string(),
922 port: 8080,
923 timeout: 600,
924 cache_prompt: true,
925 cache_reuse: 0,
926 webui: false,
927 ws_server_enabled: false,
928 ws_server_port: 49223,
929 ws_server_auth_key: None,
930 ws_server_tls_enabled: true,
931 ws_server_tls_cert: None,
932 ws_server_tls_key: None,
933 router_max_models: 4,
934 server_mode: crate::models::ServerMode::Normal,
935
936 max_tokens: None,
938 cache_type: CacheType::F16,
939 backend: {
940 use crate::backend::hardware::{GpuVendor, detect_gpu_vendors};
941 let vendors = detect_gpu_vendors();
942 let mut result = Backend::Cpu;
943 for v in &vendors {
944 if matches!(v, GpuVendor::Nvidia) {
945 result = Backend::Cuda;
946 break;
947 }
948 if matches!(v, GpuVendor::Amd) {
949 result = Backend::Rocm;
950 break;
951 }
952 if matches!(v, GpuVendor::Intel) {
953 result = Backend::Vulkan;
954 break;
955 }
956 }
957 result
958 },
959 platform: None,
960 llama_cpp_version_cpu: None,
961 llama_cpp_version_vulkan: None,
962 llama_cpp_version_rocm: None,
963 llama_cpp_version_rocm_lemonade: None,
964 llama_cpp_version_cuda: None,
965 api_endpoint_enabled: false,
966 api_endpoint_port: 49222,
967 spec_type: String::new(),
968 draft_tokens: 0,
969 tags: Vec::new(),
970 }
971 }
972}
973
974impl Default for Config {
975 fn default() -> Self {
976 Self {
977 models_dirs: vec![
978 dirs::data_dir()
979 .unwrap_or_default()
980 .join("llm-manager")
981 .join("models"),
982 ],
983 llama_server: "llama-server".into(),
984 default: DefaultParams::default(),
985 model_overrides: ModelConfigStore::new(vec![]),
986 profiles: Default::default(),
987 system_prompt_presets: Default::default(),
988 rpc_workers: Vec::new(),
989 search_limit: default_search_limit(),
990 active_panel: ActivePanel::Models,
991 left_pct: 55,
992 language: default_language(),
993 onboarding_complete: false,
994 }
995 }
996}
997
998impl Config {
999 pub fn config_path() -> PathBuf {
1000 config_base_dir().join("llm-manager").join("config.yaml")
1001 }
1002
1003 fn config_keys() -> &'static [&'static str] {
1005 &[
1006 "models_dirs", "llama_server", "default", "model_overrides", "profiles",
1007 "system_prompt_presets", "rpc_workers", "search_limit", "active_panel",
1008 "left_pct", "language", "onboarding_complete",
1009 ]
1010 }
1011
1012 fn default_params_keys() -> &'static [&'static str] {
1014 &[
1015 "context_length", "threads", "threads_batch", "batch_size", "ubatch_size",
1016 "cache_type_k", "cache_type_v", "keep", "swa_full", "mlock", "mmap", "numa",
1017 "uniform_cache", "kv_cache_offload", "parallel", "max_concurrent_predictions",
1018 "system_prompt", "system_prompt_preset_name", "gpu_layers", "gpu_layers_mode",
1019 "split_mode", "tensor_split", "main_gpu", "fit", "lora", "lora_scaled", "rpc",
1020 "embedding", "flash_attn", "jinja", "chat_template", "chat_template_kwargs",
1021 "expert_count", "seed", "temperature", "top_k", "top_p", "min_p", "typical_p",
1022 "mirostat", "mirostat_lr", "mirostat_ent", "ignore_eos", "samplers",
1023 "repeat_penalty", "repeat_last_n", "presence_penalty", "frequency_penalty",
1024 "dry_multiplier", "dry_base", "dry_allowed_length", "dry_penalty_last_n",
1025 "rope_scaling", "rope_scale", "rope_freq_base", "rope_freq_scale",
1026 "rope_yarn_enabled", "host", "port", "timeout", "cache_prompt", "cache_reuse",
1027 "webui", "ws_server_enabled", "ws_server_port", "ws_server_auth_key",
1028 "ws_server_tls_enabled", "ws_server_tls_cert", "ws_server_tls_key",
1029 "router_max_models", "server_mode", "max_tokens", "cache_type", "backend",
1030 "platform", "llama_cpp_version_cpu", "llama_cpp_version_vulkan",
1031 "llama_cpp_version_rocm", "llama_cpp_version_rocm_lemonade",
1032 "llama_cpp_version_cuda", "api_endpoint_enabled", "api_endpoint_port",
1033 "spec_type", "draft_tokens", "tags",
1034 ]
1035 }
1036
1037 fn model_override_keys() -> &'static [&'static str] {
1039 &[
1040 "context_length", "batch_size", "ubatch_size", "cache_type_k", "cache_type_v",
1041 "keep", "swa_full", "mlock", "mmap", "numa", "uniform_cache", "system_prompt",
1042 "system_prompt_preset_name", "max_concurrent_predictions", "threads",
1043 "threads_batch", "parallel", "gpu_layers", "split_mode", "tensor_split",
1044 "main_gpu", "fit", "lora", "lora_scaled", "rpc", "embedding", "kv_cache_offload",
1045 "flash_attn", "jinja", "chat_template", "chat_template_kwargs", "expert_count",
1046 "gpu_layers_mode", "seed", "temperature", "top_k", "top_p", "min_p", "typical_p",
1047 "mirostat", "mirostat_lr", "mirostat_ent", "ignore_eos", "samplers",
1048 "repeat_penalty", "repeat_last_n", "presence_penalty", "frequency_penalty",
1049 "dry_multiplier", "dry_base", "dry_allowed_length", "dry_penalty_last_n",
1050 "rope_scaling", "rope_scale", "rope_freq_base", "rope_freq_scale",
1051 "rope_yarn_enabled", "cache_prompt", "cache_reuse", "webui", "max_tokens",
1052 "cache_type", "llama_cpp_version_cpu", "llama_cpp_version_vulkan",
1053 "llama_cpp_version_rocm", "llama_cpp_version_rocm_lemonade",
1054 "llama_cpp_version_cuda", "spec_type", "draft_tokens", "tags",
1055 ]
1056 }
1057
1058 fn rpc_worker_keys() -> &'static [&'static str] {
1060 &["selected", "name", "ip", "port"]
1061 }
1062
1063 pub fn validate_unknown_fields(value: &serde_yaml::Value) -> Vec<ValidationWarning> {
1065 let mut warnings = Vec::new();
1066 Self::check_unknown_fields(value, "", Self::config_keys(), &mut warnings);
1067 warnings
1068 }
1069
1070 fn check_unknown_fields(
1071 value: &serde_yaml::Value,
1072 prefix: &str,
1073 known_keys: &'static [&'static str],
1074 warnings: &mut Vec<ValidationWarning>,
1075 ) {
1076 if let serde_yaml::Value::Mapping(map) = value {
1077 for key in map.keys() {
1078 if let Some(key_str) = key.as_str()
1079 && !known_keys.contains(&key_str) {
1080 let field = if prefix.is_empty() {
1081 key_str.to_string()
1082 } else {
1083 format!("{}.{}", prefix, key_str)
1084 };
1085 warnings.push(ValidationWarning {
1086 field,
1087 message: format!("Unknown config field: {}", key_str),
1088 severity: ValidationSeverity::Warning,
1089 });
1090 }
1091 }
1092
1093 for (key, val) in map {
1095 if let Some(key_str) = key.as_str() {
1096 let new_prefix = if prefix.is_empty() {
1097 key_str.to_string()
1098 } else {
1099 format!("{}.{}", prefix, key_str)
1100 };
1101
1102 match key_str {
1103 "default" => {
1104 Self::check_unknown_fields(val, &new_prefix, Self::default_params_keys(), warnings);
1105 }
1106 "model_overrides" => {
1107 if let serde_yaml::Value::Mapping(overrides) = val {
1108 for (override_key, override_val) in overrides {
1109 if let Some(k) = override_key.as_str() {
1110 let override_prefix = format!("{}.{}", new_prefix, k);
1111 Self::check_unknown_fields(
1112 override_val,
1113 &override_prefix,
1114 Self::model_override_keys(),
1115 warnings,
1116 );
1117 }
1118 }
1119 }
1120 }
1121 "rpc_workers" => {
1122 if let serde_yaml::Value::Sequence(items) = val {
1123 for item in items {
1124 Self::check_unknown_fields(item, &new_prefix, Self::rpc_worker_keys(), warnings);
1125 }
1126 }
1127 }
1128 _ => {
1129 Self::check_unknown_fields(val, &new_prefix, known_keys, warnings);
1130 }
1131 }
1132 }
1133 }
1134 }
1135 }
1136
1137 pub fn validate(&self) -> Vec<ValidationWarning> {
1139 let mut warnings = Vec::new();
1140 let default = &self.default;
1141
1142 if default.context_length < 512 || default.context_length > 1048576 {
1144 warnings.push(ValidationWarning {
1145 field: "default.context_length".to_string(),
1146 message: format!(
1147 "context_length {} is outside recommended range 512-1048576",
1148 default.context_length
1149 ),
1150 severity: ValidationSeverity::Warning,
1151 });
1152 }
1153 if default.threads == 0 {
1154 warnings.push(ValidationWarning {
1155 field: "default.threads".to_string(),
1156 message: "threads must be greater than 0".to_string(),
1157 severity: ValidationSeverity::Warning,
1158 });
1159 }
1160 if default.temperature < 0.0 || default.temperature > 2.0 {
1161 warnings.push(ValidationWarning {
1162 field: "default.temperature".to_string(),
1163 message: format!(
1164 "temperature {} is outside recommended range 0.0-2.0",
1165 default.temperature
1166 ),
1167 severity: ValidationSeverity::Warning,
1168 });
1169 }
1170 if default.top_k < 0 {
1171 warnings.push(ValidationWarning {
1172 field: "default.top_k".to_string(),
1173 message: "top_k must be >= 0".to_string(),
1174 severity: ValidationSeverity::Warning,
1175 });
1176 }
1177 if default.top_p < 0.0 || default.top_p > 1.0 {
1178 warnings.push(ValidationWarning {
1179 field: "default.top_p".to_string(),
1180 message: format!(
1181 "top_p {} is outside recommended range 0.0-1.0",
1182 default.top_p
1183 ),
1184 severity: ValidationSeverity::Warning,
1185 });
1186 }
1187 if default.min_p < 0.0 || default.min_p > 1.0 {
1188 warnings.push(ValidationWarning {
1189 field: "default.min_p".to_string(),
1190 message: format!(
1191 "min_p {} is outside recommended range 0.0-1.0",
1192 default.min_p
1193 ),
1194 severity: ValidationSeverity::Warning,
1195 });
1196 }
1197 if default.typical_p < 0.0 || default.typical_p > 1.0 {
1198 warnings.push(ValidationWarning {
1199 field: "default.typical_p".to_string(),
1200 message: format!(
1201 "typical_p {} is outside recommended range 0.0-1.0",
1202 default.typical_p
1203 ),
1204 severity: ValidationSeverity::Warning,
1205 });
1206 }
1207 if default.repeat_penalty < 0.0 || default.repeat_penalty > 3.0 {
1208 warnings.push(ValidationWarning {
1209 field: "default.repeat_penalty".to_string(),
1210 message: format!(
1211 "repeat_penalty {} is outside recommended range 0.0-3.0",
1212 default.repeat_penalty
1213 ),
1214 severity: ValidationSeverity::Warning,
1215 });
1216 }
1217 if default.mirostat_lr < 0.0 || default.mirostat_lr > 1.0 {
1218 warnings.push(ValidationWarning {
1219 field: "default.mirostat_lr".to_string(),
1220 message: format!(
1221 "mirostat_lr {} is outside recommended range 0.0-1.0",
1222 default.mirostat_lr
1223 ),
1224 severity: ValidationSeverity::Warning,
1225 });
1226 }
1227 if default.mirostat_ent < 0.0 || default.mirostat_ent > 10.0 {
1228 warnings.push(ValidationWarning {
1229 field: "default.mirostat_ent".to_string(),
1230 message: format!(
1231 "mirostat_ent {} is outside recommended range 0.0-10.0",
1232 default.mirostat_ent
1233 ),
1234 severity: ValidationSeverity::Warning,
1235 });
1236 }
1237 if default.dry_multiplier < 0.0 {
1238 warnings.push(ValidationWarning {
1239 field: "default.dry_multiplier".to_string(),
1240 message: "dry_multiplier must be >= 0".to_string(),
1241 severity: ValidationSeverity::Warning,
1242 });
1243 }
1244 if default.dry_base <= 0.0 {
1245 warnings.push(ValidationWarning {
1246 field: "default.dry_base".to_string(),
1247 message: "dry_base must be > 0".to_string(),
1248 severity: ValidationSeverity::Warning,
1249 });
1250 }
1251 if default.rope_scale <= 0.0 {
1252 warnings.push(ValidationWarning {
1253 field: "default.rope_scale".to_string(),
1254 message: "rope_scale must be > 0".to_string(),
1255 severity: ValidationSeverity::Warning,
1256 });
1257 }
1258 if default.rope_freq_scale <= 0.0 {
1259 warnings.push(ValidationWarning {
1260 field: "default.rope_freq_scale".to_string(),
1261 message: "rope_freq_scale must be > 0".to_string(),
1262 severity: ValidationSeverity::Warning,
1263 });
1264 }
1265 if default.port == 0 {
1266 warnings.push(ValidationWarning {
1267 field: "default.port".to_string(),
1268 message: "port must be between 1 and 65535".to_string(),
1269 severity: ValidationSeverity::Error,
1270 });
1271 }
1272 if default.port < 1024 {
1273 warnings.push(ValidationWarning {
1274 field: "default.port".to_string(),
1275 message: format!(
1276 "port {} is a privileged port (< 1024)",
1277 default.port
1278 ),
1279 severity: ValidationSeverity::Warning,
1280 });
1281 }
1282 if default.api_endpoint_port == 0 {
1283 warnings.push(ValidationWarning {
1284 field: "default.api_endpoint_port".to_string(),
1285 message: "api_endpoint_port must be between 1 and 65535".to_string(),
1286 severity: ValidationSeverity::Error,
1287 });
1288 }
1289 if default.ws_server_port == 0 {
1290 warnings.push(ValidationWarning {
1291 field: "default.ws_server_port".to_string(),
1292 message: "ws_server_port must be between 1 and 65535".to_string(),
1293 severity: ValidationSeverity::Error,
1294 });
1295 }
1296 if default.timeout < 1 {
1297 warnings.push(ValidationWarning {
1298 field: "default.timeout".to_string(),
1299 message: "timeout must be at least 1 second".to_string(),
1300 severity: ValidationSeverity::Warning,
1301 });
1302 }
1303 if default.router_max_models == 0 {
1304 warnings.push(ValidationWarning {
1305 field: "default.router_max_models".to_string(),
1306 message: "router_max_models must be >= 1".to_string(),
1307 severity: ValidationSeverity::Warning,
1308 });
1309 }
1310 if let Some(max_tokens) = default.max_tokens {
1311 if max_tokens == 0 {
1312 warnings.push(ValidationWarning {
1313 field: "default.max_tokens".to_string(),
1314 message: "max_tokens must be > 0".to_string(),
1315 severity: ValidationSeverity::Warning,
1316 });
1317 } else if max_tokens > 65536 {
1318 warnings.push(ValidationWarning {
1319 field: "default.max_tokens".to_string(),
1320 message: format!(
1321 "max_tokens {} is very large (> 65536)",
1322 max_tokens
1323 ),
1324 severity: ValidationSeverity::Warning,
1325 });
1326 }
1327 }
1328 if default.context_length > 262144 {
1329 warnings.push(ValidationWarning {
1330 field: "default.context_length".to_string(),
1331 message: format!(
1332 "context_length {} may cause high memory usage",
1333 default.context_length
1334 ),
1335 severity: ValidationSeverity::Warning,
1336 });
1337 }
1338 if default.threads_batch > default.batch_size && default.batch_size > 0 {
1339 warnings.push(ValidationWarning {
1340 field: "default.threads_batch".to_string(),
1341 message: "threads_batch should not exceed batch_size".to_string(),
1342 severity: ValidationSeverity::Warning,
1343 });
1344 }
1345
1346 if let Some(lora) = &default.lora
1348 && !lora.exists()
1349 {
1350 warnings.push(ValidationWarning {
1351 field: "default.lora".to_string(),
1352 message: format!("lora path does not exist: {}", lora.display()),
1353 severity: ValidationSeverity::Warning,
1354 });
1355 }
1356 if let Some((lora, _)) = &default.lora_scaled
1357 && !lora.exists()
1358 {
1359 warnings.push(ValidationWarning {
1360 field: "default.lora_scaled".to_string(),
1361 message: format!("lora path does not exist: {}", lora.display()),
1362 severity: ValidationSeverity::Warning,
1363 });
1364 }
1365 if let Some(cert) = &default.ws_server_tls_cert
1366 && !cert.is_empty()
1367 && !std::path::Path::new(cert).exists()
1368 {
1369 warnings.push(ValidationWarning {
1370 field: "default.ws_server_tls_cert".to_string(),
1371 message: format!("TLS cert path does not exist: {}", cert),
1372 severity: ValidationSeverity::Warning,
1373 });
1374 }
1375 if let Some(key) = &default.ws_server_tls_key
1376 && !key.is_empty()
1377 && !std::path::Path::new(key).exists()
1378 {
1379 warnings.push(ValidationWarning {
1380 field: "default.ws_server_tls_key".to_string(),
1381 message: format!("TLS key path does not exist: {}", key),
1382 severity: ValidationSeverity::Warning,
1383 });
1384 }
1385 if self.llama_server.is_absolute()
1386 && !self.llama_server.exists()
1387 {
1388 warnings.push(ValidationWarning {
1389 field: "llama_server".to_string(),
1390 message: format!(
1391 "llama_server binary not found: {}",
1392 self.llama_server.display()
1393 ),
1394 severity: ValidationSeverity::Warning,
1395 });
1396 }
1397
1398 for model_name in self.model_overrides.keys() {
1400 if let Some(override_settings) = self.model_overrides.get(model_name.as_str()) {
1401 if let Some(lora) = &override_settings.lora
1402 && !lora.exists()
1403 {
1404 warnings.push(ValidationWarning {
1405 field: format!("model_overrides.{}.lora", model_name),
1406 message: format!(
1407 "lora path does not exist: {}",
1408 lora.display()
1409 ),
1410 severity: ValidationSeverity::Warning,
1411 });
1412 }
1413 if let Some((lora, _)) = &override_settings.lora_scaled
1414 && !lora.exists()
1415 {
1416 warnings.push(ValidationWarning {
1417 field: format!("model_overrides.{}.lora_scaled", model_name),
1418 message: format!(
1419 "lora path does not exist: {}",
1420 lora.display()
1421 ),
1422 severity: ValidationSeverity::Warning,
1423 });
1424 }
1425 }
1426 }
1427
1428 if default.port == default.api_endpoint_port {
1430 warnings.push(ValidationWarning {
1431 field: "default.port".to_string(),
1432 message: format!(
1433 "port conflict: server port and API endpoint port both set to {}",
1434 default.port
1435 ),
1436 severity: ValidationSeverity::Warning,
1437 });
1438 }
1439 if default.port == default.ws_server_port {
1440 warnings.push(ValidationWarning {
1441 field: "default.port".to_string(),
1442 message: format!(
1443 "port conflict: server port and WS server port both set to {}",
1444 default.port
1445 ),
1446 severity: ValidationSeverity::Warning,
1447 });
1448 }
1449 if default.api_endpoint_port == default.ws_server_port {
1450 warnings.push(ValidationWarning {
1451 field: "default.api_endpoint_port".to_string(),
1452 message: format!(
1453 "port conflict: API endpoint port and WS server port both set to {}",
1454 default.api_endpoint_port
1455 ),
1456 severity: ValidationSeverity::Warning,
1457 });
1458 }
1459 if matches!(default.server_mode, crate::models::ServerMode::Router)
1460 && default.router_max_models < 2
1461 {
1462 warnings.push(ValidationWarning {
1463 field: "default.router_max_models".to_string(),
1464 message: "router_max_models should be >= 2 for Router mode".to_string(),
1465 severity: ValidationSeverity::Warning,
1466 });
1467 }
1468 if !default.spec_type.is_empty() && default.draft_tokens == 0 {
1469 warnings.push(ValidationWarning {
1470 field: "default.spec_type".to_string(),
1471 message: "spec_type is set but draft_tokens is 0".to_string(),
1472 severity: ValidationSeverity::Warning,
1473 });
1474 }
1475 if default.ws_server_tls_enabled {
1476 if let Some(cert) = &default.ws_server_tls_cert
1477 && !cert.is_empty()
1478 && !std::path::Path::new(cert).exists()
1479 {
1480 warnings.push(ValidationWarning {
1481 field: "default.ws_server_tls_cert".to_string(),
1482 message: format!("TLS cert path does not exist: {}", cert),
1483 severity: ValidationSeverity::Warning,
1484 });
1485 }
1486 if let Some(key) = &default.ws_server_tls_key
1487 && !key.is_empty()
1488 && !std::path::Path::new(key).exists()
1489 {
1490 warnings.push(ValidationWarning {
1491 field: "default.ws_server_tls_key".to_string(),
1492 message: format!("TLS key path does not exist: {}", key),
1493 severity: ValidationSeverity::Warning,
1494 });
1495 }
1496 }
1497
1498 warnings
1499 }
1500
1501 pub fn resolve_settings(
1503 &self,
1504 model_name: Option<&str>,
1505 profile_name: Option<&str>,
1506 ) -> crate::models::ModelSettings {
1507 let mut settings = crate::models::ModelSettings::from_config(self);
1508
1509 if let Some(name) = model_name
1511 && let Some(override_settings) = self.model_overrides.get(name)
1512 {
1513 override_settings.apply(&mut settings);
1514 }
1515
1516 if let Some(p_name) = profile_name {
1518 if let Some(profile) = self.profiles.get(p_name) {
1519 profile.settings.apply(&mut settings);
1520 } else if let Some(profile) = builtin_profiles().iter().find(|p| p.name == p_name) {
1521 profile.settings.apply(&mut settings);
1522 }
1523 }
1524
1525 if let Some(preset) = self.system_prompt_presets.get(&settings.system_prompt_preset_name) {
1527 settings.system_prompt = preset.content.clone();
1528 }
1529
1530 settings
1531 }
1532
1533 pub fn get_preset_content(&self, name: &str) -> Option<String> {
1535 self.system_prompt_presets
1536 .get(name)
1537 .map(|p| p.content.clone())
1538 }
1539
1540 fn normalize_config(mut config: Config) -> Config {
1541 for path in &mut config.models_dirs {
1543 let path_str = path.to_string_lossy();
1544 if let Some(stripped) = path_str.strip_prefix("~/") {
1545 let home = dirs::home_dir().unwrap_or_default();
1546 *path = home.join(stripped);
1547 } else if !path.is_absolute() {
1548 let home = dirs::home_dir().unwrap_or_default();
1549 *path = home.join(path_str.as_ref());
1550 }
1551 }
1552
1553 for p in builtin_profiles() {
1555 if config.profiles.get(&p.name).is_none() {
1556 config.profiles.insert_builtin(p);
1557 }
1558 }
1559
1560 for p in builtin_system_prompt_presets() {
1562 if config.system_prompt_presets.get(&p.name).is_none() {
1563 config.system_prompt_presets.insert_builtin(p);
1564 }
1565 }
1566 config
1567 }
1568
1569 fn load_impl(path: &PathBuf) -> Result<Self, Box<dyn std::error::Error>> {
1570 let content = std::fs::read_to_string(path)?;
1571
1572 let parsed: serde_yaml::Value = serde_yaml::from_str(&content)
1574 .map_err(|e| format!("Failed to parse config file {}: {}", path.display(), e))?;
1575 let mut warnings = Self::validate_unknown_fields(&parsed);
1576
1577 let config: Config = serde_yaml::from_str(&content)
1579 .map_err(|e| format!("Failed to parse config file {}: {}", path.display(), e))?;
1580 let config = Self::normalize_config(config);
1581 let config = config.auto_detect_platform();
1582
1583 warnings.extend(config.validate());
1585
1586 if !warnings.is_empty() {
1588 eprintln!("Config validation warnings:");
1589 for warning in &warnings {
1590 eprintln!(
1591 " [{}] {}: {}",
1592 match warning.severity {
1593 ValidationSeverity::Warning => "WARN",
1594 ValidationSeverity::Error => "ERROR",
1595 },
1596 warning.field,
1597 warning.message
1598 );
1599 }
1600 }
1601 Ok(config)
1602 }
1603
1604 pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
1605 let path = Self::config_path();
1606 if path.exists() {
1607 Self::load_impl(&path)
1608 } else {
1609 let mut config = Config::default();
1610 config.save()?;
1611 Ok(config)
1612 }
1613 }
1614
1615 pub fn load_from(path: PathBuf) -> Result<Self, Box<dyn std::error::Error>> {
1616 if path.exists() {
1617 let content = std::fs::read_to_string(&path)?;
1618
1619 let parsed: serde_yaml::Value = serde_yaml::from_str(&content)
1621 .map_err(|e| format!("Failed to parse config file {}: {}", path.display(), e))?;
1622 let mut warnings = Self::validate_unknown_fields(&parsed);
1623
1624 let config: Config = serde_yaml::from_str(&content)
1626 .map_err(|e| format!("Failed to parse config file {}: {}", path.display(), e))?;
1627 let config = Self::normalize_config(config);
1628 let config = config.auto_detect_platform();
1629
1630 warnings.extend(config.validate());
1632
1633 if !warnings.is_empty() {
1634 eprintln!("Config validation warnings:");
1635 for warning in &warnings {
1636 eprintln!(
1637 " [{}] {}: {}",
1638 match warning.severity {
1639 ValidationSeverity::Warning => "WARN",
1640 ValidationSeverity::Error => "ERROR",
1641 },
1642 warning.field,
1643 warning.message
1644 );
1645 }
1646 }
1647 Ok(config)
1648 } else {
1649 Err(format!("Config file not found: {}", path.display()).into())
1650 }
1651 }
1652
1653 fn auto_detect_platform(mut self) -> Self {
1655 if self.default.platform.is_none() {
1656 self.default.platform =
1657 Some(
1658 crate::backend::hardware::platform_name(
1659 crate::backend::hardware::detect_platform(),
1660 )
1661 .to_string(),
1662 );
1663 }
1664 self
1665 }
1666
1667 pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
1668 let path = Self::config_path();
1669 if let Some(parent) = path.parent() {
1670 std::fs::create_dir_all(parent)?;
1671 }
1672 let content = serde_yaml::to_string(self)?;
1673 std::fs::write(&path, content)?;
1674 let entries: Vec<(String, ModelOverride)> = self
1676 .model_overrides
1677 .keys()
1678 .iter()
1679 .filter_map(|k| self.model_overrides.get(k).map(|v| (k.clone(), v.clone())))
1680 .collect();
1681 for (name, cfg) in entries {
1682 self.model_overrides.save(&name, &cfg);
1683 }
1684 for profile in self.profiles.user_profiles() {
1686 self.profiles.save(&profile);
1687 }
1688 for preset in self.system_prompt_presets.user_presets() {
1690 self.system_prompt_presets.save(&preset);
1691 }
1692 Ok(())
1693 }
1694
1695 pub fn merged_profiles(&self) -> Vec<Profile> {
1696 self.profiles.all()
1697 }
1698
1699 pub fn merged_presets(&self) -> Vec<SystemPromptPreset> {
1700 self.system_prompt_presets.all()
1701 }
1702}
1703
1704#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1705pub enum LogLevel {
1706 Info,
1707 Warning,
1708 Error,
1709}
1710
1711#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1712pub enum ValidationSeverity {
1713 Warning,
1714 Error,
1715}
1716
1717#[derive(Debug, Clone)]
1718pub struct ValidationWarning {
1719 pub field: String,
1720 pub message: String,
1721 pub severity: ValidationSeverity,
1722}
1723
1724impl LogLevel {
1725 pub fn label(&self) -> &'static str {
1726 match self {
1727 LogLevel::Info => "INFO",
1728 LogLevel::Warning => "WARNING",
1729 LogLevel::Error => "ERROR",
1730 }
1731 }
1732}
1733
1734#[derive(Debug, Clone)]
1735pub struct LogEntry {
1736 pub timestamp: String,
1737 pub level: LogLevel,
1738 pub message: String,
1739}
1740
1741impl LogEntry {
1742 pub fn new(message: impl Into<String>, level: LogLevel) -> Self {
1743 let timestamp = Local::now().format("%H:%M:%S").to_string();
1744 let message = sanitize_log(&message.into());
1745 Self {
1746 timestamp,
1747 level,
1748 message,
1749 }
1750 }
1751}
1752
1753fn sanitize_log(input: &str) -> String {
1756 let max_len = 2000;
1758 let chars: Vec<char> = input.chars().collect();
1759 let truncated = chars.len() > max_len;
1760 let chars = if truncated {
1761 chars[..max_len].to_vec()
1762 } else {
1763 chars
1764 };
1765
1766 let mut output = String::with_capacity(chars.len());
1767 for c in chars {
1768 if c.is_control() && c != '\n' && c != '\t' {
1771 continue;
1772 }
1773 output.push(c);
1774 }
1775
1776 let output = output.replace('\t', " ");
1778
1779 let mut result = output.trim_end().to_string();
1781 if truncated {
1782 result.push_str("... (truncated)");
1783 }
1784 result
1785}