// 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); lastCreatedKey = key; reload(); } let lastCreatedKey = null; // "Connect your app" — copy-paste snippets with this gateway's real URL. // Keys are stored hashed-masked in the list, so the key itself is a // placeholder unless one was just created on this page. function snippetsCard(openMode) { const base = location.origin + "/v1"; const key = lastCreatedKey || (openMode ? "sk-anything" : "<" + t("keys.snip.keyPh") + ">"); const snips = { curl: `curl ${base}/chat/completions \\ -H "Authorization: Bearer ${key}" \\ -H "Content-Type: application/json" \\ -d '{"model":"cortiq-auto","messages":[{"role":"user","content":"Привет!"}]}'`, python: `from openai import OpenAI client = OpenAI(base_url="${base}", api_key="${key}") resp = client.chat.completions.create( model="cortiq-auto", # авто-роутинг; или точное имя модели messages=[{"role": "user", "content": "Привет!"}], ) print(resp.choices[0].message.content)`, javascript: `import OpenAI from "openai"; const client = new OpenAI({ baseURL: "${base}", apiKey: "${key}" }); const resp = await client.chat.completions.create({ model: "cortiq-auto", messages: [{ role: "user", content: "Привет!" }], }); console.log(resp.choices[0].message.content);`, }; let cur = "curl"; const pre = h("pre", { class: "conv-log snippet" }, snips[cur]); const seg = h( "div", { class: "seg" }, ...Object.keys(snips).map((k, i) => h("button", { class: i === 0 ? "active" : "", onClick: (e) => { cur = k; pre.textContent = snips[k]; seg.querySelectorAll("button").forEach((b) => b.classList.remove("active")); e.target.classList.add("active"); } }, k) ) ); const copy = h("button", { class: "btn ghost sm" }, "⧉ " + t("common.copy")); copy.addEventListener("click", async () => { await navigator.clipboard.writeText(pre.textContent); toast(t("common.copied"), "ok"); }); return h( "div", { class: "card" }, h("div", { class: "card-title" }, "🔌 " + t("keys.snip.title")), h("div", { class: "muted small", style: "margin-bottom:8px" }, lastCreatedKey ? t("keys.snip.withKey") : openMode ? t("keys.snip.openMode") : t("keys.snip.hint")), h("div", { class: "flex", style: "gap:8px; margin-bottom:8px" }, seg, h("div", { class: "grow" }), copy), pre ); } 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") ) ) ) ) ) ) ) ), snippetsCard(!!data.open_mode) ); } await reload(); return root; }