omnigateway 0.2.2 → 0.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/README.md +334 -8
- package/bin/omni.js +1929 -147
- package/gateway.js +3129 -555
- package/package.json +1 -1
- package/public/assets/{Chip-DJwp7gBu.js → Chip-DbKk1ExR.js} +1 -1
- package/public/assets/Confirm-C5DYoI93.js +4 -0
- package/public/assets/CopyValue-CXjietiZ.js +27 -0
- package/public/assets/{Field-BDtp692L.js → Field-B7GOsJf8.js} +1 -1
- package/public/assets/Lamp-QRSv-SqM.js +25 -0
- package/public/assets/{Meter-BREBUFGX.js → Meter-Dngwbpjo.js} +1 -1
- package/public/assets/Modal-DfltaSCz.js +82 -0
- package/public/assets/Rack-D1zBw9SW.js +151 -0
- package/public/assets/{Readout-CEAoN5hu.js → Readout--CQnPjoO.js} +2 -2
- package/public/assets/{States-BEKLros0.js → States-CQFkw1UZ.js} +3 -3
- package/public/assets/{Table-BBypbVWj.js → Table-Ba58Wg4-.js} +1 -1
- package/public/assets/Toggle-DWrWc9-9.js +39 -0
- package/public/assets/TokenBreakdown-CyHJv0O5.js +31 -0
- package/public/assets/_app-CTDgfstQ.js +1 -0
- package/public/assets/_app.accounts-DP6T8z7I.js +64 -0
- package/public/assets/{_app.console-DKyGL9xx.js → _app.console-DYr7TCAl.js} +10 -10
- package/public/assets/_app.database-BzO1S_VR.js +39 -0
- package/public/assets/_app.index-CBt6b-lA.js +62 -0
- package/public/assets/_app.keys-B8HGsPPS.js +76 -0
- package/public/assets/_app.logs-DKsxa2Su.js +68 -0
- package/public/assets/{_app.models-CDqVrAeF.js → _app.models-CZD2UpM-.js} +28 -28
- package/public/assets/_app.settings-B2C1LQIF.js +46 -0
- package/public/assets/_app.usage-CDWUXtZy.js +83 -0
- package/public/assets/dist-IkJ0qg-4.js +1 -0
- package/public/assets/index-ClU3Xqui.js +186 -0
- package/public/assets/login-E3gh2_Cl.js +36 -0
- package/public/assets/plus-DkO4vG-T.js +1 -0
- package/public/assets/queries-BJHk_wso.js +144 -0
- package/public/assets/reasons-Cb8yF4Oy.js +1 -0
- package/public/assets/{shared-CIhZ6FCO.js → shared-Bp35cgFf.js} +7 -7
- package/public/assets/{trash-2-CLYNAAJA.js → trash-2-DTWWI3vA.js} +1 -1
- package/public/index.html +2 -2
- package/public/assets/Confirm-CVLz9F7a.js +0 -4
- package/public/assets/CopyValue-CNZbWS59.js +0 -27
- package/public/assets/Lamp-B65uWKbW.js +0 -25
- package/public/assets/Modal-DbydSAlp.js +0 -82
- package/public/assets/Rack-Bbo4JfEX.js +0 -151
- package/public/assets/Toggle-BUr71O4J.js +0 -39
- package/public/assets/TokenBreakdown-C1ziK9d1.js +0 -31
- package/public/assets/_app-ByQ1bPbe.js +0 -1
- package/public/assets/_app.accounts-BTug_Xfo.js +0 -64
- package/public/assets/_app.index-CSw9gmMf.js +0 -62
- package/public/assets/_app.keys-07Kbw4RS.js +0 -39
- package/public/assets/_app.logs-C9QiV39p.js +0 -32
- package/public/assets/_app.settings-DmUEeTaP.js +0 -38
- package/public/assets/_app.usage-B1WuMGRr.js +0 -83
- package/public/assets/dist-Bka7ErcH.js +0 -1
- package/public/assets/index-DJfb1i4P.js +0 -186
- package/public/assets/login-DKZYqRh0.js +0 -33
- package/public/assets/queries-D8LJNQpL.js +0 -144
package/gateway.js
CHANGED
|
@@ -5234,8 +5234,8 @@ var require_dist = __commonJS(function(exports) {
|
|
|
5234
5234
|
});
|
|
5235
5235
|
|
|
5236
5236
|
// apps/gateway/src/index.ts
|
|
5237
|
-
import { existsSync } from "fs";
|
|
5238
|
-
import { resolve as
|
|
5237
|
+
import { existsSync as existsSync2 } from "fs";
|
|
5238
|
+
import { resolve as resolve3 } from "path";
|
|
5239
5239
|
|
|
5240
5240
|
// packages/control/src/adminAuth.ts
|
|
5241
5241
|
import { hash, verify } from "@node-rs/argon2";
|
|
@@ -5292,9 +5292,34 @@ function createAdminAuth(store, opts) {
|
|
|
5292
5292
|
},
|
|
5293
5293
|
logout(token) {
|
|
5294
5294
|
sessions.delete(token);
|
|
5295
|
+
},
|
|
5296
|
+
invalidateSessions() {
|
|
5297
|
+
sessions.clear();
|
|
5295
5298
|
}
|
|
5296
5299
|
};
|
|
5297
5300
|
}
|
|
5301
|
+
// packages/control/src/bodies.ts
|
|
5302
|
+
async function readRequestBody(store, requestId) {
|
|
5303
|
+
const read = await store.bodies.get(requestId);
|
|
5304
|
+
if (read === null) {
|
|
5305
|
+
return {
|
|
5306
|
+
requestId,
|
|
5307
|
+
detailState: "none",
|
|
5308
|
+
truncated: false,
|
|
5309
|
+
sizeBytes: 0,
|
|
5310
|
+
at: null,
|
|
5311
|
+
artifact: null
|
|
5312
|
+
};
|
|
5313
|
+
}
|
|
5314
|
+
return {
|
|
5315
|
+
requestId: read.row.requestId,
|
|
5316
|
+
detailState: read.row.detailState,
|
|
5317
|
+
truncated: read.row.truncated,
|
|
5318
|
+
sizeBytes: read.row.sizeBytes,
|
|
5319
|
+
at: read.row.at,
|
|
5320
|
+
artifact: read.artifact
|
|
5321
|
+
};
|
|
5322
|
+
}
|
|
5298
5323
|
// packages/ir/src/betas.ts
|
|
5299
5324
|
var CONTEXT_1M_BETA = "context-1m-2025-08-07";
|
|
5300
5325
|
var CONTEXT_1M_TOKENS = 1e6;
|
|
@@ -5378,6 +5403,7 @@ var FIELD_ORDER = [
|
|
|
5378
5403
|
"requestedModel",
|
|
5379
5404
|
"credentialId",
|
|
5380
5405
|
"apiKeyId",
|
|
5406
|
+
"snapshotId",
|
|
5381
5407
|
"attempt",
|
|
5382
5408
|
"attempts",
|
|
5383
5409
|
"code",
|
|
@@ -5395,9 +5421,11 @@ var FIELD_ORDER = [
|
|
|
5395
5421
|
"rawCount",
|
|
5396
5422
|
"dailyCount",
|
|
5397
5423
|
"quotaSampleCount",
|
|
5424
|
+
"sizeBytes",
|
|
5398
5425
|
"host",
|
|
5399
5426
|
"port",
|
|
5400
5427
|
"path",
|
|
5428
|
+
"supervisor",
|
|
5401
5429
|
"reason"
|
|
5402
5430
|
];
|
|
5403
5431
|
var REASON_MAX = 200;
|
|
@@ -5713,6 +5741,7 @@ function loadConfig(env) {
|
|
|
5713
5741
|
const staticDir = env.OMNI_STATIC_DIR?.trim();
|
|
5714
5742
|
const logFile = env.OMNI_LOG_FILE?.trim();
|
|
5715
5743
|
const exposeClaudeCodeAliases = TRUTHY.has((env.OMNI_EXPOSE_CLAUDE_CODE_ALIASES ?? "").trim().toLowerCase());
|
|
5744
|
+
const bodyLoggingAllowed = TRUTHY.has((env.OMNI_BODY_LOGGING_ALLOWED ?? "").trim().toLowerCase());
|
|
5716
5745
|
const rawLogLevel = env.OMNI_LOG_LEVEL?.trim();
|
|
5717
5746
|
const logLevel = parseLogLevel(rawLogLevel);
|
|
5718
5747
|
return {
|
|
@@ -5725,6 +5754,7 @@ function loadConfig(env) {
|
|
|
5725
5754
|
baseUrl,
|
|
5726
5755
|
staticDir: staticDir === undefined || staticDir.length === 0 ? null : staticDir,
|
|
5727
5756
|
exposeClaudeCodeAliases,
|
|
5757
|
+
bodyLoggingAllowed,
|
|
5728
5758
|
logFile: logFile === undefined || logFile.length === 0 ? null : logFile
|
|
5729
5759
|
};
|
|
5730
5760
|
}
|
|
@@ -5864,14 +5894,16 @@ function billingBlock() {
|
|
|
5864
5894
|
return `${BILLING_PREFIX} cc_version=${CLI_VERSION}.${BUILD_REVISION}; ` + `cc_entrypoint=cli; cch=${CCH_PLACEHOLDER};`;
|
|
5865
5895
|
}
|
|
5866
5896
|
var BANNED_SUBSTRINGS = [
|
|
5867
|
-
"github.com/anomalyco/
|
|
5868
|
-
"
|
|
5869
|
-
"github.com/
|
|
5870
|
-
"github.com/
|
|
5871
|
-
"
|
|
5897
|
+
"github.com/anomalyco/opencode",
|
|
5898
|
+
"opencode.ai/docs",
|
|
5899
|
+
"github.com/cline/cline",
|
|
5900
|
+
"github.com/getcursor/cursor",
|
|
5901
|
+
"continue.dev",
|
|
5902
|
+
"hermes-agent.nousresearch.com"
|
|
5872
5903
|
];
|
|
5904
|
+
var IDENTITY_PREFIXES = ["You are OpenCode", "You are Hermes Agent"];
|
|
5873
5905
|
var REWRITES = [
|
|
5874
|
-
["if
|
|
5906
|
+
["if OpenCode honestly", "if the assistant honestly"],
|
|
5875
5907
|
[
|
|
5876
5908
|
"Here is some useful information about the environment you are running in:",
|
|
5877
5909
|
"Environment context you are running in:"
|
|
@@ -5880,7 +5912,7 @@ var REWRITES = [
|
|
|
5880
5912
|
function applyAnthropicSystem(system) {
|
|
5881
5913
|
const kept = [];
|
|
5882
5914
|
for (const block of system) {
|
|
5883
|
-
const text = block.text.split(/\n{2,}/).filter((p) => !BANNED_SUBSTRINGS.some((b) => p.includes(b))).filter((p) => !p.trimStart().startsWith(
|
|
5915
|
+
const text = block.text.split(/\n{2,}/).filter((p) => !BANNED_SUBSTRINGS.some((b) => p.includes(b))).filter((p) => !IDENTITY_PREFIXES.some((i) => p.trimStart().startsWith(i))).filter((p) => !p.includes(BILLING_PREFIX)).filter((p) => p.trim() !== AGENT_PREAMBLE).join(`
|
|
5884
5916
|
|
|
5885
5917
|
`);
|
|
5886
5918
|
let rewritten = text;
|
|
@@ -23187,6 +23219,45 @@ function date4(params) {
|
|
|
23187
23219
|
|
|
23188
23220
|
// node_modules/.bun/zod@4.4.3/node_modules/zod/v4/classic/external.js
|
|
23189
23221
|
config(en_default());
|
|
23222
|
+
// packages/ratelimit/src/catalog.ts
|
|
23223
|
+
var WINDOWS = ["1m", "5h", "1w"];
|
|
23224
|
+
var WINDOW_MS = {
|
|
23225
|
+
"1m": 60000,
|
|
23226
|
+
"5h": 5 * 60 * 60 * 1000,
|
|
23227
|
+
"1w": 7 * 24 * 60 * 60 * 1000
|
|
23228
|
+
};
|
|
23229
|
+
var countLimit = exports_external.number().int().positive().nullable();
|
|
23230
|
+
var spendLimit = exports_external.number().positive().nullable();
|
|
23231
|
+
var countWindows = exports_external.object({
|
|
23232
|
+
"1m": countLimit.optional(),
|
|
23233
|
+
"5h": countLimit.optional(),
|
|
23234
|
+
"1w": countLimit.optional()
|
|
23235
|
+
}).strict();
|
|
23236
|
+
var spendWindows = exports_external.object({
|
|
23237
|
+
"5h": spendLimit.optional(),
|
|
23238
|
+
"1w": spendLimit.optional()
|
|
23239
|
+
}).strict();
|
|
23240
|
+
var limitConfigSchema = exports_external.object({
|
|
23241
|
+
requests: countWindows.optional(),
|
|
23242
|
+
tokens: countWindows.optional(),
|
|
23243
|
+
spend: spendWindows.optional(),
|
|
23244
|
+
concurrency: countLimit.optional()
|
|
23245
|
+
}).strict();
|
|
23246
|
+
function assertNoProto(value, path) {
|
|
23247
|
+
if (typeof value !== "object" || value === null)
|
|
23248
|
+
return;
|
|
23249
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
23250
|
+
if (key === "__proto__") {
|
|
23251
|
+
throw new Error(`limits cannot name "__proto__"${path === "" ? "" : ` under ${path}`}`);
|
|
23252
|
+
}
|
|
23253
|
+
assertNoProto(value[key], path === "" ? key : `${path}.${key}`);
|
|
23254
|
+
}
|
|
23255
|
+
}
|
|
23256
|
+
function parseLimitConfig(value) {
|
|
23257
|
+
assertNoProto(value, "");
|
|
23258
|
+
return limitConfigSchema.parse(value);
|
|
23259
|
+
}
|
|
23260
|
+
|
|
23190
23261
|
// packages/control/src/schemas.ts
|
|
23191
23262
|
function parseOrThrow(schema, body2) {
|
|
23192
23263
|
const result = schema.safeParse(body2);
|
|
@@ -23264,7 +23335,13 @@ var modelSchema = exports_external.object({
|
|
|
23264
23335
|
var keyCreateSchema = exports_external.object({
|
|
23265
23336
|
label: exports_external.string().min(1).default("api key"),
|
|
23266
23337
|
modelAllowlist: exports_external.array(exports_external.string().min(1)).nullable().default(null),
|
|
23267
|
-
|
|
23338
|
+
limits: limitConfigSchema.default({}),
|
|
23339
|
+
bodyLoggingOptOut: exports_external.boolean().default(false)
|
|
23340
|
+
}).strict();
|
|
23341
|
+
var keyLimitsSchema = exports_external.object({ limits: limitConfigSchema }).strict();
|
|
23342
|
+
var retentionSchema = exports_external.object({
|
|
23343
|
+
keepLatest: exports_external.number().int().min(1).max(100),
|
|
23344
|
+
maxAgeDays: exports_external.number().int().min(1).max(3650)
|
|
23268
23345
|
}).strict();
|
|
23269
23346
|
var settingsSchema = exports_external.object({
|
|
23270
23347
|
weights: exports_external.object({
|
|
@@ -23281,7 +23358,11 @@ var settingsSchema = exports_external.object({
|
|
|
23281
23358
|
breakerCooldownMs: exports_external.number().int().positive(),
|
|
23282
23359
|
logRetentionDays: exports_external.number().int().min(1),
|
|
23283
23360
|
quotaPollIntervalMs: exports_external.number().int().min(0),
|
|
23284
|
-
rtkEnabled: exports_external.boolean()
|
|
23361
|
+
rtkEnabled: exports_external.boolean(),
|
|
23362
|
+
bodyLoggingEnabled: exports_external.boolean(),
|
|
23363
|
+
bodyLoggingCaptureStreamChunks: exports_external.boolean(),
|
|
23364
|
+
snapshotKeepLatest: retentionSchema.shape.keepLatest.optional(),
|
|
23365
|
+
snapshotMaxAgeDays: retentionSchema.shape.maxAgeDays.optional()
|
|
23285
23366
|
});
|
|
23286
23367
|
var credentialPatchSchema = exports_external.object({
|
|
23287
23368
|
label: exports_external.string().min(1).optional(),
|
|
@@ -23493,7 +23574,11 @@ var DEFAULT_SETTINGS = {
|
|
|
23493
23574
|
breakerCooldownMs: 30000,
|
|
23494
23575
|
logRetentionDays: 30,
|
|
23495
23576
|
quotaPollIntervalMs: 300000,
|
|
23496
|
-
rtkEnabled: false
|
|
23577
|
+
rtkEnabled: false,
|
|
23578
|
+
bodyLoggingEnabled: false,
|
|
23579
|
+
bodyLoggingCaptureStreamChunks: false,
|
|
23580
|
+
snapshotKeepLatest: 5,
|
|
23581
|
+
snapshotMaxAgeDays: 30
|
|
23497
23582
|
};
|
|
23498
23583
|
|
|
23499
23584
|
// packages/router/src/quota.ts
|
|
@@ -23747,6 +23832,10 @@ function rank(input) {
|
|
|
23747
23832
|
return { candidates: scored, excluded };
|
|
23748
23833
|
}
|
|
23749
23834
|
|
|
23835
|
+
// packages/store/src/bodies/artifact.ts
|
|
23836
|
+
import { mkdir, readdir, readFile, rmdir, unlink, writeFile } from "fs/promises";
|
|
23837
|
+
import { dirname, join, sep } from "path";
|
|
23838
|
+
|
|
23750
23839
|
// packages/store/src/encryption.ts
|
|
23751
23840
|
var PREFIX = "enc:v1";
|
|
23752
23841
|
var IV_BYTES = 12;
|
|
@@ -23786,6 +23875,362 @@ function unhex(s) {
|
|
|
23786
23875
|
out[i] = Number.parseInt(s.slice(i * 2, i * 2 + 2), 16);
|
|
23787
23876
|
return out;
|
|
23788
23877
|
}
|
|
23878
|
+
|
|
23879
|
+
// packages/store/src/bodies/bound.ts
|
|
23880
|
+
var MAX_STRING_BYTES = 64 * 1024;
|
|
23881
|
+
var MAX_ARRAY_ITEMS = 24;
|
|
23882
|
+
var MAX_DEPTH = 6;
|
|
23883
|
+
var MAX_OBJECT_KEYS = 80;
|
|
23884
|
+
var STRING_TRUNCATION_MARKER = "\u2026[truncated]";
|
|
23885
|
+
var DEPTH_MARKER = `[omitted: nesting past ${MAX_DEPTH} levels]`;
|
|
23886
|
+
var encoder = new TextEncoder;
|
|
23887
|
+
var decoder = new TextDecoder;
|
|
23888
|
+
var CUT_BYTES = MAX_STRING_BYTES - encoder.encode(STRING_TRUNCATION_MARKER).length - 3;
|
|
23889
|
+
function truncateToBytes(value) {
|
|
23890
|
+
const bytes = encoder.encode(value);
|
|
23891
|
+
if (bytes.length <= MAX_STRING_BYTES)
|
|
23892
|
+
return value;
|
|
23893
|
+
return decoder.decode(bytes.subarray(0, CUT_BYTES)) + STRING_TRUNCATION_MARKER;
|
|
23894
|
+
}
|
|
23895
|
+
function boundValue(value) {
|
|
23896
|
+
let truncated = false;
|
|
23897
|
+
const walk = (input, depth) => {
|
|
23898
|
+
if (typeof input === "string") {
|
|
23899
|
+
const cut = truncateToBytes(input);
|
|
23900
|
+
if (cut !== input)
|
|
23901
|
+
truncated = true;
|
|
23902
|
+
return cut;
|
|
23903
|
+
}
|
|
23904
|
+
if (Array.isArray(input)) {
|
|
23905
|
+
if (depth > MAX_DEPTH) {
|
|
23906
|
+
truncated = true;
|
|
23907
|
+
return DEPTH_MARKER;
|
|
23908
|
+
}
|
|
23909
|
+
const kept = input.length > MAX_ARRAY_ITEMS ? input.slice(-MAX_ARRAY_ITEMS) : input;
|
|
23910
|
+
if (kept.length !== input.length)
|
|
23911
|
+
truncated = true;
|
|
23912
|
+
return kept.map((item) => walk(item, depth + 1));
|
|
23913
|
+
}
|
|
23914
|
+
if (input !== null && typeof input === "object") {
|
|
23915
|
+
if (depth > MAX_DEPTH) {
|
|
23916
|
+
truncated = true;
|
|
23917
|
+
return DEPTH_MARKER;
|
|
23918
|
+
}
|
|
23919
|
+
const entries = Object.entries(input);
|
|
23920
|
+
const kept = entries.length > MAX_OBJECT_KEYS ? entries.slice(0, MAX_OBJECT_KEYS) : entries;
|
|
23921
|
+
if (kept.length !== entries.length)
|
|
23922
|
+
truncated = true;
|
|
23923
|
+
return Object.fromEntries(kept.map(([k, v]) => [k, walk(v, depth + 1)]));
|
|
23924
|
+
}
|
|
23925
|
+
return input;
|
|
23926
|
+
};
|
|
23927
|
+
const bounded = walk(value, 1);
|
|
23928
|
+
return { value: bounded, truncated };
|
|
23929
|
+
}
|
|
23930
|
+
|
|
23931
|
+
// packages/store/src/bodies/mask.ts
|
|
23932
|
+
var ELIDED = "[redacted]";
|
|
23933
|
+
var BEARER = /\b(bearer)\s+[A-Za-z0-9._~+/=-]+/gi;
|
|
23934
|
+
var PREFIXED_KEY = /(?<![A-Za-z0-9_-])(?:sk|ak|pk)-[A-Za-z0-9_-]{8,}/g;
|
|
23935
|
+
var VENDOR_KEY = /(?<![A-Za-z0-9_-])(?:gh[posu]_|github_pat_|AIza|GOCSPX-)[A-Za-z0-9_-]{8,}/g;
|
|
23936
|
+
var VENDOR_PREFIX = /^(?:gh[posu]_|github_pat_|AIza|GOCSPX-)/;
|
|
23937
|
+
var XAI_KEY = /(?<![A-Za-z0-9_-])xai-[A-Za-z0-9_]{8,}/g;
|
|
23938
|
+
var OPAQUE = /[A-Za-z0-9_-]{41,}/g;
|
|
23939
|
+
var MASK_RULES = [
|
|
23940
|
+
{
|
|
23941
|
+
id: "bearer",
|
|
23942
|
+
pattern: BEARER,
|
|
23943
|
+
keep: (match) => match.length - match.replace(/^bearer\s+/i, "").length
|
|
23944
|
+
},
|
|
23945
|
+
{ id: "prefixedKey", pattern: PREFIXED_KEY, keep: () => 3 },
|
|
23946
|
+
{
|
|
23947
|
+
id: "vendorKey",
|
|
23948
|
+
pattern: VENDOR_KEY,
|
|
23949
|
+
keep: (match) => VENDOR_PREFIX.exec(match)?.[0].length ?? 0
|
|
23950
|
+
},
|
|
23951
|
+
{ id: "xaiKey", pattern: XAI_KEY, keep: () => "xai-".length },
|
|
23952
|
+
{ id: "opaque", pattern: OPAQUE, keep: () => 0 }
|
|
23953
|
+
];
|
|
23954
|
+
function maskString(value) {
|
|
23955
|
+
return MASK_RULES.reduce((masked, rule) => masked.replace(rule.pattern, (match) => `${match.slice(0, rule.keep(match))}${ELIDED}`), value);
|
|
23956
|
+
}
|
|
23957
|
+
function maskSecrets(value) {
|
|
23958
|
+
if (typeof value === "string")
|
|
23959
|
+
return maskString(value);
|
|
23960
|
+
if (Array.isArray(value))
|
|
23961
|
+
return value.map(maskSecrets);
|
|
23962
|
+
if (value !== null && typeof value === "object") {
|
|
23963
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, maskSecrets(v)]));
|
|
23964
|
+
}
|
|
23965
|
+
return value;
|
|
23966
|
+
}
|
|
23967
|
+
|
|
23968
|
+
// packages/store/src/bodies/artifact.ts
|
|
23969
|
+
var ARTIFACT_SCHEMA_VERSION = 1;
|
|
23970
|
+
var MAX_ARTIFACT_BYTES = 512 * 1024;
|
|
23971
|
+
var BODY_ROW_CAP = 1e5;
|
|
23972
|
+
var BODIES_DIRNAME = "request_bodies";
|
|
23973
|
+
var encoder2 = new TextEncoder;
|
|
23974
|
+
function bodiesDirFor(databasePath) {
|
|
23975
|
+
return join(dirname(databasePath), BODIES_DIRNAME);
|
|
23976
|
+
}
|
|
23977
|
+
var SAFE_REQUEST_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
23978
|
+
function isSafeRequestId(id) {
|
|
23979
|
+
return SAFE_REQUEST_ID.test(id);
|
|
23980
|
+
}
|
|
23981
|
+
function relPathFor(requestId, at) {
|
|
23982
|
+
const date5 = new Date(at);
|
|
23983
|
+
const yyyy = String(date5.getUTCFullYear()).padStart(4, "0");
|
|
23984
|
+
const mm = String(date5.getUTCMonth() + 1).padStart(2, "0");
|
|
23985
|
+
const dd = String(date5.getUTCDate()).padStart(2, "0");
|
|
23986
|
+
return `${yyyy}/${mm}/${dd}/${requestId}.json.enc`;
|
|
23987
|
+
}
|
|
23988
|
+
async function sha256Hex(bytes) {
|
|
23989
|
+
const digest = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes));
|
|
23990
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
23991
|
+
}
|
|
23992
|
+
function bodyPair(pair) {
|
|
23993
|
+
const request2 = boundValue(maskSecrets(pair.request));
|
|
23994
|
+
const response = boundValue(maskSecrets(pair.response));
|
|
23995
|
+
return {
|
|
23996
|
+
request: request2.value,
|
|
23997
|
+
response: response.value,
|
|
23998
|
+
truncated: pair.truncated === true || request2.truncated || response.truncated
|
|
23999
|
+
};
|
|
24000
|
+
}
|
|
24001
|
+
function omission(serializedBytes) {
|
|
24002
|
+
return {
|
|
24003
|
+
omitted: true,
|
|
24004
|
+
reason: `artifact exceeded ${MAX_ARTIFACT_BYTES} bytes after structural bounding`,
|
|
24005
|
+
serializedBytes
|
|
24006
|
+
};
|
|
24007
|
+
}
|
|
24008
|
+
function omitBodies(artifact, marker) {
|
|
24009
|
+
return {
|
|
24010
|
+
...artifact,
|
|
24011
|
+
client: { request: marker, response: marker, truncated: true },
|
|
24012
|
+
attempts: artifact.attempts.map((attempt) => ({
|
|
24013
|
+
attempt: attempt.attempt,
|
|
24014
|
+
provider: attempt.provider,
|
|
24015
|
+
request: marker,
|
|
24016
|
+
response: marker,
|
|
24017
|
+
streamChunks: null,
|
|
24018
|
+
truncated: true
|
|
24019
|
+
}))
|
|
24020
|
+
};
|
|
24021
|
+
}
|
|
24022
|
+
function prepareArtifact(input) {
|
|
24023
|
+
const client = bodyPair(input.client);
|
|
24024
|
+
const attempts = input.attempts.map((attempt) => {
|
|
24025
|
+
const pair = bodyPair(attempt);
|
|
24026
|
+
const chunks = attempt.streamChunks === null ? null : boundValue(attempt.streamChunks.map(maskString));
|
|
24027
|
+
return {
|
|
24028
|
+
attempt: attempt.attempt,
|
|
24029
|
+
provider: attempt.provider,
|
|
24030
|
+
request: pair.request,
|
|
24031
|
+
response: pair.response,
|
|
24032
|
+
streamChunks: chunks === null ? null : chunks.value,
|
|
24033
|
+
truncated: pair.truncated || (chunks?.truncated ?? false)
|
|
24034
|
+
};
|
|
24035
|
+
});
|
|
24036
|
+
const error51 = boundValue(maskSecrets(input.error));
|
|
24037
|
+
const bounded = {
|
|
24038
|
+
schemaVersion: ARTIFACT_SCHEMA_VERSION,
|
|
24039
|
+
requestId: input.requestId,
|
|
24040
|
+
at: input.at,
|
|
24041
|
+
client,
|
|
24042
|
+
attempts,
|
|
24043
|
+
error: error51.value
|
|
24044
|
+
};
|
|
24045
|
+
const json7 = JSON.stringify(bounded);
|
|
24046
|
+
const size = encoder2.encode(json7).length;
|
|
24047
|
+
if (size <= MAX_ARTIFACT_BYTES)
|
|
24048
|
+
return { artifact: bounded, json: json7 };
|
|
24049
|
+
const marker = omission(size);
|
|
24050
|
+
const omitted = omitBodies(bounded, marker);
|
|
24051
|
+
const omittedJson = JSON.stringify(omitted);
|
|
24052
|
+
if (encoder2.encode(omittedJson).length <= MAX_ARTIFACT_BYTES) {
|
|
24053
|
+
return { artifact: omitted, json: omittedJson };
|
|
24054
|
+
}
|
|
24055
|
+
const stripped = { ...omitted, error: marker };
|
|
24056
|
+
return { artifact: stripped, json: JSON.stringify(stripped) };
|
|
24057
|
+
}
|
|
24058
|
+
async function sealArtifact(key, json7) {
|
|
24059
|
+
const bytes = encoder2.encode(await encrypt(key, json7));
|
|
24060
|
+
return { bytes, sha256: await sha256Hex(bytes) };
|
|
24061
|
+
}
|
|
24062
|
+
async function readArtifact(key, dir, relPath, expectedSha256) {
|
|
24063
|
+
let bytes;
|
|
24064
|
+
try {
|
|
24065
|
+
bytes = new Uint8Array(await readFile(join(dir, relPath)));
|
|
24066
|
+
} catch {
|
|
24067
|
+
return { ok: false, failure: "missing" };
|
|
24068
|
+
}
|
|
24069
|
+
try {
|
|
24070
|
+
if (expectedSha256 !== null && await sha256Hex(bytes) !== expectedSha256) {
|
|
24071
|
+
return { ok: false, failure: "corrupt" };
|
|
24072
|
+
}
|
|
24073
|
+
const parsed = JSON.parse(await decrypt(key, new TextDecoder().decode(bytes)));
|
|
24074
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
24075
|
+
return { ok: false, failure: "corrupt" };
|
|
24076
|
+
}
|
|
24077
|
+
return { ok: true, artifact: parsed };
|
|
24078
|
+
} catch {
|
|
24079
|
+
return { ok: false, failure: "corrupt" };
|
|
24080
|
+
}
|
|
24081
|
+
}
|
|
24082
|
+
async function writeArtifact(dir, relPath, bytes) {
|
|
24083
|
+
const full = join(dir, relPath);
|
|
24084
|
+
await mkdir(dirname(full), { recursive: true });
|
|
24085
|
+
await writeFile(full, bytes);
|
|
24086
|
+
}
|
|
24087
|
+
async function deleteArtifact(dir, relPath) {
|
|
24088
|
+
const full = join(dir, relPath);
|
|
24089
|
+
try {
|
|
24090
|
+
await unlink(full);
|
|
24091
|
+
} catch {
|
|
24092
|
+
return;
|
|
24093
|
+
}
|
|
24094
|
+
let parent = dirname(full);
|
|
24095
|
+
while (parent.length > dir.length && parent.startsWith(dir + sep)) {
|
|
24096
|
+
try {
|
|
24097
|
+
await rmdir(parent);
|
|
24098
|
+
} catch {
|
|
24099
|
+
return;
|
|
24100
|
+
}
|
|
24101
|
+
parent = dirname(parent);
|
|
24102
|
+
}
|
|
24103
|
+
}
|
|
24104
|
+
async function listArtifacts(dir) {
|
|
24105
|
+
const out = [];
|
|
24106
|
+
const walk = async (current, prefix) => {
|
|
24107
|
+
let entries;
|
|
24108
|
+
try {
|
|
24109
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
24110
|
+
} catch {
|
|
24111
|
+
return;
|
|
24112
|
+
}
|
|
24113
|
+
for (const entry of entries) {
|
|
24114
|
+
const rel = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
24115
|
+
if (entry.isDirectory())
|
|
24116
|
+
await walk(join(current, entry.name), rel);
|
|
24117
|
+
else if (entry.isFile())
|
|
24118
|
+
out.push(rel);
|
|
24119
|
+
}
|
|
24120
|
+
};
|
|
24121
|
+
await walk(dir, "");
|
|
24122
|
+
return out;
|
|
24123
|
+
}
|
|
24124
|
+
// packages/store/src/sqlite/bodies.ts
|
|
24125
|
+
function toDetailState(value) {
|
|
24126
|
+
return value === "ready" || value === "missing" || value === "corrupt" ? value : "none";
|
|
24127
|
+
}
|
|
24128
|
+
var toRow = (r) => ({
|
|
24129
|
+
requestId: r.request_id,
|
|
24130
|
+
at: r.at,
|
|
24131
|
+
relPath: r.rel_path,
|
|
24132
|
+
sizeBytes: r.size_bytes,
|
|
24133
|
+
sha256: r.sha256,
|
|
24134
|
+
detailState: toDetailState(r.detail_state),
|
|
24135
|
+
truncated: r.truncated === 1
|
|
24136
|
+
});
|
|
24137
|
+
function createBodyRepo(db, key, dir) {
|
|
24138
|
+
const removeRows = async (rows) => {
|
|
24139
|
+
for (const row of rows) {
|
|
24140
|
+
if (row.relPath !== null)
|
|
24141
|
+
await deleteArtifact(dir, row.relPath);
|
|
24142
|
+
}
|
|
24143
|
+
const remove = db.prepare("DELETE FROM request_bodies WHERE request_id = ?");
|
|
24144
|
+
db.transaction(() => {
|
|
24145
|
+
for (const row of rows)
|
|
24146
|
+
remove.run(row.requestId);
|
|
24147
|
+
})();
|
|
24148
|
+
return rows.length;
|
|
24149
|
+
};
|
|
24150
|
+
const recordState = (requestId, state) => {
|
|
24151
|
+
db.run("UPDATE request_bodies SET detail_state = ? WHERE request_id = ?", [state, requestId]);
|
|
24152
|
+
};
|
|
24153
|
+
return {
|
|
24154
|
+
async put(artifact) {
|
|
24155
|
+
if (!isSafeRequestId(artifact.requestId)) {
|
|
24156
|
+
throw new Error("request id is not safe to use as an artifact path segment");
|
|
24157
|
+
}
|
|
24158
|
+
const prepared = prepareArtifact(artifact);
|
|
24159
|
+
const sealed = await sealArtifact(key, prepared.json);
|
|
24160
|
+
const relPath = relPathFor(artifact.requestId, artifact.at);
|
|
24161
|
+
await writeArtifact(dir, relPath, sealed.bytes);
|
|
24162
|
+
const truncated = prepared.artifact.client.truncated || prepared.artifact.attempts.some((attempt) => attempt.truncated);
|
|
24163
|
+
const row = {
|
|
24164
|
+
requestId: artifact.requestId,
|
|
24165
|
+
at: artifact.at,
|
|
24166
|
+
relPath,
|
|
24167
|
+
sizeBytes: sealed.bytes.length,
|
|
24168
|
+
sha256: sealed.sha256,
|
|
24169
|
+
detailState: "ready",
|
|
24170
|
+
truncated
|
|
24171
|
+
};
|
|
24172
|
+
db.run(`INSERT INTO request_bodies (request_id, at, rel_path, size_bytes, sha256, detail_state, truncated)
|
|
24173
|
+
VALUES (?,?,?,?,?,?,?)
|
|
24174
|
+
ON CONFLICT (request_id) DO UPDATE SET
|
|
24175
|
+
at = excluded.at,
|
|
24176
|
+
rel_path = excluded.rel_path,
|
|
24177
|
+
size_bytes = excluded.size_bytes,
|
|
24178
|
+
sha256 = excluded.sha256,
|
|
24179
|
+
detail_state = excluded.detail_state,
|
|
24180
|
+
truncated = excluded.truncated`, [
|
|
24181
|
+
row.requestId,
|
|
24182
|
+
row.at,
|
|
24183
|
+
row.relPath,
|
|
24184
|
+
row.sizeBytes,
|
|
24185
|
+
row.sha256,
|
|
24186
|
+
row.detailState,
|
|
24187
|
+
row.truncated ? 1 : 0
|
|
24188
|
+
]);
|
|
24189
|
+
return row;
|
|
24190
|
+
},
|
|
24191
|
+
async get(requestId) {
|
|
24192
|
+
const found = db.query("SELECT * FROM request_bodies WHERE request_id = ?").get(requestId);
|
|
24193
|
+
if (found === null)
|
|
24194
|
+
return null;
|
|
24195
|
+
const row = toRow(found);
|
|
24196
|
+
if (row.relPath === null || row.detailState === "none")
|
|
24197
|
+
return { row, artifact: null };
|
|
24198
|
+
const read = await readArtifact(key, dir, row.relPath, row.sha256);
|
|
24199
|
+
if (read.ok) {
|
|
24200
|
+
if (row.detailState !== "ready")
|
|
24201
|
+
recordState(requestId, "ready");
|
|
24202
|
+
return { row: { ...row, detailState: "ready" }, artifact: read.artifact };
|
|
24203
|
+
}
|
|
24204
|
+
recordState(requestId, read.failure);
|
|
24205
|
+
return { row: { ...row, detailState: read.failure }, artifact: null };
|
|
24206
|
+
},
|
|
24207
|
+
async prune(olderThan) {
|
|
24208
|
+
const rows = db.query("SELECT * FROM request_bodies WHERE at < ?").all(olderThan).map(toRow);
|
|
24209
|
+
return removeRows(rows);
|
|
24210
|
+
},
|
|
24211
|
+
async pruneToCap(cap = BODY_ROW_CAP) {
|
|
24212
|
+
const total = db.query("SELECT COUNT(*) AS n FROM request_bodies").get()?.n ?? 0;
|
|
24213
|
+
if (total <= cap)
|
|
24214
|
+
return 0;
|
|
24215
|
+
const rows = db.query("SELECT * FROM request_bodies ORDER BY at ASC, request_id ASC LIMIT ?").all(total - cap).map(toRow);
|
|
24216
|
+
return removeRows(rows);
|
|
24217
|
+
},
|
|
24218
|
+
async sweepOrphans() {
|
|
24219
|
+
const known = new Set(db.query("SELECT rel_path FROM request_bodies WHERE rel_path IS NOT NULL").all().map((r) => r.rel_path));
|
|
24220
|
+
const claimed = db.query("SELECT COUNT(*) AS n FROM request_bodies WHERE rel_path = ?");
|
|
24221
|
+
let removed = 0;
|
|
24222
|
+
for (const rel of await listArtifacts(dir)) {
|
|
24223
|
+
if (known.has(rel))
|
|
24224
|
+
continue;
|
|
24225
|
+
if ((claimed.get(rel)?.n ?? 0) > 0)
|
|
24226
|
+
continue;
|
|
24227
|
+
await deleteArtifact(dir, rel);
|
|
24228
|
+
removed += 1;
|
|
24229
|
+
}
|
|
24230
|
+
return removed;
|
|
24231
|
+
}
|
|
24232
|
+
};
|
|
24233
|
+
}
|
|
23789
24234
|
// packages/store/src/sqlite/config.ts
|
|
23790
24235
|
var SETTINGS_KEY = "settings";
|
|
23791
24236
|
var ADMIN_HASH_KEY = "adminPasswordHash";
|
|
@@ -23841,6 +24286,8 @@ function createConfigRepo(db, emit = () => {}) {
|
|
|
23841
24286
|
...DEFAULT_SETTINGS,
|
|
23842
24287
|
...stored,
|
|
23843
24288
|
rtkEnabled: stored.rtkEnabled === true,
|
|
24289
|
+
bodyLoggingEnabled: stored.bodyLoggingEnabled === true,
|
|
24290
|
+
bodyLoggingCaptureStreamChunks: stored.bodyLoggingCaptureStreamChunks === true,
|
|
23844
24291
|
weights: knownWeights(stored.weights)
|
|
23845
24292
|
};
|
|
23846
24293
|
} catch {
|
|
@@ -23871,6 +24318,29 @@ function createConfigRepo(db, emit = () => {}) {
|
|
|
23871
24318
|
};
|
|
23872
24319
|
}
|
|
23873
24320
|
// packages/store/src/sqlite/credentials.ts
|
|
24321
|
+
function toHealth(r) {
|
|
24322
|
+
return {
|
|
24323
|
+
credentialId: r.credential_id,
|
|
24324
|
+
model: r.model,
|
|
24325
|
+
breakerState: r.breaker_state,
|
|
24326
|
+
consecutiveFailures: r.consecutive_failures,
|
|
24327
|
+
openedAt: r.opened_at,
|
|
24328
|
+
rateLimitedUntil: r.rate_limited_until,
|
|
24329
|
+
ewmaTtftMs: r.ewma_ttft_ms,
|
|
24330
|
+
lastUsedAt: r.last_used_at
|
|
24331
|
+
};
|
|
24332
|
+
}
|
|
24333
|
+
var UPSERT_HEALTH = `INSERT INTO credential_health
|
|
24334
|
+
(credential_id, model, breaker_state, consecutive_failures, opened_at,
|
|
24335
|
+
rate_limited_until, ewma_ttft_ms, last_used_at)
|
|
24336
|
+
VALUES (?,?,?,?,?,?,?,?)
|
|
24337
|
+
ON CONFLICT (credential_id, model) DO UPDATE SET
|
|
24338
|
+
breaker_state = excluded.breaker_state,
|
|
24339
|
+
consecutive_failures = excluded.consecutive_failures,
|
|
24340
|
+
opened_at = excluded.opened_at,
|
|
24341
|
+
rate_limited_until = excluded.rate_limited_until,
|
|
24342
|
+
ewma_ttft_ms = excluded.ewma_ttft_ms,
|
|
24343
|
+
last_used_at = excluded.last_used_at`;
|
|
23874
24344
|
function createCredentialRepo(db, key, emit = () => {}) {
|
|
23875
24345
|
const open = async (v) => v === null ? null : decrypt(key, v);
|
|
23876
24346
|
const secretsFrom = async (row) => ({
|
|
@@ -24045,29 +24515,10 @@ function createCredentialRepo(db, key, emit = () => {}) {
|
|
|
24045
24515
|
emit({ type: "credentialsChanged" });
|
|
24046
24516
|
},
|
|
24047
24517
|
async listHealth() {
|
|
24048
|
-
return db.query("SELECT * FROM credential_health").all().map(
|
|
24049
|
-
credentialId: r.credential_id,
|
|
24050
|
-
model: r.model,
|
|
24051
|
-
breakerState: r.breaker_state,
|
|
24052
|
-
consecutiveFailures: r.consecutive_failures,
|
|
24053
|
-
openedAt: r.opened_at,
|
|
24054
|
-
rateLimitedUntil: r.rate_limited_until,
|
|
24055
|
-
ewmaTtftMs: r.ewma_ttft_ms,
|
|
24056
|
-
lastUsedAt: r.last_used_at
|
|
24057
|
-
}));
|
|
24518
|
+
return db.query("SELECT * FROM credential_health").all().map(toHealth);
|
|
24058
24519
|
},
|
|
24059
24520
|
async saveHealth(rows) {
|
|
24060
|
-
const stmt = db.prepare(
|
|
24061
|
-
(credential_id, model, breaker_state, consecutive_failures, opened_at,
|
|
24062
|
-
rate_limited_until, ewma_ttft_ms, last_used_at)
|
|
24063
|
-
VALUES (?,?,?,?,?,?,?,?)
|
|
24064
|
-
ON CONFLICT (credential_id, model) DO UPDATE SET
|
|
24065
|
-
breaker_state = excluded.breaker_state,
|
|
24066
|
-
consecutive_failures = excluded.consecutive_failures,
|
|
24067
|
-
opened_at = excluded.opened_at,
|
|
24068
|
-
rate_limited_until = excluded.rate_limited_until,
|
|
24069
|
-
ewma_ttft_ms = excluded.ewma_ttft_ms,
|
|
24070
|
-
last_used_at = excluded.last_used_at`);
|
|
24521
|
+
const stmt = db.prepare(UPSERT_HEALTH);
|
|
24071
24522
|
db.transaction(() => {
|
|
24072
24523
|
for (const r of rows) {
|
|
24073
24524
|
stmt.run(r.credentialId, r.model, r.breakerState, r.consecutiveFailures, r.openedAt, r.rateLimitedUntil, r.ewmaTtftMs, r.lastUsedAt);
|
|
@@ -24075,6 +24526,18 @@ function createCredentialRepo(db, key, emit = () => {}) {
|
|
|
24075
24526
|
})();
|
|
24076
24527
|
emit({ type: "healthSaved", rows });
|
|
24077
24528
|
},
|
|
24529
|
+
async updateHealth(credentialId, model, apply) {
|
|
24530
|
+
const current = db.prepare("SELECT * FROM credential_health WHERE credential_id = ? AND model = ?");
|
|
24531
|
+
const stmt = db.prepare(UPSERT_HEALTH);
|
|
24532
|
+
const written = db.transaction(() => {
|
|
24533
|
+
const row = current.get(credentialId, model);
|
|
24534
|
+
const next = apply(row === null ? null : toHealth(row));
|
|
24535
|
+
stmt.run(next.credentialId, next.model, next.breakerState, next.consecutiveFailures, next.openedAt, next.rateLimitedUntil, next.ewmaTtftMs, next.lastUsedAt);
|
|
24536
|
+
return next;
|
|
24537
|
+
})();
|
|
24538
|
+
emit({ type: "healthSaved", rows: [written] });
|
|
24539
|
+
return written;
|
|
24540
|
+
},
|
|
24078
24541
|
async listQuota() {
|
|
24079
24542
|
return db.query("SELECT * FROM quota_windows").all().map((r) => ({
|
|
24080
24543
|
credentialId: r.credential_id,
|
|
@@ -24386,7 +24849,186 @@ CREATE INDEX quota_samples_observed ON quota_samples (observed_at);
|
|
|
24386
24849
|
ALTER TABLE quota_windows ADD COLUMN window_ms INTEGER;
|
|
24387
24850
|
`;
|
|
24388
24851
|
|
|
24852
|
+
// packages/store/src/sqlite/migrations/008_body_logging.sql
|
|
24853
|
+
var _008_body_logging_default = `-- Captured request and response bodies, and the api key opt-out that suppresses
|
|
24854
|
+
-- them.
|
|
24855
|
+
--
|
|
24856
|
+
-- The row holds a pointer, never a body. Bodies are the largest thing this
|
|
24857
|
+
-- gateway could ever store \u2014 a single conversation with a pasted file in it
|
|
24858
|
+
-- dwarfs the whole of \`request_logs\` \u2014 and SQLite would carry every one of them
|
|
24859
|
+
-- through the same page cache, the same WAL, and the same backup that the
|
|
24860
|
+
-- routing tables live in. A prompt corpus inlined here would make the operating
|
|
24861
|
+
-- database grow without bound, make every \`VACUUM\` copy it, and put plaintext
|
|
24862
|
+
-- rows one \`sqlite3\` invocation away. The artifact instead lives at
|
|
24863
|
+
-- \`<dirname(databasePath)>/request_bodies/YYYY/MM/DD/<request_id>.json.enc\`,
|
|
24864
|
+
-- encrypted under the same key as provider credentials, and this row records
|
|
24865
|
+
-- only where it is and whether it can still be trusted. OmniRoute reached the
|
|
24866
|
+
-- same layout the expensive way: its \`call_logs_v1_legacy\` table exists purely
|
|
24867
|
+
-- to drain inline blobs it had already shipped into file artifacts.
|
|
24868
|
+
--
|
|
24869
|
+
-- \`rel_path\` is relative so moving an installation does not invalidate every
|
|
24870
|
+
-- row, and nullable because a row can exist with no artifact behind it: capture
|
|
24871
|
+
-- may have been suppressed, or the file may have been swept.
|
|
24872
|
+
--
|
|
24873
|
+
-- \`sha256\` is taken over the bytes as stored \u2014 the ciphertext \u2014 not over the
|
|
24874
|
+
-- plaintext. Truncation or bit-rot on disk is then detectable by a reader that
|
|
24875
|
+
-- does not hold \`OMNI_ENCRYPTION_KEY\` at all, which is what makes \`corrupt\` a
|
|
24876
|
+
-- state the reader can report rather than an exception it has to raise.
|
|
24877
|
+
--
|
|
24878
|
+
-- \`detail_state\` is one of \`none\`, \`ready\`, \`missing\`, or \`corrupt\`. A file tree
|
|
24879
|
+
-- and a table it is not written to transactionally will drift \u2014 a crash between
|
|
24880
|
+
-- the two writes is enough \u2014 so the reader records what it observed and hands
|
|
24881
|
+
-- back the metadata. \`missing\` and \`corrupt\` are answers, not failures.
|
|
24882
|
+
--
|
|
24883
|
+
-- \`truncated\` is set when structural bounding altered anything, so a reader can
|
|
24884
|
+
-- say "this is not the whole payload" without diffing it against nothing.
|
|
24885
|
+
--
|
|
24886
|
+
-- No foreign key to \`request_logs\`. Expiry is performed explicitly, deleting the
|
|
24887
|
+
-- file and the row together, because \`ON DELETE CASCADE\` only fires while the
|
|
24888
|
+
-- \`foreign_keys\` pragma is on: a pragma silently off would turn expiry of a
|
|
24889
|
+
-- prompt corpus into indefinite retention of one, and that failure is invisible
|
|
24890
|
+
-- until someone goes looking.
|
|
24891
|
+
CREATE TABLE request_bodies (
|
|
24892
|
+
request_id TEXT PRIMARY KEY,
|
|
24893
|
+
at INTEGER NOT NULL,
|
|
24894
|
+
rel_path TEXT,
|
|
24895
|
+
size_bytes INTEGER NOT NULL DEFAULT 0,
|
|
24896
|
+
sha256 TEXT,
|
|
24897
|
+
detail_state TEXT NOT NULL DEFAULT 'none',
|
|
24898
|
+
truncated INTEGER NOT NULL DEFAULT 0
|
|
24899
|
+
);
|
|
24900
|
+
|
|
24901
|
+
-- Descending, because every read of this table is time-ordered from the newest
|
|
24902
|
+
-- end: the retention sweep walks the oldest rows and the row cap walks them in
|
|
24903
|
+
-- the same direction, and the console only ever asks about recent requests.
|
|
24904
|
+
CREATE INDEX idx_request_bodies_at ON request_bodies (at DESC);
|
|
24905
|
+
|
|
24906
|
+
-- Per-key suppression, checked before any capture work begins.
|
|
24907
|
+
--
|
|
24908
|
+
-- A shared installation can be serving one client whose payloads must not be
|
|
24909
|
+
-- retained while capturing everything else, and that client's operator cannot be
|
|
24910
|
+
-- asked to trust a global switch they do not control. Default 0 keeps every
|
|
24911
|
+
-- existing key on the installation-wide setting, which is itself off by default.
|
|
24912
|
+
ALTER TABLE api_keys ADD COLUMN body_logging_opt_out INTEGER NOT NULL DEFAULT 0;
|
|
24913
|
+
`;
|
|
24914
|
+
|
|
24915
|
+
// packages/store/src/sqlite/migrations/009_key_limits.sql
|
|
24916
|
+
var _009_key_limits_default = `-- Per-key limits as a sparse \`(dimension, window)\` matrix, replacing the single
|
|
24917
|
+
-- requests-per-minute integer.
|
|
24918
|
+
--
|
|
24919
|
+
-- One JSON column rather than a column per pair, because the matrix is sparse
|
|
24920
|
+
-- and the pairs are not fixed: \`requests\` and \`tokens\` are meaningful at all
|
|
24921
|
+
-- three windows, \`spend\` only at the two long ones, and \`concurrency\` is a gauge
|
|
24922
|
+
-- with no window at all. Twelve mostly-null integer columns would encode the
|
|
24923
|
+
-- same thing while making every future pair a migration.
|
|
24924
|
+
--
|
|
24925
|
+
-- The JSON keys are a storage contract in the same class as \`RTK_FILTER_IDS\`:
|
|
24926
|
+
-- the dimension and window names are persisted in every row, so adding a name is
|
|
24927
|
+
-- free and renaming or removing one loses data. Unlike RTK ids, which are
|
|
24928
|
+
-- dropped silently on read, an unknown limit key is a parse failure \u2014 a limit
|
|
24929
|
+
-- the gateway cannot understand must never be read as "no limit", because that
|
|
24930
|
+
-- fails open on a control the operator explicitly set.
|
|
24931
|
+
--
|
|
24932
|
+
-- \`NOT NULL DEFAULT '{}'\` rather than nullable, so "unlimited" has exactly one
|
|
24933
|
+
-- spelling at the column level and every reader parses the same shape.
|
|
24934
|
+
ALTER TABLE api_keys ADD COLUMN limits TEXT NOT NULL DEFAULT '{}';
|
|
24935
|
+
|
|
24936
|
+
-- The old ceiling was requests per minute and nothing else, so it lands whole in
|
|
24937
|
+
-- \`requests["1m"]\`. A NULL meant unlimited and stays unlimited as the default
|
|
24938
|
+
-- \`{}\`: an absent key and an explicit null both mean the same thing, and the
|
|
24939
|
+
-- empty object is the shape a newly minted key starts at.
|
|
24940
|
+
--
|
|
24941
|
+
-- Only a value the reader accepts is carried over. \`rate_limit_per_min\` was
|
|
24942
|
+
-- \`INTEGER\` with no \`CHECK\`, so a hand-edited install can hold \`0\`, \`-5\` or
|
|
24943
|
+
-- \`1.5\` in it, while the new schema is \`z.number().int().positive()\`. Backfilled
|
|
24944
|
+
-- unconditionally, each of those writes a matrix \`parseLimitConfig\` refuses \u2014
|
|
24945
|
+
-- which is \`limits: null\`, which is every request for that key answered
|
|
24946
|
+
-- \`INTERNAL\` at the auth chokepoint. An upgrade must not manufacture the
|
|
24947
|
+
-- unreadable row the design describes as arising only from meddling.
|
|
24948
|
+
--
|
|
24949
|
+
-- Anything else stays at the \`{}\` default, which is unlimited: a ceiling that
|
|
24950
|
+
-- was already nonsense bounded nothing before this migration either, so
|
|
24951
|
+
-- dropping it changes no behaviour and leaves the key serving.
|
|
24952
|
+
--
|
|
24953
|
+
-- \`typeof()\` rather than a range test alone. SQLite's INTEGER affinity is a
|
|
24954
|
+
-- preference, not a constraint: \`1.5\` stays REAL and \`'sixty'\` stays TEXT, and
|
|
24955
|
+
-- TEXT sorts above every number so a bare \`> 0\` would admit the string.
|
|
24956
|
+
UPDATE api_keys
|
|
24957
|
+
SET limits = json_object('requests', json_object('1m', rate_limit_per_min))
|
|
24958
|
+
WHERE typeof(rate_limit_per_min) = 'integer'
|
|
24959
|
+
AND rate_limit_per_min > 0;
|
|
24960
|
+
|
|
24961
|
+
ALTER TABLE api_keys DROP COLUMN rate_limit_per_min;
|
|
24962
|
+
|
|
24963
|
+
-- Correctness-adjacent, not an optimisation, and it must not be dropped later as
|
|
24964
|
+
-- redundant with \`idx_request_logs_at\`.
|
|
24965
|
+
--
|
|
24966
|
+
-- \`request_logs\` leads its existing indexes with \`at\` and with \`credential_id\`;
|
|
24967
|
+
-- nothing leads with \`api_key_id\`. A sliding weekly sum for one key against
|
|
24968
|
+
-- \`idx_request_logs_at\` scans every row in the week for every key on the
|
|
24969
|
+
-- install, and that scan sits on the request hot path.
|
|
24970
|
+
--
|
|
24971
|
+
-- Composite order matters: \`(api_key_id, at DESC)\` lets the range scan start at
|
|
24972
|
+
-- the key. \`(at DESC, api_key_id)\` does not.
|
|
24973
|
+
CREATE INDEX idx_request_logs_key_at ON request_logs (api_key_id, at DESC);
|
|
24974
|
+
`;
|
|
24975
|
+
|
|
24976
|
+
// packages/store/src/sqlite/migrations/010_usage_rollup.sql
|
|
24977
|
+
var _010_usage_rollup_default = `-- Per-key hourly counters, so a long sliding window is read rather than scanned.
|
|
24978
|
+
--
|
|
24979
|
+
-- \`sumSince\` is on the admission path of every request, and \`bun:sqlite\` is
|
|
24980
|
+
-- synchronous: a \`SELECT SUM\` over a week of one key's rows blocks the entire
|
|
24981
|
+
-- event loop for its duration \u2014 \`/health\`, \`/api/*\`, the quiesce latch, and
|
|
24982
|
+
-- every other key's traffic with it. Measured on one machine, one key, WAL:
|
|
24983
|
+
-- 149ms at 0.2M rows in the window, 2.1s at 2M, 10.8s at 8M. The cost is
|
|
24984
|
+
-- O(accumulated history) and has no ceiling, and the eager-refresh rule makes it
|
|
24985
|
+
-- worst exactly when a key is busiest \u2014 a key inside the last tenth of a long
|
|
24986
|
+
-- ceiling reads through on every admission. The same reads against this table
|
|
24987
|
+
-- are flat at ~0.017ms, because a week is 168 buckets however much traffic each
|
|
24988
|
+
-- one summarises.
|
|
24989
|
+
--
|
|
24990
|
+
-- This reverses the design's own rejection of a counter table, and the reason it
|
|
24991
|
+
-- can be reversed is that this table is *derived*. \`request_logs\` stays the
|
|
24992
|
+
-- source of truth; every figure here is reproducible from it by one grouped
|
|
24993
|
+
-- select. The original objection was that a counter table can disagree with the
|
|
24994
|
+
-- log and nobody can tell which is right \u2014 that objection is answered by there
|
|
24995
|
+
-- being a rebuild, a restore that runs it, and a \`doctor\` check that compares
|
|
24996
|
+
-- the two. It would not be answered by care.
|
|
24997
|
+
--
|
|
24998
|
+
-- \`hour\` is \`at / 3600000\`, floored, and every SQL side spells that
|
|
24999
|
+
-- \`CAST(at / 3600000 AS INTEGER)\`. The cast is not decoration: nothing validates
|
|
25000
|
+
-- \`at\`, and SQLite's \`/\` is integer division only when both operands are
|
|
25001
|
+
-- integers, so a fractional \`at\` would give this INTEGER column a REAL key that
|
|
25002
|
+
-- no later integer-hour write ever merges with. Truncation toward zero and
|
|
25003
|
+
-- JavaScript's \`Math.floor\` then agree on every epoch this side of 1970, which
|
|
25004
|
+
-- is every epoch a request log holds.
|
|
25005
|
+
--
|
|
25006
|
+
-- \`api_key_id\` is NOT NULL and anonymous rows are left out: a WITHOUT ROWID
|
|
25007
|
+
-- primary key cannot hold a NULL, and nothing reads this table except a per-key
|
|
25008
|
+
-- lookup that could never match one.
|
|
25009
|
+
CREATE TABLE usage_rollup (
|
|
25010
|
+
api_key_id TEXT NOT NULL,
|
|
25011
|
+
hour INTEGER NOT NULL,
|
|
25012
|
+
requests INTEGER NOT NULL DEFAULT 0,
|
|
25013
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
25014
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
25015
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
25016
|
+
cache_write_tokens INTEGER NOT NULL DEFAULT 0,
|
|
25017
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
25018
|
+
PRIMARY KEY (api_key_id, hour)
|
|
25019
|
+
) WITHOUT ROWID;
|
|
25020
|
+
|
|
25021
|
+
-- The backfill is this migration's \`after\` hook rather than an INSERT here, and
|
|
25022
|
+
-- it is the same \`rebuildRollup\` a restore runs. Seeding an existing install and
|
|
25023
|
+
-- repairing a suspect one are the same statement, so they are the same code and
|
|
25024
|
+
-- cannot drift into disagreeing about which rows count.
|
|
25025
|
+
`;
|
|
25026
|
+
|
|
24389
25027
|
// packages/store/src/sqlite/rollup.ts
|
|
25028
|
+
var HOUR_MS = 3600000;
|
|
25029
|
+
function hourOf(at) {
|
|
25030
|
+
return Math.floor(at / HOUR_MS);
|
|
25031
|
+
}
|
|
24390
25032
|
function startOfLocalDay(at) {
|
|
24391
25033
|
const day = new Date(at);
|
|
24392
25034
|
day.setHours(0, 0, 0, 0);
|
|
@@ -24495,6 +25137,88 @@ function backfillDaily(db) {
|
|
|
24495
25137
|
upsert(db, group.key, group.counters);
|
|
24496
25138
|
return groups.size;
|
|
24497
25139
|
}
|
|
25140
|
+
var HOURLY_UPSERT = `
|
|
25141
|
+
INSERT INTO usage_rollup
|
|
25142
|
+
(api_key_id, hour, requests, input_tokens, output_tokens,
|
|
25143
|
+
cache_read_tokens, cache_write_tokens, cost_usd)
|
|
25144
|
+
VALUES (?,?,?,?,?,?,?,?)
|
|
25145
|
+
ON CONFLICT (api_key_id, hour)
|
|
25146
|
+
DO UPDATE SET
|
|
25147
|
+
requests = requests + excluded.requests,
|
|
25148
|
+
input_tokens = input_tokens + excluded.input_tokens,
|
|
25149
|
+
output_tokens = output_tokens + excluded.output_tokens,
|
|
25150
|
+
cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
|
|
25151
|
+
cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
|
|
25152
|
+
cost_usd = cost_usd + excluded.cost_usd`;
|
|
25153
|
+
function rollupHour(db, log) {
|
|
25154
|
+
const apiKeyId = log.apiKeyId;
|
|
25155
|
+
if (apiKeyId === null)
|
|
25156
|
+
return;
|
|
25157
|
+
db.run(HOURLY_UPSERT, [
|
|
25158
|
+
apiKeyId,
|
|
25159
|
+
hourOf(log.at),
|
|
25160
|
+
1,
|
|
25161
|
+
log.inputTokens,
|
|
25162
|
+
log.outputTokens,
|
|
25163
|
+
log.cacheReadTokens,
|
|
25164
|
+
log.cacheWriteTokens,
|
|
25165
|
+
log.costUsd
|
|
25166
|
+
]);
|
|
25167
|
+
}
|
|
25168
|
+
var REBUILD = `
|
|
25169
|
+
INSERT INTO usage_rollup
|
|
25170
|
+
(api_key_id, hour, requests, input_tokens, output_tokens,
|
|
25171
|
+
cache_read_tokens, cache_write_tokens, cost_usd)
|
|
25172
|
+
SELECT api_key_id,
|
|
25173
|
+
CAST(at / 3600000 AS INTEGER),
|
|
25174
|
+
COUNT(*),
|
|
25175
|
+
COALESCE(SUM(input_tokens), 0),
|
|
25176
|
+
COALESCE(SUM(output_tokens), 0),
|
|
25177
|
+
COALESCE(SUM(cache_read_tokens), 0),
|
|
25178
|
+
COALESCE(SUM(cache_write_tokens), 0),
|
|
25179
|
+
COALESCE(SUM(cost_usd), 0)
|
|
25180
|
+
FROM request_logs
|
|
25181
|
+
WHERE state = 'done' AND api_key_id IS NOT NULL
|
|
25182
|
+
GROUP BY api_key_id, CAST(at / 3600000 AS INTEGER)`;
|
|
25183
|
+
function rebuildRollup(db) {
|
|
25184
|
+
db.run("DELETE FROM usage_rollup");
|
|
25185
|
+
db.run(REBUILD);
|
|
25186
|
+
}
|
|
25187
|
+
function auditRollup(db) {
|
|
25188
|
+
const row = db.query(`WITH truth AS (
|
|
25189
|
+
SELECT api_key_id AS k,
|
|
25190
|
+
CAST(at / 3600000 AS INTEGER) AS h,
|
|
25191
|
+
COUNT(*) AS requests,
|
|
25192
|
+
COALESCE(SUM(input_tokens), 0) AS input_tokens,
|
|
25193
|
+
COALESCE(SUM(output_tokens), 0) AS output_tokens,
|
|
25194
|
+
COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
|
|
25195
|
+
COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,
|
|
25196
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd
|
|
25197
|
+
FROM request_logs
|
|
25198
|
+
WHERE state = 'done' AND api_key_id IS NOT NULL
|
|
25199
|
+
GROUP BY k, h
|
|
25200
|
+
)
|
|
25201
|
+
SELECT (SELECT COUNT(*) FROM truth) AS buckets,
|
|
25202
|
+
(SELECT COUNT(*)
|
|
25203
|
+
FROM truth
|
|
25204
|
+
LEFT JOIN usage_rollup r ON r.api_key_id = truth.k AND r.hour = truth.h
|
|
25205
|
+
WHERE r.api_key_id IS NULL
|
|
25206
|
+
OR r.requests <> truth.requests
|
|
25207
|
+
OR r.input_tokens <> truth.input_tokens
|
|
25208
|
+
OR r.output_tokens <> truth.output_tokens
|
|
25209
|
+
OR r.cache_read_tokens <> truth.cache_read_tokens
|
|
25210
|
+
OR r.cache_write_tokens <> truth.cache_write_tokens
|
|
25211
|
+
OR ABS(r.cost_usd - truth.cost_usd) > 1e-9 + ABS(truth.cost_usd) * 1e-9
|
|
25212
|
+
) AS mismatched,
|
|
25213
|
+
(SELECT COUNT(*)
|
|
25214
|
+
FROM usage_rollup r
|
|
25215
|
+
LEFT JOIN truth ON r.api_key_id = truth.k AND r.hour = truth.h
|
|
25216
|
+
WHERE truth.k IS NULL
|
|
25217
|
+
) AS orphans`).get();
|
|
25218
|
+
const buckets = row?.buckets ?? 0;
|
|
25219
|
+
const mismatched = (row?.mismatched ?? 0) + (row?.orphans ?? 0);
|
|
25220
|
+
return { buckets, mismatched, ok: mismatched === 0 };
|
|
25221
|
+
}
|
|
24498
25222
|
function backfillRtkUsage(db) {
|
|
24499
25223
|
const groups = new Map;
|
|
24500
25224
|
for (const row of db.query(`SELECT at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
|
|
@@ -24531,7 +25255,10 @@ var MIGRATIONS = [
|
|
|
24531
25255
|
{ id: 4, sql: _004_request_state_default },
|
|
24532
25256
|
{ id: 5, sql: _005_rtk_metrics_default },
|
|
24533
25257
|
{ id: 6, sql: _006_rtk_usage_default, after: backfillRtkUsage },
|
|
24534
|
-
{ id: 7, sql: _007_quota_samples_default }
|
|
25258
|
+
{ id: 7, sql: _007_quota_samples_default },
|
|
25259
|
+
{ id: 8, sql: _008_body_logging_default },
|
|
25260
|
+
{ id: 9, sql: _009_key_limits_default },
|
|
25261
|
+
{ id: 10, sql: _010_usage_rollup_default, after: rebuildRollup }
|
|
24535
25262
|
];
|
|
24536
25263
|
function openDb(path) {
|
|
24537
25264
|
const db = new Database(path, { create: true });
|
|
@@ -24562,44 +25289,128 @@ async function hashApiKey(raw) {
|
|
|
24562
25289
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(raw));
|
|
24563
25290
|
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
24564
25291
|
}
|
|
24565
|
-
|
|
25292
|
+
function parseLimits(id, raw, logger2) {
|
|
25293
|
+
try {
|
|
25294
|
+
const parsed = JSON.parse(raw);
|
|
25295
|
+
return parseLimitConfig(parsed);
|
|
25296
|
+
} catch {
|
|
25297
|
+
logger2.error("api key limits unreadable", { apiKeyId: id });
|
|
25298
|
+
return null;
|
|
25299
|
+
}
|
|
25300
|
+
}
|
|
25301
|
+
var toKey = (r, logger2) => ({
|
|
24566
25302
|
id: r.id,
|
|
24567
25303
|
label: r.label,
|
|
24568
25304
|
prefix: r.prefix,
|
|
24569
25305
|
hash: r.hash,
|
|
24570
25306
|
modelAllowlist: r.model_allowlist === null ? null : JSON.parse(r.model_allowlist),
|
|
24571
|
-
|
|
25307
|
+
limits: parseLimits(r.id, r.limits, logger2),
|
|
25308
|
+
bodyLoggingOptOut: r.body_logging_opt_out === 1,
|
|
24572
25309
|
createdAt: r.created_at,
|
|
24573
25310
|
revokedAt: r.revoked_at
|
|
24574
25311
|
});
|
|
24575
|
-
function createKeyRepo(db) {
|
|
25312
|
+
function createKeyRepo(db, logger2 = noopLogger) {
|
|
24576
25313
|
return {
|
|
24577
25314
|
async list() {
|
|
24578
|
-
return db.query("SELECT * FROM api_keys ORDER BY created_at DESC").all().map(toKey);
|
|
25315
|
+
return db.query("SELECT * FROM api_keys ORDER BY created_at DESC").all().map((row) => toKey(row, logger2));
|
|
24579
25316
|
},
|
|
24580
25317
|
async findByHash(hash3) {
|
|
24581
25318
|
const row = db.query("SELECT * FROM api_keys WHERE hash = ?").get(hash3);
|
|
24582
|
-
return row ? toKey(row) : null;
|
|
25319
|
+
return row ? toKey(row, logger2) : null;
|
|
24583
25320
|
},
|
|
24584
25321
|
async create(input) {
|
|
24585
25322
|
const now = Date.now();
|
|
24586
|
-
db.run(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist,
|
|
24587
|
-
|
|
25323
|
+
db.run(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
|
|
25324
|
+
body_logging_opt_out, created_at, revoked_at)
|
|
25325
|
+
VALUES (?,?,?,?,?,?,?,?,NULL)`, [
|
|
24588
25326
|
input.id,
|
|
24589
25327
|
input.label,
|
|
24590
25328
|
input.prefix,
|
|
24591
25329
|
input.hash,
|
|
24592
25330
|
input.modelAllowlist === null ? null : JSON.stringify(input.modelAllowlist),
|
|
24593
|
-
input.
|
|
25331
|
+
JSON.stringify(parseLimitConfig(input.limits)),
|
|
25332
|
+
input.bodyLoggingOptOut ? 1 : 0,
|
|
24594
25333
|
now
|
|
24595
25334
|
]);
|
|
24596
25335
|
return { ...input, createdAt: now, revokedAt: null };
|
|
24597
25336
|
},
|
|
25337
|
+
async setLimits(id, limits) {
|
|
25338
|
+
db.run("UPDATE api_keys SET limits = ? WHERE id = ?", [
|
|
25339
|
+
JSON.stringify(parseLimitConfig(limits)),
|
|
25340
|
+
id
|
|
25341
|
+
]);
|
|
25342
|
+
},
|
|
24598
25343
|
async revoke(id) {
|
|
24599
25344
|
db.run("UPDATE api_keys SET revoked_at = ? WHERE id = ?", [Date.now(), id]);
|
|
24600
25345
|
}
|
|
24601
25346
|
};
|
|
24602
25347
|
}
|
|
25348
|
+
// packages/store/src/sqlite/maintenance.ts
|
|
25349
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
25350
|
+
var REQUIRED_TABLES = [
|
|
25351
|
+
"api_keys",
|
|
25352
|
+
"credential_health",
|
|
25353
|
+
"credentials",
|
|
25354
|
+
"migrations",
|
|
25355
|
+
"quota_windows",
|
|
25356
|
+
"request_logs",
|
|
25357
|
+
"settings",
|
|
25358
|
+
"virtual_models"
|
|
25359
|
+
];
|
|
25360
|
+
var UNREADABLE = "unreadable";
|
|
25361
|
+
function sqlLiteral(value) {
|
|
25362
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
25363
|
+
}
|
|
25364
|
+
function createMaintenanceRepo(db) {
|
|
25365
|
+
return {
|
|
25366
|
+
async stats() {
|
|
25367
|
+
const pageSize = db.query("PRAGMA page_size").get();
|
|
25368
|
+
const pageCount = db.query("PRAGMA page_count").get();
|
|
25369
|
+
const freelist = db.query("PRAGMA freelist_count").get();
|
|
25370
|
+
const version2 = db.query("SELECT MAX(id) AS version FROM migrations").get();
|
|
25371
|
+
return {
|
|
25372
|
+
pageSize: pageSize?.page_size ?? 0,
|
|
25373
|
+
pageCount: pageCount?.page_count ?? 0,
|
|
25374
|
+
freelistCount: freelist?.freelist_count ?? 0,
|
|
25375
|
+
schemaVersion: version2?.version ?? 0
|
|
25376
|
+
};
|
|
25377
|
+
},
|
|
25378
|
+
async vacuum() {
|
|
25379
|
+
db.run("VACUUM");
|
|
25380
|
+
db.run("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
25381
|
+
},
|
|
25382
|
+
async snapshotTo(path) {
|
|
25383
|
+
db.run(`VACUUM INTO ${sqlLiteral(path)}`);
|
|
25384
|
+
},
|
|
25385
|
+
async inspect(path) {
|
|
25386
|
+
let file2;
|
|
25387
|
+
try {
|
|
25388
|
+
file2 = new Database2(path, { readonly: true });
|
|
25389
|
+
} catch {
|
|
25390
|
+
return { ok: false, quickCheck: UNREADABLE, tables: [], counts: {} };
|
|
25391
|
+
}
|
|
25392
|
+
try {
|
|
25393
|
+
const problems = file2.query("PRAGMA quick_check").all();
|
|
25394
|
+
const quickCheck = problems[0]?.quick_check ?? UNREADABLE;
|
|
25395
|
+
const tables = file2.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' " + "ORDER BY name").all().map((r) => r.name);
|
|
25396
|
+
const present = new Set(tables);
|
|
25397
|
+
const counts = {};
|
|
25398
|
+
for (const table of REQUIRED_TABLES) {
|
|
25399
|
+
if (!present.has(table))
|
|
25400
|
+
continue;
|
|
25401
|
+
const n = file2.query(`SELECT COUNT(*) AS n FROM ${table}`).get();
|
|
25402
|
+
counts[table] = n?.n ?? 0;
|
|
25403
|
+
}
|
|
25404
|
+
const complete = REQUIRED_TABLES.every((t) => present.has(t));
|
|
25405
|
+
return { ok: quickCheck === "ok" && complete, quickCheck, tables, counts };
|
|
25406
|
+
} catch {
|
|
25407
|
+
return { ok: false, quickCheck: UNREADABLE, tables: [], counts: {} };
|
|
25408
|
+
} finally {
|
|
25409
|
+
file2.close();
|
|
25410
|
+
}
|
|
25411
|
+
}
|
|
25412
|
+
};
|
|
25413
|
+
}
|
|
24603
25414
|
// packages/rtk/src/catalog.ts
|
|
24604
25415
|
var RTK_FILTER_IDS = [
|
|
24605
25416
|
"git-diff",
|
|
@@ -24666,7 +25477,7 @@ var GROUP_COLUMN = {
|
|
|
24666
25477
|
requestedModel: "requested_model",
|
|
24667
25478
|
apiKey: "api_key_id",
|
|
24668
25479
|
provider: "resolved_provider",
|
|
24669
|
-
hour: "at / 3600000",
|
|
25480
|
+
hour: "CAST(at / 3600000 AS INTEGER)",
|
|
24670
25481
|
day: null
|
|
24671
25482
|
},
|
|
24672
25483
|
daily: {
|
|
@@ -24763,8 +25574,30 @@ function createUsageRepo(db) {
|
|
|
24763
25574
|
const complete = db.transaction((log) => {
|
|
24764
25575
|
db.run(COMPLETE, values(log, "done"));
|
|
24765
25576
|
const stored = db.query("SELECT * FROM request_logs WHERE id = ?").get(log.id);
|
|
24766
|
-
if (stored !== null)
|
|
24767
|
-
|
|
25577
|
+
if (stored !== null) {
|
|
25578
|
+
const restored = toLog(stored);
|
|
25579
|
+
rollupLog(db, restored);
|
|
25580
|
+
rollupHour(db, restored);
|
|
25581
|
+
}
|
|
25582
|
+
});
|
|
25583
|
+
const pruneLogs = db.transaction((olderThan) => {
|
|
25584
|
+
db.run("DELETE FROM request_logs WHERE at < ?", [olderThan]);
|
|
25585
|
+
const removed = db.query("SELECT changes() AS n").get()?.n ?? 0;
|
|
25586
|
+
const boundary = hourOf(olderThan);
|
|
25587
|
+
db.run("DELETE FROM usage_rollup WHERE hour <= ?", [boundary]);
|
|
25588
|
+
db.run(`INSERT INTO usage_rollup
|
|
25589
|
+
(api_key_id, hour, requests, input_tokens, output_tokens,
|
|
25590
|
+
cache_read_tokens, cache_write_tokens, cost_usd)
|
|
25591
|
+
SELECT api_key_id, ?, COUNT(*),
|
|
25592
|
+
COALESCE(SUM(input_tokens), 0),
|
|
25593
|
+
COALESCE(SUM(output_tokens), 0),
|
|
25594
|
+
COALESCE(SUM(cache_read_tokens), 0),
|
|
25595
|
+
COALESCE(SUM(cache_write_tokens), 0),
|
|
25596
|
+
COALESCE(SUM(cost_usd), 0)
|
|
25597
|
+
FROM request_logs
|
|
25598
|
+
WHERE state = 'done' AND api_key_id IS NOT NULL AND at >= ? AND at < ?
|
|
25599
|
+
GROUP BY api_key_id`, [boundary, boundary * HOUR_MS, (boundary + 1) * HOUR_MS]);
|
|
25600
|
+
return removed;
|
|
24768
25601
|
});
|
|
24769
25602
|
return {
|
|
24770
25603
|
async begin(log) {
|
|
@@ -24788,6 +25621,38 @@ function createUsageRepo(db) {
|
|
|
24788
25621
|
async recent(limit) {
|
|
24789
25622
|
return db.query("SELECT * FROM request_logs ORDER BY at DESC LIMIT ?").all(limit).map(toLog);
|
|
24790
25623
|
},
|
|
25624
|
+
async sumSince(apiKeyId, sinceMs) {
|
|
25625
|
+
const boundary = hourOf(sinceMs);
|
|
25626
|
+
const whole = db.query(`SELECT COALESCE(SUM(requests), 0) AS requests,
|
|
25627
|
+
COALESCE(SUM(input_tokens + output_tokens
|
|
25628
|
+
+ cache_read_tokens + cache_write_tokens), 0) AS tokens,
|
|
25629
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd
|
|
25630
|
+
FROM usage_rollup
|
|
25631
|
+
WHERE api_key_id = ? AND hour > ?`).get(apiKeyId, boundary);
|
|
25632
|
+
const edge = db.query(`SELECT COUNT(*) AS requests,
|
|
25633
|
+
COALESCE(SUM(input_tokens + output_tokens
|
|
25634
|
+
+ cache_read_tokens + cache_write_tokens), 0) AS tokens,
|
|
25635
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd
|
|
25636
|
+
FROM request_logs
|
|
25637
|
+
WHERE api_key_id = ? AND state = 'done' AND at >= ? AND at < ?`).get(apiKeyId, sinceMs, (boundary + 1) * HOUR_MS);
|
|
25638
|
+
return {
|
|
25639
|
+
requests: (whole?.requests ?? 0) + (edge?.requests ?? 0),
|
|
25640
|
+
tokens: (whole?.tokens ?? 0) + (edge?.tokens ?? 0),
|
|
25641
|
+
costUsd: (whole?.cost_usd ?? 0) + (edge?.cost_usd ?? 0)
|
|
25642
|
+
};
|
|
25643
|
+
},
|
|
25644
|
+
async rebuildRollup() {
|
|
25645
|
+
rebuildRollup(db);
|
|
25646
|
+
},
|
|
25647
|
+
async auditRollup() {
|
|
25648
|
+
return auditRollup(db);
|
|
25649
|
+
},
|
|
25650
|
+
async oldestSince(apiKeyId, sinceMs) {
|
|
25651
|
+
const row = db.query(`SELECT MIN(at) AS at
|
|
25652
|
+
FROM request_logs
|
|
25653
|
+
WHERE api_key_id = ? AND state = 'done' AND at >= ?`).get(apiKeyId, sinceMs);
|
|
25654
|
+
return row?.at ?? null;
|
|
25655
|
+
},
|
|
24791
25656
|
async aggregate(q) {
|
|
24792
25657
|
const grain = q.grain ?? "raw";
|
|
24793
25658
|
const daily = grain === "daily";
|
|
@@ -24828,8 +25693,7 @@ function createUsageRepo(db) {
|
|
|
24828
25693
|
});
|
|
24829
25694
|
},
|
|
24830
25695
|
async prune(olderThan) {
|
|
24831
|
-
|
|
24832
|
-
return db.query("SELECT changes() AS n").get()?.n ?? 0;
|
|
25696
|
+
return pruneLogs(olderThan);
|
|
24833
25697
|
},
|
|
24834
25698
|
async pruneDaily(olderThan) {
|
|
24835
25699
|
db.run("DELETE FROM usage_daily WHERE day < ?", [olderThan]);
|
|
@@ -24841,8 +25705,6 @@ function createUsageRepo(db) {
|
|
|
24841
25705
|
// packages/store/src/sqlite/store.ts
|
|
24842
25706
|
async function createStore(opts) {
|
|
24843
25707
|
const logger2 = opts.logger ?? noopLogger;
|
|
24844
|
-
const db = openDb(opts.path);
|
|
24845
|
-
logger2.debug("store opened", { path: opts.path });
|
|
24846
25708
|
const listeners = new Set;
|
|
24847
25709
|
const emit = (change) => {
|
|
24848
25710
|
for (const listener of listeners) {
|
|
@@ -24851,23 +25713,106 @@ async function createStore(opts) {
|
|
|
24851
25713
|
} catch {}
|
|
24852
25714
|
}
|
|
24853
25715
|
};
|
|
25716
|
+
const open = () => {
|
|
25717
|
+
const db = openDb(opts.path);
|
|
25718
|
+
logger2.debug("store opened", { path: opts.path });
|
|
25719
|
+
return {
|
|
25720
|
+
db,
|
|
25721
|
+
credentials: createCredentialRepo(db, opts.encryptionKey, emit),
|
|
25722
|
+
config: createConfigRepo(db, emit),
|
|
25723
|
+
keys: createKeyRepo(db, logger2),
|
|
25724
|
+
usage: createUsageRepo(db),
|
|
25725
|
+
bodies: createBodyRepo(db, opts.encryptionKey, bodiesDirFor(opts.path)),
|
|
25726
|
+
maintenance: createMaintenanceRepo(db)
|
|
25727
|
+
};
|
|
25728
|
+
};
|
|
25729
|
+
let handle = open();
|
|
25730
|
+
let live = true;
|
|
25731
|
+
const closeHandle = () => {
|
|
25732
|
+
if (!live)
|
|
25733
|
+
return;
|
|
25734
|
+
handle.db.close();
|
|
25735
|
+
live = false;
|
|
25736
|
+
};
|
|
24854
25737
|
return {
|
|
24855
|
-
|
|
24856
|
-
|
|
24857
|
-
|
|
24858
|
-
|
|
25738
|
+
databasePath: opts.path,
|
|
25739
|
+
credentials: {
|
|
25740
|
+
list: () => handle.credentials.list(),
|
|
25741
|
+
listRouting: () => handle.credentials.listRouting(),
|
|
25742
|
+
get: (id) => handle.credentials.get(id),
|
|
25743
|
+
create: (input) => handle.credentials.create(input),
|
|
25744
|
+
update: (id, patch) => handle.credentials.update(id, patch),
|
|
25745
|
+
updateSecrets: (id, secrets, expiresAt) => handle.credentials.updateSecrets(id, secrets, expiresAt),
|
|
25746
|
+
remove: (id) => handle.credentials.remove(id),
|
|
25747
|
+
listHealth: () => handle.credentials.listHealth(),
|
|
25748
|
+
saveHealth: (rows) => handle.credentials.saveHealth(rows),
|
|
25749
|
+
updateHealth: (id, model, apply) => handle.credentials.updateHealth(id, model, apply),
|
|
25750
|
+
listQuota: () => handle.credentials.listQuota(),
|
|
25751
|
+
saveQuota: (rows) => handle.credentials.saveQuota(rows),
|
|
25752
|
+
listQuotaSamples: (q) => handle.credentials.listQuotaSamples(q),
|
|
25753
|
+
pruneQuotaSamples: (olderThan) => handle.credentials.pruneQuotaSamples(olderThan)
|
|
25754
|
+
},
|
|
25755
|
+
config: {
|
|
25756
|
+
listModels: () => handle.config.listModels(),
|
|
25757
|
+
putModel: (model) => handle.config.putModel(model),
|
|
25758
|
+
removeModel: (id) => handle.config.removeModel(id),
|
|
25759
|
+
getSettings: () => handle.config.getSettings(),
|
|
25760
|
+
putSettings: (patch) => handle.config.putSettings(patch),
|
|
25761
|
+
getAdminPasswordHash: () => handle.config.getAdminPasswordHash(),
|
|
25762
|
+
setAdminPasswordHashIfAbsent: (hash3) => handle.config.setAdminPasswordHashIfAbsent(hash3),
|
|
25763
|
+
setAdminPasswordHash: (hash3) => handle.config.setAdminPasswordHash(hash3)
|
|
25764
|
+
},
|
|
25765
|
+
keys: {
|
|
25766
|
+
list: () => handle.keys.list(),
|
|
25767
|
+
findByHash: (hash3) => handle.keys.findByHash(hash3),
|
|
25768
|
+
create: (input) => handle.keys.create(input),
|
|
25769
|
+
setLimits: (id, limits) => handle.keys.setLimits(id, limits),
|
|
25770
|
+
revoke: (id) => handle.keys.revoke(id)
|
|
25771
|
+
},
|
|
25772
|
+
usage: {
|
|
25773
|
+
begin: (log) => handle.usage.begin(log),
|
|
25774
|
+
route: (id, target) => handle.usage.route(id, target),
|
|
25775
|
+
append: (log) => handle.usage.append(log),
|
|
25776
|
+
sweepPending: () => handle.usage.sweepPending(),
|
|
25777
|
+
recent: (limit) => handle.usage.recent(limit),
|
|
25778
|
+
aggregate: (q) => handle.usage.aggregate(q),
|
|
25779
|
+
sumSince: (apiKeyId, sinceMs) => handle.usage.sumSince(apiKeyId, sinceMs),
|
|
25780
|
+
oldestSince: (apiKeyId, sinceMs) => handle.usage.oldestSince(apiKeyId, sinceMs),
|
|
25781
|
+
rebuildRollup: () => handle.usage.rebuildRollup(),
|
|
25782
|
+
auditRollup: () => handle.usage.auditRollup(),
|
|
25783
|
+
prune: (olderThan) => handle.usage.prune(olderThan),
|
|
25784
|
+
pruneDaily: (olderThan) => handle.usage.pruneDaily(olderThan)
|
|
25785
|
+
},
|
|
25786
|
+
bodies: {
|
|
25787
|
+
put: (artifact) => handle.bodies.put(artifact),
|
|
25788
|
+
get: (requestId) => handle.bodies.get(requestId),
|
|
25789
|
+
prune: (olderThan) => handle.bodies.prune(olderThan),
|
|
25790
|
+
pruneToCap: (cap) => handle.bodies.pruneToCap(cap),
|
|
25791
|
+
sweepOrphans: () => handle.bodies.sweepOrphans()
|
|
25792
|
+
},
|
|
25793
|
+
maintenance: {
|
|
25794
|
+
stats: () => handle.maintenance.stats(),
|
|
25795
|
+
vacuum: () => handle.maintenance.vacuum(),
|
|
25796
|
+
snapshotTo: (path) => handle.maintenance.snapshotTo(path),
|
|
25797
|
+
inspect: (path) => handle.maintenance.inspect(path)
|
|
25798
|
+
},
|
|
24859
25799
|
routing: {
|
|
24860
|
-
version: () => db.query("PRAGMA data_version").get()?.data_version ?? 0,
|
|
25800
|
+
version: () => handle.db.query("PRAGMA data_version").get()?.data_version ?? 0,
|
|
24861
25801
|
subscribe(listener) {
|
|
24862
25802
|
listeners.add(listener);
|
|
24863
25803
|
return () => listeners.delete(listener);
|
|
24864
25804
|
}
|
|
24865
25805
|
},
|
|
24866
|
-
|
|
25806
|
+
async reopen() {
|
|
25807
|
+
closeHandle();
|
|
25808
|
+
handle = open();
|
|
25809
|
+
live = true;
|
|
25810
|
+
},
|
|
25811
|
+
close: closeHandle
|
|
24867
25812
|
};
|
|
24868
25813
|
}
|
|
24869
25814
|
// packages/control/src/quota/burn.ts
|
|
24870
|
-
var
|
|
25815
|
+
var HOUR_MS2 = 3600000;
|
|
24871
25816
|
function windowStartOf(window2) {
|
|
24872
25817
|
if (window2.resetsAt === null)
|
|
24873
25818
|
return null;
|
|
@@ -24890,9 +25835,9 @@ function burnFor(window2, input) {
|
|
|
24890
25835
|
}
|
|
24891
25836
|
const windowStartsAt = windowStartOf(window2);
|
|
24892
25837
|
const elapsedMs = windowStartsAt === null ? null : window2.observedAt - windowStartsAt;
|
|
24893
|
-
const elapsedHours = elapsedMs === null ? null : elapsedMs /
|
|
25838
|
+
const elapsedHours = elapsedMs === null ? null : elapsedMs / HOUR_MS2;
|
|
24894
25839
|
const ratePerHour = elapsedHours === null ? null : elapsedHours <= 0 || window2.used === 0 ? 0 : window2.used / elapsedHours;
|
|
24895
|
-
const exhaustsAt = window2.limit === null || ratePerHour === null || ratePerHour <= 0 ? null : window2.observedAt + Math.max(0, window2.limit - window2.used) / ratePerHour *
|
|
25840
|
+
const exhaustsAt = window2.limit === null || ratePerHour === null || ratePerHour <= 0 ? null : window2.observedAt + Math.max(0, window2.limit - window2.used) / ratePerHour * HOUR_MS2;
|
|
24896
25841
|
return {
|
|
24897
25842
|
credentialId: window2.credentialId,
|
|
24898
25843
|
windowType: window2.windowType,
|
|
@@ -25031,6 +25976,313 @@ async function patchCredential(deps, id, input) {
|
|
|
25031
25976
|
async function removeCredential(store, id) {
|
|
25032
25977
|
await store.credentials.remove(id);
|
|
25033
25978
|
}
|
|
25979
|
+
// packages/control/src/database.ts
|
|
25980
|
+
import { basename, dirname as dirname2, join as join2, resolve } from "path";
|
|
25981
|
+
var exclusive = null;
|
|
25982
|
+
async function withExclusive(label2, operation) {
|
|
25983
|
+
if (exclusive !== null) {
|
|
25984
|
+
throw new GatewayError("CONFLICT", `${exclusive} is already running on this database`);
|
|
25985
|
+
}
|
|
25986
|
+
exclusive = label2;
|
|
25987
|
+
try {
|
|
25988
|
+
return await operation();
|
|
25989
|
+
} finally {
|
|
25990
|
+
exclusive = null;
|
|
25991
|
+
}
|
|
25992
|
+
}
|
|
25993
|
+
async function withExclusiveOrSkip(label2, operation) {
|
|
25994
|
+
if (exclusive !== null)
|
|
25995
|
+
return null;
|
|
25996
|
+
exclusive = label2;
|
|
25997
|
+
try {
|
|
25998
|
+
return await operation();
|
|
25999
|
+
} finally {
|
|
26000
|
+
exclusive = null;
|
|
26001
|
+
}
|
|
26002
|
+
}
|
|
26003
|
+
var SNAPSHOTS_DIRNAME = "snapshots";
|
|
26004
|
+
function snapshotsDir(deps) {
|
|
26005
|
+
return join2(dirname2(deps.store.databasePath), SNAPSHOTS_DIRNAME);
|
|
26006
|
+
}
|
|
26007
|
+
function snapshotName(at, reason) {
|
|
26008
|
+
const stamp = new Date(at).toISOString().replaceAll(":", "-").replaceAll(".", "-");
|
|
26009
|
+
return `db_${stamp}_${reason}.sqlite`;
|
|
26010
|
+
}
|
|
26011
|
+
var SNAPSHOT_ID = /^db_(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z_([A-Za-z]+)\.sqlite$/;
|
|
26012
|
+
function parseSnapshotName(name) {
|
|
26013
|
+
const match = SNAPSHOT_ID.exec(name);
|
|
26014
|
+
if (match === null)
|
|
26015
|
+
return null;
|
|
26016
|
+
const [, date5, hh, mm, ss, ms, reason] = match;
|
|
26017
|
+
const at = Date.parse(`${date5}T${hh}:${mm}:${ss}.${ms}Z`);
|
|
26018
|
+
if (Number.isNaN(at) || reason === undefined)
|
|
26019
|
+
return null;
|
|
26020
|
+
return { at, reason };
|
|
26021
|
+
}
|
|
26022
|
+
function snapshotPath(deps, id) {
|
|
26023
|
+
const dir = resolve(snapshotsDir(deps));
|
|
26024
|
+
const path = resolve(dir, id);
|
|
26025
|
+
if (parseSnapshotName(id) === null || dirname2(path) !== dir) {
|
|
26026
|
+
throw new GatewayError("BAD_REQUEST", "invalid snapshot id");
|
|
26027
|
+
}
|
|
26028
|
+
const real = deps.fs.realpath(path);
|
|
26029
|
+
if (real !== null && dirname2(real) !== (deps.fs.realpath(dir) ?? dir)) {
|
|
26030
|
+
throw new GatewayError("BAD_REQUEST", "invalid snapshot id");
|
|
26031
|
+
}
|
|
26032
|
+
return path;
|
|
26033
|
+
}
|
|
26034
|
+
function resolveSnapshotForDownload(deps, id) {
|
|
26035
|
+
const path = snapshotPath(deps, id);
|
|
26036
|
+
const stat = deps.fs.stat(path);
|
|
26037
|
+
if (stat === null)
|
|
26038
|
+
throw new GatewayError("BAD_REQUEST", "no such snapshot");
|
|
26039
|
+
return { path, filename: id, sizeBytes: stat.size };
|
|
26040
|
+
}
|
|
26041
|
+
function deleteSnapshot(deps, id) {
|
|
26042
|
+
const path = snapshotPath(deps, id);
|
|
26043
|
+
if (deps.fs.stat(path) === null)
|
|
26044
|
+
throw new GatewayError("BAD_REQUEST", "no such snapshot");
|
|
26045
|
+
deps.fs.unlink(path);
|
|
26046
|
+
}
|
|
26047
|
+
function listSnapshots(deps) {
|
|
26048
|
+
const dir = snapshotsDir(deps);
|
|
26049
|
+
const snapshots = [];
|
|
26050
|
+
for (const name of deps.fs.readdir(dir)) {
|
|
26051
|
+
const parsed = parseSnapshotName(name);
|
|
26052
|
+
if (parsed === null)
|
|
26053
|
+
continue;
|
|
26054
|
+
snapshots.push({
|
|
26055
|
+
id: name,
|
|
26056
|
+
filename: name,
|
|
26057
|
+
createdAt: parsed.at,
|
|
26058
|
+
sizeBytes: deps.fs.stat(join2(dir, name))?.size ?? 0,
|
|
26059
|
+
reason: parsed.reason
|
|
26060
|
+
});
|
|
26061
|
+
}
|
|
26062
|
+
return snapshots.sort((a, b) => b.createdAt - a.createdAt);
|
|
26063
|
+
}
|
|
26064
|
+
var DAY_MS = 86400000;
|
|
26065
|
+
var SNAPSHOT_HEADROOM = 1.1;
|
|
26066
|
+
function retentionOf(settings) {
|
|
26067
|
+
return { keepLatest: settings.snapshotKeepLatest, maxAgeDays: settings.snapshotMaxAgeDays };
|
|
26068
|
+
}
|
|
26069
|
+
function applyRetention(deps, policy, now) {
|
|
26070
|
+
const dir = snapshotsDir(deps);
|
|
26071
|
+
const cutoff = now - policy.maxAgeDays * DAY_MS;
|
|
26072
|
+
const snapshots = listSnapshots(deps);
|
|
26073
|
+
const undo = snapshots.find((snapshot) => snapshot.reason === "preRestore")?.id;
|
|
26074
|
+
let removed = 0;
|
|
26075
|
+
snapshots.forEach((snapshot, index) => {
|
|
26076
|
+
if (index === 0)
|
|
26077
|
+
return;
|
|
26078
|
+
if (snapshot.id === undo)
|
|
26079
|
+
return;
|
|
26080
|
+
if (index < policy.keepLatest && snapshot.createdAt >= cutoff)
|
|
26081
|
+
return;
|
|
26082
|
+
deps.fs.unlink(join2(dir, snapshot.id));
|
|
26083
|
+
removed++;
|
|
26084
|
+
});
|
|
26085
|
+
return removed;
|
|
26086
|
+
}
|
|
26087
|
+
async function pruneSnapshots(deps) {
|
|
26088
|
+
const removed = await withExclusiveOrSkip("a snapshot sweep", async () => applyRetention(deps, retentionOf(await deps.store.config.getSettings()), deps.now()));
|
|
26089
|
+
return removed ?? 0;
|
|
26090
|
+
}
|
|
26091
|
+
var IMPORT_STAGING_PREFIX = "omni-import-";
|
|
26092
|
+
var IMPORT_STAGING_SUFFIX = ".sqlite.part";
|
|
26093
|
+
var INCOMING_SUFFIX = ".incoming";
|
|
26094
|
+
var STAGING_MAX_AGE_MS = 60 * 60 * 1000;
|
|
26095
|
+
function stagedImportPath(deps, token) {
|
|
26096
|
+
return join2(dirname2(deps.store.databasePath), `${IMPORT_STAGING_PREFIX}${token}${IMPORT_STAGING_SUFFIX}`);
|
|
26097
|
+
}
|
|
26098
|
+
function sweepStaging(deps) {
|
|
26099
|
+
const live = deps.store.databasePath;
|
|
26100
|
+
const dir = dirname2(live);
|
|
26101
|
+
const incoming = `${basename(live)}${INCOMING_SUFFIX}`;
|
|
26102
|
+
const staleBefore = deps.now() - STAGING_MAX_AGE_MS;
|
|
26103
|
+
let removed = 0;
|
|
26104
|
+
for (const entry of deps.fs.readdir(dir)) {
|
|
26105
|
+
const staged = entry === incoming || entry.startsWith(IMPORT_STAGING_PREFIX) && entry.endsWith(IMPORT_STAGING_SUFFIX);
|
|
26106
|
+
if (!staged)
|
|
26107
|
+
continue;
|
|
26108
|
+
const path = join2(dir, entry);
|
|
26109
|
+
const stat = deps.fs.stat(path);
|
|
26110
|
+
if (stat === null || stat.mtimeMs >= staleBefore)
|
|
26111
|
+
continue;
|
|
26112
|
+
deps.fs.unlink(path);
|
|
26113
|
+
removed++;
|
|
26114
|
+
}
|
|
26115
|
+
return removed;
|
|
26116
|
+
}
|
|
26117
|
+
async function writeSnapshot(deps, input) {
|
|
26118
|
+
const dir = snapshotsDir(deps);
|
|
26119
|
+
const at = deps.now();
|
|
26120
|
+
const filename = snapshotName(at, input.reason);
|
|
26121
|
+
const path = join2(dir, filename);
|
|
26122
|
+
deps.fs.mkdir(dir);
|
|
26123
|
+
if (deps.fs.stat(path) !== null) {
|
|
26124
|
+
throw new GatewayError("CONFLICT", "a snapshot for this instant already exists");
|
|
26125
|
+
}
|
|
26126
|
+
const free = deps.fs.freeBytes(dir);
|
|
26127
|
+
const needed = Math.ceil((deps.fs.stat(deps.store.databasePath)?.size ?? 0) * SNAPSHOT_HEADROOM);
|
|
26128
|
+
if (free !== null && free < needed) {
|
|
26129
|
+
throw new GatewayError("CONFLICT", "not enough free disk space to write a snapshot");
|
|
26130
|
+
}
|
|
26131
|
+
try {
|
|
26132
|
+
await deps.store.maintenance.snapshotTo(path);
|
|
26133
|
+
} catch (error51) {
|
|
26134
|
+
deps.fs.unlink(path);
|
|
26135
|
+
throw error51;
|
|
26136
|
+
}
|
|
26137
|
+
const info = {
|
|
26138
|
+
id: filename,
|
|
26139
|
+
filename,
|
|
26140
|
+
createdAt: at,
|
|
26141
|
+
sizeBytes: deps.fs.stat(path)?.size ?? 0,
|
|
26142
|
+
reason: input.reason
|
|
26143
|
+
};
|
|
26144
|
+
if (input.force !== true) {
|
|
26145
|
+
applyRetention(deps, retentionOf(await deps.store.config.getSettings()), at);
|
|
26146
|
+
}
|
|
26147
|
+
return info;
|
|
26148
|
+
}
|
|
26149
|
+
async function createSnapshot(deps, input) {
|
|
26150
|
+
return withExclusive("a snapshot", () => writeSnapshot(deps, input));
|
|
26151
|
+
}
|
|
26152
|
+
|
|
26153
|
+
class SwapFailedError extends Error {
|
|
26154
|
+
preRestoreSnapshotId;
|
|
26155
|
+
cause;
|
|
26156
|
+
reopened;
|
|
26157
|
+
constructor(preRestoreSnapshotId, cause, reopened) {
|
|
26158
|
+
super("the database swap failed and the database may be incomplete");
|
|
26159
|
+
this.preRestoreSnapshotId = preRestoreSnapshotId;
|
|
26160
|
+
this.cause = cause;
|
|
26161
|
+
this.reopened = reopened;
|
|
26162
|
+
this.name = "SwapFailedError";
|
|
26163
|
+
}
|
|
26164
|
+
}
|
|
26165
|
+
var NAMED_TABLES = 5;
|
|
26166
|
+
function describeTables(tables) {
|
|
26167
|
+
if (tables.length === 0)
|
|
26168
|
+
return "it has no tables at all";
|
|
26169
|
+
const named = tables.slice(0, NAMED_TABLES);
|
|
26170
|
+
const rest = tables.length - named.length;
|
|
26171
|
+
return `it has ${named.join(", ")}${rest > 0 ? ` and ${rest} more` : ""}`;
|
|
26172
|
+
}
|
|
26173
|
+
async function swapIn(deps, candidate) {
|
|
26174
|
+
const inspection = await deps.store.maintenance.inspect(candidate.path);
|
|
26175
|
+
if (!inspection.ok) {
|
|
26176
|
+
throw new GatewayError("BAD_REQUEST", inspection.quickCheck === "ok" ? `that file is a database, but not one of ours: ${describeTables(inspection.tables)}` : `that file failed its integrity check: ${inspection.quickCheck}`);
|
|
26177
|
+
}
|
|
26178
|
+
const preRestoreSnapshot = await writeSnapshot(deps, { reason: "preRestore", force: true });
|
|
26179
|
+
const adminHashBefore = await deps.store.config.getAdminPasswordHash();
|
|
26180
|
+
const live = deps.store.databasePath;
|
|
26181
|
+
const staged = `${live}.incoming`;
|
|
26182
|
+
try {
|
|
26183
|
+
if (candidate.consume)
|
|
26184
|
+
deps.fs.rename(candidate.path, staged);
|
|
26185
|
+
else
|
|
26186
|
+
deps.fs.copyFile(candidate.path, staged);
|
|
26187
|
+
} catch (error51) {
|
|
26188
|
+
deps.fs.unlink(staged);
|
|
26189
|
+
throw error51;
|
|
26190
|
+
}
|
|
26191
|
+
try {
|
|
26192
|
+
deps.store.close();
|
|
26193
|
+
deps.fs.unlink(`${live}-wal`);
|
|
26194
|
+
deps.fs.unlink(`${live}-shm`);
|
|
26195
|
+
deps.fs.rename(staged, live);
|
|
26196
|
+
await deps.store.reopen();
|
|
26197
|
+
} catch (error51) {
|
|
26198
|
+
let reopened = true;
|
|
26199
|
+
try {
|
|
26200
|
+
await deps.store.reopen();
|
|
26201
|
+
} catch {
|
|
26202
|
+
reopened = false;
|
|
26203
|
+
}
|
|
26204
|
+
throw new SwapFailedError(preRestoreSnapshot.id, error51, reopened);
|
|
26205
|
+
}
|
|
26206
|
+
const adminHashAfter = await deps.store.config.getAdminPasswordHash();
|
|
26207
|
+
try {
|
|
26208
|
+
await deps.store.usage.rebuildRollup();
|
|
26209
|
+
} catch (error51) {
|
|
26210
|
+
(deps.logger ?? noopLogger).warn("usage rollup not rebuilt after the swap; run omni doctor", {
|
|
26211
|
+
reason: error51 instanceof Error ? error51.message : "unknown"
|
|
26212
|
+
});
|
|
26213
|
+
}
|
|
26214
|
+
return {
|
|
26215
|
+
ok: true,
|
|
26216
|
+
counts: inspection.counts,
|
|
26217
|
+
preRestoreSnapshot,
|
|
26218
|
+
adminPasswordChanged: adminHashAfter !== adminHashBefore
|
|
26219
|
+
};
|
|
26220
|
+
}
|
|
26221
|
+
async function restoreSnapshot(deps, id) {
|
|
26222
|
+
const path = snapshotPath(deps, id);
|
|
26223
|
+
if (deps.fs.stat(path) === null)
|
|
26224
|
+
throw new GatewayError("BAD_REQUEST", "no such snapshot");
|
|
26225
|
+
return withExclusive("a restore", () => swapIn(deps, { path, consume: false }));
|
|
26226
|
+
}
|
|
26227
|
+
var MAX_IMPORT_BYTES = 2 * 1024 * 1024 * 1024;
|
|
26228
|
+
async function importSnapshot(deps, input) {
|
|
26229
|
+
return withExclusive("an import", async () => {
|
|
26230
|
+
const stat = deps.fs.stat(input.path);
|
|
26231
|
+
if (stat === null)
|
|
26232
|
+
throw new GatewayError("BAD_REQUEST", "the uploaded file is not there");
|
|
26233
|
+
if (stat.size > MAX_IMPORT_BYTES) {
|
|
26234
|
+
deps.fs.unlink(input.path);
|
|
26235
|
+
throw new GatewayError("BAD_REQUEST", `a database import may not exceed ${MAX_IMPORT_BYTES} bytes`);
|
|
26236
|
+
}
|
|
26237
|
+
try {
|
|
26238
|
+
return await swapIn(deps, { path: input.path, consume: true });
|
|
26239
|
+
} catch (error51) {
|
|
26240
|
+
deps.fs.unlink(input.path);
|
|
26241
|
+
throw error51;
|
|
26242
|
+
}
|
|
26243
|
+
});
|
|
26244
|
+
}
|
|
26245
|
+
async function vacuum(deps) {
|
|
26246
|
+
return withExclusive("a vacuum", async () => {
|
|
26247
|
+
const before = deps.fs.stat(deps.store.databasePath)?.size ?? 0;
|
|
26248
|
+
const startedAt = deps.now();
|
|
26249
|
+
await deps.store.maintenance.vacuum();
|
|
26250
|
+
const after = deps.fs.stat(deps.store.databasePath)?.size ?? 0;
|
|
26251
|
+
return {
|
|
26252
|
+
reclaimedBytes: Math.max(0, before - after),
|
|
26253
|
+
durationMs: Math.max(0, deps.now() - startedAt)
|
|
26254
|
+
};
|
|
26255
|
+
});
|
|
26256
|
+
}
|
|
26257
|
+
async function getDatabaseOverview(deps) {
|
|
26258
|
+
const live = deps.store.databasePath;
|
|
26259
|
+
const stats = await deps.store.maintenance.stats();
|
|
26260
|
+
const settings = await deps.store.config.getSettings();
|
|
26261
|
+
const snapshots = listSnapshots(deps);
|
|
26262
|
+
return {
|
|
26263
|
+
stats,
|
|
26264
|
+
fileBytes: deps.fs.stat(live)?.size ?? 0,
|
|
26265
|
+
walBytes: deps.fs.stat(`${live}-wal`)?.size ?? 0,
|
|
26266
|
+
bodiesBytes: deps.fs.dirBytes(bodiesDirFor(live)),
|
|
26267
|
+
logicalBytes: stats.pageSize * stats.pageCount,
|
|
26268
|
+
freePageBytes: stats.pageSize * stats.freelistCount,
|
|
26269
|
+
freeDiskBytes: deps.fs.freeBytes(dirname2(live)),
|
|
26270
|
+
retention: retentionOf(settings),
|
|
26271
|
+
snapshots: {
|
|
26272
|
+
count: snapshots.length,
|
|
26273
|
+
totalBytes: snapshots.reduce((sum, snapshot) => sum + snapshot.sizeBytes, 0),
|
|
26274
|
+
latestAt: snapshots[0]?.createdAt ?? null
|
|
26275
|
+
}
|
|
26276
|
+
};
|
|
26277
|
+
}
|
|
26278
|
+
async function putRetention(deps, input) {
|
|
26279
|
+
const policy = parseOrThrow(retentionSchema, input);
|
|
26280
|
+
await deps.store.config.putSettings({
|
|
26281
|
+
snapshotKeepLatest: policy.keepLatest,
|
|
26282
|
+
snapshotMaxAgeDays: policy.maxAgeDays
|
|
26283
|
+
});
|
|
26284
|
+
return policy;
|
|
26285
|
+
}
|
|
25034
26286
|
// packages/control/src/dryRun.ts
|
|
25035
26287
|
async function dryRun(deps, modelId, input) {
|
|
25036
26288
|
const need = parseOrThrow(dryRunSchema, input);
|
|
@@ -25075,17 +26327,67 @@ async function dryRun(deps, modelId, input) {
|
|
|
25075
26327
|
};
|
|
25076
26328
|
}
|
|
25077
26329
|
// packages/control/src/keys.ts
|
|
25078
|
-
|
|
26330
|
+
function configuredWindows(limits) {
|
|
26331
|
+
const needed = new Set;
|
|
26332
|
+
for (const dimension of ["requests", "tokens", "spend"]) {
|
|
26333
|
+
const windows = limits[dimension];
|
|
26334
|
+
if (windows === undefined)
|
|
26335
|
+
continue;
|
|
26336
|
+
for (const window2 of WINDOWS) {
|
|
26337
|
+
const limit = windows[window2];
|
|
26338
|
+
if (limit !== undefined && limit !== null)
|
|
26339
|
+
needed.add(window2);
|
|
26340
|
+
}
|
|
26341
|
+
}
|
|
26342
|
+
return [...needed];
|
|
26343
|
+
}
|
|
26344
|
+
async function readLimitUsage(store, id, limits, now) {
|
|
26345
|
+
if (limits === null)
|
|
26346
|
+
return [];
|
|
26347
|
+
const sums = new Map;
|
|
26348
|
+
for (const window2 of configuredWindows(limits)) {
|
|
26349
|
+
sums.set(window2, await store.usage.sumSince(id, now - WINDOW_MS[window2]));
|
|
26350
|
+
}
|
|
26351
|
+
const readings = [];
|
|
26352
|
+
for (const dimension of ["requests", "tokens", "spend"]) {
|
|
26353
|
+
const windows = limits[dimension];
|
|
26354
|
+
if (windows === undefined)
|
|
26355
|
+
continue;
|
|
26356
|
+
for (const window2 of WINDOWS) {
|
|
26357
|
+
const limit = windows[window2];
|
|
26358
|
+
if (limit === undefined || limit === null)
|
|
26359
|
+
continue;
|
|
26360
|
+
const sum = sums.get(window2);
|
|
26361
|
+
const used = sum === undefined ? null : dimension === "requests" ? sum.requests : dimension === "tokens" ? sum.tokens : sum.costUsd;
|
|
26362
|
+
readings.push({ dimension, window: window2, limit, used });
|
|
26363
|
+
}
|
|
26364
|
+
}
|
|
26365
|
+
if (limits.concurrency !== undefined && limits.concurrency !== null) {
|
|
26366
|
+
readings.push({
|
|
26367
|
+
dimension: "concurrency",
|
|
26368
|
+
window: null,
|
|
26369
|
+
limit: limits.concurrency,
|
|
26370
|
+
used: null
|
|
26371
|
+
});
|
|
26372
|
+
}
|
|
26373
|
+
return readings;
|
|
26374
|
+
}
|
|
26375
|
+
async function toSummary(store, key, now) {
|
|
26376
|
+
return {
|
|
26377
|
+
id: key.id,
|
|
26378
|
+
label: key.label,
|
|
26379
|
+
prefix: key.prefix,
|
|
26380
|
+
modelAllowlist: key.modelAllowlist,
|
|
26381
|
+
limits: key.limits,
|
|
26382
|
+
limitUsage: await readLimitUsage(store, key.id, key.limits, now),
|
|
26383
|
+
bodyLoggingOptOut: key.bodyLoggingOptOut,
|
|
26384
|
+
createdAt: key.createdAt,
|
|
26385
|
+
revokedAt: key.revokedAt
|
|
26386
|
+
};
|
|
26387
|
+
}
|
|
26388
|
+
async function listKeys(store, now = Date.now()) {
|
|
25079
26389
|
const keys = await store.keys.list();
|
|
25080
|
-
return keys.map((
|
|
25081
|
-
id: k.id,
|
|
25082
|
-
label: k.label,
|
|
25083
|
-
prefix: k.prefix,
|
|
25084
|
-
modelAllowlist: k.modelAllowlist,
|
|
25085
|
-
rateLimitPerMin: k.rateLimitPerMin,
|
|
25086
|
-
createdAt: k.createdAt,
|
|
25087
|
-
revokedAt: k.revokedAt
|
|
25088
|
-
}));
|
|
26390
|
+
return Promise.all(keys.map((key) => toSummary(store, key, now)));
|
|
25089
26391
|
}
|
|
25090
26392
|
async function createKey(store, input) {
|
|
25091
26393
|
const body2 = parseOrThrow(keyCreateSchema, input);
|
|
@@ -25096,13 +26398,75 @@ async function createKey(store, input) {
|
|
|
25096
26398
|
prefix: raw.slice(0, 12),
|
|
25097
26399
|
hash: await hashApiKey(raw),
|
|
25098
26400
|
modelAllowlist: body2.modelAllowlist,
|
|
25099
|
-
|
|
26401
|
+
limits: body2.limits,
|
|
26402
|
+
bodyLoggingOptOut: body2.bodyLoggingOptOut
|
|
25100
26403
|
});
|
|
25101
26404
|
return { id: created.id, label: created.label, prefix: created.prefix, key: raw };
|
|
25102
26405
|
}
|
|
26406
|
+
async function setKeyLimits(store, id, input, now = Date.now()) {
|
|
26407
|
+
const body2 = parseOrThrow(keyLimitsSchema, input);
|
|
26408
|
+
const key = (await store.keys.list()).find((entry) => entry.id === id);
|
|
26409
|
+
if (key === undefined)
|
|
26410
|
+
throw new GatewayError("BAD_REQUEST", "no such api key");
|
|
26411
|
+
await store.keys.setLimits(id, body2.limits);
|
|
26412
|
+
return toSummary(store, { ...key, limits: body2.limits }, now);
|
|
26413
|
+
}
|
|
25103
26414
|
async function revokeKey(store, id) {
|
|
25104
26415
|
await store.keys.revoke(id);
|
|
25105
26416
|
}
|
|
26417
|
+
// packages/control/src/lifecycle.ts
|
|
26418
|
+
var DOCKERENV_PATH = "/.dockerenv";
|
|
26419
|
+
function describeLifecycle(env2, fileExists) {
|
|
26420
|
+
if (env2.JOURNAL_STREAM !== undefined) {
|
|
26421
|
+
return { supervisor: "systemd", canRestart: true, canShutdown: true };
|
|
26422
|
+
}
|
|
26423
|
+
if (fileExists(DOCKERENV_PATH)) {
|
|
26424
|
+
return {
|
|
26425
|
+
supervisor: "container",
|
|
26426
|
+
canRestart: true,
|
|
26427
|
+
canShutdown: true,
|
|
26428
|
+
note: "restart exits the container and relies on its restart policy, which cannot be read from inside it"
|
|
26429
|
+
};
|
|
26430
|
+
}
|
|
26431
|
+
return {
|
|
26432
|
+
supervisor: "none",
|
|
26433
|
+
canRestart: false,
|
|
26434
|
+
canShutdown: true,
|
|
26435
|
+
note: "no supervisor is watching this process, so nothing would start it again"
|
|
26436
|
+
};
|
|
26437
|
+
}
|
|
26438
|
+
function systemdScope(env2) {
|
|
26439
|
+
return env2.MANAGERPID === undefined ? "system" : "user";
|
|
26440
|
+
}
|
|
26441
|
+
function commandFailure(action, result) {
|
|
26442
|
+
const detail = result.stderr.split(`
|
|
26443
|
+
`)[0]?.trim().slice(0, 200) ?? "";
|
|
26444
|
+
const suffix = detail.length > 0 ? `: ${detail}` : "";
|
|
26445
|
+
return new GatewayError("INTERNAL", `${action} failed with exit ${result.code}${suffix}`);
|
|
26446
|
+
}
|
|
26447
|
+
async function requestRestart(deps) {
|
|
26448
|
+
const capability = describeLifecycle(deps.env, deps.fileExists);
|
|
26449
|
+
if (!capability.canRestart) {
|
|
26450
|
+
throw new GatewayError("CONFLICT", capability.note ?? "this installation has no supervisor that would restart the gateway");
|
|
26451
|
+
}
|
|
26452
|
+
if (capability.supervisor === "container") {
|
|
26453
|
+
deps.stop(0);
|
|
26454
|
+
return;
|
|
26455
|
+
}
|
|
26456
|
+
const scope = systemdScope(deps.env);
|
|
26457
|
+
const result = await deps.run([
|
|
26458
|
+
"systemctl",
|
|
26459
|
+
...scope === "user" ? ["--user"] : [],
|
|
26460
|
+
"--no-block",
|
|
26461
|
+
"restart",
|
|
26462
|
+
UNIT_NAME
|
|
26463
|
+
]);
|
|
26464
|
+
if (result.code !== 0)
|
|
26465
|
+
throw commandFailure("systemctl restart", result);
|
|
26466
|
+
}
|
|
26467
|
+
async function requestShutdown(deps) {
|
|
26468
|
+
deps.stop(0);
|
|
26469
|
+
}
|
|
25106
26470
|
// packages/control/src/modelLimits.ts
|
|
25107
26471
|
function narrower(a, b) {
|
|
25108
26472
|
const context = [a.contextWindow, b.contextWindow].filter((n) => n !== undefined);
|
|
@@ -25215,6 +26579,81 @@ async function putModel(store, id, input) {
|
|
|
25215
26579
|
async function removeModel(store, id) {
|
|
25216
26580
|
await store.config.removeModel(id);
|
|
25217
26581
|
}
|
|
26582
|
+
// packages/control/src/nodeFs.ts
|
|
26583
|
+
import {
|
|
26584
|
+
copyFileSync,
|
|
26585
|
+
mkdirSync,
|
|
26586
|
+
readdirSync,
|
|
26587
|
+
realpathSync,
|
|
26588
|
+
renameSync,
|
|
26589
|
+
rmSync,
|
|
26590
|
+
statfsSync,
|
|
26591
|
+
statSync
|
|
26592
|
+
} from "fs";
|
|
26593
|
+
import { join as join3 } from "path";
|
|
26594
|
+
function nodeDatabaseFs() {
|
|
26595
|
+
const dirBytes = (dir) => {
|
|
26596
|
+
let entries;
|
|
26597
|
+
try {
|
|
26598
|
+
entries = readdirSync(dir, { withFileTypes: true, encoding: "utf8" });
|
|
26599
|
+
} catch {
|
|
26600
|
+
return 0;
|
|
26601
|
+
}
|
|
26602
|
+
let total = 0;
|
|
26603
|
+
for (const entry of entries) {
|
|
26604
|
+
const path = join3(dir, entry.name);
|
|
26605
|
+
if (entry.isDirectory()) {
|
|
26606
|
+
total += dirBytes(path);
|
|
26607
|
+
continue;
|
|
26608
|
+
}
|
|
26609
|
+
try {
|
|
26610
|
+
total += statSync(path).size;
|
|
26611
|
+
} catch {}
|
|
26612
|
+
}
|
|
26613
|
+
return total;
|
|
26614
|
+
};
|
|
26615
|
+
return {
|
|
26616
|
+
readdir: (dir) => {
|
|
26617
|
+
try {
|
|
26618
|
+
return readdirSync(dir);
|
|
26619
|
+
} catch {
|
|
26620
|
+
return [];
|
|
26621
|
+
}
|
|
26622
|
+
},
|
|
26623
|
+
stat: (path) => {
|
|
26624
|
+
try {
|
|
26625
|
+
const stat = statSync(path);
|
|
26626
|
+
return { size: stat.size, mtimeMs: stat.mtimeMs };
|
|
26627
|
+
} catch {
|
|
26628
|
+
return null;
|
|
26629
|
+
}
|
|
26630
|
+
},
|
|
26631
|
+
unlink: (path) => {
|
|
26632
|
+
rmSync(path, { force: true });
|
|
26633
|
+
},
|
|
26634
|
+
rename: (from, to) => renameSync(from, to),
|
|
26635
|
+
copyFile: (from, to) => copyFileSync(from, to),
|
|
26636
|
+
mkdir: (dir) => {
|
|
26637
|
+
mkdirSync(dir, { recursive: true });
|
|
26638
|
+
},
|
|
26639
|
+
realpath: (path) => {
|
|
26640
|
+
try {
|
|
26641
|
+
return realpathSync(path);
|
|
26642
|
+
} catch {
|
|
26643
|
+
return null;
|
|
26644
|
+
}
|
|
26645
|
+
},
|
|
26646
|
+
freeBytes: (dir) => {
|
|
26647
|
+
try {
|
|
26648
|
+
const stat = statfsSync(dir);
|
|
26649
|
+
return Number(stat.bavail) * Number(stat.bsize);
|
|
26650
|
+
} catch {
|
|
26651
|
+
return null;
|
|
26652
|
+
}
|
|
26653
|
+
},
|
|
26654
|
+
dirBytes
|
|
26655
|
+
};
|
|
26656
|
+
}
|
|
25218
26657
|
// packages/control/src/oauth/pkce.ts
|
|
25219
26658
|
function randomToken() {
|
|
25220
26659
|
return Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("base64url");
|
|
@@ -26128,8 +27567,8 @@ function createRefresher(deps) {
|
|
|
26128
27567
|
};
|
|
26129
27568
|
}
|
|
26130
27569
|
// packages/control/src/quota/history.ts
|
|
26131
|
-
var
|
|
26132
|
-
var
|
|
27570
|
+
var DAY_MS2 = 24 * 60 * 60 * 1000;
|
|
27571
|
+
var HOUR_MS3 = 3600000;
|
|
26133
27572
|
async function gatewayRatesFor(store, windows) {
|
|
26134
27573
|
const rates = [];
|
|
26135
27574
|
for (const window2 of windows) {
|
|
@@ -26153,7 +27592,7 @@ async function gatewayRatesFor(store, windows) {
|
|
|
26153
27592
|
rates.push({
|
|
26154
27593
|
credentialId: window2.credentialId,
|
|
26155
27594
|
windowType: window2.windowType,
|
|
26156
|
-
gatewayRatePerHour: tokens2 / ((window2.observedAt - since) /
|
|
27595
|
+
gatewayRatePerHour: tokens2 / ((window2.observedAt - since) / HOUR_MS3)
|
|
26157
27596
|
});
|
|
26158
27597
|
}
|
|
26159
27598
|
return rates;
|
|
@@ -26161,7 +27600,7 @@ async function gatewayRatesFor(store, windows) {
|
|
|
26161
27600
|
async function quotaHistory(deps, input) {
|
|
26162
27601
|
const now = deps.now();
|
|
26163
27602
|
const settings = await deps.store.config.getSettings();
|
|
26164
|
-
const oldest = now - settings.logRetentionDays *
|
|
27603
|
+
const oldest = now - settings.logRetentionDays * DAY_MS2;
|
|
26165
27604
|
const since = Math.max(optionalNumber(input.since, oldest), oldest);
|
|
26166
27605
|
const until = Math.min(optionalNumber(input.until, now), now);
|
|
26167
27606
|
const raw = input.credentialId?.trim();
|
|
@@ -26248,9 +27687,13 @@ async function getSettings(store) {
|
|
|
26248
27687
|
return store.config.getSettings();
|
|
26249
27688
|
}
|
|
26250
27689
|
async function putSettings(store, input) {
|
|
26251
|
-
const
|
|
26252
|
-
|
|
26253
|
-
|
|
27690
|
+
const { snapshotKeepLatest, snapshotMaxAgeDays, ...rest } = parseOrThrow(settingsSchema, input);
|
|
27691
|
+
const patch = {
|
|
27692
|
+
...rest,
|
|
27693
|
+
...snapshotKeepLatest === undefined ? {} : { snapshotKeepLatest },
|
|
27694
|
+
...snapshotMaxAgeDays === undefined ? {} : { snapshotMaxAgeDays }
|
|
27695
|
+
};
|
|
27696
|
+
return store.config.putSettings(patch);
|
|
26254
27697
|
}
|
|
26255
27698
|
// packages/control/src/setup.ts
|
|
26256
27699
|
var KEY_PLACEHOLDER = "<your OmniGateway key>";
|
|
@@ -26369,7 +27812,7 @@ async function setupFiles(store, client, input, mapping) {
|
|
|
26369
27812
|
return client === "opencode" ? [opencodeConfig(described, input, mapping)] : [claudeSettings(described, input, mapping)];
|
|
26370
27813
|
}
|
|
26371
27814
|
// packages/control/src/tail.ts
|
|
26372
|
-
import { closeSync, fstatSync, openSync, readSync, statSync } from "fs";
|
|
27815
|
+
import { closeSync, fstatSync, openSync, readSync, statSync as statSync2 } from "fs";
|
|
26373
27816
|
var CHUNK = 64 * 1024;
|
|
26374
27817
|
var MAX_BYTES = 8 * 1024 * 1024;
|
|
26375
27818
|
function tailFile(path, lines) {
|
|
@@ -26428,8 +27871,8 @@ async function recentLogs(store, requested) {
|
|
|
26428
27871
|
return store.usage.recent(logLimit(requested));
|
|
26429
27872
|
}
|
|
26430
27873
|
// apps/gateway/src/app.ts
|
|
26431
|
-
import { realpathSync } from "fs";
|
|
26432
|
-
import { resolve, sep } from "path";
|
|
27874
|
+
import { existsSync, realpathSync as realpathSync2 } from "fs";
|
|
27875
|
+
import { resolve as resolve2, sep as sep2 } from "path";
|
|
26433
27876
|
|
|
26434
27877
|
// node_modules/.bun/memoirist@0.4.0/node_modules/memoirist/dist/bun/index.js
|
|
26435
27878
|
var Y = (v, b) => {
|
|
@@ -33631,11 +35074,11 @@ function removeTrailingEquals(digest) {
|
|
|
33631
35074
|
trimmedDigest = trimmedDigest.slice(0, -1);
|
|
33632
35075
|
return trimmedDigest;
|
|
33633
35076
|
}
|
|
33634
|
-
var
|
|
35077
|
+
var encoder3 = new TextEncoder;
|
|
33635
35078
|
var signCookie = async (val, secret) => {
|
|
33636
35079
|
if (typeof val == "object" ? val = JSON.stringify(val) : typeof val != "string" && (val = val + ""), secret == null)
|
|
33637
35080
|
throw new TypeError("Secret key must be provided");
|
|
33638
|
-
const secretKey = await crypto.subtle.importKey("raw",
|
|
35081
|
+
const secretKey = await crypto.subtle.importKey("raw", encoder3.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), hmacBuffer = await crypto.subtle.sign("HMAC", secretKey, encoder3.encode(val));
|
|
33639
35082
|
return val + "." + removeTrailingEquals(Buffer.from(hmacBuffer).toString("base64"));
|
|
33640
35083
|
};
|
|
33641
35084
|
var constantTimeEqual = typeof crypto?.timingSafeEqual == "function" ? (a, b) => {
|
|
@@ -34658,8 +36101,8 @@ var separateFunction = (code) => {
|
|
|
34658
36101
|
}
|
|
34659
36102
|
const start = code.indexOf("(");
|
|
34660
36103
|
if (start !== -1) {
|
|
34661
|
-
const
|
|
34662
|
-
`, 2), parameter = code.slice(0,
|
|
36104
|
+
const sep2 = code.indexOf(`
|
|
36105
|
+
`, 2), parameter = code.slice(0, sep2), end = parameter.lastIndexOf(")") + 1, body2 = code.slice(sep2 + 1);
|
|
34663
36106
|
return [
|
|
34664
36107
|
parameter.slice(start, end),
|
|
34665
36108
|
"{" + body2,
|
|
@@ -35270,13 +36713,13 @@ async function* streamResponse(response) {
|
|
|
35270
36713
|
const body2 = response.body;
|
|
35271
36714
|
if (!body2)
|
|
35272
36715
|
return;
|
|
35273
|
-
const reader = body2.getReader(),
|
|
36716
|
+
const reader = body2.getReader(), decoder2 = new TextDecoder;
|
|
35274
36717
|
try {
|
|
35275
36718
|
for (;; ) {
|
|
35276
36719
|
const { done, value } = await reader.read();
|
|
35277
36720
|
if (done)
|
|
35278
36721
|
break;
|
|
35279
|
-
typeof value == "string" ? yield value : yield
|
|
36722
|
+
typeof value == "string" ? yield value : yield decoder2.decode(value);
|
|
35280
36723
|
}
|
|
35281
36724
|
} finally {
|
|
35282
36725
|
reader.releaseLock();
|
|
@@ -35334,7 +36777,7 @@ async function tee(source, branches = 2) {
|
|
|
35334
36777
|
else {
|
|
35335
36778
|
if (done)
|
|
35336
36779
|
return;
|
|
35337
|
-
await new Promise((
|
|
36780
|
+
await new Promise((resolve2) => waiting.push({ resolve: resolve2 }));
|
|
35338
36781
|
}
|
|
35339
36782
|
}
|
|
35340
36783
|
return Array.from({ length: branches }, makeIterator);
|
|
@@ -36198,14 +37641,14 @@ function parseQuery(input) {
|
|
|
36198
37641
|
// node_modules/.bun/elysia@1.4.29+5b33ebcc42722450/node_modules/elysia/dist/trace.mjs
|
|
36199
37642
|
var ELYSIA_TRACE = Symbol("ElysiaTrace");
|
|
36200
37643
|
var createProcess = () => {
|
|
36201
|
-
const { promise: promise2, resolve } = Promise.withResolvers(), { promise: end, resolve: resolveEnd } = Promise.withResolvers(), { promise: error51, resolve: resolveError } = Promise.withResolvers(), callbacks = [], callbacksEnd = [];
|
|
37644
|
+
const { promise: promise2, resolve: resolve2 } = Promise.withResolvers(), { promise: end, resolve: resolveEnd } = Promise.withResolvers(), { promise: error51, resolve: resolveError } = Promise.withResolvers(), callbacks = [], callbacksEnd = [];
|
|
36202
37645
|
return [
|
|
36203
37646
|
(callback) => (callback && callbacks.push(callback), promise2),
|
|
36204
37647
|
(process3) => {
|
|
36205
37648
|
const processes = [], resolvers = [];
|
|
36206
37649
|
let groupError = null;
|
|
36207
37650
|
for (let i = 0;i < (process3.total ?? 0); i++) {
|
|
36208
|
-
const { promise: promise22, resolve:
|
|
37651
|
+
const { promise: promise22, resolve: resolve22 } = Promise.withResolvers(), { promise: end2, resolve: resolveEnd2 } = Promise.withResolvers(), { promise: error210, resolve: resolveError2 } = Promise.withResolvers(), callbacks2 = [], callbacksEnd2 = [];
|
|
36209
37652
|
processes.push((callback) => (callback && callbacks2.push(callback), promise22)), resolvers.push((process22) => {
|
|
36210
37653
|
const result2 = {
|
|
36211
37654
|
...process22,
|
|
@@ -36216,7 +37659,7 @@ var createProcess = () => {
|
|
|
36216
37659
|
return callback && callbacksEnd2.push(callback), end2;
|
|
36217
37660
|
}
|
|
36218
37661
|
};
|
|
36219
|
-
|
|
37662
|
+
resolve22(result2);
|
|
36220
37663
|
for (let i2 = 0;i2 < callbacks2.length; i2++)
|
|
36221
37664
|
callbacks2[i2](result2);
|
|
36222
37665
|
return (error310 = null) => {
|
|
@@ -36247,7 +37690,7 @@ var createProcess = () => {
|
|
|
36247
37690
|
return callback && callbacksEnd.push(callback), end;
|
|
36248
37691
|
}
|
|
36249
37692
|
};
|
|
36250
|
-
|
|
37693
|
+
resolve2(result);
|
|
36251
37694
|
for (let i = 0;i < callbacks.length; i++)
|
|
36252
37695
|
callbacks[i](result);
|
|
36253
37696
|
return {
|
|
@@ -40074,11 +41517,11 @@ var _Elysia = class _Elysia2 {
|
|
|
40074
41517
|
onTransform(options, handler) {
|
|
40075
41518
|
return handler ? this.on(options, "transform", handler) : this.on("transform", options);
|
|
40076
41519
|
}
|
|
40077
|
-
resolve(optionsOrResolve,
|
|
40078
|
-
|
|
41520
|
+
resolve(optionsOrResolve, resolve2) {
|
|
41521
|
+
resolve2 || (resolve2 = optionsOrResolve, optionsOrResolve = { as: "local" });
|
|
40079
41522
|
const hook = {
|
|
40080
41523
|
subType: "resolve",
|
|
40081
|
-
fn:
|
|
41524
|
+
fn: resolve2
|
|
40082
41525
|
};
|
|
40083
41526
|
return this.onBeforeHandle(optionsOrResolve, hook);
|
|
40084
41527
|
}
|
|
@@ -40806,39 +42249,411 @@ var _Elysia = class _Elysia2 {
|
|
|
40806
42249
|
}
|
|
40807
42250
|
};
|
|
40808
42251
|
var Elysia = _Elysia;
|
|
42252
|
+
// packages/ratelimit/src/window.ts
|
|
42253
|
+
class SlidingWindow {
|
|
42254
|
+
windowMs;
|
|
42255
|
+
stamps = [];
|
|
42256
|
+
constructor(windowMs) {
|
|
42257
|
+
this.windowMs = windowMs;
|
|
42258
|
+
}
|
|
42259
|
+
count(now) {
|
|
42260
|
+
const cutoff = now - this.windowMs;
|
|
42261
|
+
let aged = 0;
|
|
42262
|
+
for (const stamp of this.stamps) {
|
|
42263
|
+
if (stamp > cutoff)
|
|
42264
|
+
break;
|
|
42265
|
+
aged++;
|
|
42266
|
+
}
|
|
42267
|
+
if (aged > 0)
|
|
42268
|
+
this.stamps = this.stamps.slice(aged);
|
|
42269
|
+
return this.stamps.length;
|
|
42270
|
+
}
|
|
42271
|
+
record(now) {
|
|
42272
|
+
this.stamps.push(now);
|
|
42273
|
+
}
|
|
42274
|
+
forget(at) {
|
|
42275
|
+
for (let index = this.stamps.length - 1;index >= 0; index--) {
|
|
42276
|
+
const stamp = this.stamps[index];
|
|
42277
|
+
if (stamp === undefined || stamp < at)
|
|
42278
|
+
return;
|
|
42279
|
+
if (stamp === at) {
|
|
42280
|
+
this.stamps.splice(index, 1);
|
|
42281
|
+
return;
|
|
42282
|
+
}
|
|
42283
|
+
}
|
|
42284
|
+
}
|
|
42285
|
+
resetAt(now) {
|
|
42286
|
+
const oldest = this.stamps[0];
|
|
42287
|
+
return oldest === undefined ? now : oldest + this.windowMs;
|
|
42288
|
+
}
|
|
42289
|
+
get empty() {
|
|
42290
|
+
return this.stamps.length === 0;
|
|
42291
|
+
}
|
|
42292
|
+
}
|
|
42293
|
+
|
|
42294
|
+
// packages/ratelimit/src/index.ts
|
|
42295
|
+
var WINDOWED = ["requests", "tokens", "spend"];
|
|
42296
|
+
function lastToClear(violations) {
|
|
42297
|
+
let chosen = null;
|
|
42298
|
+
for (const candidate of violations) {
|
|
42299
|
+
if (chosen === null || candidate.resetAt > chosen.resetAt) {
|
|
42300
|
+
chosen = candidate;
|
|
42301
|
+
continue;
|
|
42302
|
+
}
|
|
42303
|
+
if (candidate.resetAt === chosen.resetAt && candidate.used / candidate.limit > chosen.used / chosen.limit) {
|
|
42304
|
+
chosen = candidate;
|
|
42305
|
+
}
|
|
42306
|
+
}
|
|
42307
|
+
return chosen;
|
|
42308
|
+
}
|
|
42309
|
+
function evaluate(config2, counters, now) {
|
|
42310
|
+
const violations = [];
|
|
42311
|
+
const headroom = {};
|
|
42312
|
+
for (const dimension of WINDOWED) {
|
|
42313
|
+
const limits = config2[dimension];
|
|
42314
|
+
if (limits === undefined)
|
|
42315
|
+
continue;
|
|
42316
|
+
const observed = counters[dimension] ?? {};
|
|
42317
|
+
let nearest = null;
|
|
42318
|
+
for (const window2 of WINDOWS) {
|
|
42319
|
+
const limit = limits[window2];
|
|
42320
|
+
if (limit === undefined || limit === null)
|
|
42321
|
+
continue;
|
|
42322
|
+
const counter = observed[window2];
|
|
42323
|
+
const used = counter?.used ?? 0;
|
|
42324
|
+
const resetAt = counter?.resetAt ?? now + WINDOW_MS[window2];
|
|
42325
|
+
if (used >= limit)
|
|
42326
|
+
violations.push({ dimension, window: window2, limit, used, resetAt });
|
|
42327
|
+
const entry = {
|
|
42328
|
+
window: window2,
|
|
42329
|
+
limit,
|
|
42330
|
+
used,
|
|
42331
|
+
remaining: Math.max(0, limit - used),
|
|
42332
|
+
resetAt
|
|
42333
|
+
};
|
|
42334
|
+
if (nearest === null || used / limit > nearest.used / nearest.limit)
|
|
42335
|
+
nearest = entry;
|
|
42336
|
+
}
|
|
42337
|
+
if (nearest !== null)
|
|
42338
|
+
headroom[dimension] = nearest;
|
|
42339
|
+
}
|
|
42340
|
+
const concurrency = config2.concurrency;
|
|
42341
|
+
if (concurrency !== undefined && concurrency !== null) {
|
|
42342
|
+
const used = counters.concurrency ?? 0;
|
|
42343
|
+
if (used >= concurrency) {
|
|
42344
|
+
violations.push({
|
|
42345
|
+
dimension: "concurrency",
|
|
42346
|
+
window: null,
|
|
42347
|
+
limit: concurrency,
|
|
42348
|
+
used,
|
|
42349
|
+
resetAt: now
|
|
42350
|
+
});
|
|
42351
|
+
}
|
|
42352
|
+
}
|
|
42353
|
+
const violation = lastToClear(violations);
|
|
42354
|
+
return { allowed: violation === null, violation, headroom };
|
|
42355
|
+
}
|
|
42356
|
+
function retryAfterMs(violation, now) {
|
|
42357
|
+
return violation === null ? 0 : Math.max(0, violation.resetAt - now);
|
|
42358
|
+
}
|
|
40809
42359
|
|
|
40810
42360
|
// apps/gateway/src/auth/rateLimit.ts
|
|
40811
|
-
var
|
|
42361
|
+
var LONG_WINDOWS = ["5h", "1w"];
|
|
42362
|
+
var CACHE_TTL_MS = 30000;
|
|
42363
|
+
var EAGER_FRACTION = 0.9;
|
|
42364
|
+
var MAX_DEBITS = 1e4;
|
|
42365
|
+
|
|
42366
|
+
class RateLimitExceeded extends GatewayError {
|
|
42367
|
+
headroom;
|
|
42368
|
+
constructor(retryAfterMs2, headroom) {
|
|
42369
|
+
super("RATE_LIMIT", "API key rate limit exceeded", { retryAfterMs: retryAfterMs2 });
|
|
42370
|
+
this.headroom = headroom;
|
|
42371
|
+
}
|
|
42372
|
+
}
|
|
42373
|
+
function configured(limit) {
|
|
42374
|
+
return limit !== undefined && limit !== null;
|
|
42375
|
+
}
|
|
42376
|
+
function anyRequestLimit(limits) {
|
|
42377
|
+
for (const window2 of ["1m", "5h", "1w"]) {
|
|
42378
|
+
if (configured(limits.requests?.[window2]))
|
|
42379
|
+
return true;
|
|
42380
|
+
}
|
|
42381
|
+
return false;
|
|
42382
|
+
}
|
|
42383
|
+
function anyLimit(limits) {
|
|
42384
|
+
if (configured(limits.concurrency) || anyRequestLimit(limits))
|
|
42385
|
+
return true;
|
|
42386
|
+
for (const window2 of ["1m", "5h", "1w"]) {
|
|
42387
|
+
if (configured(limits.tokens?.[window2]))
|
|
42388
|
+
return true;
|
|
42389
|
+
}
|
|
42390
|
+
return configured(limits.spend?.["5h"]) || configured(limits.spend?.["1w"]);
|
|
42391
|
+
}
|
|
42392
|
+
function anyLongLimit(limits) {
|
|
42393
|
+
for (const window2 of LONG_WINDOWS) {
|
|
42394
|
+
if (configured(limits.requests?.[window2]))
|
|
42395
|
+
return true;
|
|
42396
|
+
if (configured(limits.tokens?.[window2]))
|
|
42397
|
+
return true;
|
|
42398
|
+
if (configured(limits.spend?.[window2]))
|
|
42399
|
+
return true;
|
|
42400
|
+
}
|
|
42401
|
+
return false;
|
|
42402
|
+
}
|
|
42403
|
+
function sinceRead(debits, readAt) {
|
|
42404
|
+
let requests = 0;
|
|
42405
|
+
let tokens2 = 0;
|
|
42406
|
+
let costUsd = 0;
|
|
42407
|
+
for (const debit of debits) {
|
|
42408
|
+
if (debit.at < readAt)
|
|
42409
|
+
continue;
|
|
42410
|
+
requests += debit.requests;
|
|
42411
|
+
tokens2 += debit.tokens;
|
|
42412
|
+
costUsd += debit.costUsd;
|
|
42413
|
+
}
|
|
42414
|
+
return { requests, tokens: tokens2, costUsd };
|
|
42415
|
+
}
|
|
42416
|
+
var LONGEST_WINDOW_MS = Math.max(...LONG_WINDOWS.map((window2) => WINDOW_MS[window2]));
|
|
42417
|
+
function trimDebits(debits, now) {
|
|
42418
|
+
const live = debits.filter((debit) => debit.at >= now - LONGEST_WINDOW_MS);
|
|
42419
|
+
if (live.length <= MAX_DEBITS)
|
|
42420
|
+
return live;
|
|
42421
|
+
const folded = { at: 0, requests: 0, tokens: 0, costUsd: 0 };
|
|
42422
|
+
const cut = live.length - MAX_DEBITS + 1;
|
|
42423
|
+
for (const debit of live.slice(0, cut)) {
|
|
42424
|
+
folded.at = Math.max(folded.at, debit.at);
|
|
42425
|
+
folded.requests += debit.requests;
|
|
42426
|
+
folded.tokens += debit.tokens;
|
|
42427
|
+
folded.costUsd += debit.costUsd;
|
|
42428
|
+
}
|
|
42429
|
+
return [folded, ...live.slice(cut)];
|
|
42430
|
+
}
|
|
40812
42431
|
|
|
40813
42432
|
class ApiKeyRateLimiter {
|
|
42433
|
+
keys = new Map;
|
|
42434
|
+
store;
|
|
40814
42435
|
now;
|
|
40815
|
-
|
|
40816
|
-
constructor(
|
|
40817
|
-
this.
|
|
42436
|
+
logger;
|
|
42437
|
+
constructor(deps) {
|
|
42438
|
+
this.store = deps.store;
|
|
42439
|
+
this.now = deps.now;
|
|
42440
|
+
this.logger = deps.logger ?? noopLogger;
|
|
42441
|
+
}
|
|
42442
|
+
async admit(keyId, limits, requestId) {
|
|
42443
|
+
if (!anyLimit(limits))
|
|
42444
|
+
return { release: () => {}, headroom: {} };
|
|
42445
|
+
const now = this.now();
|
|
42446
|
+
this.cleanup(now);
|
|
42447
|
+
const state = this.state(keyId, limits);
|
|
42448
|
+
const claim = this.claim(state, now, true);
|
|
42449
|
+
let admitted = false;
|
|
42450
|
+
try {
|
|
42451
|
+
const counters = await this.counters(state, keyId, limits, claim, now, requestId);
|
|
42452
|
+
const decision = evaluate(limits, counters, now);
|
|
42453
|
+
await this.refuse(keyId, decision, now, requestId);
|
|
42454
|
+
admitted = true;
|
|
42455
|
+
let released = false;
|
|
42456
|
+
const release = () => {
|
|
42457
|
+
if (released)
|
|
42458
|
+
return;
|
|
42459
|
+
released = true;
|
|
42460
|
+
state.inFlight = Math.max(0, state.inFlight - 1);
|
|
42461
|
+
};
|
|
42462
|
+
return { release, headroom: decision.headroom };
|
|
42463
|
+
} finally {
|
|
42464
|
+
state.deciding--;
|
|
42465
|
+
if (!admitted)
|
|
42466
|
+
this.rollback(state, claim);
|
|
42467
|
+
}
|
|
40818
42468
|
}
|
|
40819
|
-
consume(keyId,
|
|
40820
|
-
|
|
42469
|
+
async consume(keyId, limits, requestId) {
|
|
42470
|
+
const requests = limits.requests;
|
|
42471
|
+
if (requests === undefined || !anyRequestLimit(limits))
|
|
40821
42472
|
return;
|
|
40822
42473
|
const now = this.now();
|
|
40823
42474
|
this.cleanup(now);
|
|
40824
|
-
const
|
|
40825
|
-
const
|
|
40826
|
-
|
|
40827
|
-
|
|
40828
|
-
|
|
40829
|
-
|
|
42475
|
+
const state = this.state(keyId, limits);
|
|
42476
|
+
const claim = this.claim(state, now, false);
|
|
42477
|
+
let consumed = false;
|
|
42478
|
+
try {
|
|
42479
|
+
const counters = await this.counters(state, keyId, { requests }, claim, now, requestId);
|
|
42480
|
+
await this.refuse(keyId, evaluate({ requests }, counters, now), now, requestId);
|
|
42481
|
+
consumed = true;
|
|
42482
|
+
} finally {
|
|
42483
|
+
state.deciding--;
|
|
42484
|
+
if (!consumed)
|
|
42485
|
+
this.rollback(state, claim);
|
|
42486
|
+
}
|
|
42487
|
+
}
|
|
42488
|
+
claim(state, now, gauge) {
|
|
42489
|
+
const before = {
|
|
42490
|
+
requests: { used: state.ring.count(now), resetAt: state.ring.resetAt(now) },
|
|
42491
|
+
concurrency: state.inFlight
|
|
42492
|
+
};
|
|
42493
|
+
state.ring.record(now);
|
|
42494
|
+
if (gauge)
|
|
42495
|
+
state.inFlight++;
|
|
42496
|
+
state.deciding++;
|
|
42497
|
+
return { at: now, gauge, before };
|
|
42498
|
+
}
|
|
42499
|
+
rollback(state, claim) {
|
|
42500
|
+
state.ring.forget(claim.at);
|
|
42501
|
+
if (claim.gauge)
|
|
42502
|
+
state.inFlight = Math.max(0, state.inFlight - 1);
|
|
42503
|
+
}
|
|
42504
|
+
debit(keyId, usage) {
|
|
42505
|
+
const state = this.keys.get(keyId);
|
|
42506
|
+
if (state === undefined)
|
|
42507
|
+
return;
|
|
42508
|
+
if (!anyLongLimit(state.limits))
|
|
42509
|
+
return;
|
|
42510
|
+
state.debits.push({
|
|
42511
|
+
at: this.now(),
|
|
42512
|
+
requests: 1,
|
|
42513
|
+
tokens: usage.tokens,
|
|
42514
|
+
costUsd: usage.costUsd
|
|
42515
|
+
});
|
|
42516
|
+
this.markEager(state);
|
|
42517
|
+
}
|
|
42518
|
+
inFlight(keyId) {
|
|
42519
|
+
return this.keys.get(keyId)?.inFlight ?? 0;
|
|
42520
|
+
}
|
|
42521
|
+
pendingDebits(keyId) {
|
|
42522
|
+
return this.keys.get(keyId)?.debits.length ?? 0;
|
|
42523
|
+
}
|
|
42524
|
+
async refuse(keyId, decision, now, requestId) {
|
|
42525
|
+
const violation = decision.violation;
|
|
42526
|
+
if (violation === null)
|
|
42527
|
+
return;
|
|
42528
|
+
const window2 = violation.window;
|
|
42529
|
+
const exact = await this.exactReset(keyId, window2, now, requestId);
|
|
42530
|
+
const resolved = exact === null ? violation : { ...violation, resetAt: exact };
|
|
42531
|
+
const headroom = exact === null || window2 === null ? decision.headroom : withReset(decision.headroom, window2, exact);
|
|
42532
|
+
throw new RateLimitExceeded(retryAfterMs(resolved, now), headroom);
|
|
42533
|
+
}
|
|
42534
|
+
async exactReset(keyId, window2, now, requestId) {
|
|
42535
|
+
if (window2 === null || window2 === "1m")
|
|
42536
|
+
return null;
|
|
42537
|
+
const length = WINDOW_MS[window2];
|
|
42538
|
+
try {
|
|
42539
|
+
const oldest = await this.store.usage.oldestSince(keyId, now - length);
|
|
42540
|
+
if (oldest === null)
|
|
42541
|
+
return null;
|
|
42542
|
+
return oldest + length;
|
|
42543
|
+
} catch (error51) {
|
|
42544
|
+
this.logger.warn("rate limit reset unavailable", {
|
|
42545
|
+
...requestId === undefined ? {} : { requestId },
|
|
42546
|
+
apiKeyId: keyId,
|
|
42547
|
+
reason: error51 instanceof Error ? error51.message : "unknown"
|
|
42548
|
+
});
|
|
42549
|
+
return null;
|
|
42550
|
+
}
|
|
42551
|
+
}
|
|
42552
|
+
state(keyId, limits) {
|
|
42553
|
+
const existing = this.keys.get(keyId);
|
|
42554
|
+
if (existing !== undefined) {
|
|
42555
|
+
existing.limits = limits;
|
|
42556
|
+
return existing;
|
|
42557
|
+
}
|
|
42558
|
+
const created = {
|
|
42559
|
+
ring: new SlidingWindow(WINDOW_MS["1m"]),
|
|
42560
|
+
inFlight: 0,
|
|
42561
|
+
deciding: 0,
|
|
42562
|
+
debits: [],
|
|
42563
|
+
counts: null,
|
|
42564
|
+
stale: false,
|
|
42565
|
+
limits
|
|
42566
|
+
};
|
|
42567
|
+
this.keys.set(keyId, created);
|
|
42568
|
+
return created;
|
|
42569
|
+
}
|
|
42570
|
+
async counters(state, keyId, limits, claim, now, requestId) {
|
|
42571
|
+
const requests = { "1m": claim.before.requests };
|
|
42572
|
+
const snapshot = { requests, concurrency: claim.before.concurrency };
|
|
42573
|
+
if (!anyLongLimit(limits))
|
|
42574
|
+
return snapshot;
|
|
42575
|
+
const counts = await this.longCounts(state, keyId, now, requestId);
|
|
42576
|
+
if (counts === null)
|
|
42577
|
+
return snapshot;
|
|
42578
|
+
const tokens2 = {};
|
|
42579
|
+
const spend = {};
|
|
42580
|
+
const delta2 = sinceRead(state.debits, counts.readAt);
|
|
42581
|
+
for (const window2 of LONG_WINDOWS) {
|
|
42582
|
+
const sums = counts.sums[window2];
|
|
42583
|
+
const resetAt = now + WINDOW_MS[window2];
|
|
42584
|
+
requests[window2] = { used: sums.requests + delta2.requests, resetAt };
|
|
42585
|
+
tokens2[window2] = { used: sums.tokens + delta2.tokens, resetAt };
|
|
42586
|
+
spend[window2] = { used: sums.costUsd + delta2.costUsd, resetAt };
|
|
42587
|
+
}
|
|
42588
|
+
snapshot.tokens = tokens2;
|
|
42589
|
+
snapshot.spend = spend;
|
|
42590
|
+
return snapshot;
|
|
42591
|
+
}
|
|
42592
|
+
async longCounts(state, keyId, now, requestId) {
|
|
42593
|
+
const cached2 = state.counts;
|
|
42594
|
+
if (cached2 !== null && !state.stale && now - cached2.readAt < CACHE_TTL_MS)
|
|
42595
|
+
return cached2;
|
|
42596
|
+
const readAt = now;
|
|
42597
|
+
try {
|
|
42598
|
+
const [fiveHour, oneWeek] = await Promise.all([
|
|
42599
|
+
this.store.usage.sumSince(keyId, readAt - WINDOW_MS["5h"]),
|
|
42600
|
+
this.store.usage.sumSince(keyId, readAt - WINDOW_MS["1w"])
|
|
42601
|
+
]);
|
|
42602
|
+
const newer = state.counts;
|
|
42603
|
+
if (newer !== null && newer.readAt > readAt)
|
|
42604
|
+
return newer;
|
|
42605
|
+
state.counts = { readAt, sums: { "5h": fiveHour, "1w": oneWeek } };
|
|
42606
|
+
state.stale = false;
|
|
42607
|
+
state.debits = state.debits.filter((debit) => debit.at >= readAt);
|
|
42608
|
+
return state.counts;
|
|
42609
|
+
} catch (error51) {
|
|
42610
|
+
this.logger.warn("rate limit counters unavailable", {
|
|
42611
|
+
...requestId === undefined ? {} : { requestId },
|
|
42612
|
+
apiKeyId: keyId,
|
|
42613
|
+
reason: error51 instanceof Error ? error51.message : "unknown"
|
|
40830
42614
|
});
|
|
42615
|
+
state.debits = trimDebits(state.debits, now);
|
|
42616
|
+
return null;
|
|
42617
|
+
}
|
|
42618
|
+
}
|
|
42619
|
+
markEager(state) {
|
|
42620
|
+
const counts = state.counts;
|
|
42621
|
+
if (counts === null || state.stale)
|
|
42622
|
+
return;
|
|
42623
|
+
const delta2 = sinceRead(state.debits, counts.readAt);
|
|
42624
|
+
for (const window2 of LONG_WINDOWS) {
|
|
42625
|
+
const sums = counts.sums[window2];
|
|
42626
|
+
if (near(state.limits.requests?.[window2], sums.requests + delta2.requests) || near(state.limits.tokens?.[window2], sums.tokens + delta2.tokens) || near(state.limits.spend?.[window2], sums.costUsd + delta2.costUsd)) {
|
|
42627
|
+
state.stale = true;
|
|
42628
|
+
return;
|
|
42629
|
+
}
|
|
40831
42630
|
}
|
|
40832
|
-
window2.count++;
|
|
40833
|
-
this.windows.set(keyId, window2);
|
|
40834
42631
|
}
|
|
40835
42632
|
cleanup(now) {
|
|
40836
|
-
for (const [keyId,
|
|
40837
|
-
|
|
40838
|
-
|
|
42633
|
+
for (const [keyId, state] of this.keys) {
|
|
42634
|
+
state.ring.count(now);
|
|
42635
|
+
if (state.deciding > 0 || !state.ring.empty || state.inFlight > 0)
|
|
42636
|
+
continue;
|
|
42637
|
+
if (state.debits.length > 0)
|
|
42638
|
+
continue;
|
|
42639
|
+
if (state.counts !== null && now - state.counts.readAt < CACHE_TTL_MS)
|
|
42640
|
+
continue;
|
|
42641
|
+
this.keys.delete(keyId);
|
|
40839
42642
|
}
|
|
40840
42643
|
}
|
|
40841
42644
|
}
|
|
42645
|
+
function near(limit, used) {
|
|
42646
|
+
return configured(limit) && used >= limit * EAGER_FRACTION;
|
|
42647
|
+
}
|
|
42648
|
+
function withReset(headroom, window2, resetAt) {
|
|
42649
|
+
const corrected = { ...headroom };
|
|
42650
|
+
for (const dimension of ["requests", "tokens", "spend"]) {
|
|
42651
|
+
const entry = corrected[dimension];
|
|
42652
|
+
if (entry !== undefined && entry.window === window2)
|
|
42653
|
+
corrected[dimension] = { ...entry, resetAt };
|
|
42654
|
+
}
|
|
42655
|
+
return corrected;
|
|
42656
|
+
}
|
|
40842
42657
|
|
|
40843
42658
|
// apps/gateway/src/dispatch/snapshotCache.ts
|
|
40844
42659
|
function createRoutingSnapshotCache(store, logger2 = noopLogger) {
|
|
@@ -40896,7 +42711,415 @@ function createRoutingSnapshotCache(store, logger2 = noopLogger) {
|
|
|
40896
42711
|
});
|
|
40897
42712
|
return inFlight;
|
|
40898
42713
|
};
|
|
40899
|
-
return {
|
|
42714
|
+
return {
|
|
42715
|
+
get,
|
|
42716
|
+
invalidate() {
|
|
42717
|
+
generation++;
|
|
42718
|
+
snapshot = null;
|
|
42719
|
+
stale = true;
|
|
42720
|
+
version2 = store.routing.version();
|
|
42721
|
+
},
|
|
42722
|
+
close: unsubscribe
|
|
42723
|
+
};
|
|
42724
|
+
}
|
|
42725
|
+
|
|
42726
|
+
// apps/gateway/src/egress/anthropic.ts
|
|
42727
|
+
var STOP_REASON2 = {
|
|
42728
|
+
endTurn: "end_turn",
|
|
42729
|
+
maxTokens: "max_tokens",
|
|
42730
|
+
stopSequence: "stop_sequence",
|
|
42731
|
+
toolUse: "tool_use",
|
|
42732
|
+
contentFilter: "refusal",
|
|
42733
|
+
pauseTurn: "pause_turn"
|
|
42734
|
+
};
|
|
42735
|
+
var ERROR_TYPE2 = {
|
|
42736
|
+
AUTH: "authentication_error",
|
|
42737
|
+
RATE_LIMIT: "rate_limit_error",
|
|
42738
|
+
QUOTA_EXHAUSTED: "rate_limit_error",
|
|
42739
|
+
OVERLOADED: "overloaded_error",
|
|
42740
|
+
BAD_REQUEST: "invalid_request_error",
|
|
42741
|
+
CONFLICT: "invalid_request_error",
|
|
42742
|
+
CONTENT_FILTER: "invalid_request_error",
|
|
42743
|
+
CAPABILITY_MISMATCH: "invalid_request_error",
|
|
42744
|
+
MODEL_UNAVAILABLE: "not_found_error",
|
|
42745
|
+
UPSTREAM: "api_error",
|
|
42746
|
+
TIMEOUT: "api_error",
|
|
42747
|
+
NETWORK: "api_error",
|
|
42748
|
+
NO_CANDIDATES: "overloaded_error",
|
|
42749
|
+
ALL_CANDIDATES_FAILED: "api_error",
|
|
42750
|
+
INTERNAL: "api_error"
|
|
42751
|
+
};
|
|
42752
|
+
var frame = (event, data) => ({
|
|
42753
|
+
event,
|
|
42754
|
+
data: JSON.stringify(data)
|
|
42755
|
+
});
|
|
42756
|
+
async function* anthropicStream(events, requestId) {
|
|
42757
|
+
let model = "";
|
|
42758
|
+
const suppressed2 = new Set;
|
|
42759
|
+
const outIndex = new Map;
|
|
42760
|
+
let nextOutIndex = 0;
|
|
42761
|
+
for await (const event of events) {
|
|
42762
|
+
switch (event.type) {
|
|
42763
|
+
case "start":
|
|
42764
|
+
model = event.model;
|
|
42765
|
+
yield frame("message_start", {
|
|
42766
|
+
type: "message_start",
|
|
42767
|
+
message: {
|
|
42768
|
+
id: requestId,
|
|
42769
|
+
type: "message",
|
|
42770
|
+
role: "assistant",
|
|
42771
|
+
model,
|
|
42772
|
+
content: [],
|
|
42773
|
+
stop_reason: null,
|
|
42774
|
+
stop_sequence: null,
|
|
42775
|
+
usage: {
|
|
42776
|
+
input_tokens: 0,
|
|
42777
|
+
output_tokens: 0,
|
|
42778
|
+
cache_read_input_tokens: 0,
|
|
42779
|
+
cache_creation_input_tokens: 0
|
|
42780
|
+
}
|
|
42781
|
+
}
|
|
42782
|
+
});
|
|
42783
|
+
break;
|
|
42784
|
+
case "blockStart": {
|
|
42785
|
+
const b = event.block;
|
|
42786
|
+
if (b.type === "thinking" && b.signed !== true) {
|
|
42787
|
+
suppressed2.add(event.index);
|
|
42788
|
+
break;
|
|
42789
|
+
}
|
|
42790
|
+
const content_block = b.type === "text" ? { type: "text", text: "" } : b.type === "thinking" ? { type: "thinking", thinking: "" } : b.type === "anthropicNative" ? { ...b.data, type: b.blockType } : { type: "tool_use", id: b.id, name: b.name, input: {} };
|
|
42791
|
+
const index = nextOutIndex++;
|
|
42792
|
+
outIndex.set(event.index, index);
|
|
42793
|
+
yield frame("content_block_start", {
|
|
42794
|
+
type: "content_block_start",
|
|
42795
|
+
index,
|
|
42796
|
+
content_block
|
|
42797
|
+
});
|
|
42798
|
+
break;
|
|
42799
|
+
}
|
|
42800
|
+
case "blockDelta": {
|
|
42801
|
+
if (suppressed2.has(event.index))
|
|
42802
|
+
break;
|
|
42803
|
+
const d = event.delta;
|
|
42804
|
+
const delta2 = d.type === "text" ? { type: "text_delta", text: d.text } : d.type === "thinking" ? { type: "thinking_delta", thinking: d.text } : d.type === "thinkingSignature" ? { type: "signature_delta", signature: d.signature } : d.type === "anthropicNative" ? { type: d.deltaType, ...d.data } : { type: "input_json_delta", partial_json: d.partial };
|
|
42805
|
+
yield frame("content_block_delta", {
|
|
42806
|
+
type: "content_block_delta",
|
|
42807
|
+
index: outIndex.get(event.index) ?? event.index,
|
|
42808
|
+
delta: delta2
|
|
42809
|
+
});
|
|
42810
|
+
break;
|
|
42811
|
+
}
|
|
42812
|
+
case "blockEnd":
|
|
42813
|
+
if (suppressed2.has(event.index))
|
|
42814
|
+
break;
|
|
42815
|
+
yield frame("content_block_stop", {
|
|
42816
|
+
type: "content_block_stop",
|
|
42817
|
+
index: outIndex.get(event.index) ?? event.index
|
|
42818
|
+
});
|
|
42819
|
+
break;
|
|
42820
|
+
case "end":
|
|
42821
|
+
yield frame("message_delta", {
|
|
42822
|
+
type: "message_delta",
|
|
42823
|
+
delta: { stop_reason: STOP_REASON2[event.stopReason], stop_sequence: null },
|
|
42824
|
+
usage: {
|
|
42825
|
+
input_tokens: event.usage.inputTokens,
|
|
42826
|
+
output_tokens: event.usage.outputTokens,
|
|
42827
|
+
cache_read_input_tokens: event.usage.cacheReadTokens,
|
|
42828
|
+
cache_creation_input_tokens: event.usage.cacheWriteTokens
|
|
42829
|
+
}
|
|
42830
|
+
});
|
|
42831
|
+
yield frame("message_stop", { type: "message_stop" });
|
|
42832
|
+
break;
|
|
42833
|
+
case "error":
|
|
42834
|
+
yield frame("error", {
|
|
42835
|
+
type: "error",
|
|
42836
|
+
error: { type: ERROR_TYPE2[event.code], message: event.message }
|
|
42837
|
+
});
|
|
42838
|
+
break;
|
|
42839
|
+
}
|
|
42840
|
+
}
|
|
42841
|
+
}
|
|
42842
|
+
function anthropicResponse(collected, requestId) {
|
|
42843
|
+
return {
|
|
42844
|
+
id: requestId,
|
|
42845
|
+
type: "message",
|
|
42846
|
+
role: "assistant",
|
|
42847
|
+
model: collected.model,
|
|
42848
|
+
content: collected.content.filter((b) => b.type !== "thinking" || b.signature !== undefined && b.signature !== "").map((b) => {
|
|
42849
|
+
switch (b.type) {
|
|
42850
|
+
case "text":
|
|
42851
|
+
return {
|
|
42852
|
+
type: "text",
|
|
42853
|
+
text: b.text,
|
|
42854
|
+
...b.citations === undefined ? {} : { citations: b.citations }
|
|
42855
|
+
};
|
|
42856
|
+
case "thinking":
|
|
42857
|
+
return { type: "thinking", thinking: b.text, signature: b.signature };
|
|
42858
|
+
case "toolUse":
|
|
42859
|
+
return { type: "tool_use", id: b.id, name: b.name, input: b.input };
|
|
42860
|
+
case "anthropicNative":
|
|
42861
|
+
return { ...b.data, type: b.blockType };
|
|
42862
|
+
default:
|
|
42863
|
+
return { type: "text", text: "" };
|
|
42864
|
+
}
|
|
42865
|
+
}),
|
|
42866
|
+
stop_reason: STOP_REASON2[collected.stopReason],
|
|
42867
|
+
stop_sequence: null,
|
|
42868
|
+
usage: {
|
|
42869
|
+
input_tokens: collected.usage.inputTokens,
|
|
42870
|
+
output_tokens: collected.usage.outputTokens,
|
|
42871
|
+
cache_read_input_tokens: collected.usage.cacheReadTokens,
|
|
42872
|
+
cache_creation_input_tokens: collected.usage.cacheWriteTokens
|
|
42873
|
+
}
|
|
42874
|
+
};
|
|
42875
|
+
}
|
|
42876
|
+
function anthropicErrorBody(code, message) {
|
|
42877
|
+
return { type: "error", error: { type: ERROR_TYPE2[code], message } };
|
|
42878
|
+
}
|
|
42879
|
+
var REPORTED_LIMIT_DIMENSIONS = ["requests", "tokens"];
|
|
42880
|
+
function rfc3339(at) {
|
|
42881
|
+
return `${new Date(Math.ceil(at / 1000) * 1000).toISOString().slice(0, 19)}Z`;
|
|
42882
|
+
}
|
|
42883
|
+
function anthropicRateLimitHeaders(headroom) {
|
|
42884
|
+
const headers = {};
|
|
42885
|
+
for (const dimension of REPORTED_LIMIT_DIMENSIONS) {
|
|
42886
|
+
const entry = headroom[dimension];
|
|
42887
|
+
if (entry === undefined)
|
|
42888
|
+
continue;
|
|
42889
|
+
headers[`anthropic-ratelimit-${dimension}-limit`] = String(entry.limit);
|
|
42890
|
+
headers[`anthropic-ratelimit-${dimension}-remaining`] = String(entry.remaining);
|
|
42891
|
+
headers[`anthropic-ratelimit-${dimension}-reset`] = rfc3339(entry.resetAt);
|
|
42892
|
+
}
|
|
42893
|
+
return headers;
|
|
42894
|
+
}
|
|
42895
|
+
|
|
42896
|
+
// apps/gateway/src/egress/openai.ts
|
|
42897
|
+
function usageBody(usage) {
|
|
42898
|
+
const prompt = promptTokens(usage);
|
|
42899
|
+
return {
|
|
42900
|
+
prompt_tokens: prompt,
|
|
42901
|
+
completion_tokens: usage.outputTokens,
|
|
42902
|
+
total_tokens: prompt + usage.outputTokens,
|
|
42903
|
+
prompt_tokens_details: { cached_tokens: usage.cacheReadTokens }
|
|
42904
|
+
};
|
|
42905
|
+
}
|
|
42906
|
+
var FINISH3 = {
|
|
42907
|
+
endTurn: "stop",
|
|
42908
|
+
maxTokens: "length",
|
|
42909
|
+
stopSequence: "stop",
|
|
42910
|
+
toolUse: "tool_calls",
|
|
42911
|
+
contentFilter: "content_filter",
|
|
42912
|
+
pauseTurn: "stop"
|
|
42913
|
+
};
|
|
42914
|
+
var ERROR_TYPE3 = {
|
|
42915
|
+
AUTH: { type: "invalid_request_error", code: "invalid_api_key" },
|
|
42916
|
+
RATE_LIMIT: { type: "rate_limit_error", code: "rate_limit_exceeded" },
|
|
42917
|
+
QUOTA_EXHAUSTED: { type: "insufficient_quota", code: "insufficient_quota" },
|
|
42918
|
+
OVERLOADED: { type: "server_error", code: "server_error" },
|
|
42919
|
+
BAD_REQUEST: { type: "invalid_request_error", code: "invalid_request" },
|
|
42920
|
+
CONFLICT: { type: "invalid_request_error", code: "conflict" },
|
|
42921
|
+
CONTENT_FILTER: { type: "invalid_request_error", code: "content_policy_violation" },
|
|
42922
|
+
CAPABILITY_MISMATCH: { type: "invalid_request_error", code: "invalid_request" },
|
|
42923
|
+
MODEL_UNAVAILABLE: { type: "invalid_request_error", code: "model_not_found" },
|
|
42924
|
+
UPSTREAM: { type: "server_error", code: "server_error" },
|
|
42925
|
+
TIMEOUT: { type: "server_error", code: "timeout" },
|
|
42926
|
+
NETWORK: { type: "server_error", code: "server_error" },
|
|
42927
|
+
NO_CANDIDATES: { type: "server_error", code: "service_unavailable" },
|
|
42928
|
+
ALL_CANDIDATES_FAILED: { type: "server_error", code: "server_error" },
|
|
42929
|
+
INTERNAL: { type: "server_error", code: "server_error" }
|
|
42930
|
+
};
|
|
42931
|
+
var chunk = (id, created, model, choice, usage) => ({
|
|
42932
|
+
id,
|
|
42933
|
+
object: "chat.completion.chunk",
|
|
42934
|
+
created,
|
|
42935
|
+
model,
|
|
42936
|
+
choices: [choice],
|
|
42937
|
+
...usage === undefined ? {} : { usage }
|
|
42938
|
+
});
|
|
42939
|
+
async function* openaiStream(events, requestId, created) {
|
|
42940
|
+
let model = "";
|
|
42941
|
+
let roleSent = false;
|
|
42942
|
+
let completed = false;
|
|
42943
|
+
const toolIndex = new Map;
|
|
42944
|
+
const emit = (data) => ({ event: "message", data: JSON.stringify(data) });
|
|
42945
|
+
for await (const event of events) {
|
|
42946
|
+
switch (event.type) {
|
|
42947
|
+
case "start":
|
|
42948
|
+
model = event.model;
|
|
42949
|
+
break;
|
|
42950
|
+
case "blockStart": {
|
|
42951
|
+
if (event.block.type === "text") {
|
|
42952
|
+
if (!roleSent) {
|
|
42953
|
+
roleSent = true;
|
|
42954
|
+
yield emit(chunk(requestId, created, model, {
|
|
42955
|
+
index: 0,
|
|
42956
|
+
delta: { role: "assistant", content: "" },
|
|
42957
|
+
finish_reason: null
|
|
42958
|
+
}));
|
|
42959
|
+
}
|
|
42960
|
+
} else if (event.block.type === "toolUse") {
|
|
42961
|
+
const index = toolIndex.size;
|
|
42962
|
+
toolIndex.set(event.index, index);
|
|
42963
|
+
const firstRole = roleSent ? {} : { role: "assistant" };
|
|
42964
|
+
roleSent = true;
|
|
42965
|
+
yield emit(chunk(requestId, created, model, {
|
|
42966
|
+
index: 0,
|
|
42967
|
+
delta: {
|
|
42968
|
+
...firstRole,
|
|
42969
|
+
tool_calls: [
|
|
42970
|
+
{
|
|
42971
|
+
index,
|
|
42972
|
+
id: event.block.id,
|
|
42973
|
+
type: "function",
|
|
42974
|
+
function: { name: event.block.name, arguments: "" }
|
|
42975
|
+
}
|
|
42976
|
+
]
|
|
42977
|
+
},
|
|
42978
|
+
finish_reason: null
|
|
42979
|
+
}));
|
|
42980
|
+
}
|
|
42981
|
+
break;
|
|
42982
|
+
}
|
|
42983
|
+
case "blockDelta": {
|
|
42984
|
+
const d = event.delta;
|
|
42985
|
+
if (d.type === "text") {
|
|
42986
|
+
yield emit(chunk(requestId, created, model, {
|
|
42987
|
+
index: 0,
|
|
42988
|
+
delta: { content: d.text },
|
|
42989
|
+
finish_reason: null
|
|
42990
|
+
}));
|
|
42991
|
+
} else if (d.type === "toolJson") {
|
|
42992
|
+
const index = toolIndex.get(event.index) ?? 0;
|
|
42993
|
+
yield emit(chunk(requestId, created, model, {
|
|
42994
|
+
index: 0,
|
|
42995
|
+
delta: { tool_calls: [{ index, function: { arguments: d.partial } }] },
|
|
42996
|
+
finish_reason: null
|
|
42997
|
+
}));
|
|
42998
|
+
}
|
|
42999
|
+
break;
|
|
43000
|
+
}
|
|
43001
|
+
case "blockEnd":
|
|
43002
|
+
break;
|
|
43003
|
+
case "end":
|
|
43004
|
+
completed = true;
|
|
43005
|
+
yield emit(chunk(requestId, created, model, { index: 0, delta: {}, finish_reason: FINISH3[event.stopReason] }, usageBody(event.usage)));
|
|
43006
|
+
break;
|
|
43007
|
+
case "error": {
|
|
43008
|
+
const e = ERROR_TYPE3[event.code];
|
|
43009
|
+
yield emit({ error: { message: event.message, type: e.type, code: e.code } });
|
|
43010
|
+
break;
|
|
43011
|
+
}
|
|
43012
|
+
}
|
|
43013
|
+
}
|
|
43014
|
+
if (completed)
|
|
43015
|
+
yield { event: "message", data: "[DONE]" };
|
|
43016
|
+
}
|
|
43017
|
+
function openaiResponse(collected, requestId, created) {
|
|
43018
|
+
const text = collected.content.flatMap((b) => b.type === "text" ? [b.text] : []).join("");
|
|
43019
|
+
const toolCalls = collected.content.flatMap((b) => b.type === "toolUse" ? [
|
|
43020
|
+
{
|
|
43021
|
+
id: b.id,
|
|
43022
|
+
type: "function",
|
|
43023
|
+
function: { name: b.name, arguments: JSON.stringify(b.input) }
|
|
43024
|
+
}
|
|
43025
|
+
] : []);
|
|
43026
|
+
return {
|
|
43027
|
+
id: requestId,
|
|
43028
|
+
object: "chat.completion",
|
|
43029
|
+
created,
|
|
43030
|
+
model: collected.model,
|
|
43031
|
+
choices: [
|
|
43032
|
+
{
|
|
43033
|
+
index: 0,
|
|
43034
|
+
message: {
|
|
43035
|
+
role: "assistant",
|
|
43036
|
+
content: toolCalls.length > 0 && text.length === 0 ? null : text,
|
|
43037
|
+
...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
|
|
43038
|
+
},
|
|
43039
|
+
finish_reason: FINISH3[collected.stopReason]
|
|
43040
|
+
}
|
|
43041
|
+
],
|
|
43042
|
+
usage: usageBody(collected.usage)
|
|
43043
|
+
};
|
|
43044
|
+
}
|
|
43045
|
+
function openaiErrorBody(code, message) {
|
|
43046
|
+
const e = ERROR_TYPE3[code];
|
|
43047
|
+
return { error: { message, type: e.type, code: e.code } };
|
|
43048
|
+
}
|
|
43049
|
+
function duration3(ms) {
|
|
43050
|
+
const total = Math.max(0, Math.ceil(ms / 1000));
|
|
43051
|
+
const hours = Math.floor(total / 3600);
|
|
43052
|
+
const minutes = Math.floor(total % 3600 / 60);
|
|
43053
|
+
const seconds = total % 60;
|
|
43054
|
+
if (hours > 0)
|
|
43055
|
+
return `${hours}h${minutes}m${seconds}s`;
|
|
43056
|
+
if (minutes > 0)
|
|
43057
|
+
return `${minutes}m${seconds}s`;
|
|
43058
|
+
return `${seconds}s`;
|
|
43059
|
+
}
|
|
43060
|
+
function openaiRateLimitHeaders(headroom, now) {
|
|
43061
|
+
const headers = {};
|
|
43062
|
+
for (const dimension of REPORTED_LIMIT_DIMENSIONS) {
|
|
43063
|
+
const entry = headroom[dimension];
|
|
43064
|
+
if (entry === undefined)
|
|
43065
|
+
continue;
|
|
43066
|
+
headers[`x-ratelimit-limit-${dimension}`] = String(entry.limit);
|
|
43067
|
+
headers[`x-ratelimit-remaining-${dimension}`] = String(entry.remaining);
|
|
43068
|
+
headers[`x-ratelimit-reset-${dimension}`] = duration3(entry.resetAt - now);
|
|
43069
|
+
}
|
|
43070
|
+
return headers;
|
|
43071
|
+
}
|
|
43072
|
+
|
|
43073
|
+
// apps/gateway/src/quiesce.ts
|
|
43074
|
+
function createQuiesceLatch() {
|
|
43075
|
+
let admitting = true;
|
|
43076
|
+
let inFlight = 0;
|
|
43077
|
+
let onIdle = null;
|
|
43078
|
+
const settle = () => {
|
|
43079
|
+
if (inFlight > 0 || onIdle === null)
|
|
43080
|
+
return;
|
|
43081
|
+
const resolve2 = onIdle;
|
|
43082
|
+
onIdle = null;
|
|
43083
|
+
resolve2();
|
|
43084
|
+
};
|
|
43085
|
+
return {
|
|
43086
|
+
enter() {
|
|
43087
|
+
if (!admitting)
|
|
43088
|
+
return null;
|
|
43089
|
+
inFlight++;
|
|
43090
|
+
let released = false;
|
|
43091
|
+
return () => {
|
|
43092
|
+
if (released)
|
|
43093
|
+
return;
|
|
43094
|
+
released = true;
|
|
43095
|
+
inFlight--;
|
|
43096
|
+
settle();
|
|
43097
|
+
};
|
|
43098
|
+
},
|
|
43099
|
+
isClosed: () => !admitting,
|
|
43100
|
+
open() {
|
|
43101
|
+
admitting = true;
|
|
43102
|
+
},
|
|
43103
|
+
async close(deadlineMs) {
|
|
43104
|
+
admitting = false;
|
|
43105
|
+
if (inFlight === 0)
|
|
43106
|
+
return { drained: true, inFlight: 0 };
|
|
43107
|
+
let timer;
|
|
43108
|
+
const idle = new Promise((resolve2) => {
|
|
43109
|
+
onIdle = resolve2;
|
|
43110
|
+
});
|
|
43111
|
+
const deadline = new Promise((resolve2) => {
|
|
43112
|
+
timer = setTimeout(resolve2, deadlineMs);
|
|
43113
|
+
});
|
|
43114
|
+
try {
|
|
43115
|
+
await Promise.race([idle, deadline]);
|
|
43116
|
+
} finally {
|
|
43117
|
+
clearTimeout(timer);
|
|
43118
|
+
onIdle = null;
|
|
43119
|
+
}
|
|
43120
|
+
return { drained: inFlight === 0, inFlight };
|
|
43121
|
+
}
|
|
43122
|
+
};
|
|
40900
43123
|
}
|
|
40901
43124
|
|
|
40902
43125
|
// apps/gateway/src/ingress/schemas.ts
|
|
@@ -40948,6 +43171,48 @@ function parseDataUrl(url2) {
|
|
|
40948
43171
|
}
|
|
40949
43172
|
return { mediaType: match[1], data: match[2] };
|
|
40950
43173
|
}
|
|
43174
|
+
var BASE64_MAGIC = [
|
|
43175
|
+
["iVBORw0KGgo", "image/png"],
|
|
43176
|
+
["/9j/", "image/jpeg"],
|
|
43177
|
+
["R0lGOD", "image/gif"],
|
|
43178
|
+
["UklGR", "image/webp"]
|
|
43179
|
+
];
|
|
43180
|
+
function readSidecarImage(raw, declaredMediaType) {
|
|
43181
|
+
if (!raw.startsWith("data:") && /^[a-z][a-z0-9+.-]*:/i.test(raw)) {
|
|
43182
|
+
return { ok: false, reason: "remote", detail: "the gateway does not fetch remote images" };
|
|
43183
|
+
}
|
|
43184
|
+
const dataUrl = raw.startsWith("data:") ? /^data:([^;,]+);base64,(.*)$/s.exec(raw) : null;
|
|
43185
|
+
if (raw.startsWith("data:") && dataUrl === null) {
|
|
43186
|
+
return {
|
|
43187
|
+
ok: false,
|
|
43188
|
+
reason: "unrecognized",
|
|
43189
|
+
detail: "data URL must carry base64 content"
|
|
43190
|
+
};
|
|
43191
|
+
}
|
|
43192
|
+
const resolved = dataUrl === null ? { mediaType: BASE64_MAGIC.find(([prefix]) => raw.startsWith(prefix))?.[1], data: raw } : { mediaType: dataUrl[1], data: dataUrl[2] };
|
|
43193
|
+
const mediaType = resolved.mediaType ?? declaredMediaType;
|
|
43194
|
+
if (mediaType === undefined) {
|
|
43195
|
+
return {
|
|
43196
|
+
ok: false,
|
|
43197
|
+
reason: "unrecognized",
|
|
43198
|
+
detail: "unrecognized image data; expected PNG, JPEG, GIF or WebP"
|
|
43199
|
+
};
|
|
43200
|
+
}
|
|
43201
|
+
if (!mediaType.startsWith("image/")) {
|
|
43202
|
+
return { ok: false, reason: "not-an-image", detail: `${mediaType} is not an image` };
|
|
43203
|
+
}
|
|
43204
|
+
return { ok: true, mediaType, data: resolved.data };
|
|
43205
|
+
}
|
|
43206
|
+
function requireSidecarImage(raw, field) {
|
|
43207
|
+
const result = readSidecarImage(raw, undefined);
|
|
43208
|
+
if (!result.ok)
|
|
43209
|
+
throw new GatewayError("BAD_REQUEST", `${field}: ${result.detail}`);
|
|
43210
|
+
return { mediaType: result.mediaType, data: result.data };
|
|
43211
|
+
}
|
|
43212
|
+
function optionalSidecarImage(raw, declaredMediaType) {
|
|
43213
|
+
const result = readSidecarImage(raw, declaredMediaType);
|
|
43214
|
+
return result.ok ? { mediaType: result.mediaType, data: result.data } : null;
|
|
43215
|
+
}
|
|
40951
43216
|
|
|
40952
43217
|
// apps/gateway/src/routes/http.ts
|
|
40953
43218
|
function readCookie(request2, name) {
|
|
@@ -41123,13 +43388,19 @@ function adminRoutes(deps) {
|
|
|
41123
43388
|
}).post("/api/keys", async ({ request: request2 }) => {
|
|
41124
43389
|
await requireAdmin(request2, deps.admin);
|
|
41125
43390
|
return createKey(deps.store, await readJson(request2));
|
|
43391
|
+
}).put("/api/keys/:id/limits", async ({ request: request2, params }) => {
|
|
43392
|
+
await requireAdmin(request2, deps.admin);
|
|
43393
|
+
return setKeyLimits(deps.store, params.id, await readJson(request2));
|
|
41126
43394
|
}).delete("/api/keys/:id", async ({ request: request2, params }) => {
|
|
41127
43395
|
await requireAdmin(request2, deps.admin);
|
|
41128
43396
|
await revokeKey(deps.store, params.id);
|
|
41129
43397
|
return { ok: true };
|
|
41130
43398
|
}).get("/api/settings", async ({ request: request2 }) => {
|
|
41131
43399
|
await requireAdmin(request2, deps.admin);
|
|
41132
|
-
return {
|
|
43400
|
+
return {
|
|
43401
|
+
settings: await getSettings(deps.store),
|
|
43402
|
+
bodyLoggingAllowed: deps.bodyLoggingAllowed === true
|
|
43403
|
+
};
|
|
41133
43404
|
}).put("/api/settings", async ({ request: request2 }) => {
|
|
41134
43405
|
await requireAdmin(request2, deps.admin);
|
|
41135
43406
|
await putSettings(deps.store, await readJson(request2));
|
|
@@ -41151,6 +43422,9 @@ function adminRoutes(deps) {
|
|
|
41151
43422
|
}).get("/api/logs", async ({ request: request2, query }) => {
|
|
41152
43423
|
await requireAdmin(request2, deps.admin);
|
|
41153
43424
|
return { logs: await recentLogs(deps.store, query.limit) };
|
|
43425
|
+
}).get("/api/requests/:id/body", async ({ request: request2, params }) => {
|
|
43426
|
+
await requireAdmin(request2, deps.admin);
|
|
43427
|
+
return readRequestBody(deps.store, params.id);
|
|
41154
43428
|
}).get("/api/console", async ({ request: request2, query }) => {
|
|
41155
43429
|
await requireAdmin(request2, deps.admin);
|
|
41156
43430
|
if (deps.console === undefined)
|
|
@@ -41186,8 +43460,165 @@ function connectRoutes(deps) {
|
|
|
41186
43460
|
});
|
|
41187
43461
|
}
|
|
41188
43462
|
|
|
43463
|
+
// apps/gateway/src/routes/database.ts
|
|
43464
|
+
import { dirname as dirname3 } from "path";
|
|
43465
|
+
var QUIESCE_DEADLINE_MS = 1e4;
|
|
43466
|
+
async function swap(deps, logger2, label2, operation) {
|
|
43467
|
+
const startedAt = deps.now();
|
|
43468
|
+
const closedHere = !deps.latch.isClosed();
|
|
43469
|
+
const quiesce = await deps.latch.close(deps.quiesceDeadlineMs ?? QUIESCE_DEADLINE_MS);
|
|
43470
|
+
if (!quiesce.drained) {
|
|
43471
|
+
logger2.warn("client requests still in flight at the quiesce deadline", {
|
|
43472
|
+
reason: label2,
|
|
43473
|
+
count: quiesce.inFlight
|
|
43474
|
+
});
|
|
43475
|
+
}
|
|
43476
|
+
try {
|
|
43477
|
+
const result = await operation();
|
|
43478
|
+
deps.snapshots?.invalidate();
|
|
43479
|
+
deps.latch.open();
|
|
43480
|
+
logger2.warn("database replaced", {
|
|
43481
|
+
snapshotId: result.preRestoreSnapshot.id,
|
|
43482
|
+
durationMs: deps.now() - startedAt,
|
|
43483
|
+
reason: label2
|
|
43484
|
+
});
|
|
43485
|
+
if (result.adminPasswordChanged) {
|
|
43486
|
+
deps.admin.invalidateSessions();
|
|
43487
|
+
logger2.warn("admin sessions ended: the restored database carries a different password", {
|
|
43488
|
+
reason: label2
|
|
43489
|
+
});
|
|
43490
|
+
}
|
|
43491
|
+
return result;
|
|
43492
|
+
} catch (error51) {
|
|
43493
|
+
if (error51 instanceof SwapFailedError) {
|
|
43494
|
+
logger2.error(error51.reopened ? "database swap failed; client requests stay refused" : "database swap failed and the database could not be reopened; restart this process", { snapshotId: error51.preRestoreSnapshotId, reason: label2 });
|
|
43495
|
+
throw new GatewayError("INTERNAL", error51.message);
|
|
43496
|
+
}
|
|
43497
|
+
if (closedHere)
|
|
43498
|
+
deps.latch.open();
|
|
43499
|
+
throw error51;
|
|
43500
|
+
}
|
|
43501
|
+
}
|
|
43502
|
+
async function stageUpload(deps, database, request2) {
|
|
43503
|
+
const cap = deps.maxImportBytes ?? MAX_IMPORT_BYTES;
|
|
43504
|
+
const path = stagedImportPath(database, crypto.randomUUID());
|
|
43505
|
+
const body2 = request2.body;
|
|
43506
|
+
if (body2 === null)
|
|
43507
|
+
throw new GatewayError("BAD_REQUEST", "a database import needs a body");
|
|
43508
|
+
const dir = dirname3(deps.store.databasePath);
|
|
43509
|
+
const free = deps.fs.freeBytes(dir);
|
|
43510
|
+
const reserve = Math.ceil((deps.fs.stat(deps.store.databasePath)?.size ?? 0) * SNAPSHOT_HEADROOM);
|
|
43511
|
+
const budget = free === null ? cap : Math.max(0, free - reserve);
|
|
43512
|
+
const diskBound = budget < cap;
|
|
43513
|
+
const limit = diskBound ? budget : cap;
|
|
43514
|
+
const refuse = () => diskBound ? new GatewayError("CONFLICT", "not enough free disk space to stage a database import") : new GatewayError("BAD_REQUEST", `a database import may not exceed ${cap} bytes`);
|
|
43515
|
+
const declared = Number(request2.headers.get("content-length"));
|
|
43516
|
+
if (Number.isFinite(declared) && declared > limit)
|
|
43517
|
+
throw refuse();
|
|
43518
|
+
const sink = Bun.file(path).writer();
|
|
43519
|
+
const reader = body2.getReader();
|
|
43520
|
+
let written = 0;
|
|
43521
|
+
try {
|
|
43522
|
+
while (true) {
|
|
43523
|
+
const chunk2 = await reader.read();
|
|
43524
|
+
if (chunk2.done === true)
|
|
43525
|
+
break;
|
|
43526
|
+
written += chunk2.value.byteLength;
|
|
43527
|
+
if (written > limit) {
|
|
43528
|
+
await reader.cancel();
|
|
43529
|
+
throw refuse();
|
|
43530
|
+
}
|
|
43531
|
+
sink.write(chunk2.value);
|
|
43532
|
+
}
|
|
43533
|
+
await sink.flush();
|
|
43534
|
+
return path;
|
|
43535
|
+
} catch (error51) {
|
|
43536
|
+
deps.fs.unlink(path);
|
|
43537
|
+
throw error51;
|
|
43538
|
+
} finally {
|
|
43539
|
+
await sink.end();
|
|
43540
|
+
}
|
|
43541
|
+
}
|
|
43542
|
+
function databaseRoutes(deps) {
|
|
43543
|
+
const logger2 = deps.logger ?? noopLogger;
|
|
43544
|
+
const database = { store: deps.store, fs: deps.fs, now: deps.now, logger: logger2 };
|
|
43545
|
+
return new Elysia().onError(apiErrorHandler).get("/api/database", async ({ request: request2 }) => {
|
|
43546
|
+
await requireAdmin(request2, deps.admin);
|
|
43547
|
+
return getDatabaseOverview(database);
|
|
43548
|
+
}).post("/api/database/vacuum", async ({ request: request2 }) => {
|
|
43549
|
+
await requireAdmin(request2, deps.admin);
|
|
43550
|
+
const result = await vacuum(database);
|
|
43551
|
+
logger2.info("database vacuumed", {
|
|
43552
|
+
sizeBytes: result.reclaimedBytes,
|
|
43553
|
+
durationMs: result.durationMs
|
|
43554
|
+
});
|
|
43555
|
+
return { ok: true, ...result };
|
|
43556
|
+
}).get("/api/database/snapshots", async ({ request: request2 }) => {
|
|
43557
|
+
await requireAdmin(request2, deps.admin);
|
|
43558
|
+
return { snapshots: listSnapshots(database) };
|
|
43559
|
+
}).post("/api/database/snapshots", async ({ request: request2 }) => {
|
|
43560
|
+
await requireAdmin(request2, deps.admin);
|
|
43561
|
+
const snapshot = await createSnapshot(database, { reason: "manual" });
|
|
43562
|
+
logger2.info("database snapshot created", {
|
|
43563
|
+
snapshotId: snapshot.id,
|
|
43564
|
+
sizeBytes: snapshot.sizeBytes
|
|
43565
|
+
});
|
|
43566
|
+
return snapshot;
|
|
43567
|
+
}).get("/api/database/snapshots/:id/download", async ({ request: request2, params }) => {
|
|
43568
|
+
await requireAdmin(request2, deps.admin);
|
|
43569
|
+
const snapshot = resolveSnapshotForDownload(database, params.id);
|
|
43570
|
+
logger2.info("database snapshot downloaded", {
|
|
43571
|
+
snapshotId: snapshot.filename,
|
|
43572
|
+
sizeBytes: snapshot.sizeBytes
|
|
43573
|
+
});
|
|
43574
|
+
return new Response(Bun.file(snapshot.path), {
|
|
43575
|
+
headers: {
|
|
43576
|
+
"content-type": "application/octet-stream",
|
|
43577
|
+
"content-length": String(snapshot.sizeBytes),
|
|
43578
|
+
"content-disposition": `attachment; filename="${snapshot.filename}"`,
|
|
43579
|
+
"cache-control": "no-store"
|
|
43580
|
+
}
|
|
43581
|
+
});
|
|
43582
|
+
}).delete("/api/database/snapshots/:id", async ({ request: request2, params }) => {
|
|
43583
|
+
await requireAdmin(request2, deps.admin);
|
|
43584
|
+
deleteSnapshot(database, params.id);
|
|
43585
|
+
logger2.info("database snapshot deleted", { snapshotId: params.id });
|
|
43586
|
+
return { ok: true };
|
|
43587
|
+
}).post("/api/database/snapshots/:id/restore", async ({ request: request2, params }) => {
|
|
43588
|
+
await requireAdmin(request2, deps.admin);
|
|
43589
|
+
return swap(deps, logger2, "restore", () => restoreSnapshot(database, params.id));
|
|
43590
|
+
}).post("/api/database/import", async ({ request: request2 }) => {
|
|
43591
|
+
await requireAdmin(request2, deps.admin);
|
|
43592
|
+
const path = await stageUpload(deps, database, request2);
|
|
43593
|
+
try {
|
|
43594
|
+
return await swap(deps, logger2, "import", () => importSnapshot(database, { path }));
|
|
43595
|
+
} catch (error51) {
|
|
43596
|
+
deps.fs.unlink(path);
|
|
43597
|
+
throw error51;
|
|
43598
|
+
}
|
|
43599
|
+
}).put("/api/database/retention", async ({ request: request2 }) => {
|
|
43600
|
+
await requireAdmin(request2, deps.admin);
|
|
43601
|
+
return putRetention(database, await readJson(request2));
|
|
43602
|
+
}).get("/api/lifecycle", async ({ request: request2 }) => {
|
|
43603
|
+
await requireAdmin(request2, deps.admin);
|
|
43604
|
+
return describeLifecycle(deps.lifecycle.env, deps.lifecycle.fileExists);
|
|
43605
|
+
}).post("/api/lifecycle/restart", async ({ request: request2 }) => {
|
|
43606
|
+
await requireAdmin(request2, deps.admin);
|
|
43607
|
+
const capability = describeLifecycle(deps.lifecycle.env, deps.lifecycle.fileExists);
|
|
43608
|
+
logger2.warn("restart requested", { supervisor: capability.supervisor });
|
|
43609
|
+
await requestRestart(deps.lifecycle);
|
|
43610
|
+
return { ok: true };
|
|
43611
|
+
}).post("/api/lifecycle/shutdown", async ({ request: request2 }) => {
|
|
43612
|
+
await requireAdmin(request2, deps.admin);
|
|
43613
|
+
const capability = describeLifecycle(deps.lifecycle.env, deps.lifecycle.fileExists);
|
|
43614
|
+
logger2.warn("shutdown requested", { supervisor: capability.supervisor, reason: "api" });
|
|
43615
|
+
await requestShutdown(deps.lifecycle);
|
|
43616
|
+
return { ok: true };
|
|
43617
|
+
});
|
|
43618
|
+
}
|
|
43619
|
+
|
|
41189
43620
|
// apps/gateway/src/auth/apiKey.ts
|
|
41190
|
-
async function authenticateApiKey(store, header) {
|
|
43621
|
+
async function authenticateApiKey(store, header, logger2 = noopLogger) {
|
|
41191
43622
|
const raw = extractToken(header);
|
|
41192
43623
|
if (raw === null)
|
|
41193
43624
|
throw new GatewayError("AUTH", "missing or malformed Authorization header");
|
|
@@ -41195,7 +43626,12 @@ async function authenticateApiKey(store, header) {
|
|
|
41195
43626
|
if (key === null || key.revokedAt !== null) {
|
|
41196
43627
|
throw new GatewayError("AUTH", "invalid API key");
|
|
41197
43628
|
}
|
|
41198
|
-
|
|
43629
|
+
const limits = key.limits;
|
|
43630
|
+
if (limits === null) {
|
|
43631
|
+
logger2.error("api key refused: limits unreadable", { apiKeyId: key.id });
|
|
43632
|
+
throw new GatewayError("INTERNAL", "api key configuration is unusable");
|
|
43633
|
+
}
|
|
43634
|
+
return { ...key, limits };
|
|
41199
43635
|
}
|
|
41200
43636
|
function apiKeyHeader(headers) {
|
|
41201
43637
|
const authorization = headers.get("authorization");
|
|
@@ -41221,6 +43657,139 @@ function extractToken(header) {
|
|
|
41221
43657
|
return match === null ? value : match[1].trim();
|
|
41222
43658
|
}
|
|
41223
43659
|
|
|
43660
|
+
// apps/gateway/src/bodyCapture.ts
|
|
43661
|
+
var MAX_CAPTURED_BODY_BYTES = MAX_ARTIFACT_BYTES;
|
|
43662
|
+
function asBody(text) {
|
|
43663
|
+
if (text.length === 0)
|
|
43664
|
+
return null;
|
|
43665
|
+
try {
|
|
43666
|
+
return JSON.parse(text);
|
|
43667
|
+
} catch {
|
|
43668
|
+
return text;
|
|
43669
|
+
}
|
|
43670
|
+
}
|
|
43671
|
+
function framesOf(text) {
|
|
43672
|
+
return text.replaceAll(`\r
|
|
43673
|
+
`, `
|
|
43674
|
+
`).split(`
|
|
43675
|
+
|
|
43676
|
+
`).filter((frame2) => frame2.length > 0);
|
|
43677
|
+
}
|
|
43678
|
+
function createFrameSink() {
|
|
43679
|
+
const frames = [];
|
|
43680
|
+
let bytes = 0;
|
|
43681
|
+
const sink = {
|
|
43682
|
+
frames,
|
|
43683
|
+
truncated: false,
|
|
43684
|
+
write(frame2) {
|
|
43685
|
+
frames.push(frame2);
|
|
43686
|
+
bytes += frame2.length;
|
|
43687
|
+
while (bytes > MAX_CAPTURED_BODY_BYTES && frames.length > 1) {
|
|
43688
|
+
bytes -= (frames.shift() ?? "").length;
|
|
43689
|
+
sink.truncated = true;
|
|
43690
|
+
}
|
|
43691
|
+
}
|
|
43692
|
+
};
|
|
43693
|
+
return sink;
|
|
43694
|
+
}
|
|
43695
|
+
function createBodyCollector(options) {
|
|
43696
|
+
const attempts = [];
|
|
43697
|
+
const drains = [];
|
|
43698
|
+
const client = { request: null, response: null, truncated: false };
|
|
43699
|
+
const drain = async (stream2, entry) => {
|
|
43700
|
+
const reader = stream2.getReader();
|
|
43701
|
+
const decoder2 = new TextDecoder;
|
|
43702
|
+
let bytes = 0;
|
|
43703
|
+
try {
|
|
43704
|
+
while (true) {
|
|
43705
|
+
const { done, value } = await reader.read();
|
|
43706
|
+
if (done === true)
|
|
43707
|
+
break;
|
|
43708
|
+
if (value === undefined)
|
|
43709
|
+
continue;
|
|
43710
|
+
entry.responded = true;
|
|
43711
|
+
if (bytes >= MAX_CAPTURED_BODY_BYTES) {
|
|
43712
|
+
entry.truncated = true;
|
|
43713
|
+
continue;
|
|
43714
|
+
}
|
|
43715
|
+
bytes += value.byteLength;
|
|
43716
|
+
entry.responseText += decoder2.decode(value, { stream: true });
|
|
43717
|
+
if (bytes > MAX_CAPTURED_BODY_BYTES)
|
|
43718
|
+
entry.truncated = true;
|
|
43719
|
+
}
|
|
43720
|
+
entry.responseText += decoder2.decode();
|
|
43721
|
+
} catch {
|
|
43722
|
+
entry.truncated = true;
|
|
43723
|
+
} finally {
|
|
43724
|
+
reader.releaseLock();
|
|
43725
|
+
}
|
|
43726
|
+
if (options.captureStreamChunks)
|
|
43727
|
+
entry.frames = framesOf(entry.responseText);
|
|
43728
|
+
};
|
|
43729
|
+
const captureResponse = (res, entry) => {
|
|
43730
|
+
let forAdapter;
|
|
43731
|
+
return {
|
|
43732
|
+
status: res.status,
|
|
43733
|
+
headers: res.headers,
|
|
43734
|
+
get body() {
|
|
43735
|
+
if (forAdapter !== undefined)
|
|
43736
|
+
return forAdapter;
|
|
43737
|
+
const source = res.body;
|
|
43738
|
+
if (source === null) {
|
|
43739
|
+
forAdapter = null;
|
|
43740
|
+
return null;
|
|
43741
|
+
}
|
|
43742
|
+
const [adapterBranch, captureBranch] = source.tee();
|
|
43743
|
+
drains.push(drain(captureBranch, entry));
|
|
43744
|
+
forAdapter = adapterBranch;
|
|
43745
|
+
return forAdapter;
|
|
43746
|
+
},
|
|
43747
|
+
text: async () => {
|
|
43748
|
+
const text = await res.text();
|
|
43749
|
+
entry.responded = true;
|
|
43750
|
+
entry.responseText = text.slice(0, MAX_CAPTURED_BODY_BYTES);
|
|
43751
|
+
if (entry.responseText.length < text.length)
|
|
43752
|
+
entry.truncated = true;
|
|
43753
|
+
if (options.captureStreamChunks)
|
|
43754
|
+
entry.frames = framesOf(entry.responseText);
|
|
43755
|
+
return text;
|
|
43756
|
+
}
|
|
43757
|
+
};
|
|
43758
|
+
};
|
|
43759
|
+
return {
|
|
43760
|
+
client,
|
|
43761
|
+
wrap(http) {
|
|
43762
|
+
return async (req) => {
|
|
43763
|
+
const entry = {
|
|
43764
|
+
attempt: attempts.length + 1,
|
|
43765
|
+
provider: req.provider,
|
|
43766
|
+
request: req.body.length > MAX_CAPTURED_BODY_BYTES ? req.body.slice(0, MAX_CAPTURED_BODY_BYTES) : asBody(req.body),
|
|
43767
|
+
responseText: "",
|
|
43768
|
+
responded: false,
|
|
43769
|
+
frames: null,
|
|
43770
|
+
truncated: req.body.length > MAX_CAPTURED_BODY_BYTES
|
|
43771
|
+
};
|
|
43772
|
+
attempts.push(entry);
|
|
43773
|
+
const res = await http(req);
|
|
43774
|
+
return captureResponse(res, entry);
|
|
43775
|
+
};
|
|
43776
|
+
},
|
|
43777
|
+
attempts() {
|
|
43778
|
+
return attempts.map((entry) => ({
|
|
43779
|
+
attempt: entry.attempt,
|
|
43780
|
+
provider: entry.provider,
|
|
43781
|
+
request: entry.request,
|
|
43782
|
+
response: entry.responded ? asBody(entry.responseText) : null,
|
|
43783
|
+
streamChunks: entry.frames,
|
|
43784
|
+
truncated: entry.truncated
|
|
43785
|
+
}));
|
|
43786
|
+
},
|
|
43787
|
+
async settle() {
|
|
43788
|
+
await Promise.all(drains);
|
|
43789
|
+
}
|
|
43790
|
+
};
|
|
43791
|
+
}
|
|
43792
|
+
|
|
41224
43793
|
// packages/rtk/src/command.ts
|
|
41225
43794
|
var UNSUPPORTED_OUTPUT = new Set(["--json", "--sarif"]);
|
|
41226
43795
|
function tokenize(command) {
|
|
@@ -43057,6 +45626,26 @@ function newPendingRequestLog(input) {
|
|
|
43057
45626
|
status: 0
|
|
43058
45627
|
};
|
|
43059
45628
|
}
|
|
45629
|
+
function reasonField(error51, logger2) {
|
|
45630
|
+
return error51.provider === undefined || logger2.enabled("debug") ? { reason: error51.message } : {};
|
|
45631
|
+
}
|
|
45632
|
+
function reportRejection(logger2, requestId, log, error51, surface) {
|
|
45633
|
+
const fields = {
|
|
45634
|
+
requestId,
|
|
45635
|
+
...surface === undefined ? {} : { surface },
|
|
45636
|
+
status: log.status,
|
|
45637
|
+
provider: log.resolvedProvider ?? undefined,
|
|
45638
|
+
model: log.resolvedModel ?? undefined,
|
|
45639
|
+
credentialId: log.credentialId ?? undefined,
|
|
45640
|
+
code: error51.code,
|
|
45641
|
+
attempts: log.attempts,
|
|
45642
|
+
...reasonField(error51, logger2)
|
|
45643
|
+
};
|
|
45644
|
+
if (error51.code === "INTERNAL")
|
|
45645
|
+
logger2.error("request rejected", fields);
|
|
45646
|
+
else
|
|
45647
|
+
logger2.warn("request rejected", fields);
|
|
45648
|
+
}
|
|
43060
45649
|
function report(logger2, what, requestId, error51) {
|
|
43061
45650
|
logger2.warn(what, {
|
|
43062
45651
|
requestId,
|
|
@@ -43077,12 +45666,25 @@ async function routeLog(store, requestId, target, logger2 = noopLogger) {
|
|
|
43077
45666
|
report(logger2, "failed to record request route", requestId, error51);
|
|
43078
45667
|
}
|
|
43079
45668
|
}
|
|
43080
|
-
async function finishLog(store, log, keyId, logger2 = noopLogger) {
|
|
45669
|
+
async function finishLog(store, log, keyId, logger2 = noopLogger, bodies, debit) {
|
|
43081
45670
|
try {
|
|
43082
45671
|
await store.usage.append({ ...log, apiKeyId: keyId });
|
|
43083
45672
|
} catch (error51) {
|
|
43084
45673
|
report(logger2, "failed to persist request log", log.id, error51);
|
|
43085
45674
|
}
|
|
45675
|
+
if (keyId !== null && debit !== undefined) {
|
|
45676
|
+
debit(keyId, {
|
|
45677
|
+
tokens: log.inputTokens + log.outputTokens + log.cacheReadTokens + log.cacheWriteTokens,
|
|
45678
|
+
costUsd: log.costUsd
|
|
45679
|
+
});
|
|
45680
|
+
}
|
|
45681
|
+
if (bodies === undefined)
|
|
45682
|
+
return;
|
|
45683
|
+
try {
|
|
45684
|
+
await bodies(log);
|
|
45685
|
+
} catch (error51) {
|
|
45686
|
+
report(logger2, "failed to persist request bodies", log.id, error51);
|
|
45687
|
+
}
|
|
43086
45688
|
}
|
|
43087
45689
|
|
|
43088
45690
|
// apps/gateway/src/dispatch/attempt.ts
|
|
@@ -43131,7 +45733,11 @@ var NETWORK_HINTS = [
|
|
|
43131
45733
|
];
|
|
43132
45734
|
function classify(error51) {
|
|
43133
45735
|
if (error51 instanceof GatewayError) {
|
|
43134
|
-
return
|
|
45736
|
+
return {
|
|
45737
|
+
code: error51.code,
|
|
45738
|
+
...error51.retryAfterMs === undefined ? {} : { retryAfterMs: error51.retryAfterMs },
|
|
45739
|
+
...error51.provider === undefined ? {} : { provider: error51.provider }
|
|
45740
|
+
};
|
|
43135
45741
|
}
|
|
43136
45742
|
if (error51 instanceof DOMException && error51.name === "AbortError") {
|
|
43137
45743
|
return { code: "TIMEOUT" };
|
|
@@ -43183,10 +45789,20 @@ async function dispatch(request2, deps, signal, requestId) {
|
|
|
43183
45789
|
status: 0
|
|
43184
45790
|
});
|
|
43185
45791
|
let dispatchRequest = request2;
|
|
43186
|
-
const
|
|
43187
|
-
log
|
|
43188
|
-
|
|
45792
|
+
const reject = (error51) => {
|
|
45793
|
+
reportRejection(logger2, requestId, log, error51);
|
|
45794
|
+
};
|
|
45795
|
+
const isClientAbort = (error51) => signal.aborted && error51 === signal.reason;
|
|
45796
|
+
const rewrap = (classified, message) => new GatewayError(classified.code, message, {
|
|
45797
|
+
...classified.retryAfterMs === undefined ? {} : { retryAfterMs: classified.retryAfterMs },
|
|
45798
|
+
...classified.provider === undefined ? {} : { provider: classified.provider }
|
|
45799
|
+
});
|
|
45800
|
+
const fail = (code, message, cancelled = false) => {
|
|
45801
|
+
log.errorCode = cancelled ? "interrupted" : code;
|
|
45802
|
+
log.status = cancelled ? 499 : HTTP_STATUS[code];
|
|
43189
45803
|
log.durationMs = deps.now() - startedAt;
|
|
45804
|
+
if (!cancelled)
|
|
45805
|
+
reject(new GatewayError(code, message));
|
|
43190
45806
|
return {
|
|
43191
45807
|
events: async function* () {
|
|
43192
45808
|
yield { type: "error", code, message, retryable: RETRYABLE[code] };
|
|
@@ -43230,8 +45846,10 @@ async function dispatch(request2, deps, signal, requestId) {
|
|
|
43230
45846
|
checkCancellation();
|
|
43231
45847
|
model = resolveModel(dispatchRequest.model, snapshot);
|
|
43232
45848
|
} catch (error51) {
|
|
43233
|
-
const { code } = classify(error51);
|
|
43234
45849
|
clearDeadline();
|
|
45850
|
+
if (isClientAbort(error51))
|
|
45851
|
+
return fail("TIMEOUT", "client disconnected", true);
|
|
45852
|
+
const { code } = classify(error51);
|
|
43235
45853
|
return fail(code, error51 instanceof Error ? error51.message : "unresolvable model");
|
|
43236
45854
|
}
|
|
43237
45855
|
const { candidates, excluded } = rank({
|
|
@@ -43275,10 +45893,11 @@ async function dispatch(request2, deps, signal, requestId) {
|
|
|
43275
45893
|
eager.release();
|
|
43276
45894
|
else
|
|
43277
45895
|
signal.addEventListener("abort", releaseOnAbort, { once: true });
|
|
43278
|
-
const persistHealth = async (
|
|
43279
|
-
|
|
45896
|
+
const persistHealth = async (candidate, transition) => {
|
|
45897
|
+
const credentialId = candidate.credential.id;
|
|
45898
|
+
const { model: model2 } = candidate.target;
|
|
45899
|
+
await deps.store.credentials.updateHealth(credentialId, model2, (current) => transition(current ?? blankHealth(credentialId, model2)));
|
|
43280
45900
|
};
|
|
43281
|
-
const healthFor = (candidate) => snapshot.health.get(healthKey(candidate.credential.id, candidate.target.model)) ?? blankHealth(candidate.credential.id, candidate.target.model);
|
|
43282
45901
|
async function* run() {
|
|
43283
45902
|
try {
|
|
43284
45903
|
let lastError = null;
|
|
@@ -43377,16 +45996,28 @@ async function dispatch(request2, deps, signal, requestId) {
|
|
|
43377
45996
|
throw new GatewayError(event.code, event.message);
|
|
43378
45997
|
}
|
|
43379
45998
|
committed = true;
|
|
43380
|
-
yield event;
|
|
43381
|
-
await persistHealth(recordFailure(healthFor(candidate), {
|
|
43382
|
-
settings: snapshot.settings,
|
|
43383
|
-
now: deps.now(),
|
|
43384
|
-
code: event.code,
|
|
43385
|
-
jitter: deps.rand()
|
|
43386
|
-
}));
|
|
43387
45999
|
log.status = HTTP_STATUS[event.code];
|
|
43388
46000
|
log.errorCode = event.code;
|
|
43389
46001
|
log.durationMs = deps.now() - startedAt;
|
|
46002
|
+
reject(new GatewayError(event.code, event.message, {
|
|
46003
|
+
provider: candidate.target.provider
|
|
46004
|
+
}));
|
|
46005
|
+
yield event;
|
|
46006
|
+
try {
|
|
46007
|
+
await persistHealth(candidate, (current) => recordFailure(current, {
|
|
46008
|
+
settings: snapshot.settings,
|
|
46009
|
+
now: deps.now(),
|
|
46010
|
+
code: event.code,
|
|
46011
|
+
jitter: deps.rand()
|
|
46012
|
+
}));
|
|
46013
|
+
} catch (healthError) {
|
|
46014
|
+
logger2.error("failed to persist credential health", {
|
|
46015
|
+
requestId,
|
|
46016
|
+
provider: candidate.target.provider,
|
|
46017
|
+
credentialId: candidate.credential.id,
|
|
46018
|
+
reason: healthError instanceof Error ? healthError.message : "unknown"
|
|
46019
|
+
});
|
|
46020
|
+
}
|
|
43390
46021
|
return;
|
|
43391
46022
|
}
|
|
43392
46023
|
if (!committed && event.type !== "end") {
|
|
@@ -43405,7 +46036,7 @@ async function dispatch(request2, deps, signal, requestId) {
|
|
|
43405
46036
|
if (!terminal) {
|
|
43406
46037
|
throw new GatewayError("UPSTREAM", "upstream stream ended without a terminal event");
|
|
43407
46038
|
}
|
|
43408
|
-
await persistHealth(recordSuccess(
|
|
46039
|
+
await persistHealth(candidate, (current) => recordSuccess(current, {
|
|
43409
46040
|
settings: snapshot.settings,
|
|
43410
46041
|
now: deps.now(),
|
|
43411
46042
|
ttftMs: log.ttftMs
|
|
@@ -43418,9 +46049,9 @@ async function dispatch(request2, deps, signal, requestId) {
|
|
|
43418
46049
|
if (signal.aborted)
|
|
43419
46050
|
throw signal.reason;
|
|
43420
46051
|
const classifiedError = deadlineAt !== null && dispatchSignal.aborted ? { code: "TIMEOUT" } : classify(error51);
|
|
43421
|
-
const { code: code2
|
|
43422
|
-
const
|
|
43423
|
-
lastError =
|
|
46052
|
+
const { code: code2 } = classifiedError;
|
|
46053
|
+
const message2 = error51 instanceof Error ? error51.message : "attempt failed";
|
|
46054
|
+
lastError = rewrap(classifiedError, message2);
|
|
43424
46055
|
if (code2 === "AUTH" && !committed && !authRefreshRetried && !preemptiveRefreshRequired && candidate.credential.authType === "oauth" && candidate.credential.hasRefreshToken) {
|
|
43425
46056
|
authRefreshRetried = true;
|
|
43426
46057
|
logger2.warn("attempt authentication failed; refreshing credential", {
|
|
@@ -43439,13 +46070,20 @@ async function dispatch(request2, deps, signal, requestId) {
|
|
|
43439
46070
|
throw signal.reason;
|
|
43440
46071
|
const classified = deadlineAt !== null && dispatchSignal.aborted ? { code: "TIMEOUT" } : classify(refreshError);
|
|
43441
46072
|
const refreshMessage = refreshError instanceof Error ? refreshError.message : "credential refresh failed";
|
|
43442
|
-
lastError =
|
|
43443
|
-
|
|
46073
|
+
lastError = rewrap(classified, refreshMessage);
|
|
46074
|
+
logger2.warn("credential refresh failed", {
|
|
46075
|
+
requestId,
|
|
46076
|
+
provider: candidate.target.provider,
|
|
46077
|
+
model: candidate.target.model,
|
|
46078
|
+
credentialId: candidate.credential.id,
|
|
46079
|
+
attempt: i + 1,
|
|
46080
|
+
code: classified.code,
|
|
46081
|
+
...reasonField(lastError, logger2)
|
|
43444
46082
|
});
|
|
43445
46083
|
}
|
|
43446
46084
|
}
|
|
43447
46085
|
const failure = lastError;
|
|
43448
|
-
await persistHealth(recordFailure(
|
|
46086
|
+
await persistHealth(candidate, (current) => recordFailure(current, {
|
|
43449
46087
|
settings: snapshot.settings,
|
|
43450
46088
|
now: deps.now(),
|
|
43451
46089
|
code: failure.code,
|
|
@@ -43468,6 +46106,7 @@ async function dispatch(request2, deps, signal, requestId) {
|
|
|
43468
46106
|
log.status = HTTP_STATUS[failure.code];
|
|
43469
46107
|
log.errorCode = failure.code;
|
|
43470
46108
|
log.durationMs = deps.now() - startedAt;
|
|
46109
|
+
reject(failure);
|
|
43471
46110
|
yield {
|
|
43472
46111
|
type: "error",
|
|
43473
46112
|
code: failure.code,
|
|
@@ -43489,10 +46128,12 @@ async function dispatch(request2, deps, signal, requestId) {
|
|
|
43489
46128
|
log.status = HTTP_STATUS[code];
|
|
43490
46129
|
log.errorCode = code;
|
|
43491
46130
|
log.durationMs = deps.now() - startedAt;
|
|
46131
|
+
const message = lastError?.message ?? "all candidates failed";
|
|
46132
|
+
reject(lastError?.provider === undefined ? new GatewayError(code, message) : new GatewayError(code, message, { provider: lastError.provider }));
|
|
43492
46133
|
yield {
|
|
43493
46134
|
type: "error",
|
|
43494
46135
|
code,
|
|
43495
|
-
message
|
|
46136
|
+
message,
|
|
43496
46137
|
retryable: false
|
|
43497
46138
|
};
|
|
43498
46139
|
} finally {
|
|
@@ -43535,12 +46176,12 @@ async function dispatch(request2, deps, signal, requestId) {
|
|
|
43535
46176
|
function waitForCancellation(promise2, signal) {
|
|
43536
46177
|
if (signal.aborted)
|
|
43537
46178
|
return Promise.reject(signal.reason);
|
|
43538
|
-
return new Promise((
|
|
46179
|
+
return new Promise((resolve2, reject) => {
|
|
43539
46180
|
const abort = () => reject(signal.reason);
|
|
43540
46181
|
signal.addEventListener("abort", abort, { once: true });
|
|
43541
46182
|
promise2.then((value) => {
|
|
43542
46183
|
signal.removeEventListener("abort", abort);
|
|
43543
|
-
|
|
46184
|
+
resolve2(value);
|
|
43544
46185
|
}, (error51) => {
|
|
43545
46186
|
signal.removeEventListener("abort", abort);
|
|
43546
46187
|
reject(error51);
|
|
@@ -43577,314 +46218,6 @@ function createLoadRegistry(logger2 = noopLogger) {
|
|
|
43577
46218
|
};
|
|
43578
46219
|
}
|
|
43579
46220
|
|
|
43580
|
-
// apps/gateway/src/egress/anthropic.ts
|
|
43581
|
-
var STOP_REASON2 = {
|
|
43582
|
-
endTurn: "end_turn",
|
|
43583
|
-
maxTokens: "max_tokens",
|
|
43584
|
-
stopSequence: "stop_sequence",
|
|
43585
|
-
toolUse: "tool_use",
|
|
43586
|
-
contentFilter: "refusal",
|
|
43587
|
-
pauseTurn: "pause_turn"
|
|
43588
|
-
};
|
|
43589
|
-
var ERROR_TYPE2 = {
|
|
43590
|
-
AUTH: "authentication_error",
|
|
43591
|
-
RATE_LIMIT: "rate_limit_error",
|
|
43592
|
-
QUOTA_EXHAUSTED: "rate_limit_error",
|
|
43593
|
-
OVERLOADED: "overloaded_error",
|
|
43594
|
-
BAD_REQUEST: "invalid_request_error",
|
|
43595
|
-
CONFLICT: "invalid_request_error",
|
|
43596
|
-
CONTENT_FILTER: "invalid_request_error",
|
|
43597
|
-
CAPABILITY_MISMATCH: "invalid_request_error",
|
|
43598
|
-
MODEL_UNAVAILABLE: "not_found_error",
|
|
43599
|
-
UPSTREAM: "api_error",
|
|
43600
|
-
TIMEOUT: "api_error",
|
|
43601
|
-
NETWORK: "api_error",
|
|
43602
|
-
NO_CANDIDATES: "overloaded_error",
|
|
43603
|
-
ALL_CANDIDATES_FAILED: "api_error",
|
|
43604
|
-
INTERNAL: "api_error"
|
|
43605
|
-
};
|
|
43606
|
-
var frame = (event, data) => ({
|
|
43607
|
-
event,
|
|
43608
|
-
data: JSON.stringify(data)
|
|
43609
|
-
});
|
|
43610
|
-
async function* anthropicStream(events, requestId) {
|
|
43611
|
-
let model = "";
|
|
43612
|
-
const suppressed2 = new Set;
|
|
43613
|
-
const outIndex = new Map;
|
|
43614
|
-
let nextOutIndex = 0;
|
|
43615
|
-
for await (const event of events) {
|
|
43616
|
-
switch (event.type) {
|
|
43617
|
-
case "start":
|
|
43618
|
-
model = event.model;
|
|
43619
|
-
yield frame("message_start", {
|
|
43620
|
-
type: "message_start",
|
|
43621
|
-
message: {
|
|
43622
|
-
id: requestId,
|
|
43623
|
-
type: "message",
|
|
43624
|
-
role: "assistant",
|
|
43625
|
-
model,
|
|
43626
|
-
content: [],
|
|
43627
|
-
stop_reason: null,
|
|
43628
|
-
stop_sequence: null,
|
|
43629
|
-
usage: {
|
|
43630
|
-
input_tokens: 0,
|
|
43631
|
-
output_tokens: 0,
|
|
43632
|
-
cache_read_input_tokens: 0,
|
|
43633
|
-
cache_creation_input_tokens: 0
|
|
43634
|
-
}
|
|
43635
|
-
}
|
|
43636
|
-
});
|
|
43637
|
-
break;
|
|
43638
|
-
case "blockStart": {
|
|
43639
|
-
const b = event.block;
|
|
43640
|
-
if (b.type === "thinking" && b.signed !== true) {
|
|
43641
|
-
suppressed2.add(event.index);
|
|
43642
|
-
break;
|
|
43643
|
-
}
|
|
43644
|
-
const content_block = b.type === "text" ? { type: "text", text: "" } : b.type === "thinking" ? { type: "thinking", thinking: "" } : b.type === "anthropicNative" ? { ...b.data, type: b.blockType } : { type: "tool_use", id: b.id, name: b.name, input: {} };
|
|
43645
|
-
const index = nextOutIndex++;
|
|
43646
|
-
outIndex.set(event.index, index);
|
|
43647
|
-
yield frame("content_block_start", {
|
|
43648
|
-
type: "content_block_start",
|
|
43649
|
-
index,
|
|
43650
|
-
content_block
|
|
43651
|
-
});
|
|
43652
|
-
break;
|
|
43653
|
-
}
|
|
43654
|
-
case "blockDelta": {
|
|
43655
|
-
if (suppressed2.has(event.index))
|
|
43656
|
-
break;
|
|
43657
|
-
const d = event.delta;
|
|
43658
|
-
const delta2 = d.type === "text" ? { type: "text_delta", text: d.text } : d.type === "thinking" ? { type: "thinking_delta", thinking: d.text } : d.type === "thinkingSignature" ? { type: "signature_delta", signature: d.signature } : d.type === "anthropicNative" ? { type: d.deltaType, ...d.data } : { type: "input_json_delta", partial_json: d.partial };
|
|
43659
|
-
yield frame("content_block_delta", {
|
|
43660
|
-
type: "content_block_delta",
|
|
43661
|
-
index: outIndex.get(event.index) ?? event.index,
|
|
43662
|
-
delta: delta2
|
|
43663
|
-
});
|
|
43664
|
-
break;
|
|
43665
|
-
}
|
|
43666
|
-
case "blockEnd":
|
|
43667
|
-
if (suppressed2.has(event.index))
|
|
43668
|
-
break;
|
|
43669
|
-
yield frame("content_block_stop", {
|
|
43670
|
-
type: "content_block_stop",
|
|
43671
|
-
index: outIndex.get(event.index) ?? event.index
|
|
43672
|
-
});
|
|
43673
|
-
break;
|
|
43674
|
-
case "end":
|
|
43675
|
-
yield frame("message_delta", {
|
|
43676
|
-
type: "message_delta",
|
|
43677
|
-
delta: { stop_reason: STOP_REASON2[event.stopReason], stop_sequence: null },
|
|
43678
|
-
usage: {
|
|
43679
|
-
input_tokens: event.usage.inputTokens,
|
|
43680
|
-
output_tokens: event.usage.outputTokens,
|
|
43681
|
-
cache_read_input_tokens: event.usage.cacheReadTokens,
|
|
43682
|
-
cache_creation_input_tokens: event.usage.cacheWriteTokens
|
|
43683
|
-
}
|
|
43684
|
-
});
|
|
43685
|
-
yield frame("message_stop", { type: "message_stop" });
|
|
43686
|
-
break;
|
|
43687
|
-
case "error":
|
|
43688
|
-
yield frame("error", {
|
|
43689
|
-
type: "error",
|
|
43690
|
-
error: { type: ERROR_TYPE2[event.code], message: event.message }
|
|
43691
|
-
});
|
|
43692
|
-
break;
|
|
43693
|
-
}
|
|
43694
|
-
}
|
|
43695
|
-
}
|
|
43696
|
-
function anthropicResponse(collected, requestId) {
|
|
43697
|
-
return {
|
|
43698
|
-
id: requestId,
|
|
43699
|
-
type: "message",
|
|
43700
|
-
role: "assistant",
|
|
43701
|
-
model: collected.model,
|
|
43702
|
-
content: collected.content.filter((b) => b.type !== "thinking" || b.signature !== undefined && b.signature !== "").map((b) => {
|
|
43703
|
-
switch (b.type) {
|
|
43704
|
-
case "text":
|
|
43705
|
-
return {
|
|
43706
|
-
type: "text",
|
|
43707
|
-
text: b.text,
|
|
43708
|
-
...b.citations === undefined ? {} : { citations: b.citations }
|
|
43709
|
-
};
|
|
43710
|
-
case "thinking":
|
|
43711
|
-
return { type: "thinking", thinking: b.text, signature: b.signature };
|
|
43712
|
-
case "toolUse":
|
|
43713
|
-
return { type: "tool_use", id: b.id, name: b.name, input: b.input };
|
|
43714
|
-
case "anthropicNative":
|
|
43715
|
-
return { ...b.data, type: b.blockType };
|
|
43716
|
-
default:
|
|
43717
|
-
return { type: "text", text: "" };
|
|
43718
|
-
}
|
|
43719
|
-
}),
|
|
43720
|
-
stop_reason: STOP_REASON2[collected.stopReason],
|
|
43721
|
-
stop_sequence: null,
|
|
43722
|
-
usage: {
|
|
43723
|
-
input_tokens: collected.usage.inputTokens,
|
|
43724
|
-
output_tokens: collected.usage.outputTokens,
|
|
43725
|
-
cache_read_input_tokens: collected.usage.cacheReadTokens,
|
|
43726
|
-
cache_creation_input_tokens: collected.usage.cacheWriteTokens
|
|
43727
|
-
}
|
|
43728
|
-
};
|
|
43729
|
-
}
|
|
43730
|
-
function anthropicErrorBody(code, message) {
|
|
43731
|
-
return { type: "error", error: { type: ERROR_TYPE2[code], message } };
|
|
43732
|
-
}
|
|
43733
|
-
|
|
43734
|
-
// apps/gateway/src/egress/openai.ts
|
|
43735
|
-
function usageBody(usage) {
|
|
43736
|
-
const prompt = promptTokens(usage);
|
|
43737
|
-
return {
|
|
43738
|
-
prompt_tokens: prompt,
|
|
43739
|
-
completion_tokens: usage.outputTokens,
|
|
43740
|
-
total_tokens: prompt + usage.outputTokens,
|
|
43741
|
-
prompt_tokens_details: { cached_tokens: usage.cacheReadTokens }
|
|
43742
|
-
};
|
|
43743
|
-
}
|
|
43744
|
-
var FINISH3 = {
|
|
43745
|
-
endTurn: "stop",
|
|
43746
|
-
maxTokens: "length",
|
|
43747
|
-
stopSequence: "stop",
|
|
43748
|
-
toolUse: "tool_calls",
|
|
43749
|
-
contentFilter: "content_filter",
|
|
43750
|
-
pauseTurn: "stop"
|
|
43751
|
-
};
|
|
43752
|
-
var ERROR_TYPE3 = {
|
|
43753
|
-
AUTH: { type: "invalid_request_error", code: "invalid_api_key" },
|
|
43754
|
-
RATE_LIMIT: { type: "rate_limit_error", code: "rate_limit_exceeded" },
|
|
43755
|
-
QUOTA_EXHAUSTED: { type: "insufficient_quota", code: "insufficient_quota" },
|
|
43756
|
-
OVERLOADED: { type: "server_error", code: "server_error" },
|
|
43757
|
-
BAD_REQUEST: { type: "invalid_request_error", code: "invalid_request" },
|
|
43758
|
-
CONFLICT: { type: "invalid_request_error", code: "conflict" },
|
|
43759
|
-
CONTENT_FILTER: { type: "invalid_request_error", code: "content_policy_violation" },
|
|
43760
|
-
CAPABILITY_MISMATCH: { type: "invalid_request_error", code: "invalid_request" },
|
|
43761
|
-
MODEL_UNAVAILABLE: { type: "invalid_request_error", code: "model_not_found" },
|
|
43762
|
-
UPSTREAM: { type: "server_error", code: "server_error" },
|
|
43763
|
-
TIMEOUT: { type: "server_error", code: "timeout" },
|
|
43764
|
-
NETWORK: { type: "server_error", code: "server_error" },
|
|
43765
|
-
NO_CANDIDATES: { type: "server_error", code: "service_unavailable" },
|
|
43766
|
-
ALL_CANDIDATES_FAILED: { type: "server_error", code: "server_error" },
|
|
43767
|
-
INTERNAL: { type: "server_error", code: "server_error" }
|
|
43768
|
-
};
|
|
43769
|
-
var chunk = (id, created, model, choice, usage) => ({
|
|
43770
|
-
id,
|
|
43771
|
-
object: "chat.completion.chunk",
|
|
43772
|
-
created,
|
|
43773
|
-
model,
|
|
43774
|
-
choices: [choice],
|
|
43775
|
-
...usage === undefined ? {} : { usage }
|
|
43776
|
-
});
|
|
43777
|
-
async function* openaiStream(events, requestId, created) {
|
|
43778
|
-
let model = "";
|
|
43779
|
-
let roleSent = false;
|
|
43780
|
-
let completed = false;
|
|
43781
|
-
const toolIndex = new Map;
|
|
43782
|
-
const emit = (data) => ({ event: "message", data: JSON.stringify(data) });
|
|
43783
|
-
for await (const event of events) {
|
|
43784
|
-
switch (event.type) {
|
|
43785
|
-
case "start":
|
|
43786
|
-
model = event.model;
|
|
43787
|
-
break;
|
|
43788
|
-
case "blockStart": {
|
|
43789
|
-
if (event.block.type === "text") {
|
|
43790
|
-
if (!roleSent) {
|
|
43791
|
-
roleSent = true;
|
|
43792
|
-
yield emit(chunk(requestId, created, model, {
|
|
43793
|
-
index: 0,
|
|
43794
|
-
delta: { role: "assistant", content: "" },
|
|
43795
|
-
finish_reason: null
|
|
43796
|
-
}));
|
|
43797
|
-
}
|
|
43798
|
-
} else if (event.block.type === "toolUse") {
|
|
43799
|
-
const index = toolIndex.size;
|
|
43800
|
-
toolIndex.set(event.index, index);
|
|
43801
|
-
const firstRole = roleSent ? {} : { role: "assistant" };
|
|
43802
|
-
roleSent = true;
|
|
43803
|
-
yield emit(chunk(requestId, created, model, {
|
|
43804
|
-
index: 0,
|
|
43805
|
-
delta: {
|
|
43806
|
-
...firstRole,
|
|
43807
|
-
tool_calls: [
|
|
43808
|
-
{
|
|
43809
|
-
index,
|
|
43810
|
-
id: event.block.id,
|
|
43811
|
-
type: "function",
|
|
43812
|
-
function: { name: event.block.name, arguments: "" }
|
|
43813
|
-
}
|
|
43814
|
-
]
|
|
43815
|
-
},
|
|
43816
|
-
finish_reason: null
|
|
43817
|
-
}));
|
|
43818
|
-
}
|
|
43819
|
-
break;
|
|
43820
|
-
}
|
|
43821
|
-
case "blockDelta": {
|
|
43822
|
-
const d = event.delta;
|
|
43823
|
-
if (d.type === "text") {
|
|
43824
|
-
yield emit(chunk(requestId, created, model, {
|
|
43825
|
-
index: 0,
|
|
43826
|
-
delta: { content: d.text },
|
|
43827
|
-
finish_reason: null
|
|
43828
|
-
}));
|
|
43829
|
-
} else if (d.type === "toolJson") {
|
|
43830
|
-
const index = toolIndex.get(event.index) ?? 0;
|
|
43831
|
-
yield emit(chunk(requestId, created, model, {
|
|
43832
|
-
index: 0,
|
|
43833
|
-
delta: { tool_calls: [{ index, function: { arguments: d.partial } }] },
|
|
43834
|
-
finish_reason: null
|
|
43835
|
-
}));
|
|
43836
|
-
}
|
|
43837
|
-
break;
|
|
43838
|
-
}
|
|
43839
|
-
case "blockEnd":
|
|
43840
|
-
break;
|
|
43841
|
-
case "end":
|
|
43842
|
-
completed = true;
|
|
43843
|
-
yield emit(chunk(requestId, created, model, { index: 0, delta: {}, finish_reason: FINISH3[event.stopReason] }, usageBody(event.usage)));
|
|
43844
|
-
break;
|
|
43845
|
-
case "error": {
|
|
43846
|
-
const e = ERROR_TYPE3[event.code];
|
|
43847
|
-
yield emit({ error: { message: event.message, type: e.type, code: e.code } });
|
|
43848
|
-
break;
|
|
43849
|
-
}
|
|
43850
|
-
}
|
|
43851
|
-
}
|
|
43852
|
-
if (completed)
|
|
43853
|
-
yield { event: "message", data: "[DONE]" };
|
|
43854
|
-
}
|
|
43855
|
-
function openaiResponse(collected, requestId, created) {
|
|
43856
|
-
const text = collected.content.flatMap((b) => b.type === "text" ? [b.text] : []).join("");
|
|
43857
|
-
const toolCalls = collected.content.flatMap((b) => b.type === "toolUse" ? [
|
|
43858
|
-
{
|
|
43859
|
-
id: b.id,
|
|
43860
|
-
type: "function",
|
|
43861
|
-
function: { name: b.name, arguments: JSON.stringify(b.input) }
|
|
43862
|
-
}
|
|
43863
|
-
] : []);
|
|
43864
|
-
return {
|
|
43865
|
-
id: requestId,
|
|
43866
|
-
object: "chat.completion",
|
|
43867
|
-
created,
|
|
43868
|
-
model: collected.model,
|
|
43869
|
-
choices: [
|
|
43870
|
-
{
|
|
43871
|
-
index: 0,
|
|
43872
|
-
message: {
|
|
43873
|
-
role: "assistant",
|
|
43874
|
-
content: toolCalls.length > 0 && text.length === 0 ? null : text,
|
|
43875
|
-
...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
|
|
43876
|
-
},
|
|
43877
|
-
finish_reason: FINISH3[collected.stopReason]
|
|
43878
|
-
}
|
|
43879
|
-
],
|
|
43880
|
-
usage: usageBody(collected.usage)
|
|
43881
|
-
};
|
|
43882
|
-
}
|
|
43883
|
-
function openaiErrorBody(code, message) {
|
|
43884
|
-
const e = ERROR_TYPE3[code];
|
|
43885
|
-
return { error: { message, type: e.type, code: e.code } };
|
|
43886
|
-
}
|
|
43887
|
-
|
|
43888
46221
|
// apps/gateway/src/ingress/anthropicTools.ts
|
|
43889
46222
|
var TOOL_FIELDS = {
|
|
43890
46223
|
cache_control: cacheControlSchema.nullable(),
|
|
@@ -44503,13 +46836,38 @@ var toolCall = exports_external.object({
|
|
|
44503
46836
|
type: exports_external.literal("function"),
|
|
44504
46837
|
function: exports_external.object({ name: exports_external.string(), arguments: exports_external.string() })
|
|
44505
46838
|
});
|
|
46839
|
+
var attachment = exports_external.object({
|
|
46840
|
+
url: exports_external.string(),
|
|
46841
|
+
contentType: exports_external.string().optional(),
|
|
46842
|
+
mediaType: exports_external.string().optional()
|
|
46843
|
+
});
|
|
44506
46844
|
var message2 = exports_external.object({
|
|
44507
46845
|
role: exports_external.enum(["system", "developer", "user", "assistant", "tool"]),
|
|
44508
46846
|
content: exports_external.union([exports_external.string(), exports_external.array(part), exports_external.null()]).optional(),
|
|
44509
46847
|
tool_calls: exports_external.array(toolCall).optional(),
|
|
44510
46848
|
tool_call_id: exports_external.string().optional(),
|
|
44511
|
-
cache_control: exports_external.unknown().optional()
|
|
46849
|
+
cache_control: exports_external.unknown().optional(),
|
|
46850
|
+
images: exports_external.array(exports_external.string()).optional(),
|
|
46851
|
+
attachments: exports_external.array(attachment).optional(),
|
|
46852
|
+
experimental_attachments: exports_external.array(attachment).optional()
|
|
44512
46853
|
});
|
|
46854
|
+
function hasOllamaImages(m) {
|
|
46855
|
+
return (m.images ?? []).length > 0;
|
|
46856
|
+
}
|
|
46857
|
+
function sidecarImages(m) {
|
|
46858
|
+
const blocks2 = [];
|
|
46859
|
+
for (const [index, raw] of (m.images ?? []).entries()) {
|
|
46860
|
+
blocks2.push({ type: "image", ...requireSidecarImage(raw, `messages: images[${index}]`) });
|
|
46861
|
+
}
|
|
46862
|
+
for (const list of [m.attachments, m.experimental_attachments]) {
|
|
46863
|
+
for (const a of list ?? []) {
|
|
46864
|
+
const image = optionalSidecarImage(a.url, a.mediaType ?? a.contentType);
|
|
46865
|
+
if (image !== null)
|
|
46866
|
+
blocks2.push({ type: "image", ...image });
|
|
46867
|
+
}
|
|
46868
|
+
}
|
|
46869
|
+
return blocks2;
|
|
46870
|
+
}
|
|
44513
46871
|
var schema2 = exports_external.object({
|
|
44514
46872
|
model: exports_external.string().min(1),
|
|
44515
46873
|
messages: exports_external.array(message2).min(1),
|
|
@@ -44589,6 +46947,9 @@ function parseOpenAIRequest(body2) {
|
|
|
44589
46947
|
const system = [];
|
|
44590
46948
|
const messages = [];
|
|
44591
46949
|
for (const m of parsed.messages) {
|
|
46950
|
+
if (m.role !== "user" && m.role !== "assistant" && hasOllamaImages(m)) {
|
|
46951
|
+
throw new GatewayError("BAD_REQUEST", `messages: a ${m.role} message cannot carry images; attach them to a user message`);
|
|
46952
|
+
}
|
|
44592
46953
|
if (m.role === "system" || m.role === "developer") {
|
|
44593
46954
|
const blocks2 = contentBlocks(m.content);
|
|
44594
46955
|
applyMessageCacheControl(blocks2, m.cache_control);
|
|
@@ -44614,6 +46975,7 @@ function parseOpenAIRequest(body2) {
|
|
|
44614
46975
|
continue;
|
|
44615
46976
|
}
|
|
44616
46977
|
const content = contentBlocks(m.content);
|
|
46978
|
+
content.push(...sidecarImages(m));
|
|
44617
46979
|
for (const call of m.tool_calls ?? []) {
|
|
44618
46980
|
content.push({
|
|
44619
46981
|
type: "toolUse",
|
|
@@ -44716,8 +47078,8 @@ var KEEPALIVE_MS = 1e4;
|
|
|
44716
47078
|
var KEEPALIVE = Symbol("keepalive");
|
|
44717
47079
|
function withKeepalive(pending, ms) {
|
|
44718
47080
|
let timer;
|
|
44719
|
-
const tick = new Promise((
|
|
44720
|
-
timer = setTimeout(() =>
|
|
47081
|
+
const tick = new Promise((resolve2) => {
|
|
47082
|
+
timer = setTimeout(() => resolve2(KEEPALIVE), ms);
|
|
44721
47083
|
});
|
|
44722
47084
|
return Promise.race([pending, tick]).finally(() => clearTimeout(timer));
|
|
44723
47085
|
}
|
|
@@ -44727,24 +47089,47 @@ var SSE_HEADERS = {
|
|
|
44727
47089
|
connection: "keep-alive",
|
|
44728
47090
|
"x-accel-buffering": "no"
|
|
44729
47091
|
};
|
|
44730
|
-
function
|
|
44731
|
-
|
|
47092
|
+
function errorBody(surface, code, message3) {
|
|
47093
|
+
return surface === "anthropic" ? anthropicErrorBody(code, message3) : openaiErrorBody(code, message3);
|
|
47094
|
+
}
|
|
47095
|
+
function rateLimitHeaders(surface, headroom, now) {
|
|
47096
|
+
return surface === "anthropic" ? anthropicRateLimitHeaders(headroom) : openaiRateLimitHeaders(headroom, now);
|
|
47097
|
+
}
|
|
47098
|
+
function afterThisRequest(headroom) {
|
|
47099
|
+
const requests = headroom.requests;
|
|
47100
|
+
if (requests === undefined)
|
|
47101
|
+
return headroom;
|
|
47102
|
+
return {
|
|
47103
|
+
...headroom,
|
|
47104
|
+
requests: { ...requests, remaining: Math.max(0, requests.remaining - 1) }
|
|
47105
|
+
};
|
|
47106
|
+
}
|
|
47107
|
+
function retryAfterHeaders(error51) {
|
|
47108
|
+
const ms = error51.retryAfterMs;
|
|
47109
|
+
if (ms === undefined || HTTP_STATUS[error51.code] !== 429)
|
|
47110
|
+
return {};
|
|
47111
|
+
return { "retry-after": String(Math.max(0, Math.ceil(ms / 1000))) };
|
|
47112
|
+
}
|
|
47113
|
+
function jsonResponse(body2, status2, headers = {}) {
|
|
44732
47114
|
return new Response(JSON.stringify(body2), {
|
|
44733
|
-
status:
|
|
44734
|
-
headers: { "content-type": "application/json" }
|
|
47115
|
+
status: status2,
|
|
47116
|
+
headers: { "content-type": "application/json", ...headers }
|
|
44735
47117
|
});
|
|
44736
47118
|
}
|
|
47119
|
+
function errorResponse(surface, code, message3, headers = {}) {
|
|
47120
|
+
return jsonResponse(errorBody(surface, code, message3), HTTP_STATUS[code], headers);
|
|
47121
|
+
}
|
|
44737
47122
|
function asGatewayError(error51) {
|
|
44738
47123
|
if (error51 instanceof GatewayError)
|
|
44739
47124
|
return error51;
|
|
44740
47125
|
return new GatewayError("INTERNAL", error51 instanceof Error ? error51.message : "internal error");
|
|
44741
47126
|
}
|
|
44742
|
-
function sseResponse(frames, onDone, keepaliveMs, source) {
|
|
44743
|
-
const
|
|
47127
|
+
function sseResponse(frames, onDone, keepaliveMs, source, limitHeaders, onFrame) {
|
|
47128
|
+
const encoder4 = new TextEncoder;
|
|
44744
47129
|
let done = null;
|
|
44745
|
-
const runOnce = (cancelled = false) => {
|
|
47130
|
+
const runOnce = (cancelled = false, failure) => {
|
|
44746
47131
|
if (done === null)
|
|
44747
|
-
done = onDone(cancelled);
|
|
47132
|
+
done = onDone(cancelled, failure);
|
|
44748
47133
|
return done;
|
|
44749
47134
|
};
|
|
44750
47135
|
let inflight = null;
|
|
@@ -44754,7 +47139,7 @@ function sseResponse(frames, onDone, keepaliveMs, source) {
|
|
|
44754
47139
|
inflight ??= frames.next();
|
|
44755
47140
|
const next = await withKeepalive(inflight, keepaliveMs);
|
|
44756
47141
|
if (next === KEEPALIVE) {
|
|
44757
|
-
controller.enqueue(
|
|
47142
|
+
controller.enqueue(encoder4.encode(`: keepalive
|
|
44758
47143
|
|
|
44759
47144
|
`));
|
|
44760
47145
|
return;
|
|
@@ -44766,13 +47151,15 @@ function sseResponse(frames, onDone, keepaliveMs, source) {
|
|
|
44766
47151
|
return;
|
|
44767
47152
|
}
|
|
44768
47153
|
const { event, data } = next.value;
|
|
44769
|
-
|
|
47154
|
+
onFrame?.(`event: ${event}
|
|
47155
|
+
data: ${data}`);
|
|
47156
|
+
controller.enqueue(encoder4.encode(`event: ${event}
|
|
44770
47157
|
data: ${data}
|
|
44771
47158
|
|
|
44772
47159
|
`));
|
|
44773
47160
|
} catch (error51) {
|
|
44774
47161
|
controller.error(error51);
|
|
44775
|
-
await runOnce();
|
|
47162
|
+
await runOnce(false, error51);
|
|
44776
47163
|
}
|
|
44777
47164
|
},
|
|
44778
47165
|
async cancel() {
|
|
@@ -44781,17 +47168,41 @@ data: ${data}
|
|
|
44781
47168
|
await runOnce(true);
|
|
44782
47169
|
}
|
|
44783
47170
|
});
|
|
44784
|
-
return new Response(stream2, { headers: SSE_HEADERS });
|
|
47171
|
+
return new Response(stream2, { headers: { ...SSE_HEADERS, ...limitHeaders } });
|
|
47172
|
+
}
|
|
47173
|
+
async function bodyCollectorFor(deps, key) {
|
|
47174
|
+
if (deps.bodyLoggingAllowed !== true)
|
|
47175
|
+
return null;
|
|
47176
|
+
if (key.bodyLoggingOptOut)
|
|
47177
|
+
return null;
|
|
47178
|
+
const { settings } = await deps.snapshots.get(deps.now());
|
|
47179
|
+
if (!settings.bodyLoggingEnabled)
|
|
47180
|
+
return null;
|
|
47181
|
+
return createBodyCollector({
|
|
47182
|
+
captureStreamChunks: settings.bodyLoggingCaptureStreamChunks
|
|
47183
|
+
});
|
|
44785
47184
|
}
|
|
44786
47185
|
async function handle(deps, rateLimiter, surface, request2) {
|
|
44787
47186
|
const requestId = deps.requestId();
|
|
44788
47187
|
const startedAt = deps.now();
|
|
44789
47188
|
let keyId = null;
|
|
44790
47189
|
let requestedModel = "";
|
|
47190
|
+
let outcome = null;
|
|
47191
|
+
let collector = null;
|
|
47192
|
+
let writeBodies;
|
|
47193
|
+
let frameSink = null;
|
|
47194
|
+
let logged = false;
|
|
47195
|
+
let release = null;
|
|
47196
|
+
let streaming = false;
|
|
47197
|
+
let headroom = {};
|
|
47198
|
+
const limitHeaders = () => rateLimitHeaders(surface, release === null ? headroom : afterThisRequest(headroom), startedAt);
|
|
47199
|
+
const debit = (id, usage) => {
|
|
47200
|
+
rateLimiter.debit(id, usage);
|
|
47201
|
+
};
|
|
44791
47202
|
try {
|
|
44792
47203
|
let key;
|
|
44793
47204
|
try {
|
|
44794
|
-
key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers));
|
|
47205
|
+
key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers), deps.logger);
|
|
44795
47206
|
} catch (error51) {
|
|
44796
47207
|
if (error51 instanceof GatewayError && error51.code === "AUTH") {
|
|
44797
47208
|
deps.logger.warn("authentication rejected", {
|
|
@@ -44804,8 +47215,12 @@ async function handle(deps, rateLimiter, surface, request2) {
|
|
|
44804
47215
|
}
|
|
44805
47216
|
keyId = key.id;
|
|
44806
47217
|
try {
|
|
44807
|
-
rateLimiter.
|
|
47218
|
+
const admitted = await rateLimiter.admit(key.id, key.limits, requestId);
|
|
47219
|
+
release = admitted.release;
|
|
47220
|
+
headroom = admitted.headroom;
|
|
44808
47221
|
} catch (error51) {
|
|
47222
|
+
if (error51 instanceof RateLimitExceeded)
|
|
47223
|
+
headroom = error51.headroom;
|
|
44809
47224
|
if (error51 instanceof GatewayError && error51.code === "RATE_LIMIT") {
|
|
44810
47225
|
deps.logger.warn("rate limit rejected", {
|
|
44811
47226
|
requestId,
|
|
@@ -44817,15 +47232,35 @@ async function handle(deps, rateLimiter, surface, request2) {
|
|
|
44817
47232
|
}
|
|
44818
47233
|
throw error51;
|
|
44819
47234
|
}
|
|
47235
|
+
const captured = await bodyCollectorFor(deps, key);
|
|
47236
|
+
collector = captured;
|
|
47237
|
+
if (captured !== null) {
|
|
47238
|
+
writeBodies = async (completed) => {
|
|
47239
|
+
await captured.settle();
|
|
47240
|
+
if (frameSink?.truncated === true)
|
|
47241
|
+
captured.client.truncated = true;
|
|
47242
|
+
await deps.store.bodies.put({
|
|
47243
|
+
schemaVersion: ARTIFACT_SCHEMA_VERSION,
|
|
47244
|
+
requestId,
|
|
47245
|
+
at: startedAt,
|
|
47246
|
+
client: captured.client,
|
|
47247
|
+
attempts: captured.attempts(),
|
|
47248
|
+
error: completed.errorCode === null ? null : { code: completed.errorCode, status: completed.status }
|
|
47249
|
+
});
|
|
47250
|
+
};
|
|
47251
|
+
}
|
|
44820
47252
|
const body2 = await request2.json();
|
|
47253
|
+
if (captured !== null)
|
|
47254
|
+
captured.client.request = body2;
|
|
44821
47255
|
const chatRequest = surface === "anthropic" ? parseAnthropicRequest(body2, request2.headers) : parseOpenAIRequest(body2);
|
|
44822
47256
|
if (key.modelAllowlist !== null && !key.modelAllowlist.includes(chatRequest.model)) {
|
|
44823
47257
|
throw new GatewayError("AUTH", `model "${chatRequest.model}" is not allowed for this API key`);
|
|
44824
47258
|
}
|
|
44825
47259
|
requestedModel = chatRequest.model;
|
|
44826
47260
|
let began = false;
|
|
44827
|
-
const
|
|
47261
|
+
const dispatched = await dispatch(chatRequest, {
|
|
44828
47262
|
...deps,
|
|
47263
|
+
...captured === null ? {} : { http: captured.wrap(deps.http) },
|
|
44829
47264
|
async onRoute(target) {
|
|
44830
47265
|
if (began) {
|
|
44831
47266
|
await routeLog(deps.store, requestId, target, deps.logger);
|
|
@@ -44843,55 +47278,82 @@ async function handle(deps, rateLimiter, surface, request2) {
|
|
|
44843
47278
|
await beginLog(deps.store, log2, keyId, deps.logger);
|
|
44844
47279
|
}
|
|
44845
47280
|
}, request2.signal, requestId);
|
|
44846
|
-
|
|
44847
|
-
|
|
47281
|
+
outcome = dispatched;
|
|
47282
|
+
const log = async (cancelled = false, failure2) => {
|
|
47283
|
+
const completed = dispatched.log();
|
|
44848
47284
|
const wasCancelled = cancelled || request2.signal.aborted;
|
|
47285
|
+
if (captured !== null && wasCancelled)
|
|
47286
|
+
captured.client.truncated = true;
|
|
44849
47287
|
if (wasCancelled && completed.status === 0) {
|
|
44850
47288
|
completed.status = 499;
|
|
44851
47289
|
completed.errorCode = "interrupted";
|
|
44852
47290
|
completed.durationMs = deps.now() - startedAt;
|
|
47291
|
+
} else if (!wasCancelled && failure2 !== undefined) {
|
|
47292
|
+
const gatewayError = asGatewayError(failure2);
|
|
47293
|
+
if (completed.status === 0) {
|
|
47294
|
+
completed.status = HTTP_STATUS[gatewayError.code];
|
|
47295
|
+
completed.errorCode = gatewayError.code;
|
|
47296
|
+
completed.durationMs = deps.now() - startedAt;
|
|
47297
|
+
}
|
|
47298
|
+
reportRejection(deps.logger, requestId, completed, gatewayError, surface);
|
|
44853
47299
|
}
|
|
44854
|
-
|
|
47300
|
+
logged = true;
|
|
47301
|
+
await finishLog(deps.store, completed, keyId, deps.logger, writeBodies, debit);
|
|
44855
47302
|
};
|
|
44856
47303
|
if (chatRequest.stream) {
|
|
44857
|
-
const frames = surface === "anthropic" ? anthropicStream(
|
|
44858
|
-
|
|
47304
|
+
const frames = surface === "anthropic" ? anthropicStream(dispatched.events, requestId) : openaiStream(dispatched.events, requestId, Math.floor(deps.now() / 1000));
|
|
47305
|
+
let onFrame;
|
|
47306
|
+
if (captured !== null) {
|
|
47307
|
+
const sink = createFrameSink();
|
|
47308
|
+
frameSink = sink;
|
|
47309
|
+
captured.client.response = sink.frames;
|
|
47310
|
+
onFrame = sink.write;
|
|
47311
|
+
}
|
|
47312
|
+
const finish = async (cancelled = false, failure2) => {
|
|
47313
|
+
release?.();
|
|
47314
|
+
await log(cancelled, failure2);
|
|
47315
|
+
};
|
|
47316
|
+
const response = sseResponse(frames, finish, deps.keepaliveMs, dispatched.events, limitHeaders(), onFrame);
|
|
47317
|
+
streaming = true;
|
|
47318
|
+
return response;
|
|
44859
47319
|
}
|
|
44860
47320
|
const events = [];
|
|
44861
|
-
for await (const event of
|
|
47321
|
+
for await (const event of dispatched.events)
|
|
44862
47322
|
events.push(event);
|
|
44863
|
-
await log();
|
|
44864
47323
|
const failure = events.find((e) => e.type === "error");
|
|
44865
|
-
|
|
44866
|
-
|
|
44867
|
-
|
|
44868
|
-
|
|
44869
|
-
return
|
|
44870
|
-
status: 200,
|
|
44871
|
-
headers: { "content-type": "application/json" }
|
|
44872
|
-
});
|
|
47324
|
+
const responseBody = failure !== undefined ? errorBody(surface, failure.code, failure.message) : surface === "anthropic" ? anthropicResponse(collect(events), requestId) : openaiResponse(collect(events), requestId, Math.floor(deps.now() / 1000));
|
|
47325
|
+
if (captured !== null)
|
|
47326
|
+
captured.client.response = responseBody;
|
|
47327
|
+
await log();
|
|
47328
|
+
return jsonResponse(responseBody, failure === undefined ? 200 : HTTP_STATUS[failure.code], limitHeaders());
|
|
44873
47329
|
} catch (error51) {
|
|
44874
47330
|
const gatewayError = asGatewayError(error51);
|
|
44875
|
-
const
|
|
44876
|
-
|
|
44877
|
-
|
|
44878
|
-
|
|
44879
|
-
|
|
47331
|
+
const cancelled = request2.signal.aborted && error51 === request2.signal.reason;
|
|
47332
|
+
const completed = outcome?.log() ?? newCompletedRequestLog(requestId, startedAt, { requestedModel, status: 0 });
|
|
47333
|
+
completed.status = cancelled ? 499 : HTTP_STATUS[gatewayError.code];
|
|
47334
|
+
completed.errorCode = cancelled ? "interrupted" : gatewayError.code;
|
|
47335
|
+
completed.durationMs = deps.now() - startedAt;
|
|
47336
|
+
const rejection = errorBody(surface, gatewayError.code, gatewayError.message);
|
|
47337
|
+
if (collector !== null && cancelled)
|
|
47338
|
+
collector.client.truncated = true;
|
|
47339
|
+
if (collector !== null && !logged)
|
|
47340
|
+
collector.client.response = rejection;
|
|
47341
|
+
if (!logged)
|
|
47342
|
+
await finishLog(deps.store, completed, keyId, deps.logger, writeBodies, debit);
|
|
47343
|
+
if (!cancelled)
|
|
47344
|
+
reportRejection(deps.logger, requestId, completed, gatewayError, surface);
|
|
47345
|
+
return jsonResponse(rejection, HTTP_STATUS[gatewayError.code], {
|
|
47346
|
+
...limitHeaders(),
|
|
47347
|
+
...retryAfterHeaders(gatewayError)
|
|
44880
47348
|
});
|
|
44881
|
-
|
|
44882
|
-
|
|
44883
|
-
|
|
44884
|
-
surface,
|
|
44885
|
-
status: completed.status,
|
|
44886
|
-
code: gatewayError.code,
|
|
44887
|
-
reason: gatewayError.message
|
|
44888
|
-
});
|
|
44889
|
-
return errorResponse(surface, gatewayError.code, gatewayError.message);
|
|
47349
|
+
} finally {
|
|
47350
|
+
if (!streaming)
|
|
47351
|
+
release?.();
|
|
44890
47352
|
}
|
|
44891
47353
|
}
|
|
44892
47354
|
function proxyRoutes(deps) {
|
|
44893
|
-
const rateLimiter = deps.rateLimiter ?? new ApiKeyRateLimiter(deps.now);
|
|
44894
47355
|
const logger2 = deps.logger ?? noopLogger;
|
|
47356
|
+
const rateLimiter = deps.rateLimiter ?? new ApiKeyRateLimiter({ store: deps.store, now: deps.now, logger: logger2 });
|
|
44895
47357
|
const dispatchDeps = {
|
|
44896
47358
|
...deps,
|
|
44897
47359
|
logger: logger2,
|
|
@@ -44907,7 +47369,7 @@ function proxyRoutes(deps) {
|
|
|
44907
47369
|
return handle(dispatchDeps, rateLimiter, "openai", request2);
|
|
44908
47370
|
}).get("/v1/models", async ({ request: request2 }) => {
|
|
44909
47371
|
try {
|
|
44910
|
-
const key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers));
|
|
47372
|
+
const key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers), logger2);
|
|
44911
47373
|
const snapshot = await dispatchDeps.snapshots.get(deps.now());
|
|
44912
47374
|
const models = [...snapshot.models.values()];
|
|
44913
47375
|
const visibleModels = key.modelAllowlist === null ? models : models.filter((model) => key.modelAllowlist?.includes(model.id));
|
|
@@ -44925,8 +47387,8 @@ function proxyRoutes(deps) {
|
|
|
44925
47387
|
}
|
|
44926
47388
|
}).post("/v1/messages/count_tokens", async ({ request: request2 }) => {
|
|
44927
47389
|
try {
|
|
44928
|
-
const key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers));
|
|
44929
|
-
rateLimiter.consume(key.id, key.
|
|
47390
|
+
const key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers), logger2);
|
|
47391
|
+
await rateLimiter.consume(key.id, key.limits, deps.requestId());
|
|
44930
47392
|
const body2 = await request2.json();
|
|
44931
47393
|
const chatRequest = parseAnthropicRequest(body2, request2.headers);
|
|
44932
47394
|
if (key.modelAllowlist !== null && !key.modelAllowlist.includes(chatRequest.model)) {
|
|
@@ -44940,7 +47402,7 @@ function proxyRoutes(deps) {
|
|
|
44940
47402
|
code: gatewayError.code,
|
|
44941
47403
|
reason: gatewayError.message
|
|
44942
47404
|
});
|
|
44943
|
-
return errorResponse("anthropic", gatewayError.code, gatewayError.message);
|
|
47405
|
+
return errorResponse("anthropic", gatewayError.code, gatewayError.message, retryAfterHeaders(gatewayError));
|
|
44944
47406
|
}
|
|
44945
47407
|
}).onError(({ error: error51 }) => {
|
|
44946
47408
|
const gatewayError = asGatewayError(error51);
|
|
@@ -44955,6 +47417,28 @@ function proxyRoutes(deps) {
|
|
|
44955
47417
|
|
|
44956
47418
|
// apps/gateway/src/app.ts
|
|
44957
47419
|
var ADMIN_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
|
|
47420
|
+
var QUIESCE_MESSAGE = "the gateway is briefly quiesced for database maintenance";
|
|
47421
|
+
function quiesceResponse(path) {
|
|
47422
|
+
const body2 = path === "/v1/chat/completions" ? openaiErrorBody("OVERLOADED", QUIESCE_MESSAGE) : anthropicErrorBody("OVERLOADED", QUIESCE_MESSAGE);
|
|
47423
|
+
return new Response(JSON.stringify(body2), {
|
|
47424
|
+
status: HTTP_STATUS.OVERLOADED,
|
|
47425
|
+
headers: { "content-type": "application/json", "retry-after": "5" }
|
|
47426
|
+
});
|
|
47427
|
+
}
|
|
47428
|
+
function isClientTraffic(path) {
|
|
47429
|
+
return path === "/v1" || path.startsWith("/v1/");
|
|
47430
|
+
}
|
|
47431
|
+
function absentLifecycle() {
|
|
47432
|
+
const refuse = () => {
|
|
47433
|
+
throw new GatewayError("INTERNAL", "this process has no lifecycle control installed");
|
|
47434
|
+
};
|
|
47435
|
+
return {
|
|
47436
|
+
env: process.env,
|
|
47437
|
+
fileExists: (path) => existsSync(path),
|
|
47438
|
+
run: async () => refuse(),
|
|
47439
|
+
stop: refuse
|
|
47440
|
+
};
|
|
47441
|
+
}
|
|
44958
47442
|
function createApp(deps) {
|
|
44959
47443
|
const now = deps.now ?? (() => Date.now());
|
|
44960
47444
|
const logger2 = deps.logger ?? noopLogger;
|
|
@@ -44962,7 +47446,7 @@ function createApp(deps) {
|
|
|
44962
47446
|
const http = deps.http ?? nodeHttpClient({ logger: logger2, now });
|
|
44963
47447
|
const adapters = deps.adapters ?? ADAPTERS;
|
|
44964
47448
|
const requestId = deps.requestId ?? (() => `req_${crypto.randomUUID()}`);
|
|
44965
|
-
const rateLimiter = new ApiKeyRateLimiter(now);
|
|
47449
|
+
const rateLimiter = deps.rateLimiter ?? new ApiKeyRateLimiter({ store: deps.store, now, logger: logger2 });
|
|
44966
47450
|
const snapshots = createRoutingSnapshotCache(deps.store, logger2);
|
|
44967
47451
|
const admin = createAdminAuth(deps.store, { now, sessionTtlMs: ADMIN_SESSION_TTL_MS });
|
|
44968
47452
|
const refresh = deps.refresh ?? createRefresher({
|
|
@@ -44972,14 +47456,27 @@ function createApp(deps) {
|
|
|
44972
47456
|
now,
|
|
44973
47457
|
logger: logger2
|
|
44974
47458
|
});
|
|
44975
|
-
const staticDir = deps.staticDir ?
|
|
47459
|
+
const staticDir = deps.staticDir ? resolve2(deps.staticDir) : undefined;
|
|
44976
47460
|
let staticRoot;
|
|
44977
47461
|
if (staticDir) {
|
|
44978
47462
|
try {
|
|
44979
|
-
staticRoot =
|
|
47463
|
+
staticRoot = realpathSync2(staticDir);
|
|
44980
47464
|
} catch {}
|
|
44981
47465
|
}
|
|
44982
|
-
|
|
47466
|
+
const latch = deps.latch ?? createQuiesceLatch();
|
|
47467
|
+
const admitted = new WeakMap;
|
|
47468
|
+
return new Elysia().onRequest(({ request: request2 }) => {
|
|
47469
|
+
const path = new URL(request2.url).pathname;
|
|
47470
|
+
if (!isClientTraffic(path))
|
|
47471
|
+
return;
|
|
47472
|
+
const release = latch.enter();
|
|
47473
|
+
if (release === null)
|
|
47474
|
+
return quiesceResponse(path);
|
|
47475
|
+
admitted.set(request2, release);
|
|
47476
|
+
}).onAfterResponse(({ request: request2 }) => {
|
|
47477
|
+
admitted.get(request2)?.();
|
|
47478
|
+
admitted.delete(request2);
|
|
47479
|
+
}).get("/health", () => ({ ok: true })).use(proxyRoutes({
|
|
44983
47480
|
store: deps.store,
|
|
44984
47481
|
snapshots,
|
|
44985
47482
|
adapters,
|
|
@@ -44991,16 +47488,27 @@ function createApp(deps) {
|
|
|
44991
47488
|
rateLimiter,
|
|
44992
47489
|
logger: logger2,
|
|
44993
47490
|
...deps.loadRegistry === undefined ? {} : { loadRegistry: deps.loadRegistry },
|
|
44994
|
-
discoveryMirrors: deps.discoveryMirrors === true
|
|
47491
|
+
discoveryMirrors: deps.discoveryMirrors === true,
|
|
47492
|
+
bodyLoggingAllowed: deps.bodyLoggingAllowed === true
|
|
44995
47493
|
})).use(adminRoutes({
|
|
44996
47494
|
store: deps.store,
|
|
44997
47495
|
admin,
|
|
44998
47496
|
baseUrl: deps.baseUrl,
|
|
44999
47497
|
discoveryMirrors: deps.discoveryMirrors === true,
|
|
47498
|
+
bodyLoggingAllowed: deps.bodyLoggingAllowed === true,
|
|
45000
47499
|
now,
|
|
45001
47500
|
sessionTtlMs: ADMIN_SESSION_TTL_MS,
|
|
45002
47501
|
logger: logger2,
|
|
45003
47502
|
...deps.console === undefined ? {} : { console: deps.console }
|
|
47503
|
+
})).use(databaseRoutes({
|
|
47504
|
+
store: deps.store,
|
|
47505
|
+
admin,
|
|
47506
|
+
latch,
|
|
47507
|
+
snapshots,
|
|
47508
|
+
fs: deps.databaseFs ?? nodeDatabaseFs(),
|
|
47509
|
+
now,
|
|
47510
|
+
logger: logger2,
|
|
47511
|
+
lifecycle: deps.lifecycle ?? absentLifecycle()
|
|
45004
47512
|
})).use(connectRoutes({
|
|
45005
47513
|
store: deps.store,
|
|
45006
47514
|
admin,
|
|
@@ -45026,14 +47534,14 @@ function createApp(deps) {
|
|
|
45026
47534
|
if (protectedPrefix)
|
|
45027
47535
|
return error51();
|
|
45028
47536
|
const requestedPath = decodedPath === "/" ? "/index.html" : decodedPath;
|
|
45029
|
-
const filePath =
|
|
45030
|
-
if (filePath !== staticRoot && !filePath.startsWith(`${staticRoot}${
|
|
47537
|
+
const filePath = resolve2(staticRoot, `.${requestedPath}`);
|
|
47538
|
+
if (filePath !== staticRoot && !filePath.startsWith(`${staticRoot}${sep2}`))
|
|
45031
47539
|
return error51();
|
|
45032
47540
|
let resolvedFilePath;
|
|
45033
47541
|
try {
|
|
45034
|
-
resolvedFilePath =
|
|
47542
|
+
resolvedFilePath = realpathSync2(filePath);
|
|
45035
47543
|
} catch {}
|
|
45036
|
-
if (resolvedFilePath && resolvedFilePath !== staticRoot && !resolvedFilePath.startsWith(`${staticRoot}${
|
|
47544
|
+
if (resolvedFilePath && resolvedFilePath !== staticRoot && !resolvedFilePath.startsWith(`${staticRoot}${sep2}`)) {
|
|
45037
47545
|
return error51();
|
|
45038
47546
|
}
|
|
45039
47547
|
const file3 = resolvedFilePath ? Bun.file(resolvedFilePath) : undefined;
|
|
@@ -45049,7 +47557,7 @@ function createApp(deps) {
|
|
|
45049
47557
|
const isNavigation = request2.headers.get("accept")?.includes("text/html") ?? false;
|
|
45050
47558
|
if (decodedPath.startsWith("/assets/") || !isNavigation)
|
|
45051
47559
|
return error51();
|
|
45052
|
-
const indexPath =
|
|
47560
|
+
const indexPath = resolve2(staticRoot, "index.html");
|
|
45053
47561
|
const index = Bun.file(indexPath);
|
|
45054
47562
|
if (index.size === 0)
|
|
45055
47563
|
return error51();
|
|
@@ -45059,25 +47567,92 @@ function createApp(deps) {
|
|
|
45059
47567
|
});
|
|
45060
47568
|
}
|
|
45061
47569
|
|
|
47570
|
+
// apps/gateway/src/lifecycle.ts
|
|
47571
|
+
var STOP_DEADLINE_MS = 5000;
|
|
47572
|
+
function createShutdown(deps) {
|
|
47573
|
+
let shuttingDown = false;
|
|
47574
|
+
const exitAfterClosingStore = (code) => {
|
|
47575
|
+
try {
|
|
47576
|
+
deps.closeStore();
|
|
47577
|
+
} finally {
|
|
47578
|
+
deps.exit(code);
|
|
47579
|
+
}
|
|
47580
|
+
};
|
|
47581
|
+
return (reason, mode = "graceful") => {
|
|
47582
|
+
if (mode === "force" || shuttingDown) {
|
|
47583
|
+
exitAfterClosingStore(1);
|
|
47584
|
+
return;
|
|
47585
|
+
}
|
|
47586
|
+
deps.logger.info("shutdown requested", { reason });
|
|
47587
|
+
shuttingDown = true;
|
|
47588
|
+
for (const stop of deps.stopLoops)
|
|
47589
|
+
stop();
|
|
47590
|
+
let timer;
|
|
47591
|
+
const abandoned = new Promise((resolve3) => {
|
|
47592
|
+
timer = setTimeout(() => resolve3("timeout"), deps.stopDeadlineMs ?? STOP_DEADLINE_MS);
|
|
47593
|
+
timer.unref?.();
|
|
47594
|
+
});
|
|
47595
|
+
Promise.race([deps.stopServer(), abandoned]).then((outcome) => {
|
|
47596
|
+
clearTimeout(timer);
|
|
47597
|
+
if (outcome === "timeout") {
|
|
47598
|
+
deps.logger.warn("shutdown timed out waiting for connections to drain", { reason });
|
|
47599
|
+
}
|
|
47600
|
+
exitAfterClosingStore(0);
|
|
47601
|
+
}, (error51) => {
|
|
47602
|
+
clearTimeout(timer);
|
|
47603
|
+
deps.logger.error("shutdown failed", {
|
|
47604
|
+
reason: error51 instanceof Error ? error51.message : "unknown"
|
|
47605
|
+
});
|
|
47606
|
+
exitAfterClosingStore(1);
|
|
47607
|
+
});
|
|
47608
|
+
};
|
|
47609
|
+
}
|
|
47610
|
+
var RESPONSE_FLUSH_MS = 100;
|
|
47611
|
+
function createDeferredStop(shutdown, delayMs = RESPONSE_FLUSH_MS) {
|
|
47612
|
+
return (exitCode) => {
|
|
47613
|
+
const timer = setTimeout(() => {
|
|
47614
|
+
shutdown("api", exitCode === 0 ? "graceful" : "force");
|
|
47615
|
+
}, delayMs);
|
|
47616
|
+
timer.unref?.();
|
|
47617
|
+
};
|
|
47618
|
+
}
|
|
47619
|
+
|
|
45062
47620
|
// apps/gateway/src/maintenance.ts
|
|
45063
47621
|
var SWEEP_INTERVAL_MS = 60 * 60 * 1000;
|
|
45064
|
-
var
|
|
47622
|
+
var DAY_MS3 = 24 * 60 * 60 * 1000;
|
|
45065
47623
|
var ROLLUP_RETENTION_DAYS = 400;
|
|
45066
47624
|
async function pruneLogs(store, now) {
|
|
45067
47625
|
const settings = await store.config.getSettings();
|
|
45068
|
-
const rawHorizon = now - settings.logRetentionDays *
|
|
47626
|
+
const rawHorizon = now - settings.logRetentionDays * DAY_MS3;
|
|
45069
47627
|
const raw = await store.usage.prune(rawHorizon);
|
|
45070
|
-
const daily = await store.usage.pruneDaily(now - ROLLUP_RETENTION_DAYS *
|
|
47628
|
+
const daily = await store.usage.pruneDaily(now - ROLLUP_RETENTION_DAYS * DAY_MS3);
|
|
45071
47629
|
const quotaSamples = await store.credentials.pruneQuotaSamples(rawHorizon);
|
|
45072
|
-
|
|
47630
|
+
const bodies = await store.bodies.prune(rawHorizon);
|
|
47631
|
+
const bodiesOverCap = await store.bodies.pruneToCap();
|
|
47632
|
+
const bodyOrphans = await store.bodies.sweepOrphans();
|
|
47633
|
+
return { raw, daily, quotaSamples, bodies, bodiesOverCap, bodyOrphans };
|
|
47634
|
+
}
|
|
47635
|
+
async function pruneFiles(deps) {
|
|
47636
|
+
return { snapshots: await pruneSnapshots(deps), staging: sweepStaging(deps) };
|
|
45073
47637
|
}
|
|
45074
47638
|
function startMaintenance(deps) {
|
|
45075
47639
|
const logger2 = deps.logger ?? noopLogger;
|
|
47640
|
+
const files = {
|
|
47641
|
+
store: deps.store,
|
|
47642
|
+
fs: deps.fs ?? nodeDatabaseFs(),
|
|
47643
|
+
now: deps.now
|
|
47644
|
+
};
|
|
45076
47645
|
const timer = setInterval(() => {
|
|
45077
|
-
|
|
47646
|
+
pruneFiles(files).then(({ snapshots, staging }) => logger2.debug("snapshots and staging files swept", { count: snapshots + staging })).catch((error51) => {
|
|
47647
|
+
logger2.error("snapshot sweeping failed", {
|
|
47648
|
+
reason: error51 instanceof Error ? error51.message : "unknown"
|
|
47649
|
+
});
|
|
47650
|
+
});
|
|
47651
|
+
pruneLogs(deps.store, deps.now()).then(({ raw, daily, quotaSamples, bodies, bodiesOverCap, bodyOrphans }) => logger2.debug("request logs pruned", {
|
|
45078
47652
|
rawCount: raw,
|
|
45079
47653
|
dailyCount: daily,
|
|
45080
|
-
quotaSampleCount: quotaSamples
|
|
47654
|
+
quotaSampleCount: quotaSamples,
|
|
47655
|
+
count: bodies + bodiesOverCap + bodyOrphans
|
|
45081
47656
|
})).catch((error51) => {
|
|
45082
47657
|
logger2.error("log pruning failed", {
|
|
45083
47658
|
reason: error51 instanceof Error ? error51.message : "unknown"
|
|
@@ -45179,6 +47754,17 @@ function stdoutLogger(level) {
|
|
|
45179
47754
|
});
|
|
45180
47755
|
}
|
|
45181
47756
|
var logger2 = stdoutLogger("info");
|
|
47757
|
+
var commandRunner = async (argv) => {
|
|
47758
|
+
const [cmd, ...args] = argv;
|
|
47759
|
+
if (cmd === undefined)
|
|
47760
|
+
return { code: 1, stdout: "", stderr: "empty argv" };
|
|
47761
|
+
const proc = Bun.spawn([cmd, ...args], { stdout: "pipe", stderr: "pipe" });
|
|
47762
|
+
const [stdout, stderr] = await Promise.all([
|
|
47763
|
+
new Response(proc.stdout).text(),
|
|
47764
|
+
new Response(proc.stderr).text()
|
|
47765
|
+
]);
|
|
47766
|
+
return { code: await proc.exited, stdout, stderr };
|
|
47767
|
+
};
|
|
45182
47768
|
function consoleSource(logFile) {
|
|
45183
47769
|
const source = resolveConsoleSource({
|
|
45184
47770
|
logFile,
|
|
@@ -45189,17 +47775,7 @@ function consoleSource(logFile) {
|
|
|
45189
47775
|
source,
|
|
45190
47776
|
deps: {
|
|
45191
47777
|
readFile: (path, lines) => tailFile(path, lines),
|
|
45192
|
-
run:
|
|
45193
|
-
const [cmd, ...args] = argv;
|
|
45194
|
-
if (cmd === undefined)
|
|
45195
|
-
return { code: 1, stdout: "", stderr: "empty argv" };
|
|
45196
|
-
const proc = Bun.spawn([cmd, ...args], { stdout: "pipe", stderr: "pipe" });
|
|
45197
|
-
const [stdout, stderr] = await Promise.all([
|
|
45198
|
-
new Response(proc.stdout).text(),
|
|
45199
|
-
new Response(proc.stderr).text()
|
|
45200
|
-
]);
|
|
45201
|
-
return { code: await proc.exited, stdout, stderr };
|
|
45202
|
-
}
|
|
47778
|
+
run: commandRunner
|
|
45203
47779
|
}
|
|
45204
47780
|
};
|
|
45205
47781
|
}
|
|
@@ -45214,10 +47790,10 @@ async function main() {
|
|
|
45214
47790
|
if (config2.staticDir !== null)
|
|
45215
47791
|
return config2.staticDir;
|
|
45216
47792
|
const candidates = [
|
|
45217
|
-
|
|
45218
|
-
|
|
47793
|
+
resolve3(import.meta.dir, "./public"),
|
|
47794
|
+
resolve3(import.meta.dir, "../../dashboard/dist")
|
|
45219
47795
|
];
|
|
45220
|
-
return candidates.find((path) =>
|
|
47796
|
+
return candidates.find((path) => existsSync2(path)) ?? candidates[1];
|
|
45221
47797
|
}
|
|
45222
47798
|
const encryptionKey = await deriveKey(config2.encryptionKey);
|
|
45223
47799
|
const store = await createStore({
|
|
@@ -45234,9 +47810,13 @@ async function main() {
|
|
|
45234
47810
|
const http = nodeHttpClient({ logger: logger2, now });
|
|
45235
47811
|
const refresh = createRefresher({ store, providers: OAUTH_PROVIDERS, http, now, logger: logger2 });
|
|
45236
47812
|
const staticDir = dashboardDir();
|
|
45237
|
-
logger2.info(
|
|
47813
|
+
logger2.info(existsSync2(staticDir) ? "dashboard directory resolved" : "dashboard directory absent", {
|
|
45238
47814
|
path: staticDir
|
|
45239
47815
|
});
|
|
47816
|
+
let shutdown = (reason) => {
|
|
47817
|
+
logger2.error("shutdown requested before the gateway was serving", { reason });
|
|
47818
|
+
process.exit(1);
|
|
47819
|
+
};
|
|
45240
47820
|
const app = createApp({
|
|
45241
47821
|
store,
|
|
45242
47822
|
baseUrl: config2.baseUrl,
|
|
@@ -45246,7 +47826,14 @@ async function main() {
|
|
|
45246
47826
|
staticDir,
|
|
45247
47827
|
logger: logger2,
|
|
45248
47828
|
console: console2,
|
|
45249
|
-
discoveryMirrors: config2.exposeClaudeCodeAliases
|
|
47829
|
+
discoveryMirrors: config2.exposeClaudeCodeAliases,
|
|
47830
|
+
bodyLoggingAllowed: config2.bodyLoggingAllowed,
|
|
47831
|
+
lifecycle: {
|
|
47832
|
+
env: process.env,
|
|
47833
|
+
fileExists: (path) => existsSync2(path),
|
|
47834
|
+
run: commandRunner,
|
|
47835
|
+
stop: createDeferredStop((reason, mode) => shutdown(reason, mode))
|
|
47836
|
+
}
|
|
45250
47837
|
});
|
|
45251
47838
|
const stopMaintenance = startMaintenance({ store, now, logger: logger2 });
|
|
45252
47839
|
const stopRefreshScheduler = startRefreshScheduler({ store, refresh, now, logger: logger2 });
|
|
@@ -45260,30 +47847,17 @@ async function main() {
|
|
|
45260
47847
|
});
|
|
45261
47848
|
app.listen({ port: config2.port, hostname: config2.host, idleTimeout: 255 });
|
|
45262
47849
|
logger2.info("omnigateway listening", { host: config2.host, port: config2.port });
|
|
45263
|
-
|
|
45264
|
-
|
|
45265
|
-
|
|
45266
|
-
|
|
45267
|
-
|
|
45268
|
-
|
|
45269
|
-
|
|
45270
|
-
|
|
47850
|
+
shutdown = createShutdown({
|
|
47851
|
+
logger: logger2,
|
|
47852
|
+
stopLoops: [stopMaintenance, stopRefreshScheduler, stopQuotaPoller],
|
|
47853
|
+
stopServer: async () => {
|
|
47854
|
+
await app.stop();
|
|
47855
|
+
},
|
|
47856
|
+
closeStore: () => store.close(),
|
|
47857
|
+
exit: (code) => process.exit(code)
|
|
47858
|
+
});
|
|
45271
47859
|
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
45272
|
-
process.on(signal, () =>
|
|
45273
|
-
if (shuttingDown)
|
|
45274
|
-
exitAfterClosingStore(1);
|
|
45275
|
-
logger2.info("shutdown requested", { reason: signal });
|
|
45276
|
-
shuttingDown = true;
|
|
45277
|
-
stopMaintenance();
|
|
45278
|
-
stopRefreshScheduler();
|
|
45279
|
-
stopQuotaPoller();
|
|
45280
|
-
app.stop().then(() => exitAfterClosingStore(0), (error51) => {
|
|
45281
|
-
logger2.error("shutdown failed", {
|
|
45282
|
-
reason: error51 instanceof Error ? error51.message : "unknown"
|
|
45283
|
-
});
|
|
45284
|
-
return exitAfterClosingStore(1);
|
|
45285
|
-
});
|
|
45286
|
-
});
|
|
47860
|
+
process.on(signal, () => shutdown(signal));
|
|
45287
47861
|
}
|
|
45288
47862
|
}
|
|
45289
47863
|
try {
|