Skip to main content

llm_manager/tui/
settings.rs

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