feat: universal LLM gateway with embedded multilingual admin console

- OpenAI-compatible gateway with intelligent routing via cortiq-router (allaigate)
- Embedded build-free admin SPA at /admin: 7 languages (en/ru/de/fr/es/zh/tr),
  light/dark themes, hot config reload (no restart)
- Admin API, per-request statistics + Prometheus /metrics, secret store, playground
- English documentation (primary) with Russian translations (*.ru.md)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
infosave2007
2026-06-30 10:51:37 +03:00
co-authored by Claude Opus 4.8
commit 51487e6329
63 changed files with 11339 additions and 0 deletions
+218
View File
@@ -0,0 +1,218 @@
// Dashboard — KPIs, time-series charts, breakdown, recent requests, health.
import { h, mount, money, num, ms, pct, timeAgo } from "../ui.js";
import { t } from "../i18n.js";
import { api } from "../api.js";
import { lineChart, barList } from "../charts.js";
const RANGES = ["1h", "24h", "7d"];
const GROUPS = ["model", "tier", "label", "account"];
function kpi(big, lbl) {
return h("div", { class: "kpi" }, h("div", { class: "big" }, big), h("div", { class: "lbl" }, lbl));
}
function seg(options, current, onPick, labelFn) {
return h(
"div",
{ class: "seg" },
...options.map((o) =>
h("button", { class: o === current ? "active" : "", onClick: () => onPick(o) }, labelFn ? labelFn(o) : o)
)
);
}
export async function renderDashboard() {
let range = "24h";
let groupby = "model";
const root = h("div");
async function load() {
const rangeSecs = range;
let stats, recent, health;
try {
[stats, recent, health] = await Promise.all([
api.stats(rangeSecs, groupby),
api.requests(40, 0),
api.health().catch(() => null),
]);
} catch (e) {
mount(root, h("div", { class: "callout" }, String(e.message || e)));
return;
}
const tot = stats.totals || {};
const series = stats.series || [];
const breakdown = (stats.breakdown || []).map((b) => ({ key: b.key || t("common.none"), value: b.requests }));
mount(
root,
h(
"div",
{ class: "page-head" },
h(
"div",
{ class: "flex" },
h("div", { class: "grow" }, h("h2", {}, t("dash.title")), h("p", {}, t("dash.subtitle"))),
seg(RANGES, range, (r) => {
range = r;
load();
}, (r) => t("dash.range." + r))
)
),
// KPIs
h(
"div",
{ class: "kpis" },
kpi(num(tot.requests), t("dash.kpi.requests")),
kpi(num(tot.total_tokens), t("dash.kpi.tokens")),
kpi(money(tot.cost_usd), t("dash.kpi.cost")),
kpi(ms(tot.avg_latency_ms), t("dash.kpi.latency")),
kpi(pct(tot.success_rate), t("dash.kpi.success")),
kpi(num(tot.failovers), t("dash.kpi.failovers"))
),
h(
"div",
{ class: "grid cols-2" },
h(
"div",
{ class: "card" },
h("div", { class: "card-head" }, h("h3", {}, t("dash.series.title"))),
series.length < 2 ? h("div", { class: "empty" }, t("dash.series.empty")) : lineChart(series, "requests")
),
h(
"div",
{ class: "card" },
h("div", { class: "card-head" }, h("h3", {}, t("dash.series.cost"))),
series.length < 2 ? h("div", { class: "empty" }, t("dash.series.empty")) : lineChart(series, "cost_usd")
)
),
// breakdown
h(
"div",
{ class: "card" },
h(
"div",
{ class: "card-head" },
h("h3", {}, t("dash.breakdown.title")),
h(
"div",
{ class: "right" },
seg(GROUPS, groupby, (g) => {
groupby = g;
load();
}, (g) => t("dash.groupby." + g))
)
),
barList(breakdown, num)
),
// recent
h(
"div",
{ class: "card" },
h("div", { class: "card-head" }, h("h3", {}, t("dash.recent.title"))),
recentTable(recent.requests || [])
),
health ? healthCard(health) : null
);
}
await load();
return root;
}
function recentTable(rows) {
if (!rows.length) return h("div", { class: "empty" }, t("dash.recent.empty"));
return h(
"div",
{ class: "table-wrap" },
h(
"table",
{},
h(
"thead",
{},
h(
"tr",
{},
h("th", {}, t("dash.col.time")),
h("th", {}, t("dash.col.model")),
h("th", {}, t("dash.col.tier")),
h("th", {}, t("dash.col.task")),
h("th", {}, t("dash.col.tokens")),
h("th", {}, t("dash.col.cost")),
h("th", {}, t("dash.col.latency")),
h("th", {}, t("dash.col.outcome"))
)
),
h(
"tbody",
{},
...rows.map((r) =>
h(
"tr",
{},
h("td", { class: "mono" }, timeAgo(r.ts)),
h("td", {}, r.model_id || "—", r.failover ? h("span", { class: "badge warn", style: "margin-left:6px" }, "fo") : null),
h("td", {}, r.tier ? h("span", { class: "badge tier-" + r.tier }, r.tier) : "—"),
h("td", {}, r.task_label || "—"),
h("td", { class: "mono" }, `${r.prompt_tokens}/${r.completion_tokens}`),
h("td", { class: "mono" }, money(r.cost_usd)),
h("td", { class: "mono" }, ms(r.latency_ms)),
h("td", {}, h("span", { class: "badge " + (r.outcome === "ok" ? "ok" : "error") }, r.outcome))
)
)
)
)
);
}
function healthCard(hh) {
return h(
"div",
{ class: "card" },
h("div", { class: "card-head" }, h("h3", {}, t("dash.health.title"))),
h(
"div",
{ class: "flex wrap", style: "margin-bottom:12px" },
h(
"span",
{ class: "badge " + (hh.router?.reachable ? "ok" : "bad"), title: hh.router?.url || "" },
t("status.router") + ": " + (hh.router?.reachable ? t("dash.health.reachable") : t("dash.health.unreachable"))
)
),
h(
"div",
{ class: "table-wrap" },
h(
"table",
{},
h("thead", {}, h("tr", {}, h("th", {}, t("models.col.id")), h("th", {}, t("models.col.provider")), h("th", {}, t("models.col.model")), h("th", {}, t("models.col.key")))),
h(
"tbody",
{},
...(hh.models || []).map((m) =>
h(
"tr",
{},
h("td", { class: "mono" }, m.id),
h("td", {}, m.provider),
h("td", { class: "mono" }, m.model),
h("td", {}, h("span", { class: "badge " + keyBadge(m.key_source) }, m.key_source))
)
)
)
)
)
);
}
function keyBadge(src) {
if (src === "store") return "store";
if (src === "env") return "env";
if (src === "missing") return "bad";
return "";
}
+128
View File
@@ -0,0 +1,128 @@
// API keys — list, create (with copy-once), revoke.
import { h, mount, modal, toast, confirmDialog } from "../ui.js";
import { t } from "../i18n.js";
import { api } from "../api.js";
export async function renderKeys() {
const modelsData = await api.listModels();
const ids = (modelsData.models || []).map((m) => m.id);
const root = h("div");
function openCreate(reload) {
const accountIn = h("input", { placeholder: "team-a" });
const rateIn = h("input", { type: "number", value: 600 });
const customIn = h("input", { placeholder: "sk-gw-…" });
const allowInputs = ids.map((id) => ({ id, cb: h("input", { type: "checkbox" }) }));
const body = h(
"div",
{},
h("label", { class: "field" }, h("span", {}, t("keys.form.account")), accountIn),
h("label", { class: "field" }, h("span", {}, t("keys.form.rate")), rateIn),
h(
"label",
{ class: "field" },
h("span", {}, t("keys.form.allow")),
h("div", { class: "flex wrap" }, ...allowInputs.map(({ id, cb }) => h("label", { class: "check" }, cb, id))),
h("div", { class: "hint" }, t("keys.form.allowHint"))
),
h("label", { class: "field" }, h("span", {}, t("keys.form.custom")), customIn, h("div", { class: "hint" }, t("keys.form.customHint")))
);
modal(t("keys.form.title"), body, async () => {
try {
const r = await api.createKey({
key: customIn.value.trim(),
account: accountIn.value.trim() || "default",
rate_per_min: parseInt(rateIn.value) || 0,
allow_models: allowInputs.filter(({ cb }) => cb.checked).map(({ id }) => id),
});
reload();
showCreated(r.key);
} catch (e) {
toast(String(e.message || e), "bad");
return false;
}
}, t("common.add"));
}
function showCreated(key) {
const input = h("input", { value: key, readonly: true, class: "mono" });
const copyBtn = h("button", { class: "btn", onClick: () => { input.select(); navigator.clipboard?.writeText(key); toast(t("common.copied"), "good"); } }, t("common.copy"));
modal(t("common.ok"), h("div", {}, h("p", {}, t("keys.created")), h("div", { class: "flex" }, input, copyBtn)), null);
}
async function reload() {
const data = await api.listKeys();
const keys = data.keys || [];
mount(
root,
h(
"div",
{ class: "page-head" },
h(
"div",
{ class: "flex" },
h("div", { class: "grow" }, h("h2", {}, t("keys.title")), h("p", {}, t("keys.subtitle"))),
h("button", { class: "btn primary", onClick: () => openCreate(reload) }, "+ " + t("keys.add"))
)
),
data.open_mode ? h("div", { class: "callout info" }, t("keys.openMode")) : null,
keys.length === 0
? h("div", { class: "card" }, h("div", { class: "empty" }, t("keys.empty")))
: h(
"div",
{ class: "card" },
h(
"div",
{ class: "table-wrap" },
h(
"table",
{},
h(
"thead",
{},
h("tr", {}, h("th", {}, t("keys.col.key")), h("th", {}, t("keys.col.account")), h("th", {}, t("keys.col.rate")), h("th", {}, t("keys.col.models")), h("th", { class: "right-align" }, t("common.actions")))
),
h(
"tbody",
{},
...keys.map((k) =>
h(
"tr",
{},
h("td", { class: "mono" }, k.key_masked),
h("td", {}, k.account),
h("td", { class: "mono" }, k.rate_per_min || "—"),
h("td", {}, (k.allow_models && k.allow_models.length) ? k.allow_models.join(", ") : h("span", { class: "muted" }, t("keys.allowAll"))),
h(
"td",
{ class: "right-align" },
h(
"button",
{
class: "btn sm danger",
onClick: async () => {
if (!(await confirmDialog(t("keys.revokeConfirm")))) return;
try {
await api.deleteKey(k.key);
toast(t("toast.deleted"), "good");
reload();
} catch (e) {
toast(String(e.message || e), "bad");
}
},
},
t("keys.revoke")
)
)
)
)
)
)
)
)
);
}
await reload();
return root;
}
+40
View File
@@ -0,0 +1,40 @@
// Login screen — admin token entry. Shown when there is no token or on 401.
import { h } from "../ui.js";
import { t } from "../i18n.js";
import { setToken } from "../api.js";
export function renderLogin(onOk) {
const input = h("input", {
type: "password",
placeholder: t("login.tokenPlaceholder"),
autofocus: true,
});
const submit = () => {
const val = input.value.trim();
if (!val) return;
setToken(val);
onOk();
};
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") submit();
});
return h(
"div",
{ class: "login-wrap" },
h(
"div",
{ class: "login-card" },
h(
"div",
{ class: "brand" },
h("b", { style: "font-size:20px" }, "allaigate"),
h("span", { class: "sub" }, t("brand.sub"))
),
h("h2", {}, t("login.title")),
h("p", { class: "sub" }, t("login.subtitle")),
h("label", { class: "field" }, h("span", {}, t("login.token")), input),
h("button", { class: "btn primary", style: "width:100%", onClick: submit }, t("login.submit")),
h("p", { class: "hint", style: "margin-top:16px" }, t("login.hint"))
)
);
}
+206
View File
@@ -0,0 +1,206 @@
// Models — pool table with add/edit modal, probe, and per-model secret entry.
import { h, mount, modal, toast, confirmDialog, money } from "../ui.js";
import { t } from "../i18n.js";
import { api } from "../api.js";
import { appState } from "../app.js";
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 keyBadge(src) {
const map = { store: "store", env: "env", missing: "bad", none: "" };
return h("span", { class: "badge " + (map[src] ?? "") }, src);
}
function modelForm(meta, existing) {
const m = existing || { provider: "openai", kind: "chat", cost_tier: "cheap", caps: [], price_in: 0, price_out: 0 };
const idIn = h("input", { value: m.id || "", placeholder: "local-qwen", disabled: existing ? true : null });
const providerSel = h("select", {}, ...(meta.providers || ["openai"]).map((p) => opt(p, p, p === m.provider)));
const baseUrlIn = h("input", { value: m.base_url || "", placeholder: "http://localhost:8000/v1" });
const modelIn = h("input", { value: m.model || "", placeholder: "qwen2.5-7b-instruct" });
const kindSel = h("select", {}, ...(meta.kinds || ["chat"]).map((k) => opt(k, k, k === m.kind)));
const tierSel = h("select", {}, ...(meta.cost_tiers || ["cheap"]).map((c) => opt(c, c, c === m.cost_tier)));
const priceInIn = h("input", { type: "number", step: "0.01", value: m.price_in ?? 0 });
const priceOutIn = h("input", { type: "number", step: "0.01", value: m.price_out ?? 0 });
const keyEnvIn = h("input", { value: m.api_key_env || "", placeholder: "OPENAI_API_KEY" });
const secretIn = h("input", { type: "password", placeholder: t("models.form.secretPlaceholder") });
const capInputs = (meta.caps || ["tools", "vision"]).map((c) => {
const cb = h("input", { type: "checkbox", checked: (m.caps || []).includes(c) ? true : null });
return { c, cb };
});
const node = h(
"div",
{},
h("div", { class: "row" }, field(t("models.form.id"), idIn, existing ? null : t("models.form.idHint")), field(t("models.form.provider"), providerSel)),
field(t("models.form.baseUrl"), baseUrlIn),
field(t("models.form.model"), modelIn),
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)),
field(t("models.form.caps"), h("div", { class: "flex wrap" }, ...capInputs.map(({ c, cb }) => h("label", { class: "check" }, cb, c)))),
field(t("models.form.apiKeyEnv"), keyEnvIn, t("models.form.apiKeyEnvHint")),
field(t("models.form.secret"), secretIn)
);
const getValue = () => ({
model: {
id: existing ? m.id : idIn.value.trim(),
provider: providerSel.value,
base_url: baseUrlIn.value.trim(),
model: modelIn.value.trim(),
kind: kindSel.value,
cost_tier: tierSel.value,
price_in: parseFloat(priceInIn.value) || 0,
price_out: parseFloat(priceOutIn.value) || 0,
api_key_env: keyEnvIn.value.trim() || null,
caps: capInputs.filter(({ cb }) => cb.checked).map(({ c }) => c),
},
secret: secretIn.value,
keyEnv: keyEnvIn.value.trim(),
});
return { node, getValue };
}
async function openModal(meta, existing, reload) {
const { node, getValue } = modelForm(meta, existing);
modal(existing ? t("models.form.editTitle") : t("models.form.addTitle"), node, async () => {
const { model, secret, keyEnv } = getValue();
if (!model.id) {
toast(t("models.form.id") + "?", "bad");
return false;
}
// strip null api_key_env so serde keeps it optional
if (!model.api_key_env) delete model.api_key_env;
try {
if (existing) await api.updateModel(existing.id, model);
else await api.createModel(model);
if (secret && keyEnv) await api.setSecret(keyEnv, secret);
toast(t("toast.saved"), "good");
reload();
} catch (e) {
toast(String(e.message || e), "bad");
return false;
}
});
}
export async function renderModels() {
const meta = appState.meta || (await api.meta());
const root = h("div");
async function reload() {
const data = await api.listModels();
const models = data.models || [];
mount(
root,
h(
"div",
{ class: "page-head" },
h(
"div",
{ class: "flex" },
h("div", { class: "grow" }, h("h2", {}, t("models.title")), h("p", {}, t("models.subtitle"))),
h("button", { class: "btn primary", onClick: () => openModal(meta, null, reload) }, "+ " + t("models.add"))
)
),
models.length === 0
? h("div", { class: "card" }, h("div", { class: "empty" }, t("models.empty")))
: h(
"div",
{ class: "card" },
h(
"div",
{ class: "table-wrap" },
h(
"table",
{},
h(
"thead",
{},
h(
"tr",
{},
h("th", {}, t("models.col.id")),
h("th", {}, t("models.col.provider")),
h("th", {}, t("models.col.model")),
h("th", {}, t("models.col.tier")),
h("th", {}, t("models.col.key")),
h("th", {}, t("models.col.caps")),
h("th", {}, t("models.col.price")),
h("th", { class: "right-align" }, t("common.actions"))
)
),
h(
"tbody",
{},
...models.map((m) => row(m, meta, reload))
)
)
)
)
);
}
await reload();
return root;
}
function row(m, meta, reload) {
const probeBtn = h("button", { class: "btn sm ghost" }, t("models.probe"));
probeBtn.addEventListener("click", async () => {
probeBtn.disabled = true;
probeBtn.textContent = "…";
try {
const r = await api.probeModel(m.id);
if (r.ok) toast(t("models.probe.ok", { ms: r.latency_ms }), "good");
else toast(t("models.probe.fail", { err: r.error || "" }), "bad");
} catch (e) {
toast(String(e.message || e), "bad");
}
probeBtn.disabled = false;
probeBtn.textContent = t("models.probe");
});
return h(
"tr",
{},
h("td", { class: "mono" }, m.id),
h("td", {}, m.provider),
h("td", { class: "mono" }, m.model),
h("td", {}, h("span", { class: "badge " + (m.cost_tier || "") }, m.cost_tier || "—")),
h("td", {}, keyBadge(m.key_source || (m.api_key_env ? "missing" : "none"))),
h("td", {}, (m.caps || []).length ? h("div", { class: "flex wrap" }, ...m.caps.map((c) => h("span", { class: "badge" }, c))) : "—"),
h("td", { class: "mono" }, `${m.price_in || 0}/${m.price_out || 0}`),
h(
"td",
{ class: "right-align" },
h(
"div",
{ class: "flex", style: "justify-content:flex-end" },
probeBtn,
h("button", { class: "btn sm ghost", onClick: () => openModal(meta, m, reload) }, t("common.edit")),
h(
"button",
{
class: "btn sm danger",
onClick: async () => {
if (!(await confirmDialog(t("models.deleteConfirm", { id: m.id })))) return;
try {
await api.deleteModel(m.id);
toast(t("toast.deleted"), "good");
reload();
} catch (e) {
toast(String(e.message || e), "bad");
}
},
},
t("common.delete")
)
)
)
);
}
+137
View File
@@ -0,0 +1,137 @@
// Playground — send a prompt through the live pipeline, inspect routing decision.
import { h, mount, money, ms, num } from "../ui.js";
import { t } from "../i18n.js";
import { api } from "../api.js";
import { appState } from "../app.js";
export async function renderPlayground() {
const meta = appState.meta || (await api.meta());
const modelsData = await api.listModels();
const ids = (modelsData.models || []).map((m) => m.id);
let mode = "auto";
const root = h("div");
const sysIn = h("textarea", { placeholder: t("pg.systemPlaceholder"), rows: 2 });
const promptIn = h("textarea", { placeholder: t("pg.promptPlaceholder"), rows: 5 });
const tempIn = h("input", { type: "number", step: "0.1", value: 0.7 });
const maxTokIn = h("input", { type: "number", value: 512 });
const profileSel = h("select", {}, ...(meta.profiles || []).map((p) => h("option", { value: p }, p)));
const modelSel = h("select", {}, ...ids.map((id) => h("option", { value: id }, id)));
const modeSeg = h(
"div",
{ class: "seg" },
h("button", { class: "active", onClick: (e) => setMode("auto", e) }, t("pg.mode.auto")),
h("button", { onClick: (e) => setMode("pinned", e) }, t("pg.mode.pinned"))
);
function buildModeExtra() {
return h(
"label",
{ class: "field" },
h("span", {}, mode === "auto" ? t("pg.profile") : t("pg.model")),
mode === "auto" ? profileSel : modelSel
);
}
let modeExtra = buildModeExtra();
function setMode(m, e) {
mode = m;
modeSeg.querySelectorAll("button").forEach((b) => b.classList.remove("active"));
e.target.classList.add("active");
const next = buildModeExtra();
modeExtra.replaceWith(next);
modeExtra = next;
}
const out = h("div", { class: "answer" }, h("div", { class: "empty" }, t("pg.empty")));
const decision = h("div", { class: "card routecard" }, h("div", { class: "card-head" }, h("h3", {}, t("pg.decision"))), h("div", { class: "empty" }, "—"));
const sendBtn = h("button", { class: "btn primary" }, t("pg.send"));
sendBtn.addEventListener("click", async () => {
const prompt = promptIn.value.trim();
if (!prompt) return;
const messages = [];
if (sysIn.value.trim()) messages.push({ role: "system", content: sysIn.value.trim() });
messages.push({ role: "user", content: prompt });
let model = "cortiq-auto";
if (mode === "auto") {
if (profileSel.value) model = "cortiq-auto:" + profileSel.value;
} else {
model = modelSel.value;
}
sendBtn.disabled = true;
sendBtn.textContent = t("pg.sending");
mount(out, h("div", { class: "flex" }, h("span", { class: "spinner" }), " ", t("pg.sending")));
try {
const r = await api.test({
model,
messages,
temperature: parseFloat(tempIn.value),
max_tokens: parseInt(maxTokIn.value) || null,
});
if (r.ok) {
mount(out, r.answer || h("span", { class: "muted" }, "—"));
renderDecision(r);
} else {
mount(out, h("div", { class: "callout" }, h("b", {}, t("pg.error") + ": "), r.error || ""));
mount(decision, h("div", { class: "card-head" }, h("h3", {}, t("pg.decision"))), h("div", { class: "empty" }, "—"));
}
} catch (e) {
mount(out, h("div", { class: "callout" }, String(e.message || e)));
}
sendBtn.disabled = false;
sendBtn.textContent = t("pg.send");
});
function kv(k, v) {
return h("div", { class: "kv" }, h("span", { class: "k" }, k), h("span", { class: "v" }, v));
}
function renderDecision(r) {
const c = r.cortiq || {};
const score = c.complexity?.score ?? 0;
mount(
decision,
h("div", { class: "card-head" }, h("h3", {}, t("pg.decision"))),
kv(t("pg.task"), c.task_label || "—"),
kv(t("pg.tier"), c.complexity?.tier ? h("span", { class: "badge tier-" + c.complexity.tier }, c.complexity.tier) : "—"),
h(
"div",
{ class: "kv", style: "display:block" },
h("div", { class: "flex" }, h("span", { class: "k grow" }, t("pg.score")), h("span", { class: "v" }, score.toFixed(2))),
h("div", { class: "scorebar" }, h("i", { style: `width:${Math.min(100, score * 100)}%` }))
),
kv(t("pg.selected"), h("span", { class: "mono" }, c.selected_model || "—")),
kv(t("pg.source"), c.route_source || "—"),
kv(t("pg.tokens"), `${r.usage?.prompt_tokens ?? 0}/${r.usage?.completion_tokens ?? 0}`),
kv(t("pg.cost"), money(c.cost_usd)),
kv(t("pg.latency"), ms(r.latency_ms)),
kv(t("pg.failover"), c.failover ? "yes" : "no")
);
}
mount(
root,
h("div", { class: "page-head" }, h("h2", {}, t("pg.title")), h("p", {}, t("pg.subtitle"))),
h(
"div",
{ class: "pg" },
h(
"div",
{},
h(
"div",
{ class: "card" },
h("label", { class: "field" }, h("span", {}, t("pg.system")), sysIn),
h("label", { class: "field" }, h("span", {}, t("pg.prompt")), promptIn),
h("label", { class: "field" }, h("span", {}, t("pg.mode")), modeSeg),
modeExtra,
h("div", { class: "row" }, h("label", { class: "field" }, h("span", {}, t("pg.temp")), tempIn), h("label", { class: "field" }, h("span", {}, t("pg.maxTokens")), maxTokIn)),
h("div", { class: "flex", style: "justify-content:flex-end" }, sendBtn)
),
h("div", { class: "card" }, h("div", { class: "card-head" }, h("h3", {}, t("pg.answer"))), out)
),
decision
)
);
return root;
}
+67
View File
@@ -0,0 +1,67 @@
// Protocols — live toggles for inbound API surfaces.
import { h, mount, toast } from "../ui.js";
import { t } from "../i18n.js";
import { api } from "../api.js";
import { appState } from "../app.js";
const ORDER = [
"openai_chat",
"openai_completions",
"openai_embeddings",
"openai_models",
"anthropic_messages",
"mcp",
"native_passthrough",
];
export async function renderProtocols() {
const meta = appState.meta || (await api.meta());
const impl = meta.protocols_impl || {};
const state = await api.getProtocols();
const root = h("div");
async function save() {
try {
await api.putProtocols(state);
toast(t("toast.saved"), "good");
} catch (e) {
toast(String(e.message || e), "bad");
}
}
function row(key) {
const implemented = !!impl[key];
const input = h("input", {
type: "checkbox",
checked: state[key] ? true : null,
disabled: implemented ? null : true,
onChange: (e) => {
state[key] = e.target.checked;
save();
},
});
return h(
"div",
{ class: "togglerow" },
h(
"div",
{ class: "meta" },
h(
"div",
{ class: "name" },
t("protocols." + key),
h("span", { class: "badge " + (implemented ? "ok" : "") }, implemented ? t("protocols.implemented") : t("protocols.planned"))
),
h("div", { class: "desc" }, t("protocols." + key + ".d"))
),
h("label", { class: "switch" }, input, h("span", { class: "slider" }))
);
}
mount(
root,
h("div", { class: "page-head" }, h("h2", {}, t("protocols.title")), h("p", {}, t("protocols.subtitle"))),
h("div", { class: "card" }, ...ORDER.map(row))
);
return root;
}
+158
View File
@@ -0,0 +1,158 @@
// Routing — tier editor (ordered model chips), default model, and policy.
import { h, mount, toast } from "../ui.js";
import { t } from "../i18n.js";
import { api } from "../api.js";
import { appState } from "../app.js";
export async function renderRouting() {
const meta = appState.meta || (await api.meta());
const [routing, modelsData] = await Promise.all([api.getRouting(), api.listModels()]);
const ids = (modelsData.models || []).map((m) => m.id);
// local editable state — ensure standard tiers exist for a friendly editor
const tiers = Object.assign({ low: [], medium: [], high: [] }, routing.tiers || {});
const st = {
tiers,
default: routing.default || ids[0] || "",
policy: routing.policy || { mode: "fixed_table", min_class: {} },
};
const root = h("div");
function moveChip(tier, i, dir) {
const arr = st.tiers[tier];
const ni = i + dir;
if (ni < 0 || ni >= arr.length) return;
[arr[i], arr[ni]] = [arr[ni], arr[i]];
render();
}
function removeChip(tier, i) {
st.tiers[tier].splice(i, 1);
render();
}
function addChip(tier, id) {
if (id && !st.tiers[tier].includes(id)) st.tiers[tier].push(id);
render();
}
function tierRow(tier) {
const arr = st.tiers[tier];
const avail = ids.filter((id) => !arr.includes(id));
const addSel = h(
"select",
{
style: "width:auto",
onChange: (e) => {
addChip(tier, e.target.value);
},
},
h("option", { value: "" }, t("routing.addModel")),
...avail.map((id) => h("option", { value: id }, id))
);
return h(
"div",
{ class: "tierrow" },
h("div", { class: "tname" }, h("span", { class: "badge tier-" + tier }, tier)),
h(
"div",
{ class: "chips" },
...arr.map((id, i) =>
h(
"div",
{ class: "chip" },
h("span", { class: "ord" }, i + 1),
id,
h("span", { class: "arrow", title: "←", onClick: () => moveChip(tier, i, -1) }, "◄"),
h("span", { class: "arrow", title: "→", onClick: () => moveChip(tier, i, 1) }, "►"),
h("span", { class: "x", onClick: () => removeChip(tier, i) }, "×")
)
),
avail.length ? addSel : null
)
);
}
function policyCard() {
const modeSel = h(
"select",
{ onChange: (e) => { st.policy.mode = e.target.value; render(); } },
h("option", { value: "fixed_table", selected: st.policy.mode === "fixed_table" }, t("routing.policy.fixed")),
h("option", { value: "cost_aware", selected: st.policy.mode === "cost_aware" }, t("routing.policy.costAware"))
);
const children = [
h("label", { class: "field" }, h("span", {}, t("routing.policy.mode")), modeSel),
];
if (st.policy.mode === "cost_aware") {
const maxIn = h("input", {
type: "number",
step: "0.01",
value: st.policy.max_cost_usd_per_request ?? "",
placeholder: "0.50",
onChange: (e) => { st.policy.max_cost_usd_per_request = e.target.value === "" ? null : parseFloat(e.target.value); },
});
children.push(h("label", { class: "field" }, h("span", {}, t("routing.policy.maxCost")), maxIn));
st.policy.min_class = st.policy.min_class || {};
const rows = Object.keys(st.tiers).map((tier) => {
const sel = h(
"select",
{ onChange: (e) => { st.policy.min_class[tier] = e.target.value; } },
h("option", { value: "" }, "—"),
...(meta.cost_tiers || []).map((c) => h("option", { value: c, selected: st.policy.min_class[tier] === c }, c))
);
return h("label", { class: "field" }, h("span", {}, t("routing.policy.minClass") + " · " + tier), sel);
});
children.push(h("div", { class: "row" }, ...rows));
}
return h("div", { class: "card" }, h("div", { class: "card-head" }, h("h3", {}, t("routing.policy"))), ...children);
}
function render() {
const defSel = h(
"select",
{ onChange: (e) => { st.default = e.target.value; } },
...ids.map((id) => h("option", { value: id, selected: id === st.default }, id))
);
const saveBtn = h("button", { class: "btn primary" }, t("common.save"));
saveBtn.addEventListener("click", async () => {
saveBtn.disabled = true;
try {
await api.putRouting({ tiers: st.tiers, default: st.default, policy: st.policy });
toast(t("toast.saved"), "good");
} catch (e) {
toast(String(e.message || e), "bad");
}
saveBtn.disabled = false;
});
mount(
root,
h(
"div",
{ class: "page-head" },
h(
"div",
{ class: "flex" },
h("div", { class: "grow" }, h("h2", {}, t("routing.title")), h("p", {}, t("routing.subtitle"))),
saveBtn
)
),
ids.length === 0 ? h("div", { class: "callout" }, t("routing.empty")) : null,
h(
"div",
{ class: "card" },
h("div", { class: "card-head" }, h("h3", {}, t("routing.tiers")), h("span", { class: "card-head sub" }, t("routing.tierHint"))),
...Object.keys(st.tiers).map(tierRow)
),
h(
"div",
{ class: "card" },
h("div", { class: "card-head" }, h("h3", {}, t("routing.default"))),
h("label", { class: "field" }, h("span", {}, t("routing.defaultHint")), defSel)
),
policyCard()
);
}
render();
return root;
}
+191
View File
@@ -0,0 +1,191 @@
// Settings — router/route/breaker/log/telemetry/stats + export/import config.
import { h, mount, toast } from "../ui.js";
import { t } from "../i18n.js";
import { api } from "../api.js";
import { appState } from "../app.js";
function field(label, control, hint) {
return h("label", { class: "field" }, h("span", {}, label), control, hint ? h("div", { class: "hint" }, hint) : null);
}
function check(label, checked) {
const cb = h("input", { type: "checkbox", checked: checked ? true : null });
return { node: h("label", { class: "check" }, cb, label), cb };
}
export async function renderSettings() {
const meta = appState.meta || (await api.meta());
const s = await api.getSettings();
const root = h("div");
const listenIn = h("input", { value: s.listen || "" });
// router
const rUrl = h("input", { value: s.router?.url || "" });
const rKeyEnv = h("input", { value: s.router?.api_key_env || "", placeholder: "CORTIQ_ROUTER_KEY" });
const rTimeout = h("input", { type: "number", value: s.router?.timeout_ms ?? 800 });
const rTax = h("input", { value: s.router?.taxonomy_id || "" });
const rVerify = check(t("settings.router.verifyTls"), s.router?.verify_tls);
// route
const strategySel = h("select", {}, ...(meta.text_strategies || []).map((x) => h("option", { value: x, selected: x === s.route?.text_strategy }, x)));
const maxChars = h("input", { type: "number", value: s.route?.max_chars ?? 4000 });
const cacheTtl = h("input", { value: s.route?.cache_ttl || "60s" });
const profileSel = h("select", {}, ...(meta.profiles || []).map((x) => h("option", { value: x, selected: x === s.route?.profile }, x)));
// breaker
const brThresh = h("input", { type: "number", value: s.breaker?.threshold ?? 5 });
const brCool = h("input", { value: s.breaker?.cooldown || "30s" });
// log
const logSel = h("select", {}, ...["error", "warn", "info", "debug", "trace"].map((x) => h("option", { value: x, selected: x === s.log?.level }, x)));
const logBodies = check(t("settings.log.bodies"), s.log?.bodies);
// telemetry / cortiq
const metricsCb = check(t("settings.telemetry.metrics"), s.telemetry?.metrics);
const echoCb = check(t("settings.cortiq.echo"), s.cortiq?.echo);
// stats
const statsEnabled = check(t("settings.stats.enabled"), s.stats?.enabled);
const statsFile = h("input", { value: s.stats?.file || "" });
const statsRet = h("input", { value: s.stats?.retention || "7d" });
const saveBtn = h("button", { class: "btn primary" }, t("common.save"));
saveBtn.addEventListener("click", async () => {
saveBtn.disabled = true;
const patch = {
listen: listenIn.value.trim(),
router: {
url: rUrl.value.trim(),
api_key_env: rKeyEnv.value.trim() || null,
timeout_ms: parseInt(rTimeout.value) || 800,
verify_tls: rVerify.cb.checked,
taxonomy_id: rTax.value.trim() || null,
},
route: {
text_strategy: strategySel.value,
max_chars: parseInt(maxChars.value) || 4000,
cache_ttl: cacheTtl.value.trim() || "60s",
profile: profileSel.value,
},
breaker: { threshold: parseInt(brThresh.value) || 5, cooldown: brCool.value.trim() || "30s" },
log: { level: logSel.value, bodies: logBodies.cb.checked },
telemetry: { metrics: metricsCb.cb.checked, otlp_endpoint_env: s.telemetry?.otlp_endpoint_env || null },
cortiq: { echo: echoCb.cb.checked },
stats: {
enabled: statsEnabled.cb.checked,
file: statsFile.value.trim(),
retention: statsRet.value.trim() || "7d",
ring_size: s.stats?.ring_size ?? 500,
},
};
// serde: drop null optionals
if (!patch.router.api_key_env) delete patch.router.api_key_env;
if (!patch.router.taxonomy_id) delete patch.router.taxonomy_id;
if (!patch.telemetry.otlp_endpoint_env) delete patch.telemetry.otlp_endpoint_env;
try {
const r = await api.putSettings(patch);
toast(r.needs_restart ? t("settings.needsRestart") : t("toast.saved"), r.needs_restart ? "warn" : "good");
} catch (e) {
toast(String(e.message || e), "bad");
}
saveBtn.disabled = false;
});
async function exportConfig() {
const cfg = await api.getConfig();
const blob = new Blob([JSON.stringify(cfg, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = h("a", { href: url, download: "gateway-config.json" });
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
const fileIn = h("input", { type: "file", accept: ".json,application/json", style: "display:none" });
fileIn.addEventListener("change", async () => {
const f = fileIn.files[0];
if (!f) return;
try {
const cfg = JSON.parse(await f.text());
await api.putConfig(cfg);
toast(t("toast.imported"), "good");
} catch (e) {
toast(String(e.message || e), "bad");
}
fileIn.value = "";
});
mount(
root,
h(
"div",
{ class: "page-head" },
h(
"div",
{ class: "flex" },
h("div", { class: "grow" }, h("h2", {}, t("settings.title")), h("p", {}, t("settings.subtitle"))),
saveBtn
)
),
h(
"div",
{ class: "grid cols-2" },
h(
"div",
{ class: "card" },
h("div", { class: "card-head" }, h("h3", {}, t("settings.router"))),
field(t("settings.router.url"), rUrl),
field(t("settings.router.keyEnv"), rKeyEnv),
h("div", { class: "row" }, field(t("settings.router.timeout"), rTimeout), field(t("settings.router.taxonomy"), rTax)),
rVerify.node
),
h(
"div",
{ class: "card" },
h("div", { class: "card-head" }, h("h3", {}, t("settings.route"))),
field(t("settings.route.strategy"), strategySel),
h("div", { class: "row" }, field(t("settings.route.maxChars"), maxChars), field(t("settings.route.cacheTtl"), cacheTtl)),
field(t("settings.route.profile"), profileSel)
),
h(
"div",
{ class: "card" },
h("div", { class: "card-head" }, h("h3", {}, t("settings.breaker"))),
h("div", { class: "row" }, field(t("settings.breaker.threshold"), brThresh), field(t("settings.breaker.cooldown"), brCool))
),
h(
"div",
{ class: "card" },
h("div", { class: "card-head" }, h("h3", {}, t("settings.log"))),
field(t("settings.log.level"), logSel),
logBodies.node
),
h(
"div",
{ class: "card" },
h("div", { class: "card-head" }, h("h3", {}, t("settings.telemetry"))),
metricsCb.node,
h("div", { class: "divider" }),
h("div", { class: "card-head" }, h("h3", {}, t("settings.cortiq"))),
echoCb.node
),
h(
"div",
{ class: "card" },
h("div", { class: "card-head" }, h("h3", {}, t("settings.stats"))),
statsEnabled.node,
field(t("settings.stats.file"), statsFile),
field(t("settings.stats.retention"), statsRet)
)
),
h(
"div",
{ class: "card" },
h("div", { class: "card-head" }, h("h3", {}, t("settings.listen"))),
field(t("settings.listen"), listenIn),
h("div", { class: "divider" }),
h(
"div",
{ class: "flex wrap" },
h("button", { class: "btn", onClick: exportConfig }, t("settings.export")),
h("button", { class: "btn", onClick: () => fileIn.click() }, t("settings.import")),
fileIn
)
)
);
return root;
}