// API keys — list, create (with copy-once), revoke. import { h, mount, modal, toast, confirmDialog, moneyEl, num } from "../ui.js"; import { t } from "../i18n.js"; import { api } from "../api.js"; export async function renderKeys() { const root = h("div"); let usageRange = "7d"; function openCreate(reload) { const accountIn = h("input", { placeholder: "team-a" }); const rateIn = h("input", { type: "number", value: 600 }); const budgetIn = h("input", { type: "number", step: "1", min: "0", placeholder: "0" }); const customIn = h("input", { placeholder: "sk-gw-…" }); 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.budget")), budgetIn, h("div", { class: "hint" }, t("keys.form.budgetHint"))), 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, monthly_budget_usd: parseFloat(budgetIn.value) > 0 ? parseFloat(budgetIn.value) : null, allow_models: [], }); 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 origin = location.origin; const base = origin + "/v1"; let key = lastCreatedKey || (openMode ? "sk-anything" : "<" + t("keys.snip.keyPh") + ">"); // Every scripty snippet exists in two OS flavors: bash heredocs for // Linux/macOS and PowerShell here-strings for Windows. SDK snippets // (python/js) and GUI instructions are OS-independent. let os = /win/i.test(navigator.platform || "") ? "ps" : "sh"; // Harness auto-setup scripts first (the gateway speaks OpenAI at /v1/* and // Anthropic at /v1/messages, so Claude Code connects natively), SDKs after. const buildSnips = (osKind) => osKind === "ps" ? { "Claude Code": `# ${t("keys.h.claude")} $env:ANTHROPIC_BASE_URL = "${origin}" $env:ANTHROPIC_AUTH_TOKEN = "${key}" $env:ANTHROPIC_MODEL = "cortiq-auto" claude`, Codex: `# ${t("keys.h.codex")} New-Item -ItemType Directory -Force "$HOME\\.codex" | Out-Null Add-Content "$HOME\\.codex\\config.toml" @' model = "cortiq-auto" model_provider = "cortiq" [model_providers.cortiq] name = "Cortiq Gateway" base_url = "${base}" env_key = "CORTIQ_API_KEY" wire_api = "chat" '@ $env:CORTIQ_API_KEY = "${key}" codex`, OpenCode: `# ${t("keys.h.opencode")} New-Item -ItemType Directory -Force "$HOME\\.config\\opencode" | Out-Null Set-Content "$HOME\\.config\\opencode\\opencode.json" @' { "$schema": "https://opencode.ai/config.json", "provider": { "cortiq": { "npm": "@ai-sdk/openai-compatible", "name": "Cortiq Gateway", "options": { "baseURL": "${base}", "apiKey": "${key}" }, "models": { "cortiq-auto": { "name": "Cortiq Auto (smart routing)" } } } } } '@ opencode`, Continue: `# ${t("keys.h.continue")} New-Item -ItemType Directory -Force "$HOME\\.continue" | Out-Null Add-Content "$HOME\\.continue\\config.yaml" @' models: - name: Cortiq Auto provider: openai model: cortiq-auto apiBase: ${base} apiKey: ${key} '@`, Cline: `${t("keys.h.cline1")} API Provider: OpenAI Compatible Base URL: ${base} API Key: ${key} Model ID: cortiq-auto ${t("keys.h.cline2")}`, curl: `curl.exe ${base}/chat/completions \` -H "Authorization: Bearer ${key}" \` -H "Content-Type: application/json" \` -d '{"model":"cortiq-auto","messages":[{"role":"user","content":"Привет!"}]}'`, } : { "Claude Code": `# ${t("keys.h.claude")} export ANTHROPIC_BASE_URL="${origin}" export ANTHROPIC_AUTH_TOKEN="${key}" export ANTHROPIC_MODEL="cortiq-auto" claude`, Codex: `# ${t("keys.h.codex")} mkdir -p ~/.codex && cat >> ~/.codex/config.toml <<'EOF' model = "cortiq-auto" model_provider = "cortiq" [model_providers.cortiq] name = "Cortiq Gateway" base_url = "${base}" env_key = "CORTIQ_API_KEY" wire_api = "chat" EOF export CORTIQ_API_KEY="${key}" codex`, OpenCode: `# ${t("keys.h.opencode")} mkdir -p ~/.config/opencode && cat > ~/.config/opencode/opencode.json <<'EOF' { "$schema": "https://opencode.ai/config.json", "provider": { "cortiq": { "npm": "@ai-sdk/openai-compatible", "name": "Cortiq Gateway", "options": { "baseURL": "${base}", "apiKey": "${key}" }, "models": { "cortiq-auto": { "name": "Cortiq Auto (smart routing)" } } } } } EOF opencode`, Continue: `# ${t("keys.h.continue")} mkdir -p ~/.continue && cat >> ~/.continue/config.yaml <<'EOF' models: - name: Cortiq Auto provider: openai model: cortiq-auto apiBase: ${base} apiKey: ${key} EOF`, Cline: `${t("keys.h.cline1")} API Provider: OpenAI Compatible Base URL: ${base} API Key: ${key} Model ID: cortiq-auto ${t("keys.h.cline2")}`, 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 shSnips, psSnips, snips; function rebuildSnips() { shSnips = buildSnips("sh"); psSnips = buildSnips("ps"); // python/js/Cline одинаковы — дополняем ps-набор из sh for (const k of Object.keys(shSnips)) if (!psSnips[k]) psSnips[k] = shSnips[k]; snips = os === "ps" ? psSnips : shSnips; } rebuildSnips(); let cur = "Claude Code"; 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" : "") + (["curl", "python", "javascript"].includes(k) ? " sdk" : ""), "data-snip": k, onClick: (e) => { cur = k; pre.textContent = snips[k]; protoWarn.style.display = k === "Claude Code" && protoWarn.childNodes.length ? "" : "none"; paintStatus(); seg.querySelectorAll("button").forEach((b) => b.classList.remove("active")); e.currentTarget.classList.add("active"); } }, k) ) ); // Claude Code needs the anthropic_messages protocol — offer a one-click // enable when it is off, right where the user will hit the problem. const protoWarn = h("div", { style: "display:none; margin-top:8px" }); api.getProtocols().then((prot) => { if (prot.anthropic_messages) return; const fixBtn = h("button", { class: "btn sm primary" }, t("keys.h.enableProto")); fixBtn.addEventListener("click", async () => { try { await api.putProtocols({ ...prot, anthropic_messages: true }); protoWarn.style.display = "none"; toast(t("common.saved"), "ok"); } catch (e) { toast(String(e.message || e), "err"); } }); mount(protoWarn, h("div", { class: "callout warn" }, h("span", {}, "⚠ " + t("keys.h.protoOff")), " ", fixBtn)); if (cur === "Claude Code") protoWarn.style.display = ""; }).catch(() => {}); // Detected harnesses: ✓ on the tab when installed on the gateway machine, // status line under the snippet ("installed" / "already configured") — // and a one-line INSTALL command (per OS) when it's missing. const INSTALL = { "Claude Code": { sh: "curl -fsSL https://claude.ai/install.sh | bash", ps: "irm https://claude.ai/install.ps1 | iex" }, Codex: { sh: "npm install -g @openai/codex", ps: "npm install -g @openai/codex" }, OpenCode: { sh: "curl -fsSL https://opencode.ai/install | bash", ps: "npm install -g opencode-ai" }, Continue: { sh: "code --install-extension Continue.continue", ps: "code --install-extension Continue.continue" }, Cline: { sh: "code --install-extension saoudrizwan.claude-dev", ps: "code --install-extension saoudrizwan.claude-dev" }, }; let detected = {}; const statusHost = h("div", { class: "small", style: "margin-top:6px" }); function paintStatus() { const d = detected[cur]; if (!d) { statusHost.textContent = ""; return; } statusHost.innerHTML = ""; if (d.configured) mount(statusHost, h("span", { class: "ok" }, "✓ " + t("keys.h.st.configured"))); else if (d.installed) mount(statusHost, h("span", { class: "ok" }, "✓ " + t("keys.h.st.installed"))); else { const cmd = (INSTALL[cur] || {})[os]; const copyInstall = h("button", { class: "btn ghost sm", onClick: async () => { await navigator.clipboard.writeText(cmd); toast(t("common.copied"), "good"); } }, "⧉"); mount(statusHost, h("span", { class: "muted" }, "○ " + t("keys.h.st.notFound")), cmd ? h("div", { class: "flex", style: "gap:6px; align-items:center; margin-top:4px" }, h("span", { class: "muted" }, t("keys.h.install")), h("code", { class: "mono small", style: "background:var(--panel-2,#23252c); padding:2px 8px; border-radius:6px" }, cmd), copyInstall) : null); } } api.harnesses().then((d) => { detected = d || {}; seg.querySelectorAll("button").forEach((b) => { const st = detected[b.dataset.snip]; if (st && st.installed) b.append(" ✓"); }); paintStatus(); }).catch(() => {}); // OS switch: bash for Linux/macOS, PowerShell for Windows (auto-detected) const osSeg = h( "div", { class: "seg" }, h("button", { class: os === "sh" ? "active" : "" }, "🐧🍎 Linux / macOS"), h("button", { class: os === "ps" ? "active" : "" }, "🪟 Windows") ); osSeg.querySelectorAll("button").forEach((b, i) => b.addEventListener("click", () => { os = i === 0 ? "sh" : "ps"; rebuildSnips(); osSeg.querySelectorAll("button").forEach((x, j) => x.classList.toggle("active", j === (os === "sh" ? 0 : 1))); pre.textContent = snips[cur]; paintStatus(); })); // paste-your-key: substitutes the key into every snippet immediately const keyIn = h("input", { class: "mono", placeholder: t("keys.snip.keyInputPh"), style: "max-width:230px; font-size:12px", value: lastCreatedKey || "" }); const applyKey = () => { key = keyIn.value.trim() || (openMode ? "sk-anything" : "<" + t("keys.snip.keyPh") + ">"); rebuildSnips(); pre.textContent = snips[cur]; }; const applyBtn = h("button", { class: "btn ghost sm", onClick: applyKey }, "⤵ " + t("keys.snip.apply")); keyIn.addEventListener("keydown", (e) => { if (e.key === "Enter") applyKey(); }); 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; flex-wrap:wrap" }, seg, h("div", { class: "grow" }), keyIn, applyBtn, osSeg, copy), pre, statusHost, protoWarn, h("div", { class: "muted small", style: "margin-top:6px" }, t("keys.h.note")) ); } async function reload() { const data = await api.listKeys(); const keys = data.keys || []; // per-account usage: window breakdown + cumulative-forever totals — the // "who spends what" view for departments/developers sharing one gateway let usage = {}; let alltime = {}; try { const st = await api.stats(usageRange, "account"); for (const b of st.breakdown || []) usage[b.key] = b; alltime = st.alltime_by_account || {}; } catch { /* stats disabled — columns show dashes */ } 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("div", { class: "seg", style: "margin-right:10px" }, ...["24h", "7d", "30d"].map((r) => h("button", { class: r === usageRange ? "active" : "", onClick: () => { usageRange = r; reload(); } }, r))), 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("keys.col.requests") + " (" + usageRange + ")"), h("th", { class: "right-align" }, t("keys.col.tokens") + " (" + usageRange + ")"), h("th", { class: "right-align" }, "$ (" + usageRange + ")"), h("th", { class: "right-align" }, t("keys.col.costAll")), 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 mono" }, String((usage[k.account] || {}).requests ?? "—")), h("td", { class: "right-align mono small" }, usage[k.account] ? num(usage[k.account].prompt_tokens) + " / " + num(usage[k.account].completion_tokens) : "—"), h("td", { class: "right-align" }, usage[k.account] ? moneyEl(usage[k.account].cost_usd) : "—"), h("td", { class: "right-align" }, alltime[k.account] ? moneyEl(alltime[k.account].cost_usd) : "—", k.monthly_budget_usd ? h("div", { class: "small " + (((usage[k.account] || {}).cost_usd || 0) >= k.monthly_budget_usd ? "err" : "muted") }, t("keys.budgetOf", { b: "$" + k.monthly_budget_usd })) : null), 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; }