Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3575bf7e60 | ||
|
|
96af5fca98 |
+15
-1
@@ -3,7 +3,21 @@
|
|||||||
All notable changes to this project are documented here.
|
All notable changes to this project are documented here.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
||||||
|
|
||||||
## [0.2.29] - 2026-07-21
|
## [0.2.31] - 2026-07-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Structured O(1) Nyström Attention & Reasoning Control**: Enhanced settings drawer with structured parameter sections for O(1) Nyström Streaming Attention (`o1_m`, `o1_window`, `o1_sink`), Generation (Temp, Top-P, MaxTokens, System Prompt), Reasoning ("Disable reasoning" toggle mapping to `think_budget = 0` for Qwen3/3.5 models), and Runtime (`skip_mtp` via `CMF_MTP=0`).
|
||||||
|
- **Persistent Admin Token Configuration**: Support for explicit `token` field in `config/gateway.toml` under `[admin]` block to prevent authorization invalidation across restarts.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **JavaScript UI Helper Restoration**: Restored `opt(...)` helper in `web/js/ui.js` and `web/js/views/import.js` to ensure HF model import modal options render smoothly.
|
||||||
|
- **Engine Command Flag Accuracy**: Fixed `CMF_MTP=0` environment flag pass-through for `cortiq serve` processes.
|
||||||
|
|
||||||
|
## [0.2.30] - 2026-07-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Per-Model Generation & Reasoning Parameters**: Full support in Web UI and Gateway core for per-model generation defaults: Temperature, Top-P, Max Tokens, Think Budget (Reasoning mode), O(1) Constant-Memory Streaming Attention (`o1`), MTP (Multi-Token Prediction toggle `skip_mtp`), and default System Prompt.
|
||||||
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- **Duplicate Local Model Registration**: Fixed `duplicate managed model id` conflict in `register_import` by cleanly routing managed CMF models directly into `[[cmf.servers]]` with automatic free port allocation without pushing duplicate entries to `[[models]]`.
|
- **Duplicate Local Model Registration**: Fixed `duplicate managed model id` conflict in `register_import` by cleanly routing managed CMF models directly into `[[cmf.servers]]` with automatic free port allocation without pushing duplicate entries to `[[models]]`.
|
||||||
|
|||||||
Generated
+1
-1
@@ -285,7 +285,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cortiq-gateway"
|
name = "cortiq-gateway"
|
||||||
version = "0.2.29"
|
version = "0.2.30"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cortiq-gateway"
|
name = "cortiq-gateway"
|
||||||
version = "0.2.29"
|
version = "0.2.31"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.75"
|
rust-version = "1.75"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|||||||
@@ -1330,6 +1330,16 @@ async fn register_import(State(state): State<SharedState>, Path(job): Path<Strin
|
|||||||
port,
|
port,
|
||||||
threads: cfg.cmf.threads,
|
threads: cfg.cmf.threads,
|
||||||
gpu: cfg.cmf.gpu,
|
gpu: cfg.cmf.gpu,
|
||||||
|
temperature: None,
|
||||||
|
top_p: None,
|
||||||
|
max_tokens: None,
|
||||||
|
think_budget: None,
|
||||||
|
system_prompt: None,
|
||||||
|
o1: None,
|
||||||
|
o1_m: None,
|
||||||
|
o1_window: None,
|
||||||
|
o1_sink: None,
|
||||||
|
skip_mtp: false,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
cfg.models.push(crate::config::ModelCfg {
|
cfg.models.push(crate::config::ModelCfg {
|
||||||
@@ -1343,6 +1353,13 @@ async fn register_import(State(state): State<SharedState>, Path(job): Path<Strin
|
|||||||
kind: "chat".into(),
|
kind: "chat".into(),
|
||||||
api_key_env: None,
|
api_key_env: None,
|
||||||
caps: Vec::new(),
|
caps: Vec::new(),
|
||||||
|
temperature: None,
|
||||||
|
top_p: None,
|
||||||
|
max_tokens: None,
|
||||||
|
think_budget: None,
|
||||||
|
system_prompt: None,
|
||||||
|
o1: None,
|
||||||
|
skip_mtp: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
state.reload(cfg)?;
|
state.reload(cfg)?;
|
||||||
|
|||||||
+22
-2
@@ -290,11 +290,31 @@ fn spawn_serve(server: &CmfServer, cfg: &CmfCfg) -> Result<Child, String> {
|
|||||||
if server.threads > 1 {
|
if server.threads > 1 {
|
||||||
cmd.env("CMF_THREADS", server.threads.to_string());
|
cmd.env("CMF_THREADS", server.threads.to_string());
|
||||||
}
|
}
|
||||||
// Offload large matvecs to the GPU (Metal on macOS). Memory-bandwidth-bound
|
|
||||||
// on unified memory, so mostly a prefill win; opt-in.
|
|
||||||
if server.gpu {
|
if server.gpu {
|
||||||
cmd.env("CMF_GPU", "1");
|
cmd.env("CMF_GPU", "1");
|
||||||
}
|
}
|
||||||
|
// O(1) Nyström attention: layers spec + sub-parameters.
|
||||||
|
if let Some(o1) = &server.o1 {
|
||||||
|
let s = o1.trim();
|
||||||
|
if !s.is_empty() {
|
||||||
|
// Pass even "off" so it explicitly overrides a file converter hint.
|
||||||
|
cmd.arg("--o1").arg(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(m) = server.o1_m {
|
||||||
|
cmd.arg("--o1-m").arg(m.to_string());
|
||||||
|
}
|
||||||
|
if let Some(w) = server.o1_window {
|
||||||
|
cmd.arg("--o1-window").arg(w.to_string());
|
||||||
|
}
|
||||||
|
if let Some(sink) = server.o1_sink {
|
||||||
|
cmd.arg("--o1-sink").arg(sink.to_string());
|
||||||
|
}
|
||||||
|
// MTP (Multi-Token Prediction) speculative decoding: the engine reads
|
||||||
|
// CMF_MTP env; "0" disables. There is no CLI flag for this.
|
||||||
|
if server.skip_mtp {
|
||||||
|
cmd.env("CMF_MTP", "0");
|
||||||
|
}
|
||||||
cmd.stdout(Stdio::null())
|
cmd.stdout(Stdio::null())
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::null())
|
||||||
.kill_on_drop(true)
|
.kill_on_drop(true)
|
||||||
|
|||||||
+66
-2
@@ -83,6 +83,36 @@ pub struct CmfServer {
|
|||||||
/// Offload matvecs to the GPU (CMF_GPU=1).
|
/// Offload matvecs to the GPU (CMF_GPU=1).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub gpu: bool,
|
pub gpu: bool,
|
||||||
|
/// Default generation temperature (0.0 .. 2.0).
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub temperature: Option<f32>,
|
||||||
|
/// Default nucleus sampling threshold (0.0 .. 1.0).
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub top_p: Option<f32>,
|
||||||
|
/// Default maximum completion tokens.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub max_tokens: Option<u32>,
|
||||||
|
/// Reasoning / thinking token budget.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub think_budget: Option<u32>,
|
||||||
|
/// Default system prompt prepended if request lacks one.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub system_prompt: Option<String>,
|
||||||
|
/// O(1) Nyström attention layer spec (all | deepN | i,j,k | off).
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub o1: Option<String>,
|
||||||
|
/// O(1) landmark budget (default 32; ≥4).
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub o1_m: Option<u32>,
|
||||||
|
/// O(1) exact-window width (default 128).
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub o1_window: Option<u32>,
|
||||||
|
/// O(1) permanent exact sink keys (default 4, StreamingLLM discipline).
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub o1_sink: Option<u32>,
|
||||||
|
/// Disable Multi-Token Prediction speculative decoding (CMF_MTP=0).
|
||||||
|
#[serde(default)]
|
||||||
|
pub skip_mtp: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CMF-format model factory: import a HuggingFace model, convert it to a
|
/// CMF-format model factory: import a HuggingFace model, convert it to a
|
||||||
@@ -172,6 +202,16 @@ impl CmfCfg {
|
|||||||
port: self.local_port,
|
port: self.local_port,
|
||||||
threads: self.threads,
|
threads: self.threads,
|
||||||
gpu: self.gpu,
|
gpu: self.gpu,
|
||||||
|
temperature: None,
|
||||||
|
top_p: None,
|
||||||
|
max_tokens: None,
|
||||||
|
think_budget: None,
|
||||||
|
system_prompt: None,
|
||||||
|
o1: None,
|
||||||
|
o1_m: None,
|
||||||
|
o1_window: None,
|
||||||
|
o1_sink: None,
|
||||||
|
skip_mtp: false,
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
Vec::new()
|
Vec::new()
|
||||||
@@ -306,6 +346,27 @@ pub struct ModelCfg {
|
|||||||
pub api_key_env: Option<String>,
|
pub api_key_env: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
pub caps: Vec<String>,
|
pub caps: Vec<String>,
|
||||||
|
/// Default generation temperature (0.0 .. 2.0).
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub temperature: Option<f32>,
|
||||||
|
/// Default nucleus sampling threshold (0.0 .. 1.0).
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub top_p: Option<f32>,
|
||||||
|
/// Default maximum completion tokens.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub max_tokens: Option<u32>,
|
||||||
|
/// Reasoning / thinking token budget.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub think_budget: Option<u32>,
|
||||||
|
/// Default system prompt prepended if request lacks one.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub system_prompt: Option<String>,
|
||||||
|
/// O(1) constant-memory streaming attention hint (all, deep, custom, off).
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub o1: Option<String>,
|
||||||
|
/// Skip Multi-Token Prediction (MTP) heads.
|
||||||
|
#[serde(default)]
|
||||||
|
pub skip_mtp: bool,
|
||||||
}
|
}
|
||||||
fn default_kind() -> String {
|
fn default_kind() -> String {
|
||||||
"chat".into()
|
"chat".into()
|
||||||
@@ -461,10 +522,12 @@ pub struct AdminCfg {
|
|||||||
/// Whether the admin panel and admin API (`/admin`, `/admin/api/*`) are enabled.
|
/// Whether the admin panel and admin API (`/admin`, `/admin/api/*`) are enabled.
|
||||||
#[serde(default = "default_admin_enabled")]
|
#[serde(default = "default_admin_enabled")]
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
/// Name of the env variable holding the admin token. If unset (or empty),
|
/// Name of the env variable holding the admin token.
|
||||||
/// a token is generated at startup and printed to the log.
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub token_env: Option<String>,
|
pub token_env: Option<String>,
|
||||||
|
/// Persistent admin token configured directly in toml.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub token: Option<String>,
|
||||||
/// Optional separate bind address for the admin surface (e.g. `127.0.0.1:9001`).
|
/// Optional separate bind address for the admin surface (e.g. `127.0.0.1:9001`).
|
||||||
/// If unset, the admin interface lives on the main `listen` address under `/admin`.
|
/// If unset, the admin interface lives on the main `listen` address under `/admin`.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
@@ -475,6 +538,7 @@ impl Default for AdminCfg {
|
|||||||
Self {
|
Self {
|
||||||
enabled: default_admin_enabled(),
|
enabled: default_admin_enabled(),
|
||||||
token_env: None,
|
token_env: None,
|
||||||
|
token: None,
|
||||||
listen: None,
|
listen: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ fn resolve_admin_token(cfg: &Config, args: &Args) -> (String, bool) {
|
|||||||
return (t.clone(), false);
|
return (t.clone(), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(t) = &cfg.admin.token {
|
||||||
|
if !t.is_empty() {
|
||||||
|
return (t.clone(), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
(admin::random_token(24), true)
|
(admin::random_token(24), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ pub struct GenParams {
|
|||||||
pub temperature: Option<f32>,
|
pub temperature: Option<f32>,
|
||||||
pub max_tokens: Option<u32>,
|
pub max_tokens: Option<u32>,
|
||||||
pub top_p: Option<f32>,
|
pub top_p: Option<f32>,
|
||||||
|
pub think_budget: Option<u32>,
|
||||||
pub stop: Vec<String>,
|
pub stop: Vec<String>,
|
||||||
/// Fields we do not interpret but must proxy through to the provider.
|
/// Fields we do not interpret but must proxy through to the provider.
|
||||||
pub passthrough: serde_json::Map<String, serde_json::Value>,
|
pub passthrough: serde_json::Map<String, serde_json::Value>,
|
||||||
|
|||||||
+58
-1
@@ -341,7 +341,64 @@ impl Pipeline {
|
|||||||
route_source
|
route_source
|
||||||
);
|
);
|
||||||
|
|
||||||
match provider.chat(req.clone()).await {
|
let mut req_call = req.clone();
|
||||||
|
if let Some(m_cfg) = live.cfg.models.iter().find(|m| m.id == model_id) {
|
||||||
|
if req_call.params.temperature.is_none() {
|
||||||
|
req_call.params.temperature = m_cfg.temperature;
|
||||||
|
}
|
||||||
|
if req_call.params.top_p.is_none() {
|
||||||
|
req_call.params.top_p = m_cfg.top_p;
|
||||||
|
}
|
||||||
|
if req_call.params.max_tokens.is_none() {
|
||||||
|
req_call.params.max_tokens = m_cfg.max_tokens;
|
||||||
|
}
|
||||||
|
if req_call.params.think_budget.is_none() {
|
||||||
|
req_call.params.think_budget = m_cfg.think_budget;
|
||||||
|
}
|
||||||
|
if let Some(sys) = &m_cfg.system_prompt {
|
||||||
|
if !sys.trim().is_empty()
|
||||||
|
&& !req_call.messages.iter().any(|m| m.role == "system")
|
||||||
|
{
|
||||||
|
req_call.messages.insert(
|
||||||
|
0,
|
||||||
|
Message {
|
||||||
|
role: "system".into(),
|
||||||
|
content: sys.clone(),
|
||||||
|
tool_calls: Vec::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(s_cfg) = live.cfg.cmf.servers.iter().find(|s| s.id == model_id) {
|
||||||
|
if req_call.params.temperature.is_none() {
|
||||||
|
req_call.params.temperature = s_cfg.temperature;
|
||||||
|
}
|
||||||
|
if req_call.params.top_p.is_none() {
|
||||||
|
req_call.params.top_p = s_cfg.top_p;
|
||||||
|
}
|
||||||
|
if req_call.params.max_tokens.is_none() {
|
||||||
|
req_call.params.max_tokens = s_cfg.max_tokens;
|
||||||
|
}
|
||||||
|
if req_call.params.think_budget.is_none() {
|
||||||
|
req_call.params.think_budget = s_cfg.think_budget;
|
||||||
|
}
|
||||||
|
if let Some(sys) = &s_cfg.system_prompt {
|
||||||
|
if !sys.trim().is_empty()
|
||||||
|
&& !req_call.messages.iter().any(|m| m.role == "system")
|
||||||
|
{
|
||||||
|
req_call.messages.insert(
|
||||||
|
0,
|
||||||
|
Message {
|
||||||
|
role: "system".into(),
|
||||||
|
content: sys.clone(),
|
||||||
|
tool_calls: Vec::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match provider.chat(req_call).await {
|
||||||
Ok(mut resp) => {
|
Ok(mut resp) => {
|
||||||
let cost =
|
let cost =
|
||||||
provider.price(resp.usage.prompt_tokens, resp.usage.completion_tokens);
|
provider.price(resp.usage.prompt_tokens, resp.usage.completion_tokens);
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ async fn handler(
|
|||||||
temperature: req.temperature,
|
temperature: req.temperature,
|
||||||
max_tokens: req.max_tokens,
|
max_tokens: req.max_tokens,
|
||||||
top_p: req.top_p,
|
top_p: req.top_p,
|
||||||
|
think_budget: None,
|
||||||
stop: Vec::new(),
|
stop: Vec::new(),
|
||||||
passthrough: Default::default(),
|
passthrough: Default::default(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ struct OpenAiChatRequest {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
top_p: Option<f32>,
|
top_p: Option<f32>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
think_budget: Option<u32>,
|
||||||
|
#[serde(default)]
|
||||||
|
reasoning_effort: Option<u32>,
|
||||||
|
#[serde(default)]
|
||||||
stream: bool,
|
stream: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
tools: Vec<serde_json::Value>,
|
tools: Vec<serde_json::Value>,
|
||||||
@@ -93,6 +97,7 @@ async fn handler(
|
|||||||
temperature: req.temperature,
|
temperature: req.temperature,
|
||||||
max_tokens: req.max_tokens,
|
max_tokens: req.max_tokens,
|
||||||
top_p: req.top_p,
|
top_p: req.top_p,
|
||||||
|
think_budget: req.think_budget.or(req.reasoning_effort),
|
||||||
stop: Vec::new(),
|
stop: Vec::new(),
|
||||||
passthrough: req.rest,
|
passthrough: req.rest,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ async fn handler(
|
|||||||
temperature: req.temperature,
|
temperature: req.temperature,
|
||||||
max_tokens: req.max_tokens,
|
max_tokens: req.max_tokens,
|
||||||
top_p: req.top_p,
|
top_p: req.top_p,
|
||||||
|
think_budget: None,
|
||||||
stop: Vec::new(),
|
stop: Vec::new(),
|
||||||
passthrough: Default::default(),
|
passthrough: Default::default(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,9 +42,16 @@ impl OpenAiProvider {
|
|||||||
if let Some(t) = req.params.temperature {
|
if let Some(t) = req.params.temperature {
|
||||||
body["temperature"] = t.into();
|
body["temperature"] = t.into();
|
||||||
}
|
}
|
||||||
|
if let Some(p) = req.params.top_p {
|
||||||
|
body["top_p"] = p.into();
|
||||||
|
}
|
||||||
if let Some(m) = req.params.max_tokens {
|
if let Some(m) = req.params.max_tokens {
|
||||||
body["max_tokens"] = m.into();
|
body["max_tokens"] = m.into();
|
||||||
}
|
}
|
||||||
|
if let Some(tb) = req.params.think_budget {
|
||||||
|
body["think_budget"] = tb.into();
|
||||||
|
body["reasoning_effort"] = tb.into();
|
||||||
|
}
|
||||||
if !req.tools.is_empty() {
|
if !req.tools.is_empty() {
|
||||||
body["tools"] = serde_json::Value::Array(req.tools.clone());
|
body["tools"] = serde_json::Value::Array(req.tools.clone());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,13 @@ impl Registry {
|
|||||||
kind: "chat".into(),
|
kind: "chat".into(),
|
||||||
api_key_env: None,
|
api_key_env: None,
|
||||||
caps: Vec::new(),
|
caps: Vec::new(),
|
||||||
|
temperature: s.temperature,
|
||||||
|
top_p: s.top_p,
|
||||||
|
max_tokens: s.max_tokens,
|
||||||
|
think_budget: s.think_budget,
|
||||||
|
system_prompt: s.system_prompt.clone(),
|
||||||
|
o1: s.o1.clone(),
|
||||||
|
skip_mtp: s.skip_mtp,
|
||||||
};
|
};
|
||||||
by_id.insert(m.id.clone(), Arc::new(OpenAiProvider::new(&m, None)?));
|
by_id.insert(m.id.clone(), Arc::new(OpenAiProvider::new(&m, None)?));
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+12
@@ -139,6 +139,18 @@ export default {
|
|||||||
"models.form.apiKeyHint": "Stored securely on the gateway; it is not shown again.",
|
"models.form.apiKeyHint": "Stored securely on the gateway; it is not shown again.",
|
||||||
"models.form.keyStored": "A key is saved — leave blank to keep it, or paste a new one.",
|
"models.form.keyStored": "A key is saved — leave blank to keep it, or paste a new one.",
|
||||||
"models.form.keyFromEnv": "Using a key from an environment variable.",
|
"models.form.keyFromEnv": "Using a key from an environment variable.",
|
||||||
|
"models.form.paramsTitle": "Generation & Thinking Parameters (O(1), MTP, Temperature)",
|
||||||
|
"models.form.temperature": "Temperature",
|
||||||
|
"models.form.temperatureHint": "Generation temperature (0.0 = deterministic, 1.0+ = creative).",
|
||||||
|
"models.form.topP": "Top-P (Nucleus Sampling)",
|
||||||
|
"models.form.topPHint": "Sampling threshold (0.0 .. 1.0).",
|
||||||
|
"models.form.maxTokens": "Max Tokens",
|
||||||
|
"models.form.maxTokensHint": "Maximum completion tokens to generate.",
|
||||||
|
"models.form.thinkBudget": "Thinking Mode / Think Budget",
|
||||||
|
"models.form.thinkBudgetHint": "Reasoning token budget (0 = disabled, N > 0 = max reasoning tokens).",
|
||||||
|
"models.form.systemPrompt": "Default System Prompt",
|
||||||
|
"models.form.systemPromptHint": "Prepend automatically if request lacks a system prompt.",
|
||||||
|
"models.form.systemPromptPlaceholder": "You are a helpful assistant...",
|
||||||
"models.form.testKey": "Test & load models",
|
"models.form.testKey": "Test & load models",
|
||||||
"models.form.testing": "checking…",
|
"models.form.testing": "checking…",
|
||||||
"models.form.foundModels": "{n} models loaded",
|
"models.form.foundModels": "{n} models loaded",
|
||||||
|
|||||||
Vendored
+12
@@ -138,6 +138,18 @@ export default {
|
|||||||
"models.form.apiKeyHint": "Хранится безопасно на шлюзе; больше не показывается.",
|
"models.form.apiKeyHint": "Хранится безопасно на шлюзе; больше не показывается.",
|
||||||
"models.form.keyStored": "Ключ сохранён — оставьте пустым, чтобы не менять, или вставьте новый.",
|
"models.form.keyStored": "Ключ сохранён — оставьте пустым, чтобы не менять, или вставьте новый.",
|
||||||
"models.form.keyFromEnv": "Используется ключ из переменной окружения.",
|
"models.form.keyFromEnv": "Используется ключ из переменной окружения.",
|
||||||
|
"models.form.paramsTitle": "Параметры генерации, O(1) и режим размышления",
|
||||||
|
"models.form.temperature": "Температура",
|
||||||
|
"models.form.temperatureHint": "Температура генерации (0.0 — точные факты, 1.0+ — творчество).",
|
||||||
|
"models.form.topP": "Top-P (Nucleus Sampling)",
|
||||||
|
"models.form.topPHint": "Порог сэмплирования токенов (0.0 .. 1.0).",
|
||||||
|
"models.form.maxTokens": "Макс. токенов (Max Tokens)",
|
||||||
|
"models.form.maxTokensHint": "Максимальное количество генерируемых токенов.",
|
||||||
|
"models.form.thinkBudget": "Режим размышления / Think Budget",
|
||||||
|
"models.form.thinkBudgetHint": "Бюджет токенов мыслей (0 = выключен, N > 0 = лимит рассуждений).",
|
||||||
|
"models.form.systemPrompt": "Системный промпт по умолчанию",
|
||||||
|
"models.form.systemPromptHint": "Автоматически подставляется, если запрос не содержит системное сообщение.",
|
||||||
|
"models.form.systemPromptPlaceholder": "Вы — полезный ассистент...",
|
||||||
"models.form.testKey": "Проверить и загрузить модели",
|
"models.form.testKey": "Проверить и загрузить модели",
|
||||||
"models.form.testing": "проверка…",
|
"models.form.testing": "проверка…",
|
||||||
"models.form.foundModels": "загружено моделей: {n}",
|
"models.form.foundModels": "загружено моделей: {n}",
|
||||||
|
|||||||
Vendored
+4
@@ -23,6 +23,10 @@ export function clear(node) {
|
|||||||
while (node && node.firstChild) node.removeChild(node.firstChild);
|
while (node && node.firstChild) node.removeChild(node.firstChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function opt(v, label, sel) {
|
||||||
|
return h("option", { value: v, selected: sel ? true : null }, label || v);
|
||||||
|
}
|
||||||
|
|
||||||
export function mount(node, ...children) {
|
export function mount(node, ...children) {
|
||||||
clear(node);
|
clear(node);
|
||||||
for (const c of children.flat())
|
for (const c of children.flat())
|
||||||
|
|||||||
Vendored
+1
-4
@@ -2,7 +2,7 @@
|
|||||||
// live progress view. Search HF, pick quantization + advanced params, watch
|
// live progress view. Search HF, pick quantization + advanced params, watch
|
||||||
// the conversion (multiple in parallel, resumable across tab switches), then
|
// the conversion (multiple in parallel, resumable across tab switches), then
|
||||||
// register the result as a local model.
|
// register the result as a local model.
|
||||||
import { h, mount, toast } from "../ui.js";
|
import { h, mount, toast, opt } from "../ui.js";
|
||||||
import { t } from "../i18n.js";
|
import { t } from "../i18n.js";
|
||||||
import { api } from "../api.js";
|
import { api } from "../api.js";
|
||||||
|
|
||||||
@@ -31,9 +31,6 @@ function field(label, control, hint) {
|
|||||||
return h("label", { class: "field" }, h("span", {}, label), control,
|
return h("label", { class: "field" }, h("span", {}, label), control,
|
||||||
hint ? h("div", { class: "hint" }, hint) : null);
|
hint ? h("div", { class: "hint" }, hint) : null);
|
||||||
}
|
}
|
||||||
function opt(v, label, sel) {
|
|
||||||
return h("option", { value: v, selected: sel ? true : null }, label || v);
|
|
||||||
}
|
|
||||||
function fmtNum(n) {
|
function fmtNum(n) {
|
||||||
if (n == null) return "—";
|
if (n == null) return "—";
|
||||||
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
|
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
|
||||||
|
|||||||
Vendored
+39
-4
@@ -7,9 +7,6 @@ import { appState } from "../app.js";
|
|||||||
function field(label, control, hint) {
|
function field(label, control, hint) {
|
||||||
return h("label", { class: "field" }, h("span", {}, label), control, hint ? h("div", { class: "hint" }, hint) : null);
|
return h("label", { class: "field" }, h("span", {}, label), control, hint ? h("div", { class: "hint" }, hint) : null);
|
||||||
}
|
}
|
||||||
function opt(v, label, sel) {
|
|
||||||
return h("option", { value: v, selected: sel ? true : null }, label || v);
|
|
||||||
}
|
|
||||||
|
|
||||||
function keyBadge(src) {
|
function keyBadge(src) {
|
||||||
const map = { store: "store", env: "env", missing: "bad", none: "" };
|
const map = { store: "store", env: "env", missing: "bad", none: "" };
|
||||||
@@ -118,6 +115,36 @@ function modelForm(meta, existing) {
|
|||||||
: existing && existing.key_source === "env" ? t("models.form.keyFromEnv")
|
: existing && existing.key_source === "env" ? t("models.form.keyFromEnv")
|
||||||
: t("models.form.apiKeyHint");
|
: t("models.form.apiKeyHint");
|
||||||
|
|
||||||
|
// ⚙ Generation & Thinking Parameters
|
||||||
|
const tempIn = h("input", { type: "number", step: "0.05", min: "0", max: "2.0", placeholder: "0.7", value: m.temperature ?? "" });
|
||||||
|
const topPIn = h("input", { type: "number", step: "0.05", min: "0", max: "1.0", placeholder: "0.9", value: m.top_p ?? "" });
|
||||||
|
const maxTokIn = h("input", { type: "number", min: "1", placeholder: "2048", value: m.max_tokens ?? "" });
|
||||||
|
const thinkIn = h("input", { type: "number", min: "0", placeholder: "1024", value: m.think_budget ?? "" });
|
||||||
|
const o1Sel = h("select", {},
|
||||||
|
h("option", { value: "" }, t("import.adv.auto")),
|
||||||
|
h("option", { value: "all" }, "all — O(1) Attention"),
|
||||||
|
h("option", { value: "deep" }, "deep — O(1) Deep"),
|
||||||
|
h("option", { value: "off" }, "off — Standard"));
|
||||||
|
if (m.o1) o1Sel.value = m.o1;
|
||||||
|
const skipMtpCb = h("input", { type: "checkbox", checked: m.skip_mtp ? true : null });
|
||||||
|
const sysPromptIn = h("textarea", { rows: 2, placeholder: t("models.form.systemPromptPlaceholder"), value: m.system_prompt || "" });
|
||||||
|
|
||||||
|
const advParams = h(
|
||||||
|
"details",
|
||||||
|
{ class: "adv-params", style: "margin-top:12px;padding:10px;border:1px solid var(--border);border-radius:6px;" },
|
||||||
|
h("summary", { style: "font-weight:600;cursor:pointer;" }, "⚙ " + t("models.form.paramsTitle")),
|
||||||
|
h("div", { class: "row", style: "margin-top:8px" },
|
||||||
|
field(t("models.form.temperature"), tempIn, t("models.form.temperatureHint")),
|
||||||
|
field(t("models.form.topP"), topPIn, t("models.form.topPHint"))),
|
||||||
|
h("div", { class: "row" },
|
||||||
|
field(t("models.form.maxTokens"), maxTokIn, t("models.form.maxTokensHint")),
|
||||||
|
field(t("models.form.thinkBudget"), thinkIn, t("models.form.thinkBudgetHint"))),
|
||||||
|
h("div", { class: "row" },
|
||||||
|
field("O(1) Attention", o1Sel, t("import.adv.o1Hint")),
|
||||||
|
field("MTP", h("label", { class: "check" }, skipMtpCb, t("import.adv.skipMtp")))),
|
||||||
|
field(t("models.form.systemPrompt"), sysPromptIn, t("models.form.systemPromptHint"))
|
||||||
|
);
|
||||||
|
|
||||||
const node = h(
|
const node = h(
|
||||||
"div",
|
"div",
|
||||||
{},
|
{},
|
||||||
@@ -128,7 +155,8 @@ function modelForm(meta, existing) {
|
|||||||
dl,
|
dl,
|
||||||
h("div", { class: "row" }, field(t("models.form.kind"), kindSel), field(t("models.form.costTier"), tierSel)),
|
h("div", { class: "row" }, field(t("models.form.kind"), kindSel), field(t("models.form.costTier"), tierSel)),
|
||||||
h("div", { class: "row" }, field(t("models.form.priceIn"), priceInIn), field(t("models.form.priceOut"), priceOutIn)),
|
h("div", { class: "row" }, field(t("models.form.priceIn"), priceInIn), field(t("models.form.priceOut"), priceOutIn)),
|
||||||
field(t("models.form.caps"), h("div", { class: "flex wrap" }, ...capInputs.map(({ c, cb }) => h("label", { class: "check" }, cb, c))))
|
field(t("models.form.caps"), h("div", { class: "flex wrap" }, ...capInputs.map(({ c, cb }) => h("label", { class: "check" }, cb, c)))),
|
||||||
|
advParams
|
||||||
);
|
);
|
||||||
|
|
||||||
const getValue = () => {
|
const getValue = () => {
|
||||||
@@ -147,6 +175,13 @@ function modelForm(meta, existing) {
|
|||||||
price_out: parseFloat(priceOutIn.value) || 0,
|
price_out: parseFloat(priceOutIn.value) || 0,
|
||||||
api_key_env: hasKey ? keyEnv : null,
|
api_key_env: hasKey ? keyEnv : null,
|
||||||
caps: capInputs.filter(({ cb }) => cb.checked).map(({ c }) => c),
|
caps: capInputs.filter(({ cb }) => cb.checked).map(({ c }) => c),
|
||||||
|
temperature: tempIn.value !== "" ? parseFloat(tempIn.value) : null,
|
||||||
|
top_p: topPIn.value !== "" ? parseFloat(topPIn.value) : null,
|
||||||
|
max_tokens: maxTokIn.value !== "" ? parseInt(maxTokIn.value) : null,
|
||||||
|
think_budget: thinkIn.value !== "" ? parseInt(thinkIn.value) : null,
|
||||||
|
o1: o1Sel.value || null,
|
||||||
|
skip_mtp: skipMtpCb.checked,
|
||||||
|
system_prompt: sysPromptIn.value.trim() || null,
|
||||||
},
|
},
|
||||||
secret: typedKey,
|
secret: typedKey,
|
||||||
keyEnv,
|
keyEnv,
|
||||||
|
|||||||
Vendored
+96
-2
@@ -253,9 +253,103 @@ export async function renderSettings() {
|
|||||||
} catch (e) { mount(st, String(e.message || e)); }
|
} catch (e) { mount(st, String(e.message || e)); }
|
||||||
checkBtn.disabled = false;
|
checkBtn.disabled = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── O(1) Nyström Attention ──
|
||||||
|
// NOT a "reasoning mode". This is a memory-efficient streaming attention
|
||||||
|
// kernel (Nyström/landmark) that replaces standard KV-cache attention,
|
||||||
|
// giving O(1) memory per decode step instead of O(n).
|
||||||
|
const o1Sel = h("select", { style: "width:120px", title: "O(1) Nyström attention layer spec: which layers replace KV-cache with streaming Nyström attention" },
|
||||||
|
h("option", { value: "" }, "O(1) авто"),
|
||||||
|
h("option", { value: "all" }, "O(1) все слои"),
|
||||||
|
h("option", { value: "deep" }, "O(1) глубокие"),
|
||||||
|
h("option", { value: "off" }, "O(1) выкл."));
|
||||||
|
if (sv.o1) o1Sel.value = sv.o1;
|
||||||
|
const o1mIn = h("input", { type: "number", min: "4", placeholder: "32", value: sv.o1_m ?? "", style: "width:55px", title: "Landmark budget (≥4, default 32). More landmarks ≠ better: m=64 measured worse." });
|
||||||
|
const o1wIn = h("input", { type: "number", min: "1", placeholder: "128", value: sv.o1_window ?? "", style: "width:60px", title: "Exact-window width — main quality lever (default 128)" });
|
||||||
|
const o1sinkIn = h("input", { type: "number", min: "0", placeholder: "4", value: sv.o1_sink ?? "", style: "width:50px", title: "Permanent exact sink keys, StreamingLLM discipline (default 4)" });
|
||||||
|
|
||||||
|
// ── Generation parameters ──
|
||||||
|
const tempIn = h("input", { type: "number", step: "0.05", min: "0", max: "2.0", placeholder: "0.7", value: sv.temperature ?? "", style: "width:60px" });
|
||||||
|
const topPIn = h("input", { type: "number", step: "0.05", min: "0", max: "1.0", placeholder: "0.9", value: sv.top_p ?? "", style: "width:60px" });
|
||||||
|
const maxTokIn = h("input", { type: "number", min: "1", placeholder: "2048", value: sv.max_tokens ?? "", style: "width:75px" });
|
||||||
|
// think_budget = 0 means reasoning disabled (enable_thinking=false);
|
||||||
|
// null = model decides; N>0 = token budget for the <think> block.
|
||||||
|
const noThinkCb = h("input", { type: "checkbox", checked: (sv.think_budget === 0) ? true : null, title: "Отключить режим размышлений (think_budget=0, enable_thinking=false для Qwen3/3.5)" });
|
||||||
|
const thinkIn = h("input", { type: "number", min: "1", placeholder: "авто", value: (sv.think_budget && sv.think_budget > 0) ? sv.think_budget : "", style: "width:75px", title: "Бюджет токенов на размышления (think_budget). Пустое = модель решает сама." });
|
||||||
|
if (sv.think_budget === 0) thinkIn.disabled = true;
|
||||||
|
noThinkCb.addEventListener("change", () => {
|
||||||
|
thinkIn.disabled = noThinkCb.checked;
|
||||||
|
if (noThinkCb.checked) thinkIn.value = "";
|
||||||
|
});
|
||||||
|
const sysIn = h("input", { placeholder: "System prompt (optional)", value: sv.system_prompt || "", style: "flex:1;min-width:200px" });
|
||||||
|
|
||||||
|
// ── MTP (Multi-Token Prediction): speculative decoding, NOT reasoning ──
|
||||||
|
const skipMtpCb = h("input", { type: "checkbox", checked: sv.skip_mtp ? true : null, title: "Disable MTP speculative decoding (env CMF_MTP=0)" });
|
||||||
|
|
||||||
|
// ── Settings drawer ──
|
||||||
|
const drawerStyle = "display:none;width:100%;margin-top:6px;padding:10px 12px;background:var(--bg-subtle);border-radius:6px;border:1px solid var(--border)";
|
||||||
|
const groupStyle = "display:flex;flex-wrap:wrap;gap:8px;align-items:center;padding:4px 0";
|
||||||
|
const labelStyle = "font-size:11px;color:var(--text-muted);font-weight:600;text-transform:uppercase;letter-spacing:0.5px;width:100%;margin-bottom:2px";
|
||||||
|
|
||||||
|
const drawer = h("div", { style: drawerStyle },
|
||||||
|
// Group 1: O(1) Nyström Attention
|
||||||
|
h("div", { style: labelStyle }, "O(1) Nyström Attention"),
|
||||||
|
h("div", { style: groupStyle },
|
||||||
|
h("span", { class: "small muted" }, "Слои:"), o1Sel,
|
||||||
|
h("span", { class: "small muted" }, "m:"), o1mIn,
|
||||||
|
h("span", { class: "small muted" }, "Window:"), o1wIn,
|
||||||
|
h("span", { class: "small muted" }, "Sink:"), o1sinkIn
|
||||||
|
),
|
||||||
|
h("hr", { style: "border:none;border-top:1px solid var(--border);margin:6px 0" }),
|
||||||
|
// Group 2: Generation
|
||||||
|
h("div", { style: labelStyle }, "Генерация"),
|
||||||
|
h("div", { style: groupStyle },
|
||||||
|
h("span", { class: "small muted" }, "Temp:"), tempIn,
|
||||||
|
h("span", { class: "small muted" }, "Top-P:"), topPIn,
|
||||||
|
h("span", { class: "small muted" }, "Max токенов:"), maxTokIn
|
||||||
|
),
|
||||||
|
h("div", { style: groupStyle },
|
||||||
|
h("span", { class: "small muted" }, "System prompt:"), sysIn
|
||||||
|
),
|
||||||
|
h("hr", { style: "border:none;border-top:1px solid var(--border);margin:6px 0" }),
|
||||||
|
// Group 3: Reasoning (thinking)
|
||||||
|
h("div", { style: labelStyle }, "Режим размышлений"),
|
||||||
|
h("div", { style: groupStyle },
|
||||||
|
h("label", { class: "check small", title: "Для моделей Qwen3/3.5: enable_thinking=false — модель отвечает напрямую без блока <think>" }, noThinkCb, "Отключить размышления"),
|
||||||
|
h("span", { class: "small muted" }, "Think budget:"), thinkIn
|
||||||
|
),
|
||||||
|
h("hr", { style: "border:none;border-top:1px solid var(--border);margin:6px 0" }),
|
||||||
|
// Group 4: Runtime
|
||||||
|
h("div", { style: labelStyle }, "Runtime"),
|
||||||
|
h("div", { style: groupStyle },
|
||||||
|
h("label", { class: "check small", title: "MTP — Multi-Token Prediction (спекулятивное декодирование). Отключение замедляет генерацию, но стабилизирует output." }, skipMtpCb, "Отключить MTP")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const gearBtn = h("button", { class: "btn small", type: "button", title: "⚙ Настройки модели" }, "⚙");
|
||||||
|
gearBtn.addEventListener("click", () => {
|
||||||
|
drawer.style.display = drawer.style.display === "none" ? "block" : "none";
|
||||||
|
});
|
||||||
|
|
||||||
const row = h("div", { class: "flex wrap", style: "gap:6px;align-items:center;margin:6px 0" },
|
const row = h("div", { class: "flex wrap", style: "gap:6px;align-items:center;margin:6px 0" },
|
||||||
idIn, modelSel, portIn, checkBtn, threadsIn, h("label", { class: "flex", style: "gap:4px" }, gpuIn, "GPU"), removeBtn, st);
|
idIn, modelSel, portIn, checkBtn, threadsIn, h("label", { class: "flex", style: "gap:4px" }, gpuIn, "GPU"), gearBtn, removeBtn, st, drawer);
|
||||||
const entry = { node: row, read: () => ({ id: idIn.value.trim() || autoId(modelSel.value), model: modelSel.value.trim(), port: parseInt(portIn.value) || 8090, threads: parseInt(threadsIn.value) || 0, gpu: gpuIn.checked }) };
|
const entry = { node: row, read: () => ({
|
||||||
|
id: idIn.value.trim() || autoId(modelSel.value),
|
||||||
|
model: modelSel.value.trim(),
|
||||||
|
port: parseInt(portIn.value) || 8090,
|
||||||
|
threads: parseInt(threadsIn.value) || 0,
|
||||||
|
gpu: gpuIn.checked,
|
||||||
|
temperature: tempIn.value !== "" ? parseFloat(tempIn.value) : null,
|
||||||
|
top_p: topPIn.value !== "" ? parseFloat(topPIn.value) : null,
|
||||||
|
max_tokens: maxTokIn.value !== "" ? parseInt(maxTokIn.value) : null,
|
||||||
|
think_budget: noThinkCb.checked ? 0 : (thinkIn.value !== "" ? parseInt(thinkIn.value) : null),
|
||||||
|
o1: o1Sel.value || null,
|
||||||
|
o1_m: o1mIn.value !== "" ? parseInt(o1mIn.value) : null,
|
||||||
|
o1_window: o1wIn.value !== "" ? parseInt(o1wIn.value) : null,
|
||||||
|
o1_sink: o1sinkIn.value !== "" ? parseInt(o1sinkIn.value) : null,
|
||||||
|
skip_mtp: skipMtpCb.checked,
|
||||||
|
system_prompt: sysIn.value.trim() || null,
|
||||||
|
}) };
|
||||||
removeBtn.addEventListener("click", () => { const i = serverRows.indexOf(entry); if (i >= 0) serverRows.splice(i, 1); row.remove(); });
|
removeBtn.addEventListener("click", () => { const i = serverRows.indexOf(entry); if (i >= 0) serverRows.splice(i, 1); row.remove(); });
|
||||||
serverRows.push(entry);
|
serverRows.push(entry);
|
||||||
serversWrap.appendChild(row);
|
serversWrap.appendChild(row);
|
||||||
|
|||||||
Reference in New Issue
Block a user