Move preferences from the database to a YAML config file
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
26
Cargo.lock
generated
26
Cargo.lock
generated
@@ -1152,6 +1152,7 @@ dependencies = [
|
|||||||
"rfd",
|
"rfd",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_norway",
|
||||||
"sha2",
|
"sha2",
|
||||||
"ureq",
|
"ureq",
|
||||||
]
|
]
|
||||||
@@ -3957,6 +3958,12 @@ dependencies = [
|
|||||||
"unicode-script",
|
"unicode-script",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@@ -4047,6 +4054,19 @@ dependencies = [
|
|||||||
"zmij",
|
"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]]
|
[[package]]
|
||||||
name = "serde_repr"
|
name = "serde_repr"
|
||||||
version = "0.1.21"
|
version = "0.1.21"
|
||||||
@@ -4867,6 +4887,12 @@ version = "0.2.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unsafe-libyaml-norway"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39abd59bf32521c7f2301b52d05a6a2c975b6003521cbd0c6dc1582f0a22104"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ png = "0.17.16"
|
|||||||
rfd = "0.15.4"
|
rfd = "0.15.4"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = { version = "1.0.149", features = ["preserve_order", "raw_value"] }
|
serde_json = { version = "1.0.149", features = ["preserve_order", "raw_value"] }
|
||||||
|
serde_norway = "0.9.42"
|
||||||
sha2 = "0.11.0"
|
sha2 = "0.11.0"
|
||||||
ureq = { version = "3.3.0", default-features = false, features = ["rustls"] }
|
ureq = { version = "3.3.0", default-features = false, features = ["rustls"] }
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
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
|
macOS folder picker selects each workspace, then the app asks for its display
|
||||||
name. Projects, sessions, and model preferences are persisted through Diesel in
|
name. Projects and sessions are persisted through Diesel in SQLite, while
|
||||||
SQLite. Open Preferences with `Command-,` to configure model, generation,
|
settings live in a YAML file beside it that lists only the values you changed
|
||||||
runtime, local endpoint, and idle-unload settings. The separate Model Manager
|
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`)
|
(`Shift-Command-M`)
|
||||||
lists local main and DSpark artifacts, their on-disk sizes and state, and lets
|
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
|
you download, resume, validate, or delete them. Rust-native background work
|
||||||
@@ -57,6 +58,7 @@ State is stored at:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
~/Library/Application Support/DS4Server.rfc1437.de/data.sqlite3
|
~/Library/Application Support/DS4Server.rfc1437.de/data.sqlite3
|
||||||
|
~/Library/Application Support/DS4Server.rfc1437.de/config.yaml
|
||||||
~/Library/Application Support/DS4Server.rfc1437.de/kv-cache/
|
~/Library/Application Support/DS4Server.rfc1437.de/kv-cache/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
1
migrations/20260726160000_drop_preferences/down.sql
Normal file
1
migrations/20260726160000_drop_preferences/down.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
-- Preferences live in config.yaml; the table is not recreated.
|
||||||
1
migrations/20260726160000_drop_preferences/up.sql
Normal file
1
migrations/20260726160000_drop_preferences/up.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE preferences;
|
||||||
137
src/app.rs
137
src/app.rs
@@ -12,7 +12,8 @@ use preferences::PreferenceDraft;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use preferences::{parse_optional_gib, parse_streaming_cache};
|
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")]
|
#[cfg(target_os = "macos")]
|
||||||
use crate::engine::ChatTurn;
|
use crate::engine::ChatTurn;
|
||||||
use crate::metrics::{KvCacheReport, Metrics, MetricsSnapshot};
|
use crate::metrics::{KvCacheReport, Metrics, MetricsSnapshot};
|
||||||
@@ -20,9 +21,9 @@ use crate::model::{self, DownloadOutcome, DownloadProgress, ManagedArtifactId, M
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use crate::runtime::{ActiveGeneration, CheckpointTarget, GenerationEvent, GenerationService};
|
use crate::runtime::{ActiveGeneration, CheckpointTarget, GenerationEvent, GenerationService};
|
||||||
use crate::settings::{
|
use crate::settings::{
|
||||||
DEFAULT_KV_BUDGET_GIB, DiagnosticPreferences, ExecutionPreferences, GIB, GenerationPreferences,
|
DiagnosticPreferences, ExecutionPreferences, GIB, GenerationPreferences, KvCachePreferences,
|
||||||
KvCachePreferences, ReasoningMode, RuntimePreferences, SpeculativePreferences, SsdPreferences,
|
ReasoningMode, RuntimePreferences, SpeculativePreferences, SsdPreferences, SteeringPreferences,
|
||||||
SteeringPreferences, StreamingCacheBudget,
|
StreamingCacheBudget,
|
||||||
};
|
};
|
||||||
use iced::widget::{markdown, scrollable, text_input};
|
use iced::widget::{markdown, scrollable, text_input};
|
||||||
use iced::{Size, Subscription, Task, keyboard, mouse, window};
|
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,
|
pub(super) native_edit_commands: crate::native_edit::EditCommandQueue,
|
||||||
database: Option<Database>,
|
database: Option<Database>,
|
||||||
projects: Vec<ProjectWithSessions>,
|
projects: Vec<ProjectWithSessions>,
|
||||||
preferences: AppPreferences,
|
config: Config,
|
||||||
preference_draft: PreferenceDraft,
|
preference_draft: PreferenceDraft,
|
||||||
preferences_open: bool,
|
preferences_open: bool,
|
||||||
preference_error: Option<String>,
|
preference_error: Option<String>,
|
||||||
@@ -99,7 +100,7 @@ pub(crate) struct App {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
active_titling: Option<generation::TitleRequest>,
|
active_titling: Option<generation::TitleRequest>,
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
runtime_preferences: Arc<RwLock<AppPreferences>>,
|
runtime_config: Arc<RwLock<Config>>,
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
_endpoint: Option<crate::server::ServerHandle>,
|
_endpoint: Option<crate::server::ServerHandle>,
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
@@ -217,22 +218,24 @@ pub(crate) enum Message {
|
|||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub(crate) fn load(main_window: window::Id) -> Self {
|
pub(crate) fn load(main_window: window::Id) -> Self {
|
||||||
let path = application_support_path().join("data.sqlite3");
|
let config = match Config::load(&config_path()) {
|
||||||
match Database::open(&path) {
|
Ok(config) => config,
|
||||||
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),
|
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() {
|
||||||
|
Ok(projects) => {
|
||||||
|
let preference_draft = PreferenceDraft::from_saved(&config);
|
||||||
// Before the counters are taken, so they measure what is
|
// Before the counters are taken, so they measure what is
|
||||||
// actually reachable.
|
// actually reachable.
|
||||||
sweep_orphan_checkpoints(&kv_cache_path(), &projects);
|
sweep_orphan_checkpoints(&kv_cache_path(), &projects);
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
crate::engine::sweep_transient_cache(&transient_cache_path());
|
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.
|
// Reopen on the project we left, with a fresh draft chat.
|
||||||
let last_project = preferences
|
let last_project = config
|
||||||
|
.interface
|
||||||
.last_project_id
|
.last_project_id
|
||||||
.filter(|id| projects.iter().any(|item| item.project.id == *id));
|
.filter(|id| projects.iter().any(|item| item.project.id == *id));
|
||||||
let drafts = last_project
|
let drafts = last_project
|
||||||
@@ -243,8 +246,8 @@ impl App {
|
|||||||
Arc::new(Metrics::new(&application_support_path().join("kv-cache")));
|
Arc::new(Metrics::new(&application_support_path().join("kv-cache")));
|
||||||
let metrics_snapshot = metrics.snapshot();
|
let metrics_snapshot = metrics.snapshot();
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
let (runtime_preferences, generation_service, endpoint, service_error) =
|
let (runtime_config, generation_service, endpoint, service_error) =
|
||||||
spawn_services(&preferences, Arc::clone(&metrics));
|
spawn_services(&config, Arc::clone(&metrics));
|
||||||
Self {
|
Self {
|
||||||
main_window,
|
main_window,
|
||||||
model_manager_window: None,
|
model_manager_window: None,
|
||||||
@@ -255,7 +258,7 @@ impl App {
|
|||||||
native_edit_commands: crate::native_edit::command_queue(),
|
native_edit_commands: crate::native_edit::command_queue(),
|
||||||
database: Some(database),
|
database: Some(database),
|
||||||
projects,
|
projects,
|
||||||
preferences,
|
config,
|
||||||
preference_draft,
|
preference_draft,
|
||||||
preferences_open: false,
|
preferences_open: false,
|
||||||
preference_error: None,
|
preference_error: None,
|
||||||
@@ -294,7 +297,7 @@ impl App {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
active_titling: None,
|
active_titling: None,
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
runtime_preferences,
|
runtime_config,
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
_endpoint: endpoint,
|
_endpoint: endpoint,
|
||||||
error: {
|
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 {
|
fn failed(error: String, main_window: window::Id) -> Self {
|
||||||
let preferences = AppPreferences::default();
|
let config = Config::default();
|
||||||
let preference_draft = PreferenceDraft::from_saved(&preferences)
|
let preference_draft = PreferenceDraft::from_saved(&config);
|
||||||
.expect("default preferences must use a supported model");
|
let context_limit = config.generation.context_tokens.max(0) as u32;
|
||||||
let context_limit = preferences.context_tokens.max(0) as u32;
|
|
||||||
let metrics = Arc::new(Metrics::new(&application_support_path().join("kv-cache")));
|
let metrics = Arc::new(Metrics::new(&application_support_path().join("kv-cache")));
|
||||||
let metrics_snapshot = metrics.snapshot();
|
let metrics_snapshot = metrics.snapshot();
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
let (runtime_preferences, generation_service, endpoint, service_error) =
|
let (runtime_config, generation_service, endpoint, service_error) =
|
||||||
spawn_services(&preferences, Arc::clone(&metrics));
|
spawn_services(&config, Arc::clone(&metrics));
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
let startup_error = service_error;
|
let startup_error = service_error;
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
@@ -339,7 +347,7 @@ impl App {
|
|||||||
native_edit_commands: crate::native_edit::command_queue(),
|
native_edit_commands: crate::native_edit::command_queue(),
|
||||||
database: None,
|
database: None,
|
||||||
projects: Vec::new(),
|
projects: Vec::new(),
|
||||||
preferences,
|
config,
|
||||||
preference_draft,
|
preference_draft,
|
||||||
preferences_open: false,
|
preferences_open: false,
|
||||||
preference_error: None,
|
preference_error: None,
|
||||||
@@ -378,14 +386,12 @@ impl App {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
active_titling: None,
|
active_titling: None,
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
runtime_preferences,
|
runtime_config,
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
_endpoint: endpoint,
|
_endpoint: endpoint,
|
||||||
error: Some(match startup_error {
|
error: Some(match startup_error {
|
||||||
Some(service_error) => {
|
Some(service_error) => format!("{error} {service_error}"),
|
||||||
format!("Could not open the project database: {error}. {service_error}")
|
None => error,
|
||||||
}
|
|
||||||
None => format!("Could not open the project database: {error}"),
|
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -453,29 +459,19 @@ impl App {
|
|||||||
Message::StartSidebarDrag => self.sidebar_drag = true,
|
Message::StartSidebarDrag => self.sidebar_drag = true,
|
||||||
Message::DragSidebar(x) => {
|
Message::DragSidebar(x) => {
|
||||||
if self.sidebar_drag {
|
if self.sidebar_drag {
|
||||||
self.preferences.sidebar_width =
|
self.config.interface.sidebar_width =
|
||||||
(x.round() as i32).clamp(MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH);
|
(x.round() as i32).clamp(MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::EndSidebarDrag => {
|
Message::EndSidebarDrag => {
|
||||||
if self.sidebar_drag {
|
if self.sidebar_drag {
|
||||||
self.sidebar_drag = false;
|
self.sidebar_drag = false;
|
||||||
if let Some(database) = self.database.as_mut()
|
self.store_config();
|
||||||
&& let Err(error) =
|
|
||||||
database.set_sidebar_width(self.preferences.sidebar_width)
|
|
||||||
{
|
|
||||||
self.error = Some(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::ToggleSidebar => {
|
Message::ToggleSidebar => {
|
||||||
let collapsed = !self.preferences.sidebar_collapsed;
|
self.config.interface.sidebar_collapsed = !self.config.interface.sidebar_collapsed;
|
||||||
self.preferences.sidebar_collapsed = collapsed;
|
self.store_config();
|
||||||
if let Some(database) = self.database.as_mut()
|
|
||||||
&& let Err(error) = database.set_sidebar_collapsed(collapsed)
|
|
||||||
{
|
|
||||||
self.error = Some(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Message::MetricsTick => self.sample_metrics(),
|
Message::MetricsTick => self.sample_metrics(),
|
||||||
Message::PreferenceModelChanged(model) => {
|
Message::PreferenceModelChanged(model) => {
|
||||||
@@ -829,9 +825,9 @@ impl App {
|
|||||||
let _ = fs::remove_file(session_checkpoint_path(session_id));
|
let _ = fs::remove_file(session_checkpoint_path(session_id));
|
||||||
}
|
}
|
||||||
self.drafts.remove(&project_id);
|
self.drafts.remove(&project_id);
|
||||||
if self.preferences.last_project_id == Some(project_id) {
|
if self.config.interface.last_project_id == Some(project_id) {
|
||||||
self.preferences.last_project_id = None;
|
self.config.interface.last_project_id = None;
|
||||||
let _ = database.set_last_project(None);
|
let _ = self.config.save(&config_path());
|
||||||
}
|
}
|
||||||
if self.selected_project == Some(project_id) {
|
if self.selected_project == Some(project_id) {
|
||||||
self.selected_project = None;
|
self.selected_project = None;
|
||||||
@@ -957,7 +953,7 @@ impl App {
|
|||||||
self.context_limit = if limit > 0 {
|
self.context_limit = if limit > 0 {
|
||||||
limit as u32
|
limit as u32
|
||||||
} else {
|
} 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.tokens_per_second = tokens_per_second;
|
||||||
self.error = None;
|
self.error = None;
|
||||||
@@ -1128,6 +1124,13 @@ impl App {
|
|||||||
self.finish_cache_change();
|
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) {
|
fn finish_cache_change(&mut self) {
|
||||||
self.metrics.rescan_cache(&kv_cache_path());
|
self.metrics.rescan_cache(&kv_cache_path());
|
||||||
self.metrics_snapshot = self.metrics.snapshot();
|
self.metrics_snapshot = self.metrics.snapshot();
|
||||||
@@ -1135,12 +1138,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn scan_kv_cache(&mut self) {
|
fn scan_kv_cache(&mut self) {
|
||||||
let budget = self
|
let budget = self.config.runtime.kv_cache.settings().budget_bytes;
|
||||||
.preferences
|
|
||||||
.runtime()
|
|
||||||
.map_or(DEFAULT_KV_BUDGET_GIB * GIB, |runtime| {
|
|
||||||
runtime.kv_cache.settings().budget_bytes
|
|
||||||
});
|
|
||||||
self.kv_cache_report = crate::metrics::kv_cache_report(&kv_cache_path(), budget);
|
self.kv_cache_report = crate::metrics::kv_cache_report(&kv_cache_path(), budget);
|
||||||
self.last_cache_scan = Instant::now();
|
self.last_cache_scan = Instant::now();
|
||||||
}
|
}
|
||||||
@@ -1148,34 +1146,34 @@ impl App {
|
|||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
fn spawn_services(
|
fn spawn_services(
|
||||||
preferences: &AppPreferences,
|
config: &Config,
|
||||||
metrics: Arc<Metrics>,
|
metrics: Arc<Metrics>,
|
||||||
) -> (
|
) -> (
|
||||||
Arc<RwLock<AppPreferences>>,
|
Arc<RwLock<Config>>,
|
||||||
Option<GenerationService>,
|
Option<GenerationService>,
|
||||||
Option<crate::server::ServerHandle>,
|
Option<crate::server::ServerHandle>,
|
||||||
Option<String>,
|
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)) {
|
let generation = match GenerationService::spawn(Arc::clone(&metrics)) {
|
||||||
Ok(generation) => generation,
|
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(
|
crate::server::ServerHandle::spawn(
|
||||||
generation.clone(),
|
generation.clone(),
|
||||||
Arc::clone(&runtime_preferences),
|
Arc::clone(&runtime_config),
|
||||||
models_path(),
|
models_path(),
|
||||||
transient_cache_path(),
|
transient_cache_path(),
|
||||||
u16::try_from(preferences.endpoint_port).unwrap_or(4000),
|
u16::try_from(config.endpoint.port).unwrap_or(4000),
|
||||||
preferences.endpoint_cors,
|
config.endpoint.cors,
|
||||||
metrics,
|
metrics,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
match endpoint {
|
match endpoint {
|
||||||
Some(Ok(endpoint)) => (runtime_preferences, Some(generation), Some(endpoint), None),
|
Some(Ok(endpoint)) => (runtime_config, Some(generation), Some(endpoint), None),
|
||||||
Some(Err(error)) => (runtime_preferences, Some(generation), None, Some(error)),
|
Some(Err(error)) => (runtime_config, Some(generation), None, Some(error)),
|
||||||
None => (runtime_preferences, Some(generation), None, None),
|
None => (runtime_config, Some(generation), None, None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1219,6 +1217,11 @@ fn models_path() -> PathBuf {
|
|||||||
application_support_path().join("models")
|
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 {
|
pub(super) fn chat_scroll_id() -> scrollable::Id {
|
||||||
scrollable::Id::new("chat-transcript")
|
scrollable::Id::new("chat-transcript")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,18 +79,13 @@ impl App {
|
|||||||
if prompt.is_empty() {
|
if prompt.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let model = match ModelChoice::from_id(&self.preferences.selected_model) {
|
let model = self.config.model;
|
||||||
Some(model) => model,
|
let effective = crate::settings::effective_settings(
|
||||||
None => {
|
model,
|
||||||
self.error = Some("The selected model is not supported.".into());
|
&self.config.generation,
|
||||||
return;
|
&self.config.runtime,
|
||||||
}
|
&models_path(),
|
||||||
};
|
);
|
||||||
let effective = self.preferences.generation().and_then(|generation| {
|
|
||||||
self.preferences.runtime().and_then(|runtime| {
|
|
||||||
crate::settings::effective_settings(model, &generation, &runtime, &models_path())
|
|
||||||
})
|
|
||||||
});
|
|
||||||
let mut effective = match effective {
|
let mut effective = match effective {
|
||||||
Ok(settings) => settings,
|
Ok(settings) => settings,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -158,7 +153,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let idle_timeout =
|
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(
|
self.active_generation = match service.generate(
|
||||||
effective.engine,
|
effective.engine,
|
||||||
effective.turn,
|
effective.turn,
|
||||||
@@ -255,8 +250,7 @@ impl App {
|
|||||||
Ok(GenerationEvent::Finished(result)) => {
|
Ok(GenerationEvent::Finished(result)) => {
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let model = ModelChoice::from_id(&self.preferences.selected_model)
|
let model = self.config.model;
|
||||||
.unwrap_or_default();
|
|
||||||
let content = self
|
let content = self
|
||||||
.conversation
|
.conversation
|
||||||
.last()
|
.last()
|
||||||
@@ -356,7 +350,7 @@ impl App {
|
|||||||
.find(|project| project.project.id == project_id)
|
.find(|project| project.project.id == project_id)
|
||||||
.map(|project| PathBuf::from(&project.project.path))
|
.map(|project| PathBuf::from(&project.project.path))
|
||||||
.ok_or_else(|| "The active project is unavailable.".to_owned())?;
|
.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))));
|
self.agent_tools = Some((session_id, Arc::new(Mutex::new(tools))));
|
||||||
}
|
}
|
||||||
let tools = Arc::clone(&self.agent_tools.as_ref().unwrap().1);
|
let tools = Arc::clone(&self.agent_tools.as_ref().unwrap().1);
|
||||||
@@ -369,12 +363,13 @@ impl App {
|
|||||||
let session_id = self
|
let session_id = self
|
||||||
.selected_session
|
.selected_session
|
||||||
.ok_or_else(|| "The active session is unavailable.".to_owned())?;
|
.ok_or_else(|| "The active session is unavailable.".to_owned())?;
|
||||||
let model = ModelChoice::from_id(&self.preferences.selected_model)
|
let model = self.config.model;
|
||||||
.ok_or_else(|| "The selected model is not supported.".to_owned())?;
|
let mut effective = crate::settings::effective_settings(
|
||||||
let generation = self.preferences.generation()?;
|
model,
|
||||||
let runtime = self.preferences.runtime()?;
|
&self.config.generation,
|
||||||
let mut effective =
|
&self.config.runtime,
|
||||||
crate::settings::effective_settings(model, &generation, &runtime, &models_path())?;
|
&models_path(),
|
||||||
|
)?;
|
||||||
effective.turn.system_prompt =
|
effective.turn.system_prompt =
|
||||||
crate::agent::system_prompt(model, &effective.turn.system_prompt);
|
crate::agent::system_prompt(model, &effective.turn.system_prompt);
|
||||||
let assistant_reasoning = effective.turn.reasoning_mode != ReasoningMode::Direct;
|
let assistant_reasoning = effective.turn.reasoning_mode != ReasoningMode::Direct;
|
||||||
@@ -400,8 +395,7 @@ impl App {
|
|||||||
let mut assistant = ChatMessage::from(saved.1);
|
let mut assistant = ChatMessage::from(saved.1);
|
||||||
assistant.reasoning_open = assistant_reasoning;
|
assistant.reasoning_open = assistant_reasoning;
|
||||||
self.conversation.push(assistant);
|
self.conversation.push(assistant);
|
||||||
let idle_timeout =
|
let idle_timeout = Duration::from_secs(self.config.idle_timeout_minutes.max(1) as u64 * 60);
|
||||||
Duration::from_secs(self.preferences.idle_timeout_minutes.max(1) as u64 * 60);
|
|
||||||
self.active_generation = Some(
|
self.active_generation = Some(
|
||||||
self.generation_service
|
self.generation_service
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -428,13 +422,13 @@ impl App {
|
|||||||
if self.active_titling.is_some() {
|
if self.active_titling.is_some() {
|
||||||
return Err("A title is already being generated.".into());
|
return Err("A title is already being generated.".into());
|
||||||
}
|
}
|
||||||
let model = ModelChoice::from_id(&self.preferences.selected_model)
|
let model = self.config.model;
|
||||||
.ok_or_else(|| "The selected model is not supported.".to_owned())?;
|
let mut effective = crate::settings::effective_settings(
|
||||||
let mut effective = self.preferences.generation().and_then(|generation| {
|
model,
|
||||||
self.preferences.runtime().and_then(|runtime| {
|
&self.config.generation,
|
||||||
crate::settings::effective_settings(model, &generation, &runtime, &models_path())
|
&self.config.runtime,
|
||||||
})
|
&models_path(),
|
||||||
})?;
|
)?;
|
||||||
let database = self
|
let database = self
|
||||||
.database
|
.database
|
||||||
.as_mut()
|
.as_mut()
|
||||||
@@ -475,8 +469,7 @@ impl App {
|
|||||||
.generation_service
|
.generation_service
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| "The model runtime is unavailable.".to_owned())?;
|
.ok_or_else(|| "The model runtime is unavailable.".to_owned())?;
|
||||||
let idle_timeout =
|
let idle_timeout = Duration::from_secs(self.config.idle_timeout_minutes.max(1) as u64 * 60);
|
||||||
Duration::from_secs(self.preferences.idle_timeout_minutes.max(1) as u64 * 60);
|
|
||||||
match service.generate(
|
match service.generate(
|
||||||
effective.engine,
|
effective.engine,
|
||||||
effective.turn,
|
effective.turn,
|
||||||
|
|||||||
@@ -44,104 +44,60 @@ pub(super) struct PreferenceDraft {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PreferenceDraft {
|
impl PreferenceDraft {
|
||||||
pub(super) fn from_saved(preferences: &AppPreferences) -> Result<Self, String> {
|
pub(super) fn from_saved(config: &Config) -> Self {
|
||||||
let model = ModelChoice::from_id(&preferences.selected_model)
|
let generation = &config.generation;
|
||||||
.ok_or_else(|| format!("Unsupported model: {}", preferences.selected_model))?;
|
let runtime = &config.runtime;
|
||||||
let generation = preferences.generation()?;
|
|
||||||
let runtime = preferences.runtime()?;
|
|
||||||
runtime.validate(model)?;
|
|
||||||
let execution = &runtime.execution;
|
let execution = &runtime.execution;
|
||||||
let speculative = &runtime.speculative;
|
let speculative = &runtime.speculative;
|
||||||
Ok(Self {
|
Self {
|
||||||
model,
|
model: config.model,
|
||||||
dspark_enabled: speculative.dspark_enabled,
|
dspark_enabled: speculative.dspark_enabled,
|
||||||
idle_timeout_minutes: preferences.idle_timeout_minutes.to_string(),
|
idle_timeout_minutes: config.idle_timeout_minutes.to_string(),
|
||||||
endpoint_port: preferences.endpoint_port.to_string(),
|
endpoint_port: config.endpoint.port.to_string(),
|
||||||
endpoint_enabled: preferences.endpoint_enabled,
|
endpoint_enabled: config.endpoint.enabled,
|
||||||
endpoint_cors: preferences.endpoint_cors,
|
endpoint_cors: config.endpoint.cors,
|
||||||
context_tokens: generation.context_tokens.to_string(),
|
context_tokens: generation.context_tokens.to_string(),
|
||||||
max_generated_tokens: generation.max_generated_tokens.to_string(),
|
max_generated_tokens: generation.max_generated_tokens.to_string(),
|
||||||
system_prompt: generation.system_prompt,
|
system_prompt: generation.system_prompt.clone(),
|
||||||
temperature: generation
|
temperature: optional_string(generation.temperature),
|
||||||
.temperature
|
top_p: optional_string(generation.top_p),
|
||||||
.map_or_else(String::new, |value| value.to_string()),
|
min_p: optional_string(generation.min_p),
|
||||||
top_p: generation
|
seed: optional_string(generation.seed),
|
||||||
.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()),
|
|
||||||
reasoning_mode: generation.reasoning_mode,
|
reasoning_mode: generation.reasoning_mode,
|
||||||
cpu_threads: execution
|
cpu_threads: optional_string(execution.cpu_threads),
|
||||||
.cpu_threads
|
power_percent: optional_string(execution.power_percent),
|
||||||
.map_or_else(String::new, |value| value.to_string()),
|
prefill_chunk: optional_string(execution.prefill_chunk),
|
||||||
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()),
|
|
||||||
quality: execution.quality,
|
quality: execution.quality,
|
||||||
warm_weights: execution.warm_weights,
|
warm_weights: execution.warm_weights,
|
||||||
mtp_draft_tokens: speculative.mtp_draft_tokens.to_string(),
|
mtp_draft_tokens: speculative.mtp_draft_tokens.to_string(),
|
||||||
mtp_margin: speculative.mtp_margin.to_string(),
|
mtp_margin: speculative.mtp_margin.to_string(),
|
||||||
glm_mtp: speculative.glm_mtp,
|
glm_mtp: speculative.glm_mtp,
|
||||||
glm_mtp_timing: speculative.glm_mtp_timing,
|
glm_mtp_timing: speculative.glm_mtp_timing,
|
||||||
dspark_confidence_threshold: speculative
|
dspark_confidence_threshold: optional_string(speculative.dspark_confidence_threshold),
|
||||||
.dspark_confidence_threshold
|
|
||||||
.map_or_else(String::new, |value| value.to_string()),
|
|
||||||
dspark_strict: speculative.dspark_strict,
|
dspark_strict: speculative.dspark_strict,
|
||||||
ssd_streaming: runtime.ssd.enabled,
|
ssd_streaming: runtime.ssd.enabled,
|
||||||
ssd_streaming_cold: runtime.ssd.cold,
|
ssd_streaming_cold: runtime.ssd.cold,
|
||||||
ssd_cache: runtime
|
ssd_cache: optional_string(runtime.ssd.cache),
|
||||||
.ssd
|
ssd_full_layers: optional_string(runtime.ssd.full_layers),
|
||||||
.cache
|
ssd_preload_experts: optional_string(runtime.ssd.preload_experts),
|
||||||
.map_or_else(String::new, |cache| match cache {
|
directional_steering_file: runtime.steering.file.clone().unwrap_or_default(),
|
||||||
StreamingCacheBudget::Experts(experts) => experts.to_string(),
|
directional_steering_ffn: optional_string(runtime.steering.ffn_scale),
|
||||||
StreamingCacheBudget::Gib(gib) => format!("{gib}GB"),
|
directional_steering_attn: optional_string(runtime.steering.attention_scale),
|
||||||
}),
|
simulated_used_memory_gib: optional_string(
|
||||||
ssd_full_layers: runtime
|
runtime.diagnostics.simulated_used_memory_gib,
|
||||||
.ssd
|
),
|
||||||
.full_layers
|
expert_profile_path: runtime
|
||||||
.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
|
|
||||||
.diagnostics
|
.diagnostics
|
||||||
.simulated_used_memory_gib
|
.expert_profile_path
|
||||||
.map_or_else(String::new, |value| value.to_string()),
|
.clone()
|
||||||
expert_profile_path: runtime.diagnostics.expert_profile_path.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
kv_budget_gib: runtime
|
kv_budget_gib: optional_string(runtime.kv_cache.budget_gib),
|
||||||
.kv_cache
|
kv_min_tokens: optional_string(runtime.kv_cache.min_tokens),
|
||||||
.budget_gib
|
kv_cold_max_tokens: optional_string(runtime.kv_cache.cold_max_tokens),
|
||||||
.map_or_else(String::new, |value| value.to_string()),
|
kv_continued_interval_tokens: optional_string(
|
||||||
kv_min_tokens: runtime
|
runtime.kv_cache.continued_interval_tokens,
|
||||||
.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()),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn generation(&self) -> Result<GenerationPreferences, String> {
|
pub(super) fn generation(&self) -> Result<GenerationPreferences, String> {
|
||||||
@@ -163,49 +119,7 @@ impl PreferenceDraft {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn reset(&mut self) {
|
pub(super) fn reset(&mut self) {
|
||||||
let defaults = GenerationPreferences::default();
|
*self = Self::from_saved(&Config::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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn execution(&self) -> Result<ExecutionPreferences, String> {
|
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() {
|
if value.is_empty() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
if value.len() > 2
|
value.parse().map(Some)
|
||||||
&& 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()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn parse_optional_gib(name: &str, value: &str) -> Result<Option<u64>, String> {
|
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())
|
(!value.is_empty()).then(|| value.to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_runtime_preferences(
|
/// An unset preference shows as an empty field.
|
||||||
runtime_preferences: &RwLock<AppPreferences>,
|
fn optional_string<T: ToString>(value: Option<T>) -> String {
|
||||||
preferences: &AppPreferences,
|
value.map_or_else(String::new, |value| value.to_string())
|
||||||
) {
|
}
|
||||||
*runtime_preferences
|
|
||||||
|
fn update_runtime_config(runtime_config: &RwLock<Config>, config: &Config) {
|
||||||
|
*runtime_config
|
||||||
.write()
|
.write()
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner()) = preferences.clone();
|
.unwrap_or_else(|poisoned| poisoned.into_inner()) = config.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
@@ -418,15 +311,10 @@ impl App {
|
|||||||
if self.database.is_none() || self.pending_project_path.is_some() || self.choosing_folder {
|
if self.database.is_none() || self.pending_project_path.is_some() || self.choosing_folder {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match PreferenceDraft::from_saved(&self.preferences) {
|
self.preference_draft = PreferenceDraft::from_saved(&self.config);
|
||||||
Ok(draft) => {
|
|
||||||
self.preference_draft = draft;
|
|
||||||
self.preference_error = None;
|
self.preference_error = None;
|
||||||
self.preferences_open = true;
|
self.preferences_open = true;
|
||||||
}
|
}
|
||||||
Err(error) => self.error = Some(error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn save_preferences(&mut self) {
|
pub(super) fn save_preferences(&mut self) {
|
||||||
let Ok(idle_timeout_minutes) = self
|
let Ok(idle_timeout_minutes) = self
|
||||||
@@ -438,18 +326,10 @@ impl App {
|
|||||||
self.preference_error = Some("Idle timeout must be a whole number.".into());
|
self.preference_error = Some("Idle timeout must be a whole number.".into());
|
||||||
return;
|
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 {
|
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());
|
self.preference_error = Some("Endpoint port must be a whole number.".into());
|
||||||
return;
|
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() {
|
let generation = match self.preference_draft.generation() {
|
||||||
Ok(generation) => generation,
|
Ok(generation) => generation,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -457,8 +337,6 @@ impl App {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let model = self.preference_draft.model;
|
|
||||||
let runtime = match self.preference_draft.runtime() {
|
let runtime = match self.preference_draft.runtime() {
|
||||||
Ok(runtime) => runtime,
|
Ok(runtime) => runtime,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -466,35 +344,44 @@ impl App {
|
|||||||
return;
|
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);
|
self.preference_error = Some(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
let endpoint_changed = self.preferences.endpoint_port != i32::from(endpoint_port)
|
let endpoint_changed = self.config.endpoint != config.endpoint;
|
||||||
|| self.preferences.endpoint_enabled != self.preference_draft.endpoint_enabled
|
|
||||||
|| self.preferences.endpoint_cors != self.preference_draft.endpoint_cors;
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
if endpoint_changed
|
if endpoint_changed
|
||||||
&& self.preferences.endpoint_port == i32::from(endpoint_port)
|
&& self.config.endpoint.port == config.endpoint.port
|
||||||
&& self._endpoint.is_some()
|
&& self._endpoint.is_some()
|
||||||
{
|
{
|
||||||
self._endpoint = None;
|
self._endpoint = None;
|
||||||
}
|
}
|
||||||
let pending_endpoint = if self.preference_draft.endpoint_enabled
|
let pending_endpoint =
|
||||||
&& (endpoint_changed || self._endpoint.is_none())
|
if config.endpoint.enabled && (endpoint_changed || self._endpoint.is_none()) {
|
||||||
{
|
|
||||||
let Some(generation) = &self.generation_service else {
|
let Some(generation) = &self.generation_service else {
|
||||||
self.preference_error = Some("The model runtime is unavailable.".into());
|
self.preference_error = Some("The model runtime is unavailable.".into());
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
match crate::server::ServerHandle::spawn(
|
match crate::server::ServerHandle::spawn(
|
||||||
generation.clone(),
|
generation.clone(),
|
||||||
Arc::clone(&self.runtime_preferences),
|
Arc::clone(&self.runtime_config),
|
||||||
models_path(),
|
models_path(),
|
||||||
application_support_path().join("kv-cache").join("http"),
|
application_support_path().join("kv-cache").join("http"),
|
||||||
endpoint_port,
|
endpoint_port,
|
||||||
self.preference_draft.endpoint_cors,
|
config.endpoint.cors,
|
||||||
Arc::clone(&self.metrics),
|
Arc::clone(&self.metrics),
|
||||||
) {
|
) {
|
||||||
Ok(endpoint) => Some(endpoint),
|
Ok(endpoint) => Some(endpoint),
|
||||||
@@ -506,37 +393,24 @@ impl App {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let Some(database) = &mut self.database else {
|
if let Err(error) = config.save(&config_path()) {
|
||||||
|
self.preference_error = Some(error);
|
||||||
return;
|
return;
|
||||||
};
|
}
|
||||||
match database.update_preferences(
|
self.config = config;
|
||||||
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")]
|
#[cfg(target_os = "macos")]
|
||||||
update_runtime_preferences(&self.runtime_preferences, &self.preferences);
|
update_runtime_config(&self.runtime_config, &self.config);
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
if endpoint_changed {
|
if endpoint_changed {
|
||||||
self._endpoint = pending_endpoint;
|
self._endpoint = pending_endpoint;
|
||||||
} else if let Some(endpoint) = pending_endpoint {
|
} else if let Some(endpoint) = pending_endpoint {
|
||||||
self._endpoint = Some(endpoint);
|
self._endpoint = Some(endpoint);
|
||||||
}
|
}
|
||||||
self.preference_draft = PreferenceDraft::from_saved(&self.preferences)
|
self.preference_draft = PreferenceDraft::from_saved(&self.config);
|
||||||
.expect("the saved model was selected from the supported catalog");
|
|
||||||
self.preferences_open = false;
|
self.preferences_open = false;
|
||||||
self.preference_error = None;
|
self.preference_error = None;
|
||||||
self.error = None;
|
self.error = None;
|
||||||
}
|
}
|
||||||
Err(error) => self.preference_error = Some(error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -544,26 +418,30 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_preferences_update_after_lock_poisoning() {
|
fn runtime_config_updates_after_lock_poisoning() {
|
||||||
let runtime = Arc::new(RwLock::new(AppPreferences::default()));
|
let runtime = Arc::new(RwLock::new(Config::default()));
|
||||||
let poisoned = Arc::clone(&runtime);
|
let poisoned = Arc::clone(&runtime);
|
||||||
let _ = std::thread::spawn(move || {
|
let _ = std::thread::spawn(move || {
|
||||||
let _guard = poisoned.write().unwrap();
|
let _guard = poisoned.write().unwrap();
|
||||||
panic!("poison preference lock");
|
panic!("poison configuration lock");
|
||||||
})
|
})
|
||||||
.join();
|
.join();
|
||||||
let preferences = AppPreferences {
|
let config = Config {
|
||||||
endpoint_port: 4567,
|
endpoint: EndpointConfig {
|
||||||
..AppPreferences::default()
|
port: 4567,
|
||||||
|
..EndpointConfig::default()
|
||||||
|
},
|
||||||
|
..Config::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
update_runtime_preferences(&runtime, &preferences);
|
update_runtime_config(&runtime, &config);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
runtime
|
runtime
|
||||||
.read()
|
.read()
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||||
.endpoint_port,
|
.endpoint
|
||||||
|
.port,
|
||||||
4567
|
4567
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ impl App {
|
|||||||
self.conversation.clear();
|
self.conversation.clear();
|
||||||
self.composer.clear();
|
self.composer.clear();
|
||||||
self.context_used = 0;
|
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.tokens_per_second = None;
|
||||||
self.error = 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.
|
/// 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) {
|
pub(super) fn remember_project(&mut self, project_id: i32) {
|
||||||
self.selected_project = Some(project_id);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
self.preferences.last_project_id = Some(project_id);
|
self.config.interface.last_project_id = Some(project_id);
|
||||||
if let Some(database) = &mut self.database
|
self.store_config();
|
||||||
&& let Err(error) = database.set_last_project(Some(project_id))
|
|
||||||
{
|
|
||||||
self.error = Some(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True when the sidebar row for this project's draft is the active chat.
|
/// True when the sidebar row for this project's draft is the active chat.
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ impl App {
|
|||||||
|
|
||||||
fn main_view(&self) -> Element<'_, Message> {
|
fn main_view(&self) -> Element<'_, Message> {
|
||||||
let mut body = row![].width(Length::Fill).height(Length::Fill);
|
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(self.sidebar());
|
||||||
body = body.push(
|
body = body.push(
|
||||||
mouse_area(
|
mouse_area(
|
||||||
@@ -272,7 +272,8 @@ impl App {
|
|||||||
.spacing(10),
|
.spacing(10),
|
||||||
)
|
)
|
||||||
.width(
|
.width(
|
||||||
self.preferences
|
self.config
|
||||||
|
.interface
|
||||||
.sidebar_width
|
.sidebar_width
|
||||||
.clamp(MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH) as f32,
|
.clamp(MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH) as f32,
|
||||||
)
|
)
|
||||||
@@ -327,10 +328,11 @@ impl App {
|
|||||||
.style(button::text);
|
.style(button::text);
|
||||||
// Center the tabs over the detail area, not the whole window, so the
|
// Center the tabs over the detail area, not the whole window, so the
|
||||||
// sidebar's width (plus its divider) shifts them along.
|
// 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
|
0.0
|
||||||
} else {
|
} else {
|
||||||
self.preferences
|
self.config
|
||||||
|
.interface
|
||||||
.sidebar_width
|
.sidebar_width
|
||||||
.clamp(MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH) as f32
|
.clamp(MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH) as f32
|
||||||
+ 5.0
|
+ 5.0
|
||||||
|
|||||||
@@ -115,11 +115,10 @@ impl App {
|
|||||||
} else if active && message.reasoning.is_none() {
|
} else if active && message.reasoning.is_none() {
|
||||||
body = body.push(text("Loading model…").size(14));
|
body = body.push(text("Loading model…").size(14));
|
||||||
}
|
}
|
||||||
if !message.user
|
if !message.user && !message.tool {
|
||||||
&& !message.tool
|
for summary in
|
||||||
&& let Some(model) = ModelChoice::from_id(&self.preferences.selected_model)
|
crate::agent::tool_summaries(self.config.model, &message.content)
|
||||||
{
|
{
|
||||||
for summary in crate::agent::tool_summaries(model, &message.content) {
|
|
||||||
body = body.push(text(summary).size(13).color(muted_text()));
|
body = body.push(text(summary).size(13).color(muted_text()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,12 +184,7 @@ impl App {
|
|||||||
.color(muted_text()),
|
.color(muted_text()),
|
||||||
Space::with_width(Length::Fill),
|
Space::with_width(Length::Fill),
|
||||||
icon(ICON_MODEL, 16),
|
icon(ICON_MODEL, 16),
|
||||||
text(
|
text(self.config.model.to_string()).size(12),
|
||||||
ModelChoice::from_id(&self.preferences.selected_model)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_string(),
|
|
||||||
)
|
|
||||||
.size(12),
|
|
||||||
action,
|
action,
|
||||||
]
|
]
|
||||||
.spacing(6)
|
.spacing(6)
|
||||||
|
|||||||
199
src/config.rs
Normal file
199
src/config.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
501
src/database.rs
501
src/database.rs
@@ -4,288 +4,10 @@ use std::collections::HashMap;
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::schema::{messages, preferences, projects, sessions};
|
use crate::schema::{messages, projects, sessions};
|
||||||
use crate::settings::{
|
|
||||||
DiagnosticPreferences, ExecutionPreferences, GenerationPreferences, KvCachePreferences,
|
|
||||||
ReasoningMode, RuntimePreferences, SpeculativePreferences, SsdPreferences, SteeringPreferences,
|
|
||||||
StreamingCacheBudget,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
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)]
|
#[derive(Clone, Debug, Identifiable, Queryable, Selectable)]
|
||||||
#[diesel(table_name = projects)]
|
#[diesel(table_name = projects)]
|
||||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
@@ -466,126 +188,6 @@ impl Database {
|
|||||||
.collect())
|
.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> {
|
pub fn create_project(&mut self, name: &str, path: &str) -> Result<Project, String> {
|
||||||
diesel::insert_into(projects::table)
|
diesel::insert_into(projects::table)
|
||||||
.values(NewProject { name, path })
|
.values(NewProject { name, path })
|
||||||
@@ -805,94 +407,6 @@ mod tests {
|
|||||||
let path = std::env::temp_dir().join(format!("ds4-server-{id}.sqlite3"));
|
let path = std::env::temp_dir().join(format!("ds4-server-{id}.sqlite3"));
|
||||||
let mut database = Database::open(&path).unwrap();
|
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 project = database.create_project("DS4", "/tmp/ds4").unwrap();
|
||||||
let first = database
|
let first = database
|
||||||
.create_session(project.id, "First session")
|
.create_session(project.id, "First session")
|
||||||
@@ -937,9 +451,6 @@ mod tests {
|
|||||||
let loaded = database.load_projects().unwrap();
|
let loaded = database.load_projects().unwrap();
|
||||||
assert_eq!(loaded[0].sessions[0].id, ordinary);
|
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);
|
assert!(!loaded[0].project.collapsed);
|
||||||
database.set_project_collapsed(project.id, true).unwrap();
|
database.set_project_collapsed(project.id, true).unwrap();
|
||||||
assert!(database.load_projects().unwrap()[0].project.collapsed);
|
assert!(database.load_projects().unwrap()[0].project.collapsed);
|
||||||
@@ -949,15 +460,7 @@ mod tests {
|
|||||||
drop(database);
|
drop(database);
|
||||||
|
|
||||||
let mut reopened = Database::open(&path).unwrap();
|
let mut reopened = Database::open(&path).unwrap();
|
||||||
let preferences = reopened.load_preferences().unwrap();
|
assert!(reopened.load_projects().unwrap().is_empty());
|
||||||
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);
|
|
||||||
drop(reopened);
|
drop(reopened);
|
||||||
fs::remove_file(path).unwrap();
|
fs::remove_file(path).unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
mod agent;
|
mod agent;
|
||||||
mod app;
|
mod app;
|
||||||
|
mod config;
|
||||||
mod database;
|
mod database;
|
||||||
mod engine;
|
mod engine;
|
||||||
mod metrics;
|
mod metrics;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ pub(crate) use transfer::{
|
|||||||
delete_managed_artifact, download_managed_artifact, validate_managed_artifact,
|
delete_managed_artifact, download_managed_artifact, validate_managed_artifact,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -56,11 +57,14 @@ const GLM: Artifact = Artifact {
|
|||||||
support: Some(false),
|
support: Some(false),
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
pub(crate) enum ModelChoice {
|
pub(crate) enum ModelChoice {
|
||||||
#[default]
|
#[default]
|
||||||
|
#[serde(rename = "deepseek-v4-flash")]
|
||||||
DeepSeekV4Flash,
|
DeepSeekV4Flash,
|
||||||
|
#[serde(rename = "deepseek-v4-pro")]
|
||||||
DeepSeekV4Pro,
|
DeepSeekV4Pro,
|
||||||
|
#[serde(rename = "glm-5.2")]
|
||||||
Glm52,
|
Glm52,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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! {
|
diesel::table! {
|
||||||
sessions (id) {
|
sessions (id) {
|
||||||
id -> Integer,
|
id -> Integer,
|
||||||
@@ -82,4 +33,4 @@ diesel::table! {
|
|||||||
|
|
||||||
diesel::joinable!(sessions -> projects (project_id));
|
diesel::joinable!(sessions -> projects (project_id));
|
||||||
diesel::joinable!(messages -> sessions (session_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);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use tools::{
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use tools::{canonical_tools, validate_tool_results};
|
use tools::{canonical_tools, validate_tool_results};
|
||||||
|
|
||||||
use crate::database::AppPreferences;
|
use crate::config::Config;
|
||||||
use crate::engine::ChatTurn;
|
use crate::engine::ChatTurn;
|
||||||
use crate::metrics::Metrics;
|
use crate::metrics::Metrics;
|
||||||
use crate::model::{self, ModelChoice};
|
use crate::model::{self, ModelChoice};
|
||||||
@@ -70,7 +70,7 @@ pub(crate) struct ServerHandle {
|
|||||||
|
|
||||||
struct State {
|
struct State {
|
||||||
generation: GenerationService,
|
generation: GenerationService,
|
||||||
preferences: Arc<RwLock<AppPreferences>>,
|
config: Arc<RwLock<Config>>,
|
||||||
models_path: PathBuf,
|
models_path: PathBuf,
|
||||||
cache_path: PathBuf,
|
cache_path: PathBuf,
|
||||||
sequence: AtomicU64,
|
sequence: AtomicU64,
|
||||||
@@ -212,7 +212,7 @@ struct RawOpenAiTool<'a> {
|
|||||||
impl ServerHandle {
|
impl ServerHandle {
|
||||||
pub(crate) fn spawn(
|
pub(crate) fn spawn(
|
||||||
generation: GenerationService,
|
generation: GenerationService,
|
||||||
preferences: Arc<RwLock<AppPreferences>>,
|
config: Arc<RwLock<Config>>,
|
||||||
models_path: PathBuf,
|
models_path: PathBuf,
|
||||||
cache_path: PathBuf,
|
cache_path: PathBuf,
|
||||||
port: u16,
|
port: u16,
|
||||||
@@ -230,7 +230,7 @@ impl ServerHandle {
|
|||||||
let tool_memory = read_tool_memory(&cache_path);
|
let tool_memory = read_tool_memory(&cache_path);
|
||||||
let state = Arc::new(State {
|
let state = Arc::new(State {
|
||||||
generation,
|
generation,
|
||||||
preferences,
|
config,
|
||||||
models_path,
|
models_path,
|
||||||
cache_path,
|
cache_path,
|
||||||
sequence: AtomicU64::new(0),
|
sequence: AtomicU64::new(0),
|
||||||
@@ -417,11 +417,11 @@ fn handle(mut stream: TcpStream, state: &State) {
|
|||||||
let installed = installed_endpoint_models(&state.models_path);
|
let installed = installed_endpoint_models(&state.models_path);
|
||||||
let model = model_alias(id).filter(|model| installed.contains(model));
|
let model = model_alias(id).filter(|model| installed.contains(model));
|
||||||
if let Some(model) = model {
|
if let Some(model) = model {
|
||||||
let (context, tokens) = state
|
let (context, tokens) = state.config.read().map_or((32_768, 32_768), |config| {
|
||||||
.preferences
|
(
|
||||||
.read()
|
config.generation.context_tokens,
|
||||||
.map_or((32_768, 32_768), |preferences| {
|
config.generation.max_generated_tokens,
|
||||||
(preferences.context_tokens, preferences.max_generated_tokens)
|
)
|
||||||
});
|
});
|
||||||
send_json_with_cors(
|
send_json_with_cors(
|
||||||
&mut stream,
|
&mut stream,
|
||||||
@@ -549,11 +549,11 @@ fn installed_endpoint_models(models_path: &std::path::Path) -> Vec<ModelChoice>
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn models_json(state: &State) -> Value {
|
fn models_json(state: &State) -> Value {
|
||||||
let (context, tokens) = state
|
let (context, tokens) = state.config.read().map_or((32_768, 32_768), |config| {
|
||||||
.preferences
|
(
|
||||||
.read()
|
config.generation.context_tokens,
|
||||||
.map_or((32_768, 32_768), |preferences| {
|
config.generation.max_generated_tokens,
|
||||||
(preferences.context_tokens, preferences.max_generated_tokens)
|
)
|
||||||
});
|
});
|
||||||
json!({
|
json!({
|
||||||
"object": "list",
|
"object": "list",
|
||||||
@@ -735,7 +735,7 @@ mod tests {
|
|||||||
let metrics = Arc::new(Metrics::new(PathBuf::new().as_path()));
|
let metrics = Arc::new(Metrics::new(PathBuf::new().as_path()));
|
||||||
let state = State {
|
let state = State {
|
||||||
generation: GenerationService::spawn(Arc::clone(&metrics)).unwrap(),
|
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(),
|
models_path: PathBuf::new(),
|
||||||
cache_path: PathBuf::new(),
|
cache_path: PathBuf::new(),
|
||||||
sequence: AtomicU64::new(0),
|
sequence: AtomicU64::new(0),
|
||||||
@@ -773,7 +773,7 @@ mod tests {
|
|||||||
let metrics = Arc::new(Metrics::new(&directory));
|
let metrics = Arc::new(Metrics::new(&directory));
|
||||||
let state = State {
|
let state = State {
|
||||||
generation: GenerationService::spawn(Arc::clone(&metrics)).unwrap(),
|
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(),
|
models_path: PathBuf::new(),
|
||||||
cache_path: directory.clone(),
|
cache_path: directory.clone(),
|
||||||
sequence: AtomicU64::new(0),
|
sequence: AtomicU64::new(0),
|
||||||
@@ -794,7 +794,7 @@ mod tests {
|
|||||||
let metrics = Arc::new(Metrics::new(PathBuf::new().as_path()));
|
let metrics = Arc::new(Metrics::new(PathBuf::new().as_path()));
|
||||||
let state = State {
|
let state = State {
|
||||||
generation: GenerationService::spawn(Arc::clone(&metrics)).unwrap(),
|
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(),
|
models_path: PathBuf::new(),
|
||||||
cache_path: PathBuf::new(),
|
cache_path: PathBuf::new(),
|
||||||
sequence: AtomicU64::new(0),
|
sequence: AtomicU64::new(0),
|
||||||
@@ -847,7 +847,7 @@ mod tests {
|
|||||||
let metrics = Arc::new(Metrics::new(PathBuf::new().as_path()));
|
let metrics = Arc::new(Metrics::new(PathBuf::new().as_path()));
|
||||||
let state = State {
|
let state = State {
|
||||||
generation: GenerationService::spawn(Arc::clone(&metrics)).unwrap(),
|
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(),
|
models_path: PathBuf::new(),
|
||||||
cache_path: PathBuf::new(),
|
cache_path: PathBuf::new(),
|
||||||
sequence: AtomicU64::new(0),
|
sequence: AtomicU64::new(0),
|
||||||
|
|||||||
@@ -484,13 +484,13 @@ pub(super) fn parse_chat_request(
|
|||||||
return Err((400, "missing messages".into()));
|
return Err((400, "missing messages".into()));
|
||||||
}
|
}
|
||||||
let requested_id = request.model.as_deref().unwrap_or_default();
|
let requested_id = request.model.as_deref().unwrap_or_default();
|
||||||
let preferences = state
|
let config = state
|
||||||
.preferences
|
.config
|
||||||
.read()
|
.read()
|
||||||
.map_err(|_| (500, "preferences are unavailable".to_owned()))?
|
.map_err(|_| (500, "preferences are unavailable".to_owned()))?
|
||||||
.clone();
|
.clone();
|
||||||
let model = if requested_id.is_empty() {
|
let model = if requested_id.is_empty() {
|
||||||
ModelChoice::from_id(&preferences.selected_model)
|
Some(config.model)
|
||||||
} else {
|
} else {
|
||||||
model_alias(requested_id)
|
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}")));
|
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.system_prompt.clear();
|
||||||
generation.max_generated_tokens = request
|
generation.max_generated_tokens = request
|
||||||
.max_completion_tokens
|
.max_completion_tokens
|
||||||
@@ -525,8 +525,7 @@ pub(super) fn parse_chat_request(
|
|||||||
protocol,
|
protocol,
|
||||||
)?;
|
)?;
|
||||||
generation.system_prompt = system;
|
generation.system_prompt = system;
|
||||||
let runtime = preferences.runtime().map_err(|error| (500, error))?;
|
let mut effective = effective_settings(model, &generation, &config.runtime, &state.models_path)
|
||||||
let mut effective = effective_settings(model, &generation, &runtime, &state.models_path)
|
|
||||||
.map_err(|error| (400, error))?;
|
.map_err(|error| (400, error))?;
|
||||||
effective.turn.top_k = request.top_k.unwrap_or(0);
|
effective.turn.top_k = request.top_k.unwrap_or(0);
|
||||||
if effective.turn.top_k < 0 {
|
if effective.turn.top_k < 0 {
|
||||||
@@ -549,7 +548,7 @@ pub(super) fn parse_chat_request(
|
|||||||
messages,
|
messages,
|
||||||
turn: effective.turn,
|
turn: effective.turn,
|
||||||
engine: effective.engine,
|
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,
|
stream: request.stream,
|
||||||
include_usage: request
|
include_usage: request
|
||||||
.stream_options
|
.stream_options
|
||||||
|
|||||||
101
src/settings.rs
101
src/settings.rs
@@ -1,4 +1,5 @@
|
|||||||
use crate::model::{self, EngineArtifacts, ModelChoice};
|
use crate::model::{self, EngineArtifacts, ModelChoice};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::path::Path;
|
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_COLD_MAX_TOKENS: u32 = 30_000;
|
||||||
const DEFAULT_KV_CONTINUED_INTERVAL_TOKENS: u32 = 10_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) struct SpeculativePreferences {
|
||||||
pub(crate) mtp_draft_tokens: i32,
|
pub(crate) mtp_draft_tokens: i32,
|
||||||
pub(crate) mtp_margin: f32,
|
pub(crate) mtp_margin: f32,
|
||||||
@@ -94,13 +96,70 @@ pub(crate) struct EngineSpeculativeSettings {
|
|||||||
pub(crate) dspark_strict: bool,
|
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 {
|
pub(crate) enum StreamingCacheBudget {
|
||||||
Experts(u32),
|
Experts(u32),
|
||||||
Gib(u64),
|
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) struct SsdPreferences {
|
||||||
pub(crate) enabled: bool,
|
pub(crate) enabled: bool,
|
||||||
pub(crate) cold: 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
|
/// Disk KV cache management, mirroring ds4's `--kv-disk-space-mb` budget and
|
||||||
/// its store-side gates. Unset values keep the DS4 defaults.
|
/// 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) struct KvCachePreferences {
|
||||||
pub(crate) budget_gib: Option<u64>,
|
pub(crate) budget_gib: Option<u64>,
|
||||||
pub(crate) min_tokens: Option<u32>,
|
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) struct SteeringPreferences {
|
||||||
pub(crate) file: Option<String>,
|
pub(crate) file: Option<String>,
|
||||||
pub(crate) ffn_scale: Option<f32>,
|
pub(crate) ffn_scale: Option<f32>,
|
||||||
@@ -270,7 +331,8 @@ pub(crate) struct EngineSteeringSettings {
|
|||||||
pub(crate) attention_scale: f32,
|
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) struct DiagnosticPreferences {
|
||||||
pub(crate) simulated_used_memory_gib: Option<u64>,
|
pub(crate) simulated_used_memory_gib: Option<u64>,
|
||||||
pub(crate) expert_profile_path: Option<String>,
|
pub(crate) expert_profile_path: Option<String>,
|
||||||
@@ -298,7 +360,8 @@ pub(crate) struct EngineDiagnosticSettings {
|
|||||||
pub(crate) expert_profile_path: Option<String>,
|
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) struct RuntimePreferences {
|
||||||
pub(crate) execution: ExecutionPreferences,
|
pub(crate) execution: ExecutionPreferences,
|
||||||
pub(crate) speculative: SpeculativePreferences,
|
pub(crate) speculative: SpeculativePreferences,
|
||||||
@@ -354,7 +417,8 @@ pub(crate) struct EngineSettings {
|
|||||||
pub(crate) diagnostics: EngineDiagnosticSettings,
|
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) struct ExecutionPreferences {
|
||||||
pub(crate) cpu_threads: Option<u32>,
|
pub(crate) cpu_threads: Option<u32>,
|
||||||
pub(crate) power_percent: Option<u8>,
|
pub(crate) power_percent: Option<u8>,
|
||||||
@@ -420,28 +484,16 @@ pub(crate) struct EngineExecutionSettings {
|
|||||||
pub(crate) warm_weights: bool,
|
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 {
|
pub(crate) enum ReasoningMode {
|
||||||
|
#[serde(rename = "none")]
|
||||||
Direct,
|
Direct,
|
||||||
#[default]
|
#[default]
|
||||||
High,
|
High,
|
||||||
Max,
|
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 {
|
impl fmt::Display for ReasoningMode {
|
||||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
formatter.write_str(match self {
|
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) struct GenerationPreferences {
|
||||||
pub(crate) context_tokens: i32,
|
pub(crate) context_tokens: i32,
|
||||||
pub(crate) max_generated_tokens: i32,
|
pub(crate) max_generated_tokens: i32,
|
||||||
|
|||||||
Reference in New Issue
Block a user