Startup no longer needs arguments: the config is created on first launch, a busy port is swapped for a free one, the console is open by default, and the browser lands on a setup wizard that downloads a ready-made .cmf model and wires it into routing (managed server + routing.default) when you press Finish. Download progress was reported from curl's own meter, which prints a full 0→100% bar for the -L redirect hop before the transfer starts — so every job showed 100% and then hung. Progress now comes from bytes on disk against the size from the HuggingFace tree (or a HEAD request). The UI and the gateway also disagreed about the output file name, so a finished download never marked its card as downloaded; the gateway now returns the exact stem and the UI sends it back verbatim for download, progress and delete. Featured models are read live from the infosave HF account in both the wizard and Import, and all 432 UI strings now exist in every one of the 7 languages.
187 lines
6.3 KiB
JavaScript
Vendored
187 lines
6.3 KiB
JavaScript
Vendored
// Thin client over the gateway's /admin/api surface. Adds the Bearer admin token,
|
|
// parses JSON, and throws Error(message) on non-2xx so views can try/catch.
|
|
// A 401 sets an "unauthorized" flag the app uses to show the login screen.
|
|
|
|
const BASE = "/admin/api";
|
|
const TOKEN_KEY = "allaigate_token";
|
|
|
|
let onUnauthorized = null;
|
|
export function setUnauthorizedHandler(fn) {
|
|
onUnauthorized = fn;
|
|
}
|
|
|
|
export function getToken() {
|
|
return localStorage.getItem(TOKEN_KEY) || "";
|
|
}
|
|
export function setToken(tok) {
|
|
if (tok) localStorage.setItem(TOKEN_KEY, tok);
|
|
else localStorage.removeItem(TOKEN_KEY);
|
|
}
|
|
|
|
async function j(method, path, body) {
|
|
const tok = getToken();
|
|
const headers = {};
|
|
if (tok) headers["Authorization"] = "Bearer " + tok;
|
|
const opt = { method, headers };
|
|
if (body !== undefined) {
|
|
opt.headers["content-type"] = "application/json";
|
|
opt.body = JSON.stringify(body);
|
|
}
|
|
const r = await fetch(BASE + path, opt);
|
|
if (r.status === 401) {
|
|
if (onUnauthorized) onUnauthorized();
|
|
throw new Error("unauthorized");
|
|
}
|
|
const text = await r.text();
|
|
let data = null;
|
|
try {
|
|
data = text ? JSON.parse(text) : null;
|
|
} catch {
|
|
data = text;
|
|
}
|
|
if (!r.ok) {
|
|
const msg =
|
|
(data && data.error && (data.error.message || data.error)) ||
|
|
(data && (data.detail || data.message)) ||
|
|
r.statusText;
|
|
throw new Error(typeof msg === "string" ? msg : JSON.stringify(msg));
|
|
}
|
|
return data;
|
|
}
|
|
|
|
export const api = {
|
|
// meta / health
|
|
meta: () => j("GET", "/meta"),
|
|
health: () => j("GET", "/health"),
|
|
probeRouter: () => j("POST", "/router/probe"),
|
|
|
|
// config
|
|
getConfig: () => j("GET", "/config"),
|
|
putConfig: (b) => j("PUT", "/config", b),
|
|
|
|
// models
|
|
listModels: () => j("GET", "/models"),
|
|
createModel: (b) => j("POST", "/models", b),
|
|
updateModel: (id, b) => j("PUT", "/models/" + encodeURIComponent(id), b),
|
|
deleteModel: (id) => j("DELETE", "/models/" + encodeURIComponent(id)),
|
|
probeModel: (id) => j("POST", "/models/" + encodeURIComponent(id) + "/probe"),
|
|
|
|
// routing
|
|
getRouting: () => j("GET", "/routing"),
|
|
putRouting: (b) => j("PUT", "/routing", b),
|
|
|
|
// protocols
|
|
getProtocols: () => j("GET", "/protocols"),
|
|
putProtocols: (b) => j("PUT", "/protocols", b),
|
|
|
|
// settings
|
|
getSettings: () => j("GET", "/settings"),
|
|
putSettings: (b) => j("PUT", "/settings", b),
|
|
|
|
// keys
|
|
listKeys: () => j("GET", "/keys"),
|
|
createKey: (b) => j("POST", "/keys", b),
|
|
deleteKey: (k) => j("DELETE", "/keys/" + encodeURIComponent(k)),
|
|
|
|
// secrets
|
|
listSecrets: () => j("GET", "/secrets"),
|
|
setSecret: (name, value) => j("PUT", "/secrets", { name, value }),
|
|
clearSecret: (name) => j("DELETE", "/secrets?name=" + encodeURIComponent(name)),
|
|
|
|
// stats
|
|
stats: (range, groupby) => j("GET", `/stats?range=${range}&groupby=${groupby}`),
|
|
clearStats: () => j("DELETE", "/stats"),
|
|
requests: (limit = 50, offset = 0) => j("GET", `/requests?limit=${limit}&offset=${offset}`),
|
|
|
|
// playground
|
|
test: (b) => j("POST", "/test", b),
|
|
|
|
// shadow / self-warming loop
|
|
shadow: () => j("GET", "/shadow"),
|
|
|
|
// CMF runtime (the `cortiq` CLI that runs the .cmf format)
|
|
cmfStatus: () => j("GET", "/cmf"),
|
|
cmfInstall: () => j("POST", "/cmf/install"),
|
|
cmfPortCheck: (port) => j("GET", "/cmf/port?port=" + encodeURIComponent(port)),
|
|
cmfFiles: () => j("GET", "/cmf/files"),
|
|
providerModels: (b) => j("POST", "/provider/models", b),
|
|
|
|
// 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"),
|
|
|
|
// first-run onboarding
|
|
setupStatus: () => j("GET", "/setup/status"),
|
|
// Connects every finished download as a local model unless register === false.
|
|
setupComplete: (lang, register) => j("POST", "/setup/complete", { lang, register }),
|
|
setupFeatured: () => j("GET", "/setup/featured"),
|
|
// `name` is the exact output stem from setup/featured — it wins over the
|
|
// repo/variant guess so the delete always hits the file the download wrote.
|
|
deleteSetupFile: (repo, variant, name) =>
|
|
j(
|
|
"DELETE",
|
|
`/setup/file?repo=${encodeURIComponent(repo)}` +
|
|
(variant ? `&variant=${encodeURIComponent(variant)}` : "") +
|
|
(name ? `&name=${encodeURIComponent(name)}` : "")
|
|
),
|
|
};
|
|
|
|
// Streaming playground: POST /test/stream, calls onDelta(text) per content chunk,
|
|
// returns the routing decision read from X-Cortiq-* response headers.
|
|
export async function testStream(body, onDelta) {
|
|
const tok = getToken();
|
|
const headers = { "content-type": "application/json" };
|
|
if (tok) headers["Authorization"] = "Bearer " + tok;
|
|
const r = await fetch(BASE + "/test/stream", {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (r.status === 401) {
|
|
if (onUnauthorized) onUnauthorized();
|
|
throw new Error("unauthorized");
|
|
}
|
|
if (!r.ok || !r.body) {
|
|
throw new Error((await r.text()) || r.statusText);
|
|
}
|
|
const reader = r.body.getReader();
|
|
const dec = new TextDecoder();
|
|
let buf = "";
|
|
for (;;) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
buf += dec.decode(value, { stream: true });
|
|
let idx;
|
|
while ((idx = buf.indexOf("\n\n")) >= 0) {
|
|
const line = buf.slice(0, idx).split("\n").find((l) => l.startsWith("data:"));
|
|
buf = buf.slice(idx + 2);
|
|
if (!line) continue;
|
|
const data = line.slice(5).trim();
|
|
if (!data || data === "[DONE]") continue;
|
|
try {
|
|
const d = JSON.parse(data).choices?.[0]?.delta?.content;
|
|
if (d) onDelta(d);
|
|
} catch {
|
|
/* ignore malformed chunk */
|
|
}
|
|
}
|
|
}
|
|
const h = r.headers;
|
|
return {
|
|
task_label: h.get("x-cortiq-task-label") || "",
|
|
tier: h.get("x-cortiq-complexity-tier") || "",
|
|
score: parseFloat(h.get("x-cortiq-complexity-score") || "0"),
|
|
selected_model: h.get("x-cortiq-selected-model") || "",
|
|
route_source: h.get("x-cortiq-route-source") || "",
|
|
cost_usd: parseFloat(h.get("x-cortiq-cost-usd") || "0"),
|
|
};
|
|
}
|