Store preferences by model profile

This commit is contained in:
Georg Bauer
2026-08-02 12:38:21 +02:00
parent 69c83e7059
commit e52efb80b0
13 changed files with 606 additions and 196 deletions

View File

@@ -4,6 +4,12 @@ use std::sync::RwLock;
#[derive(Clone)]
pub(super) struct PreferenceDraft {
pub(super) model: ModelChoice,
pub(super) default_reasoning_mode: ReasoningMode,
pub(super) generation_model: ModelChoice,
pub(super) generation_reasoning_mode: ReasoningMode,
pub(super) acceleration_model: ModelChoice,
generation_profiles: BTreeMap<ModelChoice, BTreeMap<ReasoningMode, GenerationPreferences>>,
model_profiles: BTreeMap<ModelChoice, ModelPreferences>,
pub(super) default_permission_mode: PermissionMode,
pub(super) legacy_mtp_enabled: bool,
pub(super) dspark_enabled: bool,
@@ -28,7 +34,6 @@ pub(super) struct PreferenceDraft {
pub(super) top_p: String,
pub(super) min_p: String,
pub(super) seed: String,
pub(super) reasoning_mode: ReasoningMode,
pub(super) cpu_threads: String,
pub(super) power_percent: String,
pub(super) prefill_chunk: String,
@@ -58,12 +63,19 @@ pub(super) struct PreferenceDraft {
impl PreferenceDraft {
pub(super) fn from_saved(config: &Config) -> Self {
let generation = &config.generation;
let runtime = &config.runtime;
let default_reasoning_mode = config.reasoning_mode(config.model);
let generation = config.generation_for(config.model, default_reasoning_mode);
let runtime = config.runtime_for(config.model);
let execution = &runtime.execution;
let speculative = &runtime.speculative;
Self {
model: config.model,
default_reasoning_mode,
generation_model: config.model,
generation_reasoning_mode: default_reasoning_mode,
acceleration_model: config.model,
generation_profiles: config.generation_profiles.clone(),
model_profiles: config.model_profiles.clone(),
default_permission_mode: config.default_permission_mode,
legacy_mtp_enabled: speculative.legacy_mtp_enabled,
dspark_enabled: speculative.dspark_enabled,
@@ -83,12 +95,11 @@ impl PreferenceDraft {
git_ignore_blank_lines: config.git.ignore_blank_lines,
context_tokens: generation.context_tokens.to_string(),
max_generated_tokens: generation.max_generated_tokens.to_string(),
system_prompt: text_editor::Content::with_text(&generation.system_prompt),
system_prompt: text_editor::Content::with_text(&config.system_prompt),
temperature: optional_string(generation.temperature),
top_p: optional_string(generation.top_p),
min_p: optional_string(generation.min_p),
seed: optional_string(generation.seed),
reasoning_mode: generation.reasoning_mode,
cpu_threads: optional_string(execution.cpu_threads),
power_percent: optional_string(execution.power_percent),
prefill_chunk: optional_string(execution.prefill_chunk),
@@ -137,7 +148,7 @@ impl PreferenceDraft {
top_p: parse_optional_f32("Top-p", &self.top_p)?,
min_p: parse_optional_f32("Min-p", &self.min_p)?,
seed: parse_optional_u64("Seed", &self.seed)?,
reasoning_mode: self.reasoning_mode,
reasoning_mode: self.generation_reasoning_mode,
};
preferences.validate()?;
Ok(preferences)
@@ -159,6 +170,39 @@ impl PreferenceDraft {
*self = Self::from_saved(&Config::default());
}
fn store_generation(&mut self) -> Result<(), String> {
let generation = self.generation()?;
self.generation_profiles
.entry(self.generation_model)
.or_default()
.insert(self.generation_reasoning_mode, generation);
Ok(())
}
pub(super) fn load_generation(&mut self, model: ModelChoice, mode: ReasoningMode) {
let mut generation = self
.generation_profiles
.get(&model)
.and_then(|profiles| profiles.get(&mode))
.cloned()
.unwrap_or_default();
generation.reasoning_mode = mode;
self.generation_model = model;
self.generation_reasoning_mode = mode;
self.context_tokens = generation.context_tokens.to_string();
self.max_generated_tokens = generation.max_generated_tokens.to_string();
self.temperature = optional_string(generation.temperature);
self.top_p = optional_string(generation.top_p);
self.min_p = optional_string(generation.min_p);
self.seed = optional_string(generation.seed);
}
fn select_generation(&mut self, model: ModelChoice, mode: ReasoningMode) -> Result<(), String> {
self.store_generation()?;
self.load_generation(model, mode);
Ok(())
}
pub(super) fn execution(&self) -> Result<ExecutionPreferences, String> {
Ok(ExecutionPreferences {
cpu_threads: parse_optional_u32("CPU helper threads", &self.cpu_threads)?,
@@ -185,20 +229,92 @@ impl PreferenceDraft {
})
}
fn acceleration(&self) -> Result<(SpeculativePreferences, SsdPreferences), String> {
let speculative = self.speculative()?;
let ssd = SsdPreferences {
enabled: self.ssd_streaming,
cold: self.ssd_streaming_cold,
cache: parse_streaming_cache(&self.ssd_cache)?,
full_layers: parse_optional_u32("SSD full-layer count", &self.ssd_full_layers)?,
preload_experts: parse_optional_u32("SSD preload experts", &self.ssd_preload_experts)?,
};
speculative.validate(self.acceleration_model)?;
ssd.validate(self.acceleration_model)?;
Ok((speculative, ssd))
}
fn store_acceleration(&mut self) -> Result<(), String> {
let (speculative, ssd) = self.acceleration()?;
let profile = self
.model_profiles
.entry(self.acceleration_model)
.or_default();
profile.speculative = speculative;
profile.ssd = ssd;
Ok(())
}
fn load_acceleration(&mut self, model: ModelChoice) {
let profile = self.model_profiles.get(&model).cloned().unwrap_or_default();
let speculative = profile.speculative;
let ssd = profile.ssd;
self.acceleration_model = model;
self.legacy_mtp_enabled = speculative.legacy_mtp_enabled;
self.dspark_enabled = speculative.dspark_enabled;
self.mtp_draft_tokens = speculative.mtp_draft_tokens.to_string();
self.mtp_margin = speculative.mtp_margin.to_string();
self.glm_mtp = speculative.glm_mtp;
self.glm_mtp_timing = speculative.glm_mtp_timing;
self.dspark_confidence_threshold = optional_string(speculative.dspark_confidence_threshold);
self.dspark_strict = speculative.dspark_strict;
self.ssd_streaming = ssd.enabled;
self.ssd_streaming_cold = ssd.cold;
self.ssd_cache = optional_string(ssd.cache);
self.ssd_full_layers = optional_string(ssd.full_layers);
self.ssd_preload_experts = optional_string(ssd.preload_experts);
}
fn select_acceleration(&mut self, model: ModelChoice) -> Result<(), String> {
self.store_acceleration()?;
self.load_acceleration(model);
Ok(())
}
pub(super) fn effective_for(
&self,
model: ModelChoice,
mode: ReasoningMode,
) -> Result<crate::settings::EffectiveSettings, String> {
let mut draft = self.clone();
draft.store_generation()?;
draft.store_acceleration()?;
let mut generation = draft
.generation_profiles
.get(&model)
.and_then(|profiles| profiles.get(&mode))
.cloned()
.unwrap_or_default();
generation.system_prompt = draft.system_prompt.text();
generation.reasoning_mode = mode;
let mut runtime = draft.runtime()?;
if let Some(profile) = draft.model_profiles.get(&model) {
runtime.speculative = profile.speculative.clone();
runtime.ssd = profile.ssd.clone();
}
crate::settings::effective_settings(model, &generation, &runtime, &models_path())
}
pub(super) fn reasoning_mode_for(&self, model: ModelChoice) -> ReasoningMode {
self.model_profiles
.get(&model)
.map_or(ReasoningMode::default(), |profile| profile.reasoning_mode)
}
pub(super) fn runtime(&self) -> Result<RuntimePreferences, String> {
Ok(RuntimePreferences {
execution: self.execution()?,
speculative: self.speculative()?,
ssd: SsdPreferences {
enabled: self.ssd_streaming,
cold: self.ssd_streaming_cold,
cache: parse_streaming_cache(&self.ssd_cache)?,
full_layers: parse_optional_u32("SSD full-layer count", &self.ssd_full_layers)?,
preload_experts: parse_optional_u32(
"SSD preload experts",
&self.ssd_preload_experts,
)?,
},
speculative: SpeculativePreferences::default(),
ssd: SsdPreferences::default(),
steering: SteeringPreferences {
file: optional_text(&self.directional_steering_file),
ffn_scale: parse_optional_f32(
@@ -386,13 +502,14 @@ impl App {
self.preference_error = Some("Endpoint port must be a whole number.".into());
return;
};
let generation = match self.preference_draft.generation() {
Ok(generation) => generation,
Err(error) => {
self.preference_error = Some(error);
return;
}
};
if let Err(error) = self.preference_draft.store_generation() {
self.preference_error = Some(error);
return;
}
if let Err(error) = self.preference_draft.store_acceleration() {
self.preference_error = Some(error);
return;
}
let runtime = match self.preference_draft.runtime() {
Ok(runtime) => runtime,
Err(error) => {
@@ -407,6 +524,11 @@ impl App {
return;
}
};
self.preference_draft
.model_profiles
.entry(self.preference_draft.model)
.or_default()
.reasoning_mode = self.preference_draft.default_reasoning_mode;
let config = Config {
model: self.preference_draft.model,
default_permission_mode: self.preference_draft.default_permission_mode,
@@ -421,7 +543,9 @@ impl App {
enabled: self.preference_draft.dev_brain_enabled,
vault_path: optional_text(&self.preference_draft.dev_brain_vault_path),
},
generation,
system_prompt: self.preference_draft.system_prompt.text(),
generation_profiles: self.preference_draft.generation_profiles.clone(),
model_profiles: self.preference_draft.model_profiles.clone(),
runtime,
git,
interface: self.config.interface.clone(),
@@ -482,6 +606,11 @@ impl App {
return;
}
self.config = config;
self.context_limit = self.config.active_generation().context_tokens.max(0) as u32;
#[cfg(target_os = "macos")]
{
self.agent_tools = None;
}
#[cfg(target_os = "macos")]
if dev_brain_changed {
self.invalidate_dev_brain_context();
@@ -507,33 +636,65 @@ impl App {
) -> Result<Message, Task<Message>> {
match message {
Message::PreferenceModelChanged(model) => {
self.preference_draft.model = model;
if !model.supports_dspark() {
self.preference_draft.legacy_mtp_enabled = false;
self.preference_draft.dspark_enabled = false;
self.preference_draft.dspark_confidence_threshold.clear();
self.preference_draft.dspark_strict = false;
if let Err(error) = self.preference_draft.store_generation() {
self.preference_error = Some(error);
return Err(Task::none());
}
if let Err(error) = self.preference_draft.store_acceleration() {
self.preference_error = Some(error);
return Err(Task::none());
}
let mode = self.preference_draft.reasoning_mode_for(model);
self.preference_draft.model = model;
self.preference_draft.default_reasoning_mode = mode;
self.preference_draft.load_generation(model, mode);
self.preference_draft.load_acceleration(model);
if model == ModelChoice::Glm52 {
self.preference_draft.power_percent.clear();
self.preference_draft.prefill_chunk.clear();
self.preference_draft.directional_steering_file.clear();
self.preference_draft.directional_steering_ffn.clear();
self.preference_draft.directional_steering_attn.clear();
} else {
self.preference_draft.glm_mtp = false;
self.preference_draft.glm_mtp_timing = false;
self.preference_draft.ssd_full_layers.clear();
}
self.preference_error = None;
}
Message::PreferenceDefaultReasoningChanged(mode) => {
if self.preference_draft.generation_model == self.preference_draft.model
&& let Err(error) = self
.preference_draft
.select_generation(self.preference_draft.model, mode)
{
self.preference_error = Some(error);
return Err(Task::none());
}
self.preference_draft.default_reasoning_mode = mode;
self.preference_draft
.model_profiles
.entry(self.preference_draft.model)
.or_default()
.reasoning_mode = mode;
self.preference_error = None;
}
Message::PreferenceGenerationModelChanged(model) => {
let mode = self.preference_draft.reasoning_mode_for(model);
self.preference_error = self.preference_draft.select_generation(model, mode).err();
}
Message::PreferenceGenerationReasoningChanged(mode) => {
self.preference_error = self
.preference_draft
.select_generation(self.preference_draft.generation_model, mode)
.err();
}
Message::PreferenceAccelerationModelChanged(model) => {
self.preference_error = self.preference_draft.select_acceleration(model).err();
}
Message::PreferencePermissionModeChanged(mode) => {
self.preference_draft.default_permission_mode = mode;
self.preference_error = None;
}
Message::PreferenceLegacyMtpChanged(enabled) => {
self.preference_draft.legacy_mtp_enabled =
self.preference_draft.model.supports_dspark() && enabled;
self.preference_draft.acceleration_model.supports_dspark() && enabled;
if self.preference_draft.legacy_mtp_enabled {
self.preference_draft.dspark_enabled = false;
self.preference_draft.dspark_confidence_threshold.clear();
@@ -543,7 +704,7 @@ impl App {
}
Message::PreferenceDsparkChanged(enabled) => {
self.preference_draft.dspark_enabled =
self.preference_draft.model.supports_dspark() && enabled;
self.preference_draft.acceleration_model.supports_dspark() && enabled;
if !self.preference_draft.dspark_enabled {
self.preference_draft.dspark_confidence_threshold.clear();
self.preference_draft.dspark_strict = false;
@@ -675,10 +836,6 @@ impl App {
self.preference_draft.seed = value;
self.preference_error = None;
}
Message::PreferenceReasoningChanged(value) => {
self.preference_draft.reasoning_mode = value;
self.preference_error = None;
}
Message::PreferenceCpuThreadsChanged(value) => {
self.preference_draft.cpu_threads = value;
self.preference_error = None;
@@ -709,7 +866,7 @@ impl App {
}
Message::PreferenceGlmMtpChanged(value) => {
self.preference_draft.glm_mtp =
self.preference_draft.model == ModelChoice::Glm52 && value;
self.preference_draft.acceleration_model == ModelChoice::Glm52 && value;
if !self.preference_draft.glm_mtp {
self.preference_draft.glm_mtp_timing = false;
}
@@ -717,7 +874,7 @@ impl App {
}
Message::PreferenceGlmMtpTimingChanged(value) => {
self.preference_draft.glm_mtp_timing =
self.preference_draft.model == ModelChoice::Glm52 && value;
self.preference_draft.acceleration_model == ModelChoice::Glm52 && value;
if self.preference_draft.glm_mtp_timing {
self.preference_draft.glm_mtp = true;
}
@@ -725,7 +882,7 @@ impl App {
}
Message::PreferenceDsparkConfidenceChanged(value) => {
self.preference_draft.dspark_confidence_threshold = value;
if self.preference_draft.model.supports_dspark()
if self.preference_draft.acceleration_model.supports_dspark()
&& !self
.preference_draft
.dspark_confidence_threshold
@@ -739,7 +896,7 @@ impl App {
}
Message::PreferenceDsparkStrictChanged(value) => {
self.preference_draft.dspark_strict =
self.preference_draft.model.supports_dspark() && value;
self.preference_draft.acceleration_model.supports_dspark() && value;
if self.preference_draft.dspark_strict {
self.preference_draft.dspark_enabled = true;
self.preference_draft.legacy_mtp_enabled = false;
@@ -759,7 +916,7 @@ impl App {
self.preference_error = None;
}
Message::PreferenceSsdFullLayersChanged(value) => {
if self.preference_draft.model == ModelChoice::Glm52 {
if self.preference_draft.acceleration_model == ModelChoice::Glm52 {
self.preference_draft.ssd_full_layers = value;
}
self.preference_error = None;
@@ -842,6 +999,30 @@ mod tests {
);
}
#[test]
fn selectors_rehydrate_their_generation_and_model_profiles() {
let mut draft = PreferenceDraft::from_saved(&Config::default());
draft.context_tokens = "123".into();
draft.ssd_streaming = true;
draft
.select_generation(ModelChoice::Glm52, ReasoningMode::Direct)
.unwrap();
assert_eq!(draft.context_tokens, "32768");
draft.context_tokens = "456".into();
draft
.select_generation(ModelChoice::DeepSeekV4Flash, ReasoningMode::High)
.unwrap();
assert_eq!(draft.context_tokens, "123");
draft.select_acceleration(ModelChoice::Glm52).unwrap();
assert!(!draft.ssd_streaming);
draft
.select_acceleration(ModelChoice::DeepSeekV4Flash)
.unwrap();
assert!(draft.ssd_streaming);
}
#[test]
fn runtime_config_updates_after_lock_poisoning() {
let runtime = Arc::new(RwLock::new(Config::default()));