Skip to main content

llm_manager/tui/
settings.rs

1use crate::config::Profile;
2use crate::models::{
3    CacheQuantType, GpuLayersMode, Mirostat, ModelSettings, NumMode, SplitMode,
4};
5use ratatui::{
6    style::{Color, Modifier, Style},
7    text::{Line, Span},
8};
9
10// ── Function pointer types (zero-cost, no dynamic dispatch) ──────────────────
11
12pub type DisplayFn = fn(&ModelSettings) -> String;
13pub type DirtyFn = fn(&ModelSettings, &ModelSettings) -> bool;
14pub type AdjustFn = fn(&mut ModelSettings, i32, u32); // u32 = context_limit (0 = no limit)
15pub type ApplyEditFn = fn(&mut ModelSettings, &str);
16pub type CtrlEToggleFn = fn(&mut ModelSettings);
17
18// ── Edit kinds ───────────────────────────────────────────────────────────────
19
20#[derive(Debug, Clone, Copy, PartialEq)]
21pub enum EditKind {
22    /// Direct text entry (digits, decimals, etc.)
23    Direct,
24    /// Toggles a boolean on Enter
25    Toggle,
26    /// Opens a modal (picker, etc.)
27    Modal,
28}
29
30// ── SettingField ─────────────────────────────────────────────────────────────
31
32pub struct SettingField {
33    pub id: &'static str,
34    pub name: &'static str,
35    pub section: &'static str,
36    pub display: DisplayFn,
37    pub dirty: DirtyFn,
38    pub adjust: AdjustFn,
39    pub apply_edit: ApplyEditFn,
40    pub ctrl_e_toggle: Option<CtrlEToggleFn>,
41    #[allow(dead_code)]
42    pub edit_kind: EditKind,
43    pub is_expert: bool,
44    pub is_ultra: bool,
45}
46
47impl SettingField {
48    pub fn name(&self) -> &str {
49        self.name
50    }
51
52    pub fn display(&self, settings: &ModelSettings) -> String {
53        (self.display)(settings)
54    }
55
56    pub fn is_dirty(&self, settings: &ModelSettings, cached: &ModelSettings) -> bool {
57        (self.dirty)(settings, cached)
58    }
59
60    pub fn adjust(&self, settings: &mut ModelSettings, delta: i32, context_limit: u32) {
61        (self.adjust)(settings, delta, context_limit);
62    }
63
64    pub fn apply_edit(&self, settings: &mut ModelSettings, buf: &str) {
65        (self.apply_edit)(settings, buf);
66    }
67
68    pub fn ctrl_e_toggle(&self, settings: &mut ModelSettings) {
69        if let Some(toggle) = self.ctrl_e_toggle {
70            toggle(settings);
71        }
72    }
73
74    /// Returns true if this field starts a new section (different from the previous field's section).
75    pub fn is_new_section(&self, prev_section: Option<&str>) -> bool {
76        Some(self.section) != prev_section
77    }
78}
79
80// ── Helper constructors (generated from macro) ───────────────────────────────
81
82/// Generate a field constructor function.
83/// Variants:
84///   - `field` / `expert_field` / `ultra_field` — no ctrl_e_toggle
85///   - `field_with_toggle` / `expert_field_with_toggle` / `ultra_field_with_toggle` — with ctrl_e_toggle
86macro_rules! make_field_fn {
87    ($fn:ident, $expert:expr, $ultra:expr, toggle) => {
88        fn $fn(
89            id: &'static str,
90            name: &'static str,
91            section: &'static str,
92            display: DisplayFn,
93            dirty: DirtyFn,
94            adjust: AdjustFn,
95            apply_edit: ApplyEditFn,
96            ctrl_e_toggle: CtrlEToggleFn,
97            edit_kind: EditKind,
98        ) -> SettingField {
99            SettingField {
100                id,
101                name,
102                section,
103                display,
104                dirty,
105                adjust,
106                apply_edit,
107                ctrl_e_toggle: Some(ctrl_e_toggle),
108                edit_kind,
109                is_expert: $expert,
110                is_ultra: $ultra,
111            }
112        }
113    };
114    ($fn:ident, $expert:expr, $ultra:expr, @none) => {
115        fn $fn(
116            id: &'static str,
117            name: &'static str,
118            section: &'static str,
119            display: DisplayFn,
120            dirty: DirtyFn,
121            adjust: AdjustFn,
122            apply_edit: ApplyEditFn,
123            edit_kind: EditKind,
124        ) -> SettingField {
125            SettingField {
126                id,
127                name,
128                section,
129                display,
130                dirty,
131                adjust,
132                apply_edit,
133                ctrl_e_toggle: None,
134                edit_kind,
135                is_expert: $expert,
136                is_ultra: $ultra,
137            }
138        }
139    };
140}
141
142make_field_fn!(field, false, false, @none);
143make_field_fn!(expert_field, true, false, @none);
144make_field_fn!(ultra_field, true, true, @none);
145make_field_fn!(field_with_toggle, false, false, toggle);
146make_field_fn!(expert_field_with_toggle, true, false, toggle);
147make_field_fn!(ultra_field_with_toggle, true, true, toggle);
148
149// ── Shared adjustment and toggle logic ───────────────────────────────────────
150
151fn gpu_layers_adjust(settings: &mut ModelSettings, delta: i32, _context_limit: u32) {
152    settings.gpu_layers_mode = match (delta, &settings.gpu_layers_mode) {
153        (1, GpuLayersMode::Auto) => GpuLayersMode::Specific(1),
154        (1, GpuLayersMode::Specific(n)) => GpuLayersMode::Specific(n + 1),
155        (1, GpuLayersMode::All) => GpuLayersMode::Auto,
156        (-1, GpuLayersMode::Auto) => GpuLayersMode::Auto,
157        (-1, GpuLayersMode::Specific(n)) if *n == 0 => GpuLayersMode::Auto,
158        (-1, GpuLayersMode::Specific(n)) if *n == 1 => GpuLayersMode::Specific(0),
159        (-1, GpuLayersMode::Specific(n)) => GpuLayersMode::Specific(n - 1),
160        (-1, GpuLayersMode::All) => GpuLayersMode::All,
161        _ => settings.gpu_layers_mode,
162    };
163}
164
165fn gpu_layers_apply(settings: &mut ModelSettings, buf: &str) {
166    if let Ok(v) = buf.parse::<i32>() {
167        settings.gpu_layers_mode = if v < 0 {
168            GpuLayersMode::All
169        } else {
170            GpuLayersMode::Specific(v as u32)
171        };
172    }
173}
174
175fn toggle_mlock(settings: &mut ModelSettings) {
176    settings.mlock = !settings.mlock;
177}
178fn toggle_flash_attn(settings: &mut ModelSettings) {
179    settings.flash_attn = !settings.flash_attn;
180}
181
182fn toggle_fit(settings: &mut ModelSettings) {
183    settings.fit = !settings.fit;
184}
185fn toggle_kv_cache_offload(settings: &mut ModelSettings) {
186    settings.kv_cache_offload = !settings.kv_cache_offload;
187}
188fn toggle_uniform_cache(settings: &mut ModelSettings) {
189    settings.uniform_cache = !settings.uniform_cache;
190}
191fn toggle_mtp(settings: &mut ModelSettings) {
192    if settings.spec_type.is_empty() {
193        settings.spec_type = "draft-mtp".to_string();
194    } else {
195        settings.spec_type = String::new();
196    }
197}
198
199fn toggle_rope_yarn_enabled(settings: &mut ModelSettings) {
200    settings.rope_yarn_enabled = !settings.rope_yarn_enabled;
201}
202fn toggle_ignore_eos(settings: &mut ModelSettings) {
203    settings.ignore_eos = !settings.ignore_eos;
204}
205fn toggle_max_tokens(settings: &mut ModelSettings) {
206    settings.max_tokens = settings.max_tokens.map_or(Some(2048), |_| None);
207}
208fn toggle_max_concurrent_predictions(settings: &mut ModelSettings) {
209    settings.max_concurrent_predictions = settings
210        .max_concurrent_predictions
211        .map_or(Some(1), |_| None);
212}
213fn toggle_cache_type_k(settings: &mut ModelSettings) {
214    settings.cache_type_k = settings.cache_type_k.map_or(Some(CacheQuantType::F16), |_| None);
215}
216fn toggle_cache_type_v(settings: &mut ModelSettings) {
217    settings.cache_type_v = settings.cache_type_v.map_or(Some(CacheQuantType::F16), |_| None);
218}
219fn toggle_expert_count(settings: &mut ModelSettings) {
220    settings.expert_count = match settings.expert_count {
221        0 => 1,
222        -1 => 0,
223        _ => -1,
224    };
225}
226fn toggle_presence_penalty(settings: &mut ModelSettings) {
227    settings.presence_penalty = settings.presence_penalty.map_or(Some(0.0), |_| None);
228}
229fn toggle_frequency_penalty(settings: &mut ModelSettings) {
230    settings.frequency_penalty = settings.frequency_penalty.map_or(Some(0.0), |_| None);
231}
232
233// ── Diff macros for profile settings comparison ──────────────────────────────
234
235macro_rules! diff_int {
236    ($parts:expr, $s:expr, $c:expr, $field:ident, $label:literal) => {
237        if let Some(v) = $s.$field && v != $c.$field {
238            $parts.push(format!("{}={}", $label, v));
239        }
240    };
241}
242macro_rules! diff_float {
243    ($parts:expr, $s:expr, $c:expr, $field:ident, $label:literal) => {
244        if let Some(v) = $s.$field && (v - $c.$field).abs() > 0.001 {
245            $parts.push(format!("{}={:.2}", $label, v));
246        }
247    };
248}
249macro_rules! diff_bool {
250    ($parts:expr, $s:expr, $c:expr, $field:ident, $label:literal) => {
251        if let Some(v) = $s.$field && v != $c.$field {
252            $parts.push(format!("{}={}", $label, v));
253        }
254    };
255}
256macro_rules! diff_string {
257    ($parts:expr, $s:expr, $c:expr, $field:ident, $label:literal) => {
258        if let Some(v) = &$s.$field && v != &$c.$field {
259            $parts.push(format!("{}={}", $label, v));
260        }
261    };
262}
263macro_rules! diff_enum {
264    ($parts:expr, $s:expr, $c:expr, $field:ident, $label:literal) => {
265        if let Some(ref v) = $s.$field && *v != $c.$field {
266            $parts.push(format!("{}={}", $label, v));
267        }
268    };
269}
270macro_rules! diff_option {
271    ($parts:expr, $s:expr, $c:expr, $field:ident, $label:literal) => {
272        if $s.$field != $c.$field {
273            if let Some(ref v) = $s.$field {
274                $parts.push(format!("{}={}", $label, v));
275            }
276        }
277    };
278}
279macro_rules! diff_option_float {
280    ($parts:expr, $s:expr, $c:expr, $field:ident, $label:literal) => {
281        if let Some(v) = $s.$field {
282            let current_val = $c.$field.unwrap_or(0.0);
283            if (v - current_val).abs() > 0.001 {
284                $parts.push(format!("{}={:.2}", $label, v));
285            }
286        }
287    };
288}
289
290// ── All Fields (Interleaved for context-aware expert mode) ────────────────────
291
292macro_rules! make_cache_type_field {
293    ($field:ident, $name:literal, $toggle:ident) => {
294        expert_field_with_toggle(
295            stringify!($field),
296            $name,
297            "GPU Offload",
298            |s| s.$field.map(|v| v.to_string()).unwrap_or_else(|| "Disabled".to_string()),
299            |s, c| s.$field != c.$field,
300            |s, delta, _| {
301                let mut val = s.$field.unwrap_or(CacheQuantType::F16);
302                val = if delta > 0 { val.next() } else { val.prev() };
303                s.$field = Some(val);
304            },
305            |s, buf| {
306                if let Ok(n) = buf.parse::<u8>() {
307                    s.$field = Some(CacheQuantType::from_u8(n));
308                }
309            },
310            $toggle,
311            EditKind::Direct,
312        )
313    };
314}
315
316pub fn all_fields() -> Vec<SettingField> {
317    vec![
318        // ── Loading ───────────────────────────────────────────────────────────
319        field(
320            "system_prompt_preset_name",
321            "Prompt",
322            "Loading",
323            |s| s.system_prompt_preset_name.clone(),
324            |s, c| s.system_prompt_preset_name != c.system_prompt_preset_name,
325            |_, _, _| {},
326            |_, _| {},
327            EditKind::Modal,
328        ),
329        field(
330            "context_length",
331            "Context",
332            "Loading",
333            |s| s.context_length.to_string(),
334            |s, c| s.context_length != c.context_length,
335            |s, delta, ctx_limit| {
336                let mut val = (s.context_length as i32 + delta * 128).max(128) as u32;
337                if ctx_limit > 0 {
338                    val = val.min(ctx_limit);
339                }
340                s.context_length = val;
341            },
342            |s, buf| {
343                if let Ok(v) = buf.parse::<u32>() {
344                    s.context_length = v.max(128);
345                }
346            },
347            EditKind::Direct,
348        ),
349        expert_field_with_toggle(
350            "rope_yarn_enabled",
351            "Yarn RoPE",
352            "Loading",
353            |s| s.rope_yarn_enabled.to_string(),
354            |s, c| s.rope_yarn_enabled != c.rope_yarn_enabled,
355            |_, _, _| {},
356            |_, _| {},
357            toggle_rope_yarn_enabled,
358            EditKind::Toggle,
359        ),
360        expert_field(
361            "yarn_params",
362            "Yarn Params",
363            "Loading",
364            |s| {
365                format!(
366                    "scale={:.2} base={:.2} scale_f={:.2}",
367                    s.rope_scale, s.rope_freq_base, s.rope_freq_scale
368                )
369            },
370            |s, c| {
371                s.rope_scale != c.rope_scale
372                    || s.rope_freq_base != c.rope_freq_base
373                    || s.rope_freq_scale != c.rope_freq_scale
374            },
375            |_, _, _| {},
376            |_, _| {},
377            EditKind::Modal,
378        ),
379        ultra_field(
380            "threads_batch",
381            "Threads Batch",
382            "Loading",
383            |s| s.threads_batch.to_string(),
384            |s, c| s.threads_batch != c.threads_batch,
385            |s, delta, _| {
386                s.threads_batch = (s.threads_batch as i32 + delta).max(1) as u32;
387            },
388            |s, buf| {
389                if let Ok(v) = buf.parse::<u32>() {
390                    s.threads_batch = v.max(1);
391                }
392            },
393            EditKind::Direct,
394        ),
395        ultra_field(
396            "ubatch_size",
397            "UBatch Size",
398            "Loading",
399            |s| s.ubatch_size.to_string(),
400            |s, c| s.ubatch_size != c.ubatch_size,
401            |s, delta, _| {
402                s.ubatch_size = (s.ubatch_size as i32 + delta * 64).max(1) as u32;
403            },
404            |s, buf| {
405                if let Ok(v) = buf.parse::<u32>() {
406                    s.ubatch_size = v.max(1);
407                }
408            },
409            EditKind::Direct,
410        ),
411        ultra_field(
412            "keep",
413            "Keep",
414            "Loading",
415            |s| s.keep.to_string(),
416            |s, c| s.keep != c.keep,
417            |s, delta, _| {
418                s.keep = (s.keep + delta).max(0);
419            },
420            |s, buf| {
421                if let Ok(v) = buf.parse::<i32>() {
422                    s.keep = v;
423                }
424            },
425            EditKind::Direct,
426        ),
427        field_with_toggle(
428            "mlock",
429            "Keep in memory (mlock)",
430            "Loading",
431            |s| s.mlock.to_string(),
432            |s, c| s.mlock != c.mlock,
433            |_, _, _| {},
434            |_, _| {},
435            toggle_mlock,
436            EditKind::Toggle,
437        ),
438        expert_field(
439            "numa",
440            "NUMA",
441            "Loading",
442            |s| s.numa.to_string(),
443            |s, c| s.numa != c.numa,
444            |s, delta, _| {
445                let mut val = s.numa;
446                val = match (delta, val) {
447                    (1, NumMode::None) => NumMode::Distribute,
448                    (1, NumMode::Distribute) => NumMode::Isolate,
449                    (1, NumMode::Isolate) => NumMode::Numactl,
450                    (1, NumMode::Numactl) => NumMode::None,
451                    (-1, NumMode::None) => NumMode::Numactl,
452                    (-1, NumMode::Distribute) => NumMode::None,
453                    (-1, NumMode::Isolate) => NumMode::Distribute,
454                    (-1, NumMode::Numactl) => NumMode::Isolate,
455                    _ => val,
456                };
457                s.numa = val;
458            },
459            |_, _| {},
460            EditKind::Toggle,
461        ),
462        // ── GPU Offload ───────────────────────────────────────────────────────
463        field(
464            "gpu_layers_mode",
465            "GPU Layers",
466            "GPU Offload",
467            |s| match s.gpu_layers_mode {
468                GpuLayersMode::Auto => "Auto".to_string(),
469                GpuLayersMode::Specific(n) => n.to_string(),
470                GpuLayersMode::All => "All".to_string(),
471            },
472            |s, c| s.gpu_layers_mode != c.gpu_layers_mode,
473            gpu_layers_adjust,
474            gpu_layers_apply,
475            EditKind::Direct,
476        ),
477        ultra_field(
478            "split_mode",
479            "Split Mode",
480            "GPU Offload",
481            |s| s.split_mode.to_string(),
482            |s, c| s.split_mode != c.split_mode,
483            |s, delta, _| {
484                let mut val = s.split_mode;
485                val = match (delta, val) {
486                    (1, SplitMode::None) => SplitMode::Layer,
487                    (1, SplitMode::Layer) => SplitMode::Row,
488                    (1, SplitMode::Row) => SplitMode::Tensor,
489                    (1, SplitMode::Tensor) => SplitMode::None,
490                    (-1, SplitMode::None) => SplitMode::Tensor,
491                    (-1, SplitMode::Layer) => SplitMode::None,
492                    (-1, SplitMode::Row) => SplitMode::Layer,
493                    (-1, SplitMode::Tensor) => SplitMode::Row,
494                    _ => val,
495                };
496                s.split_mode = val;
497            },
498            |_, _| {},
499            EditKind::Toggle,
500        ),
501        ultra_field(
502            "tensor_split",
503            "Tensor Split",
504            "GPU Offload",
505            |s| s.tensor_split.clone(),
506            |s, c| s.tensor_split != c.tensor_split,
507            |_, _, _| {},
508            |_, _| {},
509            EditKind::Modal,
510        ),
511        expert_field(
512            "main_gpu",
513            "Main GPU",
514            "GPU Offload",
515            |s| s.main_gpu.to_string(),
516            |s, c| s.main_gpu != c.main_gpu,
517            |s, delta, _| {
518                s.main_gpu = (s.main_gpu + delta).max(0);
519            },
520            |s, buf| {
521                if let Ok(v) = buf.parse::<i32>() {
522                    s.main_gpu = v;
523                }
524            },
525            EditKind::Direct,
526        ),
527        field_with_toggle(
528            "fit",
529            "Fit",
530            "GPU Offload",
531            |s| s.fit.to_string(),
532            |s, c| s.fit != c.fit,
533            |_, _, _| {},
534            |_, _| {},
535            toggle_fit,
536            EditKind::Toggle,
537        ),
538        field_with_toggle(
539            "flash_attn",
540            "Flash Attention",
541            "GPU Offload",
542            |s| s.flash_attn.to_string(),
543            |s, c| s.flash_attn != c.flash_attn,
544            |_, _, _| {},
545            |_, _| {},
546            toggle_flash_attn,
547            EditKind::Toggle,
548        ),
549        field_with_toggle(
550            "kv_cache_offload",
551            "KV Cache Offload",
552            "GPU Offload",
553            |s| s.kv_cache_offload.to_string(),
554            |s, c| s.kv_cache_offload != c.kv_cache_offload,
555            |_, _, _| {},
556            |_, _| {},
557            toggle_kv_cache_offload,
558            EditKind::Toggle,
559        ),
560        // ── Cache type fields ──────────────────────────────────────────────────
561
562        make_cache_type_field!(cache_type_k, "Cache Type K", toggle_cache_type_k),
563        make_cache_type_field!(cache_type_v, "Cache Type V", toggle_cache_type_v),
564        expert_field_with_toggle(
565            "expert_count",
566            "Active Experts",
567            "GPU Offload",
568            |s| {
569                if s.expert_count > 0 {
570                    s.expert_count.to_string()
571                } else if s.expert_count == -1 {
572                    "Auto".to_string()
573                } else {
574                    "Disabled".to_string()
575                }
576            },
577            |s, c| s.expert_count != c.expert_count,
578            |s, delta, _| {
579                s.expert_count = (s.expert_count + delta).clamp(-1, 99);
580            },
581            |s, buf| {
582                if let Ok(v) = buf.parse::<i32>() {
583                    s.expert_count = v.clamp(-1, 99);
584                }
585            },
586            toggle_expert_count,
587            EditKind::Direct,
588        ),
589        // ── Evaluation ────────────────────────────────────────────────────────
590        field(
591            "batch_size",
592            "Eval Batch",
593            "Evaluation",
594            |s| s.batch_size.to_string(),
595            |s, c| s.batch_size != c.batch_size,
596            |s, delta, _| {
597                s.batch_size = (s.batch_size as i32 + delta * 64).max(1) as u32;
598            },
599            |s, buf| {
600                if let Ok(v) = buf.parse::<u32>() {
601                    s.batch_size = v.max(1);
602                }
603            },
604            EditKind::Direct,
605        ),
606        field_with_toggle(
607            "uniform_cache",
608            "Unified KV",
609            "Evaluation",
610            |s| s.uniform_cache.to_string(),
611            |s, c| s.uniform_cache != c.uniform_cache,
612            |_, _, _| {},
613            |_, _| {},
614            toggle_uniform_cache,
615            EditKind::Toggle,
616        ),
617        expert_field_with_toggle(
618            "max_concurrent_predictions",
619            "Max Concurrent Pred",
620            "Evaluation",
621            |s| {
622                s.max_concurrent_predictions
623                    .map(|v| v.to_string())
624                    .unwrap_or_else(|| "Off".to_string())
625            },
626            |s, c| s.max_concurrent_predictions != c.max_concurrent_predictions,
627            |s, delta, _| match s.max_concurrent_predictions {
628                Some(n) => {
629                    s.max_concurrent_predictions = Some(((n as i32) + delta).clamp(1, 10) as u32)
630                }
631                None => s.max_concurrent_predictions = Some(1),
632            },
633            |s, buf| {
634                if let Ok(v) = buf.parse::<u32>() {
635                    s.max_concurrent_predictions = Some(v.clamp(1, 10));
636                }
637            },
638            toggle_max_concurrent_predictions,
639            EditKind::Direct,
640        ),
641        // ── Sampling ──────────────────────────────────────────────────────────
642        field(
643            "seed",
644            "Seed",
645            "Sampling",
646            |s| s.seed.to_string(),
647            |s, c| s.seed != c.seed,
648            |s, delta, _| {
649                s.seed = (s.seed + delta).max(-1);
650            },
651            |s, buf| {
652                if let Ok(v) = buf.parse::<i32>() {
653                    s.seed = v;
654                }
655            },
656            EditKind::Direct,
657        ),
658        field(
659            "temperature",
660            "Temp",
661            "Sampling",
662            |s| format!("{:.2}", s.temperature),
663            |s, c| (s.temperature - c.temperature).abs() > 0.001,
664            |s, delta, _| {
665                s.temperature =
666                    ((s.temperature * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 2.0);
667            },
668            |s, buf| {
669                if let Ok(v) = buf.parse::<i32>() {
670                    s.temperature = (v as f32 / 100.0).clamp(0.0, 2.0);
671                }
672            },
673            EditKind::Direct,
674        ),
675        field(
676            "top_k",
677            "Top-k",
678            "Sampling",
679            |s| s.top_k.to_string(),
680            |s, c| s.top_k != c.top_k,
681            |s, delta, _| {
682                s.top_k = (s.top_k + delta).max(1);
683            },
684            |s, buf| {
685                if let Ok(v) = buf.parse::<i32>() {
686                    s.top_k = v.max(0);
687                }
688            },
689            EditKind::Direct,
690        ),
691        field(
692            "top_p",
693            "Top-p",
694            "Sampling",
695            |s| format!("{:.2}", s.top_p),
696            |s, c| (s.top_p - c.top_p).abs() > 0.001,
697            |s, delta, _| {
698                s.top_p = ((s.top_p * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 1.0);
699            },
700            |s, buf| {
701                if let Ok(v) = buf.parse::<i32>() {
702                    s.top_p = (v as f32 / 100.0).clamp(0.0, 1.0);
703                }
704            },
705            EditKind::Direct,
706        ),
707        field(
708            "min_p",
709            "Min P",
710            "Sampling",
711            |s| format!("{:.2}", s.min_p),
712            |s, c| (s.min_p - c.min_p).abs() > 0.001,
713            |s, delta, _| {
714                s.min_p = ((s.min_p * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 1.0);
715            },
716            |s, buf| {
717                if let Ok(v) = buf.parse::<i32>() {
718                    s.min_p = (v as f32 / 100.0).clamp(0.0, 1.0);
719                }
720            },
721            EditKind::Direct,
722        ),
723        ultra_field(
724            "typical_p",
725            "Typical P",
726            "Sampling",
727            |s| format!("{:.2}", s.typical_p),
728            |s, c| (s.typical_p - c.typical_p).abs() > 0.001,
729            |s, delta, _| {
730                s.typical_p = ((s.typical_p * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 1.0);
731            },
732            |s, buf| {
733                if let Ok(v) = buf.parse::<i32>() {
734                    s.typical_p = (v as f32 / 100.0).clamp(0.0, 1.0);
735                }
736            },
737            EditKind::Direct,
738        ),
739        ultra_field(
740            "mirostat",
741            "Mirostat",
742            "Sampling",
743            |s| s.mirostat.to_string(),
744            |s, c| s.mirostat != c.mirostat,
745            |s, delta, _| {
746                let mut val = s.mirostat;
747                val = match (delta, val) {
748                    (1, Mirostat::Off) => Mirostat::V1,
749                    (1, Mirostat::V1) => Mirostat::Mirostat2,
750                    (1, Mirostat::Mirostat2) => Mirostat::Off,
751                    (-1, Mirostat::Off) => Mirostat::Mirostat2,
752                    (-1, Mirostat::V1) => Mirostat::Off,
753                    (-1, Mirostat::Mirostat2) => Mirostat::V1,
754                    _ => val,
755                };
756                s.mirostat = val;
757            },
758            |_, _| {},
759            EditKind::Toggle,
760        ),
761        ultra_field(
762            "mirostat_lr",
763            "Mirostat LR",
764            "Sampling",
765            |s| format!("{:.2}", s.mirostat_lr),
766            |s, c| (s.mirostat_lr - c.mirostat_lr).abs() > 0.001,
767            |s, delta, _| {
768                s.mirostat_lr =
769                    ((s.mirostat_lr * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 1.0);
770            },
771            |s, buf| {
772                if let Ok(v) = buf.parse::<i32>() {
773                    s.mirostat_lr = (v as f32 / 100.0).clamp(0.0, 1.0);
774                }
775            },
776            EditKind::Direct,
777        ),
778        ultra_field(
779            "mirostat_ent",
780            "Mirostat Ent",
781            "Sampling",
782            |s| format!("{:.2}", s.mirostat_ent),
783            |s, c| (s.mirostat_ent - c.mirostat_ent).abs() > 0.001,
784            |s, delta, _| {
785                s.mirostat_ent =
786                    ((s.mirostat_ent * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 10.0);
787            },
788            |s, buf| {
789                if let Ok(v) = buf.parse::<i32>() {
790                    s.mirostat_ent = (v as f32 / 100.0).clamp(0.0, 10.0);
791                }
792            },
793            EditKind::Direct,
794        ),
795        ultra_field_with_toggle(
796            "ignore_eos",
797            "Ignore EOS",
798            "Sampling",
799            |s| s.ignore_eos.to_string(),
800            |s, c| s.ignore_eos != c.ignore_eos,
801            |_, _, _| {},
802            |_, _| {},
803            toggle_ignore_eos,
804            EditKind::Toggle,
805        ),
806        ultra_field(
807            "samplers",
808            "Samplers",
809            "Sampling",
810            |s| s.samplers.0.clone(),
811            |s, c| s.samplers.0 != c.samplers.0,
812            |_, _, _| {},
813            |_, _| {},
814            EditKind::Modal,
815        ),
816        field_with_toggle(
817            "max_tokens",
818            "Max Tokens",
819            "Sampling",
820            |s| {
821                s.max_tokens
822                    .map(|v| v.to_string())
823                    .unwrap_or_else(|| "Disabled".to_string())
824            },
825            |s, c| s.max_tokens != c.max_tokens,
826            |s, delta, _| {
827                let current = s.max_tokens.unwrap_or(2048);
828                s.max_tokens = Some((current as i32 + delta * 16).max(16) as u32);
829            },
830            |s, buf| {
831                if let Ok(v) = buf.parse::<i32>() {
832                    s.max_tokens = if v == 0 { None } else { Some(v as u32) };
833                }
834            },
835            toggle_max_tokens,
836            EditKind::Direct,
837        ),
838        // ── Repetition ────────────────────────────────────────────────────────
839        field(
840            "repeat_penalty",
841            "Repeat Penalty",
842            "Repetition",
843            |s| format!("{:.2}", s.repeat_penalty),
844            |s, c| (s.repeat_penalty - c.repeat_penalty).abs() > 0.001,
845            |s, delta, _| {
846                s.repeat_penalty =
847                    ((s.repeat_penalty * 100.0 + delta as f32 * 5.0) / 100.0).clamp(1.0, 2.0);
848            },
849            |s, buf| {
850                if let Ok(v) = buf.parse::<i32>() {
851                    s.repeat_penalty = (v as f32 / 100.0).clamp(0.0, 2.0);
852                }
853            },
854            EditKind::Direct,
855        ),
856        field(
857            "repeat_last_n",
858            "Repeat Last N",
859            "Repetition",
860            |s| s.repeat_last_n.to_string(),
861            |s, c| s.repeat_last_n != c.repeat_last_n,
862            |s, delta, _| {
863                s.repeat_last_n = (s.repeat_last_n + delta).max(0);
864            },
865            |s, buf| {
866                if let Ok(v) = buf.parse::<i32>() {
867                    s.repeat_last_n = v.max(0);
868                }
869            },
870            EditKind::Direct,
871        ),
872        expert_field_with_toggle(
873            "presence_penalty",
874            "Presence Penalty",
875            "Repetition",
876            |s| {
877                s.presence_penalty
878                    .map(|v| {
879                        if (v - 0.0).abs() < 0.001 {
880                            "Off".to_string()
881                        } else {
882                            format!("{:.2}", v)
883                        }
884                    })
885                    .unwrap_or_else(|| "Off".to_string())
886            },
887            |s, c| match (s.presence_penalty, c.presence_penalty) {
888                (Some(v1), Some(v2)) => (v1 - v2).abs() > 0.001,
889                (None, None) => false,
890                _ => true,
891            },
892            |s, delta, _| {
893                let current = s.presence_penalty.unwrap_or(0.0);
894                s.presence_penalty =
895                    Some(((current * 100.0 + delta as f32 * 5.0) / 100.0).clamp(-2.0, 2.0));
896            },
897            |s, buf| {
898                if let Ok(v) = buf.parse::<i32>() {
899                    s.presence_penalty = Some((v as f32 / 100.0).clamp(0.0, 1.0));
900                }
901            },
902            toggle_presence_penalty,
903            EditKind::Direct,
904        ),
905        expert_field_with_toggle(
906            "frequency_penalty",
907            "Freq Penalty",
908            "Repetition",
909            |s| {
910                s.frequency_penalty
911                    .map(|v| {
912                        if (v - 0.0).abs() < 0.001 {
913                            "Off".to_string()
914                        } else {
915                            format!("{:.2}", v)
916                        }
917                    })
918                    .unwrap_or_else(|| "Off".to_string())
919            },
920            |s, c| match (s.frequency_penalty, c.frequency_penalty) {
921                (Some(v1), Some(v2)) => (v1 - v2).abs() > 0.001,
922                (None, None) => false,
923                _ => true,
924            },
925            |s, delta, _| {
926                let current = s.frequency_penalty.unwrap_or(0.0);
927                s.frequency_penalty =
928                    Some(((current * 100.0 + delta as f32 * 5.0) / 100.0).clamp(-2.0, 2.0));
929            },
930            |s, buf| {
931                if let Ok(v) = buf.parse::<i32>() {
932                    s.frequency_penalty = Some((v as f32 / 100.0).clamp(0.0, 1.0));
933                }
934            },
935            toggle_frequency_penalty,
936            EditKind::Direct,
937        ),
938        // ── DRY ───────────────────────────────────────────────────────────────
939        ultra_field(
940            "dry_multiplier",
941            "DRY Multiplier",
942            "DRY",
943            |s| format!("{:.2}", s.dry_multiplier),
944            |s, c| (s.dry_multiplier - c.dry_multiplier).abs() > 0.001,
945            |s, delta, _| {
946                s.dry_multiplier =
947                    ((s.dry_multiplier * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 10.0);
948            },
949            |s, buf| {
950                if let Ok(v) = buf.parse::<i32>() {
951                    s.dry_multiplier = (v as f32 / 100.0).clamp(0.0, 10.0);
952                }
953            },
954            EditKind::Direct,
955        ),
956        ultra_field(
957            "dry_base",
958            "DRY Base",
959            "DRY",
960            |s| format!("{:.2}", s.dry_base),
961            |s, c| (s.dry_base - c.dry_base).abs() > 0.001,
962            |s, delta, _| {
963                s.dry_base = ((s.dry_base * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 10.0);
964            },
965            |s, buf| {
966                if let Ok(v) = buf.parse::<i32>() {
967                    s.dry_base = (v as f32 / 100.0).clamp(0.0, 10.0);
968                }
969            },
970            EditKind::Direct,
971        ),
972        ultra_field(
973            "dry_allowed_length",
974            "DRY Allowed Length",
975            "DRY",
976            |s| s.dry_allowed_length.to_string(),
977            |s, c| s.dry_allowed_length != c.dry_allowed_length,
978            |s, delta, _| {
979                s.dry_allowed_length = (s.dry_allowed_length + delta).max(0);
980            },
981            |s, buf| {
982                if let Ok(v) = buf.parse::<i32>() {
983                    s.dry_allowed_length = v;
984                }
985            },
986            EditKind::Direct,
987        ),
988        ultra_field(
989            "dry_penalty_last_n",
990            "DRY Penalty Last N",
991            "DRY",
992            |s| s.dry_penalty_last_n.to_string(),
993            |s, c| s.dry_penalty_last_n != c.dry_penalty_last_n,
994            |s, delta, _| {
995                s.dry_penalty_last_n = (s.dry_penalty_last_n + delta).max(0);
996            },
997            |s, buf| {
998                if let Ok(v) = buf.parse::<i32>() {
999                    s.dry_penalty_last_n = v;
1000                }
1001            },
1002            EditKind::Direct,
1003        ),
1004        // ── Speculative Decoding ─────────────────────────────────────────────
1005        expert_field_with_toggle(
1006            "is_mtp",
1007            "MTP",
1008            "Speculative",
1009            |s| {
1010                if s.spec_type.is_empty() {
1011                    "Off".to_string()
1012                } else {
1013                    s.spec_type.clone()
1014                }
1015            },
1016            |s, c| s.spec_type != c.spec_type,
1017            |_, _, _| {},
1018            |_, _| {},
1019            toggle_mtp,
1020            EditKind::Modal,
1021        ),
1022        expert_field(
1023            "spec_type",
1024            "Spec Type",
1025            "Speculative",
1026            |s| {
1027                if s.spec_type.is_empty() {
1028                    "Off".to_string()
1029                } else {
1030                    s.spec_type.clone()
1031                }
1032            },
1033            |s, c| s.spec_type != c.spec_type,
1034            |_, _, _| {},
1035            |_, _| {},
1036            EditKind::Modal,
1037        ),
1038        expert_field(
1039            "draft_tokens",
1040            "Spec Draft N Max",
1041            "Speculative",
1042            |s| s.draft_tokens.to_string(),
1043            |s, c| s.draft_tokens != c.draft_tokens,
1044            |s, delta, _| {
1045                s.draft_tokens = (s.draft_tokens as i32 + delta).clamp(0, 16) as u32;
1046            },
1047            |s, buf| {
1048                if let Ok(v) = buf.parse::<u32>() {
1049                    s.draft_tokens = v.min(16);
1050                }
1051            },
1052            EditKind::Direct,
1053        ),
1054        // ── Tags ──────────────────────────────────────────────────────────────
1055        field(
1056            "tags",
1057            "Tags (Enter to edit)",
1058            "Tags",
1059            |s| {
1060                if s.tags.is_empty() {
1061                    "None".to_string()
1062                } else {
1063                    s.tags.join(", ")
1064                }
1065            },
1066            |s, c| s.tags != c.tags,
1067            |_, _, _| {},
1068            |_, _| {},
1069            EditKind::Modal,
1070        ),
1071        // ── Backend ───────────────────────────────────────────────────────────
1072        field(
1073            "backend_version",
1074            "LLama.cpp Version",
1075            "Backend",
1076            |s| s.get_active_backend_version_display().to_string(),
1077            |s, c| s.get_active_backend_version() != c.get_active_backend_version(),
1078            |_, _, _| {},
1079            |_, _| {},
1080            EditKind::Modal,
1081        ),
1082    ]
1083}
1084
1085pub fn filtered_fields(expert_mode: bool) -> Vec<SettingField> {
1086    all_fields()
1087        .into_iter()
1088        .filter(|f| {
1089            if !expert_mode {
1090                !f.is_expert
1091            } else {
1092                !f.is_ultra // In expert mode, hide ultra experts
1093            }
1094        })
1095        .collect()
1096}
1097
1098// ── Simple helper for the server settings panel (tabbed.rs) ──────────────────
1099
1100/// Render a single setting line for the server settings panel.
1101#[allow(clippy::too_many_arguments)]
1102pub fn add_setting(
1103    lines: &mut Vec<Line<'static>>,
1104    total_count: &mut usize,
1105    _settings: &ModelSettings,
1106    _cached: &ModelSettings,
1107    selected_line_idx: &mut usize,
1108    selected_content_line: &mut usize,
1109    idx: usize,
1110    name: &str,
1111    val: &str,
1112    selected: usize,
1113    _edit_buf: &str,
1114    _editing: bool,
1115    disabled: bool,
1116) {
1117    let current_line = lines.len();
1118    let name_style = if disabled {
1119        Style::default().fg(Color::DarkGray)
1120    } else {
1121        Style::default().fg(Color::Yellow)
1122    };
1123    let val_style = if disabled {
1124        Style::default()
1125            .fg(Color::DarkGray)
1126            .add_modifier(Modifier::DIM)
1127    } else {
1128        Style::default().fg(Color::White)
1129    };
1130    if idx == selected {
1131        *selected_line_idx = current_line;
1132        *selected_content_line = current_line;
1133        lines.push(Line::from(vec![
1134            Span::styled(
1135                "> ",
1136                Style::default()
1137                    .fg(Color::Yellow)
1138                    .add_modifier(if disabled {
1139                        Modifier::DIM
1140                    } else {
1141                        Modifier::BOLD
1142                    }),
1143            ),
1144            Span::styled(format!("{name}: "), name_style),
1145            Span::styled(
1146                val.to_string(),
1147                Style::default()
1148                    .fg(Color::Black)
1149                    .bg(Color::Yellow)
1150                    .add_modifier(Modifier::BOLD),
1151            ),
1152        ]));
1153    } else {
1154        lines.push(Line::from(vec![
1155            Span::styled("  ", name_style),
1156            Span::styled(format!("{name}: "), name_style),
1157            Span::styled(val.to_string(), val_style),
1158        ]));
1159    }
1160    *total_count += 1;
1161}
1162
1163/// Build a list of setting names that differ between a profile and the current settings.
1164pub fn profile_settings_parts(profile: &Profile, current: &ModelSettings) -> Vec<String> {
1165    let mut parts = Vec::new();
1166    let s = &profile.settings;
1167
1168    // ── Integers ──────────────────────────────────────────────────────────
1169    diff_int!(parts, s, current, context_length, "ctx");
1170    diff_int!(parts, s, current, threads, "threads");
1171    diff_int!(parts, s, current, threads_batch, "threads_batch");
1172    diff_int!(parts, s, current, batch_size, "batch");
1173    diff_int!(parts, s, current, ubatch_size, "ubatch");
1174    diff_int!(parts, s, current, parallel, "parallel");
1175    diff_option!(parts, s, current, max_concurrent_predictions, "concurrent");
1176    diff_int!(parts, s, current, cache_reuse, "cache_reuse");
1177    diff_option!(parts, s, current, max_tokens, "max_tokens");
1178   diff_int!(parts, s, current, draft_tokens, "draft_tokens");
1179
1180    diff_int!(parts, s, current, keep, "keep");
1181    diff_int!(parts, s, current, main_gpu, "main_gpu");
1182    diff_int!(parts, s, current, expert_count, "expert_count");
1183    diff_int!(parts, s, current, seed, "seed");
1184    diff_int!(parts, s, current, top_k, "top_k");
1185    diff_int!(parts, s, current, repeat_last_n, "repeat_last_n");
1186    diff_int!(parts, s, current, dry_allowed_length, "dry_allowed");
1187    diff_int!(parts, s, current, dry_penalty_last_n, "dry_penalty_last_n");
1188
1189    // ── Floats ────────────────────────────────────────────────────────────
1190    diff_float!(parts, s, current, temperature, "temp");
1191    diff_float!(parts, s, current, top_p, "top_p");
1192    diff_float!(parts, s, current, min_p, "min_p");
1193    diff_float!(parts, s, current, typical_p, "typical_p");
1194    diff_float!(parts, s, current, mirostat_lr, "mirostat_lr");
1195    diff_float!(parts, s, current, mirostat_ent, "mirostat_ent");
1196    diff_float!(parts, s, current, repeat_penalty, "rep_pen");
1197    diff_option_float!(parts, s, current, presence_penalty, "pres_pen");
1198    diff_option_float!(parts, s, current, frequency_penalty, "freq_pen");
1199    diff_float!(parts, s, current, dry_multiplier, "dry_mult");
1200    diff_float!(parts, s, current, dry_base, "dry_base");
1201    diff_float!(parts, s, current, rope_scale, "rope_scale");
1202    diff_float!(parts, s, current, rope_freq_base, "rope_freq_base");
1203    diff_float!(parts, s, current, rope_freq_scale, "rope_freq_scale");
1204
1205    // ── Bools ─────────────────────────────────────────────────────────────
1206    diff_bool!(parts, s, current, swa_full, "swa_full");
1207    diff_bool!(parts, s, current, mlock, "mlock");
1208    diff_bool!(parts, s, current, mmap, "mmap");
1209    diff_bool!(parts, s, current, uniform_cache, "uniform_cache");
1210    diff_bool!(parts, s, current, kv_cache_offload, "kv_cache_offload");
1211    diff_bool!(parts, s, current, fit, "fit");
1212    diff_bool!(parts, s, current, embedding, "embedding");
1213    diff_bool!(parts, s, current, flash_attn, "flash_attn");
1214    diff_bool!(parts, s, current, jinja, "jinja");
1215    diff_bool!(parts, s, current, ignore_eos, "ignore_eos");
1216    diff_bool!(parts, s, current, rope_yarn_enabled, "yarn_enabled");
1217    diff_bool!(parts, s, current, cache_prompt, "cache_prompt");
1218    diff_bool!(parts, s, current, webui, "webui");
1219    // ── Strings ───────────────────────────────────────────────────────────
1220    diff_string!(parts, s, current, system_prompt_preset_name, "preset");
1221    diff_string!(parts, s, current, tensor_split, "tensor_split");
1222    diff_string!(parts, s, current, rpc, "rpc");
1223    diff_option!(parts, s, current, chat_template, "chat_template");
1224    diff_option!(parts, s, current, chat_template_kwargs, "chat_template_kwargs");
1225    diff_option!(parts, s, current, llama_cpp_version_cpu, "llama_cpp_cpu");
1226    diff_option!(parts, s, current, llama_cpp_version_vulkan, "llama_cpp_vulkan");
1227    diff_option!(parts, s, current, llama_cpp_version_rocm, "llama_cpp_rocm");
1228    diff_option!(parts, s, current, llama_cpp_version_rocm_lemonade, "llama_cpp_rocm_lemonade");
1229    diff_option!(parts, s, current, llama_cpp_version_cuda, "llama_cpp_cuda");
1230    diff_string!(parts, s, current, spec_type, "spec_type");
1231
1232    // ── Enums ─────────────────────────────────────────────────────────────
1233    diff_enum!(parts, s, current, numa, "numa");
1234    diff_enum!(parts, s, current, split_mode, "split_mode");
1235    diff_enum!(parts, s, current, mirostat, "mirostat");
1236    diff_enum!(parts, s, current, samplers, "samplers");
1237    diff_enum!(parts, s, current, rope_scaling, "rope_scaling");
1238    diff_enum!(parts, s, current, cache_type, "cache_type");
1239    diff_option!(parts, s, current, cache_type_k, "cache_type_k");
1240    diff_option!(parts, s, current, cache_type_v, "cache_type_v");
1241
1242    // ── Special (custom display) ──────────────────────────────────────────
1243    if let Some(v) = s.gpu_layers_mode && v != current.gpu_layers_mode {
1244        let display = match v {
1245            crate::models::GpuLayersMode::Auto => "Auto".to_string(),
1246            crate::models::GpuLayersMode::Specific(n) => n.to_string(),
1247            crate::models::GpuLayersMode::All => "All".to_string(),
1248        };
1249        parts.push(format!("gpu_layers={}", display));
1250    }
1251
1252    parts
1253}