Move preferences from the database to a YAML config file
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,104 +44,60 @@ pub(super) struct PreferenceDraft {
|
||||
}
|
||||
|
||||
impl PreferenceDraft {
|
||||
pub(super) fn from_saved(preferences: &AppPreferences) -> Result<Self, String> {
|
||||
let model = ModelChoice::from_id(&preferences.selected_model)
|
||||
.ok_or_else(|| format!("Unsupported model: {}", preferences.selected_model))?;
|
||||
let generation = preferences.generation()?;
|
||||
let runtime = preferences.runtime()?;
|
||||
runtime.validate(model)?;
|
||||
pub(super) fn from_saved(config: &Config) -> Self {
|
||||
let generation = &config.generation;
|
||||
let runtime = &config.runtime;
|
||||
let execution = &runtime.execution;
|
||||
let speculative = &runtime.speculative;
|
||||
Ok(Self {
|
||||
model,
|
||||
Self {
|
||||
model: config.model,
|
||||
dspark_enabled: speculative.dspark_enabled,
|
||||
idle_timeout_minutes: preferences.idle_timeout_minutes.to_string(),
|
||||
endpoint_port: preferences.endpoint_port.to_string(),
|
||||
endpoint_enabled: preferences.endpoint_enabled,
|
||||
endpoint_cors: preferences.endpoint_cors,
|
||||
idle_timeout_minutes: config.idle_timeout_minutes.to_string(),
|
||||
endpoint_port: config.endpoint.port.to_string(),
|
||||
endpoint_enabled: config.endpoint.enabled,
|
||||
endpoint_cors: config.endpoint.cors,
|
||||
context_tokens: generation.context_tokens.to_string(),
|
||||
max_generated_tokens: generation.max_generated_tokens.to_string(),
|
||||
system_prompt: generation.system_prompt,
|
||||
temperature: generation
|
||||
.temperature
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
top_p: generation
|
||||
.top_p
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
min_p: generation
|
||||
.min_p
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
seed: generation
|
||||
.seed
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
system_prompt: generation.system_prompt.clone(),
|
||||
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: execution
|
||||
.cpu_threads
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
power_percent: execution
|
||||
.power_percent
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
prefill_chunk: execution
|
||||
.prefill_chunk
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
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: speculative
|
||||
.dspark_confidence_threshold
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
dspark_confidence_threshold: optional_string(speculative.dspark_confidence_threshold),
|
||||
dspark_strict: speculative.dspark_strict,
|
||||
ssd_streaming: runtime.ssd.enabled,
|
||||
ssd_streaming_cold: runtime.ssd.cold,
|
||||
ssd_cache: runtime
|
||||
.ssd
|
||||
.cache
|
||||
.map_or_else(String::new, |cache| match cache {
|
||||
StreamingCacheBudget::Experts(experts) => experts.to_string(),
|
||||
StreamingCacheBudget::Gib(gib) => format!("{gib}GB"),
|
||||
}),
|
||||
ssd_full_layers: runtime
|
||||
.ssd
|
||||
.full_layers
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
ssd_preload_experts: runtime
|
||||
.ssd
|
||||
.preload_experts
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
directional_steering_file: runtime.steering.file.unwrap_or_default(),
|
||||
directional_steering_ffn: runtime
|
||||
.steering
|
||||
.ffn_scale
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
directional_steering_attn: runtime
|
||||
.steering
|
||||
.attention_scale
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
simulated_used_memory_gib: runtime
|
||||
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
|
||||
.simulated_used_memory_gib
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
expert_profile_path: runtime.diagnostics.expert_profile_path.unwrap_or_default(),
|
||||
kv_budget_gib: runtime
|
||||
.kv_cache
|
||||
.budget_gib
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
kv_min_tokens: runtime
|
||||
.kv_cache
|
||||
.min_tokens
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
kv_cold_max_tokens: runtime
|
||||
.kv_cache
|
||||
.cold_max_tokens
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
kv_continued_interval_tokens: runtime
|
||||
.kv_cache
|
||||
.continued_interval_tokens
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
})
|
||||
.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> {
|
||||
@@ -163,49 +119,7 @@ impl PreferenceDraft {
|
||||
}
|
||||
|
||||
pub(super) fn reset(&mut self) {
|
||||
let defaults = GenerationPreferences::default();
|
||||
let execution = ExecutionPreferences::default();
|
||||
let speculative = SpeculativePreferences::default();
|
||||
let ssd = SsdPreferences::default();
|
||||
self.model = ModelChoice::default();
|
||||
self.dspark_enabled = false;
|
||||
self.idle_timeout_minutes = "10".into();
|
||||
self.endpoint_port = "4000".into();
|
||||
self.endpoint_enabled = true;
|
||||
self.endpoint_cors = false;
|
||||
self.context_tokens = defaults.context_tokens.to_string();
|
||||
self.max_generated_tokens = defaults.max_generated_tokens.to_string();
|
||||
self.system_prompt = defaults.system_prompt;
|
||||
self.temperature.clear();
|
||||
self.top_p.clear();
|
||||
self.min_p.clear();
|
||||
self.seed.clear();
|
||||
self.reasoning_mode = defaults.reasoning_mode;
|
||||
self.cpu_threads.clear();
|
||||
self.power_percent.clear();
|
||||
self.prefill_chunk.clear();
|
||||
self.quality = execution.quality;
|
||||
self.warm_weights = execution.warm_weights;
|
||||
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.clear();
|
||||
self.dspark_strict = speculative.dspark_strict;
|
||||
self.ssd_streaming = ssd.enabled;
|
||||
self.ssd_streaming_cold = ssd.cold;
|
||||
self.ssd_cache.clear();
|
||||
self.ssd_full_layers.clear();
|
||||
self.ssd_preload_experts.clear();
|
||||
self.directional_steering_file.clear();
|
||||
self.directional_steering_ffn.clear();
|
||||
self.directional_steering_attn.clear();
|
||||
self.simulated_used_memory_gib.clear();
|
||||
self.expert_profile_path.clear();
|
||||
self.kv_budget_gib.clear();
|
||||
self.kv_min_tokens.clear();
|
||||
self.kv_cold_max_tokens.clear();
|
||||
self.kv_continued_interval_tokens.clear();
|
||||
*self = Self::from_saved(&Config::default());
|
||||
}
|
||||
|
||||
pub(super) fn execution(&self) -> Result<ExecutionPreferences, String> {
|
||||
@@ -349,30 +263,7 @@ pub(super) fn parse_streaming_cache(value: &str) -> Result<Option<StreamingCache
|
||||
if value.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
if value.len() > 2
|
||||
&& value
|
||||
.get(value.len() - 2..)
|
||||
.is_some_and(|suffix| suffix.eq_ignore_ascii_case("gb"))
|
||||
{
|
||||
return parse_gib("SSD cache budget", value)
|
||||
.map(|gib| Some(StreamingCacheBudget::Gib(gib)));
|
||||
}
|
||||
if !value.chars().all(|character| character.is_ascii_digit()) {
|
||||
return Err(
|
||||
"SSD cache budget must be a positive expert count or whole GiB value such as 64GB."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
value
|
||||
.parse::<u32>()
|
||||
.ok()
|
||||
.filter(|value| *value > 0)
|
||||
.map(StreamingCacheBudget::Experts)
|
||||
.map(Some)
|
||||
.ok_or_else(|| {
|
||||
"SSD cache budget must be a positive expert count or whole GiB value such as 64GB."
|
||||
.into()
|
||||
})
|
||||
value.parse().map(Some)
|
||||
}
|
||||
|
||||
pub(super) fn parse_optional_gib(name: &str, value: &str) -> Result<Option<u64>, String> {
|
||||
@@ -404,13 +295,15 @@ fn optional_text(value: &str) -> Option<String> {
|
||||
(!value.is_empty()).then(|| value.to_owned())
|
||||
}
|
||||
|
||||
fn update_runtime_preferences(
|
||||
runtime_preferences: &RwLock<AppPreferences>,
|
||||
preferences: &AppPreferences,
|
||||
) {
|
||||
*runtime_preferences
|
||||
/// 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())
|
||||
}
|
||||
|
||||
fn update_runtime_config(runtime_config: &RwLock<Config>, config: &Config) {
|
||||
*runtime_config
|
||||
.write()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner()) = preferences.clone();
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner()) = config.clone();
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -418,14 +311,9 @@ impl App {
|
||||
if self.database.is_none() || self.pending_project_path.is_some() || self.choosing_folder {
|
||||
return;
|
||||
}
|
||||
match PreferenceDraft::from_saved(&self.preferences) {
|
||||
Ok(draft) => {
|
||||
self.preference_draft = draft;
|
||||
self.preference_error = None;
|
||||
self.preferences_open = true;
|
||||
}
|
||||
Err(error) => self.error = Some(error),
|
||||
}
|
||||
self.preference_draft = PreferenceDraft::from_saved(&self.config);
|
||||
self.preference_error = None;
|
||||
self.preferences_open = true;
|
||||
}
|
||||
|
||||
pub(super) fn save_preferences(&mut self) {
|
||||
@@ -438,18 +326,10 @@ impl App {
|
||||
self.preference_error = Some("Idle timeout must be a whole number.".into());
|
||||
return;
|
||||
};
|
||||
if !(1..=1440).contains(&idle_timeout_minutes) {
|
||||
self.preference_error = Some("Idle timeout must be between 1 and 1440 minutes.".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 endpoint_port == 0 {
|
||||
self.preference_error = Some("Endpoint port must be between 1 and 65535.".into());
|
||||
return;
|
||||
}
|
||||
let generation = match self.preference_draft.generation() {
|
||||
Ok(generation) => generation,
|
||||
Err(error) => {
|
||||
@@ -457,8 +337,6 @@ impl App {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let model = self.preference_draft.model;
|
||||
let runtime = match self.preference_draft.runtime() {
|
||||
Ok(runtime) => runtime,
|
||||
Err(error) => {
|
||||
@@ -466,76 +344,72 @@ impl App {
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(error) = runtime.validate(model) {
|
||||
let config = Config {
|
||||
model: self.preference_draft.model,
|
||||
idle_timeout_minutes,
|
||||
endpoint: EndpointConfig {
|
||||
port: i32::from(endpoint_port),
|
||||
enabled: self.preference_draft.endpoint_enabled,
|
||||
cors: self.preference_draft.endpoint_cors,
|
||||
},
|
||||
generation,
|
||||
runtime,
|
||||
interface: self.config.interface.clone(),
|
||||
};
|
||||
if let Err(error) = config.validate() {
|
||||
self.preference_error = Some(error);
|
||||
return;
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
let endpoint_changed = self.preferences.endpoint_port != i32::from(endpoint_port)
|
||||
|| self.preferences.endpoint_enabled != self.preference_draft.endpoint_enabled
|
||||
|| self.preferences.endpoint_cors != self.preference_draft.endpoint_cors;
|
||||
let endpoint_changed = self.config.endpoint != config.endpoint;
|
||||
#[cfg(target_os = "macos")]
|
||||
if endpoint_changed
|
||||
&& self.preferences.endpoint_port == i32::from(endpoint_port)
|
||||
&& self.config.endpoint.port == config.endpoint.port
|
||||
&& self._endpoint.is_some()
|
||||
{
|
||||
self._endpoint = None;
|
||||
}
|
||||
let pending_endpoint = if self.preference_draft.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_preferences),
|
||||
models_path(),
|
||||
application_support_path().join("kv-cache").join("http"),
|
||||
endpoint_port,
|
||||
self.preference_draft.endpoint_cors,
|
||||
Arc::clone(&self.metrics),
|
||||
) {
|
||||
Ok(endpoint) => Some(endpoint),
|
||||
Err(error) => {
|
||||
self.preference_error = Some(error);
|
||||
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
|
||||
};
|
||||
let Some(database) = &mut self.database else {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Err(error) = config.save(&config_path()) {
|
||||
self.preference_error = Some(error);
|
||||
return;
|
||||
};
|
||||
match database.update_preferences(
|
||||
model.id(),
|
||||
idle_timeout_minutes,
|
||||
i32::from(endpoint_port),
|
||||
self.preference_draft.endpoint_enabled,
|
||||
self.preference_draft.endpoint_cors,
|
||||
&generation,
|
||||
&runtime,
|
||||
) {
|
||||
Ok(preferences) => {
|
||||
self.preferences = preferences;
|
||||
#[cfg(target_os = "macos")]
|
||||
update_runtime_preferences(&self.runtime_preferences, &self.preferences);
|
||||
#[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.preferences)
|
||||
.expect("the saved model was selected from the supported catalog");
|
||||
self.preferences_open = false;
|
||||
self.preference_error = None;
|
||||
self.error = None;
|
||||
}
|
||||
Err(error) => self.preference_error = Some(error),
|
||||
}
|
||||
self.config = config;
|
||||
#[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.preferences_open = false;
|
||||
self.preference_error = None;
|
||||
self.error = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,26 +418,30 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn runtime_preferences_update_after_lock_poisoning() {
|
||||
let runtime = Arc::new(RwLock::new(AppPreferences::default()));
|
||||
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 preference lock");
|
||||
panic!("poison configuration lock");
|
||||
})
|
||||
.join();
|
||||
let preferences = AppPreferences {
|
||||
endpoint_port: 4567,
|
||||
..AppPreferences::default()
|
||||
let config = Config {
|
||||
endpoint: EndpointConfig {
|
||||
port: 4567,
|
||||
..EndpointConfig::default()
|
||||
},
|
||||
..Config::default()
|
||||
};
|
||||
|
||||
update_runtime_preferences(&runtime, &preferences);
|
||||
update_runtime_config(&runtime, &config);
|
||||
|
||||
assert_eq!(
|
||||
runtime
|
||||
.read()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.endpoint_port,
|
||||
.endpoint
|
||||
.port,
|
||||
4567
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user