aigetwey 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -4
- package/README.md +32 -11
- package/config.example.yaml +6 -6
- package/dashboard/next.config.ts +6 -0
- package/dashboard/src/app/(console)/quota/page.tsx +2 -2
- package/dashboard/src/app/globals.css +47 -0
- package/dashboard/src/components/BudgetForm.tsx +256 -0
- package/dashboard/src/components/BudgetTracker.tsx +181 -0
- package/dashboard/src/components/CooldownTimer.tsx +1 -1
- package/dashboard/src/components/EndpointView.tsx +285 -47
- package/dashboard/src/components/LogTable.tsx +97 -25
- package/dashboard/src/components/ModelPicker.tsx +15 -7
- package/dashboard/src/components/ProviderDetail.tsx +27 -29
- package/dashboard/src/components/ProviderManager.tsx +39 -31
- package/dashboard/src/components/Rail.tsx +1 -1
- package/dashboard/src/components/RoutingView.tsx +8 -4
- package/dashboard/src/components/ToolDetail.tsx +5 -3
- package/dashboard/src/components/TopBar.tsx +1 -1
- package/dashboard/src/components/UsageView.tsx +25 -6
- package/dashboard/src/components/ui.tsx +6 -1
- package/dashboard/src/lib/cliTools.ts +0 -43
- package/dashboard/src/lib/client.ts +14 -7
- package/dashboard/src/lib/gateway.ts +33 -15
- package/dashboard/src/{middleware.ts → proxy.ts} +8 -6
- package/dist/cli.js +43 -8
- package/dist/cli.js.map +1 -1
- package/dist/config.js +136 -27
- package/dist/config.js.map +1 -1
- package/dist/core/budget.js +62 -17
- package/dist/core/budget.js.map +1 -1
- package/dist/core/fallback.js +0 -6
- package/dist/core/fallback.js.map +1 -1
- package/dist/core/handler.js +24 -9
- package/dist/core/handler.js.map +1 -1
- package/dist/core/keysUsage.js +15 -0
- package/dist/core/keysUsage.js.map +1 -0
- package/dist/core/ratelimit.js +15 -0
- package/dist/core/ratelimit.js.map +1 -0
- package/dist/core/state.js +15 -15
- package/dist/core/state.js.map +1 -1
- package/dist/core/window.js +35 -0
- package/dist/core/window.js.map +1 -0
- package/dist/db.js +39 -25
- package/dist/db.js.map +1 -1
- package/dist/middleware/auth.js +15 -8
- package/dist/middleware/auth.js.map +1 -1
- package/dist/routes/admin.js +80 -17
- package/dist/routes/admin.js.map +1 -1
- package/dist/routes/v1.js +28 -11
- package/dist/routes/v1.js.map +1 -1
- package/dist/server.js +5 -7
- package/dist/server.js.map +1 -1
- package/dist/stream/openai-stream.js +3 -0
- package/dist/stream/openai-stream.js.map +1 -1
- package/dist/upstream/client.js +9 -0
- package/dist/upstream/client.js.map +1 -1
- package/package.json +3 -4
- package/src/cli.ts +44 -8
- package/src/config.ts +142 -29
- package/src/core/budget.ts +78 -25
- package/src/core/fallback.ts +0 -9
- package/src/core/handler.ts +31 -12
- package/src/core/keysUsage.ts +49 -0
- package/src/core/ratelimit.ts +25 -0
- package/src/core/state.ts +21 -16
- package/src/core/window.ts +45 -0
- package/src/db.ts +50 -28
- package/src/middleware/auth.ts +18 -8
- package/src/routes/admin.ts +93 -20
- package/src/routes/v1.ts +32 -11
- package/src/server.ts +5 -8
- package/src/stream/openai-stream.ts +3 -1
- package/src/upstream/client.ts +9 -0
- package/dashboard/src/components/BudgetEditor.tsx +0 -97
- package/dashboard/src/components/QuotaView.tsx +0 -152
- package/src/core/quota.ts +0 -253
package/dist/server.js
CHANGED
|
@@ -4,7 +4,6 @@ import { loadConfig } from "./config.js";
|
|
|
4
4
|
import { registerRoutes } from "./routes/index.js";
|
|
5
5
|
import { GatewayState } from "./core/state.js";
|
|
6
6
|
import { UsageDB } from "./db.js";
|
|
7
|
-
import { QuotaTracker } from "./core/quota.js";
|
|
8
7
|
import { AuthStore } from "./core/authStore.js";
|
|
9
8
|
import { consoleBuffer } from "./core/console-buffer.js";
|
|
10
9
|
async function main() {
|
|
@@ -48,13 +47,8 @@ async function main() {
|
|
|
48
47
|
// unified data dir (default ./data); usage tracking lives here.
|
|
49
48
|
const dataDir = resolve(process.env.AIGETWEY_DATA_DIR ?? "data");
|
|
50
49
|
const db = new UsageDB(join(dataDir, "usage.sqlite"));
|
|
51
|
-
// quota counts persist via the DB so a restart within a window keeps the budget.
|
|
52
|
-
const quota = new QuotaTracker(Date.now, {
|
|
53
|
-
load: () => db.loadQuota(),
|
|
54
|
-
save: (id, start, consumed) => db.saveQuota(id, start, consumed),
|
|
55
|
-
});
|
|
56
50
|
// holder enables runtime config edits (hot-reload) from the dashboard.
|
|
57
|
-
const state = new GatewayState(configPath, config,
|
|
51
|
+
const state = new GatewayState(configPath, config, db);
|
|
58
52
|
// admin password lives in a hash store (seeded from the env on first run,
|
|
59
53
|
// changeable at runtime from the dashboard).
|
|
60
54
|
const auth = AuthStore.open(dataDir, process.env.AIGETWEY_ADMIN_PASSWORD);
|
|
@@ -71,6 +65,10 @@ async function main() {
|
|
|
71
65
|
prefix: "/",
|
|
72
66
|
// forward the whole HTTP surface the dashboard needs (pages + its API).
|
|
73
67
|
httpMethods: ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
|
|
68
|
+
// forward WebSocket upgrades too, so `next dev`'s HMR socket works when the
|
|
69
|
+
// dashboard is proxied — this is what lets dev run single-URL on the gateway
|
|
70
|
+
// port like production. Harmless for the prebuilt prod dashboard (no socket).
|
|
71
|
+
websocket: true,
|
|
74
72
|
// keep the ORIGINAL Host so Next builds redirects (e.g. → /login) against
|
|
75
73
|
// the gateway's address, not the internal dashboard port.
|
|
76
74
|
replyOptions: {
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -16,7 +16,10 @@ export async function* streamToCanonical(events) {
|
|
|
16
16
|
/** Lift vendor reasoning fields into the canonical `delta.reasoning`. */
|
|
17
17
|
function normalize(chunk) {
|
|
18
18
|
for (const choice of chunk.choices ?? []) {
|
|
19
|
+
// a finish_reason chunk carries no `delta`; skip it (and any delta-less choice).
|
|
19
20
|
const d = choice.delta;
|
|
21
|
+
if (!d)
|
|
22
|
+
continue;
|
|
20
23
|
if (d.reasoning === undefined) {
|
|
21
24
|
const vendor = d["reasoning_content"] ?? d["reasoning"];
|
|
22
25
|
if (vendor)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openai-stream.js","sourceRoot":"","sources":["../../src/stream/openai-stream.ts"],"names":[],"mappings":"AASA,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,iBAAiB,CAAC,MAA+B;IACtE,IAAI,KAAK,EAAE,MAAM,EAAE,IAAI,MAAM,EAAE,CAAC;QAC9B,MAAM,IAAI,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,IAAI,IAAI,KAAK,QAAQ;YAAE,SAAS;QACzC,IAAI,MAAe,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5B,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QACD,MAAM,SAAS,CAAC,MAAwB,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC;AAED,yEAAyE;AACzE,SAAS,SAAS,CAAC,KAAqB;IACtC,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;QACzC,MAAM,CAAC,GAAG,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"openai-stream.js","sourceRoot":"","sources":["../../src/stream/openai-stream.ts"],"names":[],"mappings":"AASA,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,iBAAiB,CAAC,MAA+B;IACtE,IAAI,KAAK,EAAE,MAAM,EAAE,IAAI,MAAM,EAAE,CAAC;QAC9B,MAAM,IAAI,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,IAAI,IAAI,KAAK,QAAQ;YAAE,SAAS;QACzC,IAAI,MAAe,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5B,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QACD,MAAM,SAAS,CAAC,MAAwB,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC;AAED,yEAAyE;AACzE,SAAS,SAAS,CAAC,KAAqB;IACtC,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;QACzC,iFAAiF;QACjF,MAAM,CAAC,GAAG,MAAM,CAAC,KAAuE,CAAC;QACzF,IAAI,CAAC,CAAC;YAAE,SAAS;QACjB,IAAI,CAAC,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;YAC9B,MAAM,MAAM,GAAI,CAAC,CAAC,mBAAmB,CAAwB,IAAK,CAAC,CAAC,WAAW,CAAwB,CAAC;YACxG,IAAI,MAAM;gBAAE,CAAC,CAAC,SAAS,GAAG,MAAM,CAAC;QACnC,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,mBAAmB,CAAC,MAAqC;IAC9E,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QACjC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;IACxC,CAAC;IACD,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAC3B,CAAC"}
|
package/dist/upstream/client.js
CHANGED
|
@@ -57,6 +57,15 @@ function buildBody(provider, req, model, stream, thinkingIntent) {
|
|
|
57
57
|
const adapter = adapterFor(provider.format);
|
|
58
58
|
const upstreamReq = { ...req, model, stream };
|
|
59
59
|
const out = adapter.requestFromCanonical(upstreamReq);
|
|
60
|
+
// OpenAI-compatible streams omit usage entirely unless you opt in — without this
|
|
61
|
+
// every streamed call through an openai-format provider logs 0 tokens in/out
|
|
62
|
+
// (anthropic/gemini report usage inline, so they're unaffected). Ask for the
|
|
63
|
+
// final usage chunk; the handler taps it for accounting. Preserve a usage opt-in
|
|
64
|
+
// the client already set.
|
|
65
|
+
if (stream && provider.format === "openai") {
|
|
66
|
+
const existing = (out.stream_options ?? {});
|
|
67
|
+
out.stream_options = { ...existing, include_usage: true };
|
|
68
|
+
}
|
|
60
69
|
// Normalize thinking into THIS provider's native format, keyed by the upstream
|
|
61
70
|
// model's capabilities. No-op for non-reasoning models. Runs per-attempt so each
|
|
62
71
|
// provider in a fallback chain gets the right shape.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/upstream/client.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/upstream/client.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aigetwey",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Personal AI gateway — route, translate and track requests across Anthropic
|
|
5
|
+
"description": "Personal AI gateway — route, translate and track requests across Anthropic and OpenAI-compatible providers, with a built-in dashboard.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"ai",
|
|
8
8
|
"gateway",
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"proxy",
|
|
11
11
|
"anthropic",
|
|
12
12
|
"openai",
|
|
13
|
-
"gemini",
|
|
14
13
|
"router",
|
|
15
14
|
"claude",
|
|
16
15
|
"dashboard"
|
|
@@ -29,7 +28,7 @@
|
|
|
29
28
|
"node": ">=22"
|
|
30
29
|
},
|
|
31
30
|
"bin": {
|
|
32
|
-
"aigetwey": "
|
|
31
|
+
"aigetwey": "dist/cli.js"
|
|
33
32
|
},
|
|
34
33
|
"files": [
|
|
35
34
|
"dist",
|
package/src/cli.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { spawn, execSync, type ChildProcess } from "node:child_process";
|
|
16
16
|
import { randomBytes } from "node:crypto";
|
|
17
|
-
import { existsSync, copyFileSync } from "node:fs";
|
|
17
|
+
import { existsSync, copyFileSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
18
18
|
import { resolve, dirname, join } from "node:path";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
import { createInterface } from "node:readline";
|
|
@@ -65,24 +65,57 @@ const HELP = `
|
|
|
65
65
|
const GATEWAY_PORT = opts.port ?? Number(process.env.AIGETWEY_PORT ?? 18080);
|
|
66
66
|
const DASHBOARD_PORT = Number(process.env.DASHBOARD_PORT ?? 3000);
|
|
67
67
|
|
|
68
|
-
// reuse env secrets if present, otherwise generate (admin) /
|
|
68
|
+
// reuse env secrets if present, otherwise generate (admin) / persist (session).
|
|
69
69
|
const adminPassword = process.env.AIGETWEY_ADMIN_PASSWORD ?? randomBytes(6).toString("hex");
|
|
70
|
-
const sessionSecret = process.env.SESSION_SECRET ?? randomBytes(24).toString("hex");
|
|
71
70
|
const generatedPw = !process.env.AIGETWEY_ADMIN_PASSWORD;
|
|
72
71
|
|
|
72
|
+
/**
|
|
73
|
+
* The dashboard session cookie is signed+encrypted with SESSION_SECRET. A fresh
|
|
74
|
+
* random secret each boot would invalidate every cookie on restart — the symptom
|
|
75
|
+
* being "re-enter the password after a relaunch" — so persist a generated one to
|
|
76
|
+
* the data dir (alongside auth.json) and reuse it. An explicit env var wins.
|
|
77
|
+
*/
|
|
78
|
+
function loadOrCreateSessionSecret(): string {
|
|
79
|
+
if (process.env.SESSION_SECRET) return process.env.SESSION_SECRET;
|
|
80
|
+
const dataDir = resolve(process.env.AIGETWEY_DATA_DIR ?? join(root, "data"));
|
|
81
|
+
const file = join(dataDir, "session-secret");
|
|
82
|
+
try {
|
|
83
|
+
const existing = readFileSync(file, "utf8").trim();
|
|
84
|
+
if (existing) return existing;
|
|
85
|
+
} catch {
|
|
86
|
+
// not created yet — fall through and generate.
|
|
87
|
+
}
|
|
88
|
+
const secret = randomBytes(24).toString("hex");
|
|
89
|
+
try {
|
|
90
|
+
mkdirSync(dataDir, { recursive: true });
|
|
91
|
+
writeFileSync(file, secret, { mode: 0o600 });
|
|
92
|
+
} catch {
|
|
93
|
+
// unwritable data dir — fall back to an ephemeral secret (cookies won't
|
|
94
|
+
// survive this boot, but the gateway still runs).
|
|
95
|
+
}
|
|
96
|
+
return secret;
|
|
97
|
+
}
|
|
98
|
+
const sessionSecret = loadOrCreateSessionSecret();
|
|
99
|
+
|
|
73
100
|
function openBrowser(url: string): void {
|
|
74
101
|
const cmd =
|
|
75
102
|
process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
76
103
|
spawn(cmd, [url], { stdio: "ignore", detached: true, shell: process.platform === "win32" }).unref();
|
|
77
104
|
}
|
|
78
105
|
|
|
79
|
-
async function waitForGateway(
|
|
106
|
+
async function waitForGateway(
|
|
107
|
+
url: string,
|
|
108
|
+
timeoutMs = 20000,
|
|
109
|
+
ready: (status: number) => boolean = (s) => s > 0,
|
|
110
|
+
): Promise<boolean> {
|
|
80
111
|
const deadline = Date.now() + timeoutMs;
|
|
81
112
|
while (Date.now() < deadline) {
|
|
82
113
|
try {
|
|
83
114
|
const res = await fetch(url, { method: "GET" });
|
|
84
|
-
// any HTTP answer (even 401/503) means the port is up
|
|
85
|
-
|
|
115
|
+
// default: any HTTP answer (even 401/503) means the port is up. A caller
|
|
116
|
+
// can demand more — e.g. a non-5xx, to wait past a proxy's boot-time 502/500
|
|
117
|
+
// while the upstream it fronts is still coming up.
|
|
118
|
+
if (ready(res.status)) return true;
|
|
86
119
|
} catch {
|
|
87
120
|
// not up yet
|
|
88
121
|
}
|
|
@@ -349,9 +382,12 @@ async function main(): Promise<void> {
|
|
|
349
382
|
});
|
|
350
383
|
|
|
351
384
|
// one URL for everything — the gateway reverse-proxies the dashboard. Wait for
|
|
352
|
-
// the dashboard to
|
|
385
|
+
// the dashboard to be READY before opening the browser. Probe it directly on
|
|
386
|
+
// its own port (not through the proxy) and require a non-5xx answer: a proxy
|
|
387
|
+
// hit during boot returns 500 (ECONNREFUSED upstream), which a bare "port up"
|
|
388
|
+
// check would mistake for ready and open the browser into a wall of 500s.
|
|
353
389
|
const appUrl = `http://127.0.0.1:${GATEWAY_PORT}`;
|
|
354
|
-
await waitForGateway(
|
|
390
|
+
await waitForGateway(`http://127.0.0.1:${DASHBOARD_PORT}/login`, 30000, (s) => s > 0 && s < 500);
|
|
355
391
|
console.log(`\n aigetwey ${appUrl} (dashboard + API, one URL)`);
|
|
356
392
|
if (generatedPw) {
|
|
357
393
|
console.log(`\n admin password (generated): ${adminPassword}`);
|
package/src/config.ts
CHANGED
|
@@ -9,27 +9,18 @@ import {
|
|
|
9
9
|
import { dirname } from "node:path";
|
|
10
10
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
11
11
|
import { z } from "zod";
|
|
12
|
+
import { clientKeyFingerprint } from "./middleware/auth.js";
|
|
13
|
+
|
|
14
|
+
export { clientKeyFingerprint } from "./middleware/auth.js";
|
|
12
15
|
|
|
13
16
|
// ---- schema (PLAN §8) -------------------------------------------------------
|
|
14
17
|
//
|
|
15
18
|
// Shape differs from a flat OpenAI gateway: routing lives in a top-level
|
|
16
19
|
// `models[]` layer (alias -> provider chain), the endpoint block carries the
|
|
17
20
|
// token-saver toggles, and providers may be free passthroughs or service-account
|
|
18
|
-
// backed. The handler/keypool
|
|
21
|
+
// backed. The handler/keypool phases read these fields; defining the full
|
|
19
22
|
// shape up front avoids reshaping config across later phases.
|
|
20
23
|
|
|
21
|
-
/** Token quota window for a provider — drives the dashboard reset countdown. */
|
|
22
|
-
const QuotaSchema = z.object({
|
|
23
|
-
window: z.enum(["5h", "daily", "weekly", "monthly"]),
|
|
24
|
-
// daily: "HH:MM" local reset; weekly: weekday name ("monday"); others: ignored.
|
|
25
|
-
reset_at: z.string().optional(),
|
|
26
|
-
timezone: z.string().default("UTC"),
|
|
27
|
-
// optional ceiling for a progress bar; quota tracking works without it.
|
|
28
|
-
limit_tokens: z.number().int().positive().optional(),
|
|
29
|
-
// soft-alert threshold (0..1); UI flags the quota when pct >= this. Default 0.8.
|
|
30
|
-
alert_at: z.number().gt(0).lte(1).optional(),
|
|
31
|
-
});
|
|
32
|
-
|
|
33
24
|
const ProviderModelSchema = z.object({
|
|
34
25
|
id: z.string().min(1),
|
|
35
26
|
price_in: z.number().nonnegative().optional(),
|
|
@@ -56,7 +47,6 @@ const ProviderSchema = z
|
|
|
56
47
|
service_account: z.string().optional(),
|
|
57
48
|
models: z.array(ProviderModelSchema).default([]),
|
|
58
49
|
headers: z.record(z.string()).optional(),
|
|
59
|
-
quota: QuotaSchema.optional(),
|
|
60
50
|
// when true the provider is skipped in routing (kept in config, like a key's
|
|
61
51
|
// disabled state but for the whole provider).
|
|
62
52
|
disabled: z.boolean().optional(),
|
|
@@ -117,21 +107,46 @@ const ServerSchema = z
|
|
|
117
107
|
// optional friendly label per key, keyed by the key itself. Kept separate so
|
|
118
108
|
// api_keys stays a plain string[] (auth/masking paths untouched).
|
|
119
109
|
key_names: z.record(z.string()).optional(),
|
|
110
|
+
// per-key model allowlist (call-strings) + rate limit (req/min), keyed by the
|
|
111
|
+
// raw key like key_names. Absent → unrestricted / unlimited.
|
|
112
|
+
key_models: z.record(z.array(z.string().min(1))).optional(),
|
|
113
|
+
key_rpm: z.record(z.number().int().positive()).optional(),
|
|
114
|
+
// per-key access expiry, epoch ms, keyed by the RAW key. Absent → never expires.
|
|
115
|
+
key_expires: z.record(z.number().int().positive()).optional(),
|
|
120
116
|
})
|
|
121
117
|
.default({ host: "127.0.0.1", port: 18080, api_keys: [] });
|
|
122
118
|
|
|
123
119
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
120
|
+
* A spend budget scoped to the whole gateway, one provider, or one upstream
|
|
121
|
+
* model. unit picks what `limit` means — USD cost or total tokens. Soft-alert at
|
|
122
|
+
* alert_at (default 0.8), hard-stop at 100%. Each window is a rolling tumbling
|
|
123
|
+
* bucket on the epoch grid (window.ts). Opt-in: omit / empty list to disable.
|
|
127
124
|
*/
|
|
125
|
+
const BudgetScopeSchema = z.discriminatedUnion("type", [
|
|
126
|
+
z.object({ type: z.literal("global") }),
|
|
127
|
+
z.object({ type: z.literal("provider"), id: z.string().min(1) }),
|
|
128
|
+
z.object({ type: z.literal("model"), id: z.string().min(1) }),
|
|
129
|
+
z.object({ type: z.literal("key"), id: z.string().min(1) }),
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
// rolling windows replaced the old calendar windows; coerce any legacy value so
|
|
133
|
+
// existing config.yaml budgets keep loading (daily→24h, weekly→7day, monthly→30day).
|
|
134
|
+
const LEGACY_WINDOW: Record<string, string> = { daily: "24h", weekly: "7day", monthly: "30day" };
|
|
135
|
+
const WindowSchema = z.preprocess(
|
|
136
|
+
(v) => (typeof v === "string" && v in LEGACY_WINDOW ? LEGACY_WINDOW[v] : v),
|
|
137
|
+
z.enum(["5h", "24h", "7day", "30day"]),
|
|
138
|
+
);
|
|
139
|
+
|
|
128
140
|
const BudgetSchema = z.object({
|
|
141
|
+
scope: BudgetScopeSchema,
|
|
129
142
|
unit: z.enum(["usd", "tokens"]),
|
|
130
143
|
limit: z.number().positive(),
|
|
131
|
-
window:
|
|
132
|
-
|
|
133
|
-
|
|
144
|
+
window: WindowSchema,
|
|
145
|
+
// epoch ms the recurring cycle is anchored to; stamped by setBudget on create.
|
|
146
|
+
anchor: z.number().int().nonnegative().optional(),
|
|
134
147
|
alert_at: z.number().gt(0).lte(1).optional(),
|
|
148
|
+
// optional free-text label so an operator remembers what a budget is for.
|
|
149
|
+
note: z.string().max(200).optional(),
|
|
135
150
|
});
|
|
136
151
|
|
|
137
152
|
const ConfigSchema = z.object({
|
|
@@ -140,14 +155,14 @@ const ConfigSchema = z.object({
|
|
|
140
155
|
providers: z.array(ProviderSchema).default([]),
|
|
141
156
|
// the routing layer. Each entry is a "combo": an alias + a provider chain.
|
|
142
157
|
models: z.array(ModelRouteSchema).default([]),
|
|
143
|
-
|
|
158
|
+
budgets: z.array(BudgetSchema).default([]),
|
|
144
159
|
});
|
|
145
160
|
|
|
146
|
-
export type Quota = z.infer<typeof QuotaSchema>;
|
|
147
161
|
export type ProviderModel = z.infer<typeof ProviderModelSchema>;
|
|
148
162
|
export type Provider = z.infer<typeof ProviderSchema>;
|
|
149
163
|
export type ModelRoute = z.infer<typeof ModelRouteSchema>;
|
|
150
164
|
export type EndpointSettings = z.infer<typeof EndpointSchema>;
|
|
165
|
+
export type BudgetScope = z.infer<typeof BudgetScopeSchema>;
|
|
151
166
|
export type Budget = z.infer<typeof BudgetSchema>;
|
|
152
167
|
export type Config = z.infer<typeof ConfigSchema>;
|
|
153
168
|
|
|
@@ -277,6 +292,16 @@ export class GatewayConfig {
|
|
|
277
292
|
|
|
278
293
|
/** Validate an already-parsed config object. Throws with readable issues. */
|
|
279
294
|
export function validateConfig(parsed: unknown): GatewayConfig {
|
|
295
|
+
// migrate the legacy single `budget` (pre-scoped) into a global-scoped entry
|
|
296
|
+
// before zod runs — zod would otherwise strip the unknown `budget` key.
|
|
297
|
+
if (parsed && typeof parsed === "object") {
|
|
298
|
+
const raw = parsed as Record<string, unknown>;
|
|
299
|
+
if (raw.budget && !raw.budgets) {
|
|
300
|
+
const legacy = raw.budget as Record<string, unknown>;
|
|
301
|
+
raw.budgets = [{ scope: { type: "global" }, ...legacy }];
|
|
302
|
+
}
|
|
303
|
+
delete raw.budget;
|
|
304
|
+
}
|
|
280
305
|
const result = ConfigSchema.safeParse(parsed ?? {});
|
|
281
306
|
if (!result.success) {
|
|
282
307
|
const issues = result.error.issues
|
|
@@ -757,19 +782,49 @@ export function setHeadroom(
|
|
|
757
782
|
return next;
|
|
758
783
|
}
|
|
759
784
|
|
|
760
|
-
// ----
|
|
785
|
+
// ---- scoped budgets --------------------------------------------------------
|
|
761
786
|
|
|
762
|
-
/**
|
|
763
|
-
export function
|
|
787
|
+
/** Stable identity key for a budget's scope. */
|
|
788
|
+
export function budgetKey(scope: BudgetScope): string {
|
|
789
|
+
return scope.type === "global" ? "global" : `${scope.type}:${scope.id}`;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/** Add a budget, or replace the existing one with the same scope key. */
|
|
793
|
+
export function setBudget(config: Config, budget: Budget, now: number = Date.now()): Config {
|
|
794
|
+
if (budget.scope.type === "provider") {
|
|
795
|
+
const { id } = budget.scope;
|
|
796
|
+
if (!config.providers.some((p) => p.id === id)) {
|
|
797
|
+
throw new Error(`unknown provider "${id}" for budget scope`);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
if (budget.scope.type === "key") {
|
|
801
|
+
const { id } = budget.scope;
|
|
802
|
+
if (!config.server.api_keys.some((k) => clientKeyFingerprint(k) === id)) {
|
|
803
|
+
throw new Error(`unknown API key fingerprint "${id}" for budget scope`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
764
806
|
const next = cloneConfig(config);
|
|
765
|
-
|
|
807
|
+
const key = budgetKey(budget.scope);
|
|
808
|
+
const idx = next.budgets.findIndex((b) => budgetKey(b.scope) === key);
|
|
809
|
+
if (idx === -1) {
|
|
810
|
+
next.budgets.push({ ...budget, anchor: budget.anchor ?? now });
|
|
811
|
+
} else {
|
|
812
|
+
const prev = next.budgets[idx]!;
|
|
813
|
+
// keep the running cycle on edit (preserve prev anchor as-is, including a
|
|
814
|
+
// legacy undefined = epoch grid, so editing a limit never resets spend);
|
|
815
|
+
// start a fresh cycle only when the window length actually changed.
|
|
816
|
+
const anchor = budget.anchor ?? (prev.window === budget.window ? prev.anchor : now);
|
|
817
|
+
next.budgets[idx] = { ...budget, anchor };
|
|
818
|
+
}
|
|
766
819
|
return next;
|
|
767
820
|
}
|
|
768
821
|
|
|
769
|
-
/** Remove
|
|
770
|
-
export function clearBudget(config: Config): Config {
|
|
822
|
+
/** Remove a budget by its scope key (global | provider:<id> | model:<id> | key:<fp>). */
|
|
823
|
+
export function clearBudget(config: Config, key: string): Config {
|
|
771
824
|
const next = cloneConfig(config);
|
|
772
|
-
|
|
825
|
+
const idx = next.budgets.findIndex((b) => budgetKey(b.scope) === key);
|
|
826
|
+
if (idx === -1) throw new Error(`no budget with scope "${key}"`);
|
|
827
|
+
next.budgets.splice(idx, 1);
|
|
773
828
|
return next;
|
|
774
829
|
}
|
|
775
830
|
|
|
@@ -807,5 +862,63 @@ export function removeServerKey(config: Config, index: number): Config {
|
|
|
807
862
|
if (removed && next.server.key_names && removed in next.server.key_names) {
|
|
808
863
|
delete next.server.key_names[removed];
|
|
809
864
|
}
|
|
865
|
+
if (removed && next.server.key_models && removed in next.server.key_models) {
|
|
866
|
+
delete next.server.key_models[removed];
|
|
867
|
+
if (Object.keys(next.server.key_models).length === 0) next.server.key_models = undefined;
|
|
868
|
+
}
|
|
869
|
+
if (removed && next.server.key_rpm && removed in next.server.key_rpm) {
|
|
870
|
+
delete next.server.key_rpm[removed];
|
|
871
|
+
if (Object.keys(next.server.key_rpm).length === 0) next.server.key_rpm = undefined;
|
|
872
|
+
}
|
|
873
|
+
if (removed && next.server.key_expires && removed in next.server.key_expires) {
|
|
874
|
+
delete next.server.key_expires[removed];
|
|
875
|
+
if (Object.keys(next.server.key_expires).length === 0) next.server.key_expires = undefined;
|
|
876
|
+
}
|
|
810
877
|
return next;
|
|
811
878
|
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Set or clear a gateway key's scopes (by index, since keys are masked in the
|
|
882
|
+
* API). `models`/`rpm` are each applied only when present in the patch; an empty
|
|
883
|
+
* list or null/0 clears that scope. Empty maps are pruned to undefined.
|
|
884
|
+
*/
|
|
885
|
+
export function setServerKeyScope(
|
|
886
|
+
config: Config,
|
|
887
|
+
index: number,
|
|
888
|
+
patch: { models?: string[] | null; rpm?: number | null; expires?: number | null },
|
|
889
|
+
): Config {
|
|
890
|
+
const next = cloneConfig(config);
|
|
891
|
+
const keys = next.server.api_keys;
|
|
892
|
+
if (index < 0 || index >= keys.length) throw new Error(`no gateway key at index ${index}`);
|
|
893
|
+
const key = keys[index]!;
|
|
894
|
+
|
|
895
|
+
if (patch.models !== undefined) {
|
|
896
|
+
const models = { ...(next.server.key_models ?? {}) };
|
|
897
|
+
const list = (patch.models ?? []).map((m) => m.trim()).filter(Boolean);
|
|
898
|
+
if (list.length > 0) models[key] = list;
|
|
899
|
+
else delete models[key];
|
|
900
|
+
next.server.key_models = Object.keys(models).length > 0 ? models : undefined;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (patch.rpm !== undefined) {
|
|
904
|
+
const rpm = { ...(next.server.key_rpm ?? {}) };
|
|
905
|
+
if (patch.rpm && patch.rpm > 0) rpm[key] = Math.floor(patch.rpm);
|
|
906
|
+
else delete rpm[key];
|
|
907
|
+
next.server.key_rpm = Object.keys(rpm).length > 0 ? rpm : undefined;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (patch.expires !== undefined) {
|
|
911
|
+
const exp = { ...(next.server.key_expires ?? {}) };
|
|
912
|
+
if (patch.expires && patch.expires > 0) exp[key] = Math.floor(patch.expires);
|
|
913
|
+
else delete exp[key];
|
|
914
|
+
next.server.key_expires = Object.keys(exp).length > 0 ? exp : undefined;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
return next;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/** True when `rawKey` has an expiry set and `now` is strictly past it. */
|
|
921
|
+
export function isKeyExpired(server: Config["server"], rawKey: string, now: number): boolean {
|
|
922
|
+
const at = server.key_expires?.[rawKey];
|
|
923
|
+
return at !== undefined && now > at;
|
|
924
|
+
}
|
package/src/core/budget.ts
CHANGED
|
@@ -1,71 +1,124 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* truth) rather than a parallel counter.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Scoped spend budgets, derived from the usage table (the single source of
|
|
3
|
+
* truth) rather than a parallel counter. Each budget targets the whole gateway,
|
|
4
|
+
* one provider, or one upstream model. statuses() computes every budget's spend
|
|
5
|
+
* over its window; the result list is cached a few seconds so the per-request
|
|
6
|
+
* hard-stop check stays cheap. blocks() answers "is a route to this
|
|
7
|
+
* provider/model barred by an exhausted budget?".
|
|
7
8
|
*/
|
|
8
|
-
import type { Budget } from "../config.js";
|
|
9
|
-
import {
|
|
9
|
+
import type { Budget, BudgetScope } from "../config.js";
|
|
10
|
+
import { budgetKey } from "../config.js";
|
|
11
|
+
import { currentWindowStart, nextResetAt } from "./window.js";
|
|
10
12
|
|
|
11
13
|
export interface BudgetStatus {
|
|
14
|
+
scope: BudgetScope;
|
|
15
|
+
key: string;
|
|
16
|
+
label: string;
|
|
17
|
+
note?: string;
|
|
12
18
|
unit: "usd" | "tokens";
|
|
13
19
|
limit: number;
|
|
14
20
|
spent: number;
|
|
15
21
|
pct: number;
|
|
16
22
|
alert: boolean;
|
|
23
|
+
alert_at: number;
|
|
17
24
|
exhausted: boolean;
|
|
18
|
-
/** estimate in the converse unit (tokens if unit=usd, usd if unit=tokens); null when no usage yet */
|
|
19
25
|
est_converse: number | null;
|
|
20
26
|
reset_in_ms: number;
|
|
21
27
|
window: Budget["window"];
|
|
22
28
|
}
|
|
23
29
|
|
|
24
|
-
interface
|
|
25
|
-
|
|
30
|
+
interface TotalsReader {
|
|
31
|
+
totals(sinceMs: number, filter?: { provider?: string; model?: string; client_key?: string }): {
|
|
32
|
+
tokens_in: number;
|
|
33
|
+
tokens_out: number;
|
|
34
|
+
cost: number;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function scopeLabel(scope: BudgetScope, keyName: (fp: string) => string): string {
|
|
39
|
+
if (scope.type === "global") return "Global";
|
|
40
|
+
if (scope.type === "key") return keyName(scope.id);
|
|
41
|
+
return scope.id;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function scopeFilter(scope: BudgetScope): { provider?: string; model?: string; client_key?: string } | undefined {
|
|
45
|
+
if (scope.type === "provider") return { provider: scope.id };
|
|
46
|
+
if (scope.type === "model") return { model: scope.id };
|
|
47
|
+
if (scope.type === "key") return { client_key: scope.id };
|
|
48
|
+
return undefined;
|
|
26
49
|
}
|
|
27
50
|
|
|
28
51
|
export class BudgetTracker {
|
|
29
|
-
private cached?: { at: number;
|
|
52
|
+
private cached?: { at: number; list: BudgetStatus[] };
|
|
30
53
|
|
|
31
54
|
constructor(
|
|
32
|
-
private readonly
|
|
33
|
-
private readonly db:
|
|
55
|
+
private readonly getBudgets: () => Budget[],
|
|
56
|
+
private readonly db: TotalsReader,
|
|
34
57
|
private readonly now: () => number = Date.now,
|
|
35
58
|
private readonly cacheMs = 5000,
|
|
59
|
+
private readonly keyName: (fp: string) => string = (fp) => `key …${fp}`,
|
|
36
60
|
) {}
|
|
37
61
|
|
|
38
|
-
/** Flush the cached status — call after a config reload that may have changed the budget spec. */
|
|
39
62
|
clearCache(): void {
|
|
40
63
|
this.cached = undefined;
|
|
41
64
|
}
|
|
42
65
|
|
|
43
|
-
|
|
66
|
+
statuses(): BudgetStatus[] {
|
|
44
67
|
const t = this.now();
|
|
45
|
-
if (this.cached && t - this.cached.at < this.cacheMs) return this.cached.
|
|
46
|
-
const
|
|
47
|
-
this.cached = { at: t,
|
|
48
|
-
return
|
|
68
|
+
if (this.cached && t - this.cached.at < this.cacheMs) return this.cached.list;
|
|
69
|
+
const list = this.getBudgets().map((b) => this.compute(b, t));
|
|
70
|
+
this.cached = { at: t, list };
|
|
71
|
+
return list;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
globalStatus(): BudgetStatus | null {
|
|
75
|
+
return this.statuses().find((s) => s.scope.type === "global") ?? null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** First exhausted provider/model budget matching a route, or null. */
|
|
79
|
+
blocks(providerId: string, model: string): { exhausted: true; reset_in_ms: number } | null {
|
|
80
|
+
for (const s of this.statuses()) {
|
|
81
|
+
if (!s.exhausted) continue;
|
|
82
|
+
if (s.scope.type === "provider" && s.scope.id === providerId)
|
|
83
|
+
return { exhausted: true, reset_in_ms: s.reset_in_ms };
|
|
84
|
+
if (s.scope.type === "model" && s.scope.id === model)
|
|
85
|
+
return { exhausted: true, reset_in_ms: s.reset_in_ms };
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The exhausted key-scoped budget for this fingerprint, or null. */
|
|
91
|
+
blocksKey(fp: string): { exhausted: true; reset_in_ms: number } | null {
|
|
92
|
+
for (const s of this.statuses()) {
|
|
93
|
+
if (s.exhausted && s.scope.type === "key" && s.scope.id === fp) {
|
|
94
|
+
return { exhausted: true, reset_in_ms: s.reset_in_ms };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
49
98
|
}
|
|
50
99
|
|
|
51
|
-
private compute(t: number): BudgetStatus
|
|
52
|
-
const spec = this.getSpec();
|
|
53
|
-
if (!spec) return null;
|
|
100
|
+
private compute(spec: Budget, t: number): BudgetStatus {
|
|
54
101
|
const windowStart = currentWindowStart(spec, t);
|
|
55
|
-
const total = this.db.
|
|
102
|
+
const total = this.db.totals(windowStart, scopeFilter(spec.scope));
|
|
56
103
|
const tokens = total.tokens_in + total.tokens_out;
|
|
57
104
|
const cost = total.cost;
|
|
58
|
-
const rate = tokens > 0 ? cost / tokens : undefined;
|
|
105
|
+
const rate = tokens > 0 ? cost / tokens : undefined;
|
|
59
106
|
const spent = spec.unit === "usd" ? cost : tokens;
|
|
60
107
|
const limit = spec.limit;
|
|
61
108
|
const pct = limit > 0 ? Math.min(1, spent / limit) : 0;
|
|
109
|
+
const alertAt = spec.alert_at ?? 0.8;
|
|
62
110
|
const est_converse = rate === undefined ? null : spec.unit === "usd" ? limit / rate : limit * rate;
|
|
63
111
|
return {
|
|
112
|
+
scope: spec.scope,
|
|
113
|
+
key: budgetKey(spec.scope),
|
|
114
|
+
label: scopeLabel(spec.scope, this.keyName),
|
|
115
|
+
note: spec.note,
|
|
64
116
|
unit: spec.unit,
|
|
65
117
|
limit,
|
|
66
118
|
spent,
|
|
67
119
|
pct,
|
|
68
|
-
alert: pct >=
|
|
120
|
+
alert: pct >= alertAt,
|
|
121
|
+
alert_at: alertAt,
|
|
69
122
|
exhausted: spent >= limit,
|
|
70
123
|
est_converse,
|
|
71
124
|
reset_in_ms: Math.max(0, nextResetAt(spec, windowStart, t) - t),
|
package/src/core/fallback.ts
CHANGED
|
@@ -32,8 +32,6 @@ export interface FallbackOpts {
|
|
|
32
32
|
onAttempt?: (log: AttemptLog) => void;
|
|
33
33
|
/** which key the pool handed out for the winning attempt (handler uses it for usage). */
|
|
34
34
|
onServed?: (route: ResolvedRoute, key: string) => void;
|
|
35
|
-
/** when set, a provider this returns true for is skipped (quota exhausted). */
|
|
36
|
-
isExhausted?: (provider: ResolvedRoute["provider"]) => boolean;
|
|
37
35
|
/** captured client thinking intent, applied per-attempt in the provider's format. */
|
|
38
36
|
thinkingIntent?: ThinkingConfig | null;
|
|
39
37
|
}
|
|
@@ -56,13 +54,6 @@ export async function executeWithFallback(
|
|
|
56
54
|
for (const route of routes) {
|
|
57
55
|
const { provider } = route;
|
|
58
56
|
|
|
59
|
-
// skip a provider whose token budget is spent for this window — like a key
|
|
60
|
-
// cooling down, but for the whole provider. Falls through to the next route.
|
|
61
|
-
if (opts.isExhausted?.(provider)) {
|
|
62
|
-
log({ provider: provider.id, model: route.model, outcome: "skip", detail: "quota exhausted" });
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
57
|
const attempts = provider.max_retries + 1;
|
|
67
58
|
|
|
68
59
|
for (let i = 0; i < attempts; i++) {
|