Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25f93d1a27 | ||
|
|
0f89cdaa07 | ||
|
|
d211eb6150 |
Generated
+1
-1
@@ -285,7 +285,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cortiq-gateway"
|
name = "cortiq-gateway"
|
||||||
version = "0.2.3"
|
version = "0.2.5"
|
||||||
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.3"
|
version = "0.2.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.75"
|
rust-version = "1.75"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|||||||
+17
-28
@@ -28,40 +28,29 @@ cache_ttl = "60s"
|
|||||||
profile = "balanced" # cost-saver | balanced | quality-first
|
profile = "balanced" # cost-saver | balanced | quality-first
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Model pool. This example ships ONE local CMF model, served by the managed
|
# Model pool — EMPTY by default. A fresh gateway starts with no models and an
|
||||||
# cortiq server (see the [cmf] section below). Add more models — local or cloud —
|
# empty pool; add them in the admin console (/admin → Models), or run a local
|
||||||
# by copying the [[models]] shape: provider = "openai"/"anthropic", and for a
|
# .cmf via the [cmf] section below. To add one by hand, copy this shape:
|
||||||
# hosted provider an api_key_env = "..." (never put secrets in this file).
|
#
|
||||||
|
# [[models]]
|
||||||
|
# id = "my-model"
|
||||||
|
# provider = "openai" # openai | anthropic (openai also covers
|
||||||
|
# base_url = "http://127.0.0.1:8081/v1" # OpenRouter/Together/Groq/vLLM/Ollama)
|
||||||
|
# model = "my-model"
|
||||||
|
# # api_key_env = "OPENAI_API_KEY" # for hosted providers (never inline a key)
|
||||||
|
# cost_tier = "local"
|
||||||
|
# caps = ["tools"]
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
[[models]]
|
|
||||||
id = "cmf-local"
|
|
||||||
provider = "openai"
|
|
||||||
base_url = "http://127.0.0.1:8081/v1" # the managed cortiq server (see [cmf])
|
|
||||||
model = "cmf-local"
|
|
||||||
cost_tier = "local"
|
|
||||||
price_in = 0.0
|
|
||||||
price_out = 0.0
|
|
||||||
caps = ["tools"]
|
|
||||||
|
|
||||||
# --- embedding model (optional: /v1/embeddings + the semantic cache) ---------
|
|
||||||
[[models]]
|
|
||||||
id = "local-embed"
|
|
||||||
provider = "openai"
|
|
||||||
base_url = "http://localhost:8000/v1" # a local embeddings server (vLLM / llama.cpp / Ollama)
|
|
||||||
model = "nomic-embed-text"
|
|
||||||
cost_tier = "local"
|
|
||||||
kind = "embedding"
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Routing table: complexity tier → ordered pool (priority = fallback chain).
|
# Routing table: complexity tier → ordered pool (priority = fallback chain).
|
||||||
# Add cloud models above and list them here for cost/quality-based fallback.
|
# Leave default empty for a fresh install; set it to a model id once you add one.
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
[routing]
|
[routing]
|
||||||
low = ["cmf-local"]
|
low = []
|
||||||
medium = ["cmf-local"]
|
medium = []
|
||||||
high = ["cmf-local"]
|
high = []
|
||||||
default = "cmf-local" # used when the router is unreachable
|
default = "" # set to a model id once you add one
|
||||||
|
|
||||||
[routing.policy]
|
[routing.policy]
|
||||||
mode = "fixed_table" # fixed_table | cost_aware
|
mode = "fixed_table" # fixed_table | cost_aware
|
||||||
|
|||||||
+12
-2
@@ -16,11 +16,18 @@ use rust_embed::RustEmbed;
|
|||||||
struct WebAssets;
|
struct WebAssets;
|
||||||
|
|
||||||
fn serve_file(path: &str) -> Response {
|
fn serve_file(path: &str) -> Response {
|
||||||
|
// The panel is rebuilt often (assets are embedded at compile time and served
|
||||||
|
// from memory), so tell the browser to always revalidate — otherwise a stale
|
||||||
|
// cached SPA hides freshly shipped fixes until a hard reload.
|
||||||
|
const NO_CACHE: &str = "no-store, must-revalidate";
|
||||||
match WebAssets::get(path) {
|
match WebAssets::get(path) {
|
||||||
Some(content) => {
|
Some(content) => {
|
||||||
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
||||||
(
|
(
|
||||||
[(header::CONTENT_TYPE, mime.as_ref())],
|
[
|
||||||
|
(header::CONTENT_TYPE, mime.as_ref()),
|
||||||
|
(header::CACHE_CONTROL, NO_CACHE),
|
||||||
|
],
|
||||||
content.data.into_owned(),
|
content.data.into_owned(),
|
||||||
)
|
)
|
||||||
.into_response()
|
.into_response()
|
||||||
@@ -29,7 +36,10 @@ fn serve_file(path: &str) -> Response {
|
|||||||
// SPA: unknown path under /admin → serve index.html
|
// SPA: unknown path under /admin → serve index.html
|
||||||
match WebAssets::get("index.html") {
|
match WebAssets::get("index.html") {
|
||||||
Some(content) => (
|
Some(content) => (
|
||||||
[(header::CONTENT_TYPE, "text/html; charset=utf-8")],
|
[
|
||||||
|
(header::CONTENT_TYPE, "text/html; charset=utf-8"),
|
||||||
|
(header::CACHE_CONTROL, NO_CACHE),
|
||||||
|
],
|
||||||
content.data.into_owned(),
|
content.data.into_owned(),
|
||||||
)
|
)
|
||||||
.into_response(),
|
.into_response(),
|
||||||
|
|||||||
+6
-1
@@ -599,7 +599,12 @@ impl Config {
|
|||||||
// is what lets the gateway run on local models alone (no cloud, no router).
|
// is what lets the gateway run on local models alone (no cloud, no router).
|
||||||
let has_local = self.cmf.manage_server && !self.cmf.local_model.trim().is_empty();
|
let has_local = self.cmf.manage_server && !self.cmf.local_model.trim().is_empty();
|
||||||
if self.models.is_empty() && !has_local {
|
if self.models.is_empty() && !has_local {
|
||||||
anyhow::bail!("config: at least one [[models]] entry (or a managed local CMF model) is required");
|
// Fresh install: start with an EMPTY model pool so the admin console
|
||||||
|
// comes up — the user adds models in /admin → Models (or points [cmf]
|
||||||
|
// at a local .cmf). Routing simply has nothing to select until then.
|
||||||
|
tracing::warn!(
|
||||||
|
"no models configured — starting with an empty pool; add one in /admin → Models"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// uniqueness of model ids
|
// uniqueness of model ids
|
||||||
let mut seen = std::collections::HashSet::new();
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
|||||||
+24
-8
@@ -223,6 +223,17 @@ fn sanitize(s: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Kick off a conversion. Returns the job id immediately; progress streams
|
/// Kick off a conversion. Returns the job id immediately; progress streams
|
||||||
|
/// Map the UI quant label to the native `cortiq` quant flag.
|
||||||
|
fn map_quant(q: &str) -> &'static str {
|
||||||
|
match q.to_ascii_uppercase().as_str() {
|
||||||
|
s if s.starts_with("Q4") => "q4",
|
||||||
|
"Q8_2F" => "q8_2f",
|
||||||
|
"F16" | "FP16" => "f16",
|
||||||
|
"VBIT" => "vbit",
|
||||||
|
_ => "q8",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// into the job log. Fails fast if the converter script is missing.
|
/// into the job log. Fails fast if the converter script is missing.
|
||||||
pub fn start_import(store: Arc<JobStore>, cfg: &CmfCfg, p: ImportParams) -> Result<String, String> {
|
pub fn start_import(store: Arc<JobStore>, cfg: &CmfCfg, p: ImportParams) -> Result<String, String> {
|
||||||
if p.repo.trim().is_empty() {
|
if p.repo.trim().is_empty() {
|
||||||
@@ -290,18 +301,23 @@ pub fn start_import(store: Arc<JobStore>, cfg: &CmfCfg, p: ImportParams) -> Resu
|
|||||||
if p.skip_mtp { a.push("--skip-mtp".into()); }
|
if p.skip_mtp { a.push("--skip-mtp".into()); }
|
||||||
let wd = conv.parent().map(|d| d.to_path_buf()).unwrap_or_else(|| std::path::PathBuf::from("."));
|
let wd = conv.parent().map(|d| d.to_path_buf()).unwrap_or_else(|| std::path::PathBuf::from("."));
|
||||||
(cfg.python_bin.clone(), a, wd)
|
(cfg.python_bin.clone(), a, wd)
|
||||||
|
} else if p.repo.to_ascii_lowercase().contains("gguf") {
|
||||||
|
// GGUF repo → native `cortiq import-gguf` (downloads + dequantizes any
|
||||||
|
// common ggml quant type: Q4_0/1, Q5_0/1, Q8_0, Q2_K..Q6_K, IQ4_NL/XS).
|
||||||
|
let mut a = vec![
|
||||||
|
"import-gguf".into(),
|
||||||
|
p.repo.clone(),
|
||||||
|
"--quant".into(), map_quant(&p.quant).into(),
|
||||||
|
"--output".into(), output_abs.clone(),
|
||||||
|
];
|
||||||
|
if let Some(t) = &hf_token { a.push("--hf-token".into()); a.push(t.clone()); }
|
||||||
|
(cfg.cortiq_bin.clone(), a, std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")))
|
||||||
} else {
|
} else {
|
||||||
// native Rust converter — quant name maps to cortiq's scheme.
|
// safetensors → native `cortiq convert` (dense / MoE / GatedDeltaNet).
|
||||||
let quant = match p.quant.to_ascii_uppercase().as_str() {
|
|
||||||
s if s.starts_with("Q4") => "q4",
|
|
||||||
"Q8_2F" => "q8_2f",
|
|
||||||
"F16" | "FP16" => "f16",
|
|
||||||
_ => "q8",
|
|
||||||
};
|
|
||||||
let mut a = vec![
|
let mut a = vec![
|
||||||
"convert".into(),
|
"convert".into(),
|
||||||
"--model".into(), p.repo.clone(),
|
"--model".into(), p.repo.clone(),
|
||||||
"--quant".into(), quant.into(),
|
"--quant".into(), map_quant(&p.quant).into(),
|
||||||
"--output".into(), output_abs.clone(),
|
"--output".into(), output_abs.clone(),
|
||||||
];
|
];
|
||||||
if let Some(t) = &hf_token { a.push("--hf-token".into()); a.push(t.clone()); }
|
if let Some(t) = &hf_token { a.push("--hf-token".into()); a.push(t.clone()); }
|
||||||
|
|||||||
Vendored
+78
@@ -307,3 +307,81 @@ tbody tr:hover td { background: var(--hover); }
|
|||||||
.callout { border: 1px solid var(--border); border-left: 3px solid var(--warn); background: color-mix(in srgb, var(--warn) 6%, transparent); border-radius: 10px; padding: 13px 15px; color: var(--muted); margin-bottom: 16px; }
|
.callout { border: 1px solid var(--border); border-left: 3px solid var(--warn); background: color-mix(in srgb, var(--warn) 6%, transparent); border-radius: 10px; padding: 13px 15px; color: var(--muted); margin-bottom: 16px; }
|
||||||
.callout.info { border-left-color: var(--accent-2); background: color-mix(in srgb, var(--accent-2) 6%, transparent); }
|
.callout.info { border-left-color: var(--accent-2); background: color-mix(in srgb, var(--accent-2) 6%, transparent); }
|
||||||
.callout b { color: var(--text); }
|
.callout b { color: var(--text); }
|
||||||
|
|
||||||
|
/* ── Import model (HuggingFace → CMF) ── */
|
||||||
|
.view-head { margin-bottom: 14px; }
|
||||||
|
.view-head h2 { margin: 0 0 2px; }
|
||||||
|
.card-title { font-weight: 600; margin-bottom: 10px; }
|
||||||
|
.actions { margin-top: 12px; display: flex; gap: 8px; }
|
||||||
|
.pad { padding: 18px; }
|
||||||
|
.err { color: #e5534b; }
|
||||||
|
.btn.sm { padding: 4px 10px; font-size: 12px; }
|
||||||
|
|
||||||
|
.hf-search {
|
||||||
|
width: 100%; box-sizing: border-box; margin-bottom: 12px;
|
||||||
|
padding: 10px 12px; font-size: 15px;
|
||||||
|
border: 1px solid var(--border, #2a2f3a); border-radius: 8px;
|
||||||
|
background: var(--bg-2, #12151c); color: inherit;
|
||||||
|
}
|
||||||
|
.hf-grid {
|
||||||
|
display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
|
||||||
|
gap: 10px; max-height: 420px; overflow-y: auto;
|
||||||
|
}
|
||||||
|
.hf-card {
|
||||||
|
border: 1px solid var(--border, #2a2f3a); border-radius: 8px;
|
||||||
|
padding: 10px 12px; cursor: pointer; transition: border-color .12s, background .12s;
|
||||||
|
background: var(--bg-2, #12151c);
|
||||||
|
}
|
||||||
|
.hf-card:hover { border-color: var(--accent, #5b8def); }
|
||||||
|
.hf-card.sel { border-color: var(--accent, #5b8def); background: rgba(91,141,239,.10); }
|
||||||
|
.hf-id { font-weight: 600; font-size: 13px; word-break: break-all; margin-bottom: 6px; }
|
||||||
|
.hf-meta { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; font-size: 12px; }
|
||||||
|
.badge.bad { background: rgba(229,83,75,.15); color: #e5534b; }
|
||||||
|
|
||||||
|
.import-form { margin-top: 14px; }
|
||||||
|
.adv { margin-top: 8px; }
|
||||||
|
.adv-body { margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--border, #2a2f3a); }
|
||||||
|
|
||||||
|
.pbar {
|
||||||
|
height: 8px; border-radius: 6px; overflow: hidden; margin: 10px 0;
|
||||||
|
background: var(--bg-2, #12151c); border: 1px solid var(--border, #2a2f3a);
|
||||||
|
}
|
||||||
|
.pbar-fill { height: 100%; width: 100%; border-radius: 6px; }
|
||||||
|
.pbar-fill.run {
|
||||||
|
width: 40%;
|
||||||
|
background: linear-gradient(90deg, transparent, var(--accent, #5b8def), transparent);
|
||||||
|
animation: pbar-slide 1.2s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
.pbar-fill.ok { background: #3fb950; }
|
||||||
|
.pbar-fill.err { background: #e5534b; }
|
||||||
|
@keyframes pbar-slide { 0% { margin-left: -40%; } 100% { margin-left: 100%; } }
|
||||||
|
|
||||||
|
.conv-log {
|
||||||
|
margin-top: 10px; max-height: 260px; overflow-y: auto;
|
||||||
|
background: var(--bg-2, #0d1017); border: 1px solid var(--border, #2a2f3a);
|
||||||
|
border-radius: 8px; padding: 10px 12px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
|
||||||
|
white-space: pre-wrap; word-break: break-word; color: var(--muted, #9aa4b2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Router-key field: input grows, inline "Save key" button sits beside it */
|
||||||
|
.key-row { display: flex; gap: 8px; align-items: stretch; }
|
||||||
|
.key-row input { flex: 1 1 auto; min-width: 0; }
|
||||||
|
.key-row .btn { flex: 0 0 auto; white-space: nowrap; }
|
||||||
|
|
||||||
|
/* Determinate progress fill (known %); .run remains the indeterminate slider */
|
||||||
|
.pbar-fill.det {
|
||||||
|
background: linear-gradient(90deg, var(--accent, #5b8def), #7aa2f7);
|
||||||
|
transition: width .4s ease;
|
||||||
|
}
|
||||||
|
/* Import job cards (multiple conversions, live) */
|
||||||
|
.job { padding: 12px 0; border-top: 1px solid var(--border-soft, #232833); }
|
||||||
|
.job:first-of-type { border-top: 0; }
|
||||||
|
.job-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||||
|
.job-head b { word-break: break-all; }
|
||||||
|
.small { font-size: 12px; }
|
||||||
|
/* Router usage: readable, wrapping key/value rows (no horizontal overflow) */
|
||||||
|
.usage-box { max-width: 100%; overflow-x: hidden; }
|
||||||
|
.mono.kv { display: flex; gap: 6px; padding: 1px 0; white-space: normal; overflow-wrap: anywhere; word-break: break-word; }
|
||||||
|
.mono.kv .kv-k { color: var(--muted, #9aa4b2); flex: 0 0 auto; }
|
||||||
|
.mono.kv .kv-v { font-weight: 600; overflow-wrap: anywhere; word-break: break-word; min-width: 0; }
|
||||||
|
|||||||
Vendored
+16
@@ -50,6 +50,7 @@ export const api = {
|
|||||||
// meta / health
|
// meta / health
|
||||||
meta: () => j("GET", "/meta"),
|
meta: () => j("GET", "/meta"),
|
||||||
health: () => j("GET", "/health"),
|
health: () => j("GET", "/health"),
|
||||||
|
probeRouter: () => j("POST", "/router/probe"),
|
||||||
|
|
||||||
// config
|
// config
|
||||||
getConfig: () => j("GET", "/config"),
|
getConfig: () => j("GET", "/config"),
|
||||||
@@ -90,6 +91,21 @@ export const api = {
|
|||||||
|
|
||||||
// playground
|
// playground
|
||||||
test: (b) => j("POST", "/test", b),
|
test: (b) => j("POST", "/test", b),
|
||||||
|
|
||||||
|
// shadow / self-warming loop
|
||||||
|
shadow: () => j("GET", "/shadow"),
|
||||||
|
|
||||||
|
// CMF model factory (HuggingFace → local .cmf)
|
||||||
|
hfSearch: (q, limit = 24) =>
|
||||||
|
j("GET", `/hf/search?q=${encodeURIComponent(q || "")}&limit=${limit}`),
|
||||||
|
startImport: (b) => j("POST", "/import", b),
|
||||||
|
listImports: () => j("GET", "/import"),
|
||||||
|
importStatus: (job) => j("GET", "/import/" + encodeURIComponent(job)),
|
||||||
|
cancelImport: (job) =>
|
||||||
|
j("POST", "/import/" + encodeURIComponent(job) + "/cancel"),
|
||||||
|
deleteImport: (job) => j("DELETE", "/import/" + encodeURIComponent(job)),
|
||||||
|
registerImport: (job) =>
|
||||||
|
j("POST", "/import/" + encodeURIComponent(job) + "/register"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Streaming playground: POST /test/stream, calls onDelta(text) per content chunk,
|
// Streaming playground: POST /test/stream, calls onDelta(text) per content chunk,
|
||||||
|
|||||||
Vendored
+24
-2
@@ -5,6 +5,7 @@ import { api, getToken, setToken, setUnauthorizedHandler } from "./api.js";
|
|||||||
import { renderLogin } from "./views/login.js";
|
import { renderLogin } from "./views/login.js";
|
||||||
import { renderDashboard } from "./views/dashboard.js";
|
import { renderDashboard } from "./views/dashboard.js";
|
||||||
import { renderModels } from "./views/models.js";
|
import { renderModels } from "./views/models.js";
|
||||||
|
import { renderImport } from "./views/import.js";
|
||||||
import { renderRouting } from "./views/routing.js";
|
import { renderRouting } from "./views/routing.js";
|
||||||
import { renderProtocols } from "./views/protocols.js";
|
import { renderProtocols } from "./views/protocols.js";
|
||||||
import { renderKeys } from "./views/keys.js";
|
import { renderKeys } from "./views/keys.js";
|
||||||
@@ -14,6 +15,7 @@ import { renderSettings } from "./views/settings.js";
|
|||||||
const ROUTES = [
|
const ROUTES = [
|
||||||
{ id: "dashboard", icon: "▤", view: renderDashboard },
|
{ id: "dashboard", icon: "▤", view: renderDashboard },
|
||||||
{ id: "models", icon: "▦", view: renderModels },
|
{ id: "models", icon: "▦", view: renderModels },
|
||||||
|
{ id: "import", icon: "⬇", view: renderImport },
|
||||||
{ id: "routing", icon: "⇄", view: renderRouting },
|
{ id: "routing", icon: "⇄", view: renderRouting },
|
||||||
{ id: "protocols", icon: "⇆", view: renderProtocols },
|
{ id: "protocols", icon: "⇆", view: renderProtocols },
|
||||||
{ id: "keys", icon: "🔑", view: renderKeys },
|
{ id: "keys", icon: "🔑", view: renderKeys },
|
||||||
@@ -23,6 +25,9 @@ const ROUTES = [
|
|||||||
|
|
||||||
export const appState = { meta: null, health: null, version: "" };
|
export const appState = { meta: null, health: null, version: "" };
|
||||||
|
|
||||||
|
// allaigate site — where router keys are issued and topped up
|
||||||
|
export const SITE_URL = "https://api.allaigate.com/";
|
||||||
|
|
||||||
function currentRoute() {
|
function currentRoute() {
|
||||||
const id = location.hash.replace(/^#\/?/, "") || "dashboard";
|
const id = location.hash.replace(/^#\/?/, "") || "dashboard";
|
||||||
return ROUTES.find((r) => r.id === id) || ROUTES[0];
|
return ROUTES.find((r) => r.id === id) || ROUTES[0];
|
||||||
@@ -53,6 +58,13 @@ function sidebar(activeId) {
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
h("div", { class: "nav-spacer" }),
|
h("div", { class: "nav-spacer" }),
|
||||||
|
h(
|
||||||
|
"a",
|
||||||
|
{ class: "nav-item", href: SITE_URL, target: "_blank", rel: "noopener" },
|
||||||
|
h("span", { class: "ico" }, "◈"),
|
||||||
|
t("nav.billing"),
|
||||||
|
h("span", { style: "margin-left:auto;opacity:.55" }, "↗")
|
||||||
|
),
|
||||||
h("div", { class: "nav-foot" }, t("nav.foot", { v: appState.version || "—" }))
|
h("div", { class: "nav-foot" }, t("nav.foot", { v: appState.version || "—" }))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -61,6 +73,16 @@ function statusPills() {
|
|||||||
const h_ = appState.health;
|
const h_ = appState.health;
|
||||||
const gwOk = !!appState.meta;
|
const gwOk = !!appState.meta;
|
||||||
const routerOk = h_?.router?.reachable;
|
const routerOk = h_?.router?.reachable;
|
||||||
|
// reachable but no key resolves → warn: routing silently degrades to default
|
||||||
|
const keyMissing = h_?.router?.key_source === "missing";
|
||||||
|
const routerDot = h_ ? (routerOk ? (keyMissing ? "warn" : "ok") : "bad") : "warn";
|
||||||
|
const routerText = h_
|
||||||
|
? routerOk
|
||||||
|
? keyMissing
|
||||||
|
? t("status.noKey")
|
||||||
|
: t("status.online")
|
||||||
|
: t("status.offline")
|
||||||
|
: t("status.checking");
|
||||||
return h(
|
return h(
|
||||||
"div",
|
"div",
|
||||||
{ class: "flex" },
|
{ class: "flex" },
|
||||||
@@ -73,8 +95,8 @@ function statusPills() {
|
|||||||
h(
|
h(
|
||||||
"span",
|
"span",
|
||||||
{ class: "pill", title: h_?.router?.url || "" },
|
{ class: "pill", title: h_?.router?.url || "" },
|
||||||
h("span", { class: "dot " + (h_ ? (routerOk ? "ok" : "bad") : "warn") }),
|
h("span", { class: "dot " + routerDot }),
|
||||||
t("status.router") + ": " + (h_ ? (routerOk ? t("status.online") : t("status.offline")) : t("status.checking"))
|
t("status.router") + ": " + routerText
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+15
@@ -33,6 +33,7 @@ export default {
|
|||||||
"nav.playground": "Playground",
|
"nav.playground": "Playground",
|
||||||
"nav.settings": "Einstellungen",
|
"nav.settings": "Einstellungen",
|
||||||
"nav.foot": "Gateway · v{v}",
|
"nav.foot": "Gateway · v{v}",
|
||||||
|
"nav.billing": "Router-Key & Abrechnung",
|
||||||
|
|
||||||
// topbar / status
|
// topbar / status
|
||||||
"status.router": "Router",
|
"status.router": "Router",
|
||||||
@@ -40,6 +41,7 @@ export default {
|
|||||||
"status.online": "online",
|
"status.online": "online",
|
||||||
"status.offline": "offline",
|
"status.offline": "offline",
|
||||||
"status.checking": "Prüfe…",
|
"status.checking": "Prüfe…",
|
||||||
|
"status.noKey": "kein Key",
|
||||||
"theme.toggle": "Design",
|
"theme.toggle": "Design",
|
||||||
"logout": "Abmelden",
|
"logout": "Abmelden",
|
||||||
|
|
||||||
@@ -88,6 +90,7 @@ export default {
|
|||||||
"dash.health.title": "Status",
|
"dash.health.title": "Status",
|
||||||
"dash.health.reachable": "erreichbar",
|
"dash.health.reachable": "erreichbar",
|
||||||
"dash.health.unreachable": "nicht erreichbar",
|
"dash.health.unreachable": "nicht erreichbar",
|
||||||
|
"dash.health.getKey": "Router-Key holen",
|
||||||
|
|
||||||
// models
|
// models
|
||||||
"models.title": "Modelle",
|
"models.title": "Modelle",
|
||||||
@@ -225,6 +228,18 @@ export default {
|
|||||||
"settings.router.timeout": "Timeout (ms)",
|
"settings.router.timeout": "Timeout (ms)",
|
||||||
"settings.router.verifyTls": "TLS-Zertifikat prüfen",
|
"settings.router.verifyTls": "TLS-Zertifikat prüfen",
|
||||||
"settings.router.taxonomy": "Taxonomie-ID",
|
"settings.router.taxonomy": "Taxonomie-ID",
|
||||||
|
"settings.router.getKey": "Router-Key holen oder aufladen:",
|
||||||
|
"settings.router.secret": "Router-Key (Wert)",
|
||||||
|
"settings.router.test": "Router testen",
|
||||||
|
"router.probe.ok": "Router OK ({ms} ms)",
|
||||||
|
"router.probe.noKey": "Router-Key ist nicht gesetzt",
|
||||||
|
"router.probe.auth": "Key abgelehnt — ungültig oder widerrufen",
|
||||||
|
"router.probe.payment": "Zahlung erforderlich — Tarif abgelaufen oder Kontingent erschöpft",
|
||||||
|
"router.probe.quota": "Ratenlimit überschritten",
|
||||||
|
"router.probe.timeout": "Router-Zeitüberschreitung",
|
||||||
|
"router.probe.unreachable": "Router nicht erreichbar",
|
||||||
|
"router.probe.error": "Router-Fehler",
|
||||||
|
"router.usage": "Verbrauch",
|
||||||
"settings.route": "Routing-Eingabe",
|
"settings.route": "Routing-Eingabe",
|
||||||
"settings.route.strategy": "Textstrategie",
|
"settings.route.strategy": "Textstrategie",
|
||||||
"settings.route.maxChars": "Max. Zeichen",
|
"settings.route.maxChars": "Max. Zeichen",
|
||||||
|
|||||||
Vendored
+15
@@ -33,6 +33,7 @@ export default {
|
|||||||
"nav.playground": "Playground",
|
"nav.playground": "Playground",
|
||||||
"nav.settings": "Configuración",
|
"nav.settings": "Configuración",
|
||||||
"nav.foot": "Gateway · v{v}",
|
"nav.foot": "Gateway · v{v}",
|
||||||
|
"nav.billing": "Clave del router y pagos",
|
||||||
|
|
||||||
// topbar / status
|
// topbar / status
|
||||||
"status.router": "Router",
|
"status.router": "Router",
|
||||||
@@ -40,6 +41,7 @@ export default {
|
|||||||
"status.online": "en línea",
|
"status.online": "en línea",
|
||||||
"status.offline": "sin conexión",
|
"status.offline": "sin conexión",
|
||||||
"status.checking": "verificando…",
|
"status.checking": "verificando…",
|
||||||
|
"status.noKey": "sin clave",
|
||||||
"theme.toggle": "Tema",
|
"theme.toggle": "Tema",
|
||||||
"logout": "Cerrar sesión",
|
"logout": "Cerrar sesión",
|
||||||
|
|
||||||
@@ -88,6 +90,7 @@ export default {
|
|||||||
"dash.health.title": "Estado",
|
"dash.health.title": "Estado",
|
||||||
"dash.health.reachable": "accesible",
|
"dash.health.reachable": "accesible",
|
||||||
"dash.health.unreachable": "inaccesible",
|
"dash.health.unreachable": "inaccesible",
|
||||||
|
"dash.health.getKey": "Obtener una clave del router",
|
||||||
|
|
||||||
// models
|
// models
|
||||||
"models.title": "Modelos",
|
"models.title": "Modelos",
|
||||||
@@ -225,6 +228,18 @@ export default {
|
|||||||
"settings.router.timeout": "Tiempo de espera (ms)",
|
"settings.router.timeout": "Tiempo de espera (ms)",
|
||||||
"settings.router.verifyTls": "Verificar certificado TLS",
|
"settings.router.verifyTls": "Verificar certificado TLS",
|
||||||
"settings.router.taxonomy": "ID de taxonomía",
|
"settings.router.taxonomy": "ID de taxonomía",
|
||||||
|
"settings.router.getKey": "Obtén o recarga una clave del router en",
|
||||||
|
"settings.router.secret": "Clave del router (valor)",
|
||||||
|
"settings.router.test": "Probar router",
|
||||||
|
"router.probe.ok": "Router OK ({ms} ms)",
|
||||||
|
"router.probe.noKey": "La clave del router no está configurada",
|
||||||
|
"router.probe.auth": "Clave rechazada — inválida o revocada",
|
||||||
|
"router.probe.payment": "Pago requerido — plan vencido o cuota agotada",
|
||||||
|
"router.probe.quota": "Límite de tasa excedido",
|
||||||
|
"router.probe.timeout": "Tiempo de espera del router agotado",
|
||||||
|
"router.probe.unreachable": "Router inaccesible",
|
||||||
|
"router.probe.error": "Error del router",
|
||||||
|
"router.usage": "Consumo",
|
||||||
"settings.route": "Entrada de enrutamiento",
|
"settings.route": "Entrada de enrutamiento",
|
||||||
"settings.route.strategy": "Estrategia de texto",
|
"settings.route.strategy": "Estrategia de texto",
|
||||||
"settings.route.maxChars": "Máx. caracteres",
|
"settings.route.maxChars": "Máx. caracteres",
|
||||||
|
|||||||
Vendored
+15
@@ -33,6 +33,7 @@ export default {
|
|||||||
"nav.playground": "Playground",
|
"nav.playground": "Playground",
|
||||||
"nav.settings": "Paramètres",
|
"nav.settings": "Paramètres",
|
||||||
"nav.foot": "Gateway · v{v}",
|
"nav.foot": "Gateway · v{v}",
|
||||||
|
"nav.billing": "Clé routeur & facturation",
|
||||||
|
|
||||||
// topbar / status
|
// topbar / status
|
||||||
"status.router": "Routeur",
|
"status.router": "Routeur",
|
||||||
@@ -40,6 +41,7 @@ export default {
|
|||||||
"status.online": "en ligne",
|
"status.online": "en ligne",
|
||||||
"status.offline": "hors ligne",
|
"status.offline": "hors ligne",
|
||||||
"status.checking": "vérification…",
|
"status.checking": "vérification…",
|
||||||
|
"status.noKey": "pas de clé",
|
||||||
"theme.toggle": "Thème",
|
"theme.toggle": "Thème",
|
||||||
"logout": "Déconnexion",
|
"logout": "Déconnexion",
|
||||||
|
|
||||||
@@ -88,6 +90,7 @@ export default {
|
|||||||
"dash.health.title": "Santé",
|
"dash.health.title": "Santé",
|
||||||
"dash.health.reachable": "accessible",
|
"dash.health.reachable": "accessible",
|
||||||
"dash.health.unreachable": "inaccessible",
|
"dash.health.unreachable": "inaccessible",
|
||||||
|
"dash.health.getKey": "Obtenir une clé routeur",
|
||||||
|
|
||||||
// models
|
// models
|
||||||
"models.title": "Modèles",
|
"models.title": "Modèles",
|
||||||
@@ -225,6 +228,18 @@ export default {
|
|||||||
"settings.router.timeout": "Délai d'expiration (ms)",
|
"settings.router.timeout": "Délai d'expiration (ms)",
|
||||||
"settings.router.verifyTls": "Vérifier le certificat TLS",
|
"settings.router.verifyTls": "Vérifier le certificat TLS",
|
||||||
"settings.router.taxonomy": "ID de taxonomie",
|
"settings.router.taxonomy": "ID de taxonomie",
|
||||||
|
"settings.router.getKey": "Obtenir ou recharger une clé routeur sur",
|
||||||
|
"settings.router.secret": "Clé routeur (valeur)",
|
||||||
|
"settings.router.test": "Tester le routeur",
|
||||||
|
"router.probe.ok": "Routeur OK ({ms} ms)",
|
||||||
|
"router.probe.noKey": "La clé routeur n'est pas définie",
|
||||||
|
"router.probe.auth": "Clé rejetée — invalide ou révoquée",
|
||||||
|
"router.probe.payment": "Paiement requis — forfait expiré ou quota épuisé",
|
||||||
|
"router.probe.quota": "Limite de débit dépassée",
|
||||||
|
"router.probe.timeout": "Délai du routeur dépassé",
|
||||||
|
"router.probe.unreachable": "Routeur inaccessible",
|
||||||
|
"router.probe.error": "Erreur du routeur",
|
||||||
|
"router.usage": "Consommation",
|
||||||
"settings.route": "Entrée de routage",
|
"settings.route": "Entrée de routage",
|
||||||
"settings.route.strategy": "Stratégie de texte",
|
"settings.route.strategy": "Stratégie de texte",
|
||||||
"settings.route.maxChars": "Caractères max",
|
"settings.route.maxChars": "Caractères max",
|
||||||
|
|||||||
Vendored
+15
@@ -33,6 +33,7 @@ export default {
|
|||||||
"nav.playground": "Oyun alanı",
|
"nav.playground": "Oyun alanı",
|
||||||
"nav.settings": "Ayarlar",
|
"nav.settings": "Ayarlar",
|
||||||
"nav.foot": "Gateway · v{v}",
|
"nav.foot": "Gateway · v{v}",
|
||||||
|
"nav.billing": "Router anahtarı ve ödeme",
|
||||||
|
|
||||||
// topbar / status
|
// topbar / status
|
||||||
"status.router": "Yönlendirici",
|
"status.router": "Yönlendirici",
|
||||||
@@ -40,6 +41,7 @@ export default {
|
|||||||
"status.online": "çevrimiçi",
|
"status.online": "çevrimiçi",
|
||||||
"status.offline": "çevrimdışı",
|
"status.offline": "çevrimdışı",
|
||||||
"status.checking": "kontrol ediliyor…",
|
"status.checking": "kontrol ediliyor…",
|
||||||
|
"status.noKey": "anahtar yok",
|
||||||
"theme.toggle": "Tema",
|
"theme.toggle": "Tema",
|
||||||
"logout": "Çıkış yap",
|
"logout": "Çıkış yap",
|
||||||
|
|
||||||
@@ -88,6 +90,7 @@ export default {
|
|||||||
"dash.health.title": "Sağlık",
|
"dash.health.title": "Sağlık",
|
||||||
"dash.health.reachable": "erişilebilir",
|
"dash.health.reachable": "erişilebilir",
|
||||||
"dash.health.unreachable": "erişilemiyor",
|
"dash.health.unreachable": "erişilemiyor",
|
||||||
|
"dash.health.getKey": "Router anahtarı al",
|
||||||
|
|
||||||
// models
|
// models
|
||||||
"models.title": "Modeller",
|
"models.title": "Modeller",
|
||||||
@@ -225,6 +228,18 @@ export default {
|
|||||||
"settings.router.timeout": "Zaman aşımı (ms)",
|
"settings.router.timeout": "Zaman aşımı (ms)",
|
||||||
"settings.router.verifyTls": "TLS sertifikasını doğrula",
|
"settings.router.verifyTls": "TLS sertifikasını doğrula",
|
||||||
"settings.router.taxonomy": "Taksonomi id",
|
"settings.router.taxonomy": "Taksonomi id",
|
||||||
|
"settings.router.getKey": "Router anahtarı almak veya bakiye yüklemek için:",
|
||||||
|
"settings.router.secret": "Router anahtarı (değer)",
|
||||||
|
"settings.router.test": "Router'ı test et",
|
||||||
|
"router.probe.ok": "Router çalışıyor ({ms} ms)",
|
||||||
|
"router.probe.noKey": "Router anahtarı ayarlanmadı",
|
||||||
|
"router.probe.auth": "Anahtar reddedildi — geçersiz veya iptal edilmiş",
|
||||||
|
"router.probe.payment": "Ödeme gerekli — plan süresi doldu veya kota tükendi",
|
||||||
|
"router.probe.quota": "Hız sınırı aşıldı",
|
||||||
|
"router.probe.timeout": "Router zaman aşımına uğradı",
|
||||||
|
"router.probe.unreachable": "Router erişilemiyor",
|
||||||
|
"router.probe.error": "Router hatası",
|
||||||
|
"router.usage": "Kullanım",
|
||||||
"settings.route": "Yönlendirme girişi",
|
"settings.route": "Yönlendirme girişi",
|
||||||
"settings.route.strategy": "Metin stratejisi",
|
"settings.route.strategy": "Metin stratejisi",
|
||||||
"settings.route.maxChars": "Maks. karakter",
|
"settings.route.maxChars": "Maks. karakter",
|
||||||
|
|||||||
Vendored
+15
@@ -33,6 +33,7 @@ export default {
|
|||||||
"nav.playground": "沙盒",
|
"nav.playground": "沙盒",
|
||||||
"nav.settings": "设置",
|
"nav.settings": "设置",
|
||||||
"nav.foot": "网关 · v{v}",
|
"nav.foot": "网关 · v{v}",
|
||||||
|
"nav.billing": "路由器密钥与充值",
|
||||||
|
|
||||||
// topbar / status
|
// topbar / status
|
||||||
"status.router": "路由器",
|
"status.router": "路由器",
|
||||||
@@ -40,6 +41,7 @@ export default {
|
|||||||
"status.online": "在线",
|
"status.online": "在线",
|
||||||
"status.offline": "离线",
|
"status.offline": "离线",
|
||||||
"status.checking": "检测中…",
|
"status.checking": "检测中…",
|
||||||
|
"status.noKey": "无密钥",
|
||||||
"theme.toggle": "主题",
|
"theme.toggle": "主题",
|
||||||
"logout": "退出",
|
"logout": "退出",
|
||||||
|
|
||||||
@@ -88,6 +90,7 @@ export default {
|
|||||||
"dash.health.title": "健康状态",
|
"dash.health.title": "健康状态",
|
||||||
"dash.health.reachable": "可达",
|
"dash.health.reachable": "可达",
|
||||||
"dash.health.unreachable": "不可达",
|
"dash.health.unreachable": "不可达",
|
||||||
|
"dash.health.getKey": "获取路由器密钥",
|
||||||
|
|
||||||
// models
|
// models
|
||||||
"models.title": "模型",
|
"models.title": "模型",
|
||||||
@@ -225,6 +228,18 @@ export default {
|
|||||||
"settings.router.timeout": "超时(ms)",
|
"settings.router.timeout": "超时(ms)",
|
||||||
"settings.router.verifyTls": "验证 TLS 证书",
|
"settings.router.verifyTls": "验证 TLS 证书",
|
||||||
"settings.router.taxonomy": "分类体系 ID",
|
"settings.router.taxonomy": "分类体系 ID",
|
||||||
|
"settings.router.getKey": "获取或充值路由器密钥:",
|
||||||
|
"settings.router.secret": "路由器密钥(值)",
|
||||||
|
"settings.router.test": "测试路由器",
|
||||||
|
"router.probe.ok": "路由器正常({ms} ms)",
|
||||||
|
"router.probe.noKey": "未设置路由器密钥",
|
||||||
|
"router.probe.auth": "密钥被拒绝——无效或已吊销",
|
||||||
|
"router.probe.payment": "需要付款——套餐到期或配额用尽",
|
||||||
|
"router.probe.quota": "超出速率限制",
|
||||||
|
"router.probe.timeout": "路由器响应超时",
|
||||||
|
"router.probe.unreachable": "路由器不可达",
|
||||||
|
"router.probe.error": "路由器错误",
|
||||||
|
"router.usage": "用量",
|
||||||
"settings.route": "路由输入",
|
"settings.route": "路由输入",
|
||||||
"settings.route.strategy": "文本策略",
|
"settings.route.strategy": "文本策略",
|
||||||
"settings.route.maxChars": "最大字符数",
|
"settings.route.maxChars": "最大字符数",
|
||||||
|
|||||||
Vendored
+27
-3
@@ -3,6 +3,7 @@ import { h, mount, money, num, ms, pct, timeAgo } from "../ui.js";
|
|||||||
import { t } from "../i18n.js";
|
import { t } from "../i18n.js";
|
||||||
import { api } from "../api.js";
|
import { api } from "../api.js";
|
||||||
import { lineChart, barList } from "../charts.js";
|
import { lineChart, barList } from "../charts.js";
|
||||||
|
import { SITE_URL } from "../app.js";
|
||||||
|
|
||||||
const RANGES = ["1h", "24h", "7d"];
|
const RANGES = ["1h", "24h", "7d"];
|
||||||
const GROUPS = ["model", "tier", "label", "account"];
|
const GROUPS = ["model", "tier", "label", "account"];
|
||||||
@@ -186,7 +187,19 @@ function recentTable(rows) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function keySourceLabel(src) {
|
||||||
|
if (src === "store") return t("models.form.secretStored");
|
||||||
|
if (src === "env") return t("models.form.secretEnv");
|
||||||
|
return t("models.form.secretMissing");
|
||||||
|
}
|
||||||
|
|
||||||
function healthCard(hh) {
|
function healthCard(hh) {
|
||||||
|
const r = hh.router || {};
|
||||||
|
const keySrc = r.key_source || "none";
|
||||||
|
const err = r.last_error;
|
||||||
|
// problems fixed on the site (get / pay for a key) get the top-up link
|
||||||
|
const needsSite =
|
||||||
|
!r.reachable || keySrc === "missing" || (err && ["auth", "payment", "quota"].includes(err.kind));
|
||||||
return h(
|
return h(
|
||||||
"div",
|
"div",
|
||||||
{ class: "card" },
|
{ class: "card" },
|
||||||
@@ -196,9 +209,20 @@ function healthCard(hh) {
|
|||||||
{ class: "flex wrap", style: "margin-bottom:12px" },
|
{ class: "flex wrap", style: "margin-bottom:12px" },
|
||||||
h(
|
h(
|
||||||
"span",
|
"span",
|
||||||
{ class: "badge " + (hh.router?.reachable ? "ok" : "bad"), title: hh.router?.url || "" },
|
{ class: "badge " + (r.reachable ? "ok" : "bad"), title: r.url || "" },
|
||||||
t("status.router") + ": " + (hh.router?.reachable ? t("dash.health.reachable") : t("dash.health.unreachable"))
|
t("status.router") + ": " + (r.reachable ? t("dash.health.reachable") : t("dash.health.unreachable"))
|
||||||
)
|
),
|
||||||
|
keySrc !== "none"
|
||||||
|
? h(
|
||||||
|
"span",
|
||||||
|
{ class: "badge " + keyBadge(keySrc), title: r.key_env || "" },
|
||||||
|
t("models.col.key") + ": " + keySourceLabel(keySrc)
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
err ? h("span", { class: "badge bad" }, t("router.probe." + (err.kind || "error"))) : null,
|
||||||
|
needsSite
|
||||||
|
? h("a", { href: SITE_URL, target: "_blank", rel: "noopener" }, t("dash.health.getKey") + " ↗")
|
||||||
|
: null
|
||||||
),
|
),
|
||||||
h(
|
h(
|
||||||
"div",
|
"div",
|
||||||
|
|||||||
Vendored
+332
@@ -0,0 +1,332 @@
|
|||||||
|
// Import a HuggingFace model → convert to a local .cmf (our format) with a
|
||||||
|
// live progress view. Search HF, pick quantization + advanced params, watch
|
||||||
|
// the conversion (multiple in parallel, resumable across tab switches), then
|
||||||
|
// register the result as a local model.
|
||||||
|
import { h, mount, toast } from "../ui.js";
|
||||||
|
import { t } from "../i18n.js";
|
||||||
|
import { api } from "../api.js";
|
||||||
|
|
||||||
|
const QUANTS = [
|
||||||
|
["Q8_2F", "import.q.q8_2f"],
|
||||||
|
["Q8_ROW", "import.q.q8_row"],
|
||||||
|
["Q4_BLOCK", "import.q.q4"],
|
||||||
|
["VBIT", "import.q.vbit"],
|
||||||
|
["F16", "import.q.f16"],
|
||||||
|
["F32", "import.q.f32"],
|
||||||
|
];
|
||||||
|
|
||||||
|
function field(label, control, hint) {
|
||||||
|
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 fmtNum(n) {
|
||||||
|
if (n == null) return "—";
|
||||||
|
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
|
||||||
|
if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
|
||||||
|
return String(n);
|
||||||
|
}
|
||||||
|
function fmtBytes(n) {
|
||||||
|
if (!n) return "";
|
||||||
|
const g = n / 1e9;
|
||||||
|
return g >= 1 ? g.toFixed(2) + " GB" : (n / 1e6).toFixed(0) + " MB";
|
||||||
|
}
|
||||||
|
// state → localized label (falls back to the raw state)
|
||||||
|
function stateLabel(s) {
|
||||||
|
const k = "import.state." + s;
|
||||||
|
const v = t(k);
|
||||||
|
return v === k ? s : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderImport() {
|
||||||
|
let selected = null;
|
||||||
|
let lastModels = []; // cache of the last search, so re-highlighting needs no refetch
|
||||||
|
let jobsTimer = null;
|
||||||
|
|
||||||
|
const searchIn = h("input", {
|
||||||
|
class: "hf-search",
|
||||||
|
placeholder: t("import.searchPlaceholder"),
|
||||||
|
autofocus: true,
|
||||||
|
});
|
||||||
|
const results = h("div", { class: "hf-grid" });
|
||||||
|
const formHost = h("div", {});
|
||||||
|
const jobsHost = h("div", {});
|
||||||
|
|
||||||
|
// ── live HF search (debounced) ──
|
||||||
|
let debounce = null;
|
||||||
|
function renderCards(models) {
|
||||||
|
if (!models.length) {
|
||||||
|
mount(results, h("div", { class: "muted pad" }, t("import.noResults")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mount(results, ...models.map(modelCard));
|
||||||
|
}
|
||||||
|
async function doSearch() {
|
||||||
|
const q = searchIn.value.trim();
|
||||||
|
mount(results, h("div", { class: "muted pad" }, t("import.searching")));
|
||||||
|
try {
|
||||||
|
const r = await api.hfSearch(q, 24);
|
||||||
|
lastModels = r.models || [];
|
||||||
|
renderCards(lastModels);
|
||||||
|
} catch (e) {
|
||||||
|
mount(results, h("div", { class: "err pad" }, "HF: " + e.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
searchIn.addEventListener("input", () => {
|
||||||
|
clearTimeout(debounce);
|
||||||
|
debounce = setTimeout(doSearch, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
function modelCard(m) {
|
||||||
|
const card = h(
|
||||||
|
"div",
|
||||||
|
{ class: "hf-card" + (selected && selected.id === m.id ? " sel" : "") },
|
||||||
|
h("div", { class: "hf-id" }, m.id),
|
||||||
|
h(
|
||||||
|
"div",
|
||||||
|
{ class: "hf-meta" },
|
||||||
|
m.pipeline_tag ? h("span", { class: "badge" }, m.pipeline_tag) : null,
|
||||||
|
h("span", { class: "muted" }, "▼ " + fmtNum(m.downloads)),
|
||||||
|
h("span", { class: "muted" }, "♥ " + fmtNum(m.likes)),
|
||||||
|
(m.tags || []).some((x) => x === "gated") ? h("span", { class: "badge bad" }, "gated") : null
|
||||||
|
)
|
||||||
|
);
|
||||||
|
card.addEventListener("click", () => selectModel(m));
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectModel(m) {
|
||||||
|
selected = m;
|
||||||
|
renderCards(lastModels); // re-highlight from cache — no refetch, no flash
|
||||||
|
renderForm();
|
||||||
|
formHost.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── conversion form ──
|
||||||
|
function renderForm() {
|
||||||
|
if (!selected) {
|
||||||
|
mount(formHost);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nameIn = h("input", { value: (selected.id.split("/").pop() || "model").toLowerCase() });
|
||||||
|
const quantSel = h("select", {}, ...QUANTS.map(([v], i) => opt(v, v, i === 0)));
|
||||||
|
const quantHint = h("div", { class: "hint" }, t("import.q.q8_2f"));
|
||||||
|
quantSel.addEventListener("change", () => {
|
||||||
|
const k = QUANTS.find(([v]) => v === quantSel.value);
|
||||||
|
quantHint.textContent = k ? t(k[1]) : "";
|
||||||
|
});
|
||||||
|
|
||||||
|
// advanced (collapsed)
|
||||||
|
const linearSel = h("select", {}, opt("", t("import.adv.auto")), opt("gated_delta_net", "gated_delta_net"), opt("vmf_phase", "vmf_phase"));
|
||||||
|
const nphaseIn = h("input", { type: "number", min: "1", placeholder: "16" });
|
||||||
|
const vbitSel = h("select", {}, opt("", t("import.adv.auto")), opt("log2", "log2"), opt("cubic", "cubic"));
|
||||||
|
const meanBitsIn = h("input", { type: "number", step: "0.1", placeholder: "5.0" });
|
||||||
|
const shardIn = h("input", { type: "number", step: "0.5", placeholder: t("import.adv.noShard") });
|
||||||
|
const skipMtp = h("input", { type: "checkbox" });
|
||||||
|
const advBody = h(
|
||||||
|
"div",
|
||||||
|
{ class: "adv-body", style: "display:none" },
|
||||||
|
h("div", { class: "row" },
|
||||||
|
field(t("import.adv.linearCore"), linearSel, t("import.adv.linearHint")),
|
||||||
|
field(t("import.adv.nphase"), nphaseIn)),
|
||||||
|
h("div", { class: "row" },
|
||||||
|
field(t("import.adv.vbitShape"), vbitSel),
|
||||||
|
field(t("import.adv.meanBits"), meanBitsIn)),
|
||||||
|
h("div", { class: "row" },
|
||||||
|
field(t("import.adv.shard"), shardIn),
|
||||||
|
field("MTP", h("label", { class: "check" }, skipMtp, t("import.adv.skipMtp")))),
|
||||||
|
);
|
||||||
|
const advToggle = h("button", { class: "btn ghost sm" }, "⚙ " + t("import.adv.title"));
|
||||||
|
advToggle.addEventListener("click", () => {
|
||||||
|
advBody.style.display = advBody.style.display === "none" ? "block" : "none";
|
||||||
|
});
|
||||||
|
|
||||||
|
const goBtn = h("button", { class: "btn primary" }, "⬇ " + t("import.convert"));
|
||||||
|
goBtn.addEventListener("click", async () => {
|
||||||
|
const params = {
|
||||||
|
repo: selected.id,
|
||||||
|
quant: quantSel.value,
|
||||||
|
name: nameIn.value.trim(),
|
||||||
|
skip_mtp: skipMtp.checked,
|
||||||
|
};
|
||||||
|
if (linearSel.value) params.linear_core = linearSel.value;
|
||||||
|
if (nphaseIn.value) params.nphase = parseInt(nphaseIn.value);
|
||||||
|
if (vbitSel.value) params.vbit_shape = vbitSel.value;
|
||||||
|
if (meanBitsIn.value) params.mean_bits = parseFloat(meanBitsIn.value);
|
||||||
|
if (shardIn.value) params.shard_max_gb = parseFloat(shardIn.value);
|
||||||
|
goBtn.disabled = true;
|
||||||
|
try {
|
||||||
|
await api.startImport(params);
|
||||||
|
toast(t("import.started"), "ok");
|
||||||
|
await refreshJobs(); // picks up the new job + (re)starts polling
|
||||||
|
jobsHost.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||||||
|
} catch (e) {
|
||||||
|
toast(e.message, "err");
|
||||||
|
}
|
||||||
|
goBtn.disabled = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
mount(
|
||||||
|
formHost,
|
||||||
|
h("div", { class: "card import-form" },
|
||||||
|
h("div", { class: "card-title" }, t("import.configure") + ": " + selected.id),
|
||||||
|
h("div", { class: "row" },
|
||||||
|
field(t("import.outName"), nameIn, t("import.outNameHint")),
|
||||||
|
field(t("import.quant"), quantSel)),
|
||||||
|
quantHint,
|
||||||
|
h("div", { class: "adv" }, advToggle, advBody),
|
||||||
|
h("div", { class: "actions" }, goBtn),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── jobs panel: all conversions, live, resumable, cancellable ──
|
||||||
|
function ensurePolling() {
|
||||||
|
if (!jobsTimer) {
|
||||||
|
jobsTimer = setInterval(() => {
|
||||||
|
// stop once this view instance was replaced (navigated away)
|
||||||
|
if (!root.isConnected) {
|
||||||
|
stopPolling();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
refreshJobs();
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function stopPolling() {
|
||||||
|
if (jobsTimer) {
|
||||||
|
clearInterval(jobsTimer);
|
||||||
|
jobsTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Load + render all jobs. Runs on mount (before `root` is attached — so no
|
||||||
|
// isConnected guard here) and on each poll tick.
|
||||||
|
async function refreshJobs() {
|
||||||
|
let jobs = [];
|
||||||
|
try {
|
||||||
|
jobs = (await api.listImports()).jobs || [];
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
jobs = jobs.filter((j) => j.state !== "cancelled"); // hide user-cancelled jobs
|
||||||
|
renderJobs(jobs);
|
||||||
|
if (jobs.some((j) => j.state === "running")) ensurePolling();
|
||||||
|
else stopPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
function jobCard(j) {
|
||||||
|
const running = j.state === "running";
|
||||||
|
const pct = j.progress != null ? Math.round(j.progress * 100) : null;
|
||||||
|
// det = solid bar at a known %, run = indeterminate slider, ok/err = terminal.
|
||||||
|
let bar;
|
||||||
|
if (j.state === "done") {
|
||||||
|
bar = h("div", { class: "pbar-fill ok", style: "width:100%" });
|
||||||
|
} else if (running && pct != null) {
|
||||||
|
bar = h("div", { class: "pbar-fill det", style: `width:${pct}%` });
|
||||||
|
} else if (running) {
|
||||||
|
bar = h("div", { class: "pbar-fill run" }); // CSS drives width + animation
|
||||||
|
} else {
|
||||||
|
bar = h("div", { class: "pbar-fill err", style: "width:100%" }); // error / cancelled
|
||||||
|
}
|
||||||
|
const barWrap = h("div", { class: "pbar" }, bar);
|
||||||
|
const badge = h(
|
||||||
|
"span",
|
||||||
|
{ class: "badge " + (j.state === "done" ? "" : running ? "" : "bad") },
|
||||||
|
stateLabel(j.state) +
|
||||||
|
(running && pct != null ? " · " + pct + "%" : "") +
|
||||||
|
(j.size_bytes ? " · " + fmtBytes(j.size_bytes) : "")
|
||||||
|
);
|
||||||
|
const phase = h("div", { class: "muted small" }, j.phase || "");
|
||||||
|
const logEl = h("pre", { class: "conv-log" }, (j.log || []).slice(-10).join("\n"));
|
||||||
|
|
||||||
|
const actions = [];
|
||||||
|
if (running) {
|
||||||
|
const c = h("button", { class: "btn ghost sm" }, "✕ " + t("import.cancel"));
|
||||||
|
c.addEventListener("click", async () => {
|
||||||
|
c.disabled = true;
|
||||||
|
try {
|
||||||
|
await api.cancelImport(j.id);
|
||||||
|
toast(t("import.cancelling"), "warn");
|
||||||
|
refreshJobs();
|
||||||
|
} catch (e) {
|
||||||
|
toast(e.message, "err");
|
||||||
|
c.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
actions.push(c);
|
||||||
|
}
|
||||||
|
if (j.state === "done") {
|
||||||
|
const r = h("button", { class: "btn primary sm" }, "✓ " + t("import.register"));
|
||||||
|
r.addEventListener("click", async () => {
|
||||||
|
r.disabled = true;
|
||||||
|
try {
|
||||||
|
const rr = await api.registerImport(j.id);
|
||||||
|
toast(t("import.registered", { id: rr.model_id }), "ok");
|
||||||
|
} catch (e) {
|
||||||
|
toast(e.message, "err");
|
||||||
|
r.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
actions.push(r);
|
||||||
|
}
|
||||||
|
if (!running) {
|
||||||
|
// delete the job + its converted .cmf file(s) from disk
|
||||||
|
const d = h("button", { class: "btn ghost sm danger" }, "🗑 " + t("import.delete"));
|
||||||
|
d.addEventListener("click", async () => {
|
||||||
|
if (!window.confirm(t("import.deleteConfirm"))) return;
|
||||||
|
d.disabled = true;
|
||||||
|
try {
|
||||||
|
await api.deleteImport(j.id);
|
||||||
|
toast(t("import.deleted"), "ok");
|
||||||
|
refreshJobs();
|
||||||
|
} catch (e) {
|
||||||
|
toast(e.message, "err");
|
||||||
|
d.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
actions.push(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
return h(
|
||||||
|
"div",
|
||||||
|
{ class: "job" },
|
||||||
|
h("div", { class: "job-head" }, h("b", {}, j.repo || j.id), badge),
|
||||||
|
barWrap,
|
||||||
|
phase,
|
||||||
|
logEl,
|
||||||
|
actions.length ? h("div", { class: "actions" }, ...actions) : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderJobs(jobs) {
|
||||||
|
if (!jobs.length) {
|
||||||
|
mount(jobsHost);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mount(
|
||||||
|
jobsHost,
|
||||||
|
h(
|
||||||
|
"div",
|
||||||
|
{ class: "card" },
|
||||||
|
h("div", { class: "card-title" }, t("import.jobs")),
|
||||||
|
...jobs.map(jobCard)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = h(
|
||||||
|
"div",
|
||||||
|
{},
|
||||||
|
h("div", { class: "view-head" },
|
||||||
|
h("h2", {}, t("nav.import")),
|
||||||
|
h("div", { class: "muted" }, t("import.subtitle"))),
|
||||||
|
jobsHost, // progress panel on top, above the search
|
||||||
|
h("div", { class: "card" }, searchIn, results),
|
||||||
|
formHost
|
||||||
|
);
|
||||||
|
doSearch(); // initial trending list (mounts into `results`)
|
||||||
|
refreshJobs(); // resume: show any running/recent conversions immediately
|
||||||
|
return root;
|
||||||
|
}
|
||||||
Vendored
+7
-1
@@ -2,6 +2,7 @@
|
|||||||
import { h } from "../ui.js";
|
import { h } from "../ui.js";
|
||||||
import { t } from "../i18n.js";
|
import { t } from "../i18n.js";
|
||||||
import { setToken } from "../api.js";
|
import { setToken } from "../api.js";
|
||||||
|
import { SITE_URL } from "../app.js";
|
||||||
|
|
||||||
export function renderLogin(onOk) {
|
export function renderLogin(onOk) {
|
||||||
const input = h("input", {
|
const input = h("input", {
|
||||||
@@ -34,7 +35,12 @@ export function renderLogin(onOk) {
|
|||||||
h("p", { class: "sub" }, t("login.subtitle")),
|
h("p", { class: "sub" }, t("login.subtitle")),
|
||||||
h("label", { class: "field" }, h("span", {}, t("login.token")), input),
|
h("label", { class: "field" }, h("span", {}, t("login.token")), input),
|
||||||
h("button", { class: "btn primary", style: "width:100%", onClick: submit }, t("login.submit")),
|
h("button", { class: "btn primary", style: "width:100%", onClick: submit }, t("login.submit")),
|
||||||
h("p", { class: "hint", style: "margin-top:16px" }, t("login.hint"))
|
h("p", { class: "hint", style: "margin-top:16px" }, t("login.hint")),
|
||||||
|
h(
|
||||||
|
"p",
|
||||||
|
{ class: "hint", style: "margin-top:8px" },
|
||||||
|
h("a", { href: SITE_URL, target: "_blank", rel: "noopener" }, t("nav.billing") + " · api.allaigate.com ↗")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user