1073 lines
44 KiB
Rust
1073 lines
44 KiB
Rust
use super::*;
|
|
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,
|
|
pub(super) idle_timeout_minutes: String,
|
|
pub(super) a2ui_enabled: bool,
|
|
pub(super) endpoint_port: String,
|
|
pub(super) endpoint_enabled: bool,
|
|
pub(super) endpoint_cors: bool,
|
|
pub(super) dev_brain_enabled: bool,
|
|
pub(super) dev_brain_vault_path: String,
|
|
pub(super) git_diff_layout: GitDiffLayout,
|
|
pub(super) git_diff_algorithm: GitDiffAlgorithm,
|
|
pub(super) git_context_lines: String,
|
|
pub(super) git_interhunk_lines: String,
|
|
pub(super) git_indent_heuristic: bool,
|
|
pub(super) git_whitespace: GitDiffWhitespace,
|
|
pub(super) git_ignore_blank_lines: bool,
|
|
pub(super) context_tokens: String,
|
|
pub(super) max_generated_tokens: String,
|
|
pub(super) system_prompt: text_editor::Content,
|
|
pub(super) temperature: String,
|
|
pub(super) top_p: String,
|
|
pub(super) min_p: String,
|
|
pub(super) seed: String,
|
|
pub(super) cpu_threads: String,
|
|
pub(super) power_percent: String,
|
|
pub(super) prefill_chunk: String,
|
|
pub(super) quality: bool,
|
|
pub(super) warm_weights: bool,
|
|
pub(super) mtp_draft_tokens: String,
|
|
pub(super) mtp_margin: String,
|
|
pub(super) glm_mtp: bool,
|
|
pub(super) glm_mtp_timing: bool,
|
|
pub(super) dspark_confidence_threshold: String,
|
|
pub(super) dspark_strict: bool,
|
|
pub(super) dspark_exact_sampling: bool,
|
|
pub(super) ssd_streaming: bool,
|
|
pub(super) ssd_streaming_cold: bool,
|
|
pub(super) ssd_cache: String,
|
|
pub(super) ssd_full_layers: String,
|
|
pub(super) ssd_preload_experts: String,
|
|
pub(super) directional_steering_file: String,
|
|
pub(super) directional_steering_ffn: String,
|
|
pub(super) directional_steering_attn: String,
|
|
pub(super) simulated_used_memory_gib: String,
|
|
pub(super) expert_profile_path: String,
|
|
pub(super) kv_budget_gib: String,
|
|
pub(super) kv_min_tokens: String,
|
|
pub(super) kv_cold_max_tokens: String,
|
|
pub(super) kv_continued_interval_tokens: String,
|
|
}
|
|
|
|
impl PreferenceDraft {
|
|
pub(super) fn from_saved(config: &Config) -> Self {
|
|
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,
|
|
idle_timeout_minutes: config.idle_timeout_minutes.to_string(),
|
|
a2ui_enabled: config.a2ui_enabled,
|
|
endpoint_port: config.endpoint.port.to_string(),
|
|
endpoint_enabled: config.endpoint.enabled,
|
|
endpoint_cors: config.endpoint.cors,
|
|
dev_brain_enabled: config.dev_brain.enabled,
|
|
dev_brain_vault_path: config.dev_brain.vault_path.clone().unwrap_or_default(),
|
|
git_diff_layout: config.git.diff_layout,
|
|
git_diff_algorithm: config.git.diff_algorithm,
|
|
git_context_lines: config.git.context_lines.to_string(),
|
|
git_interhunk_lines: config.git.interhunk_lines.to_string(),
|
|
git_indent_heuristic: config.git.indent_heuristic,
|
|
git_whitespace: config.git.whitespace,
|
|
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(&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),
|
|
cpu_threads: optional_string(execution.cpu_threads),
|
|
power_percent: optional_string(execution.power_percent),
|
|
prefill_chunk: optional_string(execution.prefill_chunk),
|
|
quality: execution.quality,
|
|
warm_weights: execution.warm_weights,
|
|
mtp_draft_tokens: speculative.mtp_draft_tokens.to_string(),
|
|
mtp_margin: speculative.mtp_margin.to_string(),
|
|
glm_mtp: speculative.glm_mtp,
|
|
glm_mtp_timing: speculative.glm_mtp_timing,
|
|
dspark_confidence_threshold: optional_string(speculative.dspark_confidence_threshold),
|
|
dspark_strict: speculative.dspark_strict,
|
|
dspark_exact_sampling: speculative.dspark_exact_sampling,
|
|
ssd_streaming: runtime.ssd.enabled,
|
|
ssd_streaming_cold: runtime.ssd.cold,
|
|
ssd_cache: optional_string(runtime.ssd.cache),
|
|
ssd_full_layers: optional_string(runtime.ssd.full_layers),
|
|
ssd_preload_experts: optional_string(runtime.ssd.preload_experts),
|
|
directional_steering_file: runtime.steering.file.clone().unwrap_or_default(),
|
|
directional_steering_ffn: optional_string(runtime.steering.ffn_scale),
|
|
directional_steering_attn: optional_string(runtime.steering.attention_scale),
|
|
simulated_used_memory_gib: optional_string(
|
|
runtime.diagnostics.simulated_used_memory_gib,
|
|
),
|
|
expert_profile_path: runtime
|
|
.diagnostics
|
|
.expert_profile_path
|
|
.clone()
|
|
.unwrap_or_default(),
|
|
kv_budget_gib: optional_string(runtime.kv_cache.budget_gib),
|
|
kv_min_tokens: optional_string(runtime.kv_cache.min_tokens),
|
|
kv_cold_max_tokens: optional_string(runtime.kv_cache.cold_max_tokens),
|
|
kv_continued_interval_tokens: optional_string(
|
|
runtime.kv_cache.continued_interval_tokens,
|
|
),
|
|
}
|
|
}
|
|
|
|
pub(super) fn generation(&self) -> Result<GenerationPreferences, String> {
|
|
let preferences = GenerationPreferences {
|
|
context_tokens: parse_positive_i32("Context tokens", &self.context_tokens)?,
|
|
max_generated_tokens: parse_positive_i32(
|
|
"Maximum generated tokens",
|
|
&self.max_generated_tokens,
|
|
)?,
|
|
system_prompt: self.system_prompt.text(),
|
|
temperature: parse_optional_f32("Temperature", &self.temperature)?,
|
|
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.generation_reasoning_mode,
|
|
};
|
|
preferences.validate()?;
|
|
Ok(preferences)
|
|
}
|
|
|
|
pub(super) fn git(&self) -> Result<GitConfig, String> {
|
|
Ok(GitConfig {
|
|
diff_layout: self.git_diff_layout,
|
|
diff_algorithm: self.git_diff_algorithm,
|
|
context_lines: parse_u32("Git diff context lines", &self.git_context_lines)?,
|
|
interhunk_lines: parse_u32("Git diff interhunk lines", &self.git_interhunk_lines)?,
|
|
indent_heuristic: self.git_indent_heuristic,
|
|
whitespace: self.git_whitespace,
|
|
ignore_blank_lines: self.git_ignore_blank_lines,
|
|
})
|
|
}
|
|
|
|
pub(super) fn reset(&mut self) {
|
|
*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)?,
|
|
power_percent: parse_optional_u8("GPU power", &self.power_percent)?,
|
|
prefill_chunk: parse_optional_u32("Prefill chunk", &self.prefill_chunk)?,
|
|
quality: self.quality,
|
|
warm_weights: self.warm_weights,
|
|
})
|
|
}
|
|
|
|
pub(super) fn speculative(&self) -> Result<SpeculativePreferences, String> {
|
|
Ok(SpeculativePreferences {
|
|
mtp_draft_tokens: parse_positive_i32("MTP draft tokens", &self.mtp_draft_tokens)?,
|
|
mtp_margin: parse_f32("MTP margin", &self.mtp_margin)?,
|
|
legacy_mtp_enabled: self.legacy_mtp_enabled,
|
|
glm_mtp: self.glm_mtp,
|
|
glm_mtp_timing: self.glm_mtp_timing,
|
|
dspark_enabled: self.dspark_enabled,
|
|
dspark_confidence_threshold: parse_optional_f32(
|
|
"DSpark confidence",
|
|
&self.dspark_confidence_threshold,
|
|
)?,
|
|
dspark_strict: self.dspark_strict,
|
|
dspark_exact_sampling: self.dspark_exact_sampling,
|
|
})
|
|
}
|
|
|
|
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.dspark_exact_sampling = speculative.dspark_exact_sampling;
|
|
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: SpeculativePreferences::default(),
|
|
ssd: SsdPreferences::default(),
|
|
steering: SteeringPreferences {
|
|
file: optional_text(&self.directional_steering_file),
|
|
ffn_scale: parse_optional_f32(
|
|
"Directional FFN scale",
|
|
&self.directional_steering_ffn,
|
|
)?,
|
|
attention_scale: parse_optional_f32(
|
|
"Directional attention scale",
|
|
&self.directional_steering_attn,
|
|
)?,
|
|
},
|
|
diagnostics: DiagnosticPreferences {
|
|
simulated_used_memory_gib: parse_optional_gib(
|
|
"Simulated used memory",
|
|
&self.simulated_used_memory_gib,
|
|
)?,
|
|
expert_profile_path: optional_text(&self.expert_profile_path),
|
|
},
|
|
kv_cache: KvCachePreferences {
|
|
budget_gib: parse_optional_gib("KV cache budget", &self.kv_budget_gib)?,
|
|
min_tokens: parse_optional_u32("KV cache minimum tokens", &self.kv_min_tokens)?,
|
|
cold_max_tokens: parse_optional_u32(
|
|
"KV cache cold maximum",
|
|
&self.kv_cold_max_tokens,
|
|
)?,
|
|
continued_interval_tokens: parse_optional_u32(
|
|
"KV cache continued interval",
|
|
&self.kv_continued_interval_tokens,
|
|
)?,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
fn parse_positive_i32(name: &str, value: &str) -> Result<i32, String> {
|
|
value
|
|
.trim()
|
|
.parse::<i32>()
|
|
.ok()
|
|
.filter(|value| *value > 0)
|
|
.ok_or_else(|| format!("{name} must be a positive whole number."))
|
|
}
|
|
|
|
fn parse_optional_f32(name: &str, value: &str) -> Result<Option<f32>, String> {
|
|
let value = value.trim();
|
|
if value.is_empty() {
|
|
Ok(None)
|
|
} else {
|
|
value
|
|
.parse()
|
|
.map(Some)
|
|
.map_err(|_| format!("{name} must be a number or left blank for the DS4 default."))
|
|
}
|
|
}
|
|
|
|
fn parse_f32(name: &str, value: &str) -> Result<f32, String> {
|
|
value
|
|
.trim()
|
|
.parse()
|
|
.map_err(|_| format!("{name} must be a number."))
|
|
}
|
|
|
|
fn parse_optional_u64(name: &str, value: &str) -> Result<Option<u64>, String> {
|
|
let value = value.trim();
|
|
if value.is_empty() {
|
|
Ok(None)
|
|
} else {
|
|
value
|
|
.parse()
|
|
.map(Some)
|
|
.map_err(|_| format!("{name} must be a positive whole number or left blank."))
|
|
}
|
|
}
|
|
|
|
fn parse_optional_u32(name: &str, value: &str) -> Result<Option<u32>, String> {
|
|
parse_optional_number(name, value)
|
|
}
|
|
|
|
fn parse_u32(name: &str, value: &str) -> Result<u32, String> {
|
|
value
|
|
.trim()
|
|
.parse()
|
|
.map_err(|_| format!("{name} must be a non-negative whole number."))
|
|
}
|
|
|
|
fn parse_optional_u8(name: &str, value: &str) -> Result<Option<u8>, String> {
|
|
parse_optional_number(name, value)
|
|
}
|
|
|
|
fn parse_optional_number<T: std::str::FromStr>(
|
|
name: &str,
|
|
value: &str,
|
|
) -> Result<Option<T>, String> {
|
|
let value = value.trim();
|
|
if value.is_empty() {
|
|
Ok(None)
|
|
} else {
|
|
value
|
|
.parse()
|
|
.map(Some)
|
|
.map_err(|_| format!("{name} must be a positive whole number or left blank."))
|
|
}
|
|
}
|
|
|
|
pub(super) fn parse_streaming_cache(value: &str) -> Result<Option<StreamingCacheBudget>, String> {
|
|
let value = value.trim();
|
|
if value.is_empty() {
|
|
return Ok(None);
|
|
}
|
|
value.parse().map(Some)
|
|
}
|
|
|
|
pub(super) fn parse_optional_gib(name: &str, value: &str) -> Result<Option<u64>, String> {
|
|
let value = value.trim();
|
|
if value.is_empty() {
|
|
Ok(None)
|
|
} else {
|
|
parse_gib(name, value).map(Some)
|
|
}
|
|
}
|
|
|
|
fn parse_gib(name: &str, value: &str) -> Result<u64, String> {
|
|
let value = value
|
|
.get(value.len().saturating_sub(2)..)
|
|
.filter(|suffix| suffix.eq_ignore_ascii_case("gb"))
|
|
.map_or(value, |_| &value[..value.len() - 2]);
|
|
if !value.chars().all(|character| character.is_ascii_digit()) {
|
|
return Err(format!("{name} must be a positive whole GiB value."));
|
|
}
|
|
value
|
|
.parse::<u64>()
|
|
.ok()
|
|
.filter(|value| *value > 0 && *value <= u64::MAX / GIB)
|
|
.ok_or_else(|| format!("{name} must be a positive whole GiB value."))
|
|
}
|
|
|
|
fn optional_text(value: &str) -> Option<String> {
|
|
let value = value.trim();
|
|
(!value.is_empty()).then(|| value.to_owned())
|
|
}
|
|
|
|
/// An unset preference shows as an empty field.
|
|
fn optional_string<T: ToString>(value: Option<T>) -> String {
|
|
value.map_or_else(String::new, |value| value.to_string())
|
|
}
|
|
|
|
pub(super) fn update_runtime_config(runtime_config: &RwLock<Config>, config: &Config) {
|
|
*runtime_config
|
|
.write()
|
|
.unwrap_or_else(|poisoned| poisoned.into_inner()) = config.clone();
|
|
}
|
|
|
|
impl App {
|
|
pub(super) fn open_preferences(&mut self) -> Task<Message> {
|
|
if let Some(id) = self.preferences_window {
|
|
return window::gain_focus(id);
|
|
}
|
|
if self.database.is_none() || self.pending_project_path.is_some() || self.choosing_folder {
|
|
return Task::none();
|
|
}
|
|
self.preference_draft = PreferenceDraft::from_saved(&self.config);
|
|
self.preference_error = None;
|
|
self.restore_dev_brain_confirmation = false;
|
|
let (id, open) = window::open(window::Settings {
|
|
size: Size::new(920.0, 700.0),
|
|
min_size: Some(Size::new(720.0, 480.0)),
|
|
icon: Some(app_icon()),
|
|
..Default::default()
|
|
});
|
|
self.preferences_window = Some(id);
|
|
open.map(Message::PreferencesOpened)
|
|
}
|
|
|
|
pub(super) fn save_preferences(&mut self) {
|
|
let Ok(idle_timeout_minutes) = self
|
|
.preference_draft
|
|
.idle_timeout_minutes
|
|
.trim()
|
|
.parse::<i32>()
|
|
else {
|
|
self.preference_error = Some("Idle timeout must be a whole number.".into());
|
|
return;
|
|
};
|
|
let Ok(endpoint_port) = self.preference_draft.endpoint_port.trim().parse::<u16>() else {
|
|
self.preference_error = Some("Endpoint port must be a whole number.".into());
|
|
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) => {
|
|
self.preference_error = Some(error);
|
|
return;
|
|
}
|
|
};
|
|
let git = match self.preference_draft.git() {
|
|
Ok(git) => git,
|
|
Err(error) => {
|
|
self.preference_error = Some(error);
|
|
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,
|
|
idle_timeout_minutes,
|
|
a2ui_enabled: self.preference_draft.a2ui_enabled,
|
|
endpoint: EndpointConfig {
|
|
port: i32::from(endpoint_port),
|
|
enabled: self.preference_draft.endpoint_enabled,
|
|
cors: self.preference_draft.endpoint_cors,
|
|
},
|
|
dev_brain: DevBrainConfig {
|
|
enabled: self.preference_draft.dev_brain_enabled,
|
|
vault_path: optional_text(&self.preference_draft.dev_brain_vault_path),
|
|
},
|
|
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(),
|
|
};
|
|
if let Err(error) = config.validate() {
|
|
self.preference_error = Some(error);
|
|
return;
|
|
}
|
|
if config.dev_brain.enabled {
|
|
let projects = self
|
|
.projects
|
|
.iter()
|
|
.map(|project| project.project.clone())
|
|
.collect::<Vec<_>>();
|
|
if let Err(error) = crate::dev_brain::DevBrain::open(&config.dev_brain, &projects) {
|
|
self.preference_error = Some(error);
|
|
return;
|
|
}
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
let dev_brain_changed = self.config.dev_brain != config.dev_brain;
|
|
#[cfg(target_os = "macos")]
|
|
let endpoint_changed = self.config.endpoint != config.endpoint;
|
|
#[cfg(target_os = "macos")]
|
|
if endpoint_changed
|
|
&& self.config.endpoint.port == config.endpoint.port
|
|
&& self._endpoint.is_some()
|
|
{
|
|
self._endpoint = None;
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
let pending_endpoint =
|
|
if config.endpoint.enabled && (endpoint_changed || self._endpoint.is_none()) {
|
|
let Some(generation) = &self.generation_service else {
|
|
self.preference_error = Some("The model runtime is unavailable.".into());
|
|
return;
|
|
};
|
|
match crate::server::ServerHandle::spawn(
|
|
generation.clone(),
|
|
Arc::clone(&self.runtime_config),
|
|
models_path(),
|
|
application_support_path().join("kv-cache").join("http"),
|
|
endpoint_port,
|
|
config.endpoint.cors,
|
|
Arc::clone(&self.metrics),
|
|
) {
|
|
Ok(endpoint) => Some(endpoint),
|
|
Err(error) => {
|
|
self.preference_error = Some(error);
|
|
return;
|
|
}
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
if let Err(error) = config.save(&config_path()) {
|
|
self.preference_error = Some(error);
|
|
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();
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
update_runtime_config(&self.runtime_config, &self.config);
|
|
#[cfg(target_os = "macos")]
|
|
if endpoint_changed {
|
|
self._endpoint = pending_endpoint;
|
|
} else if let Some(endpoint) = pending_endpoint {
|
|
self._endpoint = Some(endpoint);
|
|
}
|
|
self.preference_draft = PreferenceDraft::from_saved(&self.config);
|
|
self.preference_error = None;
|
|
self.error = None;
|
|
}
|
|
}
|
|
|
|
impl App {
|
|
pub(super) fn update_preference_message(
|
|
&mut self,
|
|
message: Message,
|
|
) -> Result<Message, Task<Message>> {
|
|
match message {
|
|
Message::PreferenceModelChanged(model) => {
|
|
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();
|
|
}
|
|
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
|
|
.acceleration_model
|
|
.supports_legacy_mtp()
|
|
&& enabled;
|
|
if self.preference_draft.legacy_mtp_enabled {
|
|
self.preference_draft.dspark_enabled = false;
|
|
self.preference_draft.dspark_confidence_threshold.clear();
|
|
self.preference_draft.dspark_strict = false;
|
|
self.preference_draft.dspark_exact_sampling = false;
|
|
}
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceDsparkChanged(enabled) => {
|
|
self.preference_draft.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;
|
|
self.preference_draft.dspark_exact_sampling = false;
|
|
} else {
|
|
self.preference_draft.legacy_mtp_enabled = false;
|
|
}
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceTimeoutChanged(value) => {
|
|
self.preference_draft.idle_timeout_minutes = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceA2uiChanged(enabled) => {
|
|
self.preference_draft.a2ui_enabled = enabled;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceEndpointPortChanged(value) => {
|
|
self.preference_draft.endpoint_port = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceEndpointEnabledChanged(enabled) => {
|
|
self.preference_draft.endpoint_enabled = enabled;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceEndpointCorsChanged(enabled) => {
|
|
self.preference_draft.endpoint_cors = enabled;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceDevBrainEnabledChanged(enabled) => {
|
|
self.preference_draft.dev_brain_enabled = enabled;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceDevBrainVaultChanged(value) => {
|
|
self.preference_draft.dev_brain_vault_path = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceGitDiffLayoutChanged(layout) => {
|
|
self.preference_draft.git_diff_layout = layout;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceGitDiffAlgorithmChanged(algorithm) => {
|
|
self.preference_draft.git_diff_algorithm = algorithm;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceGitContextLinesChanged(value) => {
|
|
self.preference_draft.git_context_lines = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceGitInterhunkLinesChanged(value) => {
|
|
self.preference_draft.git_interhunk_lines = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceGitIndentHeuristicChanged(enabled) => {
|
|
self.preference_draft.git_indent_heuristic = enabled;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceGitWhitespaceChanged(whitespace) => {
|
|
self.preference_draft.git_whitespace = whitespace;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceGitIgnoreBlankLinesChanged(enabled) => {
|
|
self.preference_draft.git_ignore_blank_lines = enabled;
|
|
self.preference_error = None;
|
|
}
|
|
Message::ChooseDevBrainVault => {
|
|
return Err(Task::perform(
|
|
async {
|
|
AsyncFileDialog::new()
|
|
.set_title("Choose an Obsidian vault")
|
|
.pick_folder()
|
|
.await
|
|
.map(|folder| folder.path().to_path_buf())
|
|
},
|
|
Message::DevBrainVaultPicked,
|
|
));
|
|
}
|
|
Message::DevBrainVaultPicked(path) => {
|
|
if let Some(path) = path {
|
|
self.preference_draft.dev_brain_vault_path =
|
|
path.to_string_lossy().into_owned();
|
|
self.preference_error = None;
|
|
}
|
|
}
|
|
Message::RestoreDevBrainDefaultGuides => {
|
|
self.preference_error = None;
|
|
self.restore_dev_brain_confirmation = true;
|
|
}
|
|
Message::ConfirmRestoreDevBrainDefaultGuides => {
|
|
self.restore_dev_brain_confirmation = false;
|
|
let projects = self
|
|
.projects
|
|
.iter()
|
|
.map(|project| project.project.clone())
|
|
.collect::<Vec<_>>();
|
|
self.preference_error = crate::dev_brain::restore_default_guides(
|
|
Path::new(&self.preference_draft.dev_brain_vault_path),
|
|
&projects,
|
|
)
|
|
.err();
|
|
}
|
|
Message::CancelRestoreDevBrainDefaultGuides => {
|
|
self.restore_dev_brain_confirmation = false;
|
|
}
|
|
Message::PreferenceContextChanged(value) => {
|
|
self.preference_draft.context_tokens = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceMaxTokensChanged(value) => {
|
|
self.preference_draft.max_generated_tokens = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceSystemPromptAction(action) => {
|
|
self.preference_draft.system_prompt.perform(action);
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceTemperatureChanged(value) => {
|
|
self.preference_draft.temperature = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceTopPChanged(value) => {
|
|
self.preference_draft.top_p = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceMinPChanged(value) => {
|
|
self.preference_draft.min_p = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceSeedChanged(value) => {
|
|
self.preference_draft.seed = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceCpuThreadsChanged(value) => {
|
|
self.preference_draft.cpu_threads = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferencePowerChanged(value) => {
|
|
self.preference_draft.power_percent = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferencePrefillChunkChanged(value) => {
|
|
self.preference_draft.prefill_chunk = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceQualityChanged(value) => {
|
|
self.preference_draft.quality = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceWarmWeightsChanged(value) => {
|
|
self.preference_draft.warm_weights = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceMtpDraftChanged(value) => {
|
|
self.preference_draft.mtp_draft_tokens = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceMtpMarginChanged(value) => {
|
|
self.preference_draft.mtp_margin = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceGlmMtpChanged(value) => {
|
|
self.preference_draft.glm_mtp =
|
|
self.preference_draft.acceleration_model == ModelChoice::Glm52 && value;
|
|
if !self.preference_draft.glm_mtp {
|
|
self.preference_draft.glm_mtp_timing = false;
|
|
}
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceGlmMtpTimingChanged(value) => {
|
|
self.preference_draft.glm_mtp_timing =
|
|
self.preference_draft.acceleration_model == ModelChoice::Glm52 && value;
|
|
if self.preference_draft.glm_mtp_timing {
|
|
self.preference_draft.glm_mtp = true;
|
|
}
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceDsparkConfidenceChanged(value) => {
|
|
self.preference_draft.dspark_confidence_threshold = value;
|
|
if self.preference_draft.acceleration_model.supports_dspark()
|
|
&& !self
|
|
.preference_draft
|
|
.dspark_confidence_threshold
|
|
.trim()
|
|
.is_empty()
|
|
{
|
|
self.preference_draft.dspark_enabled = true;
|
|
self.preference_draft.legacy_mtp_enabled = false;
|
|
}
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceDsparkStrictChanged(value) => {
|
|
self.preference_draft.dspark_strict =
|
|
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;
|
|
}
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceDsparkExactSamplingChanged(value) => {
|
|
self.preference_draft.dspark_exact_sampling =
|
|
self.preference_draft.acceleration_model.supports_dspark() && value;
|
|
if self.preference_draft.dspark_exact_sampling {
|
|
self.preference_draft.dspark_enabled = true;
|
|
self.preference_draft.legacy_mtp_enabled = false;
|
|
}
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceSsdChanged(value) => {
|
|
self.preference_draft.ssd_streaming = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceSsdColdChanged(value) => {
|
|
self.preference_draft.ssd_streaming_cold = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceSsdCacheChanged(value) => {
|
|
self.preference_draft.ssd_cache = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceSsdFullLayersChanged(value) => {
|
|
if self.preference_draft.acceleration_model == ModelChoice::Glm52 {
|
|
self.preference_draft.ssd_full_layers = value;
|
|
}
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceSsdPreloadChanged(value) => {
|
|
self.preference_draft.ssd_preload_experts = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceSteeringFileChanged(value) => {
|
|
if self.preference_draft.model != ModelChoice::Glm52 {
|
|
self.preference_draft.directional_steering_file = value;
|
|
}
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceSteeringFfnChanged(value) => {
|
|
if self.preference_draft.model != ModelChoice::Glm52 {
|
|
self.preference_draft.directional_steering_ffn = value;
|
|
}
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceSteeringAttnChanged(value) => {
|
|
if self.preference_draft.model != ModelChoice::Glm52 {
|
|
self.preference_draft.directional_steering_attn = value;
|
|
}
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceSimulatedMemoryChanged(value) => {
|
|
self.preference_draft.simulated_used_memory_gib = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceExpertProfileChanged(value) => {
|
|
self.preference_draft.expert_profile_path = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceKvBudgetChanged(value) => {
|
|
self.preference_draft.kv_budget_gib = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceKvMinTokensChanged(value) => {
|
|
self.preference_draft.kv_min_tokens = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceKvColdMaxChanged(value) => {
|
|
self.preference_draft.kv_cold_max_tokens = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::PreferenceKvContinuedIntervalChanged(value) => {
|
|
self.preference_draft.kv_continued_interval_tokens = value;
|
|
self.preference_error = None;
|
|
}
|
|
Message::ResetPreferences => {
|
|
self.preference_draft.reset();
|
|
self.preference_error = None;
|
|
}
|
|
Message::SavePreferences => {
|
|
self.save_preferences();
|
|
if self.preference_error.is_none()
|
|
&& let Some(id) = self.preferences_window
|
|
{
|
|
return Err(window::close(id));
|
|
}
|
|
}
|
|
message => return Ok(message),
|
|
}
|
|
Err(Task::none())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn multiline_system_prompt_round_trips_through_the_editor() {
|
|
let draft = PreferenceDraft::from_saved(&Config::default());
|
|
|
|
assert_eq!(
|
|
draft.generation().unwrap().system_prompt,
|
|
crate::settings::DEFAULT_SYSTEM_PROMPT
|
|
);
|
|
}
|
|
|
|
#[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::DeepSeekV4Flash0731, ReasoningMode::High)
|
|
.unwrap();
|
|
assert_eq!(draft.context_tokens, "123");
|
|
|
|
draft.select_acceleration(ModelChoice::Glm52).unwrap();
|
|
assert!(!draft.ssd_streaming);
|
|
draft
|
|
.select_acceleration(ModelChoice::DeepSeekV4Flash0731)
|
|
.unwrap();
|
|
assert!(draft.ssd_streaming);
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_config_updates_after_lock_poisoning() {
|
|
let runtime = Arc::new(RwLock::new(Config::default()));
|
|
let poisoned = Arc::clone(&runtime);
|
|
let _ = std::thread::spawn(move || {
|
|
let _guard = poisoned.write().unwrap();
|
|
panic!("poison configuration lock");
|
|
})
|
|
.join();
|
|
let config = Config {
|
|
endpoint: EndpointConfig {
|
|
port: 4567,
|
|
..EndpointConfig::default()
|
|
},
|
|
..Config::default()
|
|
};
|
|
|
|
update_runtime_config(&runtime, &config);
|
|
|
|
assert_eq!(
|
|
runtime
|
|
.read()
|
|
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
.endpoint
|
|
.port,
|
|
4567
|
|
);
|
|
}
|
|
}
|