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().max(1) 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 => -2,
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 {
521 base.gpu_layers_mode = match n {
522 -2 => crate::models::GpuLayersMode::Auto,
523 n if n < 0 => crate::models::GpuLayersMode::All,
524 n => crate::models::GpuLayersMode::Specific(n as u32),
525 };
526 }
527
528 }
544}
545
546pub fn builtin_profiles() -> Vec<Profile> {
548 vec![
549 Profile {
550 name: "Qwen".into(),
551 description: "Optimized for Qwen models (dense)".into(),
552 settings: ModelOverride {
553 context_length: Some(131072),
554 temperature: Some(0.7),
555 top_k: Some(20),
556 top_p: Some(0.95),
557 max_tokens: Some(4096),
558 presence_penalty: Some(0.0),
559 uniform_cache: Some(true),
560 jinja: Some(true),
561 ..Default::default()
562 },
563 },
564 Profile {
565 name: "Qwen-MoE".into(),
566 description: "Optimized for Qwen MoE models (35B-A3B)".into(),
567 settings: ModelOverride {
568 context_length: Some(131072),
569 temperature: Some(0.8),
570 top_k: Some(20),
571 top_p: Some(0.95),
572 max_tokens: Some(4096),
573 presence_penalty: Some(1.5),
574 uniform_cache: Some(true),
575 jinja: Some(true),
576 ..Default::default()
577 },
578 },
579 Profile {
580 name: "Qwen-Coding".into(),
581 description: "Optimized for Qwen models in coding mode".into(),
582 settings: ModelOverride {
583 context_length: Some(131072),
584 temperature: Some(0.6),
585 top_k: Some(20),
586 top_p: Some(0.95),
587 max_tokens: Some(4096),
588 presence_penalty: Some(0.0),
589 uniform_cache: Some(true),
590 jinja: Some(true),
591 ..Default::default()
592 },
593 },
594 Profile {
595 name: "Gemma".into(),
596 description: "Optimized for Gemma 2/4 models".into(),
597 settings: ModelOverride {
598 context_length: Some(131072),
599 min_p: Some(0.1),
600 temperature: Some(1.0),
601 top_k: Some(65),
602 top_p: Some(0.95),
603 max_tokens: Some(4096),
604 uniform_cache: Some(true),
605 jinja: Some(true),
606 ..Default::default()
607 },
608 },
609 Profile {
610 name: "Llama".into(),
611 description: "Optimized for Llama 3.1/3.3 models".into(),
612 settings: ModelOverride {
613 context_length: Some(131072),
614 temperature: Some(0.7),
615 top_p: Some(0.9),
616 repeat_penalty: Some(1.1),
617 max_tokens: Some(4096),
618 uniform_cache: Some(true),
619 jinja: Some(true),
620 ..Default::default()
621 },
622 },
623 Profile {
624 name: "Mistral".into(),
625 description: "Optimized for Mistral 7B/NeMo models".into(),
626 settings: ModelOverride {
627 context_length: Some(131072),
628 temperature: Some(0.7),
629 top_k: Some(50),
630 top_p: Some(0.9),
631 max_tokens: Some(4096),
632 uniform_cache: Some(true),
633 jinja: Some(true),
634 ..Default::default()
635 },
636 },
637 Profile {
638 name: "Phi".into(),
639 description: "Optimized for Phi 3.5 Mini models".into(),
640 settings: ModelOverride {
641 context_length: Some(131072),
642 temperature: Some(0.7),
643 top_k: Some(50),
644 top_p: Some(0.9),
645 repeat_penalty: Some(1.1),
646 max_tokens: Some(4096),
647 uniform_cache: Some(true),
648 ..Default::default()
649 },
650 },
651 ]
652}
653
654#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
655#[serde(default)]
656pub struct DefaultParams {
657 #[serde(default)]
659 pub context_length: u32,
660 #[serde(default)]
661 pub threads: u32,
662 #[serde(default)]
663 pub threads_batch: u32,
664 #[serde(default)]
665 pub batch_size: u32,
666 #[serde(default)]
667 pub ubatch_size: u32,
668 #[serde(default = "default_cache_type_k")]
669 pub cache_type_k: Option<CacheTypeK>,
670 #[serde(default = "default_cache_type_v")]
671 pub cache_type_v: Option<CacheTypeV>,
672 #[serde(default)]
673 pub keep: i32,
674 #[serde(default)]
675 pub swa_full: bool,
676 #[serde(default)]
677 pub mlock: bool,
678 #[serde(default)]
679 pub mmap: bool,
680 #[serde(default)]
681 pub numa: NumMode,
682 #[serde(default)]
683 pub uniform_cache: bool,
684 #[serde(default)]
685 pub kv_cache_offload: bool,
686 #[serde(default)]
687 pub parallel: u32,
688 #[serde(default)]
689 pub max_concurrent_predictions: Option<u32>,
690 #[serde(default)]
691 pub system_prompt: String,
692 #[serde(default = "default_system_prompt_preset_name")]
693 pub system_prompt_preset_name: String,
694 #[serde(default)]
696 pub gpu_layers: i32,
697 #[serde(default = "default_gpu_layers_mode")]
698 pub gpu_layers_mode: crate::models::GpuLayersMode,
699 #[serde(default)]
700 pub split_mode: SplitMode,
701 #[serde(default)]
702 pub tensor_split: String,
703 #[serde(default)]
704 pub main_gpu: i32,
705 #[serde(default)]
706 pub fit: bool,
707 #[serde(default)]
708 pub lora: Option<PathBuf>,
709 #[serde(default)]
710 pub lora_scaled: Option<(PathBuf, f32)>,
711 #[serde(default)]
712 pub rpc: String,
713 #[serde(default)]
714 pub embedding: bool,
715 #[serde(default)]
716 pub flash_attn: bool,
717 #[serde(default)]
718 pub jinja: bool,
719 #[serde(default)]
720 pub auto_chat_template: bool,
721 #[serde(default)]
722 pub chat_template: Option<String>,
723 #[serde(default)]
724 pub chat_template_kwargs: Option<String>,
725 #[serde(default)]
726 pub expert_count: i32,
727
728 #[serde(default)]
730 pub seed: i32,
731 #[serde(default)]
732 pub temperature: f32,
733 #[serde(default)]
734 pub top_k: i32,
735 #[serde(default)]
736 pub top_p: f32,
737 #[serde(default)]
738 pub min_p: f32,
739 #[serde(default)]
740 pub typical_p: f32,
741 #[serde(default)]
742 pub mirostat: Mirostat,
743 #[serde(default)]
744 pub mirostat_lr: f32,
745 #[serde(default)]
746 pub mirostat_ent: f32,
747 #[serde(default)]
748 pub ignore_eos: bool,
749 #[serde(default)]
750 pub samplers: Samplers,
751
752 #[serde(default)]
754 pub repeat_penalty: f32,
755 #[serde(default)]
756 pub repeat_last_n: i32,
757 #[serde(default = "default_presence_penalty")]
758 pub presence_penalty: Option<f32>,
759 #[serde(default = "default_frequency_penalty")]
760 pub frequency_penalty: Option<f32>,
761 #[serde(default)]
762 pub dry_multiplier: f32,
763 #[serde(default)]
764 pub dry_base: f32,
765 #[serde(default)]
766 pub dry_allowed_length: i32,
767 #[serde(default)]
768 pub dry_penalty_last_n: i32,
769
770 #[serde(default)]
772 pub rope_scaling: RopeScaling,
773 #[serde(default)]
774 pub rope_scale: f32,
775 #[serde(default)]
776 pub rope_freq_base: f32,
777 #[serde(default)]
778 pub rope_freq_scale: f32,
779 #[serde(default)]
780 pub rope_yarn_enabled: bool,
781
782 #[serde(default)]
784 pub host: String,
785 #[serde(default)]
786 pub port: u16,
787 #[serde(default)]
788 pub timeout: u32,
789 #[serde(default = "default_cache_prompt")]
790 pub cache_prompt: bool,
791 #[serde(default)]
792 pub cache_reuse: u32,
793 #[serde(default)]
794 pub webui: bool,
795 #[serde(default)]
796 pub ws_server_enabled: bool,
797 #[serde(default = "default_ws_server_port")]
798 pub ws_server_port: u16,
799 #[serde(default = "default_server_tls_enabled")]
800 pub server_tls_enabled: bool,
801 #[serde(default)]
802 pub server_tls_cert: Option<String>,
803 #[serde(default)]
804 pub server_tls_key: Option<String>,
805 #[serde(default)]
806 pub router_max_models: u32,
807 #[serde(default)]
808 pub server_mode: crate::models::ServerMode,
809 #[serde(default = "default_log_level")]
810 pub log_level: String,
811
812 #[serde(default = "default_max_tokens")]
814 pub max_tokens: Option<u32>,
815 #[serde(default)]
816 pub cache_type: CacheType,
817 #[serde(default)]
818 pub backend: Backend,
819 #[serde(default)]
821 pub platform: Option<String>,
822 #[serde(default)]
823 pub llama_cpp_version_cpu: Option<String>,
824 #[serde(default)]
825 pub llama_cpp_version_vulkan: Option<String>,
826 #[serde(default)]
827 pub llama_cpp_version_rocm: Option<String>,
828 #[serde(default)]
829 pub llama_cpp_version_rocm_lemonade: Option<String>,
830 #[serde(default)]
831 pub llama_cpp_version_cuda: Option<String>,
832
833 #[serde(default)]
835 pub api_endpoint_enabled: bool,
836 #[serde(default = "default_api_endpoint_port")]
837 pub api_endpoint_port: u16,
838 #[serde(default = "default_web_search_engine")]
839 pub web_search_engine: String,
840 #[serde(default)]
841 pub web_search_engine_url: String,
842 #[serde(default = "default_web_search_enabled")]
843 pub web_search_enabled: bool,
844 #[serde(default)]
845 pub web_search_api_key: Option<String>,
846 #[serde(default)]
847 pub api_endpoint_key: Option<String>,
848 #[serde(default)]
849 pub spec_type: String,
850 #[serde(default)]
851 pub draft_tokens: u32,
852 #[serde(default)]
853 pub tags: Vec<String>,
854}
855
856fn default_api_endpoint_port() -> u16 {
857 49222
858}
859
860fn default_web_search_engine() -> String {
861 "searxng".to_string()
862}
863
864fn default_web_search_enabled() -> bool {
865 false
866}
867
868fn default_system_prompt_preset_name() -> String {
869 "General".to_string()
870}
871
872fn default_cache_type_k() -> Option<CacheTypeK> {
873 None
874}
875fn default_cache_type_v() -> Option<CacheTypeV> {
876 None
877}
878fn default_presence_penalty() -> Option<f32> {
879 None
880}
881fn default_frequency_penalty() -> Option<f32> {
882 None
883}
884fn default_max_tokens() -> Option<u32> {
885 None
886}
887fn default_cache_prompt() -> bool {
888 true
889}
890fn default_ws_server_port() -> u16 {
891 49223
892}
893
894fn default_server_tls_enabled() -> bool {
895 true
896}
897
898fn default_log_level() -> String {
899 "trace".to_string()
900}
901
902fn default_gpu_layers_mode() -> crate::models::GpuLayersMode {
903 crate::models::GpuLayersMode::Auto
904}
905
906impl Default for DefaultParams {
907 fn default() -> Self {
908 Self {
909 context_length: 131072,
911 threads: physical_cores(),
912 threads_batch: 8,
913 batch_size: 512,
914 ubatch_size: 512,
915 cache_type_k: None,
916 cache_type_v: None,
917 keep: 0,
918 swa_full: false,
919 mlock: false,
920 mmap: true,
921 numa: NumMode::None,
922 uniform_cache: true,
923 kv_cache_offload: true,
924 parallel: 1,
925 max_concurrent_predictions: None,
926 system_prompt: DEFAULT_SYSTEM_PROMPT.to_string(),
927 system_prompt_preset_name: "Coder".to_string(),
928
929 gpu_layers: -1,
931 gpu_layers_mode: crate::models::GpuLayersMode::Auto,
932 split_mode: SplitMode::Layer,
933 tensor_split: String::new(),
934 main_gpu: 0,
935 fit: true,
936 lora: None,
937 lora_scaled: None,
938 rpc: String::new(),
939 embedding: false,
940 flash_attn: true,
941 jinja: true,
942 auto_chat_template: false,
943 chat_template: None,
944 chat_template_kwargs: None,
945 expert_count: -1,
946
947 seed: -1,
949 temperature: 0.8,
950 top_k: 40,
951 top_p: 0.95,
952 min_p: 0.0,
953 typical_p: 1.0,
954 mirostat: Mirostat::Off,
955 mirostat_lr: 0.1,
956 mirostat_ent: 5.0,
957 ignore_eos: false,
958 samplers: Samplers::default(),
959
960 repeat_penalty: 1.1,
962 repeat_last_n: 64,
963 presence_penalty: None,
964 frequency_penalty: None,
965 dry_multiplier: 0.0,
966 dry_base: 1.75,
967 dry_allowed_length: 2,
968 dry_penalty_last_n: -1,
969
970 rope_scaling: RopeScaling::None,
972 rope_scale: 1.0,
973 rope_freq_base: 0.0,
974 rope_freq_scale: 1.0,
975 rope_yarn_enabled: false,
976
977 host: "127.0.0.1".to_string(),
979 port: 8080,
980 timeout: 600,
981 cache_prompt: true,
982 cache_reuse: 0,
983 webui: false,
984 ws_server_enabled: false,
985 ws_server_port: 49223,
986 server_tls_enabled: true,
987 server_tls_cert: None,
988 server_tls_key: None,
989 router_max_models: 4,
990 server_mode: crate::models::ServerMode::Normal,
991 log_level: "trace".to_string(),
992
993 max_tokens: None,
995 cache_type: CacheType::F16,
996 backend: {
997 use crate::backend::hardware::{GpuVendor, detect_gpu_vendors};
998 let vendors = detect_gpu_vendors();
999 let mut result = Backend::Cpu;
1000 for v in &vendors {
1001 if matches!(v, GpuVendor::Nvidia) {
1002 result = Backend::Cuda;
1003 break;
1004 }
1005 if matches!(v, GpuVendor::Amd) {
1006 result = Backend::Rocm;
1007 break;
1008 }
1009 if matches!(v, GpuVendor::Intel) {
1010 result = Backend::Vulkan;
1011 break;
1012 }
1013 }
1014 result
1015 },
1016 platform: None,
1017 llama_cpp_version_cpu: None,
1018 llama_cpp_version_vulkan: None,
1019 llama_cpp_version_rocm: None,
1020 llama_cpp_version_rocm_lemonade: None,
1021 llama_cpp_version_cuda: None,
1022 api_endpoint_enabled: false,
1023 api_endpoint_port: 49222,
1024 api_endpoint_key: None,
1025 web_search_engine: "searxng".to_string(),
1026 web_search_engine_url: String::new(),
1027 web_search_enabled: false,
1028 web_search_api_key: None,
1029 spec_type: String::new(),
1030 draft_tokens: 0,
1031 tags: Vec::new(),
1032 }
1033 }
1034}
1035
1036impl Default for Config {
1037 fn default() -> Self {
1038 Self {
1039 models_dirs: vec![
1040 dirs::data_dir()
1041 .unwrap_or_default()
1042 .join("llm-manager")
1043 .join("models"),
1044 ],
1045 llama_server: "llama-server".into(),
1046 default: DefaultParams::default(),
1047 model_overrides: ModelConfigStore::new(),
1048 profiles: Default::default(),
1049 system_prompt_presets: Default::default(),
1050 rpc_workers: Vec::new(),
1051 search_limit: default_search_limit(),
1052 active_panel: ActivePanel::Models,
1053 left_pct: 55,
1054 server_settings_height: 9,
1055 language: default_language(),
1056 onboarding_complete: false,
1057 }
1058 }
1059}
1060
1061impl Config {
1062 pub fn config_path() -> PathBuf {
1063 config_base_dir().join("llm-manager").join("config.yaml")
1064 }
1065
1066 fn config_keys() -> &'static [&'static str] {
1068 &[
1069 "models_dirs",
1070 "llama_server",
1071 "default",
1072 "model_overrides",
1073 "profiles",
1074 "system_prompt_presets",
1075 "rpc_workers",
1076 "search_limit",
1077 "active_panel",
1078 "left_pct",
1079 "server_settings_height",
1080 "language",
1081 "onboarding_complete",
1082 ]
1083 }
1084
1085 fn default_params_keys() -> &'static [&'static str] {
1087 &[
1088 "context_length",
1089 "threads",
1090 "threads_batch",
1091 "batch_size",
1092 "ubatch_size",
1093 "cache_type_k",
1094 "cache_type_v",
1095 "keep",
1096 "swa_full",
1097 "mlock",
1098 "mmap",
1099 "numa",
1100 "uniform_cache",
1101 "kv_cache_offload",
1102 "parallel",
1103 "max_concurrent_predictions",
1104 "system_prompt",
1105 "system_prompt_preset_name",
1106 "gpu_layers",
1107 "gpu_layers_mode",
1108 "split_mode",
1109 "tensor_split",
1110 "main_gpu",
1111 "fit",
1112 "lora",
1113 "lora_scaled",
1114 "rpc",
1115 "embedding",
1116 "flash_attn",
1117 "jinja",
1118 "chat_template",
1119 "chat_template_kwargs",
1120 "expert_count",
1121 "seed",
1122 "temperature",
1123 "top_k",
1124 "top_p",
1125 "min_p",
1126 "typical_p",
1127 "mirostat",
1128 "mirostat_lr",
1129 "mirostat_ent",
1130 "ignore_eos",
1131 "samplers",
1132 "repeat_penalty",
1133 "repeat_last_n",
1134 "presence_penalty",
1135 "frequency_penalty",
1136 "dry_multiplier",
1137 "dry_base",
1138 "dry_allowed_length",
1139 "dry_penalty_last_n",
1140 "rope_scaling",
1141 "rope_scale",
1142 "rope_freq_base",
1143 "rope_freq_scale",
1144 "rope_yarn_enabled",
1145 "host",
1146 "port",
1147 "timeout",
1148 "cache_prompt",
1149 "cache_reuse",
1150 "webui",
1151 "ws_server_enabled",
1152 "ws_server_port",
1153 "server_tls_enabled",
1154 "server_tls_cert",
1155 "server_tls_key",
1156 "router_max_models",
1157 "server_mode",
1158 "log_level",
1159 "max_tokens",
1160 "cache_type",
1161 "backend",
1162 "platform",
1163 "llama_cpp_version_cpu",
1164 "llama_cpp_version_vulkan",
1165 "llama_cpp_version_rocm",
1166 "llama_cpp_version_rocm_lemonade",
1167 "llama_cpp_version_cuda",
1168 "api_endpoint_enabled",
1169 "api_endpoint_port",
1170 "api_endpoint_key",
1171 "web_search_engine",
1172 "web_search_engine_url",
1173 "web_search_enabled",
1174 "web_search_api_key",
1175 "spec_type",
1176 "draft_tokens",
1177 "tags",
1178 "auto_chat_template",
1179 ]
1180 }
1181
1182 fn model_override_keys() -> &'static [&'static str] {
1184 &[
1185 "context_length",
1186 "batch_size",
1187 "ubatch_size",
1188 "cache_type_k",
1189 "cache_type_v",
1190 "keep",
1191 "swa_full",
1192 "mlock",
1193 "mmap",
1194 "numa",
1195 "uniform_cache",
1196 "system_prompt",
1197 "system_prompt_preset_name",
1198 "max_concurrent_predictions",
1199 "threads",
1200 "threads_batch",
1201 "parallel",
1202 "gpu_layers",
1203 "split_mode",
1204 "tensor_split",
1205 "main_gpu",
1206 "fit",
1207 "lora",
1208 "lora_scaled",
1209 "rpc",
1210 "embedding",
1211 "kv_cache_offload",
1212 "flash_attn",
1213 "jinja",
1214 "auto_chat_template",
1215 "chat_template",
1216 "chat_template_kwargs",
1217 "expert_count",
1218 "gpu_layers_mode",
1219 "seed",
1220 "temperature",
1221 "top_k",
1222 "top_p",
1223 "min_p",
1224 "typical_p",
1225 "mirostat",
1226 "mirostat_lr",
1227 "mirostat_ent",
1228 "ignore_eos",
1229 "samplers",
1230 "repeat_penalty",
1231 "repeat_last_n",
1232 "presence_penalty",
1233 "frequency_penalty",
1234 "dry_multiplier",
1235 "dry_base",
1236 "dry_allowed_length",
1237 "dry_penalty_last_n",
1238 "rope_scaling",
1239 "rope_scale",
1240 "rope_freq_base",
1241 "rope_freq_scale",
1242 "rope_yarn_enabled",
1243 "cache_prompt",
1244 "cache_reuse",
1245 "webui",
1246 "max_tokens",
1247 "cache_type",
1248 "llama_cpp_version_cpu",
1249 "llama_cpp_version_vulkan",
1250 "llama_cpp_version_rocm",
1251 "llama_cpp_version_rocm_lemonade",
1252 "llama_cpp_version_cuda",
1253 "spec_type",
1254 "draft_tokens",
1255 "tags",
1256 ]
1257 }
1258
1259 fn rpc_worker_keys() -> &'static [&'static str] {
1261 &["selected", "name", "ip", "port"]
1262 }
1263
1264 pub fn validate_unknown_fields(value: &serde_yml::Value) -> Vec<ValidationWarning> {
1266 let mut warnings = Vec::new();
1267 Self::check_unknown_fields(value, "", Self::config_keys(), &mut warnings);
1268 warnings
1269 }
1270
1271 fn check_unknown_fields(
1272 value: &serde_yml::Value,
1273 prefix: &str,
1274 known_keys: &'static [&'static str],
1275 warnings: &mut Vec<ValidationWarning>,
1276 ) {
1277 if let serde_yml::Value::Mapping(map) = value {
1278 for key in map.keys() {
1279 let key_str = key.as_str();
1280 if !known_keys.contains(&key_str) {
1281 let field = if prefix.is_empty() {
1282 key_str.to_string()
1283 } else {
1284 format!("{}.{}", prefix, key_str)
1285 };
1286 warnings.push(ValidationWarning {
1287 field,
1288 message: format!("Unknown config field: {}", key_str),
1289 severity: ValidationSeverity::Warning,
1290 });
1291 }
1292 }
1293
1294 for (key, val) in map {
1296 let key_str = key.as_str();
1297 let new_prefix = if prefix.is_empty() {
1298 key_str.to_string()
1299 } else {
1300 format!("{}.{}", prefix, key_str)
1301 };
1302
1303 match key_str {
1304 "default" => {
1305 Self::check_unknown_fields(
1306 val,
1307 &new_prefix,
1308 Self::default_params_keys(),
1309 warnings,
1310 );
1311 }
1312 "model_overrides" => {
1313 if let serde_yml::Value::Mapping(overrides) = val {
1314 for (override_key, override_val) in overrides {
1315 let k = override_key.as_str();
1316 let override_prefix = format!("{}.{}", new_prefix, k);
1317 Self::check_unknown_fields(
1318 override_val,
1319 &override_prefix,
1320 Self::model_override_keys(),
1321 warnings,
1322 );
1323 }
1324 }
1325 }
1326 "rpc_workers" => {
1327 if let serde_yml::Value::Sequence(items) = val {
1328 for item in items {
1329 Self::check_unknown_fields(
1330 item,
1331 &new_prefix,
1332 Self::rpc_worker_keys(),
1333 warnings,
1334 );
1335 }
1336 }
1337 }
1338 _ => {
1339 Self::check_unknown_fields(val, &new_prefix, known_keys, warnings);
1340 }
1341 }
1342 }
1343 }
1344 }
1345
1346 pub fn validate(&self) -> Vec<ValidationWarning> {
1348 let mut warnings = Vec::new();
1349 let default = &self.default;
1350
1351 if default.context_length < 512 || default.context_length > 1048576 {
1353 warnings.push(ValidationWarning {
1354 field: "default.context_length".to_string(),
1355 message: format!(
1356 "context_length {} is outside recommended range 512-1048576",
1357 default.context_length
1358 ),
1359 severity: ValidationSeverity::Warning,
1360 });
1361 }
1362 if default.threads == 0 {
1363 warnings.push(ValidationWarning {
1364 field: "default.threads".to_string(),
1365 message: "threads must be greater than 0".to_string(),
1366 severity: ValidationSeverity::Warning,
1367 });
1368 }
1369 if default.temperature < 0.0 || default.temperature > 2.0 {
1370 warnings.push(ValidationWarning {
1371 field: "default.temperature".to_string(),
1372 message: format!(
1373 "temperature {} is outside recommended range 0.0-2.0",
1374 default.temperature
1375 ),
1376 severity: ValidationSeverity::Warning,
1377 });
1378 }
1379 if default.top_k < 0 {
1380 warnings.push(ValidationWarning {
1381 field: "default.top_k".to_string(),
1382 message: "top_k must be >= 0".to_string(),
1383 severity: ValidationSeverity::Warning,
1384 });
1385 }
1386 if default.top_p < 0.0 || default.top_p > 1.0 {
1387 warnings.push(ValidationWarning {
1388 field: "default.top_p".to_string(),
1389 message: format!(
1390 "top_p {} is outside recommended range 0.0-1.0",
1391 default.top_p
1392 ),
1393 severity: ValidationSeverity::Warning,
1394 });
1395 }
1396 if default.min_p < 0.0 || default.min_p > 1.0 {
1397 warnings.push(ValidationWarning {
1398 field: "default.min_p".to_string(),
1399 message: format!(
1400 "min_p {} is outside recommended range 0.0-1.0",
1401 default.min_p
1402 ),
1403 severity: ValidationSeverity::Warning,
1404 });
1405 }
1406 if default.typical_p < 0.0 || default.typical_p > 1.0 {
1407 warnings.push(ValidationWarning {
1408 field: "default.typical_p".to_string(),
1409 message: format!(
1410 "typical_p {} is outside recommended range 0.0-1.0",
1411 default.typical_p
1412 ),
1413 severity: ValidationSeverity::Warning,
1414 });
1415 }
1416 if default.repeat_penalty < 0.0 || default.repeat_penalty > 3.0 {
1417 warnings.push(ValidationWarning {
1418 field: "default.repeat_penalty".to_string(),
1419 message: format!(
1420 "repeat_penalty {} is outside recommended range 0.0-3.0",
1421 default.repeat_penalty
1422 ),
1423 severity: ValidationSeverity::Warning,
1424 });
1425 }
1426 if default.mirostat_lr < 0.0 || default.mirostat_lr > 1.0 {
1427 warnings.push(ValidationWarning {
1428 field: "default.mirostat_lr".to_string(),
1429 message: format!(
1430 "mirostat_lr {} is outside recommended range 0.0-1.0",
1431 default.mirostat_lr
1432 ),
1433 severity: ValidationSeverity::Warning,
1434 });
1435 }
1436 if default.mirostat_ent < 0.0 || default.mirostat_ent > 10.0 {
1437 warnings.push(ValidationWarning {
1438 field: "default.mirostat_ent".to_string(),
1439 message: format!(
1440 "mirostat_ent {} is outside recommended range 0.0-10.0",
1441 default.mirostat_ent
1442 ),
1443 severity: ValidationSeverity::Warning,
1444 });
1445 }
1446 if default.dry_multiplier < 0.0 {
1447 warnings.push(ValidationWarning {
1448 field: "default.dry_multiplier".to_string(),
1449 message: "dry_multiplier must be >= 0".to_string(),
1450 severity: ValidationSeverity::Warning,
1451 });
1452 }
1453 if default.dry_base <= 0.0 {
1454 warnings.push(ValidationWarning {
1455 field: "default.dry_base".to_string(),
1456 message: "dry_base must be > 0".to_string(),
1457 severity: ValidationSeverity::Warning,
1458 });
1459 }
1460 if default.rope_scale <= 0.0 {
1461 warnings.push(ValidationWarning {
1462 field: "default.rope_scale".to_string(),
1463 message: "rope_scale must be > 0".to_string(),
1464 severity: ValidationSeverity::Warning,
1465 });
1466 }
1467 if default.rope_freq_scale <= 0.0 {
1468 warnings.push(ValidationWarning {
1469 field: "default.rope_freq_scale".to_string(),
1470 message: "rope_freq_scale must be > 0".to_string(),
1471 severity: ValidationSeverity::Warning,
1472 });
1473 }
1474 if default.port == 0 {
1475 warnings.push(ValidationWarning {
1476 field: "default.port".to_string(),
1477 message: "port must be between 1 and 65535".to_string(),
1478 severity: ValidationSeverity::Error,
1479 });
1480 }
1481 if default.port < 1024 {
1482 warnings.push(ValidationWarning {
1483 field: "default.port".to_string(),
1484 message: format!("port {} is a privileged port (< 1024)", default.port),
1485 severity: ValidationSeverity::Warning,
1486 });
1487 }
1488 if default.api_endpoint_port == 0 {
1489 warnings.push(ValidationWarning {
1490 field: "default.api_endpoint_port".to_string(),
1491 message: "api_endpoint_port must be between 1 and 65535".to_string(),
1492 severity: ValidationSeverity::Error,
1493 });
1494 }
1495 if default.ws_server_port == 0 {
1496 warnings.push(ValidationWarning {
1497 field: "default.ws_server_port".to_string(),
1498 message: "ws_server_port must be between 1 and 65535".to_string(),
1499 severity: ValidationSeverity::Error,
1500 });
1501 }
1502 if default.timeout < 1 {
1503 warnings.push(ValidationWarning {
1504 field: "default.timeout".to_string(),
1505 message: "timeout must be at least 1 second".to_string(),
1506 severity: ValidationSeverity::Warning,
1507 });
1508 }
1509 if default.router_max_models == 0 {
1510 warnings.push(ValidationWarning {
1511 field: "default.router_max_models".to_string(),
1512 message: "router_max_models must be >= 1".to_string(),
1513 severity: ValidationSeverity::Warning,
1514 });
1515 }
1516 if let Some(max_tokens) = default.max_tokens {
1517 if max_tokens == 0 {
1518 warnings.push(ValidationWarning {
1519 field: "default.max_tokens".to_string(),
1520 message: "max_tokens must be > 0".to_string(),
1521 severity: ValidationSeverity::Warning,
1522 });
1523 } else if max_tokens > 65536 {
1524 warnings.push(ValidationWarning {
1525 field: "default.max_tokens".to_string(),
1526 message: format!("max_tokens {} is very large (> 65536)", max_tokens),
1527 severity: ValidationSeverity::Warning,
1528 });
1529 }
1530 }
1531 if default.context_length > 262144 {
1532 warnings.push(ValidationWarning {
1533 field: "default.context_length".to_string(),
1534 message: format!(
1535 "context_length {} may cause high memory usage",
1536 default.context_length
1537 ),
1538 severity: ValidationSeverity::Warning,
1539 });
1540 }
1541 if default.threads_batch > default.batch_size && default.batch_size > 0 {
1542 warnings.push(ValidationWarning {
1543 field: "default.threads_batch".to_string(),
1544 message: "threads_batch should not exceed batch_size".to_string(),
1545 severity: ValidationSeverity::Warning,
1546 });
1547 }
1548
1549 if let Some(lora) = &default.lora
1551 && !lora.exists()
1552 {
1553 warnings.push(ValidationWarning {
1554 field: "default.lora".to_string(),
1555 message: format!("lora path does not exist: {}", lora.display()),
1556 severity: ValidationSeverity::Warning,
1557 });
1558 }
1559 if let Some((lora, _)) = &default.lora_scaled
1560 && !lora.exists()
1561 {
1562 warnings.push(ValidationWarning {
1563 field: "default.lora_scaled".to_string(),
1564 message: format!("lora path does not exist: {}", lora.display()),
1565 severity: ValidationSeverity::Warning,
1566 });
1567 }
1568 if self.llama_server.is_absolute() && !self.llama_server.exists() {
1569 warnings.push(ValidationWarning {
1570 field: "llama_server".to_string(),
1571 message: format!(
1572 "llama_server binary not found: {}",
1573 self.llama_server.display()
1574 ),
1575 severity: ValidationSeverity::Warning,
1576 });
1577 }
1578
1579 for model_name in self.model_overrides.keys() {
1581 if let Some(override_settings) = self.model_overrides.get(model_name.as_str()) {
1582 if let Some(lora) = &override_settings.lora
1583 && !lora.exists()
1584 {
1585 warnings.push(ValidationWarning {
1586 field: format!("model_overrides.{}.lora", model_name),
1587 message: format!("lora path does not exist: {}", lora.display()),
1588 severity: ValidationSeverity::Warning,
1589 });
1590 }
1591 if let Some((lora, _)) = &override_settings.lora_scaled
1592 && !lora.exists()
1593 {
1594 warnings.push(ValidationWarning {
1595 field: format!("model_overrides.{}.lora_scaled", model_name),
1596 message: format!("lora path does not exist: {}", lora.display()),
1597 severity: ValidationSeverity::Warning,
1598 });
1599 }
1600 }
1601 }
1602
1603 if default.port == default.api_endpoint_port {
1605 warnings.push(ValidationWarning {
1606 field: "default.port".to_string(),
1607 message: format!(
1608 "port conflict: server port and API endpoint port both set to {}",
1609 default.port
1610 ),
1611 severity: ValidationSeverity::Warning,
1612 });
1613 }
1614 if default.port == default.ws_server_port {
1615 warnings.push(ValidationWarning {
1616 field: "default.port".to_string(),
1617 message: format!(
1618 "port conflict: server port and WS server port both set to {}",
1619 default.port
1620 ),
1621 severity: ValidationSeverity::Warning,
1622 });
1623 }
1624 if default.api_endpoint_port == default.ws_server_port {
1625 warnings.push(ValidationWarning {
1626 field: "default.api_endpoint_port".to_string(),
1627 message: format!(
1628 "port conflict: API endpoint port and WS server port both set to {}",
1629 default.api_endpoint_port
1630 ),
1631 severity: ValidationSeverity::Warning,
1632 });
1633 }
1634 if matches!(default.server_mode, crate::models::ServerMode::Router)
1635 && default.router_max_models < 2
1636 {
1637 warnings.push(ValidationWarning {
1638 field: "default.router_max_models".to_string(),
1639 message: "router_max_models should be >= 2 for Router mode".to_string(),
1640 severity: ValidationSeverity::Warning,
1641 });
1642 }
1643 if !default.spec_type.is_empty() && default.draft_tokens == 0 {
1644 warnings.push(ValidationWarning {
1645 field: "default.spec_type".to_string(),
1646 message: "spec_type is set but draft_tokens is 0".to_string(),
1647 severity: ValidationSeverity::Warning,
1648 });
1649 }
1650 if default.server_tls_enabled {
1651 if let Some(cert) = &default.server_tls_cert
1652 && !cert.is_empty()
1653 && !std::path::Path::new(cert).exists()
1654 {
1655 warnings.push(ValidationWarning {
1656 field: "default.server_tls_cert".to_string(),
1657 message: format!("TLS cert path does not exist: {}", cert),
1658 severity: ValidationSeverity::Warning,
1659 });
1660 }
1661 if let Some(key) = &default.server_tls_key
1662 && !key.is_empty()
1663 && !std::path::Path::new(key).exists()
1664 {
1665 warnings.push(ValidationWarning {
1666 field: "default.server_tls_key".to_string(),
1667 message: format!("TLS key path does not exist: {}", key),
1668 severity: ValidationSeverity::Warning,
1669 });
1670 }
1671 }
1672
1673 warnings
1674 }
1675
1676 pub fn resolve_settings(
1678 &self,
1679 model_name: Option<&str>,
1680 profile_name: Option<&str>,
1681 ) -> crate::models::ModelSettings {
1682 let mut settings = crate::models::ModelSettings::from_config(self);
1683
1684 if let Some(name) = model_name
1686 && let Some(override_settings) = self.model_overrides.get(name)
1687 {
1688 override_settings.apply(&mut settings);
1689 }
1690
1691 if let Some(p_name) = profile_name {
1693 if let Some(profile) = self.profiles.get(p_name) {
1694 profile.settings.apply(&mut settings);
1695 } else if let Some(profile) = builtin_profiles().iter().find(|p| p.name == p_name) {
1696 profile.settings.apply(&mut settings);
1697 }
1698 }
1699
1700 if let Some(preset) = self
1702 .system_prompt_presets
1703 .get(&settings.system_prompt_preset_name)
1704 {
1705 settings.system_prompt = preset.content.clone();
1706 }
1707
1708 settings
1709 }
1710
1711 pub fn get_preset_content(&self, name: &str) -> Option<String> {
1713 self.system_prompt_presets
1714 .get(name)
1715 .map(|p| p.content.clone())
1716 }
1717
1718 fn normalize_config(mut config: Config) -> Config {
1719 for path in &mut config.models_dirs {
1721 let path_str = path.to_string_lossy();
1722 if let Some(stripped) = path_str.strip_prefix("~/") {
1723 let home = dirs::home_dir().unwrap_or_default();
1724 *path = home.join(stripped);
1725 } else if !path.is_absolute() {
1726 let home = dirs::home_dir().unwrap_or_default();
1727 *path = home.join(path_str.as_ref());
1728 }
1729 }
1730
1731 for p in builtin_profiles() {
1733 if config.profiles.get(&p.name).is_none() {
1734 config.profiles.insert_builtin(p);
1735 }
1736 }
1737
1738 for p in builtin_system_prompt_presets() {
1740 if config.system_prompt_presets.get(&p.name).is_none() {
1741 config.system_prompt_presets.insert_builtin(p);
1742 }
1743 }
1744 config
1745 }
1746
1747 fn load_impl(path: &PathBuf) -> Result<Self, Box<dyn std::error::Error>> {
1748 let content = std::fs::read_to_string(path)?;
1749
1750 let parsed: serde_yml::Value = serde_yml::from_str(&content)
1752 .map_err(|e| format!("Failed to parse config file {}: {}", path.display(), e))?;
1753 let mut warnings = Self::validate_unknown_fields(&parsed);
1754
1755 let config: Config = serde_yml::from_str(&content)
1757 .map_err(|e| format!("Failed to parse config file {}: {}", path.display(), e))?;
1758 let config = Self::normalize_config(config);
1759 let config = config.auto_detect_platform();
1760
1761 warnings.extend(config.validate());
1763
1764 if !warnings.is_empty() {
1766 eprintln!("Config validation warnings:");
1767 for warning in &warnings {
1768 eprintln!(
1769 " [{}] {}: {}",
1770 match warning.severity {
1771 ValidationSeverity::Warning => "WARN",
1772 ValidationSeverity::Error => "ERROR",
1773 },
1774 warning.field,
1775 warning.message
1776 );
1777 }
1778 }
1779 Ok(config)
1780 }
1781
1782 pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
1783 let path = Self::config_path();
1784 if path.exists() {
1785 Self::load_impl(&path)
1786 } else {
1787 let mut config = Config::default();
1788 config.save()?;
1789 Ok(config)
1790 }
1791 }
1792
1793 pub fn load_from(path: PathBuf) -> Result<Self, Box<dyn std::error::Error>> {
1794 if path.exists() {
1795 Self::load_impl(&path)
1796 } else {
1797 Err(format!("Config file not found: {}", path.display()).into())
1798 }
1799 }
1800
1801 fn auto_detect_platform(mut self) -> Self {
1803 if self.default.platform.is_none() {
1804 self.default.platform =
1805 Some(
1806 crate::backend::hardware::platform_name(
1807 crate::backend::hardware::detect_platform(),
1808 )
1809 .to_string(),
1810 );
1811 }
1812 self
1813 }
1814
1815 pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
1816 let path = Self::config_path();
1817 if let Some(parent) = path.parent() {
1818 std::fs::create_dir_all(parent)?;
1819 }
1820 let content = serde_yml::to_string(self)?;
1821 std::fs::write(&path, content)?;
1822 #[cfg(unix)]
1823 {
1824 use std::os::unix::fs::PermissionsExt;
1825 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?;
1826 }
1827 let entries: Vec<(String, ModelOverride)> = self
1829 .model_overrides
1830 .keys()
1831 .iter()
1832 .filter_map(|k| self.model_overrides.get(k).map(|v| (k.clone(), v.clone())))
1833 .collect();
1834 for (name, cfg) in entries {
1835 self.model_overrides.save(&name, &cfg);
1836 }
1837 for profile in self.profiles.user_profiles() {
1839 self.profiles.save(&profile);
1840 }
1841 for preset in self.system_prompt_presets.user_presets() {
1843 self.system_prompt_presets.save(&preset);
1844 }
1845 Ok(())
1846 }
1847
1848 pub fn merged_profiles(&self) -> Vec<Profile> {
1849 self.profiles.all()
1850 }
1851
1852 pub fn merged_presets(&self) -> Vec<SystemPromptPreset> {
1853 self.system_prompt_presets.all()
1854 }
1855}
1856
1857#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1858pub enum LogLevel {
1859 Info,
1860 Warning,
1861 Error,
1862}
1863
1864#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1865pub enum ValidationSeverity {
1866 Warning,
1867 Error,
1868}
1869
1870#[derive(Debug, Clone)]
1871pub struct ValidationWarning {
1872 pub field: String,
1873 pub message: String,
1874 pub severity: ValidationSeverity,
1875}
1876
1877impl LogLevel {
1878 pub fn label(&self) -> &'static str {
1879 match self {
1880 LogLevel::Info => "INFO",
1881 LogLevel::Warning => "WARNING",
1882 LogLevel::Error => "ERROR",
1883 }
1884 }
1885}
1886
1887#[derive(Debug, Clone)]
1888pub struct LogEntry {
1889 pub timestamp: String,
1890 pub level: LogLevel,
1891 pub message: String,
1892}
1893
1894impl LogEntry {
1895 pub fn new(message: impl Into<String>, level: LogLevel) -> Self {
1896 let timestamp = Local::now().format("%H:%M:%S").to_string();
1897 let message = sanitize_log(&message.into());
1898 Self {
1899 timestamp,
1900 level,
1901 message,
1902 }
1903 }
1904}
1905
1906fn sanitize_log(input: &str) -> String {
1909 let max_len = 2000;
1911 let chars: Vec<char> = input.chars().collect();
1912 let truncated = chars.len() > max_len;
1913 let chars = if truncated {
1914 chars[..max_len].to_vec()
1915 } else {
1916 chars
1917 };
1918
1919 let mut output = String::with_capacity(chars.len());
1920 for c in chars {
1921 if c.is_control() && c != '\n' && c != '\t' {
1924 continue;
1925 }
1926 output.push(c);
1927 }
1928
1929 let output = output.replace('\t', " ");
1931
1932 let mut result = output.trim_end().to_string();
1934 if truncated {
1935 result.push_str("... (truncated)");
1936 }
1937 result
1938}