feat!: API keys actually gate the gateway — auth, attribution, rate limits, budgets
The office-readiness audit found the foundation missing: configured API keys were decorative. Any bearer — or none — passed every model endpoint, per-key rate limits existed only in the form, and all real traffic was recorded as "anonymous", leaving the per-key spend table permanently blank. A middleware on the model surface (/v1/*, /route, /mcp) now enforces keys: - no keys configured → open mode as before (the console already warns); - keys exist → Bearer or x-api-key must match (401 invalid_api_key); - per-key rate_per_min → fixed one-minute window, 429 with Retry-After; - NEW per-key monthly_budget_usd → 429 insufficient_quota once the account's month-to-date spend crosses the ceiling; - the matched account rides the request into stats — the keys table's spend columns finally see real traffic. Month-to-date spend is tracked per account in stats and persisted in the totals sidecar with a watermark (no double counting across restarts). The key dialog gains a budget field; the table shows "budget $N/mo" in red once exhausted. All verified live: 401 wrong/missing key, 200 + attribution with the right key, 429 + Retry-After over the limit, 429 insufficient_quota on the second request past a $0.000001 budget. Also: the dashboard shows the OpenRouter credit balance (GET /api/v1/credits via the stored key, cached 5 min, hidden without a key, red under $5 — live: $16.18), the server drains connections on Ctrl-C instead of dropping them, request bodies are capped at 32 MB, and docs/DEPLOY(.ru) cover office deployment: systemd/launchd/Windows service, nginx/caddy TLS, security checklist, backups. 10 strings ×7 languages.
This commit is contained in:
Vendored
+1
@@ -74,6 +74,7 @@ export const api = {
|
||||
// protocols
|
||||
getProtocols: () => j("GET", "/protocols"),
|
||||
harnesses: () => j("GET", "/harnesses"),
|
||||
balance: () => j("GET", "/balance"),
|
||||
putProtocols: (b) => j("PUT", "/protocols", b),
|
||||
|
||||
// settings
|
||||
|
||||
Vendored
+4
@@ -639,4 +639,8 @@ export default {
|
||||
"settings.cache.entries": "Einträge",
|
||||
"settings.cache.hits": "Treffer",
|
||||
"settings.cache.saved": "gespart",
|
||||
"keys.form.budget": "Monatsbudget, $ (0 = unbegrenzt)",
|
||||
"keys.form.budgetHint": "Überschreiten die Monatsausgaben des Kontos die Grenze, werden Anfragen mit 429 insufficient_quota abgelehnt.",
|
||||
"keys.budgetOf": "Budget {b}/Mon.",
|
||||
"dash.kpi.orBalance": "OpenRouter-Guthaben",
|
||||
};
|
||||
|
||||
Vendored
+4
@@ -632,4 +632,8 @@ export default {
|
||||
"settings.cache.entries": "entries",
|
||||
"settings.cache.hits": "hits",
|
||||
"settings.cache.saved": "saved",
|
||||
"keys.form.budget": "Monthly budget, $ (0 = unlimited)",
|
||||
"keys.form.budgetHint": "Requests are rejected with 429 insufficient_quota once the account's month-to-date spend crosses this.",
|
||||
"keys.budgetOf": "budget {b}/mo",
|
||||
"dash.kpi.orBalance": "OpenRouter balance",
|
||||
};
|
||||
|
||||
Vendored
+4
@@ -639,4 +639,8 @@ export default {
|
||||
"settings.cache.entries": "entradas",
|
||||
"settings.cache.hits": "aciertos",
|
||||
"settings.cache.saved": "ahorrado",
|
||||
"keys.form.budget": "Presupuesto mensual, $ (0 = sin límite)",
|
||||
"keys.form.budgetHint": "Cuando el gasto del mes supera el techo, las peticiones se rechazan con 429 insufficient_quota.",
|
||||
"keys.budgetOf": "presupuesto {b}/mes",
|
||||
"dash.kpi.orBalance": "Saldo OpenRouter",
|
||||
};
|
||||
|
||||
Vendored
+4
@@ -639,4 +639,8 @@ export default {
|
||||
"settings.cache.entries": "entrées",
|
||||
"settings.cache.hits": "hits",
|
||||
"settings.cache.saved": "économisé",
|
||||
"keys.form.budget": "Budget mensuel, $ (0 = illimité)",
|
||||
"keys.form.budgetHint": "Quand les dépenses du mois dépassent le plafond, les requêtes sont rejetées avec 429 insufficient_quota.",
|
||||
"keys.budgetOf": "budget {b}/mois",
|
||||
"dash.kpi.orBalance": "Solde OpenRouter",
|
||||
};
|
||||
|
||||
Vendored
+4
@@ -631,4 +631,8 @@ export default {
|
||||
"settings.cache.entries": "записей",
|
||||
"settings.cache.hits": "попаданий",
|
||||
"settings.cache.saved": "сэкономлено",
|
||||
"keys.form.budget": "Бюджет в месяц, $ (0 = без лимита)",
|
||||
"keys.form.budgetHint": "Когда траты аккаунта за месяц превысят потолок, запросы отклоняются с 429 insufficient_quota.",
|
||||
"keys.budgetOf": "бюджет {b}/мес",
|
||||
"dash.kpi.orBalance": "Баланс OpenRouter",
|
||||
};
|
||||
|
||||
Vendored
+4
@@ -639,4 +639,8 @@ export default {
|
||||
"settings.cache.entries": "kayıt",
|
||||
"settings.cache.hits": "isabet",
|
||||
"settings.cache.saved": "tasarruf",
|
||||
"keys.form.budget": "Aylık bütçe, $ (0 = sınırsız)",
|
||||
"keys.form.budgetHint": "Hesabın aylık harcaması tavanı aşınca istekler 429 insufficient_quota ile reddedilir.",
|
||||
"keys.budgetOf": "bütçe {b}/ay",
|
||||
"dash.kpi.orBalance": "OpenRouter bakiyesi",
|
||||
};
|
||||
|
||||
Vendored
+4
@@ -639,4 +639,8 @@ export default {
|
||||
"settings.cache.entries": "条目",
|
||||
"settings.cache.hits": "命中",
|
||||
"settings.cache.saved": "已节省",
|
||||
"keys.form.budget": "每月预算,$(0 = 不限)",
|
||||
"keys.form.budgetHint": "当账户当月支出超过上限时,请求将被拒绝并返回 429 insufficient_quota。",
|
||||
"keys.budgetOf": "预算 {b}/月",
|
||||
"dash.kpi.orBalance": "OpenRouter 余额",
|
||||
};
|
||||
|
||||
Vendored
+8
-2
@@ -110,12 +110,13 @@ export async function renderDashboard() {
|
||||
|
||||
async function load() {
|
||||
const rangeSecs = range;
|
||||
let stats, recent, health;
|
||||
let stats, recent, health, balance;
|
||||
try {
|
||||
[stats, recent, health] = await Promise.all([
|
||||
[stats, recent, health, balance] = await Promise.all([
|
||||
api.stats(rangeSecs, groupby),
|
||||
api.requests(40, 0),
|
||||
api.health().catch(() => null),
|
||||
api.balance().catch(() => null),
|
||||
]);
|
||||
} catch (e) {
|
||||
mount(root, h("div", { class: "callout" }, String(e.message || e)));
|
||||
@@ -162,6 +163,11 @@ export async function renderDashboard() {
|
||||
kpi(num(tot.requests), t("dash.kpi.requests")),
|
||||
kpi(num(tot.total_tokens), t("dash.kpi.tokens")),
|
||||
kpi(moneyEl(tot.cost_usd), t("dash.kpi.cost")),
|
||||
balance && balance.openrouter_usd != null
|
||||
? kpi(
|
||||
h("span", { class: balance.openrouter_usd < 5 ? "err" : "" }, "$" + balance.openrouter_usd.toFixed(2)),
|
||||
t("dash.kpi.orBalance"))
|
||||
: null,
|
||||
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"))
|
||||
|
||||
Vendored
+9
-1
@@ -10,12 +10,14 @@ export async function renderKeys() {
|
||||
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 () => {
|
||||
@@ -24,6 +26,7 @@ export async function renderKeys() {
|
||||
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();
|
||||
@@ -273,7 +276,12 @@ console.log(resp.choices[0].message.content);`,
|
||||
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) : "—"),
|
||||
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" },
|
||||
|
||||
Reference in New Issue
Block a user