From 2a3de1712d3faeaa5905bec4bf3c077798bff08a Mon Sep 17 00:00:00 2001 From: eraflo <81640753+eraflo@users.noreply.github.com> Date: Thu, 8 Oct 2026 23:24:19 +0200 Subject: [PATCH 01/26] docs: drop the beta notice ahead of v1.0.0 Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++-- site/index.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 17d252d..54009dd 100644 --- a/README.md +++ b/README.md @@ -78,8 +78,8 @@ every web page you visit. 3. Chat with it, or create an API key in **Server › API keys** and connect an app. > [!NOTE] -> Calcine is in beta and its installers aren't code-signed yet, so Windows SmartScreen asks -> for confirmation before installing (**More info › Run anyway**). Releases list SHA-256 +> Calcine's installers aren't code-signed yet, so Windows SmartScreen asks for +> confirmation before installing (**More info › Run anyway**). Releases list SHA-256 > checksums, and stable releases also carry an SBOM and a build provenance attestation > (`gh attestation verify -R eraflo/Calcine`). diff --git a/site/index.html b/site/index.html index d022e96..5955af2 100644 --- a/site/index.html +++ b/site/index.html @@ -324,7 +324,7 @@

Get started in three steps

Windows 11 ARM64 · GenieX included

- Calcine is in beta and not code-signed yet: Windows SmartScreen asks for confirmation + Calcine isn't code-signed yet: Windows SmartScreen asks for confirmation before installing (More info › Run anyway).

