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| format_context_k(s.context_length, s.rope_yarn_enabled, s.rope_scale),
317            |s, c| s.context_length != c.context_length,
318            |s, delta, ctx_limit| {
319                let mut val = (s.context_length as i32 + delta * 128).max(128) as u32;
320                if ctx_limit > 0 {
321                    val = val.min(ctx_limit);
322                }
323                s.context_length = val;
324            },
325            |s, buf| {
326                if let Ok(v) = buf.parse::<u32>() {
327                    s.context_length = v.max(128);
328                }
329            },
330            "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.",
331        ),
332        expert_field_with_toggle(
333            "rope_yarn_enabled",
334            "Yarn RoPE",
335            "Loading",
336            |s| s.rope_yarn_enabled.to_string(),
337            |s, c| s.rope_yarn_enabled != c.rope_yarn_enabled,
338            |_, _, _| {},
339            |_, _| {},
340            toggle_rope_yarn_enabled,
341            "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.",
342        ),
343        {
344            let mut f = expert_field(
345                "yarn_params",
346                "Yarn Params",
347                "Loading",
348                |s| {
349                    format!(
350                        "scale={:.2} base={:.2} scale_f={:.2}",
351                        s.rope_scale, s.rope_freq_base, s.rope_freq_scale
352                    )
353                },
354                |s, c| {
355                    s.rope_scale != c.rope_scale
356                        || s.rope_freq_base != c.rope_freq_base
357                        || s.rope_freq_scale != c.rope_freq_scale
358                },
359                |_, _, _| {},
360                |_, _| {},
361                "YaRN configuration: rope_scale (context multiplier), rope_freq_base (frequency base), rope_freq_scale (frequency scaling). Press Enter to open the YaRN parameter editor.",
362            );
363            f.is_enabled = Some(|s| s.rope_yarn_enabled);
364            f
365        },
366        ultra_field(
367            "threads_batch",
368            "Threads Batch",
369            "Loading",
370            |s| s.threads_batch.to_string(),
371            |s, c| s.threads_batch != c.threads_batch,
372            |s, delta, _| {
373                s.threads_batch = (s.threads_batch as i32 + delta).max(1) as u32;
374            },
375            |s, buf| {
376                if let Ok(v) = buf.parse::<u32>() {
377                    s.threads_batch = v.max(1);
378                }
379            },
380            "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.",
381        ),
382        ultra_field(
383            "ubatch_size",
384            "UBatch Size",
385            "Loading",
386            |s| s.ubatch_size.to_string(),
387            |s, c| s.ubatch_size != c.ubatch_size,
388            |s, delta, _| {
389                s.ubatch_size = (s.ubatch_size as i32 + delta * 64).max(1) as u32;
390            },
391            |s, buf| {
392                if let Ok(v) = buf.parse::<u32>() {
393                    s.ubatch_size = v.max(1);
394                }
395            },
396            "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.",
397        ),
398        ultra_field(
399            "keep",
400            "Keep",
401            "Loading",
402            |s| s.keep.to_string(),
403            |s, c| s.keep != c.keep,
404            |s, delta, _| {
405                s.keep = (s.keep + delta).max(0);
406            },
407            |s, buf| {
408                if let Ok(v) = buf.parse::<i32>() {
409                    s.keep = v;
410                }
411            },
412            "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).",
413        ),
414        field_with_toggle(
415            "mlock",
416            "Keep in memory (mlock)",
417            "Loading",
418            |s| s.mlock.to_string(),
419            |s, c| s.mlock != c.mlock,
420            |_, _, _| {},
421            |_, _| {},
422            toggle_mlock,
423            "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.",
424        ),
425        expert_field_with_toggle(
426            "chat_template",
427            "Chat Template",
428            "Loading",
429            |s| {
430                if s.auto_chat_template {
431                    "Auto (detect)".to_string()
432                } else if let Some(ref t) = s.chat_template {
433                    if t.ends_with(".jinja") {
434                        let filename = std::path::Path::new(t)
435                            .file_name()
436                            .and_then(|n| n.to_str())
437                            .unwrap_or(t);
438                        filename.to_string()
439                    } else {
440                        t.clone()
441                    }
442                } else {
443                    "None".to_string()
444                }
445            },
446            |s, c| {
447                s.auto_chat_template != c.auto_chat_template || s.chat_template != c.chat_template
448            },
449            |_, _, _| {},
450            |_, _| {},
451            toggle_auto_chat_template,
452            "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.",
453        ),
454        expert_field(
455            "numa",
456            "NUMA",
457            "Loading",
458            |s| s.numa.to_string(),
459            |s, c| s.numa != c.numa,
460            |s, delta, _| {
461                let mut val = s.numa;
462                val = match (delta, val) {
463                    (1, NumMode::None) => NumMode::Distribute,
464                    (1, NumMode::Distribute) => NumMode::Isolate,
465                    (1, NumMode::Isolate) => NumMode::Numactl,
466                    (1, NumMode::Numactl) => NumMode::None,
467                    (-1, NumMode::None) => NumMode::Numactl,
468                    (-1, NumMode::Distribute) => NumMode::None,
469                    (-1, NumMode::Isolate) => NumMode::Distribute,
470                    (-1, NumMode::Numactl) => NumMode::Isolate,
471                    _ => val,
472                };
473                s.numa = val;
474            },
475            |_, _| {},
476            "NUMA (Non-Uniform Memory Access) strategy: None, Distribute, Isolate, or Numactl. Affects CPU thread affinity on multi-socket systems. None = default.",
477        ),
478        // ── GPU Offload ───────────────────────────────────────────────────────
479        field(
480            "gpu_layers_mode",
481            "GPU Layers",
482            "GPU Offload",
483            |s| match s.gpu_layers_mode {
484                GpuLayersMode::Auto => "Auto".to_string(),
485                GpuLayersMode::Specific(n) => n.to_string(),
486                GpuLayersMode::All => "All".to_string(),
487            },
488            |s, c| s.gpu_layers_mode != c.gpu_layers_mode,
489            gpu_layers_adjust,
490            gpu_layers_apply,
491            "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.",
492        ),
493        ultra_field(
494            "split_mode",
495            "Split Mode",
496            "GPU Offload",
497            |s| s.split_mode.to_string(),
498            |s, c| s.split_mode != c.split_mode,
499            |s, delta, _| {
500                let mut val = s.split_mode;
501                val = match (delta, val) {
502                    (1, SplitMode::None) => SplitMode::Layer,
503                    (1, SplitMode::Layer) => SplitMode::Row,
504                    (1, SplitMode::Row) => SplitMode::Tensor,
505                    (1, SplitMode::Tensor) => SplitMode::None,
506                    (-1, SplitMode::None) => SplitMode::Tensor,
507                    (-1, SplitMode::Layer) => SplitMode::None,
508                    (-1, SplitMode::Row) => SplitMode::Layer,
509                    (-1, SplitMode::Tensor) => SplitMode::Row,
510                    _ => val,
511                };
512                s.split_mode = val;
513            },
514            |_, _| {},
515            "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.",
516        ),
517        ultra_field(
518            "tensor_split",
519            "Tensor Split",
520            "GPU Offload",
521            |s| s.tensor_split.clone(),
522            |s, c| s.tensor_split != c.tensor_split,
523            |_, _, _| {},
524            |_, _| {},
525            "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.",
526        ),
527        expert_field(
528            "main_gpu",
529            "Main GPU",
530            "GPU Offload",
531            |s| s.main_gpu.to_string(),
532            |s, c| s.main_gpu != c.main_gpu,
533            |s, delta, _| {
534                s.main_gpu = (s.main_gpu + delta).max(0);
535            },
536            |s, buf| {
537                if let Ok(v) = buf.parse::<i32>() {
538                    s.main_gpu = v;
539                }
540            },
541            "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.",
542        ),
543        field_with_toggle(
544            "fit",
545            "Fit",
546            "GPU Offload",
547            |s| s.fit.to_string(),
548            |s, c| s.fit != c.fit,
549            |_, _, _| {},
550            |_, _| {},
551            toggle_fit,
552            "Automatically adjust arguments to fit device memory. Toggle on/off with Enter.",
553        ),
554        field_with_toggle(
555            "flash_attn",
556            "Flash Attention",
557            "GPU Offload",
558            |s| s.flash_attn.to_string(),
559            |s, c| s.flash_attn != c.flash_attn,
560            |_, _, _| {},
561            |_, _| {},
562            toggle_flash_attn,
563            "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.",
564        ),
565        field_with_toggle(
566            "kv_cache_offload",
567            "KV Cache Offload",
568            "GPU Offload",
569            |s| s.kv_cache_offload.to_string(),
570            |s, c| s.kv_cache_offload != c.kv_cache_offload,
571            |_, _, _| {},
572            |_, _| {},
573            toggle_kv_cache_offload,
574            "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.",
575        ),
576        // ── Cache type fields ──────────────────────────────────────────────────
577        {
578            let mut f = expert_field(
579                "cache_type_k",
580                "Cache Type K",
581                "GPU Offload",
582                |s| {
583                    s.cache_type_k
584                        .map(|v| v.to_string())
585                        .unwrap_or_else(|| "Disabled".to_string())
586                },
587                |s, c| s.cache_type_k != c.cache_type_k,
588                |s, delta, _| {
589                    let mut val = s.cache_type_k.unwrap_or(CacheQuantType::F16);
590                    val = if delta > 0 { val.next() } else { val.prev() };
591                    s.cache_type_k = Some(val);
592                },
593                |s, buf| {
594                    if let Ok(n) = buf.parse::<u8>() {
595                        s.cache_type_k = Some(CacheQuantType::from_u8(n));
596                    }
597                },
598                "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.",
599            );
600            f.ctrl_e_toggle = Some(toggle_cache_type_k);
601            f
602        },
603        {
604            let mut f = expert_field(
605                "cache_type_v",
606                "Cache Type V",
607                "GPU Offload",
608                |s| {
609                    s.cache_type_v
610                        .map(|v| v.to_string())
611                        .unwrap_or_else(|| "Disabled".to_string())
612                },
613                |s, c| s.cache_type_v != c.cache_type_v,
614                |s, delta, _| {
615                    let mut val = s.cache_type_v.unwrap_or(CacheQuantType::F16);
616                    val = if delta > 0 { val.next() } else { val.prev() };
617                    s.cache_type_v = Some(val);
618                },
619                |s, buf| {
620                    if let Ok(n) = buf.parse::<u8>() {
621                        s.cache_type_v = Some(CacheQuantType::from_u8(n));
622                    }
623                },
624                "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.",
625            );
626            f.ctrl_e_toggle = Some(toggle_cache_type_v);
627            f
628        },
629        expert_field_with_toggle(
630            "expert_count",
631            "Active Experts",
632            "GPU Offload",
633            |s| {
634                if s.expert_count > 0 {
635                    s.expert_count.to_string()
636                } else if s.expert_count == -1 {
637                    "Auto".to_string()
638                } else {
639                    "Disabled".to_string()
640                }
641            },
642            |s, c| s.expert_count != c.expert_count,
643            |s, delta, _| {
644                s.expert_count = (s.expert_count + delta).clamp(-1, 99);
645            },
646            |s, buf| {
647                if let Ok(v) = buf.parse::<i32>() {
648                    s.expert_count = v.clamp(-1, 99);
649                }
650            },
651            toggle_expert_count,
652            "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.",
653        ),
654        // ── Evaluation ────────────────────────────────────────────────────────
655        field(
656            "batch_size",
657            "Eval Batch",
658            "Evaluation",
659            |s| s.batch_size.to_string(),
660            |s, c| s.batch_size != c.batch_size,
661            |s, delta, _| {
662                s.batch_size = (s.batch_size as i32 + delta * 64).max(1) as u32;
663            },
664            |s, buf| {
665                if let Ok(v) = buf.parse::<u32>() {
666                    s.batch_size = v.max(1);
667                }
668            },
669            "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.",
670        ),
671        field_with_toggle(
672            "uniform_cache",
673            "Unified KV",
674            "Evaluation",
675            |s| s.uniform_cache.to_string(),
676            |s, c| s.uniform_cache != c.uniform_cache,
677            |_, _, _| {},
678            |_, _| {},
679            toggle_uniform_cache,
680            "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.",
681        ),
682        expert_field(
683            "cache_reuse",
684            "Cache Reuse",
685            "Evaluation",
686            |s| s.cache_reuse.to_string(),
687            |s, c| s.cache_reuse != c.cache_reuse,
688            |s, delta, _| {
689                s.cache_reuse = (s.cache_reuse as i32 + delta * 16).max(0) as u32;
690            },
691            |s, buf| {
692                if let Ok(v) = buf.parse::<u32>() {
693                    s.cache_reuse = v;
694                }
695            },
696            "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).",
697        ),
698        expert_field_with_toggle(
699            "swa_full",
700            "SWA Full Cache",
701            "Evaluation",
702            |s| s.swa_full.to_string(),
703            |s, c| s.swa_full != c.swa_full,
704            |_, _, _| {},
705            |_, _| {},
706            toggle_swa_full,
707            "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.",
708        ),
709        expert_field_with_toggle(
710            "max_concurrent_predictions",
711            "Max Concurrent Pred",
712            "Evaluation",
713            |s| {
714                s.max_concurrent_predictions
715                    .map(|v| v.to_string())
716                    .unwrap_or_else(|| "Off".to_string())
717            },
718            |s, c| s.max_concurrent_predictions != c.max_concurrent_predictions,
719            |s, delta, _| match s.max_concurrent_predictions {
720                Some(n) => {
721                    s.max_concurrent_predictions = Some(((n as i32) + delta).clamp(1, 10) as u32)
722                }
723                None => s.max_concurrent_predictions = Some(1),
724            },
725            |s, buf| {
726                if let Ok(v) = buf.parse::<u32>() {
727                    s.max_concurrent_predictions = Some(v.clamp(1, 10));
728                }
729            },
730            toggle_max_concurrent_predictions,
731            "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.",
732        ),
733        // ── Sampling ──────────────────────────────────────────────────────────
734        field(
735            "seed",
736            "Seed",
737            "Sampling",
738            |s| s.seed.to_string(),
739            |s, c| s.seed != c.seed,
740            |s, delta, _| {
741                s.seed = (s.seed + delta).max(-1);
742            },
743            |s, buf| {
744                if let Ok(v) = buf.parse::<i32>() {
745                    s.seed = v;
746                }
747            },
748            "Random seed for reproducible outputs. -1 = random (default). Set to a fixed value for deterministic, repeatable responses — useful for debugging or testing prompts.",
749        ),
750        field(
751            "temperature",
752            "Temp",
753            "Sampling",
754            |s| format!("{:.2}", s.temperature),
755            |s, c| (s.temperature - c.temperature).abs() > 0.001,
756            |s, delta, _| {
757                s.temperature =
758                    ((s.temperature * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 2.0);
759            },
760            |s, buf| {
761                if let Ok(v) = buf.parse::<f32>() {
762                    s.temperature = v.clamp(0.0, 2.0);
763                }
764            },
765            "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.",
766        ),
767        field(
768            "top_k",
769            "Top-k",
770            "Sampling",
771            |s| s.top_k.to_string(),
772            |s, c| s.top_k != c.top_k,
773            |s, delta, _| {
774                s.top_k = (s.top_k + delta).max(1);
775            },
776            |s, buf| {
777                if let Ok(v) = buf.parse::<i32>() {
778                    s.top_k = v.max(0);
779                }
780            },
781            "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.",
782        ),
783        field(
784            "top_p",
785            "Top-p",
786            "Sampling",
787            |s| format!("{:.2}", s.top_p),
788            |s, c| (s.top_p - c.top_p).abs() > 0.001,
789            |s, delta, _| {
790                s.top_p = ((s.top_p * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 1.0);
791            },
792            |s, buf| {
793                if let Ok(v) = buf.parse::<f32>() {
794                    s.top_p = v.clamp(0.0, 1.0);
795                }
796            },
797            "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.",
798        ),
799        field(
800            "min_p",
801            "Min P",
802            "Sampling",
803            |s| format!("{:.2}", s.min_p),
804            |s, c| (s.min_p - c.min_p).abs() > 0.001,
805            |s, delta, _| {
806                s.min_p = ((s.min_p * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 1.0);
807            },
808            |s, buf| {
809                if let Ok(v) = buf.parse::<f32>() {
810                    s.min_p = v.clamp(0.0, 1.0);
811                }
812            },
813            "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.",
814        ),
815        ultra_field(
816            "typical_p",
817            "Typical P",
818            "Sampling",
819            |s| format!("{:.2}", s.typical_p),
820            |s, c| (s.typical_p - c.typical_p).abs() > 0.001,
821            |s, delta, _| {
822                s.typical_p = ((s.typical_p * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 1.0);
823            },
824            |s, buf| {
825                if let Ok(v) = buf.parse::<f32>() {
826                    s.typical_p = v.clamp(0.0, 1.0);
827                }
828            },
829            "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).",
830        ),
831        ultra_field(
832            "mirostat",
833            "Mirostat",
834            "Sampling",
835            |s| s.mirostat.to_string(),
836            |s, c| s.mirostat != c.mirostat,
837            |s, delta, _| {
838                let mut val = s.mirostat;
839                val = match (delta, val) {
840                    (1, Mirostat::Off) => Mirostat::V1,
841                    (1, Mirostat::V1) => Mirostat::Mirostat2,
842                    (1, Mirostat::Mirostat2) => Mirostat::Off,
843                    (-1, Mirostat::Off) => Mirostat::Mirostat2,
844                    (-1, Mirostat::V1) => Mirostat::Off,
845                    (-1, Mirostat::Mirostat2) => Mirostat::V1,
846                    _ => val,
847                };
848                s.mirostat = val;
849            },
850            |_, _| {},
851            "Mirostat sampling mode: Off (default), Mirostat, or Mirostat2. Adaptive temperature control that maintains target perplexity. Mirostat2 is more aggressive. Useful for consistent output quality.",
852        ),
853        ultra_field(
854            "mirostat_lr",
855            "Mirostat LR",
856            "Sampling",
857            |s| format!("{:.2}", s.mirostat_lr),
858            |s, c| (s.mirostat_lr - c.mirostat_lr).abs() > 0.001,
859            |s, delta, _| {
860                s.mirostat_lr =
861                    ((s.mirostat_lr * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 1.0);
862            },
863            |s, buf| {
864                if let Ok(v) = buf.parse::<f32>() {
865                    s.mirostat_lr = v.clamp(0.0, 1.0);
866                }
867            },
868            "Mirostat learning rate (eta). Controls how quickly the temperature adapts. Smaller = smoother adjustments. Typical: 0.1.",
869        ),
870        ultra_field(
871            "mirostat_ent",
872            "Mirostat Ent",
873            "Sampling",
874            |s| format!("{:.2}", s.mirostat_ent),
875            |s, c| (s.mirostat_ent - c.mirostat_ent).abs() > 0.001,
876            |s, delta, _| {
877                s.mirostat_ent =
878                    ((s.mirostat_ent * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 10.0);
879            },
880            |s, buf| {
881                if let Ok(v) = buf.parse::<f32>() {
882                    s.mirostat_ent = v.clamp(0.0, 10.0);
883                }
884            },
885            "Mirostat target entropy. Controls the diversity of output. Higher = more diverse. Typical: 5.0.",
886        ),
887        ultra_field_with_toggle(
888            "ignore_eos",
889            "Ignore EOS",
890            "Sampling",
891            |s| s.ignore_eos.to_string(),
892            |s, c| s.ignore_eos != c.ignore_eos,
893            |_, _, _| {},
894            |_, _| {},
895            toggle_ignore_eos,
896            "Ignore end-of-sequence tokens during generation. Toggle on/off with Enter. Useful when you want to force the model to continue generating.",
897        ),
898        ultra_field(
899            "samplers",
900            "Samplers",
901            "Sampling",
902            |s| s.samplers.0.clone(),
903            |s, c| s.samplers.0 != c.samplers.0,
904            |_, _, _| {},
905            |_, _| {},
906            "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.",
907        ),
908        field_with_toggle(
909            "max_tokens",
910            "Max Tokens",
911            "Sampling",
912            |s| {
913                s.max_tokens
914                    .map(|v| v.to_string())
915                    .unwrap_or_else(|| "Disabled".to_string())
916            },
917            |s, c| s.max_tokens != c.max_tokens,
918            |s, delta, _| {
919                let current = s.max_tokens.unwrap_or(0);
920                s.max_tokens = Some((current as i32 + delta * 16).max(16) as u32);
921            },
922            |s, buf| {
923                if let Ok(v) = buf.parse::<i32>() {
924                    s.max_tokens = if v == 0 { None } else { Some(v as u32) };
925                }
926            },
927            toggle_max_tokens,
928            "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.",
929        ),
930        // ── Repetition ────────────────────────────────────────────────────────
931        field(
932            "repeat_penalty",
933            "Repeat Penalty",
934            "Repetition",
935            |s| format!("{:.2}", s.repeat_penalty),
936            |s, c| (s.repeat_penalty - c.repeat_penalty).abs() > 0.001,
937            |s, delta, _| {
938                s.repeat_penalty =
939                    ((s.repeat_penalty * 100.0 + delta as f32 * 5.0) / 100.0).clamp(1.0, 2.0);
940            },
941            |s, buf| {
942                if let Ok(v) = buf.parse::<f32>() {
943                    s.repeat_penalty = v.clamp(0.0, 2.0);
944                }
945            },
946            "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.",
947        ),
948        field(
949            "repeat_last_n",
950            "Repeat Last N",
951            "Repetition",
952            |s| s.repeat_last_n.to_string(),
953            |s, c| s.repeat_last_n != c.repeat_last_n,
954            |s, delta, _| {
955                s.repeat_last_n = (s.repeat_last_n + delta).max(0);
956            },
957            |s, buf| {
958                if let Ok(v) = buf.parse::<i32>() {
959                    s.repeat_last_n = v.max(0);
960                }
961            },
962            "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.",
963        ),
964        expert_field_with_toggle(
965            "presence_penalty",
966            "Presence Penalty",
967            "Repetition",
968            |s| {
969                s.presence_penalty
970                    .map(|v| {
971                        if (v - 0.0).abs() < 0.001 {
972                            "Off".to_string()
973                        } else {
974                            format!("{:.2}", v)
975                        }
976                    })
977                    .unwrap_or_else(|| "Off".to_string())
978            },
979            |s, c| match (s.presence_penalty, c.presence_penalty) {
980                (Some(v1), Some(v2)) => (v1 - v2).abs() > 0.001,
981                (None, None) => false,
982                _ => true,
983            },
984            |s, delta, _| {
985                let current = s.presence_penalty.unwrap_or(0.0);
986                s.presence_penalty =
987                    Some(((current * 100.0 + delta as f32 * 5.0) / 100.0).clamp(-2.0, 2.0));
988            },
989            |s, buf| {
990                if let Ok(v) = buf.parse::<f32>() {
991                    s.presence_penalty = Some(v.clamp(-2.0, 2.0));
992                }
993            },
994            toggle_presence_penalty,
995            "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).",
996        ),
997        expert_field_with_toggle(
998            "frequency_penalty",
999            "Freq Penalty",
1000            "Repetition",
1001            |s| {
1002                s.frequency_penalty
1003                    .map(|v| {
1004                        if (v - 0.0).abs() < 0.001 {
1005                            "Off".to_string()
1006                        } else {
1007                            format!("{:.2}", v)
1008                        }
1009                    })
1010                    .unwrap_or_else(|| "Off".to_string())
1011            },
1012            |s, c| match (s.frequency_penalty, c.frequency_penalty) {
1013                (Some(v1), Some(v2)) => (v1 - v2).abs() > 0.001,
1014                (None, None) => false,
1015                _ => true,
1016            },
1017            |s, delta, _| {
1018                let current = s.frequency_penalty.unwrap_or(0.0);
1019                s.frequency_penalty =
1020                    Some(((current * 100.0 + delta as f32 * 5.0) / 100.0).clamp(-2.0, 2.0));
1021            },
1022            |s, buf| {
1023                if let Ok(v) = buf.parse::<f32>() {
1024                    s.frequency_penalty = Some(v.clamp(-2.0, 2.0));
1025                }
1026            },
1027            toggle_frequency_penalty,
1028            "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).",
1029        ),
1030        // ── DRY ───────────────────────────────────────────────────────────────
1031        ultra_field(
1032            "dry_multiplier",
1033            "DRY Multiplier",
1034            "DRY",
1035            |s| format!("{:.2}", s.dry_multiplier),
1036            |s, c| (s.dry_multiplier - c.dry_multiplier).abs() > 0.001,
1037            |s, delta, _| {
1038                s.dry_multiplier =
1039                    ((s.dry_multiplier * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 10.0);
1040            },
1041            |s, buf| {
1042                if let Ok(v) = buf.parse::<f32>() {
1043                    s.dry_multiplier = v.clamp(0.0, 10.0);
1044                }
1045            },
1046            "DRY (Don't Repeat Yourself) multiplier. Scales the penalty for repetition. Higher values = stronger anti-repetition. Typical: 1.75.",
1047        ),
1048        ultra_field(
1049            "dry_base",
1050            "DRY Base",
1051            "DRY",
1052            |s| format!("{:.2}", s.dry_base),
1053            |s, c| (s.dry_base - c.dry_base).abs() > 0.001,
1054            |s, delta, _| {
1055                s.dry_base = ((s.dry_base * 100.0 + delta as f32 * 5.0) / 100.0).clamp(0.0, 10.0);
1056            },
1057            |s, buf| {
1058                if let Ok(v) = buf.parse::<f32>() {
1059                    s.dry_base = v.clamp(0.0, 10.0);
1060                }
1061            },
1062            "DRY penalty base (log scale). Controls the strength of the repetition penalty. Typical: 1.0 (log2) or 0.0 (linear).",
1063        ),
1064        ultra_field(
1065            "dry_allowed_length",
1066            "DRY Allowed Length",
1067            "DRY",
1068            |s| s.dry_allowed_length.to_string(),
1069            |s, c| s.dry_allowed_length != c.dry_allowed_length,
1070            |s, delta, _| {
1071                s.dry_allowed_length = (s.dry_allowed_length + delta).max(0);
1072            },
1073            |s, buf| {
1074                if let Ok(v) = buf.parse::<i32>() {
1075                    s.dry_allowed_length = v;
1076                }
1077            },
1078            "Number of recent tokens to check for repetition (penalty starts after this). Higher values check longer context. Typical: 2.",
1079        ),
1080        ultra_field(
1081            "dry_penalty_last_n",
1082            "DRY Penalty Last N",
1083            "DRY",
1084            |s| s.dry_penalty_last_n.to_string(),
1085            |s, c| s.dry_penalty_last_n != c.dry_penalty_last_n,
1086            |s, delta, _| {
1087                s.dry_penalty_last_n = (s.dry_penalty_last_n + delta).max(0);
1088            },
1089            |s, buf| {
1090                if let Ok(v) = buf.parse::<i32>() {
1091                    s.dry_penalty_last_n = v;
1092                }
1093            },
1094            "How many tokens to consider for DRY penalty (0 = all). Larger values catch longer repetition patterns. Typical: -1 (all) or 128.",
1095        ),
1096        // ── Speculative Decoding ─────────────────────────────────────────────
1097        expert_field_with_toggle(
1098            "is_mtp",
1099            "MTP",
1100            "Speculative",
1101            |s| {
1102                if s.spec_type.is_empty() {
1103                    "Off".to_string()
1104                } else {
1105                    s.spec_type.clone()
1106                }
1107            },
1108            |s, c| s.spec_type != c.spec_type,
1109            |_, _, _| {},
1110            |_, _| {},
1111            toggle_mtp,
1112            "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.",
1113        ),
1114        expert_field(
1115            "draft_tokens",
1116            "Spec Draft N Max",
1117            "Speculative",
1118            |s| s.draft_tokens.to_string(),
1119            |s, c| s.draft_tokens != c.draft_tokens,
1120            |s, delta, _| {
1121                s.draft_tokens = (s.draft_tokens as i32 + delta).clamp(0, 16) as u32;
1122            },
1123            |s, buf| {
1124                if let Ok(v) = buf.parse::<u32>() {
1125                    s.draft_tokens = v.min(16);
1126                }
1127            },
1128            "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.",
1129        ),
1130        // ── Tags ──────────────────────────────────────────────────────────────
1131        field(
1132            "tags",
1133            "Tags (Enter to edit)",
1134            "Tags",
1135            |s| {
1136                if s.tags.is_empty() {
1137                    "None".to_string()
1138                } else {
1139                    s.tags.join(", ")
1140                }
1141            },
1142            |s, c| s.tags != c.tags,
1143            |_, _, _| {},
1144            |_, _| {},
1145            "Comma-separated labels for the model (e.g., 'coding, chat, reasoning'). Used for filtering and organization. Press Enter to open a tag editor.",
1146        ),
1147        // ── Backend ───────────────────────────────────────────────────────────
1148        field(
1149            "backend_version",
1150            "LLama.cpp Version",
1151            "Backend",
1152            |s| s.get_active_backend_version_display().to_string(),
1153            |s, c| s.get_active_backend_version() != c.get_active_backend_version(),
1154            |_, _, _| {},
1155            |_, _| {},
1156            "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.",
1157        ),
1158    ]
1159}
1160
1161pub fn filtered_fields(expert_mode: bool) -> Vec<SettingField> {
1162    all_fields()
1163        .into_iter()
1164        .filter(|f| {
1165            if !expert_mode {
1166                !f.is_expert
1167            } else {
1168                !f.is_ultra // In expert mode, hide ultra experts
1169            }
1170        })
1171        .collect()
1172}
1173
1174// ── Simple helper for the server settings panel (tabbed.rs) ──────────────────
1175
1176/// Render a single setting line for the server settings panel.
1177#[allow(clippy::too_many_arguments)]
1178pub fn add_setting(
1179    lines: &mut Vec<Line<'static>>,
1180    total_count: &mut usize,
1181    _settings: &ModelSettings,
1182    _cached: &ModelSettings,
1183    selected_line_idx: &mut usize,
1184    selected_content_line: &mut usize,
1185    idx: usize,
1186    name: &str,
1187    val: &str,
1188    selected: usize,
1189    _edit_buf: &str,
1190    _editing: bool,
1191    disabled: bool,
1192) {
1193    let current_line = lines.len();
1194    let name_style = if disabled {
1195        Style::default().fg(GRAY)
1196    } else {
1197        Style::default().fg(ACCENT)
1198    };
1199    let val_style = if disabled {
1200        Style::default().fg(GRAY)
1201    } else {
1202        Style::default().fg(WHITE)
1203    };
1204    if idx == selected {
1205        *selected_line_idx = current_line;
1206        *selected_content_line = current_line;
1207        lines.push(Line::from(vec![
1208            Span::styled(
1209                "> ",
1210                Style::default().fg(ACCENT).add_modifier(if disabled {
1211                    Modifier::DIM
1212                } else {
1213                    Modifier::BOLD
1214                }),
1215            ),
1216            Span::styled(format!("{name}: "), name_style),
1217            Span::styled(
1218                val.to_string(),
1219                Style::default()
1220                    .fg(BLACK)
1221                    .bg(ACCENT)
1222                    .add_modifier(Modifier::BOLD),
1223            ),
1224        ]));
1225    } else {
1226        lines.push(Line::from(vec![
1227            Span::styled("  ", name_style),
1228            Span::styled(format!("{name}: "), name_style),
1229            Span::styled(val.to_string(), val_style),
1230        ]));
1231    }
1232    *total_count += 1;
1233}
1234
1235/// Build a list of setting names that differ between a profile and the current settings.
1236pub fn profile_settings_parts(profile: &Profile, current: &ModelSettings) -> Vec<String> {
1237    let mut parts = Vec::new();
1238    let s = &profile.settings;
1239
1240    // ── Integers ──────────────────────────────────────────────────────────
1241    diff_int!(parts, s, current, context_length, "ctx");
1242    diff_int!(parts, s, current, threads, "threads");
1243    diff_int!(parts, s, current, threads_batch, "threads_batch");
1244    diff_int!(parts, s, current, batch_size, "batch");
1245    diff_int!(parts, s, current, ubatch_size, "ubatch");
1246    diff_int!(parts, s, current, parallel, "parallel");
1247    diff_option!(parts, s, current, max_concurrent_predictions, "concurrent");
1248    diff_int!(parts, s, current, cache_reuse, "cache_reuse");
1249    diff_option!(parts, s, current, max_tokens, "max_tokens");
1250    diff_int!(parts, s, current, draft_tokens, "draft_tokens");
1251
1252    diff_int!(parts, s, current, keep, "keep");
1253    diff_int!(parts, s, current, main_gpu, "main_gpu");
1254    diff_int!(parts, s, current, expert_count, "expert_count");
1255    diff_int!(parts, s, current, seed, "seed");
1256    diff_int!(parts, s, current, top_k, "top_k");
1257    diff_int!(parts, s, current, repeat_last_n, "repeat_last_n");
1258    diff_int!(parts, s, current, dry_allowed_length, "dry_allowed");
1259    diff_int!(parts, s, current, dry_penalty_last_n, "dry_penalty_last_n");
1260
1261    // ── Floats ────────────────────────────────────────────────────────────
1262    diff_float!(parts, s, current, temperature, "temp");
1263    diff_float!(parts, s, current, top_p, "top_p");
1264    diff_float!(parts, s, current, min_p, "min_p");
1265    diff_float!(parts, s, current, typical_p, "typical_p");
1266    diff_float!(parts, s, current, mirostat_lr, "mirostat_lr");
1267    diff_float!(parts, s, current, mirostat_ent, "mirostat_ent");
1268    diff_float!(parts, s, current, repeat_penalty, "rep_pen");
1269    diff_option_float!(parts, s, current, presence_penalty, "pres_pen");
1270    diff_option_float!(parts, s, current, frequency_penalty, "freq_pen");
1271    diff_float!(parts, s, current, dry_multiplier, "dry_mult");
1272    diff_float!(parts, s, current, dry_base, "dry_base");
1273    diff_float!(parts, s, current, rope_scale, "rope_scale");
1274    diff_float!(parts, s, current, rope_freq_base, "rope_freq_base");
1275    diff_float!(parts, s, current, rope_freq_scale, "rope_freq_scale");
1276
1277    // ── Bools ─────────────────────────────────────────────────────────────
1278    diff_bool!(parts, s, current, swa_full, "swa_full");
1279    diff_bool!(parts, s, current, mlock, "mlock");
1280    diff_bool!(parts, s, current, mmap, "mmap");
1281    diff_bool!(parts, s, current, uniform_cache, "uniform_cache");
1282    diff_bool!(parts, s, current, kv_cache_offload, "kv_cache_offload");
1283    diff_bool!(parts, s, current, fit, "fit");
1284    diff_bool!(parts, s, current, embedding, "embedding");
1285    diff_bool!(parts, s, current, flash_attn, "flash_attn");
1286    diff_bool!(parts, s, current, jinja, "jinja");
1287    diff_bool!(parts, s, current, auto_chat_template, "auto_chat_template");
1288    diff_bool!(parts, s, current, ignore_eos, "ignore_eos");
1289    diff_bool!(parts, s, current, rope_yarn_enabled, "yarn_enabled");
1290    diff_bool!(parts, s, current, cache_prompt, "cache_prompt");
1291    diff_bool!(parts, s, current, webui, "webui");
1292    // ── Strings ───────────────────────────────────────────────────────────
1293    diff_string!(parts, s, current, system_prompt_preset_name, "preset");
1294    diff_string!(parts, s, current, tensor_split, "tensor_split");
1295    diff_string!(parts, s, current, rpc, "rpc");
1296    if s.chat_template != current.chat_template
1297        && let Some(ref v) = s.chat_template
1298    {
1299        let filename = std::path::Path::new(v)
1300            .file_name()
1301            .and_then(|n| n.to_str())
1302            .unwrap_or(v);
1303        parts.push(format!("chat_template={}", filename));
1304    }
1305    diff_option!(
1306        parts,
1307        s,
1308        current,
1309        chat_template_kwargs,
1310        "chat_template_kwargs"
1311    );
1312    diff_option!(parts, s, current, llama_cpp_version_cpu, "llama_cpp_cpu");
1313    diff_option!(
1314        parts,
1315        s,
1316        current,
1317        llama_cpp_version_vulkan,
1318        "llama_cpp_vulkan"
1319    );
1320    diff_option!(parts, s, current, llama_cpp_version_rocm, "llama_cpp_rocm");
1321    diff_option!(
1322        parts,
1323        s,
1324        current,
1325        llama_cpp_version_rocm_lemonade,
1326        "llama_cpp_rocm_lemonade"
1327    );
1328    diff_option!(parts, s, current, llama_cpp_version_cuda, "llama_cpp_cuda");
1329    diff_string!(parts, s, current, spec_type, "spec_type");
1330
1331    // ── Enums ─────────────────────────────────────────────────────────────
1332    diff_enum!(parts, s, current, numa, "numa");
1333    diff_enum!(parts, s, current, split_mode, "split_mode");
1334    diff_enum!(parts, s, current, mirostat, "mirostat");
1335    diff_enum!(parts, s, current, samplers, "samplers");
1336    diff_enum!(parts, s, current, rope_scaling, "rope_scaling");
1337    diff_enum!(parts, s, current, cache_type, "cache_type");
1338    diff_option!(parts, s, current, cache_type_k, "cache_type_k");
1339    diff_option!(parts, s, current, cache_type_v, "cache_type_v");
1340
1341    // ── Special (custom display) ──────────────────────────────────────────
1342    if let Some(v) = s.gpu_layers_mode
1343        && v != current.gpu_layers_mode
1344    {
1345        let display = match v {
1346            crate::models::GpuLayersMode::Auto => "Auto".to_string(),
1347            crate::models::GpuLayersMode::Specific(n) => n.to_string(),
1348            crate::models::GpuLayersMode::All => "All".to_string(),
1349        };
1350        parts.push(format!("gpu_layers={}", display));
1351    }
1352
1353    parts
1354}