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::{ModelConfigStore, display_from_key, key_from_display};
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_server_settings_height")]
116 pub server_settings_height: u16,
117 #[serde(default = "default_language")]
119 pub language: String,
120 #[serde(default = "default_onboarding")]
122 pub onboarding_complete: bool,
123}
124
125fn default_language() -> String {
126 "en".to_string()
127}
128
129fn default_left_pct() -> u16 {
130 55
131}
132
133fn default_server_settings_height() -> u16 {
134 9
135}
136
137fn default_onboarding() -> bool {
138 false
139}
140
141fn default_search_limit() -> u32 {
142 50
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
147pub struct Profile {
148 pub name: String,
149 pub description: String,
151 #[serde(default)]
153 pub settings: ModelOverride,
154}
155
156impl Profile {
157 pub fn apply(&self, mut base: crate::models::ModelSettings) -> crate::models::ModelSettings {
159 self.settings.apply(&mut base);
160 base
161 }
162}
163
164impl crate::config::store::NamedItem for Profile {
165 fn name(&self) -> &str {
166 &self.name
167 }
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct SystemPromptPreset {
173 pub name: String,
174 pub description: String,
175 pub content: String,
176}
177
178impl crate::config::store::NamedItem for SystemPromptPreset {
179 fn name(&self) -> &str {
180 &self.name
181 }
182}
183
184pub fn builtin_system_prompt_presets() -> Vec<SystemPromptPreset> {
186 vec![
187 SystemPromptPreset {
188 name: "General".into(),
189 description: "General-purpose assistant".into(),
190 content: "You are a helpful assistant.".into(),
191 },
192 SystemPromptPreset {
193 name: "Coder".into(),
194 description: "Expert software developer".into(),
195 content: "You are an expert software developer. Write clean, well-documented code. Explain your reasoning and suggest improvements. Split code to avoid too big files. Always try to re-use existing code, avoid duplicate function. In case of reviewing code: every claim must have file:line reference. For kernel Every Kconfig symbol must be verified against actual Kconfig files. Every XML example must be validated against Relax NG schema. Every limitation must be verified against source code. Do NOT include claims you cannot verify. Use tables for structured data. Use code blocks for commands. Cross-reference between components. Workflows using SVG.".into(),
196 },
197 SystemPromptPreset {
198 name: "Thinker".into(),
199 description: "Analytical and thoughtful".into(),
200 content: "You are a thoughtful and analytical AI assistant. Think carefully before answering. Provide well-reasoned responses with clear explanations. Do NOT include claims you cannot verify".into(),
201 },
202 SystemPromptPreset {
203 name: "Mathematician".into(),
204 description: "Expert in mathematics".into(),
205 content: "You are an expert in mathematics. Provide clear, step-by-step solutions to mathematical problems. Show your reasoning and explain key concepts.".into(),
206 },
207 ]
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
211pub struct ModelOverride {
212 pub context_length: Option<u32>,
214 pub batch_size: Option<u32>,
215 pub ubatch_size: Option<u32>,
216 pub cache_type_k: Option<CacheTypeK>,
217 pub cache_type_v: Option<CacheTypeV>,
218 pub keep: Option<i32>,
219 pub swa_full: Option<bool>,
220 pub mlock: Option<bool>,
221 pub mmap: Option<bool>,
222 pub numa: Option<NumMode>,
223 pub uniform_cache: Option<bool>,
224 pub system_prompt: Option<String>,
225 pub system_prompt_preset_name: Option<String>,
226 pub max_concurrent_predictions: Option<u32>,
227 pub threads: Option<u32>,
228 pub threads_batch: Option<u32>,
229 pub parallel: Option<u32>,
230
231 pub gpu_layers: Option<i32>,
233 pub split_mode: Option<SplitMode>,
234 pub tensor_split: Option<String>,
235 pub main_gpu: Option<i32>,
236 pub fit: Option<bool>,
237 pub lora: Option<PathBuf>,
238 pub lora_scaled: Option<(PathBuf, f32)>,
239 pub rpc: Option<String>,
240 pub embedding: Option<bool>,
241 pub kv_cache_offload: Option<bool>,
242 pub flash_attn: Option<bool>,
243 pub jinja: Option<bool>,
244 pub auto_chat_template: Option<bool>,
245 pub chat_template: Option<String>,
246 pub chat_template_kwargs: Option<String>,
247 pub expert_count: Option<i32>,
248 pub gpu_layers_mode: Option<crate::models::GpuLayersMode>,
249
250 pub seed: Option<i32>,
252 pub temperature: Option<f32>,
253 pub top_k: Option<i32>,
254 pub top_p: Option<f32>,
255 pub min_p: Option<f32>,
256 pub typical_p: Option<f32>,
257 pub mirostat: Option<Mirostat>,
258 pub mirostat_lr: Option<f32>,
259 pub mirostat_ent: Option<f32>,
260 pub ignore_eos: Option<bool>,
261 pub samplers: Option<Samplers>,
262
263 pub repeat_penalty: Option<f32>,
265 pub repeat_last_n: Option<i32>,
266 pub presence_penalty: Option<f32>,
267 pub frequency_penalty: Option<f32>,
268 pub dry_multiplier: Option<f32>,
269 pub dry_base: Option<f32>,
270 pub dry_allowed_length: Option<i32>,
271 pub dry_penalty_last_n: Option<i32>,
272
273 pub rope_scaling: Option<RopeScaling>,
275 pub rope_scale: Option<f32>,
276 pub rope_freq_base: Option<f32>,
277 pub rope_freq_scale: Option<f32>,
278 pub rope_yarn_enabled: Option<bool>,
279
280 pub cache_prompt: Option<bool>,
282 pub cache_reuse: Option<u32>,
283 pub webui: Option<bool>,
284
285 pub max_tokens: Option<u32>,
287 pub cache_type: Option<CacheType>,
288 pub llama_cpp_version_cpu: Option<String>,
289 pub llama_cpp_version_vulkan: Option<String>,
290 pub llama_cpp_version_rocm: Option<String>,
291 pub llama_cpp_version_rocm_lemonade: Option<String>,
292 pub llama_cpp_version_cuda: Option<String>,
293 pub spec_type: Option<String>,
294 pub draft_tokens: Option<u32>,
295 pub tags: Option<Vec<String>>,
296}
297
298macro_rules! apply_scalar {
300 ($self:ident, $base:ident, $($field:ident),+ $(,)?) => {
301 $(
302 $base.$field = $self.$field.unwrap_or($base.$field);
303 )+
304 };
305}
306
307macro_rules! apply_clone {
309 ($self:ident, $base:ident, $($field:ident),+ $(,)?) => {
310 $(
311 if let Some(v) = &$self.$field {
312 $base.$field = v.clone();
313 }
314 )+
315 };
316}
317
318macro_rules! apply_option {
320 ($self:ident, $base:ident, $($field:ident),+ $(,)?) => {
321 $(
322 if let Some(v) = &$self.$field {
323 $base.$field = Some(v.clone());
324 }
325 )+
326 };
327}
328
329impl ModelOverride {
330 pub fn from_settings(s: &crate::models::ModelSettings) -> Self {
331 Self {
332 context_length: Some(s.context_length),
333 batch_size: Some(s.batch_size),
334 ubatch_size: Some(s.ubatch_size),
335 cache_type_k: s.cache_type_k,
336 cache_type_v: s.cache_type_v,
337 keep: Some(s.keep),
338 swa_full: Some(s.swa_full),
339 mlock: Some(s.mlock),
340 mmap: Some(s.mmap),
341 numa: Some(s.numa),
342 uniform_cache: Some(s.uniform_cache),
343 system_prompt: Some(s.system_prompt.clone()),
344 system_prompt_preset_name: Some(s.system_prompt_preset_name.clone()),
345 max_concurrent_predictions: s.max_concurrent_predictions,
346 threads: Some(s.threads),
347 threads_batch: Some(s.threads_batch),
348 parallel: Some(s.parallel),
349 gpu_layers: Some(match s.gpu_layers_mode {
350 crate::models::GpuLayersMode::Auto => 0,
351 crate::models::GpuLayersMode::Specific(n) => n as i32,
352 crate::models::GpuLayersMode::All => -1,
353 }),
354 gpu_layers_mode: Some(s.gpu_layers_mode),
355 split_mode: Some(s.split_mode),
356 tensor_split: Some(s.tensor_split.clone()),
357 main_gpu: Some(s.main_gpu),
358 fit: Some(s.fit),
359 lora: s.lora.clone(),
360 lora_scaled: s.lora_scaled.clone(),
361 rpc: Some(s.rpc.clone()),
362 embedding: Some(s.embedding),
363 kv_cache_offload: Some(s.kv_cache_offload),
364 flash_attn: Some(s.flash_attn),
365 jinja: Some(s.jinja),
366 auto_chat_template: Some(s.auto_chat_template),
367 chat_template: s.chat_template.clone(),
368 chat_template_kwargs: s.chat_template_kwargs.clone(),
369 expert_count: Some(s.expert_count),
370 seed: Some(s.seed),
371 temperature: Some(s.temperature),
372 top_k: Some(s.top_k),
373 top_p: Some(s.top_p),
374 min_p: Some(s.min_p),
375 typical_p: Some(s.typical_p),
376 mirostat: Some(s.mirostat),
377 mirostat_lr: Some(s.mirostat_lr),
378 mirostat_ent: Some(s.mirostat_ent),
379 ignore_eos: Some(s.ignore_eos),
380 samplers: Some(s.samplers.clone()),
381 repeat_penalty: Some(s.repeat_penalty),
382 repeat_last_n: Some(s.repeat_last_n),
383 presence_penalty: s.presence_penalty,
384 frequency_penalty: s.frequency_penalty,
385 dry_multiplier: Some(s.dry_multiplier),
386 dry_base: Some(s.dry_base),
387 dry_allowed_length: Some(s.dry_allowed_length),
388 dry_penalty_last_n: Some(s.dry_penalty_last_n),
389 rope_scaling: Some(s.rope_scaling),
390 rope_scale: Some(s.rope_scale),
391 rope_freq_base: Some(s.rope_freq_base),
392 rope_freq_scale: Some(s.rope_freq_scale),
393 rope_yarn_enabled: Some(s.rope_yarn_enabled),
394 cache_prompt: Some(s.cache_prompt),
395 cache_reuse: Some(s.cache_reuse),
396 webui: Some(s.webui),
397 max_tokens: s.max_tokens,
398 cache_type: Some(s.cache_type),
399 llama_cpp_version_cpu: s.llama_cpp_version_cpu.clone(),
400 llama_cpp_version_vulkan: s.llama_cpp_version_vulkan.clone(),
401 llama_cpp_version_rocm: s.llama_cpp_version_rocm.clone(),
402 llama_cpp_version_rocm_lemonade: s.llama_cpp_version_rocm_lemonade.clone(),
403 llama_cpp_version_cuda: s.llama_cpp_version_cuda.clone(),
404 spec_type: Some(s.spec_type.clone()),
405 draft_tokens: Some(s.draft_tokens),
406 tags: Some(s.tags.clone()),
407 }
408 }
409
410 pub fn apply(&self, base: &mut crate::models::ModelSettings) {
412 apply_scalar!(
417 self,
418 base,
419 context_length,
420 batch_size,
421 ubatch_size,
422 keep,
423 swa_full,
424 mlock,
425 mmap,
426 numa,
427 uniform_cache,
428 kv_cache_offload,
429 threads,
430 threads_batch,
431 parallel,
432 split_mode,
433 main_gpu,
434 fit,
435 embedding,
436 flash_attn,
437 jinja,
438 auto_chat_template,
439 expert_count,
440 seed,
441 temperature,
442 top_k,
443 top_p,
444 min_p,
445 typical_p,
446 mirostat,
447 mirostat_lr,
448 mirostat_ent,
449 ignore_eos,
450 repeat_penalty,
451 repeat_last_n,
452 dry_multiplier,
453 dry_base,
454 dry_allowed_length,
455 dry_penalty_last_n,
456 rope_scaling,
457 rope_scale,
458 rope_freq_base,
459 rope_freq_scale,
460 rope_yarn_enabled,
461 cache_prompt,
462 cache_reuse,
463 webui,
464 cache_type,
465 draft_tokens,
466 gpu_layers_mode,
467 );
468
469 apply_clone!(
471 self,
472 base,
473 system_prompt,
474 system_prompt_preset_name,
475 tensor_split,
476 rpc,
477 samplers,
478 spec_type,
479 tags,
480 );
481
482 apply_option!(
484 self,
485 base,
486 lora,
487 lora_scaled,
488 chat_template,
489 chat_template_kwargs,
490 llama_cpp_version_cpu,
491 llama_cpp_version_vulkan,
492 llama_cpp_version_rocm,
493 llama_cpp_version_rocm_lemonade,
494 llama_cpp_version_cuda,
495 );
496
497 if let Some(v) = self.cache_type_k {
499 base.cache_type_k = Some(v);
500 }
501 if let Some(v) = self.cache_type_v {
502 base.cache_type_v = Some(v);
503 }
504 if let Some(v) = self.presence_penalty {
505 base.presence_penalty = Some(v);
506 }
507 if let Some(v) = self.frequency_penalty {
508 base.frequency_penalty = Some(v);
509 }
510 base.max_tokens = self.max_tokens.or(base.max_tokens);
511
512 base.max_concurrent_predictions = self
514 .max_concurrent_predictions
515 .or(base.max_concurrent_predictions);
516
517 if let Some(n) = self.gpu_layers {
520 base.gpu_layers_mode = match n {
521 n if n < 0 => crate::models::GpuLayersMode::All,
522 n => crate::models::GpuLayersMode::Specific(n as u32),
523 };
524 }
525
526 }
542}
543
544pub fn builtin_profiles() -> Vec<Profile> {
546 vec![
547 Profile {
548 name: "Qwen".into(),
549 description: "Optimized for Qwen models (dense)".into(),
550 settings: ModelOverride {
551 context_length: Some(131072),
552 temperature: Some(0.7),
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: "Qwen-MoE".into(),
564 description: "Optimized for Qwen MoE models (35B-A3B)".into(),
565 settings: ModelOverride {
566 context_length: Some(131072),
567 temperature: Some(0.8),
568 top_k: Some(20),
569 top_p: Some(0.95),
570 max_tokens: Some(4096),
571 presence_penalty: Some(1.5),
572 uniform_cache: Some(true),
573 jinja: Some(true),
574 ..Default::default()
575 },
576 },
577 Profile {
578 name: "Qwen-Coding".into(),
579 description: "Optimized for Qwen models in coding mode".into(),
580 settings: ModelOverride {
581 context_length: Some(131072),
582 temperature: Some(0.6),
583 top_k: Some(20),
584 top_p: Some(0.95),
585 max_tokens: Some(4096),
586 presence_penalty: Some(0.0),
587 uniform_cache: Some(true),
588 jinja: Some(true),
589 ..Default::default()
590 },
591 },
592 Profile {
593 name: "Gemma".into(),
594 description: "Optimized for Gemma 2/4 models".into(),
595 settings: ModelOverride {
596 context_length: Some(131072),
597 min_p: Some(0.1),
598 temperature: Some(1.0),
599 top_k: Some(65),
600 top_p: Some(0.95),
601 max_tokens: Some(4096),
602 uniform_cache: Some(true),
603 jinja: Some(true),
604 ..Default::default()
605 },
606 },
607 Profile {
608 name: "Llama".into(),
609 description: "Optimized for Llama 3.1/3.3 models".into(),
610 settings: ModelOverride {
611 context_length: Some(131072),
612 temperature: Some(0.7),
613 top_p: Some(0.9),
614 repeat_penalty: Some(1.1),
615 max_tokens: Some(4096),
616 uniform_cache: Some(true),
617 jinja: Some(true),
618 ..Default::default()
619 },
620 },
621 Profile {
622 name: "Mistral".into(),
623 description: "Optimized for Mistral 7B/NeMo models".into(),
624 settings: ModelOverride {
625 context_length: Some(131072),
626 temperature: Some(0.7),
627 top_k: Some(50),
628 top_p: Some(0.9),
629 max_tokens: Some(4096),
630 uniform_cache: Some(true),
631 jinja: Some(true),
632 ..Default::default()
633 },
634 },
635 Profile {
636 name: "Phi".into(),
637 description: "Optimized for Phi 3.5 Mini models".into(),
638 settings: ModelOverride {
639 context_length: Some(131072),
640 temperature: Some(0.7),
641 top_k: Some(50),
642 top_p: Some(0.9),
643 repeat_penalty: Some(1.1),
644 max_tokens: Some(4096),
645 uniform_cache: Some(true),
646 ..Default::default()
647 },
648 },
649 ]
650}
651
652#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
653#[serde(default)]
654pub struct DefaultParams {
655 #[serde(default)]
657 pub context_length: u32,
658 #[serde(default)]
659 pub threads: u32,
660 #[serde(default)]
661 pub threads_batch: u32,
662 #[serde(default)]
663 pub batch_size: u32,
664 #[serde(default)]
665 pub ubatch_size: u32,
666 #[serde(default = "default_cache_type_k")]
667 pub cache_type_k: Option<CacheTypeK>,
668 #[serde(default = "default_cache_type_v")]
669 pub cache_type_v: Option<CacheTypeV>,
670 #[serde(default)]
671 pub keep: i32,
672 #[serde(default)]
673 pub swa_full: bool,
674 #[serde(default)]
675 pub mlock: bool,
676 #[serde(default)]
677 pub mmap: bool,
678 #[serde(default)]
679 pub numa: NumMode,
680 #[serde(default)]
681 pub uniform_cache: bool,
682 #[serde(default)]
683 pub kv_cache_offload: bool,
684 #[serde(default)]
685 pub parallel: u32,
686 #[serde(default)]
687 pub max_concurrent_predictions: Option<u32>,
688 #[serde(default)]
689 pub system_prompt: String,
690 #[serde(default = "default_system_prompt_preset_name")]
691 pub system_prompt_preset_name: String,
692 #[serde(default)]
694 pub gpu_layers: i32,
695 #[serde(default = "default_gpu_layers_mode")]
696 pub gpu_layers_mode: crate::models::GpuLayersMode,
697 #[serde(default)]
698 pub split_mode: SplitMode,
699 #[serde(default)]
700 pub tensor_split: String,
701 #[serde(default)]
702 pub main_gpu: i32,
703 #[serde(default)]
704 pub fit: bool,
705 #[serde(default)]
706 pub lora: Option<PathBuf>,
707 #[serde(default)]
708 pub lora_scaled: Option<(PathBuf, f32)>,
709 #[serde(default)]
710 pub rpc: String,
711 #[serde(default)]
712 pub embedding: bool,
713 #[serde(default)]
714 pub flash_attn: bool,
715 #[serde(default)]
716 pub jinja: bool,
717 #[serde(default)]
718 pub auto_chat_template: bool,
719 #[serde(default)]
720 pub chat_template: Option<String>,
721 #[serde(default)]
722 pub chat_template_kwargs: Option<String>,
723 #[serde(default)]
724 pub expert_count: i32,
725
726 #[serde(default)]
728 pub seed: i32,
729 #[serde(default)]
730 pub temperature: f32,
731 #[serde(default)]
732 pub top_k: i32,
733 #[serde(default)]
734 pub top_p: f32,
735 #[serde(default)]
736 pub min_p: f32,
737 #[serde(default)]
738 pub typical_p: f32,
739 #[serde(default)]
740 pub mirostat: Mirostat,
741 #[serde(default)]
742 pub mirostat_lr: f32,
743 #[serde(default)]
744 pub mirostat_ent: f32,
745 #[serde(default)]
746 pub ignore_eos: bool,
747 #[serde(default)]
748 pub samplers: Samplers,
749
750 #[serde(default)]
752 pub repeat_penalty: f32,
753 #[serde(default)]
754 pub repeat_last_n: i32,
755 #[serde(default = "default_presence_penalty")]
756 pub presence_penalty: Option<f32>,
757 #[serde(default = "default_frequency_penalty")]
758 pub frequency_penalty: Option<f32>,
759 #[serde(default)]
760 pub dry_multiplier: f32,
761 #[serde(default)]
762 pub dry_base: f32,
763 #[serde(default)]
764 pub dry_allowed_length: i32,
765 #[serde(default)]
766 pub dry_penalty_last_n: i32,
767
768 #[serde(default)]
770 pub rope_scaling: RopeScaling,
771 #[serde(default)]
772 pub rope_scale: f32,
773 #[serde(default)]
774 pub rope_freq_base: f32,
775 #[serde(default)]
776 pub rope_freq_scale: f32,
777 #[serde(default)]
778 pub rope_yarn_enabled: bool,
779
780 #[serde(default)]
782 pub host: String,
783 #[serde(default)]
784 pub port: u16,
785 #[serde(default)]
786 pub timeout: u32,
787 #[serde(default = "default_cache_prompt")]
788 pub cache_prompt: bool,
789 #[serde(default)]
790 pub cache_reuse: u32,
791 #[serde(default)]
792 pub webui: bool,
793 #[serde(default)]
794 pub ws_server_enabled: bool,
795 #[serde(default = "default_ws_server_port")]
796 pub ws_server_port: u16,
797 #[serde(default = "default_server_tls_enabled")]
798 pub server_tls_enabled: bool,
799 #[serde(default)]
800 pub server_tls_cert: Option<String>,
801 #[serde(default)]
802 pub server_tls_key: Option<String>,
803 #[serde(default)]
804 pub router_max_models: u32,
805 #[serde(default)]
806 pub server_mode: crate::models::ServerMode,
807 #[serde(default = "default_log_level")]
808 pub log_level: String,
809
810 #[serde(default = "default_max_tokens")]
812 pub max_tokens: Option<u32>,
813 #[serde(default)]
814 pub cache_type: CacheType,
815 #[serde(default)]
816 pub backend: Backend,
817 #[serde(default)]
819 pub platform: Option<String>,
820 #[serde(default)]
821 pub llama_cpp_version_cpu: Option<String>,
822 #[serde(default)]
823 pub llama_cpp_version_vulkan: Option<String>,
824 #[serde(default)]
825 pub llama_cpp_version_rocm: Option<String>,
826 #[serde(default)]
827 pub llama_cpp_version_rocm_lemonade: Option<String>,
828 #[serde(default)]
829 pub llama_cpp_version_cuda: Option<String>,
830
831 #[serde(default)]
833 pub api_endpoint_enabled: bool,
834 #[serde(default = "default_api_endpoint_port")]
835 pub api_endpoint_port: u16,
836 #[serde(default = "default_web_search_engine")]
837 pub web_search_engine: String,
838 #[serde(default)]
839 pub web_search_engine_url: String,
840 #[serde(default = "default_web_search_enabled")]
841 pub web_search_enabled: bool,
842 #[serde(default)]
843 pub web_search_api_key: Option<String>,
844 #[serde(default)]
845 pub api_endpoint_key: Option<String>,
846 #[serde(default)]
847 pub spec_type: String,
848 #[serde(default)]
849 pub draft_tokens: u32,
850 #[serde(default)]
851 pub tags: Vec<String>,
852}
853
854fn default_api_endpoint_port() -> u16 {
855 49222
856}
857
858fn default_web_search_engine() -> String {
859 "searxng".to_string()
860}
861
862fn default_web_search_enabled() -> bool {
863 false
864}
865
866fn default_system_prompt_preset_name() -> String {
867 "General".to_string()
868}
869
870fn default_cache_type_k() -> Option<CacheTypeK> {
871 None
872}
873fn default_cache_type_v() -> Option<CacheTypeV> {
874 None
875}
876fn default_presence_penalty() -> Option<f32> {
877 None
878}
879fn default_frequency_penalty() -> Option<f32> {
880 None
881}
882fn default_max_tokens() -> Option<u32> {
883 None
884}
885fn default_cache_prompt() -> bool {
886 true
887}
888fn default_ws_server_port() -> u16 {
889 49223
890}
891
892fn default_server_tls_enabled() -> bool {
893 true
894}
895
896fn default_log_level() -> String {
897 "trace".to_string()
898}
899
900fn default_gpu_layers_mode() -> crate::models::GpuLayersMode {
901 crate::models::GpuLayersMode::Auto
902}
903
904impl Default for DefaultParams {
905 fn default() -> Self {
906 Self {
907 context_length: 131072,
909 threads: physical_cores(),
910 threads_batch: 8,
911 batch_size: 512,
912 ubatch_size: 512,
913 cache_type_k: None,
914 cache_type_v: None,
915 keep: 0,
916 swa_full: false,
917 mlock: false,
918 mmap: true,
919 numa: NumMode::None,
920 uniform_cache: true,
921 kv_cache_offload: true,
922 parallel: 1,
923 max_concurrent_predictions: None,
924 system_prompt: DEFAULT_SYSTEM_PROMPT.to_string(),
925 system_prompt_preset_name: "Coder".to_string(),
926
927 gpu_layers: -1,
929 gpu_layers_mode: crate::models::GpuLayersMode::Auto,
930 split_mode: SplitMode::Layer,
931 tensor_split: String::new(),
932 main_gpu: 0,
933 fit: true,
934 lora: None,
935 lora_scaled: None,
936 rpc: String::new(),
937 embedding: false,
938 flash_attn: true,
939 jinja: true,
940 auto_chat_template: false,
941 chat_template: None,
942 chat_template_kwargs: None,
943 expert_count: -1,
944
945 seed: -1,
947 temperature: 0.8,
948 top_k: 40,
949 top_p: 0.95,
950 min_p: 0.0,
951 typical_p: 1.0,
952 mirostat: Mirostat::Off,
953 mirostat_lr: 0.1,
954 mirostat_ent: 5.0,
955 ignore_eos: false,
956 samplers: Samplers::default(),
957
958 repeat_penalty: 1.1,
960 repeat_last_n: 64,
961 presence_penalty: None,
962 frequency_penalty: None,
963 dry_multiplier: 0.0,
964 dry_base: 1.75,
965 dry_allowed_length: 2,
966 dry_penalty_last_n: -1,
967
968 rope_scaling: RopeScaling::None,
970 rope_scale: 1.0,
971 rope_freq_base: 0.0,
972 rope_freq_scale: 1.0,
973 rope_yarn_enabled: false,
974
975 host: "127.0.0.1".to_string(),
977 port: 8080,
978 timeout: 600,
979 cache_prompt: true,
980 cache_reuse: 0,
981 webui: false,
982 ws_server_enabled: false,
983 ws_server_port: 49223,
984 server_tls_enabled: true,
985 server_tls_cert: None,
986 server_tls_key: None,
987 router_max_models: 4,
988 server_mode: crate::models::ServerMode::Normal,
989 log_level: "trace".to_string(),
990
991 max_tokens: None,
993 cache_type: CacheType::F16,
994 backend: {
995 use crate::backend::hardware::{GpuVendor, detect_gpu_vendors};
996 let vendors = detect_gpu_vendors();
997 let mut result = Backend::Cpu;
998 for v in &vendors {
999 if matches!(v, GpuVendor::Nvidia) {
1000 result = Backend::Cuda;
1001 break;
1002 }
1003 if matches!(v, GpuVendor::Amd) {
1004 result = Backend::Rocm;
1005 break;
1006 }
1007 if matches!(v, GpuVendor::Intel) {
1008 result = Backend::Vulkan;
1009 break;
1010 }
1011 }
1012 result
1013 },
1014 platform: None,
1015 llama_cpp_version_cpu: None,
1016 llama_cpp_version_vulkan: None,
1017 llama_cpp_version_rocm: None,
1018 llama_cpp_version_rocm_lemonade: None,
1019 llama_cpp_version_cuda: None,
1020 api_endpoint_enabled: false,
1021 api_endpoint_port: 49222,
1022 api_endpoint_key: None,
1023 web_search_engine: "searxng".to_string(),
1024 web_search_engine_url: String::new(),
1025 web_search_enabled: false,
1026 web_search_api_key: None,
1027 spec_type: String::new(),
1028 draft_tokens: 0,
1029 tags: Vec::new(),
1030 }
1031 }
1032}
1033
1034impl Default for Config {
1035 fn default() -> Self {
1036 Self {
1037 models_dirs: vec![
1038 dirs::data_dir()
1039 .unwrap_or_default()
1040 .join("llm-manager")
1041 .join("models"),
1042 ],
1043 llama_server: "llama-server".into(),
1044 default: DefaultParams::default(),
1045 model_overrides: ModelConfigStore::new(),
1046 profiles: Default::default(),
1047 system_prompt_presets: Default::default(),
1048 rpc_workers: Vec::new(),
1049 search_limit: default_search_limit(),
1050 active_panel: ActivePanel::Models,
1051 left_pct: 55,
1052 server_settings_height: 9,
1053 language: default_language(),
1054 onboarding_complete: false,
1055 }
1056 }
1057}
1058
1059impl Config {
1060 pub fn config_path() -> PathBuf {
1061 config_base_dir().join("llm-manager").join("config.yaml")
1062 }
1063
1064 fn config_keys() -> &'static [&'static str] {
1066 &[
1067 "models_dirs",
1068 "llama_server",
1069 "default",
1070 "model_overrides",
1071 "profiles",
1072 "system_prompt_presets",
1073 "rpc_workers",
1074 "search_limit",
1075 "active_panel",
1076 "left_pct",
1077 "server_settings_height",
1078 "language",
1079 "onboarding_complete",
1080 ]
1081 }
1082
1083 fn default_params_keys() -> &'static [&'static str] {
1085 &[
1086 "context_length",
1087 "threads",
1088 "threads_batch",
1089 "batch_size",
1090 "ubatch_size",
1091 "cache_type_k",
1092 "cache_type_v",
1093 "keep",
1094 "swa_full",
1095 "mlock",
1096 "mmap",
1097 "numa",
1098 "uniform_cache",
1099 "kv_cache_offload",
1100 "parallel",
1101 "max_concurrent_predictions",
1102 "system_prompt",
1103 "system_prompt_preset_name",
1104 "gpu_layers",
1105 "gpu_layers_mode",
1106 "split_mode",
1107 "tensor_split",
1108 "main_gpu",
1109 "fit",
1110 "lora",
1111 "lora_scaled",
1112 "rpc",
1113 "embedding",
1114 "flash_attn",
1115 "jinja",
1116 "chat_template",
1117 "chat_template_kwargs",
1118 "expert_count",
1119 "seed",
1120 "temperature",
1121 "top_k",
1122 "top_p",
1123 "min_p",
1124 "typical_p",
1125 "mirostat",
1126 "mirostat_lr",
1127 "mirostat_ent",
1128 "ignore_eos",
1129 "samplers",
1130 "repeat_penalty",
1131 "repeat_last_n",
1132 "presence_penalty",
1133 "frequency_penalty",
1134 "dry_multiplier",
1135 "dry_base",
1136 "dry_allowed_length",
1137 "dry_penalty_last_n",
1138 "rope_scaling",
1139 "rope_scale",
1140 "rope_freq_base",
1141 "rope_freq_scale",
1142 "rope_yarn_enabled",
1143 "host",
1144 "port",
1145 "timeout",
1146 "cache_prompt",
1147 "cache_reuse",
1148 "webui",
1149 "ws_server_enabled",
1150 "ws_server_port",
1151 "server_tls_enabled",
1152 "server_tls_cert",
1153 "server_tls_key",
1154 "router_max_models",
1155 "server_mode",
1156 "log_level",
1157 "max_tokens",
1158 "cache_type",
1159 "backend",
1160 "platform",
1161 "llama_cpp_version_cpu",
1162 "llama_cpp_version_vulkan",
1163 "llama_cpp_version_rocm",
1164 "llama_cpp_version_rocm_lemonade",
1165 "llama_cpp_version_cuda",
1166 "api_endpoint_enabled",
1167 "api_endpoint_port",
1168 "api_endpoint_key",
1169 "web_search_engine",
1170 "web_search_engine_url",
1171 "web_search_enabled",
1172 "web_search_api_key",
1173 "spec_type",
1174 "draft_tokens",
1175 "tags",
1176 "auto_chat_template",
1177 ]
1178 }
1179
1180 fn model_override_keys() -> &'static [&'static str] {
1182 &[
1183 "context_length",
1184 "batch_size",
1185 "ubatch_size",
1186 "cache_type_k",
1187 "cache_type_v",
1188 "keep",
1189 "swa_full",
1190 "mlock",
1191 "mmap",
1192 "numa",
1193 "uniform_cache",
1194 "system_prompt",
1195 "system_prompt_preset_name",
1196 "max_concurrent_predictions",
1197 "threads",
1198 "threads_batch",
1199 "parallel",
1200 "gpu_layers",
1201 "split_mode",
1202 "tensor_split",
1203 "main_gpu",
1204 "fit",
1205 "lora",
1206 "lora_scaled",
1207 "rpc",
1208 "embedding",
1209 "kv_cache_offload",
1210 "flash_attn",
1211 "jinja",
1212 "chat_template",
1213 "chat_template_kwargs",
1214 "expert_count",
1215 "gpu_layers_mode",
1216 "seed",
1217 "temperature",
1218 "top_k",
1219 "top_p",
1220 "min_p",
1221 "typical_p",
1222 "mirostat",
1223 "mirostat_lr",
1224 "mirostat_ent",
1225 "ignore_eos",
1226 "samplers",
1227 "repeat_penalty",
1228 "repeat_last_n",
1229 "presence_penalty",
1230 "frequency_penalty",
1231 "dry_multiplier",
1232 "dry_base",
1233 "dry_allowed_length",
1234 "dry_penalty_last_n",
1235 "rope_scaling",
1236 "rope_scale",
1237 "rope_freq_base",
1238 "rope_freq_scale",
1239 "rope_yarn_enabled",
1240 "cache_prompt",
1241 "cache_reuse",
1242 "webui",
1243 "max_tokens",
1244 "cache_type",
1245 "llama_cpp_version_cpu",
1246 "llama_cpp_version_vulkan",
1247 "llama_cpp_version_rocm",
1248 "llama_cpp_version_rocm_lemonade",
1249 "llama_cpp_version_cuda",
1250 "spec_type",
1251 "draft_tokens",
1252 "tags",
1253 ]
1254 }
1255
1256 fn rpc_worker_keys() -> &'static [&'static str] {
1258 &["selected", "name", "ip", "port"]
1259 }
1260
1261 pub fn validate_unknown_fields(value: &serde_yml::Value) -> Vec<ValidationWarning> {
1263 let mut warnings = Vec::new();
1264 Self::check_unknown_fields(value, "", Self::config_keys(), &mut warnings);
1265 warnings
1266 }
1267
1268 fn check_unknown_fields(
1269 value: &serde_yml::Value,
1270 prefix: &str,
1271 known_keys: &'static [&'static str],
1272 warnings: &mut Vec<ValidationWarning>,
1273 ) {
1274 if let serde_yml::Value::Mapping(map) = value {
1275 for key in map.keys() {
1276 let key_str = key.as_str();
1277 if !known_keys.contains(&key_str) {
1278 let field = if prefix.is_empty() {
1279 key_str.to_string()
1280 } else {
1281 format!("{}.{}", prefix, key_str)
1282 };
1283 warnings.push(ValidationWarning {
1284 field,
1285 message: format!("Unknown config field: {}", key_str),
1286 severity: ValidationSeverity::Warning,
1287 });
1288 }
1289 }
1290
1291 for (key, val) in map {
1293 let key_str = key.as_str();
1294 let new_prefix = if prefix.is_empty() {
1295 key_str.to_string()
1296 } else {
1297 format!("{}.{}", prefix, key_str)
1298 };
1299
1300 match key_str {
1301 "default" => {
1302 Self::check_unknown_fields(
1303 val,
1304 &new_prefix,
1305 Self::default_params_keys(),
1306 warnings,
1307 );
1308 }
1309 "model_overrides" => {
1310 if let serde_yml::Value::Mapping(overrides) = val {
1311 for (override_key, override_val) in overrides {
1312 let k = override_key.as_str();
1313 let override_prefix = format!("{}.{}", new_prefix, k);
1314 Self::check_unknown_fields(
1315 override_val,
1316 &override_prefix,
1317 Self::model_override_keys(),
1318 warnings,
1319 );
1320 }
1321 }
1322 }
1323 "rpc_workers" => {
1324 if let serde_yml::Value::Sequence(items) = val {
1325 for item in items {
1326 Self::check_unknown_fields(
1327 item,
1328 &new_prefix,
1329 Self::rpc_worker_keys(),
1330 warnings,
1331 );
1332 }
1333 }
1334 }
1335 _ => {
1336 Self::check_unknown_fields(val, &new_prefix, known_keys, warnings);
1337 }
1338 }
1339 }
1340 }
1341 }
1342
1343 pub fn validate(&self) -> Vec<ValidationWarning> {
1345 let mut warnings = Vec::new();
1346 let default = &self.default;
1347
1348 if default.context_length < 512 || default.context_length > 1048576 {
1350 warnings.push(ValidationWarning {
1351 field: "default.context_length".to_string(),
1352 message: format!(
1353 "context_length {} is outside recommended range 512-1048576",
1354 default.context_length
1355 ),
1356 severity: ValidationSeverity::Warning,
1357 });
1358 }
1359 if default.threads == 0 {
1360 warnings.push(ValidationWarning {
1361 field: "default.threads".to_string(),
1362 message: "threads must be greater than 0".to_string(),
1363 severity: ValidationSeverity::Warning,
1364 });
1365 }
1366 if default.temperature < 0.0 || default.temperature > 2.0 {
1367 warnings.push(ValidationWarning {
1368 field: "default.temperature".to_string(),
1369 message: format!(
1370 "temperature {} is outside recommended range 0.0-2.0",
1371 default.temperature
1372 ),
1373 severity: ValidationSeverity::Warning,
1374 });
1375 }
1376 if default.top_k < 0 {
1377 warnings.push(ValidationWarning {
1378 field: "default.top_k".to_string(),
1379 message: "top_k must be >= 0".to_string(),
1380 severity: ValidationSeverity::Warning,
1381 });
1382 }
1383 if default.top_p < 0.0 || default.top_p > 1.0 {
1384 warnings.push(ValidationWarning {
1385 field: "default.top_p".to_string(),
1386 message: format!(
1387 "top_p {} is outside recommended range 0.0-1.0",
1388 default.top_p
1389 ),
1390 severity: ValidationSeverity::Warning,
1391 });
1392 }
1393 if default.min_p < 0.0 || default.min_p > 1.0 {
1394 warnings.push(ValidationWarning {
1395 field: "default.min_p".to_string(),
1396 message: format!(
1397 "min_p {} is outside recommended range 0.0-1.0",
1398 default.min_p
1399 ),
1400 severity: ValidationSeverity::Warning,
1401 });
1402 }
1403 if default.typical_p < 0.0 || default.typical_p > 1.0 {
1404 warnings.push(ValidationWarning {
1405 field: "default.typical_p".to_string(),
1406 message: format!(
1407 "typical_p {} is outside recommended range 0.0-1.0",
1408 default.typical_p
1409 ),
1410 severity: ValidationSeverity::Warning,
1411 });
1412 }
1413 if default.repeat_penalty < 0.0 || default.repeat_penalty > 3.0 {
1414 warnings.push(ValidationWarning {
1415 field: "default.repeat_penalty".to_string(),
1416 message: format!(
1417 "repeat_penalty {} is outside recommended range 0.0-3.0",
1418 default.repeat_penalty
1419 ),
1420 severity: ValidationSeverity::Warning,
1421 });
1422 }
1423 if default.mirostat_lr < 0.0 || default.mirostat_lr > 1.0 {
1424 warnings.push(ValidationWarning {
1425 field: "default.mirostat_lr".to_string(),
1426 message: format!(
1427 "mirostat_lr {} is outside recommended range 0.0-1.0",
1428 default.mirostat_lr
1429 ),
1430 severity: ValidationSeverity::Warning,
1431 });
1432 }
1433 if default.mirostat_ent < 0.0 || default.mirostat_ent > 10.0 {
1434 warnings.push(ValidationWarning {
1435 field: "default.mirostat_ent".to_string(),
1436 message: format!(
1437 "mirostat_ent {} is outside recommended range 0.0-10.0",
1438 default.mirostat_ent
1439 ),
1440 severity: ValidationSeverity::Warning,
1441 });
1442 }
1443 if default.dry_multiplier < 0.0 {
1444 warnings.push(ValidationWarning {
1445 field: "default.dry_multiplier".to_string(),
1446 message: "dry_multiplier must be >= 0".to_string(),
1447 severity: ValidationSeverity::Warning,
1448 });
1449 }
1450 if default.dry_base <= 0.0 {
1451 warnings.push(ValidationWarning {
1452 field: "default.dry_base".to_string(),
1453 message: "dry_base must be > 0".to_string(),
1454 severity: ValidationSeverity::Warning,
1455 });
1456 }
1457 if default.rope_scale <= 0.0 {
1458 warnings.push(ValidationWarning {
1459 field: "default.rope_scale".to_string(),
1460 message: "rope_scale must be > 0".to_string(),
1461 severity: ValidationSeverity::Warning,
1462 });
1463 }
1464 if default.rope_freq_scale <= 0.0 {
1465 warnings.push(ValidationWarning {
1466 field: "default.rope_freq_scale".to_string(),
1467 message: "rope_freq_scale must be > 0".to_string(),
1468 severity: ValidationSeverity::Warning,
1469 });
1470 }
1471 if default.port == 0 {
1472 warnings.push(ValidationWarning {
1473 field: "default.port".to_string(),
1474 message: "port must be between 1 and 65535".to_string(),
1475 severity: ValidationSeverity::Error,
1476 });
1477 }
1478 if default.port < 1024 {
1479 warnings.push(ValidationWarning {
1480 field: "default.port".to_string(),
1481 message: format!("port {} is a privileged port (< 1024)", default.port),
1482 severity: ValidationSeverity::Warning,
1483 });
1484 }
1485 if default.api_endpoint_port == 0 {
1486 warnings.push(ValidationWarning {
1487 field: "default.api_endpoint_port".to_string(),
1488 message: "api_endpoint_port must be between 1 and 65535".to_string(),
1489 severity: ValidationSeverity::Error,
1490 });
1491 }
1492 if default.ws_server_port == 0 {
1493 warnings.push(ValidationWarning {
1494 field: "default.ws_server_port".to_string(),
1495 message: "ws_server_port must be between 1 and 65535".to_string(),
1496 severity: ValidationSeverity::Error,
1497 });
1498 }
1499 if default.timeout < 1 {
1500 warnings.push(ValidationWarning {
1501 field: "default.timeout".to_string(),
1502 message: "timeout must be at least 1 second".to_string(),
1503 severity: ValidationSeverity::Warning,
1504 });
1505 }
1506 if default.router_max_models == 0 {
1507 warnings.push(ValidationWarning {
1508 field: "default.router_max_models".to_string(),
1509 message: "router_max_models must be >= 1".to_string(),
1510 severity: ValidationSeverity::Warning,
1511 });
1512 }
1513 if let Some(max_tokens) = default.max_tokens {
1514 if max_tokens == 0 {
1515 warnings.push(ValidationWarning {
1516 field: "default.max_tokens".to_string(),
1517 message: "max_tokens must be > 0".to_string(),
1518 severity: ValidationSeverity::Warning,
1519 });
1520 } else if max_tokens > 65536 {
1521 warnings.push(ValidationWarning {
1522 field: "default.max_tokens".to_string(),
1523 message: format!("max_tokens {} is very large (> 65536)", max_tokens),
1524 severity: ValidationSeverity::Warning,
1525 });
1526 }
1527 }
1528 if default.context_length > 262144 {
1529 warnings.push(ValidationWarning {
1530 field: "default.context_length".to_string(),
1531 message: format!(
1532 "context_length {} may cause high memory usage",
1533 default.context_length
1534 ),
1535 severity: ValidationSeverity::Warning,
1536 });
1537 }
1538 if default.threads_batch > default.batch_size && default.batch_size > 0 {
1539 warnings.push(ValidationWarning {
1540 field: "default.threads_batch".to_string(),
1541 message: "threads_batch should not exceed batch_size".to_string(),
1542 severity: ValidationSeverity::Warning,
1543 });
1544 }
1545
1546 if let Some(lora) = &default.lora
1548 && !lora.exists()
1549 {
1550 warnings.push(ValidationWarning {
1551 field: "default.lora".to_string(),
1552 message: format!("lora path does not exist: {}", lora.display()),
1553 severity: ValidationSeverity::Warning,
1554 });
1555 }
1556 if let Some((lora, _)) = &default.lora_scaled
1557 && !lora.exists()
1558 {
1559 warnings.push(ValidationWarning {
1560 field: "default.lora_scaled".to_string(),
1561 message: format!("lora path does not exist: {}", lora.display()),
1562 severity: ValidationSeverity::Warning,
1563 });
1564 }
1565 if let Some(cert) = &default.server_tls_cert
1566 && !cert.is_empty()
1567 && !std::path::Path::new(cert).exists()
1568 {
1569 warnings.push(ValidationWarning {
1570 field: "default.server_tls_cert".to_string(),
1571 message: format!("TLS cert path does not exist: {}", cert),
1572 severity: ValidationSeverity::Warning,
1573 });
1574 }
1575 if let Some(key) = &default.server_tls_key
1576 && !key.is_empty()
1577 && !std::path::Path::new(key).exists()
1578 {
1579 warnings.push(ValidationWarning {
1580 field: "default.server_tls_key".to_string(),
1581 message: format!("TLS key path does not exist: {}", key),
1582 severity: ValidationSeverity::Warning,
1583 });
1584 }
1585 if self.llama_server.is_absolute() && !self.llama_server.exists() {
1586 warnings.push(ValidationWarning {
1587 field: "llama_server".to_string(),
1588 message: format!(
1589 "llama_server binary not found: {}",
1590 self.llama_server.display()
1591 ),
1592 severity: ValidationSeverity::Warning,
1593 });
1594 }
1595
1596 for model_name in self.model_overrides.keys() {
1598 if let Some(override_settings) = self.model_overrides.get(model_name.as_str()) {
1599 if let Some(lora) = &override_settings.lora
1600 && !lora.exists()
1601 {
1602 warnings.push(ValidationWarning {
1603 field: format!("model_overrides.{}.lora", model_name),
1604 message: format!("lora path does not exist: {}", lora.display()),
1605 severity: ValidationSeverity::Warning,
1606 });
1607 }
1608 if let Some((lora, _)) = &override_settings.lora_scaled
1609 && !lora.exists()
1610 {
1611 warnings.push(ValidationWarning {
1612 field: format!("model_overrides.{}.lora_scaled", model_name),
1613 message: format!("lora path does not exist: {}", lora.display()),
1614 severity: ValidationSeverity::Warning,
1615 });
1616 }
1617 }
1618 }
1619
1620 if default.port == default.api_endpoint_port {
1622 warnings.push(ValidationWarning {
1623 field: "default.port".to_string(),
1624 message: format!(
1625 "port conflict: server port and API endpoint port both set to {}",
1626 default.port
1627 ),
1628 severity: ValidationSeverity::Warning,
1629 });
1630 }
1631 if default.port == default.ws_server_port {
1632 warnings.push(ValidationWarning {
1633 field: "default.port".to_string(),
1634 message: format!(
1635 "port conflict: server port and WS server port both set to {}",
1636 default.port
1637 ),
1638 severity: ValidationSeverity::Warning,
1639 });
1640 }
1641 if default.api_endpoint_port == default.ws_server_port {
1642 warnings.push(ValidationWarning {
1643 field: "default.api_endpoint_port".to_string(),
1644 message: format!(
1645 "port conflict: API endpoint port and WS server port both set to {}",
1646 default.api_endpoint_port
1647 ),
1648 severity: ValidationSeverity::Warning,
1649 });
1650 }
1651 if matches!(default.server_mode, crate::models::ServerMode::Router)
1652 && default.router_max_models < 2
1653 {
1654 warnings.push(ValidationWarning {
1655 field: "default.router_max_models".to_string(),
1656 message: "router_max_models should be >= 2 for Router mode".to_string(),
1657 severity: ValidationSeverity::Warning,
1658 });
1659 }
1660 if !default.spec_type.is_empty() && default.draft_tokens == 0 {
1661 warnings.push(ValidationWarning {
1662 field: "default.spec_type".to_string(),
1663 message: "spec_type is set but draft_tokens is 0".to_string(),
1664 severity: ValidationSeverity::Warning,
1665 });
1666 }
1667 if default.server_tls_enabled {
1668 if let Some(cert) = &default.server_tls_cert
1669 && !cert.is_empty()
1670 && !std::path::Path::new(cert).exists()
1671 {
1672 warnings.push(ValidationWarning {
1673 field: "default.server_tls_cert".to_string(),
1674 message: format!("TLS cert path does not exist: {}", cert),
1675 severity: ValidationSeverity::Warning,
1676 });
1677 }
1678 if let Some(key) = &default.server_tls_key
1679 && !key.is_empty()
1680 && !std::path::Path::new(key).exists()
1681 {
1682 warnings.push(ValidationWarning {
1683 field: "default.server_tls_key".to_string(),
1684 message: format!("TLS key path does not exist: {}", key),
1685 severity: ValidationSeverity::Warning,
1686 });
1687 }
1688 }
1689
1690 warnings
1691 }
1692
1693 pub fn resolve_settings(
1695 &self,
1696 model_name: Option<&str>,
1697 profile_name: Option<&str>,
1698 ) -> crate::models::ModelSettings {
1699 let mut settings = crate::models::ModelSettings::from_config(self);
1700
1701 if let Some(name) = model_name
1703 && let Some(override_settings) = self.model_overrides.get(name)
1704 {
1705 override_settings.apply(&mut settings);
1706 }
1707
1708 if let Some(p_name) = profile_name {
1710 if let Some(profile) = self.profiles.get(p_name) {
1711 profile.settings.apply(&mut settings);
1712 } else if let Some(profile) = builtin_profiles().iter().find(|p| p.name == p_name) {
1713 profile.settings.apply(&mut settings);
1714 }
1715 }
1716
1717 if let Some(preset) = self
1719 .system_prompt_presets
1720 .get(&settings.system_prompt_preset_name)
1721 {
1722 settings.system_prompt = preset.content.clone();
1723 }
1724
1725 settings
1726 }
1727
1728 pub fn get_preset_content(&self, name: &str) -> Option<String> {
1730 self.system_prompt_presets
1731 .get(name)
1732 .map(|p| p.content.clone())
1733 }
1734
1735 fn normalize_config(mut config: Config) -> Config {
1736 for path in &mut config.models_dirs {
1738 let path_str = path.to_string_lossy();
1739 if let Some(stripped) = path_str.strip_prefix("~/") {
1740 let home = dirs::home_dir().unwrap_or_default();
1741 *path = home.join(stripped);
1742 } else if !path.is_absolute() {
1743 let home = dirs::home_dir().unwrap_or_default();
1744 *path = home.join(path_str.as_ref());
1745 }
1746 }
1747
1748 for p in builtin_profiles() {
1750 if config.profiles.get(&p.name).is_none() {
1751 config.profiles.insert_builtin(p);
1752 }
1753 }
1754
1755 for p in builtin_system_prompt_presets() {
1757 if config.system_prompt_presets.get(&p.name).is_none() {
1758 config.system_prompt_presets.insert_builtin(p);
1759 }
1760 }
1761 config
1762 }
1763
1764 fn load_impl(path: &PathBuf) -> Result<Self, Box<dyn std::error::Error>> {
1765 let content = std::fs::read_to_string(path)?;
1766
1767 let parsed: serde_yml::Value = serde_yml::from_str(&content)
1769 .map_err(|e| format!("Failed to parse config file {}: {}", path.display(), e))?;
1770 let mut warnings = Self::validate_unknown_fields(&parsed);
1771
1772 let config: Config = serde_yml::from_str(&content)
1774 .map_err(|e| format!("Failed to parse config file {}: {}", path.display(), e))?;
1775 let config = Self::normalize_config(config);
1776 let config = config.auto_detect_platform();
1777
1778 warnings.extend(config.validate());
1780
1781 if !warnings.is_empty() {
1783 eprintln!("Config validation warnings:");
1784 for warning in &warnings {
1785 eprintln!(
1786 " [{}] {}: {}",
1787 match warning.severity {
1788 ValidationSeverity::Warning => "WARN",
1789 ValidationSeverity::Error => "ERROR",
1790 },
1791 warning.field,
1792 warning.message
1793 );
1794 }
1795 }
1796 Ok(config)
1797 }
1798
1799 pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
1800 let path = Self::config_path();
1801 if path.exists() {
1802 Self::load_impl(&path)
1803 } else {
1804 let mut config = Config::default();
1805 config.save()?;
1806 Ok(config)
1807 }
1808 }
1809
1810 pub fn load_from(path: PathBuf) -> Result<Self, Box<dyn std::error::Error>> {
1811 if path.exists() {
1812 let content = std::fs::read_to_string(&path)?;
1813
1814 let parsed: serde_yml::Value = serde_yml::from_str(&content)
1816 .map_err(|e| format!("Failed to parse config file {}: {}", path.display(), e))?;
1817 let mut warnings = Self::validate_unknown_fields(&parsed);
1818
1819 let config: Config = serde_yml::from_str(&content)
1821 .map_err(|e| format!("Failed to parse config file {}: {}", path.display(), e))?;
1822 let config = Self::normalize_config(config);
1823 let config = config.auto_detect_platform();
1824
1825 warnings.extend(config.validate());
1827
1828 if !warnings.is_empty() {
1829 eprintln!("Config validation warnings:");
1830 for warning in &warnings {
1831 eprintln!(
1832 " [{}] {}: {}",
1833 match warning.severity {
1834 ValidationSeverity::Warning => "WARN",
1835 ValidationSeverity::Error => "ERROR",
1836 },
1837 warning.field,
1838 warning.message
1839 );
1840 }
1841 }
1842 Ok(config)
1843 } else {
1844 Err(format!("Config file not found: {}", path.display()).into())
1845 }
1846 }
1847
1848 fn auto_detect_platform(mut self) -> Self {
1850 if self.default.platform.is_none() {
1851 self.default.platform =
1852 Some(
1853 crate::backend::hardware::platform_name(
1854 crate::backend::hardware::detect_platform(),
1855 )
1856 .to_string(),
1857 );
1858 }
1859 self
1860 }
1861
1862 pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
1863 let path = Self::config_path();
1864 if let Some(parent) = path.parent() {
1865 std::fs::create_dir_all(parent)?;
1866 }
1867 let content = serde_yml::to_string(self)?;
1868 std::fs::write(&path, content)?;
1869 #[cfg(unix)]
1870 {
1871 use std::os::unix::fs::PermissionsExt;
1872 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?;
1873 }
1874 let entries: Vec<(String, ModelOverride)> = self
1876 .model_overrides
1877 .keys()
1878 .iter()
1879 .filter_map(|k| self.model_overrides.get(k).map(|v| (k.clone(), v.clone())))
1880 .collect();
1881 for (name, cfg) in entries {
1882 self.model_overrides.save(&name, &cfg);
1883 }
1884 for profile in self.profiles.user_profiles() {
1886 self.profiles.save(&profile);
1887 }
1888 for preset in self.system_prompt_presets.user_presets() {
1890 self.system_prompt_presets.save(&preset);
1891 }
1892 Ok(())
1893 }
1894
1895 pub fn merged_profiles(&self) -> Vec<Profile> {
1896 self.profiles.all()
1897 }
1898
1899 pub fn merged_presets(&self) -> Vec<SystemPromptPreset> {
1900 self.system_prompt_presets.all()
1901 }
1902}
1903
1904#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1905pub enum LogLevel {
1906 Info,
1907 Warning,
1908 Error,
1909}
1910
1911#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1912pub enum ValidationSeverity {
1913 Warning,
1914 Error,
1915}
1916
1917#[derive(Debug, Clone)]
1918pub struct ValidationWarning {
1919 pub field: String,
1920 pub message: String,
1921 pub severity: ValidationSeverity,
1922}
1923
1924impl LogLevel {
1925 pub fn label(&self) -> &'static str {
1926 match self {
1927 LogLevel::Info => "INFO",
1928 LogLevel::Warning => "WARNING",
1929 LogLevel::Error => "ERROR",
1930 }
1931 }
1932}
1933
1934#[derive(Debug, Clone)]
1935pub struct LogEntry {
1936 pub timestamp: String,
1937 pub level: LogLevel,
1938 pub message: String,
1939}
1940
1941impl LogEntry {
1942 pub fn new(message: impl Into<String>, level: LogLevel) -> Self {
1943 let timestamp = Local::now().format("%H:%M:%S").to_string();
1944 let message = sanitize_log(&message.into());
1945 Self {
1946 timestamp,
1947 level,
1948 message,
1949 }
1950 }
1951}
1952
1953fn sanitize_log(input: &str) -> String {
1956 let max_len = 2000;
1958 let chars: Vec<char> = input.chars().collect();
1959 let truncated = chars.len() > max_len;
1960 let chars = if truncated {
1961 chars[..max_len].to_vec()
1962 } else {
1963 chars
1964 };
1965
1966 let mut output = String::with_capacity(chars.len());
1967 for c in chars {
1968 if c.is_control() && c != '\n' && c != '\t' {
1971 continue;
1972 }
1973 output.push(c);
1974 }
1975
1976 let output = output.replace('\t', " ");
1978
1979 let mut result = output.trim_end().to_string();
1981 if truncated {
1982 result.push_str("... (truncated)");
1983 }
1984 result
1985}