From 8dd91558c1b7bbeef6dea1503d99c02fe3b16c6b Mon Sep 17 00:00:00 2001 From: eraflo <81640753+eraflo@users.noreply.github.com> Date: Thu, 8 Oct 2026 23:55:50 +0200 Subject: [PATCH 02/26] feat: sampling presets, advanced chat options and model loading settings Chat: Precise / Balanced / Creative presets, top-p, top-k, min-p, penalties, seed, stop sequences, offloaded layers, and saved system prompts. Optional settings are only sent when set, so GenieX's defaults apply otherwise. Measured on GenieX 0.8: seed and ngl only apply to llama.cpp models, and stop is ignored over the API, so the chat cuts the reply at the first stop sequence itself. nctx sent per request is ignored too: the context window is now a server option. Server page: Model loading card with the unload delay (--keepalive) and the llama.cpp context window (--nctx), saved in server.json. Changing them restarts a running GenieX. Co-Authored-By: Claude Opus 5.5 --- crates/calcine-core/src/runtime/mod.rs | 2 +- crates/calcine-core/src/runtime/server.rs | 78 ++++ crates/calcine-geniex/src/backend/mod.rs | 2 +- crates/calcine-geniex/src/backend/serve.rs | 49 +-- crates/calcine-geniex/src/lib.rs | 2 +- crates/calcine-geniex/tests/real_geniex.rs | 6 +- crates/calcine-mock/src/server.rs | 17 +- docs/cli-coverage.md | 4 +- src-tauri/src/ipc/commands/server.rs | 22 ++ src-tauri/src/ipc/mod.rs | 2 + src-tauri/src/setup/mod.rs | 9 + src-tauri/src/setup/server_options.rs | 58 +++ src-tauri/src/setup/services.rs | 4 +- .../chat/components/settings-panel.tsx | 339 ++++++++++++++++-- .../chat/components/system-prompts.tsx | 107 ++++++ src/features/chat/lib/inputs.test.ts | 23 ++ src/features/chat/lib/inputs.ts | 27 ++ src/features/chat/lib/request.test.ts | 76 +++- src/features/chat/lib/request.ts | 90 ++++- src/features/chat/messages.ts | 82 +++++ src/features/chat/store.ts | 32 +- src/features/chat/use-send.ts | 24 +- src/features/server/api.ts | 15 + .../server/components/loading-card.tsx | 88 +++++ src/features/server/messages.ts | 33 ++ src/features/server/server-page.tsx | 2 + src/lib/bindings.ts | 15 + 27 files changed, 1144 insertions(+), 64 deletions(-) create mode 100644 src-tauri/src/setup/server_options.rs create mode 100644 src/features/chat/components/system-prompts.tsx create mode 100644 src/features/chat/lib/inputs.test.ts create mode 100644 src/features/chat/lib/inputs.ts create mode 100644 src/features/server/components/loading-card.tsx diff --git a/crates/calcine-core/src/runtime/mod.rs b/crates/calcine-core/src/runtime/mod.rs index 69f0490..dd8d177 100644 --- a/crates/calcine-core/src/runtime/mod.rs +++ b/crates/calcine-core/src/runtime/mod.rs @@ -7,7 +7,7 @@ mod types; mod update; pub use manager::RuntimeManager; -pub use server::{InferenceServer, ServerState}; +pub use server::{InferenceServer, ServerOptions, ServerState}; pub use types::RuntimeInfo; pub use update::{ CachedInstaller, InstallSource, InstallerAsset, ReleaseChannel, RuntimeInstaller, diff --git a/crates/calcine-core/src/runtime/server.rs b/crates/calcine-core/src/runtime/server.rs index 6ddc405..f209210 100644 --- a/crates/calcine-core/src/runtime/server.rs +++ b/crates/calcine-core/src/runtime/server.rs @@ -28,6 +28,51 @@ pub enum ServerState { }, } +/// Defaults `geniex serve` starts with. Changing them restarts the server. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase", default)] +pub struct ServerOptions { + /// Unload the model after this many idle seconds (`--keepalive`). + pub keepalive_secs: u32, + /// Context window of llama.cpp models, in tokens (`--nctx`). AI Hub + /// models have theirs compiled in. + pub context_size: u32, +} + +impl ServerOptions { + pub const MIN_KEEPALIVE_SECS: u32 = 30; + pub const MIN_CONTEXT_SIZE: u32 = 512; + pub const MAX_CONTEXT_SIZE: u32 = 131_072; + + /// Why these options can't be used, if they can't. + pub fn validate(&self) -> std::result::Result<(), String> { + if self.keepalive_secs < Self::MIN_KEEPALIVE_SECS { + return Err(format!( + "keep the model loaded for at least {} seconds", + Self::MIN_KEEPALIVE_SECS + )); + } + if !(Self::MIN_CONTEXT_SIZE..=Self::MAX_CONTEXT_SIZE).contains(&self.context_size) { + return Err(format!( + "choose a context window between {} and {} tokens", + Self::MIN_CONTEXT_SIZE, + Self::MAX_CONTEXT_SIZE + )); + } + Ok(()) + } +} + +impl Default for ServerOptions { + /// GenieX's own defaults. + fn default() -> Self { + Self { + keepalive_secs: 300, + context_size: 4096, + } + } +} + /// Runs the OpenAI-compatible inference server on demand. #[async_trait] pub trait InferenceServer: Send + Sync { @@ -43,4 +88,37 @@ pub trait InferenceServer: Send + Sync { /// Recent output lines of the server process, oldest first. fn logs(&self) -> Vec; + + /// Options the server starts with. + fn options(&self) -> ServerOptions; + + /// Change the options. A running server restarts with them. + async fn set_options(&self, options: ServerOptions) -> Result<()>; +} + +#[cfg(test)] +mod tests { + use super::ServerOptions; + + #[test] + fn validates_ranges() { + assert!(ServerOptions::default().validate().is_ok()); + let short = ServerOptions { + keepalive_secs: 5, + ..ServerOptions::default() + }; + assert!(short.validate().is_err()); + let huge = ServerOptions { + context_size: 1 << 20, + ..ServerOptions::default() + }; + assert!(huge.validate().is_err()); + } + + #[test] + fn missing_fields_take_defaults() { + let options: ServerOptions = serde_json::from_str(r#"{"keepaliveSecs": 60}"#).unwrap(); + assert_eq!(options.context_size, 4096); + assert_eq!(options.keepalive_secs, 60); + } } diff --git a/crates/calcine-geniex/src/backend/mod.rs b/crates/calcine-geniex/src/backend/mod.rs index c4805c6..b5db4cf 100644 --- a/crates/calcine-geniex/src/backend/mod.rs +++ b/crates/calcine-geniex/src/backend/mod.rs @@ -5,4 +5,4 @@ mod pull; mod runtime; mod serve; -pub use serve::{GeniexServer, ServeOptions}; +pub use serve::GeniexServer; diff --git a/crates/calcine-geniex/src/backend/serve.rs b/crates/calcine-geniex/src/backend/serve.rs index 9094dd5..8293dee 100644 --- a/crates/calcine-geniex/src/backend/serve.rs +++ b/crates/calcine-geniex/src/backend/serve.rs @@ -8,7 +8,7 @@ use std::sync::{Arc, Mutex, PoisonError}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use async_trait::async_trait; -use calcine_core::runtime::{InferenceServer, ServerState}; +use calcine_core::runtime::{InferenceServer, ServerOptions, ServerState}; use calcine_core::{Error, Result}; use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader}; use tokio::sync::{oneshot, watch}; @@ -21,26 +21,11 @@ const LOG_LINES: usize = 500; const STARTUP_TIMEOUT: Duration = Duration::from_secs(60); const HEALTH_INTERVAL: Duration = Duration::from_millis(250); -/// Options passed to `geniex serve`. -#[derive(Debug, Clone)] -pub struct ServeOptions { - /// Unload the model after this many idle seconds (`--keepalive`). - pub keepalive_secs: u32, -} - -impl Default for ServeOptions { - fn default() -> Self { - Self { - keepalive_secs: 300, - } - } -} - /// The supervised `geniex serve` process. #[derive(Debug)] pub struct GeniexServer { geniex: Geniex, - options: ServeOptions, + options: Mutex, running: tokio::sync::Mutex>, state: watch::Sender, logs: Arc>>, @@ -55,10 +40,10 @@ struct Running { } impl GeniexServer { - pub fn new(geniex: Geniex, options: ServeOptions) -> Self { + pub fn new(geniex: Geniex, options: ServerOptions) -> Self { Self { geniex, - options, + options: Mutex::new(options), running: tokio::sync::Mutex::new(None), state: watch::Sender::new(ServerState::Stopped), logs: Arc::new(Mutex::new(VecDeque::with_capacity(LOG_LINES))), @@ -69,7 +54,9 @@ impl GeniexServer { async fn start(&self) -> Result { let port = free_port()?; let host = format!("127.0.0.1:{port}"); - let keepalive = self.options.keepalive_secs.to_string(); + let options = self.options(); + let keepalive = options.keepalive_secs.to_string(); + let context_size = options.context_size.to_string(); // The server is internal: only the gateway talks to it, never a browser. let args = [ "serve", @@ -79,6 +66,8 @@ impl GeniexServer { "http://127.0.0.1", "--keepalive", &keepalive, + "--nctx", + &context_size, ]; let mut child = self.geniex.cli()?.command(&args).spawn()?; tracing::info!(%host, "starting geniex serve"); @@ -192,6 +181,26 @@ impl InferenceServer for GeniexServer { .cloned() .collect() } + + fn options(&self) -> ServerOptions { + *self.options.lock().unwrap_or_else(PoisonError::into_inner) + } + + async fn set_options(&self, options: ServerOptions) -> Result<()> { + options.validate().map_err(Error::InvalidInput)?; + *self.options.lock().unwrap_or_else(PoisonError::into_inner) = options; + let was_running = self + .running + .lock() + .await + .as_ref() + .is_some_and(|r| !r.exited.load(Ordering::SeqCst)); + if was_running { + self.stop().await?; + self.ensure_running().await?; + } + Ok(()) + } } /// Wait for the process to exit or for a stop request, and report a crash. diff --git a/crates/calcine-geniex/src/lib.rs b/crates/calcine-geniex/src/lib.rs index 0aa7e9f..8e9b425 100644 --- a/crates/calcine-geniex/src/lib.rs +++ b/crates/calcine-geniex/src/lib.rs @@ -9,7 +9,7 @@ //! - [`update`]: installing and updating GenieX with its official installer mod backend; -pub use backend::{GeniexServer, ServeOptions}; +pub use backend::GeniexServer; pub mod cli; pub mod parse; pub mod update; diff --git a/crates/calcine-geniex/tests/real_geniex.rs b/crates/calcine-geniex/tests/real_geniex.rs index 6e3f767..6957c6a 100644 --- a/crates/calcine-geniex/tests/real_geniex.rs +++ b/crates/calcine-geniex/tests/real_geniex.rs @@ -57,10 +57,10 @@ async fn lists_the_ai_hub_catalog_for_this_device() { #[tokio::test] #[ignore = "needs a local GenieX install"] async fn serve_starts_answers_and_stops() { - use calcine_core::runtime::{InferenceServer, ServerState}; - use calcine_geniex::{GeniexServer, ServeOptions}; + use calcine_core::runtime::{InferenceServer, ServerOptions, ServerState}; + use calcine_geniex::GeniexServer; - let server = GeniexServer::new(Geniex::default(), ServeOptions::default()); + let server = GeniexServer::new(Geniex::default(), ServerOptions::default()); let url = server .ensure_running() .await diff --git a/crates/calcine-mock/src/server.rs b/crates/calcine-mock/src/server.rs index c9e2e7a..64bc9eb 100644 --- a/crates/calcine-mock/src/server.rs +++ b/crates/calcine-mock/src/server.rs @@ -4,6 +4,7 @@ //! `enable_think` is false), so the gateway and the Chat page work anywhere. use std::convert::Infallible; +use std::sync::{Mutex, PoisonError}; use std::time::Duration; use async_trait::async_trait; @@ -13,7 +14,7 @@ use axum::http::header; use axum::response::{IntoResponse, Response}; use axum::routing::{get, post}; use calcine_core::Result; -use calcine_core::runtime::{InferenceServer, ServerState}; +use calcine_core::runtime::{InferenceServer, ServerOptions, ServerState}; use serde_json::{Value, json}; use tokio::sync::{mpsc, oneshot, watch}; use tokio_stream::wrappers::ReceiverStream; @@ -24,6 +25,7 @@ const TOKEN_DELAY: Duration = Duration::from_millis(25); pub struct MockServer { running: tokio::sync::Mutex)>>, state: watch::Sender, + options: Mutex, } impl Default for MockServer { @@ -31,6 +33,7 @@ impl Default for MockServer { Self { running: tokio::sync::Mutex::new(None), state: watch::Sender::new(ServerState::Stopped), + options: Mutex::new(ServerOptions::default()), } } } @@ -79,6 +82,18 @@ impl InferenceServer for MockServer { fn logs(&self) -> Vec { vec!["Mock inference server: replies are canned, no model runs.".into()] } + + fn options(&self) -> ServerOptions { + *self.options.lock().unwrap_or_else(PoisonError::into_inner) + } + + async fn set_options(&self, options: ServerOptions) -> Result<()> { + options + .validate() + .map_err(calcine_core::Error::InvalidInput)?; + *self.options.lock().unwrap_or_else(PoisonError::into_inner) = options; + Ok(()) + } } async fn chat_completions(Json(request): Json) -> Response { diff --git a/docs/cli-coverage.md b/docs/cli-coverage.md index 5f25326..808e0e3 100644 --- a/docs/cli-coverage.md +++ b/docs/cli-coverage.md @@ -16,8 +16,8 @@ command or flag becomes available in the app. Reference: GenieX v0.8.0. | `remove --yes ` | — | ✅ `Services::remove_models` | ✅ Library (model or one precision) | Stops `geniex serve` first (Windows can't delete a loaded model) | | `clean --yes` | — | ✅ `Services::clean_models` | ✅ Settings › Storage › Danger zone | Typed confirmation; stops `geniex serve` first | | `model set-type ` | — | ✅ `ModelStore::set_type` | ✅ Library (Text / Vision on each model) | Always passes the type, so the interactive picker never opens | -| `serve --host 127.0.0.1: --origins --keepalive` | — | ✅ `InferenceServer` (`GeniexServer`, supervised) | ✅ Server page (start/stop, output), on demand from the gateway | Never exposed directly; see security-model.md | -| `infer` / `run` | — | via `/v1/chat/completions` | ✅ Chat (streaming, reasoning, temperature, max tokens, think, compute, power mode, images and microphone for VLMs), Hardware › Self-test | The REPL itself isn't wrapped; the HTTP API takes the same options | +| `serve --host 127.0.0.1: --origins --keepalive --nctx` | — | ✅ `InferenceServer` (`GeniexServer`, supervised), `ServerOptions` saved in `server.json` | ✅ Server page (start/stop, output, Model loading: unload delay and llama.cpp context window), on demand from the gateway | Never exposed directly; see security-model.md. Changing the options restarts it. `nctx` sent per request is ignored by GenieX 0.8, hence a server option | +| `infer` / `run` | — | via `/v1/chat/completions` | ✅ Chat (streaming, reasoning, presets, temperature, top-p/k, min-p, penalties, seed, stop sequences, max tokens, think, compute, offloaded layers, power mode, saved system prompts, images and microphone for VLMs), Hardware › Self-test | The REPL itself isn't wrapped; the HTTP API takes the same options. Measured on GenieX 0.8: `seed` and `ngl` only apply to llama.cpp models, and `stop` is ignored over the API, so the Chat cuts the reply itself. Not yet: grammars, speculative decoding (M5) | | `update` | — | ✅ `RuntimeInstaller` (`calcine-geniex::update`), `Services::start_runtime_install` | ✅ Hardware › GenieX runtime › Updates (stable / pre-releases, roll back, repair), sidebar badge | Not wrapped: Calcine reads the same `index.json` and manifests, downloads the installer with progress, checks its SHA-256 and Authenticode signature, stops `geniex serve`, runs the installer silently, checks `geniex version`, restarts the server. Installers are cached to roll back | | `completion` | — | — | — | M5 | diff --git a/src-tauri/src/ipc/commands/server.rs b/src-tauri/src/ipc/commands/server.rs index eee53fc..20ec392 100644 --- a/src-tauri/src/ipc/commands/server.rs +++ b/src-tauri/src/ipc/commands/server.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use calcine_core::Services; +use calcine_core::runtime::ServerOptions; use calcine_gateway::{ ApiKeyInfo, CreatedApiKey, Gateway, GatewaySettings, GatewayStatus, KeyError, NewApiKey, RequestEntry, @@ -10,6 +11,7 @@ use specta::Type; use tauri::State; use crate::ipc::error::{ApiError, ApiResult}; +use crate::setup::ServerOptionsFile; /// How Calcine's own UI talks to the gateway. #[derive(Debug, Serialize, Type)] @@ -56,6 +58,26 @@ pub fn server_logs(services: State<'_, Services>) -> Vec { services.server.logs() } +/// What `geniex serve` starts with: model unload delay, context window. +#[tauri::command] +#[specta::specta] +pub fn server_options(services: State<'_, Services>) -> ServerOptions { + services.server.options() +} + +/// Save new server options. A running GenieX restarts to apply them. +#[tauri::command] +#[specta::specta] +pub async fn set_server_options( + services: State<'_, Services>, + file: State<'_, ServerOptionsFile>, + options: ServerOptions, +) -> ApiResult<()> { + options.validate().map_err(ApiError::invalid_input)?; + file.save(options).map_err(ApiError::io)?; + Ok(services.server.set_options(options).await?) +} + #[tauri::command] #[specta::specta] pub async fn set_require_api_key(gateway: State<'_, Arc>, require: bool) -> ApiResult<()> { diff --git a/src-tauri/src/ipc/mod.rs b/src-tauri/src/ipc/mod.rs index 195507b..1ae5493 100644 --- a/src-tauri/src/ipc/mod.rs +++ b/src-tauri/src/ipc/mod.rs @@ -56,6 +56,8 @@ pub fn builder() -> Builder { commands::server::start_server, commands::server::stop_server, commands::server::server_logs, + commands::server::server_options, + commands::server::set_server_options, commands::server::set_require_api_key, commands::server::gateway_settings, commands::server::set_gateway_port, diff --git a/src-tauri/src/setup/mod.rs b/src-tauri/src/setup/mod.rs index a4b56f1..96a495a 100644 --- a/src-tauri/src/setup/mod.rs +++ b/src-tauri/src/setup/mod.rs @@ -1,6 +1,7 @@ //! Build the app state at startup and tear it down on exit. mod gateway; +mod server_options; mod services; use std::sync::Arc; @@ -10,6 +11,7 @@ use calcine_gateway::Gateway; use tauri::path::BaseDirectory; use tauri::{App, AppHandle, Manager}; +pub use self::server_options::ServerOptionsFile; use self::services::{AppPaths, services_from_env}; /// Build the services, start the gateway, and forward state changes to the @@ -29,6 +31,13 @@ pub fn start(app: &App) -> tauri::Result { app.manage(services.clone()); let data_dir = app.path().app_data_dir()?; + // The server isn't running yet: this only sets what it starts with. + let options_file = ServerOptionsFile::new(&data_dir); + let options = options_file.load(); + if let Err(err) = tauri::async_runtime::block_on(services.server.set_options(options)) { + tracing::warn!(%err, "ignoring the saved server options"); + } + app.manage(options_file); let gateway = Arc::new(gateway::create(services.clone(), &data_dir)); app.manage(gateway.clone()); crate::ipc::events::forward(app.handle(), &services.jobs, &gateway); diff --git a/src-tauri/src/setup/server_options.rs b/src-tauri/src/setup/server_options.rs new file mode 100644 index 0000000..0ccdaa2 --- /dev/null +++ b/src-tauri/src/setup/server_options.rs @@ -0,0 +1,58 @@ +//! `geniex serve` options chosen on the Server page, kept across launches. + +use std::path::{Path, PathBuf}; + +use calcine_core::runtime::ServerOptions; + +/// Where the options are saved (`server.json` in the app's data folder). +#[derive(Debug, Clone)] +pub struct ServerOptionsFile(PathBuf); + +impl ServerOptionsFile { + pub fn new(data_dir: &Path) -> Self { + Self(data_dir.join("server.json")) + } + + /// The saved options, or GenieX's defaults when missing or invalid. + pub fn load(&self) -> ServerOptions { + std::fs::read(&self.0) + .ok() + .and_then(|bytes| serde_json::from_slice::(&bytes).ok()) + .filter(|options| options.validate().is_ok()) + .unwrap_or_default() + } + + pub fn save(&self, options: ServerOptions) -> std::io::Result<()> { + if let Some(dir) = self.0.parent() { + std::fs::create_dir_all(dir)?; + } + let json = serde_json::to_vec_pretty(&options).map_err(std::io::Error::other)?; + std::fs::write(&self.0, json) + } +} + +#[cfg(test)] +mod tests { + use calcine_core::runtime::ServerOptions; + + use super::ServerOptionsFile; + + #[test] + fn round_trips_and_falls_back_to_defaults() { + let dir = + std::env::temp_dir().join(format!("calcine-server-options-{}", std::process::id())); + let file = ServerOptionsFile::new(&dir); + assert_eq!(file.load(), ServerOptions::default()); + + let options = ServerOptions { + keepalive_secs: 900, + context_size: 8192, + }; + file.save(options).unwrap(); + assert_eq!(file.load(), options); + + std::fs::write(dir.join("server.json"), br#"{"keepaliveSecs": 1}"#).unwrap(); + assert_eq!(file.load(), ServerOptions::default()); + std::fs::remove_dir_all(dir).unwrap(); + } +} diff --git a/src-tauri/src/setup/services.rs b/src-tauri/src/setup/services.rs index 597f74d..e2aa9de 100644 --- a/src-tauri/src/setup/services.rs +++ b/src-tauri/src/setup/services.rs @@ -4,11 +4,11 @@ use std::path::PathBuf; use std::sync::Arc; use calcine_core::jobs::JobManager; +use calcine_core::runtime::ServerOptions; use calcine_core::{BackendKind, Services}; use calcine_geniex::update::RELEASE_ENDPOINT; use calcine_geniex::{ BundledInstaller, Geniex, GeniexConfig, GeniexInstaller, GeniexServer, InstallerConfig, - ServeOptions, }; use calcine_hub::{HubConfig, HubDirectory}; use calcine_hw::SystemProbe; @@ -46,7 +46,7 @@ pub fn services_from_env(paths: &AppPaths) -> Services { fn geniex_services(paths: &AppPaths) -> Services { let geniex = Geniex::new(GeniexConfig::default()); - let server = Arc::new(GeniexServer::new(geniex.clone(), ServeOptions::default())); + let server = Arc::new(GeniexServer::new(geniex.clone(), ServerOptions::default())); let installer = GeniexInstaller::new( geniex.clone(), InstallerConfig { diff --git a/src/features/chat/components/settings-panel.tsx b/src/features/chat/components/settings-panel.tsx index fb6309b..1109f02 100644 --- a/src/features/chat/components/settings-panel.tsx +++ b/src/features/chat/components/settings-panel.tsx @@ -1,9 +1,12 @@ import * as Collapsible from "@radix-ui/react-collapsible"; import { useQuery } from "@tanstack/react-query"; -import { ChevronRight } from "lucide-react"; -import { type ReactNode, useState } from "react"; +import { ChevronRight, Dices, X } from "lucide-react"; +import { type KeyboardEvent, type ReactNode, useState } from "react"; import { CopyButton } from "@/components/calcine/actions/copy-button"; -import { Field, Input, Select, Textarea } from "@/components/ui/field"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Field, Input, Select } from "@/components/ui/field"; +import { SegmentedControl } from "@/components/ui/segmented-control"; import { Switch } from "@/components/ui/switch"; import { gatewayQuery } from "@/features/server/api"; import { useT } from "@/i18n"; @@ -11,16 +14,22 @@ import { common } from "@/i18n/common"; import type { LocalModel } from "@/lib/api"; import { formatNumber } from "@/lib/format"; import { cn } from "@/lib/utils"; +import { clamp, escapeStop, parseOptional, unescapeStop } from "../lib/inputs"; import { + activePreset, buildChatRequest, type ChatSettings, + DEFAULT_SETTINGS, POWER_MODES, type PowerMode, + PRESETS, + type Preset, supportsComputeChoice, toCurl, } from "../lib/request"; import { messages } from "../messages"; import { useChat } from "../store"; +import { SystemPromptField } from "./system-prompts"; const COMPUTE_UNITS = [ { value: "npu", tone: "text-npu" }, @@ -28,6 +37,19 @@ const COMPUTE_UNITS = [ { value: "cpu", tone: "text-cpu" }, ] as const; +const ADVANCED_SAMPLING = [ + "topP", + "topK", + "minP", + "repetitionPenalty", + "presencePenalty", + "frequencyPenalty", + "seed", + "stop", +] as const satisfies readonly (keyof ChatSettings)[]; + +const MODEL_LOADING = ["gpuLayers"] as const satisfies readonly (keyof ChatSettings)[]; + /** Generation options, adapted to what the selected model supports. */ export function SettingsPanel({ model, @@ -40,19 +62,12 @@ export function SettingsPanel({ const tc = useT(common); const settings = useChat((state) => state.settings); const setSettings = useChat((state) => state.setSettings); - const canChooseCompute = supportsComputeChoice(model); + const llamaCpp = supportsComputeChoice(model); + const preset = activePreset(settings); return (