Skip to main content

llm_manager/tui/
settings.rs

1use crate::config::Profile;
2use crate::models::{CacheQuantType, GpuLayersMode, Mirostat, ModelSettings, NumMode, SplitMode};
3use crate::tui::colors::*;
4use crate::tui::format_context_k;
5use ratatui::{
6    style::{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// ── SettingField ─────────────────────────────────────────────────────────────
19
20pub struct SettingField {
21    pub id: &'static str,
22    pub name: &'static str,
23    pub section: &'static str,
24    pub display: DisplayFn,
25    pub dirty: DirtyFn,
26    pub adjust: AdjustFn,
27    pub apply_edit: ApplyEditFn,
28    pub ctrl_e_toggle: Option<CtrlEToggleFn>,
29    pub is_expert: bool,
30    pub is_ultra: bool,
31    pub is_enabled: Option<fn(&ModelSettings) -> bool>,
32}
33
34impl SettingField {
35    pub fn name(&self) -> &str {
36        self.name
37    }
38
39    pub fn display(&self, settings: &ModelSettings) -> String {
40        (self.display)(settings)
41    }
42
43    pub fn is_dirty(&self, settings: &ModelSettings, cached: &ModelSettings) -> bool {
44        (self.dirty)(settings, cached)
45    }
46
47    pub fn adjust(&self, settings: &mut ModelSettings, delta: i32, context_limit: u32) {
48        (self.adjust)(settings, delta, context_limit);
49    }
50
51    pub fn apply_edit(&self, settings: &mut ModelSettings, buf: &str) {
52        (self.apply_edit)(settings, buf);
53    }
54
55    pub fn ctrl_e_toggle(&self, settings: &mut ModelSettings) {
56        if let Some(toggle) = self.ctrl_e_toggle {
57            toggle(settings);
58        }
59    }
60
61    /// Returns true if this field starts a new section (different from the previous field's section).
62    pub fn is_new_section(&self, prev_section: Option<&str>) -> bool {
63        Some(self.section) != prev_section
64    }
65}
66
67// ── Helper constructors (generated from macro) ───────────────────────────────
68
69/// Generate a field constructor function.
70/// Variants:
71///   - `field` / `expert_field` / `ultra_field` — no ctrl_e_toggle
72///   - `field_with_toggle` / `expert_field_with_toggle` / `ultra_field_with_toggle` — with ctrl_e_toggle
73macro_rules! make_field_fn {
74    ($fn:ident, $expert:expr, $ultra:expr, toggle) => {
75        fn $fn(
76            id: &'static str,
77            name: &'static str,
78            section: &'static str,
79            display: DisplayFn,
80            dirty: DirtyFn,
81            adjust: AdjustFn,
82            apply_edit: ApplyEditFn,
83            ctrl_e_toggle: CtrlEToggleFn,
84            _help_text: &'static str,
85        ) -> SettingField {
86            SettingField {
87                id,
88                name,
89                section,
90                display,
91                dirty,
92                adjust,
93                apply_edit,
94                ctrl_e_toggle: Some(ctrl_e_toggle),
95                is_expert: $expert,
96                is_ultra: $ultra,
97                is_enabled: None,
98            }
99        }
100    };
101    ($fn:ident, $expert:expr, $ultra:expr, @none) => {
102        fn $fn(
103            id: &'static str,
104            name: &'static str,
105            section: &'static str,
106            display: DisplayFn,
107            dirty: DirtyFn,
108            adjust: AdjustFn,
109            apply_edit: ApplyEditFn,
110            _help_text: &'static str,
111        ) -> SettingField {
112            SettingField {
113                id,
114                name,
115                section,
116                display,
117                dirty,
118                adjust,
119                apply_edit,
120                ctrl_e_toggle: None,
121                is_expert: $expert,
122                is_ultra: $ultra,
123                is_enabled: None,
124            }
125        }
126    };
127}
128
129make_field_fn!(field, false, false, @none);
130make_field_fn!(expert_field, true, false, @none);
131make_field_fn!(ultra_field, true, true, @none);
132make_field_fn!(field_with_toggle, false, false, toggle);
133make_field_fn!(expert_field_with_toggle, true, false, toggle);
134make_field_fn!(ultra_field_with_toggle, true, true, toggle);
135
136// ── Shared adjustment and toggle logic ───────────────────────────────────────
137
138fn gpu_layers_adjust(settings: &mut ModelSettings, delta: i32, _context_limit: u32) {
139    settings.gpu_layers_mode = match (delta, &settings.gpu_layers_mode) {
140        (1, GpuLayersMode::Auto) => GpuLayersMode::Specific(1),
141        (1, GpuLayersMode::Specific(n)) => GpuLayersMode::Specific(n + 1),
142        (1, GpuLayersMode::All) => GpuLayersMode::Auto,
143        (-1, GpuLayersMode::Auto) => GpuLayersMode::Auto,
144        (-1, GpuLayersMode::Specific(n)) if *n == 0 => GpuLayersMode::Auto,
145        (-1, GpuLayersMode::Specific(n)) if *n == 1 => GpuLayersMode::Specific(0),
146        (-1, GpuLayersMode::Specific(n)) => GpuLayersMode::Specific(n - 1),
147        (-1, GpuLayersMode::All) => GpuLayersMode::All,
148        _ => settings.gpu_layers_mode,
149    };
150}
151
152fn gpu_layers_apply(settings: &mut ModelSettings, buf: &str) {
153    if let Ok(v) = buf.parse::<i32>() {
154        settings.gpu_layers_mode = if v < 0 {
155            GpuLayersMode::All
156        } else {
157            GpuLayersMode::Specific(v as u32)
158        };
159    }
160}
161
162fn toggle_mlock(settings: &mut ModelSettings) {
163    settings.mlock = !settings.mlock;
164}
165fn toggle_flash_attn(settings: &mut ModelSettings) {
166    settings.flash_attn = !settings.flash_attn;
167}
168fn toggle_auto_chat_template(settings: &mut ModelSettings) {
169    settings.auto_chat_template = !settings.auto_chat_template;
170}
171
172fn toggle_fit(settings: &mut ModelSettings) {
173    settings.fit = !settings.fit;
174}
175fn toggle_kv_cache_offload(settings: &mut ModelSettings) {
176    settings.kv_cache_offload = !settings.kv_cache_offload;
177}
178fn toggle_uniform_cache(settings: &mut ModelSettings) {
179    settings.uniform_cache = !settings.uniform_cache;
180}
181fn toggle_swa_full(settings: &mut ModelSettings) {
182    settings.swa_full = !settings.swa_full;
183}
184fn toggle_mtp(settings: &mut ModelSettings) {
185    if settings.spec_type.is_empty() {
186        settings.spec_type = "draft-mtp".to_string();
187    } else {
188        settings.spec_type = String::new();
189    }
190}
191
192fn toggle_rope_yarn_enabled(settings: &mut ModelSettings) {
193    settings.rope_yarn_enabled = !settings.rope_yarn_enabled;
194}
195fn toggle_ignore_eos(settings: &mut ModelSettings) {
196    settings.ignore_eos = !settings.ignore_eos;
197}
198fn toggle_max_tokens(settings: &mut ModelSettings) {
199    settings.max_tokens = settings.max_tokens.map_or(Some(2048), |_| None);
200}
201fn toggle_max_concurrent_predictions(settings: &mut ModelSettings) {
202    settings.max_concurrent_predictions = settings
203        .max_concurrent_predictions
204        .map_or(Some(1), |_| None);
205}
206fn toggle_cache_type_k(settings: &mut ModelSettings) {
207    settings.cache_type_k = settings
208        .cache_type_k
209        .map_or(Some(CacheQuantType::F16), |_| None);
210}
211fn toggle_cache_type_v(settings: &mut ModelSettings) {
212    settings.cache_type_v = settings
213        .cache_type_v
214        .map_or(Some(CacheQuantType::F16), |_| None);
215}
216fn toggle_expert_count(settings: &mut ModelSettings) {
217    settings.expert_count = match settings.expert_count {
218        0 => 1,
219        -1 => 0,
220        _ => -1,
221    };
222}
223fn toggle_presence_penalty(settings: &mut ModelSettings) {
224    settings.presence_penalty = settings.presence_penalty.map_or(Some(0.0), |_| None);
225}
226fn toggle_frequency_penalty(settings: &mut ModelSettings) {
227    settings.frequency_penalty = settings.frequency_penalty.map_or(Some(0.0), |_| None);
228}
229
230// ── Diff macros for profile settings comparison ──────────────────────────────
231
232macro_rules! diff_int {
233    ($parts:expr, $s:expr, $c:expr, $field:ident, $label:literal) => {
234        if let Some(v) = $s.$field
235            && v != $c.$field
236        {
237            $parts.push(format!("{}={}", $label, v));
238        }
239    };
240}
241macro_rules! diff_float {
242    ($parts:expr, $s:expr, $c:expr, $field:ident, $label:literal) => {
243        if let Some(v) = $s.$field
244            && (v - $c.$field).abs() > 0.001
245        {
246            $parts.push(format!("{}={:.2}", $label, v));
247        }
248    };
249}
250macro_rules! diff_bool {
251    ($parts:expr, $s:expr, $c:expr, $field:ident, $label:literal) => {
252        if let Some(v) = $s.$field
253            && v != $c.$field
254        {
255            $parts.push(format!("{}={}", $label, v));
256        }
257    };
258}
259macro_rules! diff_string {
260    ($parts:expr, $s:expr, $c:expr, $field:ident, $label:literal) => {
261        if let Some(v) = &$s.$field
262            && v != &$c.$field
263        {
264            $parts.push(format!("{}={}", $label, v));
265        }
266    };
267}
268macro_rules! diff_enum {
269    ($parts:expr, $s:expr, $c:expr, $field:ident, $label:literal) => {
270        if let Some(ref v) = $s.$field
271            && *v != $c.$field
272        {
273            $parts.push(format!("{}={}", $label, v));
274        }
275    };
276}
277macro_rules! diff_option {
278    ($parts:expr, $s:expr, $c:expr, $field:ident, $label:literal) => {
279        if $s.$field != $c.$field {
280            if let Some(ref v) = $s.$field {
281                $parts.push(format!("{}={}", $label, v));
282            }
283        }
284    };
285}
286macro_rules! diff_option_float {
287    ($parts:expr, $s:expr, $c:expr, $field:ident, $label:literal) => {
288        if let Some(v) = $s.$field {
289            let current_val = $c.$field.unwrap_or(0.0);
290            if (v - current_val).abs() > 0.001 {
291                $parts.push(format!("{}={:.2}", $label, v));
292            }
293        }
294    };
295}
296
297// ── All Fields (Interleaved for context-aware expert mode) ────────────────────
298
299pub fn all_fields() -> Vec<SettingField> {
300    vec![
301        // ── Loading ───────────────────────────────────────────────────────────
302        field(
303            "system_prompt_preset_name",
304            "Prompt",
305            "Loading",
306            |s| s.system_prompt_preset_name.clone(),
307            |s, c| s.system_prompt_preset_name != c.system_prompt_preset_name,
308            |_, _, _| {},
309            |_, _| {},
310            "System prompt preset. Pre-configured prompts that shape how the model behaves (e.g., 'coder', 'assistant', 'creative'). Affects the model's personality and output style.",
311        ),
312        field(
313            "context_length",
314            "Context",
315            "Loading",
316            |s| {
317                format_context_k(s.context_length, s.rope_yarn_enabled, s.rope_scale)
318            },
319            |s, c| s.context_length != c.context_length,
320            |s, delta, ctx_limit| {
321                let mut val = (s.context_length as i32 + delta * 128).max(128) as u32;
322                if ctx_limit > 0 {
323                    val = val.min(ctx_limit);
324                }
325                s.context_length = val;
326            },
327            |s, buf| {
328                if let Ok(v) = buf.parse::<u32>() {
329                    s.context_length = v.max(128);
330                }
331            },
332            "Context window size in tokens. Determines how much of the conversation history is kept in memory. A larger context allows longer conversations but uses more RAM. Typical: 32k-256k depending on model and RAM.",
333        ),
334        expert_field_with_toggle(
335            "rope_yarn_enabled",
336            "Yarn RoPE",
337            "Loading",
338            |s| s.rope_yarn_enabled.to_string(),
339            |s, c| s.rope_yarn_enabled != c.rope_yarn_enabled,
340            |_, _, _| {},
341            |_, _| {},
342            toggle_rope_yarn_enabled,
343            "Enable YaRN (Yet another RoPE extensioN) for scaling context beyond training limits. YaRN uses a frequency rescaling technique to handle longer contexts. Toggle on/off with Enter.",
344        ),
345        {
346            let mut f = expert_field(
347                "yarn_params",
348                "Yarn Params",
349                "Loading",
350                |s| {
351                    format!(
352                        "scale={:.2} base={:.2} scale_f={:.2}",
353                        s.rope_scale, s.rope_freq_base, s.rope_freq_scale
354                    )
355                },
356                |s, c| {
357                    s.rope_scale != c.rope_scale
358                        || s.rope_freq_base != c.rope_freq_base
359                        || s.rope_freq_scale != c.rope_freq_scale
360                },
361                |_, _, _| {},
362                |_, _| {},
363                "YaRN configuration: rope_scale (context multiplier), rope_freq_base (frequency base), rope_freq_scale (frequency scaling). Press Enter to open the YaRN parameter editor.",
364            );
365            f.is_enabled = Some(|s| s.rope_yarn_enabled);
366            f
367        },
368        ultra_field(
369            "threads_batch",
370            "Threads Batch",
371            "Loading",
372            |s| s.threads_batch.to_string(),
373            |s, c| s.threads_batch != c.threads_batch,
374            |s, delta, _| {
375                s.threads_batch = (s.threads_batch as i32 + delta).max(1) as u32;
376            },
377            |s, buf| {
378                if let Ok(v) = buf.parse::<u32>() {
379                    s.threads_batch = v.max(1);
380                }
381            },
382            "CPU threads for batch processing (1 to 32). Separate from Threads (inference threads). Keep equal for most workloads, or reduce batch threads to lower CPU usage during batch operations.",
383        ),
384        ultra_field(
385            "ubatch_size",
386            "UBatch Size",
387            "Loading",
388            |s| s.ubatch_size.to_string(),
389            |s, c| s.ubatch_size != c.ubatch_size,
390            |s, delta, _| {
391                s.ubatch_size = (s.ubatch_size as i32 + delta * 64).max(1) as u32;
392            },
393            |s, buf| {
394                if let Ok(v) = buf.parse::<u32>() {
395                    s.ubatch_size = v.max(1);
396                }
397            },
398            "Unlimited batch size for prompt processing. Larger values improve prompt evaluation throughput but use more RAM. Typical: 512-2048. Set to 0 to match context_length.",
399        ),
400        ultra_field(
401            "keep",
402            "Keep",
403            "Loading",
404            |s| s.keep.to_string(),
405            |s, c| s.keep != c.keep,
406            |s, delta, _| {
407                s.keep = (s.keep + delta).max(0);
408            },
409            |s, buf| {
410                if let Ok(v) = buf.parse::<i32>() {
411                    s.keep = v;
412                }
413            },
414            "Number of layers to keep in memory when swapping (negative = all). Useful for fast reloading of the same model. Typical: -1 (all) or 0 (none).",
415        ),
416        field_with_toggle(
417            "mlock",
418            "Keep in memory (mlock)",
419            "Loading",
420            |s| s.mlock.to_string(),
421            |s, c| s.mlock != c.mlock,
422            |_, _, _| {},
423            |_, _| {},
424            toggle_mlock,
425            "Lock model weights in RAM (mlock). Prevents the OS from swapping model weights to disk. Slows model load time but ensures faster inference once loaded. Useful for repeated use.",
426        ),
427        expert_field_with_toggle(
428            "chat_template",
429            "Chat Template",
430            "Loading",
431            |s| {
432                if s.auto_chat_template {
433                    "Auto (detect)".to_string()
434                } else if let Some(ref t) = s.chat_template {
435                    if t.ends_with(".jinja") {
436                        let filename = std::path::Path::new(t)
437                            .file_name()
438                            .and_then(|n| n.to_str())
439                            .unwrap_or(t);
440                        filename.to_string()
441                    } else {
442                        t.clone()
443                    }
444                } else {
445                    "None".to_string()
446                }
447            },
448            |s, c| {
449                s.auto_chat_template != c.auto_chat_template || s.chat_template != c.chat_template
450            },
451            |_, _, _| {},
452            |_, _| {},
453            toggle_auto_chat_template,
454            "Select chat template: Auto (detect) uses GGUF architecture metadata, specific template names use llama.cpp built-in templates, Select a Template file picks a .jinja file from any directory, None disables template. Press Enter to open picker.",
455        ),
456        expert_field(
457            "numa",
458            "NUMA",
459            "Loading",
460            |s| s.numa.to_string(),
461            |s, c| s.numa != c.numa,
462            |s, delta, _| {
463                let mut val = s.numa;
464                val = match (delta, val) {
465                    (1, NumMode::None) => NumMode::Distribute,
466                    (1, NumMode::Distribute) => NumMode::Isolate,
467                    (1, NumMode::Isolate) => NumMode::Numactl,
468                    (1, NumMode::Numactl) => NumMode::None,
469                    (-1, NumMode::None) => NumMode::Numactl,
470                    (-1, NumMode::Distribute) => NumMode::None,
471                    (-1, NumMode::Isolate) => NumMode::Distribute,
472                    (-1, NumMode::Numactl) => NumMode::Isolate,
473                    _ => val,
474                };
475                s.numa = val;
476            },
477            |_, _| {},
478            "NUMA (Non-Uniform Memory Access) strategy: None, Distribute, Isolate, or Numactl. Affects CPU thread affinity on multi-socket systems. None = default.",
479        ),
480        // ── GPU Offload ───────────────────────────────────────────────────────
481        field(
482            "gpu_layers_mode",
483            "GPU Layers",
484            "GPU Offload",
485            |s| match s.gpu_layers_mode {
486                GpuLayersMode::Auto => "Auto".to_string(),
487                GpuLayersMode::Specific(n) => n.to_string(),
488                GpuLayersMode::All => "All".to_string(),
489            },
490            |s, c| s.gpu_layers_mode != c.gpu_layers_mode,
491            gpu_layers_adjust,
492            gpu_layers_apply,
493            "How many model layers to offload to GPU. Arrow keys cycle: Auto → 1 → 2 → ... → N → All → Auto. Auto lets llama.cpp decide based on VRAM. All loads every layer (999). Specific number sets exact offload count.",
494        ),
495        ultra_field(
496            "split_mode",
497            "Split Mode",
498            "GPU Offload",
499            |s| s.split_mode.to_string(),
500            |s, c| s.split_mode != c.split_mode,
501            |s, delta, _| {
502                let mut val = s.split_mode;
503                val = match (delta, val) {
504                    (1, SplitMode::None) => SplitMode::Layer,
505                    (1, SplitMode::Layer) => SplitMode::Row,
506                    (1, SplitMode::Row) => SplitMode::Tensor,
507                    (1, SplitMode::Tensor) => SplitMode::None,
508                    (-1, SplitMode::None) => SplitMode::Tensor,
509                    (-1, SplitMode::Layer) => SplitMode::None,
510                    (-1, SplitMode::Row) => SplitMode::Layer,
511                    (-1, SplitMode::Tensor) => SplitMode::Row,
512                    _ => val,
513                };
514                s.split_mode = val;
515            },
516            |_, _| {},
517            "GPU split strategy: None, Layer (default), Row, or Tensor. Controls how model layers are distributed across multiple GPUs. Layer splits by layer count, Row/Tensor split by matrix dimensions for multi-GPU setups.",
518        ),
519        ultra_field(
520            "tensor_split",
521            "Tensor Split",
522            "GPU Offload",
523            |s| s.tensor_split.clone(),
524            |s, c| s.tensor_split != c.tensor_split,
525            |_, _, _| {},
526            |_, _| {},
527            "Fraction of model weights to load on each GPU (colon-separated for multi-GPU, e.g., '0.5:0.5'). For single GPU, leave empty. Press Enter to edit.",
528        ),
529        expert_field(
530            "main_gpu",
531            "Main GPU",
532            "GPU Offload",
533            |s| s.main_gpu.to_string(),
534            |s, c| s.main_gpu != c.main_gpu,
535            |s, delta, _| {
536                s.main_gpu = (s.main_gpu + delta).max(0);
537            },
538            |s, buf| {
539                if let Ok(v) = buf.parse::<i32>() {
540                    s.main_gpu = v;
541                }
542            },
543            "Index of the main GPU (0-based). Handles initial model loading and some computations. Typical: 0 for single GPU, 0 for primary in multi-GPU setups.",
544        ),
545        field_with_toggle(
546            "fit",
547            "Fit",
548            "GPU Offload",
549            |s| s.fit.to_string(),
550            |s, c| s.fit != c.fit,
551            |_, _, _| {},
552            |_, _| {},
553            toggle_fit,
554            "Automatically adjust arguments to fit device memory. Toggle on/off with Enter.",
555        ),
556        field_with_toggle(
557            "flash_attn",
558            "Flash Attention",
559            "GPU Offload",
560            |s| s.flash_attn.to_string(),
561            |s, c| s.flash_attn != c.flash_attn,
562            |_, _, _| {},
563            |_, _| {},
564            toggle_flash_attn,
565            "Enable Flash Attention (flash-attn) for faster inference. Requires compatible GPU (Ampere+ / Ada). Significantly speeds up long-context inference. Only works with certain GGUF formats.",
566        ),
567        field_with_toggle(
568            "kv_cache_offload",
569            "KV Cache Offload",
570            "GPU Offload",
571            |s| s.kv_cache_offload.to_string(),
572            |s, c| s.kv_cache_offload != c.kv_cache_offload,
573            |_, _, _| {},
574            |_, _| {},
575            toggle_kv_cache_offload,
576            "Offload KV cache to RAM when GPU memory is full. Allows larger batch sizes and contexts at the cost of some speed. Useful when VRAM is limited but you still want longer conversations.",
577        ),
578        // ── Cache type fields ──────────────────────────────────────────────────
579        {
580            let mut f = expert_field(
581                "cache_type_k",
582                "Cache Type K",
583                "GPU Offload",
584                |s| {
585                    s.cache_type_k
586                        .map(|v| v.to_string())
587                        .unwrap_or_else(|| "Disabled".to_string())
588                },
589                |s, c| s.cache_type_k != c.cache_type_k,
590                |s, delta, _| {
591                    let mut val = s.cache_type_k.unwrap_or(CacheQuantType::F16);
592                    val = if delta > 0 { val.next() } else { val.prev() };
593                    s.cache_type_k = Some(val);
594                },
595                |s, buf| {
596                    if let Ok(n) = buf.parse::<u8>() {
597                        s.cache_type_k = Some(CacheQuantType::from_u8(n));
598                    }
599                },
600                "Quantization precision for KV cache keys. Lower precision (e.g., Q4, Q8) saves VRAM but may slightly reduce quality. Default is usually FP16. Use lower values if running out of VRAM.",
601            );
602            f.ctrl_e_toggle = Some(toggle_cache_type_k);
603            f
604        },
605        {
606            let mut f = expert_field(
607                "cache_type_v",
608                "Cache Type V",
609                "GPU Offload",
610                |s| {
611                    s.cache_type_v
612                        .map(|v| v.to_string())
613                        .unwrap_or_else(|| "Disabled".to_string())
614                },
615                |s, c| s.cache_type_v != c.cache_type_v,
616                |s, delta, _| {
617                    let mut val = s.cache_type_v.unwrap_or(CacheQuantType::F16);
618                    val = if delta > 0 { val.next() } else { val.prev() };
619                    s.cache_type_v = Some(val);
620                },
621                |s, buf| {
622                    if let Ok(n) = buf.parse::<u8>() {
623                        s.cache_type_v = Some(CacheQuantType::from_u8(n));
624                    }
625                },
626                "Quantization precision for KV cache values. Lower precision (e.g., Q4, Q8) saves VRAM but may slightly reduce quality. Default is usually FP16. Use lower values if running out of VRAM.",
627            );
628            f.ctrl_e_toggle = Some(toggle_cache_type_v);
629            f
630        },
631        expert_field_with_toggle(
632            "expert_count",
633            "Active Experts",
634            "GPU Offload",
635            |s| {
636                if s.expert_count > 0 {
637                    s.expert_count.to_string()
638                } else if s.expert_count == -1 {
639                    "Auto".to_string()
640                } else {
641                    "Disabled".to_string()
642                }
643            },
644            |s, c| s.expert_count != c.expert_count,
645            |s, delta, _| {
646                s.expert_count = (s.expert_count + delta).clamp(-1, 99);
647            },
648            |s, buf| {
649                if let Ok(v) = buf.parse::<i32>() {
650                    s.expert_count = v.clamp(-1, 99);
651                }
652            },
653            toggle_expert_count,
654            "Number of MoE (Mixture of Experts) experts to activate per token. -1 = auto (all active). Reducing this speeds up inference for MoE models like Mixtral but may reduce quality. Typical: 2-8 for Mixtral.",
655        ),
656        // ── Evaluation ────────────────────────────────────────────────────────
657        field(
658            "batch_size",
659            "Eval Batch",
660            "Evaluation",
661            |s| s.batch_size.to_string(),
662            |s, c| s.batch_size != c.batch_size,
663            |s, delta, _| {
664                s.batch_size = (s.batch_size as i32 + delta * 64).max(1) as u32;
665            },
666            |s, buf| {
667                if let Ok(v) = buf.parse::<u32>() {
668                    s.batch_size = v.max(1);
669                }
670            },
671            "Batch size for evaluation (inference). Larger batches use more VRAM but can improve throughput via parallelism. Small values (1-8) for low VRAM, larger (16-128) for high VRAM setups.",
672        ),
673        field_with_toggle(
674            "uniform_cache",
675            "Unified KV",
676            "Evaluation",
677            |s| s.uniform_cache.to_string(),
678            |s, c| s.uniform_cache != c.uniform_cache,
679            |_, _, _| {},
680            |_, _| {},
681            toggle_uniform_cache,
682            "Share KV cache across sequences. Reduces VRAM usage when running multiple requests by reusing allocated cache. May slightly reduce performance but enables more concurrent users.",
683        ),
684        expert_field(
685            "cache_reuse",
686            "Cache Reuse",
687            "Evaluation",
688            |s| s.cache_reuse.to_string(),
689            |s, c| s.cache_reuse != c.cache_reuse,
690            |s, delta, _| {
691                s.cache_reuse = (s.cache_reuse as i32 + delta * 16).max(0) as u32;
692            },
693            |s, buf| {
694                if let Ok(v) = buf.parse::<u32>() {
695                    s.cache_reuse = v;
696                }
697            },
698            "Minimum chunk size (in tokens) before KV cache is reused across requests. Higher values (e.g., 128, 256) only cache large shared prefixes, reducing disk write churn. Lower values (0-32) cache more aggressively. Adjust with Left/Right arrows (step 16).",
699        ),
700        expert_field_with_toggle(
701            "swa_full",
702            "SWA Full Cache",
703            "Evaluation",
704            |s| s.swa_full.to_string(),
705            |s, c| s.swa_full != c.swa_full,
706            |_, _, _| {},
707            |_, _| {},
708            toggle_swa_full,
709            "Enable full-size sliding window attention cache. Stores complete KV entries for SWA layers instead of compressed representation. Uses more VRAM but preserves quality on very long contexts. Toggle with Enter.",
710        ),
711        expert_field_with_toggle(
712            "max_concurrent_predictions",
713            "Max Concurrent Pred",
714            "Evaluation",
715            |s| {
716                s.max_concurrent_predictions
717                    .map(|v| v.to_string())
718                    .unwrap_or_else(|| "Off".to_string())
719            },
720            |s, c| s.max_concurrent_predictions != c.max_concurrent_predictions,
721            |s, delta, _| match s.max_concurrent_predictions {
722                Some(n) => {
723                    s.max_concurrent_predictions = Some(((n as i32) + delta).clamp(1, 10) as u32)
724                }
725                None => s.max_concurrent_predictions = Some(1),
726            },
727            |s, buf| {
728                if let Ok(v) = buf.parse::<u32>() {
729                    s.max_concurrent_predictions = Some(v.clamp(1, 10));
730                }
731            },
732            toggle_max_concurrent_predictions,
733            "Maximum number of models that can run simultaneously. Press Enter to open a picker that shows how context length divides per model. Each model needs its own VRAM/CPU resources.",
734        ),
735        // ── Sampling ──────────────────────────────────────────────────────────
736        field(
737            "seed",
738            "Seed",
739            "Sampling",
740            |s| s.seed.to_string(),
741            |s, c| s.seed != c.seed,
742            |s, delta, _| {
743                s.seed = (s.seed + delta).max(-1);
744            },
745            |s, buf| {
746                if let Ok(v) = buf.parse::<i32>() {
747                    s.seed = v;
748                }
749            },
750            "Random seed for reproducible outputs. -1 = random (default). Set to a fixed value for deterministic, repeatable responses — useful for debugging or testing prompts.",
751        ),
752        field(
753            "temperature",
754            "Temp",
755            "Sampling",
756            |s| format!("{:.2}", s.temperature),
757            |s, c| (s.temperature - c.temperature).abs() > 0.001,
758            |s, delta, _| {
759                s.temperature =
760                    ((s.temperature * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 2.0);
761            },
762            |s, buf| {
763                if let Ok(v) = buf.parse::<f32>() {
764                    s.temperature = v.clamp(0.0, 2.0);
765                }
766            },
767            "Sampling temperature. Controls creativity: 0 = deterministic (most predictable), 0.7 = balanced, 1.0+ = creative. Lower values produce more focused, factual outputs. Typical: 0.7-0.9 for general use.",
768        ),
769        field(
770            "top_k",
771            "Top-k",
772            "Sampling",
773            |s| s.top_k.to_string(),
774            |s, c| s.top_k != c.top_k,
775            |s, delta, _| {
776                s.top_k = (s.top_k + delta).max(1);
777            },
778            |s, buf| {
779                if let Ok(v) = buf.parse::<i32>() {
780                    s.top_k = v.max(0);
781                }
782            },
783            "Only consider the top k most likely tokens at each step. Smaller top-k (e.g., 10-40) makes output more deterministic. Larger values allow more variety. Typical: 40-50. Set to 0 to disable.",
784        ),
785        field(
786            "top_p",
787            "Top-p",
788            "Sampling",
789            |s| format!("{:.2}", s.top_p),
790            |s, c| (s.top_p - c.top_p).abs() > 0.001,
791            |s, delta, _| {
792                s.top_p = ((s.top_p * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 1.0);
793            },
794            |s, buf| {
795                if let Ok(v) = buf.parse::<f32>() {
796                    s.top_p = v.clamp(0.0, 1.0);
797                }
798            },
799            "Nucleus sampling: only consider tokens whose cumulative probability reaches p. Smaller top-p (e.g., 0.9) is more conservative, larger (e.g., 0.95-0.99) allows more variety. Often preferred over top-k. Typical: 0.9-0.95.",
800        ),
801        field(
802            "min_p",
803            "Min P",
804            "Sampling",
805            |s| format!("{:.2}", s.min_p),
806            |s, c| (s.min_p - c.min_p).abs() > 0.001,
807            |s, delta, _| {
808                s.min_p = ((s.min_p * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 1.0);
809            },
810            |s, buf| {
811                if let Ok(v) = buf.parse::<f32>() {
812                    s.min_p = v.clamp(0.0, 1.0);
813                }
814            },
815            "Minimum probability threshold relative to the most likely token. Tokens below min_p * max_prob are excluded. A filter that's more principled than top-k/top-p for controlling diversity. Typical: 0.01-0.1.",
816        ),
817        ultra_field(
818            "typical_p",
819            "Typical P",
820            "Sampling",
821            |s| format!("{:.2}", s.typical_p),
822            |s, c| (s.typical_p - c.typical_p).abs() > 0.001,
823            |s, delta, _| {
824                s.typical_p = ((s.typical_p * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 1.0);
825            },
826            |s, buf| {
827                if let Ok(v) = buf.parse::<f32>() {
828                    s.typical_p = v.clamp(0.0, 1.0);
829                }
830            },
831            "Locally typical sampling (typ_p). Controls diversity by keeping tokens with typical probability mass. Values near 1.0 = no effect, 0.1-0.5 = moderate diversity. Typical: 1.0 (off).",
832        ),
833        ultra_field(
834            "mirostat",
835            "Mirostat",
836            "Sampling",
837            |s| s.mirostat.to_string(),
838            |s, c| s.mirostat != c.mirostat,
839            |s, delta, _| {
840                let mut val = s.mirostat;
841                val = match (delta, val) {
842                    (1, Mirostat::Off) => Mirostat::V1,
843                    (1, Mirostat::V1) => Mirostat::Mirostat2,
844                    (1, Mirostat::Mirostat2) => Mirostat::Off,
845                    (-1, Mirostat::Off) => Mirostat::Mirostat2,
846                    (-1, Mirostat::V1) => Mirostat::Off,
847                    (-1, Mirostat::Mirostat2) => Mirostat::V1,
848                    _ => val,
849                };
850                s.mirostat = val;
851            },
852            |_, _| {},
853            "Mirostat sampling mode: Off (default), Mirostat, or Mirostat2. Adaptive temperature control that maintains target perplexity. Mirostat2 is more aggressive. Useful for consistent output quality.",
854        ),
855        ultra_field(
856            "mirostat_lr",
857            "Mirostat LR",
858            "Sampling",
859            |s| format!("{:.2}", s.mirostat_lr),
860            |s, c| (s.mirostat_lr - c.mirostat_lr).abs() > 0.001,
861            |s, delta, _| {
862                s.mirostat_lr =
863                    ((s.mirostat_lr * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 1.0);
864            },
865            |s, buf| {
866                if let Ok(v) = buf.parse::<f32>() {
867                    s.mirostat_lr = v.clamp(0.0, 1.0);
868                }
869            },
870            "Mirostat learning rate (eta). Controls how quickly the temperature adapts. Smaller = smoother adjustments. Typical: 0.1.",
871        ),
872        ultra_field(
873            "mirostat_ent",
874            "Mirostat Ent",
875            "Sampling",
876            |s| format!("{:.2}", s.mirostat_ent),
877            |s, c| (s.mirostat_ent - c.mirostat_ent).abs() > 0.001,
878            |s, delta, _| {
879                s.mirostat_ent =
880                    ((s.mirostat_ent * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 10.0);
881            },
882            |s, buf| {
883                if let Ok(v) = buf.parse::<f32>() {
884                    s.mirostat_ent = v.clamp(0.0, 10.0);
885                }
886            },
887            "Mirostat target entropy. Controls the diversity of output. Higher = more diverse. Typical: 5.0.",
888        ),
889        ultra_field_with_toggle(
890            "ignore_eos",
891            "Ignore EOS",
892            "Sampling",
893            |s| s.ignore_eos.to_string(),
894            |s, c| s.ignore_eos != c.ignore_eos,
895            |_, _, _| {},
896            |_, _| {},
897            toggle_ignore_eos,
898            "Ignore end-of-sequence tokens during generation. Toggle on/off with Enter. Useful when you want to force the model to continue generating.",
899        ),
900        ultra_field(
901            "samplers",
902            "Samplers",
903            "Sampling",
904            |s| s.samplers.0.clone(),
905            |s, c| s.samplers.0 != c.samplers.0,
906            |_, _, _| {},
907            |_, _| {},
908            "Semicolon-separated sampler order string (e.g., 'mirostat;temperature;top_k;top_p'). Controls which samplers are applied and in what order. Press Enter to edit.",
909        ),
910        field_with_toggle(
911            "max_tokens",
912            "Max Tokens",
913            "Sampling",
914            |s| {
915                s.max_tokens
916                    .map(|v| v.to_string())
917                    .unwrap_or_else(|| "Disabled".to_string())
918            },
919            |s, c| s.max_tokens != c.max_tokens,
920            |s, delta, _| {
921                let current = s.max_tokens.unwrap_or(0);
922                s.max_tokens = Some((current as i32 + delta * 16).max(16) as u32);
923            },
924            |s, buf| {
925                if let Ok(v) = buf.parse::<i32>() {
926                    s.max_tokens = if v == 0 { None } else { Some(v as u32) };
927                }
928            },
929            toggle_max_tokens,
930            "Maximum number of tokens to generate in the response. Prevents runaway responses. Set to 0 or Disabled for no limit. Typical: 4096-8192 for chat, higher for code generation.",
931        ),
932        // ── Repetition ────────────────────────────────────────────────────────
933        field(
934            "repeat_penalty",
935            "Repeat Penalty",
936            "Repetition",
937            |s| format!("{:.2}", s.repeat_penalty),
938            |s, c| (s.repeat_penalty - c.repeat_penalty).abs() > 0.001,
939            |s, delta, _| {
940                s.repeat_penalty =
941                    ((s.repeat_penalty * 100.0 + delta as f32 * 5.0) / 100.0).clamp(1.0, 2.0);
942            },
943            |s, buf| {
944                if let Ok(v) = buf.parse::<f32>() {
945                    s.repeat_penalty = v.clamp(0.0, 2.0);
946                }
947            },
948            "Controls repetition penalty (1.0 = no penalty, 1.1 = mild, 1.2 = strong). Higher values discourage the model from repeating phrases. Typical: 1.05-1.15 for most use cases.",
949        ),
950        field(
951            "repeat_last_n",
952            "Repeat Last N",
953            "Repetition",
954            |s| s.repeat_last_n.to_string(),
955            |s, c| s.repeat_last_n != c.repeat_last_n,
956            |s, delta, _| {
957                s.repeat_last_n = (s.repeat_last_n + delta).max(0);
958            },
959            |s, buf| {
960                if let Ok(v) = buf.parse::<i32>() {
961                    s.repeat_last_n = v.max(0);
962                }
963            },
964            "How many recent tokens to check for repetition (0 = all). Smaller values (32-64) focus on local repetition, larger values (128-256) catch longer patterns. Typical: 64.",
965        ),
966        expert_field_with_toggle(
967            "presence_penalty",
968            "Presence Penalty",
969            "Repetition",
970            |s| {
971                s.presence_penalty
972                    .map(|v| {
973                        if (v - 0.0).abs() < 0.001 {
974                            "Off".to_string()
975                        } else {
976                            format!("{:.2}", v)
977                        }
978                    })
979                    .unwrap_or_else(|| "Off".to_string())
980            },
981            |s, c| match (s.presence_penalty, c.presence_penalty) {
982                (Some(v1), Some(v2)) => (v1 - v2).abs() > 0.001,
983                (None, None) => false,
984                _ => true,
985            },
986            |s, delta, _| {
987                let current = s.presence_penalty.unwrap_or(0.0);
988                s.presence_penalty =
989                    Some(((current * 100.0 + delta as f32 * 5.0) / 100.0).clamp(-2.0, 2.0));
990            },
991            |s, buf| {
992                if let Ok(v) = buf.parse::<f32>() {
993                    s.presence_penalty = Some(v.clamp(-2.0, 2.0));
994                }
995            },
996            toggle_presence_penalty,
997            "Encourages the model to talk about new topics (+) or stay on topic (-). Positive values reduce topic repetition, negative values encourage deeper exploration. Typical: 0.0 (off).",
998        ),
999        expert_field_with_toggle(
1000            "frequency_penalty",
1001            "Freq Penalty",
1002            "Repetition",
1003            |s| {
1004                s.frequency_penalty
1005                    .map(|v| {
1006                        if (v - 0.0).abs() < 0.001 {
1007                            "Off".to_string()
1008                        } else {
1009                            format!("{:.2}", v)
1010                        }
1011                    })
1012                    .unwrap_or_else(|| "Off".to_string())
1013            },
1014            |s, c| match (s.frequency_penalty, c.frequency_penalty) {
1015                (Some(v1), Some(v2)) => (v1 - v2).abs() > 0.001,
1016                (None, None) => false,
1017                _ => true,
1018            },
1019            |s, delta, _| {
1020                let current = s.frequency_penalty.unwrap_or(0.0);
1021                s.frequency_penalty =
1022                    Some(((current * 100.0 + delta as f32 * 5.0) / 100.0).clamp(-2.0, 2.0));
1023            },
1024            |s, buf| {
1025                if let Ok(v) = buf.parse::<f32>() {
1026                    s.frequency_penalty = Some(v.clamp(-2.0, 2.0));
1027                }
1028            },
1029            toggle_frequency_penalty,
1030            "Penalizes tokens based on how often they appear in the text (+) or rewards them (-). Positive values reduce word repetition, negative values encourage denser language. Typical: 0.0 (off).",
1031        ),
1032        // ── DRY ───────────────────────────────────────────────────────────────
1033        ultra_field(
1034            "dry_multiplier",
1035            "DRY Multiplier",
1036            "DRY",
1037            |s| format!("{:.2}", s.dry_multiplier),
1038            |s, c| (s.dry_multiplier - c.dry_multiplier).abs() > 0.001,
1039            |s, delta, _| {
1040                s.dry_multiplier =
1041                    ((s.dry_multiplier * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 10.0);
1042            },
1043            |s, buf| {
1044                if let Ok(v) = buf.parse::<f32>() {
1045                    s.dry_multiplier = v.clamp(0.0, 10.0);
1046                }
1047            },
1048            "DRY (Don't Repeat Yourself) multiplier. Scales the penalty for repetition. Higher values = stronger anti-repetition. Typical: 1.75.",
1049        ),
1050        ultra_field(
1051            "dry_base",
1052            "DRY Base",
1053            "DRY",
1054            |s| format!("{:.2}", s.dry_base),
1055            |s, c| (s.dry_base - c.dry_base).abs() > 0.001,
1056            |s, delta, _| {
1057                s.dry_base = ((s.dry_base * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 10.0);
1058            },
1059            |s, buf| {
1060                if let Ok(v) = buf.parse::<f32>() {
1061                    s.dry_base = v.clamp(0.0, 10.0);
1062                }
1063            },
1064            "DRY penalty base (log scale). Controls the strength of the repetition penalty. Typical: 1.0 (log2) or 0.0 (linear).",
1065        ),
1066        ultra_field(
1067            "dry_allowed_length",
1068            "DRY Allowed Length",
1069            "DRY",
1070            |s| s.dry_allowed_length.to_string(),
1071            |s, c| s.dry_allowed_length != c.dry_allowed_length,
1072            |s, delta, _| {
1073                s.dry_allowed_length = (s.dry_allowed_length + delta).max(0);
1074            },
1075            |s, buf| {
1076                if let Ok(v) = buf.parse::<i32>() {
1077                    s.dry_allowed_length = v;
1078                }
1079            },
1080            "Number of recent tokens to check for repetition (penalty starts after this). Higher values check longer context. Typical: 2.",
1081        ),
1082        ultra_field(
1083            "dry_penalty_last_n",
1084            "DRY Penalty Last N",
1085            "DRY",
1086            |s| s.dry_penalty_last_n.to_string(),
1087            |s, c| s.dry_penalty_last_n != c.dry_penalty_last_n,
1088            |s, delta, _| {
1089                s.dry_penalty_last_n = (s.dry_penalty_last_n + delta).max(0);
1090            },
1091            |s, buf| {
1092                if let Ok(v) = buf.parse::<i32>() {
1093                    s.dry_penalty_last_n = v;
1094                }
1095            },
1096            "How many tokens to consider for DRY penalty (0 = all). Larger values catch longer repetition patterns. Typical: -1 (all) or 128.",
1097        ),
1098        // ── Speculative Decoding ─────────────────────────────────────────────
1099        expert_field_with_toggle(
1100            "is_mtp",
1101            "MTP",
1102            "Speculative",
1103            |s| {
1104                if s.spec_type.is_empty() {
1105                    "Off".to_string()
1106                } else {
1107                    s.spec_type.clone()
1108                }
1109            },
1110            |s, c| s.spec_type != c.spec_type,
1111            |_, _, _| {},
1112            |_, _| {},
1113            toggle_mtp,
1114            "Speculative decoding method for faster inference. Options: Off, draft-mtp (MTP-based), draft-simple, draft-eagle3, ngram-simple, ngram-map-k, ngram-map-k4v, ngram-mod, ngram-cache. Draft-mtp requires a compatible model with MTP architecture.",
1115        ),
1116        expert_field(
1117            "spec_type",
1118            "Spec Type",
1119            "Speculative",
1120            |s| {
1121                if s.spec_type.is_empty() {
1122                    "Off".to_string()
1123                } else {
1124                    s.spec_type.clone()
1125                }
1126            },
1127            |s, c| s.spec_type != c.spec_type,
1128            |_, _, _| {},
1129            |_, _| {},
1130            "Speculative decoding method for faster inference. Options: Off, draft-mtp (MTP-based), draft-simple, draft-eagle3, ngram-simple, ngram-map-k, ngram-map-k4v, ngram-mod, ngram-cache. Draft-mtp requires a compatible model with MTP architecture.",
1131        ),
1132        expert_field(
1133            "draft_tokens",
1134            "Spec Draft N Max",
1135            "Speculative",
1136            |s| s.draft_tokens.to_string(),
1137            |s, c| s.draft_tokens != c.draft_tokens,
1138            |s, delta, _| {
1139                s.draft_tokens = (s.draft_tokens as i32 + delta).clamp(0, 16) as u32;
1140            },
1141            |s, buf| {
1142                if let Ok(v) = buf.parse::<u32>() {
1143                    s.draft_tokens = v.min(16);
1144                }
1145            },
1146            "Maximum number of draft tokens per step (0-16). More drafts = more potential speedup but also more wasted computation if drafts are rejected. Typical: 4-8 for draft-mtp.",
1147        ),
1148        // ── Tags ──────────────────────────────────────────────────────────────
1149        field(
1150            "tags",
1151            "Tags (Enter to edit)",
1152            "Tags",
1153            |s| {
1154                if s.tags.is_empty() {
1155                    "None".to_string()
1156                } else {
1157                    s.tags.join(", ")
1158                }
1159            },
1160            |s, c| s.tags != c.tags,
1161            |_, _, _| {},
1162            |_, _| {},
1163            "Comma-separated labels for the model (e.g., 'coding, chat, reasoning'). Used for filtering and organization. Press Enter to open a tag editor.",
1164        ),
1165        // ── Backend ───────────────────────────────────────────────────────────
1166        field(
1167            "backend_version",
1168            "LLama.cpp Version",
1169            "Backend",
1170            |s| s.get_active_backend_version_display().to_string(),
1171            |s, c| s.get_active_backend_version() != c.get_active_backend_version(),
1172            |_, _, _| {},
1173            |_, _| {},
1174            "Select the llama.cpp backend binary (CPU / Vulkan / ROCm / CUDA). Press Enter to open a version picker. Different backends support different GPU types and features.",
1175        ),
1176    ]
1177}
1178
1179pub fn filtered_fields(expert_mode: bool) -> Vec<SettingField> {
1180    all_fields()
1181        .into_iter()
1182        .filter(|f| {
1183            if !expert_mode {
1184                !f.is_expert
1185            } else {
1186                !f.is_ultra // In expert mode, hide ultra experts
1187            }
1188        })
1189        .collect()
1190}
1191
1192// ── Simple helper for the server settings panel (tabbed.rs) ──────────────────
1193
1194/// Render a single setting line for the server settings panel.
1195#[allow(clippy::too_many_arguments)]
1196pub fn add_setting(
1197    lines: &mut Vec<Line<'static>>,
1198    total_count: &mut usize,
1199    _settings: &ModelSettings,
1200    _cached: &ModelSettings,
1201    selected_line_idx: &mut usize,
1202    selected_content_line: &mut usize,
1203    idx: usize,
1204    name: &str,
1205    val: &str,
1206    selected: usize,
1207    _edit_buf: &str,
1208    _editing: bool,
1209    disabled: bool,
1210) {
1211    let current_line = lines.len();
1212    let name_style = if disabled {
1213        Style::default().fg(GRAY)
1214    } else {
1215        Style::default().fg(YELLOW)
1216    };
1217    let val_style = if disabled {
1218        Style::default()
1219            .fg(GRAY)
1220    } else {
1221        Style::default().fg(WHITE)
1222    };
1223    if idx == selected {
1224        *selected_line_idx = current_line;
1225        *selected_content_line = current_line;
1226        lines.push(Line::from(vec![
1227            Span::styled(
1228                "> ",
1229                Style::default()
1230                    .fg(YELLOW)
1231                    .add_modifier(if disabled {
1232                        Modifier::DIM
1233                    } else {
1234                        Modifier::BOLD
1235                    }),
1236            ),
1237            Span::styled(format!("{name}: "), name_style),
1238            Span::styled(
1239                 val.to_string(),
1240                 Style::default()
1241                     .fg(BLACK)
1242                     .bg(GREEN)
1243                     .add_modifier(Modifier::BOLD),
1244             ),
1245        ]));
1246    } else {
1247        lines.push(Line::from(vec![
1248            Span::styled("  ", name_style),
1249            Span::styled(format!("{name}: "), name_style),
1250            Span::styled(val.to_string(), val_style),
1251        ]));
1252    }
1253    *total_count += 1;
1254}
1255
1256/// Build a list of setting names that differ between a profile and the current settings.
1257pub fn profile_settings_parts(profile: &Profile, current: &ModelSettings) -> Vec<String> {
1258    let mut parts = Vec::new();
1259    let s = &profile.settings;
1260
1261    // ── Integers ──────────────────────────────────────────────────────────
1262    diff_int!(parts, s, current, context_length, "ctx");
1263    diff_int!(parts, s, current, threads, "threads");
1264    diff_int!(parts, s, current, threads_batch, "threads_batch");
1265    diff_int!(parts, s, current, batch_size, "batch");
1266    diff_int!(parts, s, current, ubatch_size, "ubatch");
1267    diff_int!(parts, s, current, parallel, "parallel");
1268    diff_option!(parts, s, current, max_concurrent_predictions, "concurrent");
1269    diff_int!(parts, s, current, cache_reuse, "cache_reuse");
1270    diff_option!(parts, s, current, max_tokens, "max_tokens");
1271    diff_int!(parts, s, current, draft_tokens, "draft_tokens");
1272
1273    diff_int!(parts, s, current, keep, "keep");
1274    diff_int!(parts, s, current, main_gpu, "main_gpu");
1275    diff_int!(parts, s, current, expert_count, "expert_count");
1276    diff_int!(parts, s, current, seed, "seed");
1277    diff_int!(parts, s, current, top_k, "top_k");
1278    diff_int!(parts, s, current, repeat_last_n, "repeat_last_n");
1279    diff_int!(parts, s, current, dry_allowed_length, "dry_allowed");
1280    diff_int!(parts, s, current, dry_penalty_last_n, "dry_penalty_last_n");
1281
1282    // ── Floats ────────────────────────────────────────────────────────────
1283    diff_float!(parts, s, current, temperature, "temp");
1284    diff_float!(parts, s, current, top_p, "top_p");
1285    diff_float!(parts, s, current, min_p, "min_p");
1286    diff_float!(parts, s, current, typical_p, "typical_p");
1287    diff_float!(parts, s, current, mirostat_lr, "mirostat_lr");
1288    diff_float!(parts, s, current, mirostat_ent, "mirostat_ent");
1289    diff_float!(parts, s, current, repeat_penalty, "rep_pen");
1290    diff_option_float!(parts, s, current, presence_penalty, "pres_pen");
1291    diff_option_float!(parts, s, current, frequency_penalty, "freq_pen");
1292    diff_float!(parts, s, current, dry_multiplier, "dry_mult");
1293    diff_float!(parts, s, current, dry_base, "dry_base");
1294    diff_float!(parts, s, current, rope_scale, "rope_scale");
1295    diff_float!(parts, s, current, rope_freq_base, "rope_freq_base");
1296    diff_float!(parts, s, current, rope_freq_scale, "rope_freq_scale");
1297
1298    // ── Bools ─────────────────────────────────────────────────────────────
1299    diff_bool!(parts, s, current, swa_full, "swa_full");
1300    diff_bool!(parts, s, current, mlock, "mlock");
1301    diff_bool!(parts, s, current, mmap, "mmap");
1302    diff_bool!(parts, s, current, uniform_cache, "uniform_cache");
1303    diff_bool!(parts, s, current, kv_cache_offload, "kv_cache_offload");
1304    diff_bool!(parts, s, current, fit, "fit");
1305    diff_bool!(parts, s, current, embedding, "embedding");
1306    diff_bool!(parts, s, current, flash_attn, "flash_attn");
1307    diff_bool!(parts, s, current, jinja, "jinja");
1308    diff_bool!(parts, s, current, auto_chat_template, "auto_chat_template");
1309    diff_bool!(parts, s, current, ignore_eos, "ignore_eos");
1310    diff_bool!(parts, s, current, rope_yarn_enabled, "yarn_enabled");
1311    diff_bool!(parts, s, current, cache_prompt, "cache_prompt");
1312    diff_bool!(parts, s, current, webui, "webui");
1313    // ── Strings ───────────────────────────────────────────────────────────
1314    diff_string!(parts, s, current, system_prompt_preset_name, "preset");
1315    diff_string!(parts, s, current, tensor_split, "tensor_split");
1316    diff_string!(parts, s, current, rpc, "rpc");
1317    if s.chat_template != current.chat_template {
1318        if let Some(ref v) = s.chat_template {
1319            let filename = std::path::Path::new(v)
1320                .file_name()
1321                .and_then(|n| n.to_str())
1322                .unwrap_or(v);
1323            parts.push(format!("chat_template={}", filename));
1324        }
1325    }
1326    diff_option!(
1327        parts,
1328        s,
1329        current,
1330        chat_template_kwargs,
1331        "chat_template_kwargs"
1332    );
1333    diff_option!(parts, s, current, llama_cpp_version_cpu, "llama_cpp_cpu");
1334    diff_option!(
1335        parts,
1336        s,
1337        current,
1338        llama_cpp_version_vulkan,
1339        "llama_cpp_vulkan"
1340    );
1341    diff_option!(parts, s, current, llama_cpp_version_rocm, "llama_cpp_rocm");
1342    diff_option!(
1343        parts,
1344        s,
1345        current,
1346        llama_cpp_version_rocm_lemonade,
1347        "llama_cpp_rocm_lemonade"
1348    );
1349    diff_option!(parts, s, current, llama_cpp_version_cuda, "llama_cpp_cuda");
1350    diff_string!(parts, s, current, spec_type, "spec_type");
1351
1352    // ── Enums ─────────────────────────────────────────────────────────────
1353    diff_enum!(parts, s, current, numa, "numa");
1354    diff_enum!(parts, s, current, split_mode, "split_mode");
1355    diff_enum!(parts, s, current, mirostat, "mirostat");
1356    diff_enum!(parts, s, current, samplers, "samplers");
1357    diff_enum!(parts, s, current, rope_scaling, "rope_scaling");
1358    diff_enum!(parts, s, current, cache_type, "cache_type");
1359    diff_option!(parts, s, current, cache_type_k, "cache_type_k");
1360    diff_option!(parts, s, current, cache_type_v, "cache_type_v");
1361
1362    // ── Special (custom display) ──────────────────────────────────────────
1363    if let Some(v) = s.gpu_layers_mode
1364        && v != current.gpu_layers_mode
1365    {
1366        let display = match v {
1367            crate::models::GpuLayersMode::Auto => "Auto".to_string(),
1368            crate::models::GpuLayersMode::Specific(n) => n.to_string(),
1369            crate::models::GpuLayersMode::All => "All".to_string(),
1370        };
1371        parts.push(format!("gpu_layers={}", display));
1372    }
1373
1374    parts
1375}