Move preferences from the database to a YAML config file

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Georg Bauer
2026-07-26 09:50:39 +02:00
parent 3f2c42513f
commit 5d441038bc
19 changed files with 571 additions and 964 deletions

26
Cargo.lock generated
View File

@@ -1152,6 +1152,7 @@ dependencies = [
"rfd",
"serde",
"serde_json",
"serde_norway",
"sha2",
"ureq",
]
@@ -3957,6 +3958,12 @@ dependencies = [
"unicode-script",
]
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "same-file"
version = "1.0.6"
@@ -4047,6 +4054,19 @@ dependencies = [
"zmij",
]
[[package]]
name = "serde_norway"
version = "0.9.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e408f29489b5fd500fab51ff1484fc859bb655f32c671f307dcd733b72e8168c"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml-norway",
]
[[package]]
name = "serde_repr"
version = "0.1.21"
@@ -4867,6 +4887,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unsafe-libyaml-norway"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39abd59bf32521c7f2301b52d05a6a2c975b6003521cbd0c6dc1582f0a22104"
[[package]]
name = "untrusted"
version = "0.9.0"

View File

@@ -20,6 +20,7 @@ png = "0.17.16"
rfd = "0.15.4"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = { version = "1.0.149", features = ["preserve_order", "raw_value"] }
serde_norway = "0.9.42"
sha2 = "0.11.0"
ureq = { version = "3.3.0", default-features = false, features = ["rustls"] }

View File

@@ -12,9 +12,10 @@ notices are retained in [`native/metal/LICENSE`](native/metal/LICENSE) and
The current milestone provides a Codex-inspired project/session layout. A native
macOS folder picker selects each workspace, then the app asks for its display
name. Projects, sessions, and model preferences are persisted through Diesel in
SQLite. Open Preferences with `Command-,` to configure model, generation,
runtime, local endpoint, and idle-unload settings. The separate Model Manager
name. Projects and sessions are persisted through Diesel in SQLite, while
settings live in a YAML file beside it that lists only the values you changed
and can be edited by hand. Open Preferences with `Command-,` to configure model,
generation, runtime, local endpoint, and idle-unload settings. The separate Model Manager
(`Shift-Command-M`)
lists local main and DSpark artifacts, their on-disk sizes and state, and lets
you download, resume, validate, or delete them. Rust-native background work
@@ -57,6 +58,7 @@ State is stored at:
```text
~/Library/Application Support/DS4Server.rfc1437.de/data.sqlite3
~/Library/Application Support/DS4Server.rfc1437.de/config.yaml
~/Library/Application Support/DS4Server.rfc1437.de/kv-cache/
```

View File

@@ -0,0 +1 @@
-- Preferences live in config.yaml; the table is not recreated.

View File

@@ -0,0 +1 @@
DROP TABLE preferences;

View File

