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