Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d93ac537c8 | ||
|
|
4220bc27c2 |
Generated
+1
-1
@@ -285,7 +285,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cortiq-gateway"
|
name = "cortiq-gateway"
|
||||||
version = "0.2.16"
|
version = "0.2.18"
|
||||||
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.16"
|
version = "0.2.18"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.75"
|
rust-version = "1.75"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|||||||
+23
-8
@@ -606,7 +606,10 @@ fn cmf_file_for_model(
|
|||||||
cmf: &crate::config::CmfCfg,
|
cmf: &crate::config::CmfCfg,
|
||||||
m: &crate::config::ModelCfg,
|
m: &crate::config::ModelCfg,
|
||||||
) -> Option<std::path::PathBuf> {
|
) -> Option<std::path::PathBuf> {
|
||||||
|
// Any loopback URL counts as local — managed servers may sit on any port.
|
||||||
let local = m.base_url == cmf.cortiq_server_url
|
let local = m.base_url == cmf.cortiq_server_url
|
||||||
|
|| m.base_url.contains("//127.0.0.1:")
|
||||||
|
|| m.base_url.contains("//localhost:")
|
||||||
|| m.base_url
|
|| m.base_url
|
||||||
.contains(&format!("{}:{}", cmf.local_host, cmf.local_port));
|
.contains(&format!("{}:{}", cmf.local_host, cmf.local_port));
|
||||||
let stem = m.model.trim();
|
let stem = m.model.trim();
|
||||||
@@ -646,12 +649,24 @@ fn cmf_path_referenced(cfg: &crate::config::Config, path: &std::path::Path) -> b
|
|||||||
.any(|m| cmf_file_for_model(&cfg.cmf, m).as_deref() == Some(path))
|
.any(|m| cmf_file_for_model(&cfg.cmf, m).as_deref() == Some(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_model(State(state): State<SharedState>, Path(id): Path<String>) -> ApiResult {
|
#[derive(Deserialize)]
|
||||||
|
struct DeleteModelQuery {
|
||||||
|
/// Also remove the local `.cmf` from disk. Defaults to true (the historical
|
||||||
|
/// behaviour); the UI asks the user and passes it explicitly.
|
||||||
|
delete_file: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_model(
|
||||||
|
State(state): State<SharedState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
Query(q): Query<DeleteModelQuery>,
|
||||||
|
) -> ApiResult {
|
||||||
let mut cfg = current_cfg(&state);
|
let mut cfg = current_cfg(&state);
|
||||||
|
let delete_file = q.delete_file.unwrap_or(true);
|
||||||
|
|
||||||
// Managed local models live in [cmf], not [[models]]. Deleting one stops its
|
// Managed local models live in [cmf], not [[models]]. Deleting one stops its
|
||||||
// server, removes it from the managed config, and deletes its file — unless
|
// server, removes it from the managed config, and (when asked) deletes its
|
||||||
// another model still points at the same file.
|
// file — unless another model still points at the same file.
|
||||||
let managed = cfg.cmf.effective_servers().into_iter().find(|s| s.id == id);
|
let managed = cfg.cmf.effective_servers().into_iter().find(|s| s.id == id);
|
||||||
if let Some(server) = managed {
|
if let Some(server) = managed {
|
||||||
if !cfg.models.iter().any(|x| x.id == id) {
|
if !cfg.models.iter().any(|x| x.id == id) {
|
||||||
@@ -666,10 +681,10 @@ async fn delete_model(State(state): State<SharedState>, Path(id): Path<String>)
|
|||||||
let shared = cmf_path_referenced(&cfg, &path);
|
let shared = cmf_path_referenced(&cfg, &path);
|
||||||
state.cmf.stop_one(&id).await;
|
state.cmf.stop_one(&id).await;
|
||||||
state.reload(cfg)?;
|
state.reload(cfg)?;
|
||||||
let file_removed = if shared {
|
let file_removed = if delete_file && !shared {
|
||||||
false
|
|
||||||
} else {
|
|
||||||
remove_cmf_path(&path)
|
remove_cmf_path(&path)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
};
|
};
|
||||||
return ok(json!({ "ok": true, "file_removed": file_removed }));
|
return ok(json!({ "ok": true, "file_removed": file_removed }));
|
||||||
}
|
}
|
||||||
@@ -685,8 +700,8 @@ async fn delete_model(State(state): State<SharedState>, Path(id): Path<String>)
|
|||||||
let file = victim
|
let file = victim
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|m| cmf_file_for_model(&cfg.cmf, m));
|
.and_then(|m| cmf_file_for_model(&cfg.cmf, m));
|
||||||
// Only remove the file if no remaining model still references it.
|
// Only remove the file if asked to AND no remaining model still references it.
|
||||||
let file = file.filter(|p| !cmf_path_referenced(&cfg, p));
|
let file = file.filter(|p| delete_file && !cmf_path_referenced(&cfg, p));
|
||||||
state.reload(cfg)?;
|
state.reload(cfg)?;
|
||||||
let file_removed = file.map(|p| remove_cmf_path(&p)).unwrap_or(false);
|
let file_removed = file.map(|p| remove_cmf_path(&p)).unwrap_or(false);
|
||||||
ok(json!({ "ok": true, "file_removed": file_removed }))
|
ok(json!({ "ok": true, "file_removed": file_removed }))
|
||||||
|
|||||||
Vendored
+2
-1
@@ -60,7 +60,8 @@ export const api = {
|
|||||||
listModels: () => j("GET", "/models"),
|
listModels: () => j("GET", "/models"),
|
||||||
createModel: (b) => j("POST", "/models", b),
|
createModel: (b) => j("POST", "/models", b),
|
||||||
updateModel: (id, b) => j("PUT", "/models/" + encodeURIComponent(id), b),
|
updateModel: (id, b) => j("PUT", "/models/" + encodeURIComponent(id), b),
|
||||||
deleteModel: (id) => j("DELETE", "/models/" + encodeURIComponent(id)),
|
deleteModel: (id, deleteFile = true) =>
|
||||||
|
j("DELETE", "/models/" + encodeURIComponent(id) + "?delete_file=" + (deleteFile ? "true" : "false")),
|
||||||
probeModel: (id) => j("POST", "/models/" + encodeURIComponent(id) + "/probe"),
|
probeModel: (id) => j("POST", "/models/" + encodeURIComponent(id) + "/probe"),
|
||||||
|
|
||||||
// routing
|
// routing
|
||||||
|
|||||||
Vendored
+8
@@ -132,6 +132,8 @@ export default {
|
|||||||
"models.form.secretMissing": "not set",
|
"models.form.secretMissing": "not set",
|
||||||
"models.form.secretPlaceholder": "set or replace key (write-only)",
|
"models.form.secretPlaceholder": "set or replace key (write-only)",
|
||||||
"models.deleteConfirm": "Delete model \"{id}\"?",
|
"models.deleteConfirm": "Delete model \"{id}\"?",
|
||||||
|
"models.deleteFile": "also delete the model file from disk",
|
||||||
|
"models.deletedWithFile": "Deleted (file removed from disk)",
|
||||||
"models.form.idOrModel": "Enter an id or a model name",
|
"models.form.idOrModel": "Enter an id or a model name",
|
||||||
"models.form.apiKey": "API Key",
|
"models.form.apiKey": "API Key",
|
||||||
"models.form.apiKeyPlaceholder": "paste your API key",
|
"models.form.apiKeyPlaceholder": "paste your API key",
|
||||||
@@ -410,6 +412,12 @@ export default {
|
|||||||
"import.adv.o1DeepN": "N deep layers",
|
"import.adv.o1DeepN": "N deep layers",
|
||||||
"import.adv.o1Layers": "Layers (comma-separated)",
|
"import.adv.o1Layers": "Layers (comma-separated)",
|
||||||
"import.adv.o1mHint": "landmark budget (default 32)",
|
"import.adv.o1mHint": "landmark budget (default 32)",
|
||||||
|
"import.adv.o1mLabel": "Landmarks (--o1-m)",
|
||||||
|
"import.adv.o1mHelp": "How many landmark tokens summarize the whole older history when attention is approximated. More = closer to exact attention but slower and more memory; fewer = faster but rougher. 32 is the validated sweet spot — change only if you know why.",
|
||||||
|
"import.adv.o1windowLabel": "Exact window (--o1-window)",
|
||||||
|
"import.adv.o1windowHelp": "The last N tokens always get exact (non-approximated) attention. This protects quality on the text the model just wrote/read. Bigger = better local quality, more memory. Default 128.",
|
||||||
|
"import.adv.o1sinkLabel": "Sink keys (--o1-sink)",
|
||||||
|
"import.adv.o1sinkHelp": "The first N tokens of the context are kept exact forever ('attention sinks'). Models lean on these anchors — keeping them exact stabilizes long generations. Default 4; rarely needs changing.",
|
||||||
"import.adv.o1windowHint": "exact-window width (default 128)",
|
"import.adv.o1windowHint": "exact-window width (default 128)",
|
||||||
"import.adv.o1sinkHint": "permanent exact sink keys (default 4)",
|
"import.adv.o1sinkHint": "permanent exact sink keys (default 4)",
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+8
@@ -131,6 +131,8 @@ export default {
|
|||||||
"models.form.secretMissing": "не задан",
|
"models.form.secretMissing": "не задан",
|
||||||
"models.form.secretPlaceholder": "задать или заменить ключ (только запись)",
|
"models.form.secretPlaceholder": "задать или заменить ключ (только запись)",
|
||||||
"models.deleteConfirm": "Удалить модель \"{id}\"?",
|
"models.deleteConfirm": "Удалить модель \"{id}\"?",
|
||||||
|
"models.deleteFile": "также удалить файл модели с диска",
|
||||||
|
"models.deletedWithFile": "Удалено (файл стёрт с диска)",
|
||||||
"models.form.idOrModel": "Укажите id или имя модели",
|
"models.form.idOrModel": "Укажите id или имя модели",
|
||||||
"models.form.apiKey": "API-ключ",
|
"models.form.apiKey": "API-ключ",
|
||||||
"models.form.apiKeyPlaceholder": "вставьте API-ключ",
|
"models.form.apiKeyPlaceholder": "вставьте API-ключ",
|
||||||
@@ -409,6 +411,12 @@ export default {
|
|||||||
"import.adv.o1DeepN": "N глубоких слоёв",
|
"import.adv.o1DeepN": "N глубоких слоёв",
|
||||||
"import.adv.o1Layers": "Слои (через запятую)",
|
"import.adv.o1Layers": "Слои (через запятую)",
|
||||||
"import.adv.o1mHint": "число опорных точек (по умолчанию 32)",
|
"import.adv.o1mHint": "число опорных точек (по умолчанию 32)",
|
||||||
|
"import.adv.o1mLabel": "Опорные точки (--o1-m)",
|
||||||
|
"import.adv.o1mHelp": "Сколько опорных токенов представляют всю старую историю при аппроксимации внимания. Больше — ближе к точному вниманию, но медленнее и больше памяти; меньше — быстрее, но грубее. 32 — проверенное значение; меняйте, только если понимаете зачем.",
|
||||||
|
"import.adv.o1windowLabel": "Точное окно (--o1-window)",
|
||||||
|
"import.adv.o1windowHelp": "Последние N токенов всегда обрабатываются точным (не аппроксимированным) вниманием — это защищает качество на только что написанном/прочитанном тексте. Больше — лучше локальное качество, больше памяти. По умолчанию 128.",
|
||||||
|
"import.adv.o1sinkLabel": "Sink-ключи (--o1-sink)",
|
||||||
|
"import.adv.o1sinkHelp": "Первые N токенов контекста навсегда остаются точными («якоря внимания»). Модели опираются на эти якоря — их точность стабилизирует длинную генерацию. По умолчанию 4; менять почти никогда не нужно.",
|
||||||
"import.adv.o1windowHint": "ширина точного окна (по умолчанию 128)",
|
"import.adv.o1windowHint": "ширина точного окна (по умолчанию 128)",
|
||||||
"import.adv.o1sinkHint": "постоянные точные sink-ключи (по умолчанию 4)",
|
"import.adv.o1sinkHint": "постоянные точные sink-ключи (по умолчанию 4)",
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+11
-5
@@ -15,8 +15,14 @@ const QUANTS = [
|
|||||||
["F32", "import.q.f32"],
|
["F32", "import.q.f32"],
|
||||||
];
|
];
|
||||||
|
|
||||||
function field(label, control, hint) {
|
// A small "?" badge with a hover/focus tooltip (same style as Settings).
|
||||||
return h("label", { class: "field" }, h("span", {}, label), control,
|
function help(text) {
|
||||||
|
return text ? h("span", { class: "help", title: text, tabindex: "0", role: "img", "aria-label": text }, "?") : null;
|
||||||
|
}
|
||||||
|
function field(label, control, hint, helpText) {
|
||||||
|
return h("label", { class: "field" },
|
||||||
|
h("span", {}, label, helpText ? " " : null, help(helpText)),
|
||||||
|
control,
|
||||||
hint ? h("div", { class: "hint" }, hint) : null);
|
hint ? h("div", { class: "hint" }, hint) : null);
|
||||||
}
|
}
|
||||||
function opt(v, label, sel) {
|
function opt(v, label, sel) {
|
||||||
@@ -141,9 +147,9 @@ export function renderImport() {
|
|||||||
field(t("import.adv.o1DeepN"), o1DeepIn),
|
field(t("import.adv.o1DeepN"), o1DeepIn),
|
||||||
field(t("import.adv.o1Layers"), o1CustomIn));
|
field(t("import.adv.o1Layers"), o1CustomIn));
|
||||||
const o1Knobs = h("div", { class: "row", style: "display:none" },
|
const o1Knobs = h("div", { class: "row", style: "display:none" },
|
||||||
field("--o1-m", o1M, t("import.adv.o1mHint")),
|
field(t("import.adv.o1mLabel"), o1M, t("import.adv.o1mHint"), t("import.adv.o1mHelp")),
|
||||||
field("--o1-window", o1Window, t("import.adv.o1windowHint")),
|
field(t("import.adv.o1windowLabel"), o1Window, t("import.adv.o1windowHint"), t("import.adv.o1windowHelp")),
|
||||||
field("--o1-sink", o1Sink, t("import.adv.o1sinkHint")));
|
field(t("import.adv.o1sinkLabel"), o1Sink, t("import.adv.o1sinkHint"), t("import.adv.o1sinkHelp")));
|
||||||
o1Sel.addEventListener("change", () => {
|
o1Sel.addEventListener("change", () => {
|
||||||
const on = !!o1Sel.value;
|
const on = !!o1Sel.value;
|
||||||
o1Knobs.style.display = on ? "" : "none";
|
o1Knobs.style.display = on ? "" : "none";
|
||||||
|
|||||||
Vendored
+26
-10
@@ -1,5 +1,5 @@
|
|||||||
// Models — pool table with add/edit modal, probe, and per-model secret entry.
|
// Models — pool table with add/edit modal, probe, and per-model secret entry.
|
||||||
import { h, mount, modal, toast, confirmDialog, money } from "../ui.js";
|
import { h, mount, modal, toast, money } from "../ui.js";
|
||||||
import { t } from "../i18n.js";
|
import { t } from "../i18n.js";
|
||||||
import { api } from "../api.js";
|
import { api } from "../api.js";
|
||||||
import { appState } from "../app.js";
|
import { appState } from "../app.js";
|
||||||
@@ -280,15 +280,31 @@ function row(m, meta, reload) {
|
|||||||
"button",
|
"button",
|
||||||
{
|
{
|
||||||
class: "btn sm danger",
|
class: "btn sm danger",
|
||||||
onClick: async () => {
|
onClick: () => {
|
||||||
if (!(await confirmDialog(t("models.deleteConfirm", { id: m.id })))) return;
|
// local models may have a .cmf on disk — let the user decide its fate
|
||||||
try {
|
const isLocal = m.managed || /127\.0\.0\.1|localhost/.test(m.base_url || "");
|
||||||
await api.deleteModel(m.id);
|
const fileCb = h("input", { type: "checkbox", checked: isLocal ? true : null });
|
||||||
toast(t("toast.deleted"), "good");
|
const body = h(
|
||||||
await reload();
|
"div",
|
||||||
} catch (e) {
|
{},
|
||||||
toast(String(e.message || e), "bad");
|
h("p", {}, t("models.deleteConfirm", { id: m.id })),
|
||||||
}
|
isLocal
|
||||||
|
? h("label", { class: "check" }, fileCb, t("models.deleteFile"))
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
modal(t("common.delete") + ": " + m.id, body, async () => {
|
||||||
|
try {
|
||||||
|
const r = await api.deleteModel(m.id, isLocal && fileCb.checked);
|
||||||
|
toast(
|
||||||
|
r.file_removed ? t("models.deletedWithFile") : t("toast.deleted"),
|
||||||
|
"good"
|
||||||
|
);
|
||||||
|
await reload();
|
||||||
|
} catch (e) {
|
||||||
|
toast(String(e.message || e), "bad");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, t("common.delete"));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
t("common.delete")
|
t("common.delete")
|
||||||
|
|||||||
Reference in New Issue
Block a user