@@ -12,7 +12,8 @@ use preferences::PreferenceDraft;
#[cfg(test)]
use preferences::{parse_optional_gib, parse_streaming_cache};
use crate::database::{AppPreferences, Database, ProjectWithSessions, SessionState, StoredMessage};
use crate::config::{Config, EndpointConfig};
use crate::database::{Database, ProjectWithSessions, SessionState, StoredMessage};
#[cfg(target_os = "macos")]
use crate::engine::ChatTurn;
use crate::metrics::{KvCacheReport, Metrics, MetricsSnapshot};
@@ -20,9 +21,9 @@ use crate::model::{self, DownloadOutcome, DownloadProgress, ManagedArtifactId, M
#[cfg(target_os = "macos")]
use crate::runtime::{ActiveGeneration, CheckpointTarget, GenerationEvent, GenerationService};
use crate::settings::{
DEFAULT_KV_BUDGET_GIB, DiagnosticPreferences, ExecutionPreferences, GIB, GenerationPreferences,
KvCachePreferences, ReasoningMode, RuntimePreferences, SpeculativePreferences, SsdPreferences,
SteeringPreferences, StreamingCacheBudget,
DiagnosticPreferences, ExecutionPreferences, GIB, GenerationPreferences, KvCachePreferences,
ReasoningMode, RuntimePreferences, SpeculativePreferences, SsdPreferences, SteeringPreferences,
StreamingCacheBudget,
};
use iced::widget::{markdown, scrollable, text_input};
use iced::{Size, Subscription, Task, keyboard, mouse, window};
@@ -53,7 +54,7 @@ pub(crate) struct App {
pub(super) native_edit_commands: crate::native_edit::EditCommandQueue,
database: Option<Database>,
projects: Vec<ProjectWithSessions>,
preferences: AppPreferences,
config: Config,
preference_draft: PreferenceDraft,
preferences_open: bool,
preference_error: Option<String>,
@@ -99,7 +100,7 @@ pub(crate) struct App {
#[cfg(target_os = "macos")]
active_titling: Option<generation::TitleRequest>,
#[cfg(target_os = "macos")]
runtime_preferences: Arc<RwLock<AppPreferences>>,
runtime_config: Arc<RwLock<Config>>,
#[cfg(target_os = "macos")]
_endpoint: Option<crate::server::ServerHandle>,
error: Option<String>,
@@ -217,22 +218,24 @@ pub(crate) enum Message {
impl App {
pub(crate) fn load(main_window: window::Id) -> Self {
let config = match Config::load(&config_path()) {
Ok(config) => config,
Err(error) => return Self::failed(error, main_window),
};
let path = application_support_path().join("data.sqlite3");
match Database::open(&path) {
Ok(mut database) => match (database.load_projects(), database.load_preferences()) {
(Ok(projects), Ok(preferences)) => {
let preference_draft = match PreferenceDraft::from_saved(&preferences) {
Ok(draft) => draft,
Err(error) => return Self::failed(error, main_window),
};
Ok(mut database) => match database.load_projects() {
Ok(projects) => {
let preference_draft = PreferenceDraft::from_saved(&config);
// Before the counters are taken, so they measure what is
// actually reachable.
sweep_orphan_checkpoints(&kv_cache_path(), &projects);
#[cfg(target_os = "macos")]
crate::engine::sweep_transient_cache(&transient_cache_path());
let context_limit = preferences.context_tokens.max(0) as u32;
let context_limit = config.generation.context_tokens.max(0) as u32;
// Reopen on the project we left, with a fresh draft chat.
let last_project = preferences
let last_project = config
.interface
.last_project_id
.filter(|id| projects.iter().any(|item| item.project.id == *id));
let drafts = last_project
@@ -243,8 +246,8 @@ impl App {
Arc::new(Metrics::new(&application_support_path().join("kv-cache")));
let metrics_snapshot = metrics.snapshot();
#[cfg(target_os = "macos")]
let (runtime_preferences, generation_service, endpoint, service_error) =
spawn_services(&preferences, Arc::clone(&metrics));
let (runtime_config, generation_service, endpoint, service_error) =
spawn_services(&config, Arc::clone(&metrics));
Self {
main_window,
model_manager_window: None,
@@ -255,7 +258,7 @@ impl App {
native_edit_commands: crate::native_edit::command_queue(),
database: Some(database),
projects,
preferences,
config,
preference_draft,
preferences_open: false,
preference_error: None,
@@ -294,7 +297,7 @@ impl App {
#[cfg(target_os = "macos")]
active_titling: None,
#[cfg(target_os = "macos")]
runtime_preferences,
runtime_config,
#[cfg(target_os = "macos")]
_endpoint: endpoint,
error: {
@@ -309,22 +312,27 @@ impl App {
},
}
}
(Err(error), _) | (_, Err(error)) => Self::failed(error, main_window),
Err(error) => Self::failed(
format!("Could not open the project database: {error}"),
main_window,
),
},
Err(error) => Self::failed(error, main_window),
Err(error) => Self::failed(
format!("Could not open the project database: {error}"),
main_window,
),
}
}
fn failed(error: String, main_window: window::Id) -> Self {
let preferences = AppPreferences::default();
let preference_draft = PreferenceDraft::from_saved(&preferences)
.expect("default preferences must use a supported model");
let context_limit = preferences.context_tokens.max(0) as u32;
let config = Config::default();
let preference_draft = PreferenceDraft::from_saved(&config);
let context_limit = config.generation.context_tokens.max(0) as u32;
let metrics = Arc::new(Metrics::new(&application_support_path().join("kv-cache")));
let metrics_snapshot = metrics.snapshot();
#[cfg(target_os = "macos")]
let (runtime_preferences, generation_service, endpoint, service_error) =
spawn_services(&preferences, Arc::clone(&metrics));
let (runtime_config, generation_service, endpoint, service_error) =
spawn_services(&config, Arc::clone(&metrics));
#[cfg(target_os = "macos")]
let startup_error = service_error;
#[cfg(not(target_os = "macos"))]
@@ -339,7 +347,7 @@ impl App {
native_edit_commands: crate::native_edit::command_queue(),
database: None,
projects: Vec::new(),
preferences,
config,
preference_draft,
preferences_open: false,
preference_error: None,
@@ -378,14 +386,12 @@ impl App {
#[cfg(target_os = "macos")]
active_titling: None,
#[cfg(target_os = "macos")]
runtime_preferences,
runtime_config,
#[cfg(target_os = "macos")]
_endpoint: endpoint,
error: Some(match startup_error {
Some(service_error) => {
format!("Could not open the project database: {error}. {service_error}")
}
None => format!("Could not open the project database: {error}"),
Some(service_error) => format!("{error} {service_error}"),
None => error,
}),
}
}
@@ -453,29 +459,19 @@ impl App {
Message::StartSidebarDrag => self.sidebar_drag = true,
Message::DragSidebar(x) => {
if self.sidebar_drag {
self.preferences.sidebar_width =
self.config.interface.sidebar_width =
(x.round() as i32).clamp(MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH);
}
}
Message::EndSidebarDrag => {
if self.sidebar_drag {
self.sidebar_drag = false;
if let Some(database) = self.database.as_mut()
&& let Err(error) =
database.set_sidebar_width(self.preferences.sidebar_width)
{
self.error = Some(error);
}
self.store_config();
}
}
Message::ToggleSidebar => {
let collapsed = !self.preferences.sidebar_collapsed;
self.preferences.sidebar_collapsed = collapsed;
if let Some(database) = self.database.as_mut()
&& let Err(error) = database.set_sidebar_collapsed(collapsed)
{
self.error = Some(error);
}
self.config.interface.sidebar_collapsed = !self.config.interface.sidebar_collapsed;
self.store_config();
}
Message::MetricsTick => self.sample_metrics(),
Message::PreferenceModelChanged(model) => {
@@ -829,9 +825,9 @@ impl App {
let _ = fs::remove_file(session_checkpoint_path(session_id));
}
self.drafts.remove(&project_id);
if self.preferences.last_project_id == Some(project_id) {
self.preferences.last_project_id = None;
let _ = database.set_last_project(None);
if self.config.interface.last_project_id == Some(project_id) {
self.config.interface.last_project_id = None;
let _ = self.config.save(&config_path());
}
if self.selected_project == Some(project_id) {
self.selected_project = None;
@@ -957,7 +953,7 @@ impl App {
self.context_limit = if limit > 0 {
limit as u32
} else {
self.preferences.context_tokens.max(0) as u32
self.config.generation.context_tokens.max(0) as u32
};
self.tokens_per_second = tokens_per_second;
self.error = None;
@@ -1128,6 +1124,13 @@ impl App {
self.finish_cache_change();
}
/// Writes the configuration file, surfacing a failure in the error banner.
fn store_config(&mut self) {
if let Err(error) = self.config.save(&config_path()) {
self.error = Some(error);
}
}
fn finish_cache_change(&mut self) {
self.metrics.rescan_cache(&kv_cache_path());
self.metrics_snapshot = self.metrics.snapshot();
@@ -1135,12 +1138,7 @@ impl App {
}
fn scan_kv_cache(&mut self) {
let budget = self
.preferences
.runtime()
.map_or(DEFAULT_KV_BUDGET_GIB * GIB, |runtime| {
runtime.kv_cache.settings().budget_bytes
});
let budget = self.config.runtime.kv_cache.settings().budget_bytes;
self.kv_cache_report = crate::metrics::kv_cache_report(&kv_cache_path(), budget);
self.last_cache_scan = Instant::now();
}
@@ -1148,34 +1146,34 @@ impl App {
#[cfg(target_os = "macos")]
fn spawn_services(
preferences: &AppPreferences,
config: &Config,
metrics: Arc<Metrics>,
) -> (
Arc<RwLock<AppPreferences>>,
Arc<RwLock<Config>>,
Option<GenerationService>,
Option<crate::server::ServerHandle>,
Option<String>,
) {
let runtime_preferences = Arc::new(RwLock::new(preferences.clone()));
let runtime_config = Arc::new(RwLock::new(config.clone()));
let generation = match GenerationService::spawn(Arc::clone(&metrics)) {
Ok(generation) => generation,
Err(error) => return (runtime_preferences, None, None, Some(error)),
Err(error) => return (runtime_config, None, None, Some(error)),
};
let endpoint = preferences.endpoint_enabled.then(|| {
let endpoint = config.endpoint.enabled.then(|| {
crate::server::ServerHandle::spawn(
generation.clone(),
Arc::clone(&runtime_preferences),
Arc::clone(&runtime_config),
models_path(),
transient_cache_path(),
u16::try_from(preferences.endpoint_port).unwrap_or(4000),
preferences.endpoint_cors,
u16::try_from(config.endpoint.port).unwrap_or(4000),
config.endpoint.cors,
metrics,
)
});
match endpoint {
Some(Ok(endpoint)) => (runtime_preferences, Some(generation), Some(endpoint), None),
Some(Err(error)) => (runtime_preferences, Some(generation), None, Some(error)),
None => (runtime_preferences, Some(generation), None, None),
Some(Ok(endpoint)) => (runtime_config, Some(generation), Some(endpoint), None),
Some(Err(error)) => (runtime_config, Some(generation), None, Some(error)),
None => (runtime_config, Some(generation), None, None),
}
}
@@ -1219,6 +1217,11 @@ fn models_path() -> PathBuf {
application_support_path().join("models")
}
/// The settings file, beside the project database.
fn config_path() -> PathBuf {
application_support_path().join("config.yaml")
}
pub(super) fn chat_scroll_id() -> scrollable::Id {
scrollable::Id::new("chat-transcript")
}

View File

@@ -79,18 +79,13 @@ impl App {
if prompt.is_empty() {
return;
}
let model = match ModelChoice::from_id(&self.preferences.selected_model) {
Some(model) => model,
None => {
self.error = Some("The selected model is not supported.".into());
return;
}
};
let effective = self.preferences.generation().and_then(|generation| {
self.preferences.runtime().and_then(|runtime| {
crate::settings::effective_settings(model, &generation, &runtime, &models_path())
})
});
let model = self.config.model;
let effective = crate::settings::effective_settings(
model,
&self.config.generation,
&self.config.runtime,
&models_path(),
);
let mut effective = match effective {
Ok(settings) => settings,
Err(error) => {
@@ -158,7 +153,7 @@ impl App {
}
};
let idle_timeout =
Duration::from_secs(self.preferences.idle_timeout_minutes.max(1) as u64 * 60);
Duration::from_secs(self.config.idle_timeout_minutes.max(1) as u64 * 60);
self.active_generation = match service.generate(
effective.engine,
effective.turn,
@@ -255,8 +250,7 @@ impl App {
Ok(GenerationEvent::Finished(result)) => {
match result {
Ok(_) => {
let model = ModelChoice::from_id(&self.preferences.selected_model)
.unwrap_or_default();
let model = self.config.model;
let content = self
.conversation
.last()
@@ -356,7 +350,7 @@ impl App {
.find(|project| project.project.id == project_id)
.map(|project| PathBuf::from(&project.project.path))
.ok_or_else(|| "The active project is unavailable.".to_owned())?;
let tools = crate::agent::Tools::new(&root, self.preferences.context_tokens)?;
let tools = crate::agent::Tools::new(&root, self.config.generation.context_tokens)?;
self.agent_tools = Some((session_id, Arc::new(Mutex::new(tools))));
}
let tools = Arc::clone(&self.agent_tools.as_ref().unwrap().1);
@@ -369,12 +363,13 @@ impl App {
let session_id = self
.selected_session
.ok_or_else(|| "The active session is unavailable.".to_owned())?;
let model = ModelChoice::from_id(&self.preferences.selected_model)
.ok_or_else(|| "The selected model is not supported.".to_owned())?;
let generation = self.preferences.generation()?;
let runtime = self.preferences.runtime()?;
let mut effective =
crate::settings::effective_settings(model, &generation, &runtime, &models_path())?;
let model = self.config.model;
let mut effective = crate::settings::effective_settings(
model,
&self.config.generation,
&self.config.runtime,
&models_path(),
)?;
effective.turn.system_prompt =
crate::agent::system_prompt(model, &effective.turn.system_prompt);
let assistant_reasoning = effective.turn.reasoning_mode != ReasoningMode::Direct;
@@ -400,8 +395,7 @@ impl App {
let mut assistant = ChatMessage::from(saved.1);
assistant.reasoning_open = assistant_reasoning;
self.conversation.push(assistant);
let idle_timeout =
Duration::from_secs(self.preferences.idle_timeout_minutes.max(1) as u64 * 60);
let idle_timeout = Duration::from_secs(self.config.idle_timeout_minutes.max(1) as u64 * 60);
self.active_generation = Some(
self.generation_service
.as_ref()
@@ -428,13 +422,13 @@ impl App {
if self.active_titling.is_some() {
return Err("A title is already being generated.".into());
}
let model = ModelChoice::from_id(&self.preferences.selected_model)
.ok_or_else(|| "The selected model is not supported.".to_owned())?;
let mut effective = self.preferences.generation().and_then(|generation| {
self.preferences.runtime().and_then(|runtime| {
crate::settings::effective_settings(model, &generation, &runtime, &models_path())
})
})?;
let model = self.config.model;
let mut effective = crate::settings::effective_settings(
model,
&self.config.generation,
&self.config.runtime,
&models_path(),
)?;
let database = self
.database
.as_mut()
@@ -475,8 +469,7 @@ impl App {
.generation_service
.as_ref()
.ok_or_else(|| "The model runtime is unavailable.".to_owned())?;
let idle_timeout =
Duration::from_secs(self.preferences.idle_timeout_minutes.max(1) as u64 * 60);
let idle_timeout = Duration::from_secs(self.config.idle_timeout_minutes.max(1) as u64 * 60);
match service.generate(
effective.engine,
effective.turn,

View File

@@ -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
);
}

View File

@@ -75,7 +75,7 @@ impl App {
self.conversation.clear();
self.composer.clear();
self.context_used = 0;
self.context_limit = self.preferences.context_tokens.max(0) as u32;
self.context_limit = self.config.generation.context_tokens.max(0) as u32;
self.tokens_per_second = None;
self.error = None;
}
@@ -113,15 +113,11 @@ impl App {
/// Selects a project and stores it as the one to reopen on the next launch.
pub(super) fn remember_project(&mut self, project_id: i32) {
self.selected_project = Some(project_id);
if self.preferences.last_project_id == Some(project_id) {
if self.config.interface.last_project_id == Some(project_id) {
return;
}
self.preferences.last_project_id = Some(project_id);
if let Some(database) = &mut self.database
&& let Err(error) = database.set_last_project(Some(project_id))
{
self.error = Some(error);
}
self.config.interface.last_project_id = Some(project_id);
self.store_config();
}
/// True when the sidebar row for this project's draft is the active chat.

View File

@@ -64,7 +64,7 @@ impl App {
fn main_view(&self) -> Element<'_, Message> {
let mut body = row![].width(Length::Fill).height(Length::Fill);
if !self.preferences.sidebar_collapsed {
if !self.config.interface.sidebar_collapsed {
body = body.push(self.sidebar());
body = body.push(
mouse_area(
@@ -272,7 +272,8 @@ impl App {
.spacing(10),
)
.width(
self.preferences
self.config
.interface
.sidebar_width
.clamp(MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH) as f32,
)
@@ -327,10 +328,11 @@ impl App {
.style(button::text);
// Center the tabs over the detail area, not the whole window, so the
// sidebar's width (plus its divider) shifts them along.
let detail_offset = if self.preferences.sidebar_collapsed {
let detail_offset = if self.config.interface.sidebar_collapsed {
0.0
} else {
self.preferences
self.config
.interface
.sidebar_width
.clamp(MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH) as f32
+ 5.0

View File

@@ -115,11 +115,10 @@ impl App {
} else if active && message.reasoning.is_none() {
body = body.push(text("Loading model…").size(14));
}
if !message.user
&& !message.tool
&& let Some(model) = ModelChoice::from_id(&self.preferences.selected_model)
{
for summary in crate::agent::tool_summaries(model, &message.content) {
if !message.user && !message.tool {
for summary in
crate::agent::tool_summaries(self.config.model, &message.content)
{
body = body.push(text(summary).size(13).color(muted_text()));
}
}
@@ -185,12 +184,7 @@ impl App {
.color(muted_text()),
Space::with_width(Length::Fill),
icon(ICON_MODEL, 16),
text(
ModelChoice::from_id(&self.preferences.selected_model)
.unwrap_or_default()
.to_string(),
)
.size(12),
text(self.config.model.to_string()).size(12),
action,
]
.spacing(6)

199
src/config.rs Normal file
View File

@@ -0,0 +1,199 @@
use serde::{Deserialize, Serialize};
use serde_norway::{Mapping, Value};
use std::fs;
use std::path::Path;
use crate::model::ModelChoice;
use crate::settings::{GenerationPreferences, RuntimePreferences};
/// Settings the application persists beside its database, as a YAML file users
/// and agents can edit by hand. Only values that differ from the defaults are
/// written; anything missing falls back to the default.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct Config {
pub model: ModelChoice,
pub idle_timeout_minutes: i32,
pub endpoint: EndpointConfig,
pub generation: GenerationPreferences,
pub runtime: RuntimePreferences,
pub interface: InterfaceConfig,
}
impl Default for Config {
fn default() -> Self {
Self {
model: ModelChoice::default(),
idle_timeout_minutes: 10,
endpoint: EndpointConfig::default(),
generation: GenerationPreferences::default(),
runtime: RuntimePreferences::default(),
interface: InterfaceConfig::default(),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct EndpointConfig {
pub port: i32,
pub enabled: bool,
pub cors: bool,
}
impl Default for EndpointConfig {
fn default() -> Self {
Self {
port: 4000,
enabled: true,
cors: false,
}
}
}
/// Window state the application restores on the next launch.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct InterfaceConfig {
pub sidebar_collapsed: bool,
pub sidebar_width: i32,
/// Project the app reopens on. Cleared when that project goes away.
pub last_project_id: Option<i32>,
}
impl Default for InterfaceConfig {
fn default() -> Self {
Self {
sidebar_collapsed: false,
sidebar_width: 276,
last_project_id: None,
}
}
}
impl Config {
/// A missing file is the default configuration; a malformed one is an error,
/// so a typo never silently resets everything.
pub fn load(path: &Path) -> Result<Self, String> {
let text = match fs::read_to_string(path) {
Ok(text) => text,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return Ok(Self::default());
}
Err(error) => return Err(format!("Could not read {}: {error}", path.display())),
};
let config: Self = serde_norway::from_str(&text)
.map_err(|error| format!("Could not read {}: {error}", path.display()))?;
config.validate()?;
Ok(config)
}
pub fn save(&self, path: &Path) -> Result<(), String> {
self.validate()?;
let value = serde_norway::to_value(self).map_err(|error| error.to_string())?;
let defaults =
serde_norway::to_value(Self::default()).map_err(|error| error.to_string())?;
let value = without_defaults(value, &defaults)
.unwrap_or_else(|| Value::Mapping(Mapping::default()));
let text = serde_norway::to_string(&value).map_err(|error| error.to_string())?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|error| error.to_string())?;
}
fs::write(path, text)
.map_err(|error| format!("Could not write {}: {error}", path.display()))
}
pub fn validate(&self) -> Result<(), String> {
if !(1..=1440).contains(&self.idle_timeout_minutes) {
return Err("Idle timeout must be between 1 and 1440 minutes.".into());
}
if !(1..=65_535).contains(&self.endpoint.port) {
return Err("Endpoint port must be between 1 and 65535.".into());
}
self.generation.validate()?;
self.runtime.validate(self.model)
}
}
/// Drops every value that still matches the default, so the file lists only what
/// the user actually set. Mappings that end up empty disappear with their key.
fn without_defaults(value: Value, defaults: &Value) -> Option<Value> {
if let (Value::Mapping(entries), Value::Mapping(defaults)) = (&value, defaults) {
let kept = entries
.iter()
.filter_map(|(key, entry)| {
let default = defaults.get(key)?;
Some((key.clone(), without_defaults(entry.clone(), default)?))
})
.collect::<Mapping>();
return (!kept.is_empty()).then_some(Value::Mapping(kept));
}
(&value != defaults).then_some(value)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::settings::{ReasoningMode, SsdPreferences, StreamingCacheBudget};
#[test]
fn defaults_write_an_empty_file_and_reload_unchanged() {
let directory = std::env::temp_dir().join(format!("ds4-config-{}", std::process::id()));
let path = directory.join("config.yaml");
Config::default().save(&path).unwrap();
assert_eq!(fs::read_to_string(&path).unwrap().trim(), "{}");
assert_eq!(Config::load(&path).unwrap(), Config::default());
fs::remove_dir_all(&directory).unwrap();
}
#[test]
fn only_changed_values_are_written_and_read_back() {
let directory = std::env::temp_dir().join(format!("ds4-config-set-{}", std::process::id()));
let path = directory.join("config.yaml");
let config = Config {
model: ModelChoice::Glm52,
generation: GenerationPreferences {
context_tokens: 65_536,
reasoning_mode: ReasoningMode::Direct,
..GenerationPreferences::default()
},
runtime: RuntimePreferences {
ssd: SsdPreferences {
enabled: true,
cache: Some(StreamingCacheBudget::Gib(64)),
..SsdPreferences::default()
},
..RuntimePreferences::default()
},
..Config::default()
};
config.save(&path).unwrap();
let text = fs::read_to_string(&path).unwrap();
assert_eq!(
text,
"model: glm-5.2\n\
generation:\n context_tokens: 65536\n reasoning_mode: none\n\
runtime:\n ssd:\n enabled: true\n cache: 64GB\n"
);
assert_eq!(Config::load(&path).unwrap(), config);
fs::remove_dir_all(&directory).unwrap();
}
#[test]
fn invalid_files_are_reported_instead_of_reset() {
let directory = std::env::temp_dir().join(format!("ds4-config-bad-{}", std::process::id()));
let path = directory.join("config.yaml");
fs::create_dir_all(&directory).unwrap();
fs::write(&path, "idle_timeout_minutes: 0\n").unwrap();
assert!(Config::load(&path).is_err());
fs::write(&path, "nonsense: 1\n").unwrap();
assert!(Config::load(&path).is_err());
fs::remove_dir_all(&directory).unwrap();
// A missing file is simply the defaults.
assert_eq!(Config::load(&path).unwrap(), Config::default());
}
}

View File

@@ -4,288 +4,10 @@ use std::collections::HashMap;
use std::fs;
use std::path::Path;
use crate::schema::{messages, preferences, projects, sessions};
use crate::settings::{
DiagnosticPreferences, ExecutionPreferences, GenerationPreferences, KvCachePreferences,
ReasoningMode, RuntimePreferences, SpeculativePreferences, SsdPreferences, SteeringPreferences,
StreamingCacheBudget,
};
use crate::schema::{messages, projects, sessions};
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
#[derive(Clone, Debug, Identifiable, Queryable, Selectable)]
#[diesel(table_name = preferences)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct AppPreferences {
pub id: i32,
pub selected_model: String,
pub dspark_enabled: bool,
pub idle_timeout_minutes: i32,
pub context_tokens: i32,
pub max_generated_tokens: i32,
pub system_prompt: String,
pub temperature: Option<f32>,
pub top_p: Option<f32>,
pub min_p: Option<f32>,
pub seed: Option<String>,
pub reasoning_mode: String,
pub cpu_threads: Option<i32>,
pub power_percent: Option<i32>,
pub prefill_chunk: Option<i32>,
pub quality: bool,
pub warm_weights: bool,
pub mtp_draft_tokens: i32,
pub mtp_margin: f32,
pub glm_mtp: bool,
pub glm_mtp_timing: bool,
pub dspark_confidence_threshold: Option<f32>,
pub dspark_strict: bool,
pub ssd_streaming: bool,
pub ssd_streaming_cold: bool,
pub ssd_cache_experts: Option<i64>,
pub ssd_cache_gib: Option<i64>,
pub ssd_full_layers: Option<i32>,
pub ssd_preload_experts: Option<i32>,
pub directional_steering_file: Option<String>,
pub directional_steering_ffn: Option<f32>,
pub directional_steering_attn: Option<f32>,
pub simulated_used_memory_gib: Option<i64>,
pub expert_profile_path: Option<String>,
pub endpoint_port: i32,
pub endpoint_enabled: bool,
pub endpoint_cors: bool,
pub sidebar_collapsed: bool,
/// Project the app reopens on. Cleared when that project goes away.
pub last_project_id: Option<i32>,
pub sidebar_width: i32,
pub kv_budget_gib: Option<i64>,
pub kv_min_tokens: Option<i32>,
pub kv_cold_max_tokens: Option<i32>,
pub kv_continued_interval_tokens: Option<i32>,
}
impl Default for AppPreferences {
fn default() -> Self {
Self {
id: 1,
selected_model: "deepseek-v4-flash".into(),
dspark_enabled: false,
idle_timeout_minutes: 10,
context_tokens: 32_768,
max_generated_tokens: 50_000,
system_prompt: "You are a helpful assistant".into(),
temperature: None,
top_p: None,
min_p: None,
seed: None,
reasoning_mode: "high".into(),
cpu_threads: None,
power_percent: None,
prefill_chunk: None,
quality: false,
warm_weights: false,
mtp_draft_tokens: 1,
mtp_margin: 3.0,
glm_mtp: false,
glm_mtp_timing: false,
dspark_confidence_threshold: None,
dspark_strict: false,
ssd_streaming: false,
ssd_streaming_cold: false,
ssd_cache_experts: None,
ssd_cache_gib: None,
ssd_full_layers: None,
ssd_preload_experts: None,
directional_steering_file: None,
directional_steering_ffn: None,
directional_steering_attn: None,
simulated_used_memory_gib: None,
expert_profile_path: None,
endpoint_port: 4000,
endpoint_enabled: true,
endpoint_cors: false,
sidebar_collapsed: false,
last_project_id: None,
sidebar_width: 276,
kv_budget_gib: None,
kv_min_tokens: None,
kv_cold_max_tokens: None,
kv_continued_interval_tokens: None,
}
}
}
impl AppPreferences {
pub(crate) fn generation(&self) -> Result<GenerationPreferences, String> {
let preferences = GenerationPreferences {
context_tokens: self.context_tokens,
max_generated_tokens: self.max_generated_tokens,
system_prompt: self.system_prompt.clone(),
temperature: self.temperature,
top_p: self.top_p,
min_p: self.min_p,
seed: self
.seed
.as_deref()
.map(str::parse)
.transpose()
.map_err(|_| "Saved seed is not a valid positive whole number.".to_owned())?,
reasoning_mode: ReasoningMode::from_id(&self.reasoning_mode)
.ok_or_else(|| format!("Unsupported reasoning mode: {}", self.reasoning_mode))?,
};
preferences.validate()?;
Ok(preferences)
}
pub(crate) fn execution(&self) -> Result<ExecutionPreferences, String> {
Ok(ExecutionPreferences {
cpu_threads: self
.cpu_threads
.map(u32::try_from)
.transpose()
.map_err(|_| "Saved CPU helper threads is invalid.".to_owned())?,
power_percent: self
.power_percent
.map(u8::try_from)
.transpose()
.map_err(|_| "Saved GPU power is invalid.".to_owned())?,
prefill_chunk: self
.prefill_chunk
.map(u32::try_from)
.transpose()
.map_err(|_| "Saved prefill chunk is invalid.".to_owned())?,
quality: self.quality,
warm_weights: self.warm_weights,
})
}
pub(crate) fn speculative(&self) -> SpeculativePreferences {
SpeculativePreferences {
mtp_draft_tokens: self.mtp_draft_tokens,
mtp_margin: self.mtp_margin,
glm_mtp: self.glm_mtp,
glm_mtp_timing: self.glm_mtp_timing,
dspark_enabled: self.dspark_enabled,
dspark_confidence_threshold: self.dspark_confidence_threshold,
dspark_strict: self.dspark_strict,
}
}
pub(crate) fn runtime(&self) -> Result<RuntimePreferences, String> {
let cache = match (self.ssd_cache_experts, self.ssd_cache_gib) {
(None, None) => None,
(Some(experts), None) => Some(StreamingCacheBudget::Experts(
u32::try_from(experts).map_err(|_| "Saved SSD expert count is invalid.")?,
)),
(None, Some(gib)) => Some(StreamingCacheBudget::Gib(
u64::try_from(gib).map_err(|_| "Saved SSD cache budget is invalid.")?,
)),
(Some(_), Some(_)) => {
return Err("Saved SSD cache budget has both count and GiB values.".into());
}
};
Ok(RuntimePreferences {
execution: self.execution()?,
speculative: self.speculative(),
ssd: SsdPreferences {
enabled: self.ssd_streaming,
cold: self.ssd_streaming_cold,
cache,
full_layers: self
.ssd_full_layers
.map(u32::try_from)
.transpose()
.map_err(|_| "Saved SSD full-layer count is invalid.")?,
preload_experts: self
.ssd_preload_experts
.map(u32::try_from)
.transpose()
.map_err(|_| "Saved SSD preload count is invalid.")?,
},
steering: SteeringPreferences {
file: self.directional_steering_file.clone(),
ffn_scale: self.directional_steering_ffn,
attention_scale: self.directional_steering_attn,
},
diagnostics: DiagnosticPreferences {
simulated_used_memory_gib: self
.simulated_used_memory_gib
.map(u64::try_from)
.transpose()
.map_err(|_| "Saved simulated memory is invalid.")?,
expert_profile_path: self.expert_profile_path.clone(),
},
kv_cache: KvCachePreferences {
budget_gib: self
.kv_budget_gib
.map(u64::try_from)
.transpose()
.map_err(|_| "Saved KV cache budget is invalid.")?,
min_tokens: self
.kv_min_tokens
.map(u32::try_from)
.transpose()
.map_err(|_| "Saved KV cache minimum tokens is invalid.")?,
cold_max_tokens: self
.kv_cold_max_tokens
.map(u32::try_from)
.transpose()
.map_err(|_| "Saved KV cache cold maximum is invalid.")?,
continued_interval_tokens: self
.kv_continued_interval_tokens
.map(u32::try_from)
.transpose()
.map_err(|_| "Saved KV cache continued interval is invalid.")?,
},
})
}
}
#[derive(AsChangeset)]
#[diesel(table_name = preferences)]
struct PreferenceChanges<'a> {
selected_model: &'a str,
dspark_enabled: bool,
idle_timeout_minutes: i32,
context_tokens: i32,
max_generated_tokens: i32,
system_prompt: &'a str,
temperature: Option<f32>,
top_p: Option<f32>,
min_p: Option<f32>,
seed: Option<&'a str>,
reasoning_mode: &'a str,
cpu_threads: Option<i32>,
power_percent: Option<i32>,
prefill_chunk: Option<i32>,
quality: bool,
warm_weights: bool,
mtp_draft_tokens: i32,
mtp_margin: f32,
glm_mtp: bool,
glm_mtp_timing: bool,
dspark_confidence_threshold: Option<f32>,
dspark_strict: bool,
ssd_streaming: bool,
ssd_streaming_cold: bool,
ssd_cache_experts: Option<i64>,
ssd_cache_gib: Option<i64>,
ssd_full_layers: Option<i32>,
ssd_preload_experts: Option<i32>,
directional_steering_file: Option<&'a str>,
directional_steering_ffn: Option<f32>,
directional_steering_attn: Option<f32>,
simulated_used_memory_gib: Option<i64>,
expert_profile_path: Option<&'a str>,
endpoint_port: i32,
endpoint_enabled: bool,
endpoint_cors: bool,
kv_budget_gib: Option<i64>,
kv_min_tokens: Option<i32>,
kv_cold_max_tokens: Option<i32>,
kv_continued_interval_tokens: Option<i32>,
}
#[derive(Clone, Debug, Identifiable, Queryable, Selectable)]
#[diesel(table_name = projects)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
@@ -466,126 +188,6 @@ impl Database {
.collect())
}
pub fn load_preferences(&mut self) -> Result<AppPreferences, String> {
preferences::table
.find(1)
.select(AppPreferences::as_select())
.first(&mut self.connection)
.map_err(|error| error.to_string())
}
pub fn set_sidebar_collapsed(&mut self, collapsed: bool) -> Result<(), String> {
diesel::update(preferences::table.find(1))
.set(preferences::sidebar_collapsed.eq(collapsed))
.execute(&mut self.connection)
.map(|_| ())
.map_err(|error| error.to_string())
}
pub fn set_sidebar_width(&mut self, width: i32) -> Result<(), String> {
diesel::update(preferences::table.find(1))
.set(preferences::sidebar_width.eq(width))
.execute(&mut self.connection)
.map(|_| ())
.map_err(|error| error.to_string())
}
pub fn set_last_project(&mut self, project_id: Option<i32>) -> Result<(), String> {
diesel::update(preferences::table.find(1))
.set(preferences::last_project_id.eq(project_id))
.execute(&mut self.connection)
.map(|_| ())
.map_err(|error| error.to_string())
}
#[allow(clippy::too_many_arguments)]
pub fn update_preferences(
&mut self,
selected_model: &str,
idle_timeout_minutes: i32,
endpoint_port: i32,
endpoint_enabled: bool,
endpoint_cors: bool,
generation: &GenerationPreferences,
runtime: &RuntimePreferences,
) -> Result<AppPreferences, String> {
generation.validate()?;
let model = crate::model::ModelChoice::from_id(selected_model)
.ok_or_else(|| format!("Unsupported model: {selected_model}"))?;
runtime.validate(model)?;
if !(1..=65_535).contains(&endpoint_port) {
return Err("Endpoint port must be between 1 and 65535.".into());
}
let execution = &runtime.execution;
let speculative = &runtime.speculative;
let (ssd_cache_experts, ssd_cache_gib) = match runtime.ssd.cache {
None => (None, None),
Some(StreamingCacheBudget::Experts(experts)) => (Some(i64::from(experts)), None),
Some(StreamingCacheBudget::Gib(gib)) => (
None,
Some(i64::try_from(gib).map_err(|_| "SSD cache budget is too large.")?),
),
};
let seed = generation.seed.map(|seed| seed.to_string());
diesel::update(preferences::table.find(1))
.set(PreferenceChanges {
selected_model,
dspark_enabled: speculative.dspark_enabled,
idle_timeout_minutes,
context_tokens: generation.context_tokens,
max_generated_tokens: generation.max_generated_tokens,
system_prompt: &generation.system_prompt,
temperature: generation.temperature,
top_p: generation.top_p,
min_p: generation.min_p,
seed: seed.as_deref(),
reasoning_mode: generation.reasoning_mode.id(),
cpu_threads: execution.cpu_threads.map(|value| value as i32),
power_percent: execution.power_percent.map(i32::from),
prefill_chunk: execution.prefill_chunk.map(|value| value as i32),
quality: execution.quality,
warm_weights: execution.warm_weights,
mtp_draft_tokens: speculative.mtp_draft_tokens,
mtp_margin: speculative.mtp_margin,
glm_mtp: speculative.glm_mtp,
glm_mtp_timing: speculative.glm_mtp_timing,
dspark_confidence_threshold: speculative.dspark_confidence_threshold,
dspark_strict: speculative.dspark_strict,
ssd_streaming: runtime.ssd.enabled,
ssd_streaming_cold: runtime.ssd.cold,
ssd_cache_experts,
ssd_cache_gib,
ssd_full_layers: runtime.ssd.full_layers.map(|value| value as i32),
ssd_preload_experts: runtime.ssd.preload_experts.map(|value| value as i32),
directional_steering_file: runtime.steering.file.as_deref(),
directional_steering_ffn: runtime.steering.ffn_scale,
directional_steering_attn: runtime.steering.attention_scale,
simulated_used_memory_gib: runtime
.diagnostics
.simulated_used_memory_gib
.map(|value| value as i64),
expert_profile_path: runtime.diagnostics.expert_profile_path.as_deref(),
endpoint_port,
endpoint_enabled,
endpoint_cors,
kv_budget_gib: runtime
.kv_cache
.budget_gib
.map(i64::try_from)
.transpose()
.map_err(|_| "KV cache budget is too large.")?,
kv_min_tokens: runtime.kv_cache.min_tokens.map(|value| value as i32),
kv_cold_max_tokens: runtime.kv_cache.cold_max_tokens.map(|value| value as i32),
kv_continued_interval_tokens: runtime
.kv_cache
.continued_interval_tokens
.map(|value| value as i32),
})
.returning(AppPreferences::as_returning())
.get_result(&mut self.connection)
.map_err(|error| error.to_string())
}
pub fn create_project(&mut self, name: &str, path: &str) -> Result<Project, String> {
diesel::insert_into(projects::table)
.values(NewProject { name, path })
@@ -805,94 +407,6 @@ mod tests {
let path = std::env::temp_dir().join(format!("ds4-server-{id}.sqlite3"));
let mut database = Database::open(&path).unwrap();
let preferences = database.load_preferences().unwrap();
assert_eq!(preferences.selected_model, "deepseek-v4-flash");
assert!(!preferences.dspark_enabled);
assert_eq!(preferences.idle_timeout_minutes, 10);
assert_eq!(preferences.endpoint_port, 4000);
assert!(preferences.endpoint_enabled);
assert!(!preferences.endpoint_cors);
let generation = GenerationPreferences::default();
let runtime = RuntimePreferences::default();
assert!(
database
.update_preferences(
"glm-5.2",
30,
4000,
true,
false,
&generation,
&RuntimePreferences {
speculative: SpeculativePreferences {
dspark_enabled: true,
..runtime.speculative.clone()
},
..runtime.clone()
}
)
.is_err()
);
assert!(
database
.update_preferences(
"deepseek-v4-flash",
0,
4000,
true,
false,
&generation,
&runtime,
)
.is_err()
);
let generation = GenerationPreferences {
top_p: Some(0.7),
seed: Some(u64::MAX),
reasoning_mode: ReasoningMode::Max,
..generation
};
let execution = ExecutionPreferences {
cpu_threads: Some(8),
quality: true,
..runtime.execution
};
let speculative = SpeculativePreferences {
glm_mtp: true,
glm_mtp_timing: true,
mtp_draft_tokens: 4,
mtp_margin: 2.5,
..runtime.speculative
};
let runtime = RuntimePreferences {
execution,
speculative,
ssd: SsdPreferences {
enabled: true,
cache: Some(StreamingCacheBudget::Gib(64)),
full_layers: Some(0),
preload_experts: Some(32),
..SsdPreferences::default()
},
diagnostics: DiagnosticPreferences {
simulated_used_memory_gib: Some(8),
expert_profile_path: Some("/tmp/experts.json".into()),
},
kv_cache: KvCachePreferences {
budget_gib: Some(16),
min_tokens: Some(256),
cold_max_tokens: Some(0),
continued_interval_tokens: Some(4_096),
},
..RuntimePreferences::default()
};
database
.update_preferences("glm-5.2", 30, 4567, false, true, &generation, &runtime)
.unwrap();
let preferences = database.load_preferences().unwrap();
assert!(!preferences.endpoint_enabled);
assert!(preferences.endpoint_cors);
let project = database.create_project("DS4", "/tmp/ds4").unwrap();
let first = database
.create_session(project.id, "First session")
@@ -937,9 +451,6 @@ mod tests {
let loaded = database.load_projects().unwrap();
assert_eq!(loaded[0].sessions[0].id, ordinary);
database.set_sidebar_collapsed(true).unwrap();
database.set_last_project(Some(project.id)).unwrap();
database.set_sidebar_width(320).unwrap();
assert!(!loaded[0].project.collapsed);
database.set_project_collapsed(project.id, true).unwrap();
assert!(database.load_projects().unwrap()[0].project.collapsed);
@@ -949,15 +460,7 @@ mod tests {
drop(database);
let mut reopened = Database::open(&path).unwrap();
let preferences = reopened.load_preferences().unwrap();
assert!(preferences.sidebar_collapsed);
assert_eq!(preferences.last_project_id, Some(project.id));
assert_eq!(preferences.sidebar_width, 320);
assert_eq!(preferences.selected_model, "glm-5.2");
assert_eq!(preferences.idle_timeout_minutes, 30);
assert_eq!(preferences.endpoint_port, 4567);
assert_eq!(preferences.generation().unwrap(), generation);
assert_eq!(preferences.runtime().unwrap(), runtime);
assert!(reopened.load_projects().unwrap().is_empty());
drop(reopened);
fs::remove_file(path).unwrap();
}

View File

@@ -1,5 +1,6 @@
mod agent;
mod app;
mod config;
mod database;
mod engine;
mod metrics;

View File

@@ -4,6 +4,7 @@ pub(crate) use transfer::{
delete_managed_artifact, download_managed_artifact, validate_managed_artifact,
};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
@@ -56,11 +57,14 @@ const GLM: Artifact = Artifact {
support: Some(false),
};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub(crate) enum ModelChoice {
#[default]
#[serde(rename = "deepseek-v4-flash")]
DeepSeekV4Flash,
#[serde(rename = "deepseek-v4-pro")]
DeepSeekV4Pro,
#[serde(rename = "glm-5.2")]
Glm52,
}

View File

@@ -19,55 +19,6 @@ diesel::table! {
}
}
diesel::table! {
preferences (id) {
id -> Integer,
selected_model -> Text,
dspark_enabled -> Bool,
idle_timeout_minutes -> Integer,
context_tokens -> Integer,
max_generated_tokens -> Integer,
system_prompt -> Text,
temperature -> Nullable<Float>,
top_p -> Nullable<Float>,
min_p -> Nullable<Float>,
seed -> Nullable<Text>,
reasoning_mode -> Text,
cpu_threads -> Nullable<Integer>,
power_percent -> Nullable<Integer>,
prefill_chunk -> Nullable<Integer>,
quality -> Bool,
warm_weights -> Bool,
mtp_draft_tokens -> Integer,
mtp_margin -> Float,
glm_mtp -> Bool,
glm_mtp_timing -> Bool,
dspark_confidence_threshold -> Nullable<Float>,
dspark_strict -> Bool,
ssd_streaming -> Bool,
ssd_streaming_cold -> Bool,
ssd_cache_experts -> Nullable<BigInt>,
ssd_cache_gib -> Nullable<BigInt>,
ssd_full_layers -> Nullable<Integer>,
ssd_preload_experts -> Nullable<Integer>,
directional_steering_file -> Nullable<Text>,
directional_steering_ffn -> Nullable<Float>,
directional_steering_attn -> Nullable<Float>,
simulated_used_memory_gib -> Nullable<BigInt>,
expert_profile_path -> Nullable<Text>,
endpoint_port -> Integer,
endpoint_enabled -> Bool,
endpoint_cors -> Bool,
sidebar_collapsed -> Bool,
last_project_id -> Nullable<Integer>,
sidebar_width -> Integer,
kv_budget_gib -> Nullable<BigInt>,
kv_min_tokens -> Nullable<Integer>,
kv_cold_max_tokens -> Nullable<Integer>,
kv_continued_interval_tokens -> Nullable<Integer>,
}
}
diesel::table! {
sessions (id) {
id -> Integer,
@@ -82,4 +33,4 @@ diesel::table! {
diesel::joinable!(sessions -> projects (project_id));
diesel::joinable!(messages -> sessions (session_id));
diesel::allow_tables_to_appear_in_same_query!(messages, preferences, projects, sessions);
diesel::allow_tables_to_appear_in_same_query!(messages, projects, sessions);

View File

@@ -20,7 +20,7 @@ use tools::{
#[cfg(test)]
use tools::{canonical_tools, validate_tool_results};
use crate::database::AppPreferences;
use crate::config::Config;
use crate::engine::ChatTurn;
use crate::metrics::Metrics;
use crate::model::{self, ModelChoice};
@@ -70,7 +70,7 @@ pub(crate) struct ServerHandle {
struct State {
generation: GenerationService,
preferences: Arc<RwLock<AppPreferences>>,
config: Arc<RwLock<Config>>,
models_path: PathBuf,
cache_path: PathBuf,
sequence: AtomicU64,
@@ -212,7 +212,7 @@ struct RawOpenAiTool<'a> {
impl ServerHandle {
pub(crate) fn spawn(
generation: GenerationService,
preferences: Arc<RwLock<AppPreferences>>,
config: Arc<RwLock<Config>>,
models_path: PathBuf,
cache_path: PathBuf,
port: u16,
@@ -230,7 +230,7 @@ impl ServerHandle {
let tool_memory = read_tool_memory(&cache_path);
let state = Arc::new(State {
generation,
preferences,
config,
models_path,
cache_path,
sequence: AtomicU64::new(0),
@@ -417,12 +417,12 @@ fn handle(mut stream: TcpStream, state: &State) {
let installed = installed_endpoint_models(&state.models_path);
let model = model_alias(id).filter(|model| installed.contains(model));
if let Some(model) = model {
let (context, tokens) = state
.preferences
.read()
.map_or((32_768, 32_768), |preferences| {
(preferences.context_tokens, preferences.max_generated_tokens)
});
let (context, tokens) = state.config.read().map_or((32_768, 32_768), |config| {
(
config.generation.context_tokens,
config.generation.max_generated_tokens,
)
});
send_json_with_cors(
&mut stream,
200,
@@ -549,12 +549,12 @@ fn installed_endpoint_models(models_path: &std::path::Path) -> Vec<ModelChoice>
}
fn models_json(state: &State) -> Value {
let (context, tokens) = state
.preferences
.read()
.map_or((32_768, 32_768), |preferences| {
(preferences.context_tokens, preferences.max_generated_tokens)
});
let (context, tokens) = state.config.read().map_or((32_768, 32_768), |config| {
(
config.generation.context_tokens,
config.generation.max_generated_tokens,
)
});
json!({
"object": "list",
"data": installed_endpoint_models(&state.models_path)
@@ -735,7 +735,7 @@ mod tests {
let metrics = Arc::new(Metrics::new(PathBuf::new().as_path()));
let state = State {
generation: GenerationService::spawn(Arc::clone(&metrics)).unwrap(),
preferences: Arc::new(RwLock::new(AppPreferences::default())),
config: Arc::new(RwLock::new(Config::default())),
models_path: PathBuf::new(),
cache_path: PathBuf::new(),
sequence: AtomicU64::new(0),
@@ -773,7 +773,7 @@ mod tests {
let metrics = Arc::new(Metrics::new(&directory));
let state = State {
generation: GenerationService::spawn(Arc::clone(&metrics)).unwrap(),
preferences: Arc::new(RwLock::new(AppPreferences::default())),
config: Arc::new(RwLock::new(Config::default())),
models_path: PathBuf::new(),
cache_path: directory.clone(),
sequence: AtomicU64::new(0),
@@ -794,7 +794,7 @@ mod tests {
let metrics = Arc::new(Metrics::new(PathBuf::new().as_path()));
let state = State {
generation: GenerationService::spawn(Arc::clone(&metrics)).unwrap(),
preferences: Arc::new(RwLock::new(AppPreferences::default())),
config: Arc::new(RwLock::new(Config::default())),
models_path: PathBuf::new(),
cache_path: PathBuf::new(),
sequence: AtomicU64::new(0),
@@ -847,7 +847,7 @@ mod tests {
let metrics = Arc::new(Metrics::new(PathBuf::new().as_path()));
let state = State {
generation: GenerationService::spawn(Arc::clone(&metrics)).unwrap(),
preferences: Arc::new(RwLock::new(AppPreferences::default())),
config: Arc::new(RwLock::new(Config::default())),
models_path: PathBuf::new(),
cache_path: PathBuf::new(),
sequence: AtomicU64::new(0),

View File

@@ -484,13 +484,13 @@ pub(super) fn parse_chat_request(
return Err((400, "missing messages".into()));
}
let requested_id = request.model.as_deref().unwrap_or_default();
let preferences = state
.preferences
let config = state
.config
.read()
.map_err(|_| (500, "preferences are unavailable".to_owned()))?
.clone();
let model = if requested_id.is_empty() {
ModelChoice::from_id(&preferences.selected_model)
Some(config.model)
} else {
model_alias(requested_id)
}
@@ -499,7 +499,7 @@ pub(super) fn parse_chat_request(
return Err((400, format!("model is not installed and verified: {model}")));
}
let mut generation = preferences.generation().map_err(|error| (500, error))?;
let mut generation = config.generation.clone();
generation.system_prompt.clear();
generation.max_generated_tokens = request
.max_completion_tokens
@@ -525,8 +525,7 @@ pub(super) fn parse_chat_request(
protocol,
)?;
generation.system_prompt = system;
let runtime = preferences.runtime().map_err(|error| (500, error))?;
let mut effective = effective_settings(model, &generation, &runtime, &state.models_path)
let mut effective = effective_settings(model, &generation, &config.runtime, &state.models_path)
.map_err(|error| (400, error))?;
effective.turn.top_k = request.top_k.unwrap_or(0);
if effective.turn.top_k < 0 {
@@ -549,7 +548,7 @@ pub(super) fn parse_chat_request(
messages,
turn: effective.turn,
engine: effective.engine,
idle_timeout: Duration::from_secs(preferences.idle_timeout_minutes.max(1) as u64 * 60),
idle_timeout: Duration::from_secs(config.idle_timeout_minutes.max(1) as u64 * 60),
stream: request.stream,
include_usage: request
.stream_options

View File

@@ -1,4 +1,5 @@
use crate::model::{self, EngineArtifacts, ModelChoice};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::Path;
@@ -17,7 +18,8 @@ const DEFAULT_KV_MIN_TOKENS: u32 = 512;
const DEFAULT_KV_COLD_MAX_TOKENS: u32 = 30_000;
const DEFAULT_KV_CONTINUED_INTERVAL_TOKENS: u32 = 10_000;
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(default, deny_unknown_fields)]
pub(crate) struct SpeculativePreferences {
pub(crate) mtp_draft_tokens: i32,
pub(crate) mtp_margin: f32,
@@ -94,13 +96,70 @@ pub(crate) struct EngineSpeculativeSettings {
pub(crate) dspark_strict: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
/// An expert count, or a whole GiB budget. Written as `4` or `64GB`, the same
/// spelling the preferences window accepts.
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(into = "String", try_from = "String")]
pub(crate) enum StreamingCacheBudget {
Experts(u32),
Gib(u64),
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
impl fmt::Display for StreamingCacheBudget {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Experts(experts) => write!(formatter, "{experts}"),
Self::Gib(gib) => write!(formatter, "{gib}GB"),
}
}
}
impl From<StreamingCacheBudget> for String {
fn from(budget: StreamingCacheBudget) -> Self {
budget.to_string()
}
}
impl TryFrom<String> for StreamingCacheBudget {
type Error = String;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}
impl std::str::FromStr for StreamingCacheBudget {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let value = value.trim();
let gib = value
.get(value.len().saturating_sub(2)..)
.is_some_and(|suffix| suffix.eq_ignore_ascii_case("gb"));
let digits = if gib {
&value[..value.len() - 2]
} else {
value
};
let number = (!digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit()))
.then(|| digits.parse::<u64>().ok())
.flatten()
.filter(|number| *number > 0);
match number {
Some(number) if gib && number <= u64::MAX / GIB => Ok(Self::Gib(number)),
Some(number) if !gib && number <= u64::from(u32::MAX) => {
Ok(Self::Experts(number as u32))
}
_ => Err(
"SSD cache budget must be a positive expert count or whole GiB value such as 64GB."
.into(),
),
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, deny_unknown_fields)]
pub(crate) struct SsdPreferences {
pub(crate) enabled: bool,
pub(crate) cold: bool,
@@ -166,7 +225,8 @@ pub(crate) struct EngineSsdSettings {
/// Disk KV cache management, mirroring ds4's `--kv-disk-space-mb` budget and
/// its store-side gates. Unset values keep the DS4 defaults.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, deny_unknown_fields)]
pub(crate) struct KvCachePreferences {
pub(crate) budget_gib: Option<u64>,
pub(crate) min_tokens: Option<u32>,
@@ -219,7 +279,8 @@ impl KvCacheSettings {
}
}
#[derive(Clone, Debug, Default, PartialEq)]
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(default, deny_unknown_fields)]
pub(crate) struct SteeringPreferences {
pub(crate) file: Option<String>,
pub(crate) ffn_scale: Option<f32>,
@@ -270,7 +331,8 @@ pub(crate) struct EngineSteeringSettings {
pub(crate) attention_scale: f32,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, deny_unknown_fields)]
pub(crate) struct DiagnosticPreferences {
pub(crate) simulated_used_memory_gib: Option<u64>,
pub(crate) expert_profile_path: Option<String>,
@@ -298,7 +360,8 @@ pub(crate) struct EngineDiagnosticSettings {
pub(crate) expert_profile_path: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq)]
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(default, deny_unknown_fields)]
pub(crate) struct RuntimePreferences {
pub(crate) execution: ExecutionPreferences,
pub(crate) speculative: SpeculativePreferences,
@@ -354,7 +417,8 @@ pub(crate) struct EngineSettings {
pub(crate) diagnostics: EngineDiagnosticSettings,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, deny_unknown_fields)]
pub(crate) struct ExecutionPreferences {
pub(crate) cpu_threads: Option<u32>,
pub(crate) power_percent: Option<u8>,
@@ -420,28 +484,16 @@ pub(crate) struct EngineExecutionSettings {
pub(crate) warm_weights: bool,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum ReasoningMode {
#[serde(rename = "none")]
Direct,
#[default]
High,
Max,
}
impl ReasoningMode {
pub(crate) fn id(self) -> &'static str {
match self {
Self::Direct => "none",
Self::High => "high",
Self::Max => "max",
}
}
pub(crate) fn from_id(id: &str) -> Option<Self> {
REASONING_MODES.into_iter().find(|mode| mode.id() == id)
}
}
impl fmt::Display for ReasoningMode {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
@@ -452,7 +504,8 @@ impl fmt::Display for ReasoningMode {
}
}
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(default, deny_unknown_fields)]
pub(crate) struct GenerationPreferences {
pub(crate) context_tokens: i32,
pub(crate) max_generated_tokens: i32,