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/bin/omni.js
CHANGED
|
@@ -102,9 +102,19 @@ function formatSpan(ms) {
|
|
|
102
102
|
function formatUsd(value) {
|
|
103
103
|
return `$${value.toFixed(value < 1 ? 4 : 2)}`;
|
|
104
104
|
}
|
|
105
|
+
function formatBytes(bytes) {
|
|
106
|
+
if (!Number.isFinite(bytes) || bytes < 0)
|
|
107
|
+
return "\u2014";
|
|
108
|
+
if (bytes < 1024)
|
|
109
|
+
return `${Math.round(bytes)} B`;
|
|
110
|
+
const kb = bytes / 1024;
|
|
111
|
+
if (kb < 1024)
|
|
112
|
+
return `${kb.toFixed(1)} KB`;
|
|
113
|
+
return `${(kb / 1024).toFixed(1)} MB`;
|
|
114
|
+
}
|
|
105
115
|
|
|
106
116
|
// apps/cli/src/run.ts
|
|
107
|
-
import { existsSync as existsSync6, mkdirSync as
|
|
117
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
108
118
|
import { homedir as homedir3 } from "os";
|
|
109
119
|
|
|
110
120
|
// packages/control/src/adminAuth.ts
|
|
@@ -161,9 +171,34 @@ function createAdminAuth(store, opts) {
|
|
|
161
171
|
},
|
|
162
172
|
logout(token) {
|
|
163
173
|
sessions.delete(token);
|
|
174
|
+
},
|
|
175
|
+
invalidateSessions() {
|
|
176
|
+
sessions.clear();
|
|
164
177
|
}
|
|
165
178
|
};
|
|
166
179
|
}
|
|
180
|
+
// packages/control/src/bodies.ts
|
|
181
|
+
async function readRequestBody(store, requestId) {
|
|
182
|
+
const read = await store.bodies.get(requestId);
|
|
183
|
+
if (read === null) {
|
|
184
|
+
return {
|
|
185
|
+
requestId,
|
|
186
|
+
detailState: "none",
|
|
187
|
+
truncated: false,
|
|
188
|
+
sizeBytes: 0,
|
|
189
|
+
at: null,
|
|
190
|
+
artifact: null
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
requestId: read.row.requestId,
|
|
195
|
+
detailState: read.row.detailState,
|
|
196
|
+
truncated: read.row.truncated,
|
|
197
|
+
sizeBytes: read.row.sizeBytes,
|
|
198
|
+
at: read.row.at,
|
|
199
|
+
artifact: read.artifact
|
|
200
|
+
};
|
|
201
|
+
}
|
|
167
202
|
// packages/ir/src/betas.ts
|
|
168
203
|
var CONTEXT_1M_BETA = "context-1m-2025-08-07";
|
|
169
204
|
var CONTEXT_1M_TOKENS = 1e6;
|
|
@@ -362,6 +397,7 @@ function loadConfig(env) {
|
|
|
362
397
|
const staticDir = env.OMNI_STATIC_DIR?.trim();
|
|
363
398
|
const logFile = env.OMNI_LOG_FILE?.trim();
|
|
364
399
|
const exposeClaudeCodeAliases = TRUTHY.has((env.OMNI_EXPOSE_CLAUDE_CODE_ALIASES ?? "").trim().toLowerCase());
|
|
400
|
+
const bodyLoggingAllowed = TRUTHY.has((env.OMNI_BODY_LOGGING_ALLOWED ?? "").trim().toLowerCase());
|
|
365
401
|
const rawLogLevel = env.OMNI_LOG_LEVEL?.trim();
|
|
366
402
|
const logLevel = parseLogLevel(rawLogLevel);
|
|
367
403
|
return {
|
|
@@ -374,6 +410,7 @@ function loadConfig(env) {
|
|
|
374
410
|
baseUrl,
|
|
375
411
|
staticDir: staticDir === undefined || staticDir.length === 0 ? null : staticDir,
|
|
376
412
|
exposeClaudeCodeAliases,
|
|
413
|
+
bodyLoggingAllowed,
|
|
377
414
|
logFile: logFile === undefined || logFile.length === 0 ? null : logFile
|
|
378
415
|
};
|
|
379
416
|
}
|
|
@@ -513,14 +550,16 @@ function billingBlock() {
|
|
|
513
550
|
return `${BILLING_PREFIX} cc_version=${CLI_VERSION}.${BUILD_REVISION}; ` + `cc_entrypoint=cli; cch=${CCH_PLACEHOLDER};`;
|
|
514
551
|
}
|
|
515
552
|
var BANNED_SUBSTRINGS = [
|
|
516
|
-
"github.com/anomalyco/
|
|
517
|
-
"
|
|
518
|
-
"github.com/
|
|
519
|
-
"github.com/
|
|
520
|
-
"
|
|
553
|
+
"github.com/anomalyco/opencode",
|
|
554
|
+
"opencode.ai/docs",
|
|
555
|
+
"github.com/cline/cline",
|
|
556
|
+
"github.com/getcursor/cursor",
|
|
557
|
+
"continue.dev",
|
|
558
|
+
"hermes-agent.nousresearch.com"
|
|
521
559
|
];
|
|
560
|
+
var IDENTITY_PREFIXES = ["You are OpenCode", "You are Hermes Agent"];
|
|
522
561
|
var REWRITES = [
|
|
523
|
-
["if
|
|
562
|
+
["if OpenCode honestly", "if the assistant honestly"],
|
|
524
563
|
[
|
|
525
564
|
"Here is some useful information about the environment you are running in:",
|
|
526
565
|
"Environment context you are running in:"
|
|
@@ -529,7 +568,7 @@ var REWRITES = [
|
|
|
529
568
|
function applyAnthropicSystem(system) {
|
|
530
569
|
const kept = [];
|
|
531
570
|
for (const block of system) {
|
|
532
|
-
const text = block.text.split(/\n{2,}/).filter((p) => !BANNED_SUBSTRINGS.some((b) => p.includes(b))).filter((p) => !p.trimStart().startsWith(
|
|
571
|
+
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(`
|
|
533
572
|
|
|
534
573
|
`);
|
|
535
574
|
let rewritten = text;
|
|
@@ -17811,6 +17850,45 @@ function date4(params) {
|
|
|
17811
17850
|
|
|
17812
17851
|
// node_modules/.bun/zod@4.4.3/node_modules/zod/v4/classic/external.js
|
|
17813
17852
|
config(en_default());
|
|
17853
|
+
// packages/ratelimit/src/catalog.ts
|
|
17854
|
+
var WINDOWS = ["1m", "5h", "1w"];
|
|
17855
|
+
var WINDOW_MS = {
|
|
17856
|
+
"1m": 60000,
|
|
17857
|
+
"5h": 5 * 60 * 60 * 1000,
|
|
17858
|
+
"1w": 7 * 24 * 60 * 60 * 1000
|
|
17859
|
+
};
|
|
17860
|
+
var countLimit = exports_external.number().int().positive().nullable();
|
|
17861
|
+
var spendLimit = exports_external.number().positive().nullable();
|
|
17862
|
+
var countWindows = exports_external.object({
|
|
17863
|
+
"1m": countLimit.optional(),
|
|
17864
|
+
"5h": countLimit.optional(),
|
|
17865
|
+
"1w": countLimit.optional()
|
|
17866
|
+
}).strict();
|
|
17867
|
+
var spendWindows = exports_external.object({
|
|
17868
|
+
"5h": spendLimit.optional(),
|
|
17869
|
+
"1w": spendLimit.optional()
|
|
17870
|
+
}).strict();
|
|
17871
|
+
var limitConfigSchema = exports_external.object({
|
|
17872
|
+
requests: countWindows.optional(),
|
|
17873
|
+
tokens: countWindows.optional(),
|
|
17874
|
+
spend: spendWindows.optional(),
|
|
17875
|
+
concurrency: countLimit.optional()
|
|
17876
|
+
}).strict();
|
|
17877
|
+
function assertNoProto(value, path) {
|
|
17878
|
+
if (typeof value !== "object" || value === null)
|
|
17879
|
+
return;
|
|
17880
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
17881
|
+
if (key === "__proto__") {
|
|
17882
|
+
throw new Error(`limits cannot name "__proto__"${path === "" ? "" : ` under ${path}`}`);
|
|
17883
|
+
}
|
|
17884
|
+
assertNoProto(value[key], path === "" ? key : `${path}.${key}`);
|
|
17885
|
+
}
|
|
17886
|
+
}
|
|
17887
|
+
function parseLimitConfig(value) {
|
|
17888
|
+
assertNoProto(value, "");
|
|
17889
|
+
return limitConfigSchema.parse(value);
|
|
17890
|
+
}
|
|
17891
|
+
|
|
17814
17892
|
// packages/control/src/schemas.ts
|
|
17815
17893
|
function parseOrThrow(schema, body2) {
|
|
17816
17894
|
const result = schema.safeParse(body2);
|
|
@@ -17888,7 +17966,13 @@ var modelSchema = exports_external.object({
|
|
|
17888
17966
|
var keyCreateSchema = exports_external.object({
|
|
17889
17967
|
label: exports_external.string().min(1).default("api key"),
|
|
17890
17968
|
modelAllowlist: exports_external.array(exports_external.string().min(1)).nullable().default(null),
|
|
17891
|
-
|
|
17969
|
+
limits: limitConfigSchema.default({}),
|
|
17970
|
+
bodyLoggingOptOut: exports_external.boolean().default(false)
|
|
17971
|
+
}).strict();
|
|
17972
|
+
var keyLimitsSchema = exports_external.object({ limits: limitConfigSchema }).strict();
|
|
17973
|
+
var retentionSchema = exports_external.object({
|
|
17974
|
+
keepLatest: exports_external.number().int().min(1).max(100),
|
|
17975
|
+
maxAgeDays: exports_external.number().int().min(1).max(3650)
|
|
17892
17976
|
}).strict();
|
|
17893
17977
|
var settingsSchema = exports_external.object({
|
|
17894
17978
|
weights: exports_external.object({
|
|
@@ -17905,7 +17989,11 @@ var settingsSchema = exports_external.object({
|
|
|
17905
17989
|
breakerCooldownMs: exports_external.number().int().positive(),
|
|
17906
17990
|
logRetentionDays: exports_external.number().int().min(1),
|
|
17907
17991
|
quotaPollIntervalMs: exports_external.number().int().min(0),
|
|
17908
|
-
rtkEnabled: exports_external.boolean()
|
|
17992
|
+
rtkEnabled: exports_external.boolean(),
|
|
17993
|
+
bodyLoggingEnabled: exports_external.boolean(),
|
|
17994
|
+
bodyLoggingCaptureStreamChunks: exports_external.boolean(),
|
|
17995
|
+
snapshotKeepLatest: retentionSchema.shape.keepLatest.optional(),
|
|
17996
|
+
snapshotMaxAgeDays: retentionSchema.shape.maxAgeDays.optional()
|
|
17909
17997
|
});
|
|
17910
17998
|
var credentialPatchSchema = exports_external.object({
|
|
17911
17999
|
label: exports_external.string().min(1).optional(),
|
|
@@ -18128,7 +18216,11 @@ var DEFAULT_SETTINGS = {
|
|
|
18128
18216
|
breakerCooldownMs: 30000,
|
|
18129
18217
|
logRetentionDays: 30,
|
|
18130
18218
|
quotaPollIntervalMs: 300000,
|
|
18131
|
-
rtkEnabled: false
|
|
18219
|
+
rtkEnabled: false,
|
|
18220
|
+
bodyLoggingEnabled: false,
|
|
18221
|
+
bodyLoggingCaptureStreamChunks: false,
|
|
18222
|
+
snapshotKeepLatest: 5,
|
|
18223
|
+
snapshotMaxAgeDays: 30
|
|
18132
18224
|
};
|
|
18133
18225
|
|
|
18134
18226
|
// packages/router/src/quota.ts
|
|
@@ -18267,6 +18359,10 @@ function rank(input) {
|
|
|
18267
18359
|
return { candidates: scored, excluded };
|
|
18268
18360
|
}
|
|
18269
18361
|
|
|
18362
|
+
// packages/store/src/bodies/artifact.ts
|
|
18363
|
+
import { mkdir, readdir, readFile, rmdir, unlink, writeFile } from "fs/promises";
|
|
18364
|
+
import { dirname, join, sep } from "path";
|
|
18365
|
+
|
|
18270
18366
|
// packages/store/src/encryption.ts
|
|
18271
18367
|
var PREFIX = "enc:v1";
|
|
18272
18368
|
var IV_BYTES = 12;
|
|
@@ -18306,6 +18402,362 @@ function unhex(s) {
|
|
|
18306
18402
|
out[i] = Number.parseInt(s.slice(i * 2, i * 2 + 2), 16);
|
|
18307
18403
|
return out;
|
|
18308
18404
|
}
|
|
18405
|
+
|
|
18406
|
+
// packages/store/src/bodies/bound.ts
|
|
18407
|
+
var MAX_STRING_BYTES = 64 * 1024;
|
|
18408
|
+
var MAX_ARRAY_ITEMS = 24;
|
|
18409
|
+
var MAX_DEPTH = 6;
|
|
18410
|
+
var MAX_OBJECT_KEYS = 80;
|
|
18411
|
+
var STRING_TRUNCATION_MARKER = "\u2026[truncated]";
|
|
18412
|
+
var DEPTH_MARKER = `[omitted: nesting past ${MAX_DEPTH} levels]`;
|
|
18413
|
+
var encoder = new TextEncoder;
|
|
18414
|
+
var decoder = new TextDecoder;
|
|
18415
|
+
var CUT_BYTES = MAX_STRING_BYTES - encoder.encode(STRING_TRUNCATION_MARKER).length - 3;
|
|
18416
|
+
function truncateToBytes(value) {
|
|
18417
|
+
const bytes = encoder.encode(value);
|
|
18418
|
+
if (bytes.length <= MAX_STRING_BYTES)
|
|
18419
|
+
return value;
|
|
18420
|
+
return decoder.decode(bytes.subarray(0, CUT_BYTES)) + STRING_TRUNCATION_MARKER;
|
|
18421
|
+
}
|
|
18422
|
+
function boundValue(value) {
|
|
18423
|
+
let truncated = false;
|
|
18424
|
+
const walk = (input, depth) => {
|
|
18425
|
+
if (typeof input === "string") {
|
|
18426
|
+
const cut = truncateToBytes(input);
|
|
18427
|
+
if (cut !== input)
|
|
18428
|
+
truncated = true;
|
|
18429
|
+
return cut;
|
|
18430
|
+
}
|
|
18431
|
+
if (Array.isArray(input)) {
|
|
18432
|
+
if (depth > MAX_DEPTH) {
|
|
18433
|
+
truncated = true;
|
|
18434
|
+
return DEPTH_MARKER;
|
|
18435
|
+
}
|
|
18436
|
+
const kept = input.length > MAX_ARRAY_ITEMS ? input.slice(-MAX_ARRAY_ITEMS) : input;
|
|
18437
|
+
if (kept.length !== input.length)
|
|
18438
|
+
truncated = true;
|
|
18439
|
+
return kept.map((item) => walk(item, depth + 1));
|
|
18440
|
+
}
|
|
18441
|
+
if (input !== null && typeof input === "object") {
|
|
18442
|
+
if (depth > MAX_DEPTH) {
|
|
18443
|
+
truncated = true;
|
|
18444
|
+
return DEPTH_MARKER;
|
|
18445
|
+
}
|
|
18446
|
+
const entries = Object.entries(input);
|
|
18447
|
+
const kept = entries.length > MAX_OBJECT_KEYS ? entries.slice(0, MAX_OBJECT_KEYS) : entries;
|
|
18448
|
+
if (kept.length !== entries.length)
|
|
18449
|
+
truncated = true;
|
|
18450
|
+
return Object.fromEntries(kept.map(([k, v]) => [k, walk(v, depth + 1)]));
|
|
18451
|
+
}
|
|
18452
|
+
return input;
|
|
18453
|
+
};
|
|
18454
|
+
const bounded = walk(value, 1);
|
|
18455
|
+
return { value: bounded, truncated };
|
|
18456
|
+
}
|
|
18457
|
+
|
|
18458
|
+
// packages/store/src/bodies/mask.ts
|
|
18459
|
+
var ELIDED = "[redacted]";
|
|
18460
|
+
var BEARER = /\b(bearer)\s+[A-Za-z0-9._~+/=-]+/gi;
|
|
18461
|
+
var PREFIXED_KEY = /(?<![A-Za-z0-9_-])(?:sk|ak|pk)-[A-Za-z0-9_-]{8,}/g;
|
|
18462
|
+
var VENDOR_KEY = /(?<![A-Za-z0-9_-])(?:gh[posu]_|github_pat_|AIza|GOCSPX-)[A-Za-z0-9_-]{8,}/g;
|
|
18463
|
+
var VENDOR_PREFIX = /^(?:gh[posu]_|github_pat_|AIza|GOCSPX-)/;
|
|
18464
|
+
var XAI_KEY = /(?<![A-Za-z0-9_-])xai-[A-Za-z0-9_]{8,}/g;
|
|
18465
|
+
var OPAQUE = /[A-Za-z0-9_-]{41,}/g;
|
|
18466
|
+
var MASK_RULES = [
|
|
18467
|
+
{
|
|
18468
|
+
id: "bearer",
|
|
18469
|
+
pattern: BEARER,
|
|
18470
|
+
keep: (match) => match.length - match.replace(/^bearer\s+/i, "").length
|
|
18471
|
+
},
|
|
18472
|
+
{ id: "prefixedKey", pattern: PREFIXED_KEY, keep: () => 3 },
|
|
18473
|
+
{
|
|
18474
|
+
id: "vendorKey",
|
|
18475
|
+
pattern: VENDOR_KEY,
|
|
18476
|
+
keep: (match) => VENDOR_PREFIX.exec(match)?.[0].length ?? 0
|
|
18477
|
+
},
|
|
18478
|
+
{ id: "xaiKey", pattern: XAI_KEY, keep: () => "xai-".length },
|
|
18479
|
+
{ id: "opaque", pattern: OPAQUE, keep: () => 0 }
|
|
18480
|
+
];
|
|
18481
|
+
function maskString(value) {
|
|
18482
|
+
return MASK_RULES.reduce((masked, rule) => masked.replace(rule.pattern, (match) => `${match.slice(0, rule.keep(match))}${ELIDED}`), value);
|
|
18483
|
+
}
|
|
18484
|
+
function maskSecrets(value) {
|
|
18485
|
+
if (typeof value === "string")
|
|
18486
|
+
return maskString(value);
|
|
18487
|
+
if (Array.isArray(value))
|
|
18488
|
+
return value.map(maskSecrets);
|
|
18489
|
+
if (value !== null && typeof value === "object") {
|
|
18490
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, maskSecrets(v)]));
|
|
18491
|
+
}
|
|
18492
|
+
return value;
|
|
18493
|
+
}
|
|
18494
|
+
|
|
18495
|
+
// packages/store/src/bodies/artifact.ts
|
|
18496
|
+
var ARTIFACT_SCHEMA_VERSION = 1;
|
|
18497
|
+
var MAX_ARTIFACT_BYTES = 512 * 1024;
|
|
18498
|
+
var BODY_ROW_CAP = 1e5;
|
|
18499
|
+
var BODIES_DIRNAME = "request_bodies";
|
|
18500
|
+
var encoder2 = new TextEncoder;
|
|
18501
|
+
function bodiesDirFor(databasePath) {
|
|
18502
|
+
return join(dirname(databasePath), BODIES_DIRNAME);
|
|
18503
|
+
}
|
|
18504
|
+
var SAFE_REQUEST_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
18505
|
+
function isSafeRequestId(id) {
|
|
18506
|
+
return SAFE_REQUEST_ID.test(id);
|
|
18507
|
+
}
|
|
18508
|
+
function relPathFor(requestId, at) {
|
|
18509
|
+
const date5 = new Date(at);
|
|
18510
|
+
const yyyy = String(date5.getUTCFullYear()).padStart(4, "0");
|
|
18511
|
+
const mm = String(date5.getUTCMonth() + 1).padStart(2, "0");
|
|
18512
|
+
const dd = String(date5.getUTCDate()).padStart(2, "0");
|
|
18513
|
+
return `${yyyy}/${mm}/${dd}/${requestId}.json.enc`;
|
|
18514
|
+
}
|
|
18515
|
+
async function sha256Hex(bytes) {
|
|
18516
|
+
const digest = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes));
|
|
18517
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
18518
|
+
}
|
|
18519
|
+
function bodyPair(pair) {
|
|
18520
|
+
const request2 = boundValue(maskSecrets(pair.request));
|
|
18521
|
+
const response = boundValue(maskSecrets(pair.response));
|
|
18522
|
+
return {
|
|
18523
|
+
request: request2.value,
|
|
18524
|
+
response: response.value,
|
|
18525
|
+
truncated: pair.truncated === true || request2.truncated || response.truncated
|
|
18526
|
+
};
|
|
18527
|
+
}
|
|
18528
|
+
function omission(serializedBytes) {
|
|
18529
|
+
return {
|
|
18530
|
+
omitted: true,
|
|
18531
|
+
reason: `artifact exceeded ${MAX_ARTIFACT_BYTES} bytes after structural bounding`,
|
|
18532
|
+
serializedBytes
|
|
18533
|
+
};
|
|
18534
|
+
}
|
|
18535
|
+
function omitBodies(artifact, marker) {
|
|
18536
|
+
return {
|
|
18537
|
+
...artifact,
|
|
18538
|
+
client: { request: marker, response: marker, truncated: true },
|
|
18539
|
+
attempts: artifact.attempts.map((attempt) => ({
|
|
18540
|
+
attempt: attempt.attempt,
|
|
18541
|
+
provider: attempt.provider,
|
|
18542
|
+
request: marker,
|
|
18543
|
+
response: marker,
|
|
18544
|
+
streamChunks: null,
|
|
18545
|
+
truncated: true
|
|
18546
|
+
}))
|
|
18547
|
+
};
|
|
18548
|
+
}
|
|
18549
|
+
function prepareArtifact(input) {
|
|
18550
|
+
const client = bodyPair(input.client);
|
|
18551
|
+
const attempts = input.attempts.map((attempt) => {
|
|
18552
|
+
const pair = bodyPair(attempt);
|
|
18553
|
+
const chunks = attempt.streamChunks === null ? null : boundValue(attempt.streamChunks.map(maskString));
|
|
18554
|
+
return {
|
|
18555
|
+
attempt: attempt.attempt,
|
|
18556
|
+
provider: attempt.provider,
|
|
18557
|
+
request: pair.request,
|
|
18558
|
+
response: pair.response,
|
|
18559
|
+
streamChunks: chunks === null ? null : chunks.value,
|
|
18560
|
+
truncated: pair.truncated || (chunks?.truncated ?? false)
|
|
18561
|
+
};
|
|
18562
|
+
});
|
|
18563
|
+
const error51 = boundValue(maskSecrets(input.error));
|
|
18564
|
+
const bounded = {
|
|
18565
|
+
schemaVersion: ARTIFACT_SCHEMA_VERSION,
|
|
18566
|
+
requestId: input.requestId,
|
|
18567
|
+
at: input.at,
|
|
18568
|
+
client,
|
|
18569
|
+
attempts,
|
|
18570
|
+
error: error51.value
|
|
18571
|
+
};
|
|
18572
|
+
const json7 = JSON.stringify(bounded);
|
|
18573
|
+
const size = encoder2.encode(json7).length;
|
|
18574
|
+
if (size <= MAX_ARTIFACT_BYTES)
|
|
18575
|
+
return { artifact: bounded, json: json7 };
|
|
18576
|
+
const marker = omission(size);
|
|
18577
|
+
const omitted = omitBodies(bounded, marker);
|
|
18578
|
+
const omittedJson = JSON.stringify(omitted);
|
|
18579
|
+
if (encoder2.encode(omittedJson).length <= MAX_ARTIFACT_BYTES) {
|
|
18580
|
+
return { artifact: omitted, json: omittedJson };
|
|
18581
|
+
}
|
|
18582
|
+
const stripped = { ...omitted, error: marker };
|
|
18583
|
+
return { artifact: stripped, json: JSON.stringify(stripped) };
|
|
18584
|
+
}
|
|
18585
|
+
async function sealArtifact(key, json7) {
|
|
18586
|
+
const bytes = encoder2.encode(await encrypt(key, json7));
|
|
18587
|
+
return { bytes, sha256: await sha256Hex(bytes) };
|
|
18588
|
+
}
|
|
18589
|
+
async function readArtifact(key, dir, relPath, expectedSha256) {
|
|
18590
|
+
let bytes;
|
|
18591
|
+
try {
|
|
18592
|
+
bytes = new Uint8Array(await readFile(join(dir, relPath)));
|
|
18593
|
+
} catch {
|
|
18594
|
+
return { ok: false, failure: "missing" };
|
|
18595
|
+
}
|
|
18596
|
+
try {
|
|
18597
|
+
if (expectedSha256 !== null && await sha256Hex(bytes) !== expectedSha256) {
|
|
18598
|
+
return { ok: false, failure: "corrupt" };
|
|
18599
|
+
}
|
|
18600
|
+
const parsed = JSON.parse(await decrypt(key, new TextDecoder().decode(bytes)));
|
|
18601
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
18602
|
+
return { ok: false, failure: "corrupt" };
|
|
18603
|
+
}
|
|
18604
|
+
return { ok: true, artifact: parsed };
|
|
18605
|
+
} catch {
|
|
18606
|
+
return { ok: false, failure: "corrupt" };
|
|
18607
|
+
}
|
|
18608
|
+
}
|
|
18609
|
+
async function writeArtifact(dir, relPath, bytes) {
|
|
18610
|
+
const full = join(dir, relPath);
|
|
18611
|
+
await mkdir(dirname(full), { recursive: true });
|
|
18612
|
+
await writeFile(full, bytes);
|
|
18613
|
+
}
|
|
18614
|
+
async function deleteArtifact(dir, relPath) {
|
|
18615
|
+
const full = join(dir, relPath);
|
|
18616
|
+
try {
|
|
18617
|
+
await unlink(full);
|
|
18618
|
+
} catch {
|
|
18619
|
+
return;
|
|
18620
|
+
}
|
|
18621
|
+
let parent = dirname(full);
|
|
18622
|
+
while (parent.length > dir.length && parent.startsWith(dir + sep)) {
|
|
18623
|
+
try {
|
|
18624
|
+
await rmdir(parent);
|
|
18625
|
+
} catch {
|
|
18626
|
+
return;
|
|
18627
|
+
}
|
|
18628
|
+
parent = dirname(parent);
|
|
18629
|
+
}
|
|
18630
|
+
}
|
|
18631
|
+
async function listArtifacts(dir) {
|
|
18632
|
+
const out = [];
|
|
18633
|
+
const walk = async (current, prefix) => {
|
|
18634
|
+
let entries;
|
|
18635
|
+
try {
|
|
18636
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
18637
|
+
} catch {
|
|
18638
|
+
return;
|
|
18639
|
+
}
|
|
18640
|
+
for (const entry of entries) {
|
|
18641
|
+
const rel = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
18642
|
+
if (entry.isDirectory())
|
|
18643
|
+
await walk(join(current, entry.name), rel);
|
|
18644
|
+
else if (entry.isFile())
|
|
18645
|
+
out.push(rel);
|
|
18646
|
+
}
|
|
18647
|
+
};
|
|
18648
|
+
await walk(dir, "");
|
|
18649
|
+
return out;
|
|
18650
|
+
}
|
|
18651
|
+
// packages/store/src/sqlite/bodies.ts
|
|
18652
|
+
function toDetailState(value) {
|
|
18653
|
+
return value === "ready" || value === "missing" || value === "corrupt" ? value : "none";
|
|
18654
|
+
}
|
|
18655
|
+
var toRow = (r) => ({
|
|
18656
|
+
requestId: r.request_id,
|
|
18657
|
+
at: r.at,
|
|
18658
|
+
relPath: r.rel_path,
|
|
18659
|
+
sizeBytes: r.size_bytes,
|
|
18660
|
+
sha256: r.sha256,
|
|
18661
|
+
detailState: toDetailState(r.detail_state),
|
|
18662
|
+
truncated: r.truncated === 1
|
|
18663
|
+
});
|
|
18664
|
+
function createBodyRepo(db, key, dir) {
|
|
18665
|
+
const removeRows = async (rows) => {
|
|
18666
|
+
for (const row of rows) {
|
|
18667
|
+
if (row.relPath !== null)
|
|
18668
|
+
await deleteArtifact(dir, row.relPath);
|
|
18669
|
+
}
|
|
18670
|
+
const remove = db.prepare("DELETE FROM request_bodies WHERE request_id = ?");
|
|
18671
|
+
db.transaction(() => {
|
|
18672
|
+
for (const row of rows)
|
|
18673
|
+
remove.run(row.requestId);
|
|
18674
|
+
})();
|
|
18675
|
+
return rows.length;
|
|
18676
|
+
};
|
|
18677
|
+
const recordState = (requestId, state) => {
|
|
18678
|
+
db.run("UPDATE request_bodies SET detail_state = ? WHERE request_id = ?", [state, requestId]);
|
|
18679
|
+
};
|
|
18680
|
+
return {
|
|
18681
|
+
async put(artifact) {
|
|
18682
|
+
if (!isSafeRequestId(artifact.requestId)) {
|
|
18683
|
+
throw new Error("request id is not safe to use as an artifact path segment");
|
|
18684
|
+
}
|
|
18685
|
+
const prepared = prepareArtifact(artifact);
|
|
18686
|
+
const sealed = await sealArtifact(key, prepared.json);
|
|
18687
|
+
const relPath = relPathFor(artifact.requestId, artifact.at);
|
|
18688
|
+
await writeArtifact(dir, relPath, sealed.bytes);
|
|
18689
|
+
const truncated = prepared.artifact.client.truncated || prepared.artifact.attempts.some((attempt) => attempt.truncated);
|
|
18690
|
+
const row = {
|
|
18691
|
+
requestId: artifact.requestId,
|
|
18692
|
+
at: artifact.at,
|
|
18693
|
+
relPath,
|
|
18694
|
+
sizeBytes: sealed.bytes.length,
|
|
18695
|
+
sha256: sealed.sha256,
|
|
18696
|
+
detailState: "ready",
|
|
18697
|
+
truncated
|
|
18698
|
+
};
|
|
18699
|
+
db.run(`INSERT INTO request_bodies (request_id, at, rel_path, size_bytes, sha256, detail_state, truncated)
|
|
18700
|
+
VALUES (?,?,?,?,?,?,?)
|
|
18701
|
+
ON CONFLICT (request_id) DO UPDATE SET
|
|
18702
|
+
at = excluded.at,
|
|
18703
|
+
rel_path = excluded.rel_path,
|
|
18704
|
+
size_bytes = excluded.size_bytes,
|
|
18705
|
+
sha256 = excluded.sha256,
|
|
18706
|
+
detail_state = excluded.detail_state,
|
|
18707
|
+
truncated = excluded.truncated`, [
|
|
18708
|
+
row.requestId,
|
|
18709
|
+
row.at,
|
|
18710
|
+
row.relPath,
|
|
18711
|
+
row.sizeBytes,
|
|
18712
|
+
row.sha256,
|
|
18713
|
+
row.detailState,
|
|
18714
|
+
row.truncated ? 1 : 0
|
|
18715
|
+
]);
|
|
18716
|
+
return row;
|
|
18717
|
+
},
|
|
18718
|
+
async get(requestId) {
|
|
18719
|
+
const found = db.query("SELECT * FROM request_bodies WHERE request_id = ?").get(requestId);
|
|
18720
|
+
if (found === null)
|
|
18721
|
+
return null;
|
|
18722
|
+
const row = toRow(found);
|
|
18723
|
+
if (row.relPath === null || row.detailState === "none")
|
|
18724
|
+
return { row, artifact: null };
|
|
18725
|
+
const read = await readArtifact(key, dir, row.relPath, row.sha256);
|
|
18726
|
+
if (read.ok) {
|
|
18727
|
+
if (row.detailState !== "ready")
|
|
18728
|
+
recordState(requestId, "ready");
|
|
18729
|
+
return { row: { ...row, detailState: "ready" }, artifact: read.artifact };
|
|
18730
|
+
}
|
|
18731
|
+
recordState(requestId, read.failure);
|
|
18732
|
+
return { row: { ...row, detailState: read.failure }, artifact: null };
|
|
18733
|
+
},
|
|
18734
|
+
async prune(olderThan) {
|
|
18735
|
+
const rows = db.query("SELECT * FROM request_bodies WHERE at < ?").all(olderThan).map(toRow);
|
|
18736
|
+
return removeRows(rows);
|
|
18737
|
+
},
|
|
18738
|
+
async pruneToCap(cap = BODY_ROW_CAP) {
|
|
18739
|
+
const total = db.query("SELECT COUNT(*) AS n FROM request_bodies").get()?.n ?? 0;
|
|
18740
|
+
if (total <= cap)
|
|
18741
|
+
return 0;
|
|
18742
|
+
const rows = db.query("SELECT * FROM request_bodies ORDER BY at ASC, request_id ASC LIMIT ?").all(total - cap).map(toRow);
|
|
18743
|
+
return removeRows(rows);
|
|
18744
|
+
},
|
|
18745
|
+
async sweepOrphans() {
|
|
18746
|
+
const known = new Set(db.query("SELECT rel_path FROM request_bodies WHERE rel_path IS NOT NULL").all().map((r) => r.rel_path));
|
|
18747
|
+
const claimed = db.query("SELECT COUNT(*) AS n FROM request_bodies WHERE rel_path = ?");
|
|
18748
|
+
let removed = 0;
|
|
18749
|
+
for (const rel of await listArtifacts(dir)) {
|
|
18750
|
+
if (known.has(rel))
|
|
18751
|
+
continue;
|
|
18752
|
+
if ((claimed.get(rel)?.n ?? 0) > 0)
|
|
18753
|
+
continue;
|
|
18754
|
+
await deleteArtifact(dir, rel);
|
|
18755
|
+
removed += 1;
|
|
18756
|
+
}
|
|
18757
|
+
return removed;
|
|
18758
|
+
}
|
|
18759
|
+
};
|
|
18760
|
+
}
|
|
18309
18761
|
// packages/store/src/sqlite/config.ts
|
|
18310
18762
|
var SETTINGS_KEY = "settings";
|
|
18311
18763
|
var ADMIN_HASH_KEY = "adminPasswordHash";
|
|
@@ -18361,6 +18813,8 @@ function createConfigRepo(db, emit2 = () => {}) {
|
|
|
18361
18813
|
...DEFAULT_SETTINGS,
|
|
18362
18814
|
...stored,
|
|
18363
18815
|
rtkEnabled: stored.rtkEnabled === true,
|
|
18816
|
+
bodyLoggingEnabled: stored.bodyLoggingEnabled === true,
|
|
18817
|
+
bodyLoggingCaptureStreamChunks: stored.bodyLoggingCaptureStreamChunks === true,
|
|
18364
18818
|
weights: knownWeights(stored.weights)
|
|
18365
18819
|
};
|
|
18366
18820
|
} catch {
|
|
@@ -18391,6 +18845,29 @@ function createConfigRepo(db, emit2 = () => {}) {
|
|
|
18391
18845
|
};
|
|
18392
18846
|
}
|
|
18393
18847
|
// packages/store/src/sqlite/credentials.ts
|
|
18848
|
+
function toHealth(r) {
|
|
18849
|
+
return {
|
|
18850
|
+
credentialId: r.credential_id,
|
|
18851
|
+
model: r.model,
|
|
18852
|
+
breakerState: r.breaker_state,
|
|
18853
|
+
consecutiveFailures: r.consecutive_failures,
|
|
18854
|
+
openedAt: r.opened_at,
|
|
18855
|
+
rateLimitedUntil: r.rate_limited_until,
|
|
18856
|
+
ewmaTtftMs: r.ewma_ttft_ms,
|
|
18857
|
+
lastUsedAt: r.last_used_at
|
|
18858
|
+
};
|
|
18859
|
+
}
|
|
18860
|
+
var UPSERT_HEALTH = `INSERT INTO credential_health
|
|
18861
|
+
(credential_id, model, breaker_state, consecutive_failures, opened_at,
|
|
18862
|
+
rate_limited_until, ewma_ttft_ms, last_used_at)
|
|
18863
|
+
VALUES (?,?,?,?,?,?,?,?)
|
|
18864
|
+
ON CONFLICT (credential_id, model) DO UPDATE SET
|
|
18865
|
+
breaker_state = excluded.breaker_state,
|
|
18866
|
+
consecutive_failures = excluded.consecutive_failures,
|
|
18867
|
+
opened_at = excluded.opened_at,
|
|
18868
|
+
rate_limited_until = excluded.rate_limited_until,
|
|
18869
|
+
ewma_ttft_ms = excluded.ewma_ttft_ms,
|
|
18870
|
+
last_used_at = excluded.last_used_at`;
|
|
18394
18871
|
function createCredentialRepo(db, key, emit2 = () => {}) {
|
|
18395
18872
|
const open = async (v) => v === null ? null : decrypt(key, v);
|
|
18396
18873
|
const secretsFrom = async (row) => ({
|
|
@@ -18565,29 +19042,10 @@ function createCredentialRepo(db, key, emit2 = () => {}) {
|
|
|
18565
19042
|
emit2({ type: "credentialsChanged" });
|
|
18566
19043
|
},
|
|
18567
19044
|
async listHealth() {
|
|
18568
|
-
return db.query("SELECT * FROM credential_health").all().map(
|
|
18569
|
-
credentialId: r.credential_id,
|
|
18570
|
-
model: r.model,
|
|
18571
|
-
breakerState: r.breaker_state,
|
|
18572
|
-
consecutiveFailures: r.consecutive_failures,
|
|
18573
|
-
openedAt: r.opened_at,
|
|
18574
|
-
rateLimitedUntil: r.rate_limited_until,
|
|
18575
|
-
ewmaTtftMs: r.ewma_ttft_ms,
|
|
18576
|
-
lastUsedAt: r.last_used_at
|
|
18577
|
-
}));
|
|
19045
|
+
return db.query("SELECT * FROM credential_health").all().map(toHealth);
|
|
18578
19046
|
},
|
|
18579
19047
|
async saveHealth(rows) {
|
|
18580
|
-
const stmt = db.prepare(
|
|
18581
|
-
(credential_id, model, breaker_state, consecutive_failures, opened_at,
|
|
18582
|
-
rate_limited_until, ewma_ttft_ms, last_used_at)
|
|
18583
|
-
VALUES (?,?,?,?,?,?,?,?)
|
|
18584
|
-
ON CONFLICT (credential_id, model) DO UPDATE SET
|
|
18585
|
-
breaker_state = excluded.breaker_state,
|
|
18586
|
-
consecutive_failures = excluded.consecutive_failures,
|
|
18587
|
-
opened_at = excluded.opened_at,
|
|
18588
|
-
rate_limited_until = excluded.rate_limited_until,
|
|
18589
|
-
ewma_ttft_ms = excluded.ewma_ttft_ms,
|
|
18590
|
-
last_used_at = excluded.last_used_at`);
|
|
19048
|
+
const stmt = db.prepare(UPSERT_HEALTH);
|
|
18591
19049
|
db.transaction(() => {
|
|
18592
19050
|
for (const r of rows) {
|
|
18593
19051
|
stmt.run(r.credentialId, r.model, r.breakerState, r.consecutiveFailures, r.openedAt, r.rateLimitedUntil, r.ewmaTtftMs, r.lastUsedAt);
|
|
@@ -18595,6 +19053,18 @@ function createCredentialRepo(db, key, emit2 = () => {}) {
|
|
|
18595
19053
|
})();
|
|
18596
19054
|
emit2({ type: "healthSaved", rows });
|
|
18597
19055
|
},
|
|
19056
|
+
async updateHealth(credentialId, model, apply) {
|
|
19057
|
+
const current = db.prepare("SELECT * FROM credential_health WHERE credential_id = ? AND model = ?");
|
|
19058
|
+
const stmt = db.prepare(UPSERT_HEALTH);
|
|
19059
|
+
const written = db.transaction(() => {
|
|
19060
|
+
const row = current.get(credentialId, model);
|
|
19061
|
+
const next = apply(row === null ? null : toHealth(row));
|
|
19062
|
+
stmt.run(next.credentialId, next.model, next.breakerState, next.consecutiveFailures, next.openedAt, next.rateLimitedUntil, next.ewmaTtftMs, next.lastUsedAt);
|
|
19063
|
+
return next;
|
|
19064
|
+
})();
|
|
19065
|
+
emit2({ type: "healthSaved", rows: [written] });
|
|
19066
|
+
return written;
|
|
19067
|
+
},
|
|
18598
19068
|
async listQuota() {
|
|
18599
19069
|
return db.query("SELECT * FROM quota_windows").all().map((r) => ({
|
|
18600
19070
|
credentialId: r.credential_id,
|
|
@@ -18906,7 +19376,186 @@ CREATE INDEX quota_samples_observed ON quota_samples (observed_at);
|
|
|
18906
19376
|
ALTER TABLE quota_windows ADD COLUMN window_ms INTEGER;
|
|
18907
19377
|
`;
|
|
18908
19378
|
|
|
19379
|
+
// packages/store/src/sqlite/migrations/008_body_logging.sql
|
|
19380
|
+
var _008_body_logging_default = `-- Captured request and response bodies, and the api key opt-out that suppresses
|
|
19381
|
+
-- them.
|
|
19382
|
+
--
|
|
19383
|
+
-- The row holds a pointer, never a body. Bodies are the largest thing this
|
|
19384
|
+
-- gateway could ever store \u2014 a single conversation with a pasted file in it
|
|
19385
|
+
-- dwarfs the whole of \`request_logs\` \u2014 and SQLite would carry every one of them
|
|
19386
|
+
-- through the same page cache, the same WAL, and the same backup that the
|
|
19387
|
+
-- routing tables live in. A prompt corpus inlined here would make the operating
|
|
19388
|
+
-- database grow without bound, make every \`VACUUM\` copy it, and put plaintext
|
|
19389
|
+
-- rows one \`sqlite3\` invocation away. The artifact instead lives at
|
|
19390
|
+
-- \`<dirname(databasePath)>/request_bodies/YYYY/MM/DD/<request_id>.json.enc\`,
|
|
19391
|
+
-- encrypted under the same key as provider credentials, and this row records
|
|
19392
|
+
-- only where it is and whether it can still be trusted. OmniRoute reached the
|
|
19393
|
+
-- same layout the expensive way: its \`call_logs_v1_legacy\` table exists purely
|
|
19394
|
+
-- to drain inline blobs it had already shipped into file artifacts.
|
|
19395
|
+
--
|
|
19396
|
+
-- \`rel_path\` is relative so moving an installation does not invalidate every
|
|
19397
|
+
-- row, and nullable because a row can exist with no artifact behind it: capture
|
|
19398
|
+
-- may have been suppressed, or the file may have been swept.
|
|
19399
|
+
--
|
|
19400
|
+
-- \`sha256\` is taken over the bytes as stored \u2014 the ciphertext \u2014 not over the
|
|
19401
|
+
-- plaintext. Truncation or bit-rot on disk is then detectable by a reader that
|
|
19402
|
+
-- does not hold \`OMNI_ENCRYPTION_KEY\` at all, which is what makes \`corrupt\` a
|
|
19403
|
+
-- state the reader can report rather than an exception it has to raise.
|
|
19404
|
+
--
|
|
19405
|
+
-- \`detail_state\` is one of \`none\`, \`ready\`, \`missing\`, or \`corrupt\`. A file tree
|
|
19406
|
+
-- and a table it is not written to transactionally will drift \u2014 a crash between
|
|
19407
|
+
-- the two writes is enough \u2014 so the reader records what it observed and hands
|
|
19408
|
+
-- back the metadata. \`missing\` and \`corrupt\` are answers, not failures.
|
|
19409
|
+
--
|
|
19410
|
+
-- \`truncated\` is set when structural bounding altered anything, so a reader can
|
|
19411
|
+
-- say "this is not the whole payload" without diffing it against nothing.
|
|
19412
|
+
--
|
|
19413
|
+
-- No foreign key to \`request_logs\`. Expiry is performed explicitly, deleting the
|
|
19414
|
+
-- file and the row together, because \`ON DELETE CASCADE\` only fires while the
|
|
19415
|
+
-- \`foreign_keys\` pragma is on: a pragma silently off would turn expiry of a
|
|
19416
|
+
-- prompt corpus into indefinite retention of one, and that failure is invisible
|
|
19417
|
+
-- until someone goes looking.
|
|
19418
|
+
CREATE TABLE request_bodies (
|
|
19419
|
+
request_id TEXT PRIMARY KEY,
|
|
19420
|
+
at INTEGER NOT NULL,
|
|
19421
|
+
rel_path TEXT,
|
|
19422
|
+
size_bytes INTEGER NOT NULL DEFAULT 0,
|
|
19423
|
+
sha256 TEXT,
|
|
19424
|
+
detail_state TEXT NOT NULL DEFAULT 'none',
|
|
19425
|
+
truncated INTEGER NOT NULL DEFAULT 0
|
|
19426
|
+
);
|
|
19427
|
+
|
|
19428
|
+
-- Descending, because every read of this table is time-ordered from the newest
|
|
19429
|
+
-- end: the retention sweep walks the oldest rows and the row cap walks them in
|
|
19430
|
+
-- the same direction, and the console only ever asks about recent requests.
|
|
19431
|
+
CREATE INDEX idx_request_bodies_at ON request_bodies (at DESC);
|
|
19432
|
+
|
|
19433
|
+
-- Per-key suppression, checked before any capture work begins.
|
|
19434
|
+
--
|
|
19435
|
+
-- A shared installation can be serving one client whose payloads must not be
|
|
19436
|
+
-- retained while capturing everything else, and that client's operator cannot be
|
|
19437
|
+
-- asked to trust a global switch they do not control. Default 0 keeps every
|
|
19438
|
+
-- existing key on the installation-wide setting, which is itself off by default.
|
|
19439
|
+
ALTER TABLE api_keys ADD COLUMN body_logging_opt_out INTEGER NOT NULL DEFAULT 0;
|
|
19440
|
+
`;
|
|
19441
|
+
|
|
19442
|
+
// packages/store/src/sqlite/migrations/009_key_limits.sql
|
|
19443
|
+
var _009_key_limits_default = `-- Per-key limits as a sparse \`(dimension, window)\` matrix, replacing the single
|
|
19444
|
+
-- requests-per-minute integer.
|
|
19445
|
+
--
|
|
19446
|
+
-- One JSON column rather than a column per pair, because the matrix is sparse
|
|
19447
|
+
-- and the pairs are not fixed: \`requests\` and \`tokens\` are meaningful at all
|
|
19448
|
+
-- three windows, \`spend\` only at the two long ones, and \`concurrency\` is a gauge
|
|
19449
|
+
-- with no window at all. Twelve mostly-null integer columns would encode the
|
|
19450
|
+
-- same thing while making every future pair a migration.
|
|
19451
|
+
--
|
|
19452
|
+
-- The JSON keys are a storage contract in the same class as \`RTK_FILTER_IDS\`:
|
|
19453
|
+
-- the dimension and window names are persisted in every row, so adding a name is
|
|
19454
|
+
-- free and renaming or removing one loses data. Unlike RTK ids, which are
|
|
19455
|
+
-- dropped silently on read, an unknown limit key is a parse failure \u2014 a limit
|
|
19456
|
+
-- the gateway cannot understand must never be read as "no limit", because that
|
|
19457
|
+
-- fails open on a control the operator explicitly set.
|
|
19458
|
+
--
|
|
19459
|
+
-- \`NOT NULL DEFAULT '{}'\` rather than nullable, so "unlimited" has exactly one
|
|
19460
|
+
-- spelling at the column level and every reader parses the same shape.
|
|
19461
|
+
ALTER TABLE api_keys ADD COLUMN limits TEXT NOT NULL DEFAULT '{}';
|
|
19462
|
+
|
|
19463
|
+
-- The old ceiling was requests per minute and nothing else, so it lands whole in
|
|
19464
|
+
-- \`requests["1m"]\`. A NULL meant unlimited and stays unlimited as the default
|
|
19465
|
+
-- \`{}\`: an absent key and an explicit null both mean the same thing, and the
|
|
19466
|
+
-- empty object is the shape a newly minted key starts at.
|
|
19467
|
+
--
|
|
19468
|
+
-- Only a value the reader accepts is carried over. \`rate_limit_per_min\` was
|
|
19469
|
+
-- \`INTEGER\` with no \`CHECK\`, so a hand-edited install can hold \`0\`, \`-5\` or
|
|
19470
|
+
-- \`1.5\` in it, while the new schema is \`z.number().int().positive()\`. Backfilled
|
|
19471
|
+
-- unconditionally, each of those writes a matrix \`parseLimitConfig\` refuses \u2014
|
|
19472
|
+
-- which is \`limits: null\`, which is every request for that key answered
|
|
19473
|
+
-- \`INTERNAL\` at the auth chokepoint. An upgrade must not manufacture the
|
|
19474
|
+
-- unreadable row the design describes as arising only from meddling.
|
|
19475
|
+
--
|
|
19476
|
+
-- Anything else stays at the \`{}\` default, which is unlimited: a ceiling that
|
|
19477
|
+
-- was already nonsense bounded nothing before this migration either, so
|
|
19478
|
+
-- dropping it changes no behaviour and leaves the key serving.
|
|
19479
|
+
--
|
|
19480
|
+
-- \`typeof()\` rather than a range test alone. SQLite's INTEGER affinity is a
|
|
19481
|
+
-- preference, not a constraint: \`1.5\` stays REAL and \`'sixty'\` stays TEXT, and
|
|
19482
|
+
-- TEXT sorts above every number so a bare \`> 0\` would admit the string.
|
|
19483
|
+
UPDATE api_keys
|
|
19484
|
+
SET limits = json_object('requests', json_object('1m', rate_limit_per_min))
|
|
19485
|
+
WHERE typeof(rate_limit_per_min) = 'integer'
|
|
19486
|
+
AND rate_limit_per_min > 0;
|
|
19487
|
+
|
|
19488
|
+
ALTER TABLE api_keys DROP COLUMN rate_limit_per_min;
|
|
19489
|
+
|
|
19490
|
+
-- Correctness-adjacent, not an optimisation, and it must not be dropped later as
|
|
19491
|
+
-- redundant with \`idx_request_logs_at\`.
|
|
19492
|
+
--
|
|
19493
|
+
-- \`request_logs\` leads its existing indexes with \`at\` and with \`credential_id\`;
|
|
19494
|
+
-- nothing leads with \`api_key_id\`. A sliding weekly sum for one key against
|
|
19495
|
+
-- \`idx_request_logs_at\` scans every row in the week for every key on the
|
|
19496
|
+
-- install, and that scan sits on the request hot path.
|
|
19497
|
+
--
|
|
19498
|
+
-- Composite order matters: \`(api_key_id, at DESC)\` lets the range scan start at
|
|
19499
|
+
-- the key. \`(at DESC, api_key_id)\` does not.
|
|
19500
|
+
CREATE INDEX idx_request_logs_key_at ON request_logs (api_key_id, at DESC);
|
|
19501
|
+
`;
|
|
19502
|
+
|
|
19503
|
+
// packages/store/src/sqlite/migrations/010_usage_rollup.sql
|
|
19504
|
+
var _010_usage_rollup_default = `-- Per-key hourly counters, so a long sliding window is read rather than scanned.
|
|
19505
|
+
--
|
|
19506
|
+
-- \`sumSince\` is on the admission path of every request, and \`bun:sqlite\` is
|
|
19507
|
+
-- synchronous: a \`SELECT SUM\` over a week of one key's rows blocks the entire
|
|
19508
|
+
-- event loop for its duration \u2014 \`/health\`, \`/api/*\`, the quiesce latch, and
|
|
19509
|
+
-- every other key's traffic with it. Measured on one machine, one key, WAL:
|
|
19510
|
+
-- 149ms at 0.2M rows in the window, 2.1s at 2M, 10.8s at 8M. The cost is
|
|
19511
|
+
-- O(accumulated history) and has no ceiling, and the eager-refresh rule makes it
|
|
19512
|
+
-- worst exactly when a key is busiest \u2014 a key inside the last tenth of a long
|
|
19513
|
+
-- ceiling reads through on every admission. The same reads against this table
|
|
19514
|
+
-- are flat at ~0.017ms, because a week is 168 buckets however much traffic each
|
|
19515
|
+
-- one summarises.
|
|
19516
|
+
--
|
|
19517
|
+
-- This reverses the design's own rejection of a counter table, and the reason it
|
|
19518
|
+
-- can be reversed is that this table is *derived*. \`request_logs\` stays the
|
|
19519
|
+
-- source of truth; every figure here is reproducible from it by one grouped
|
|
19520
|
+
-- select. The original objection was that a counter table can disagree with the
|
|
19521
|
+
-- log and nobody can tell which is right \u2014 that objection is answered by there
|
|
19522
|
+
-- being a rebuild, a restore that runs it, and a \`doctor\` check that compares
|
|
19523
|
+
-- the two. It would not be answered by care.
|
|
19524
|
+
--
|
|
19525
|
+
-- \`hour\` is \`at / 3600000\`, floored, and every SQL side spells that
|
|
19526
|
+
-- \`CAST(at / 3600000 AS INTEGER)\`. The cast is not decoration: nothing validates
|
|
19527
|
+
-- \`at\`, and SQLite's \`/\` is integer division only when both operands are
|
|
19528
|
+
-- integers, so a fractional \`at\` would give this INTEGER column a REAL key that
|
|
19529
|
+
-- no later integer-hour write ever merges with. Truncation toward zero and
|
|
19530
|
+
-- JavaScript's \`Math.floor\` then agree on every epoch this side of 1970, which
|
|
19531
|
+
-- is every epoch a request log holds.
|
|
19532
|
+
--
|
|
19533
|
+
-- \`api_key_id\` is NOT NULL and anonymous rows are left out: a WITHOUT ROWID
|
|
19534
|
+
-- primary key cannot hold a NULL, and nothing reads this table except a per-key
|
|
19535
|
+
-- lookup that could never match one.
|
|
19536
|
+
CREATE TABLE usage_rollup (
|
|
19537
|
+
api_key_id TEXT NOT NULL,
|
|
19538
|
+
hour INTEGER NOT NULL,
|
|
19539
|
+
requests INTEGER NOT NULL DEFAULT 0,
|
|
19540
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
19541
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
19542
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
19543
|
+
cache_write_tokens INTEGER NOT NULL DEFAULT 0,
|
|
19544
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
19545
|
+
PRIMARY KEY (api_key_id, hour)
|
|
19546
|
+
) WITHOUT ROWID;
|
|
19547
|
+
|
|
19548
|
+
-- The backfill is this migration's \`after\` hook rather than an INSERT here, and
|
|
19549
|
+
-- it is the same \`rebuildRollup\` a restore runs. Seeding an existing install and
|
|
19550
|
+
-- repairing a suspect one are the same statement, so they are the same code and
|
|
19551
|
+
-- cannot drift into disagreeing about which rows count.
|
|
19552
|
+
`;
|
|
19553
|
+
|
|
18909
19554
|
// packages/store/src/sqlite/rollup.ts
|
|
19555
|
+
var HOUR_MS = 3600000;
|
|
19556
|
+
function hourOf(at) {
|
|
19557
|
+
return Math.floor(at / HOUR_MS);
|
|
19558
|
+
}
|
|
18910
19559
|
function startOfLocalDay(at) {
|
|
18911
19560
|
const day = new Date(at);
|
|
18912
19561
|
day.setHours(0, 0, 0, 0);
|
|
@@ -19015,6 +19664,88 @@ function backfillDaily(db) {
|
|
|
19015
19664
|
upsert(db, group.key, group.counters);
|
|
19016
19665
|
return groups.size;
|
|
19017
19666
|
}
|
|
19667
|
+
var HOURLY_UPSERT = `
|
|
19668
|
+
INSERT INTO usage_rollup
|
|
19669
|
+
(api_key_id, hour, requests, input_tokens, output_tokens,
|
|
19670
|
+
cache_read_tokens, cache_write_tokens, cost_usd)
|
|
19671
|
+
VALUES (?,?,?,?,?,?,?,?)
|
|
19672
|
+
ON CONFLICT (api_key_id, hour)
|
|
19673
|
+
DO UPDATE SET
|
|
19674
|
+
requests = requests + excluded.requests,
|
|
19675
|
+
input_tokens = input_tokens + excluded.input_tokens,
|
|
19676
|
+
output_tokens = output_tokens + excluded.output_tokens,
|
|
19677
|
+
cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
|
|
19678
|
+
cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
|
|
19679
|
+
cost_usd = cost_usd + excluded.cost_usd`;
|
|
19680
|
+
function rollupHour(db, log) {
|
|
19681
|
+
const apiKeyId = log.apiKeyId;
|
|
19682
|
+
if (apiKeyId === null)
|
|
19683
|
+
return;
|
|
19684
|
+
db.run(HOURLY_UPSERT, [
|
|
19685
|
+
apiKeyId,
|
|
19686
|
+
hourOf(log.at),
|
|
19687
|
+
1,
|
|
19688
|
+
log.inputTokens,
|
|
19689
|
+
log.outputTokens,
|
|
19690
|
+
log.cacheReadTokens,
|
|
19691
|
+
log.cacheWriteTokens,
|
|
19692
|
+
log.costUsd
|
|
19693
|
+
]);
|
|
19694
|
+
}
|
|
19695
|
+
var REBUILD = `
|
|
19696
|
+
INSERT INTO usage_rollup
|
|
19697
|
+
(api_key_id, hour, requests, input_tokens, output_tokens,
|
|
19698
|
+
cache_read_tokens, cache_write_tokens, cost_usd)
|
|
19699
|
+
SELECT api_key_id,
|
|
19700
|
+
CAST(at / 3600000 AS INTEGER),
|
|
19701
|
+
COUNT(*),
|
|
19702
|
+
COALESCE(SUM(input_tokens), 0),
|
|
19703
|
+
COALESCE(SUM(output_tokens), 0),
|
|
19704
|
+
COALESCE(SUM(cache_read_tokens), 0),
|
|
19705
|
+
COALESCE(SUM(cache_write_tokens), 0),
|
|
19706
|
+
COALESCE(SUM(cost_usd), 0)
|
|
19707
|
+
FROM request_logs
|
|
19708
|
+
WHERE state = 'done' AND api_key_id IS NOT NULL
|
|
19709
|
+
GROUP BY api_key_id, CAST(at / 3600000 AS INTEGER)`;
|
|
19710
|
+
function rebuildRollup(db) {
|
|
19711
|
+
db.run("DELETE FROM usage_rollup");
|
|
19712
|
+
db.run(REBUILD);
|
|
19713
|
+
}
|
|
19714
|
+
function auditRollup(db) {
|
|
19715
|
+
const row = db.query(`WITH truth AS (
|
|
19716
|
+
SELECT api_key_id AS k,
|
|
19717
|
+
CAST(at / 3600000 AS INTEGER) AS h,
|
|
19718
|
+
COUNT(*) AS requests,
|
|
19719
|
+
COALESCE(SUM(input_tokens), 0) AS input_tokens,
|
|
19720
|
+
COALESCE(SUM(output_tokens), 0) AS output_tokens,
|
|
19721
|
+
COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
|
|
19722
|
+
COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,
|
|
19723
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd
|
|
19724
|
+
FROM request_logs
|
|
19725
|
+
WHERE state = 'done' AND api_key_id IS NOT NULL
|
|
19726
|
+
GROUP BY k, h
|
|
19727
|
+
)
|
|
19728
|
+
SELECT (SELECT COUNT(*) FROM truth) AS buckets,
|
|
19729
|
+
(SELECT COUNT(*)
|
|
19730
|
+
FROM truth
|
|
19731
|
+
LEFT JOIN usage_rollup r ON r.api_key_id = truth.k AND r.hour = truth.h
|
|
19732
|
+
WHERE r.api_key_id IS NULL
|
|
19733
|
+
OR r.requests <> truth.requests
|
|
19734
|
+
OR r.input_tokens <> truth.input_tokens
|
|
19735
|
+
OR r.output_tokens <> truth.output_tokens
|
|
19736
|
+
OR r.cache_read_tokens <> truth.cache_read_tokens
|
|
19737
|
+
OR r.cache_write_tokens <> truth.cache_write_tokens
|
|
19738
|
+
OR ABS(r.cost_usd - truth.cost_usd) > 1e-9 + ABS(truth.cost_usd) * 1e-9
|
|
19739
|
+
) AS mismatched,
|
|
19740
|
+
(SELECT COUNT(*)
|
|
19741
|
+
FROM usage_rollup r
|
|
19742
|
+
LEFT JOIN truth ON r.api_key_id = truth.k AND r.hour = truth.h
|
|
19743
|
+
WHERE truth.k IS NULL
|
|
19744
|
+
) AS orphans`).get();
|
|
19745
|
+
const buckets = row?.buckets ?? 0;
|
|
19746
|
+
const mismatched = (row?.mismatched ?? 0) + (row?.orphans ?? 0);
|
|
19747
|
+
return { buckets, mismatched, ok: mismatched === 0 };
|
|
19748
|
+
}
|
|
19018
19749
|
function backfillRtkUsage(db) {
|
|
19019
19750
|
const groups = new Map;
|
|
19020
19751
|
for (const row of db.query(`SELECT at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
|
|
@@ -19051,7 +19782,10 @@ var MIGRATIONS = [
|
|
|
19051
19782
|
{ id: 4, sql: _004_request_state_default },
|
|
19052
19783
|
{ id: 5, sql: _005_rtk_metrics_default },
|
|
19053
19784
|
{ id: 6, sql: _006_rtk_usage_default, after: backfillRtkUsage },
|
|
19054
|
-
{ id: 7, sql: _007_quota_samples_default }
|
|
19785
|
+
{ id: 7, sql: _007_quota_samples_default },
|
|
19786
|
+
{ id: 8, sql: _008_body_logging_default },
|
|
19787
|
+
{ id: 9, sql: _009_key_limits_default },
|
|
19788
|
+
{ id: 10, sql: _010_usage_rollup_default, after: rebuildRollup }
|
|
19055
19789
|
];
|
|
19056
19790
|
function openDb(path) {
|
|
19057
19791
|
const db = new Database(path, { create: true });
|
|
@@ -19082,44 +19816,128 @@ async function hashApiKey(raw) {
|
|
|
19082
19816
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(raw));
|
|
19083
19817
|
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
19084
19818
|
}
|
|
19085
|
-
|
|
19819
|
+
function parseLimits(id, raw, logger2) {
|
|
19820
|
+
try {
|
|
19821
|
+
const parsed = JSON.parse(raw);
|
|
19822
|
+
return parseLimitConfig(parsed);
|
|
19823
|
+
} catch {
|
|
19824
|
+
logger2.error("api key limits unreadable", { apiKeyId: id });
|
|
19825
|
+
return null;
|
|
19826
|
+
}
|
|
19827
|
+
}
|
|
19828
|
+
var toKey = (r, logger2) => ({
|
|
19086
19829
|
id: r.id,
|
|
19087
19830
|
label: r.label,
|
|
19088
19831
|
prefix: r.prefix,
|
|
19089
19832
|
hash: r.hash,
|
|
19090
19833
|
modelAllowlist: r.model_allowlist === null ? null : JSON.parse(r.model_allowlist),
|
|
19091
|
-
|
|
19834
|
+
limits: parseLimits(r.id, r.limits, logger2),
|
|
19835
|
+
bodyLoggingOptOut: r.body_logging_opt_out === 1,
|
|
19092
19836
|
createdAt: r.created_at,
|
|
19093
19837
|
revokedAt: r.revoked_at
|
|
19094
19838
|
});
|
|
19095
|
-
function createKeyRepo(db) {
|
|
19839
|
+
function createKeyRepo(db, logger2 = noopLogger) {
|
|
19096
19840
|
return {
|
|
19097
19841
|
async list() {
|
|
19098
|
-
return db.query("SELECT * FROM api_keys ORDER BY created_at DESC").all().map(toKey);
|
|
19842
|
+
return db.query("SELECT * FROM api_keys ORDER BY created_at DESC").all().map((row) => toKey(row, logger2));
|
|
19099
19843
|
},
|
|
19100
19844
|
async findByHash(hash3) {
|
|
19101
19845
|
const row = db.query("SELECT * FROM api_keys WHERE hash = ?").get(hash3);
|
|
19102
|
-
return row ? toKey(row) : null;
|
|
19846
|
+
return row ? toKey(row, logger2) : null;
|
|
19103
19847
|
},
|
|
19104
19848
|
async create(input) {
|
|
19105
19849
|
const now = Date.now();
|
|
19106
|
-
db.run(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist,
|
|
19107
|
-
|
|
19850
|
+
db.run(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
|
|
19851
|
+
body_logging_opt_out, created_at, revoked_at)
|
|
19852
|
+
VALUES (?,?,?,?,?,?,?,?,NULL)`, [
|
|
19108
19853
|
input.id,
|
|
19109
19854
|
input.label,
|
|
19110
19855
|
input.prefix,
|
|
19111
19856
|
input.hash,
|
|
19112
19857
|
input.modelAllowlist === null ? null : JSON.stringify(input.modelAllowlist),
|
|
19113
|
-
input.
|
|
19858
|
+
JSON.stringify(parseLimitConfig(input.limits)),
|
|
19859
|
+
input.bodyLoggingOptOut ? 1 : 0,
|
|
19114
19860
|
now
|
|
19115
19861
|
]);
|
|
19116
19862
|
return { ...input, createdAt: now, revokedAt: null };
|
|
19117
19863
|
},
|
|
19864
|
+
async setLimits(id, limits) {
|
|
19865
|
+
db.run("UPDATE api_keys SET limits = ? WHERE id = ?", [
|
|
19866
|
+
JSON.stringify(parseLimitConfig(limits)),
|
|
19867
|
+
id
|
|
19868
|
+
]);
|
|
19869
|
+
},
|
|
19118
19870
|
async revoke(id) {
|
|
19119
19871
|
db.run("UPDATE api_keys SET revoked_at = ? WHERE id = ?", [Date.now(), id]);
|
|
19120
19872
|
}
|
|
19121
19873
|
};
|
|
19122
19874
|
}
|
|
19875
|
+
// packages/store/src/sqlite/maintenance.ts
|
|
19876
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
19877
|
+
var REQUIRED_TABLES = [
|
|
19878
|
+
"api_keys",
|
|
19879
|
+
"credential_health",
|
|
19880
|
+
"credentials",
|
|
19881
|
+
"migrations",
|
|
19882
|
+
"quota_windows",
|
|
19883
|
+
"request_logs",
|
|
19884
|
+
"settings",
|
|
19885
|
+
"virtual_models"
|
|
19886
|
+
];
|
|
19887
|
+
var UNREADABLE = "unreadable";
|
|
19888
|
+
function sqlLiteral(value) {
|
|
19889
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
19890
|
+
}
|
|
19891
|
+
function createMaintenanceRepo(db) {
|
|
19892
|
+
return {
|
|
19893
|
+
async stats() {
|
|
19894
|
+
const pageSize = db.query("PRAGMA page_size").get();
|
|
19895
|
+
const pageCount = db.query("PRAGMA page_count").get();
|
|
19896
|
+
const freelist = db.query("PRAGMA freelist_count").get();
|
|
19897
|
+
const version2 = db.query("SELECT MAX(id) AS version FROM migrations").get();
|
|
19898
|
+
return {
|
|
19899
|
+
pageSize: pageSize?.page_size ?? 0,
|
|
19900
|
+
pageCount: pageCount?.page_count ?? 0,
|
|
19901
|
+
freelistCount: freelist?.freelist_count ?? 0,
|
|
19902
|
+
schemaVersion: version2?.version ?? 0
|
|
19903
|
+
};
|
|
19904
|
+
},
|
|
19905
|
+
async vacuum() {
|
|
19906
|
+
db.run("VACUUM");
|
|
19907
|
+
db.run("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
19908
|
+
},
|
|
19909
|
+
async snapshotTo(path) {
|
|
19910
|
+
db.run(`VACUUM INTO ${sqlLiteral(path)}`);
|
|
19911
|
+
},
|
|
19912
|
+
async inspect(path) {
|
|
19913
|
+
let file2;
|
|
19914
|
+
try {
|
|
19915
|
+
file2 = new Database2(path, { readonly: true });
|
|
19916
|
+
} catch {
|
|
19917
|
+
return { ok: false, quickCheck: UNREADABLE, tables: [], counts: {} };
|
|
19918
|
+
}
|
|
19919
|
+
try {
|
|
19920
|
+
const problems = file2.query("PRAGMA quick_check").all();
|
|
19921
|
+
const quickCheck = problems[0]?.quick_check ?? UNREADABLE;
|
|
19922
|
+
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);
|
|
19923
|
+
const present = new Set(tables);
|
|
19924
|
+
const counts = {};
|
|
19925
|
+
for (const table2 of REQUIRED_TABLES) {
|
|
19926
|
+
if (!present.has(table2))
|
|
19927
|
+
continue;
|
|
19928
|
+
const n = file2.query(`SELECT COUNT(*) AS n FROM ${table2}`).get();
|
|
19929
|
+
counts[table2] = n?.n ?? 0;
|
|
19930
|
+
}
|
|
19931
|
+
const complete = REQUIRED_TABLES.every((t) => present.has(t));
|
|
19932
|
+
return { ok: quickCheck === "ok" && complete, quickCheck, tables, counts };
|
|
19933
|
+
} catch {
|
|
19934
|
+
return { ok: false, quickCheck: UNREADABLE, tables: [], counts: {} };
|
|
19935
|
+
} finally {
|
|
19936
|
+
file2.close();
|
|
19937
|
+
}
|
|
19938
|
+
}
|
|
19939
|
+
};
|
|
19940
|
+
}
|
|
19123
19941
|
// packages/rtk/src/catalog.ts
|
|
19124
19942
|
var RTK_FILTER_IDS = [
|
|
19125
19943
|
"git-diff",
|
|
@@ -19186,7 +20004,7 @@ var GROUP_COLUMN = {
|
|
|
19186
20004
|
requestedModel: "requested_model",
|
|
19187
20005
|
apiKey: "api_key_id",
|
|
19188
20006
|
provider: "resolved_provider",
|
|
19189
|
-
hour: "at / 3600000",
|
|
20007
|
+
hour: "CAST(at / 3600000 AS INTEGER)",
|
|
19190
20008
|
day: null
|
|
19191
20009
|
},
|
|
19192
20010
|
daily: {
|
|
@@ -19283,8 +20101,30 @@ function createUsageRepo(db) {
|
|
|
19283
20101
|
const complete = db.transaction((log) => {
|
|
19284
20102
|
db.run(COMPLETE, values(log, "done"));
|
|
19285
20103
|
const stored = db.query("SELECT * FROM request_logs WHERE id = ?").get(log.id);
|
|
19286
|
-
if (stored !== null)
|
|
19287
|
-
|
|
20104
|
+
if (stored !== null) {
|
|
20105
|
+
const restored = toLog(stored);
|
|
20106
|
+
rollupLog(db, restored);
|
|
20107
|
+
rollupHour(db, restored);
|
|
20108
|
+
}
|
|
20109
|
+
});
|
|
20110
|
+
const pruneLogs = db.transaction((olderThan) => {
|
|
20111
|
+
db.run("DELETE FROM request_logs WHERE at < ?", [olderThan]);
|
|
20112
|
+
const removed = db.query("SELECT changes() AS n").get()?.n ?? 0;
|
|
20113
|
+
const boundary = hourOf(olderThan);
|
|
20114
|
+
db.run("DELETE FROM usage_rollup WHERE hour <= ?", [boundary]);
|
|
20115
|
+
db.run(`INSERT INTO usage_rollup
|
|
20116
|
+
(api_key_id, hour, requests, input_tokens, output_tokens,
|
|
20117
|
+
cache_read_tokens, cache_write_tokens, cost_usd)
|
|
20118
|
+
SELECT api_key_id, ?, COUNT(*),
|
|
20119
|
+
COALESCE(SUM(input_tokens), 0),
|
|
20120
|
+
COALESCE(SUM(output_tokens), 0),
|
|
20121
|
+
COALESCE(SUM(cache_read_tokens), 0),
|
|
20122
|
+
COALESCE(SUM(cache_write_tokens), 0),
|
|
20123
|
+
COALESCE(SUM(cost_usd), 0)
|
|
20124
|
+
FROM request_logs
|
|
20125
|
+
WHERE state = 'done' AND api_key_id IS NOT NULL AND at >= ? AND at < ?
|
|
20126
|
+
GROUP BY api_key_id`, [boundary, boundary * HOUR_MS, (boundary + 1) * HOUR_MS]);
|
|
20127
|
+
return removed;
|
|
19288
20128
|
});
|
|
19289
20129
|
return {
|
|
19290
20130
|
async begin(log) {
|
|
@@ -19308,6 +20148,38 @@ function createUsageRepo(db) {
|
|
|
19308
20148
|
async recent(limit) {
|
|
19309
20149
|
return db.query("SELECT * FROM request_logs ORDER BY at DESC LIMIT ?").all(limit).map(toLog);
|
|
19310
20150
|
},
|
|
20151
|
+
async sumSince(apiKeyId, sinceMs) {
|
|
20152
|
+
const boundary = hourOf(sinceMs);
|
|
20153
|
+
const whole = db.query(`SELECT COALESCE(SUM(requests), 0) AS requests,
|
|
20154
|
+
COALESCE(SUM(input_tokens + output_tokens
|
|
20155
|
+
+ cache_read_tokens + cache_write_tokens), 0) AS tokens,
|
|
20156
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd
|
|
20157
|
+
FROM usage_rollup
|
|
20158
|
+
WHERE api_key_id = ? AND hour > ?`).get(apiKeyId, boundary);
|
|
20159
|
+
const edge = db.query(`SELECT COUNT(*) AS requests,
|
|
20160
|
+
COALESCE(SUM(input_tokens + output_tokens
|
|
20161
|
+
+ cache_read_tokens + cache_write_tokens), 0) AS tokens,
|
|
20162
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd
|
|
20163
|
+
FROM request_logs
|
|
20164
|
+
WHERE api_key_id = ? AND state = 'done' AND at >= ? AND at < ?`).get(apiKeyId, sinceMs, (boundary + 1) * HOUR_MS);
|
|
20165
|
+
return {
|
|
20166
|
+
requests: (whole?.requests ?? 0) + (edge?.requests ?? 0),
|
|
20167
|
+
tokens: (whole?.tokens ?? 0) + (edge?.tokens ?? 0),
|
|
20168
|
+
costUsd: (whole?.cost_usd ?? 0) + (edge?.cost_usd ?? 0)
|
|
20169
|
+
};
|
|
20170
|
+
},
|
|
20171
|
+
async rebuildRollup() {
|
|
20172
|
+
rebuildRollup(db);
|
|
20173
|
+
},
|
|
20174
|
+
async auditRollup() {
|
|
20175
|
+
return auditRollup(db);
|
|
20176
|
+
},
|
|
20177
|
+
async oldestSince(apiKeyId, sinceMs) {
|
|
20178
|
+
const row = db.query(`SELECT MIN(at) AS at
|
|
20179
|
+
FROM request_logs
|
|
20180
|
+
WHERE api_key_id = ? AND state = 'done' AND at >= ?`).get(apiKeyId, sinceMs);
|
|
20181
|
+
return row?.at ?? null;
|
|
20182
|
+
},
|
|
19311
20183
|
async aggregate(q) {
|
|
19312
20184
|
const grain = q.grain ?? "raw";
|
|
19313
20185
|
const daily = grain === "daily";
|
|
@@ -19348,8 +20220,7 @@ function createUsageRepo(db) {
|
|
|
19348
20220
|
});
|
|
19349
20221
|
},
|
|
19350
20222
|
async prune(olderThan) {
|
|
19351
|
-
|
|
19352
|
-
return db.query("SELECT changes() AS n").get()?.n ?? 0;
|
|
20223
|
+
return pruneLogs(olderThan);
|
|
19353
20224
|
},
|
|
19354
20225
|
async pruneDaily(olderThan) {
|
|
19355
20226
|
db.run("DELETE FROM usage_daily WHERE day < ?", [olderThan]);
|
|
@@ -19361,8 +20232,6 @@ function createUsageRepo(db) {
|
|
|
19361
20232
|
// packages/store/src/sqlite/store.ts
|
|
19362
20233
|
async function createStore(opts) {
|
|
19363
20234
|
const logger2 = opts.logger ?? noopLogger;
|
|
19364
|
-
const db = openDb(opts.path);
|
|
19365
|
-
logger2.debug("store opened", { path: opts.path });
|
|
19366
20235
|
const listeners = new Set;
|
|
19367
20236
|
const emit2 = (change) => {
|
|
19368
20237
|
for (const listener of listeners) {
|
|
@@ -19371,23 +20240,106 @@ async function createStore(opts) {
|
|
|
19371
20240
|
} catch {}
|
|
19372
20241
|
}
|
|
19373
20242
|
};
|
|
20243
|
+
const open = () => {
|
|
20244
|
+
const db = openDb(opts.path);
|
|
20245
|
+
logger2.debug("store opened", { path: opts.path });
|
|
20246
|
+
return {
|
|
20247
|
+
db,
|
|
20248
|
+
credentials: createCredentialRepo(db, opts.encryptionKey, emit2),
|
|
20249
|
+
config: createConfigRepo(db, emit2),
|
|
20250
|
+
keys: createKeyRepo(db, logger2),
|
|
20251
|
+
usage: createUsageRepo(db),
|
|
20252
|
+
bodies: createBodyRepo(db, opts.encryptionKey, bodiesDirFor(opts.path)),
|
|
20253
|
+
maintenance: createMaintenanceRepo(db)
|
|
20254
|
+
};
|
|
20255
|
+
};
|
|
20256
|
+
let handle = open();
|
|
20257
|
+
let live = true;
|
|
20258
|
+
const closeHandle = () => {
|
|
20259
|
+
if (!live)
|
|
20260
|
+
return;
|
|
20261
|
+
handle.db.close();
|
|
20262
|
+
live = false;
|
|
20263
|
+
};
|
|
19374
20264
|
return {
|
|
19375
|
-
|
|
19376
|
-
|
|
19377
|
-
|
|
19378
|
-
|
|
20265
|
+
databasePath: opts.path,
|
|
20266
|
+
credentials: {
|
|
20267
|
+
list: () => handle.credentials.list(),
|
|
20268
|
+
listRouting: () => handle.credentials.listRouting(),
|
|
20269
|
+
get: (id) => handle.credentials.get(id),
|
|
20270
|
+
create: (input) => handle.credentials.create(input),
|
|
20271
|
+
update: (id, patch) => handle.credentials.update(id, patch),
|
|
20272
|
+
updateSecrets: (id, secrets, expiresAt) => handle.credentials.updateSecrets(id, secrets, expiresAt),
|
|
20273
|
+
remove: (id) => handle.credentials.remove(id),
|
|
20274
|
+
listHealth: () => handle.credentials.listHealth(),
|
|
20275
|
+
saveHealth: (rows) => handle.credentials.saveHealth(rows),
|
|
20276
|
+
updateHealth: (id, model, apply) => handle.credentials.updateHealth(id, model, apply),
|
|
20277
|
+
listQuota: () => handle.credentials.listQuota(),
|
|
20278
|
+
saveQuota: (rows) => handle.credentials.saveQuota(rows),
|
|
20279
|
+
listQuotaSamples: (q) => handle.credentials.listQuotaSamples(q),
|
|
20280
|
+
pruneQuotaSamples: (olderThan) => handle.credentials.pruneQuotaSamples(olderThan)
|
|
20281
|
+
},
|
|
20282
|
+
config: {
|
|
20283
|
+
listModels: () => handle.config.listModels(),
|
|
20284
|
+
putModel: (model) => handle.config.putModel(model),
|
|
20285
|
+
removeModel: (id) => handle.config.removeModel(id),
|
|
20286
|
+
getSettings: () => handle.config.getSettings(),
|
|
20287
|
+
putSettings: (patch) => handle.config.putSettings(patch),
|
|
20288
|
+
getAdminPasswordHash: () => handle.config.getAdminPasswordHash(),
|
|
20289
|
+
setAdminPasswordHashIfAbsent: (hash3) => handle.config.setAdminPasswordHashIfAbsent(hash3),
|
|
20290
|
+
setAdminPasswordHash: (hash3) => handle.config.setAdminPasswordHash(hash3)
|
|
20291
|
+
},
|
|
20292
|
+
keys: {
|
|
20293
|
+
list: () => handle.keys.list(),
|
|
20294
|
+
findByHash: (hash3) => handle.keys.findByHash(hash3),
|
|
20295
|
+
create: (input) => handle.keys.create(input),
|
|
20296
|
+
setLimits: (id, limits) => handle.keys.setLimits(id, limits),
|
|
20297
|
+
revoke: (id) => handle.keys.revoke(id)
|
|
20298
|
+
},
|
|
20299
|
+
usage: {
|
|
20300
|
+
begin: (log) => handle.usage.begin(log),
|
|
20301
|
+
route: (id, target) => handle.usage.route(id, target),
|
|
20302
|
+
append: (log) => handle.usage.append(log),
|
|
20303
|
+
sweepPending: () => handle.usage.sweepPending(),
|
|
20304
|
+
recent: (limit) => handle.usage.recent(limit),
|
|
20305
|
+
aggregate: (q) => handle.usage.aggregate(q),
|
|
20306
|
+
sumSince: (apiKeyId, sinceMs) => handle.usage.sumSince(apiKeyId, sinceMs),
|
|
20307
|
+
oldestSince: (apiKeyId, sinceMs) => handle.usage.oldestSince(apiKeyId, sinceMs),
|
|
20308
|
+
rebuildRollup: () => handle.usage.rebuildRollup(),
|
|
20309
|
+
auditRollup: () => handle.usage.auditRollup(),
|
|
20310
|
+
prune: (olderThan) => handle.usage.prune(olderThan),
|
|
20311
|
+
pruneDaily: (olderThan) => handle.usage.pruneDaily(olderThan)
|
|
20312
|
+
},
|
|
20313
|
+
bodies: {
|
|
20314
|
+
put: (artifact) => handle.bodies.put(artifact),
|
|
20315
|
+
get: (requestId) => handle.bodies.get(requestId),
|
|
20316
|
+
prune: (olderThan) => handle.bodies.prune(olderThan),
|
|
20317
|
+
pruneToCap: (cap) => handle.bodies.pruneToCap(cap),
|
|
20318
|
+
sweepOrphans: () => handle.bodies.sweepOrphans()
|
|
20319
|
+
},
|
|
20320
|
+
maintenance: {
|
|
20321
|
+
stats: () => handle.maintenance.stats(),
|
|
20322
|
+
vacuum: () => handle.maintenance.vacuum(),
|
|
20323
|
+
snapshotTo: (path) => handle.maintenance.snapshotTo(path),
|
|
20324
|
+
inspect: (path) => handle.maintenance.inspect(path)
|
|
20325
|
+
},
|
|
19379
20326
|
routing: {
|
|
19380
|
-
version: () => db.query("PRAGMA data_version").get()?.data_version ?? 0,
|
|
20327
|
+
version: () => handle.db.query("PRAGMA data_version").get()?.data_version ?? 0,
|
|
19381
20328
|
subscribe(listener) {
|
|
19382
20329
|
listeners.add(listener);
|
|
19383
20330
|
return () => listeners.delete(listener);
|
|
19384
20331
|
}
|
|
19385
20332
|
},
|
|
19386
|
-
|
|
20333
|
+
async reopen() {
|
|
20334
|
+
closeHandle();
|
|
20335
|
+
handle = open();
|
|
20336
|
+
live = true;
|
|
20337
|
+
},
|
|
20338
|
+
close: closeHandle
|
|
19387
20339
|
};
|
|
19388
20340
|
}
|
|
19389
20341
|
// packages/control/src/quota/burn.ts
|
|
19390
|
-
var
|
|
20342
|
+
var HOUR_MS2 = 3600000;
|
|
19391
20343
|
function windowStartOf(window) {
|
|
19392
20344
|
if (window.resetsAt === null)
|
|
19393
20345
|
return null;
|
|
@@ -19410,9 +20362,9 @@ function burnFor(window, input) {
|
|
|
19410
20362
|
}
|
|
19411
20363
|
const windowStartsAt = windowStartOf(window);
|
|
19412
20364
|
const elapsedMs = windowStartsAt === null ? null : window.observedAt - windowStartsAt;
|
|
19413
|
-
const elapsedHours = elapsedMs === null ? null : elapsedMs /
|
|
20365
|
+
const elapsedHours = elapsedMs === null ? null : elapsedMs / HOUR_MS2;
|
|
19414
20366
|
const ratePerHour = elapsedHours === null ? null : elapsedHours <= 0 || window.used === 0 ? 0 : window.used / elapsedHours;
|
|
19415
|
-
const exhaustsAt = window.limit === null || ratePerHour === null || ratePerHour <= 0 ? null : window.observedAt + Math.max(0, window.limit - window.used) / ratePerHour *
|
|
20367
|
+
const exhaustsAt = window.limit === null || ratePerHour === null || ratePerHour <= 0 ? null : window.observedAt + Math.max(0, window.limit - window.used) / ratePerHour * HOUR_MS2;
|
|
19416
20368
|
return {
|
|
19417
20369
|
credentialId: window.credentialId,
|
|
19418
20370
|
windowType: window.windowType,
|
|
@@ -19592,6 +20544,236 @@ async function patchCredential(deps, id, input) {
|
|
|
19592
20544
|
async function removeCredential(store, id) {
|
|
19593
20545
|
await store.credentials.remove(id);
|
|
19594
20546
|
}
|
|
20547
|
+
// packages/control/src/database.ts
|
|
20548
|
+
import { basename, dirname as dirname2, join as join2, resolve } from "path";
|
|
20549
|
+
var exclusive = null;
|
|
20550
|
+
async function withExclusive(label2, operation) {
|
|
20551
|
+
if (exclusive !== null) {
|
|
20552
|
+
throw new GatewayError("CONFLICT", `${exclusive} is already running on this database`);
|
|
20553
|
+
}
|
|
20554
|
+
exclusive = label2;
|
|
20555
|
+
try {
|
|
20556
|
+
return await operation();
|
|
20557
|
+
} finally {
|
|
20558
|
+
exclusive = null;
|
|
20559
|
+
}
|
|
20560
|
+
}
|
|
20561
|
+
var SNAPSHOTS_DIRNAME = "snapshots";
|
|
20562
|
+
function snapshotsDir(deps) {
|
|
20563
|
+
return join2(dirname2(deps.store.databasePath), SNAPSHOTS_DIRNAME);
|
|
20564
|
+
}
|
|
20565
|
+
function snapshotName(at, reason) {
|
|
20566
|
+
const stamp = new Date(at).toISOString().replaceAll(":", "-").replaceAll(".", "-");
|
|
20567
|
+
return `db_${stamp}_${reason}.sqlite`;
|
|
20568
|
+
}
|
|
20569
|
+
var SNAPSHOT_ID = /^db_(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z_([A-Za-z]+)\.sqlite$/;
|
|
20570
|
+
function parseSnapshotName(name) {
|
|
20571
|
+
const match = SNAPSHOT_ID.exec(name);
|
|
20572
|
+
if (match === null)
|
|
20573
|
+
return null;
|
|
20574
|
+
const [, date5, hh, mm, ss, ms, reason] = match;
|
|
20575
|
+
const at = Date.parse(`${date5}T${hh}:${mm}:${ss}.${ms}Z`);
|
|
20576
|
+
if (Number.isNaN(at) || reason === undefined)
|
|
20577
|
+
return null;
|
|
20578
|
+
return { at, reason };
|
|
20579
|
+
}
|
|
20580
|
+
function snapshotPath(deps, id) {
|
|
20581
|
+
const dir = resolve(snapshotsDir(deps));
|
|
20582
|
+
const path = resolve(dir, id);
|
|
20583
|
+
if (parseSnapshotName(id) === null || dirname2(path) !== dir) {
|
|
20584
|
+
throw new GatewayError("BAD_REQUEST", "invalid snapshot id");
|
|
20585
|
+
}
|
|
20586
|
+
const real = deps.fs.realpath(path);
|
|
20587
|
+
if (real !== null && dirname2(real) !== (deps.fs.realpath(dir) ?? dir)) {
|
|
20588
|
+
throw new GatewayError("BAD_REQUEST", "invalid snapshot id");
|
|
20589
|
+
}
|
|
20590
|
+
return path;
|
|
20591
|
+
}
|
|
20592
|
+
function listSnapshots(deps) {
|
|
20593
|
+
const dir = snapshotsDir(deps);
|
|
20594
|
+
const snapshots = [];
|
|
20595
|
+
for (const name of deps.fs.readdir(dir)) {
|
|
20596
|
+
const parsed = parseSnapshotName(name);
|
|
20597
|
+
if (parsed === null)
|
|
20598
|
+
continue;
|
|
20599
|
+
snapshots.push({
|
|
20600
|
+
id: name,
|
|
20601
|
+
filename: name,
|
|
20602
|
+
createdAt: parsed.at,
|
|
20603
|
+
sizeBytes: deps.fs.stat(join2(dir, name))?.size ?? 0,
|
|
20604
|
+
reason: parsed.reason
|
|
20605
|
+
});
|
|
20606
|
+
}
|
|
20607
|
+
return snapshots.sort((a, b) => b.createdAt - a.createdAt);
|
|
20608
|
+
}
|
|
20609
|
+
var DAY_MS = 86400000;
|
|
20610
|
+
var SNAPSHOT_HEADROOM = 1.1;
|
|
20611
|
+
function retentionOf(settings) {
|
|
20612
|
+
return { keepLatest: settings.snapshotKeepLatest, maxAgeDays: settings.snapshotMaxAgeDays };
|
|
20613
|
+
}
|
|
20614
|
+
function applyRetention(deps, policy, now) {
|
|
20615
|
+
const dir = snapshotsDir(deps);
|
|
20616
|
+
const cutoff = now - policy.maxAgeDays * DAY_MS;
|
|
20617
|
+
const snapshots = listSnapshots(deps);
|
|
20618
|
+
const undo = snapshots.find((snapshot) => snapshot.reason === "preRestore")?.id;
|
|
20619
|
+
let removed = 0;
|
|
20620
|
+
snapshots.forEach((snapshot, index) => {
|
|
20621
|
+
if (index === 0)
|
|
20622
|
+
return;
|
|
20623
|
+
if (snapshot.id === undo)
|
|
20624
|
+
return;
|
|
20625
|
+
if (index < policy.keepLatest && snapshot.createdAt >= cutoff)
|
|
20626
|
+
return;
|
|
20627
|
+
deps.fs.unlink(join2(dir, snapshot.id));
|
|
20628
|
+
removed++;
|
|
20629
|
+
});
|
|
20630
|
+
return removed;
|
|
20631
|
+
}
|
|
20632
|
+
var STAGING_MAX_AGE_MS = 60 * 60 * 1000;
|
|
20633
|
+
async function writeSnapshot(deps, input) {
|
|
20634
|
+
const dir = snapshotsDir(deps);
|
|
20635
|
+
const at = deps.now();
|
|
20636
|
+
const filename = snapshotName(at, input.reason);
|
|
20637
|
+
const path = join2(dir, filename);
|
|
20638
|
+
deps.fs.mkdir(dir);
|
|
20639
|
+
if (deps.fs.stat(path) !== null) {
|
|
20640
|
+
throw new GatewayError("CONFLICT", "a snapshot for this instant already exists");
|
|
20641
|
+
}
|
|
20642
|
+
const free = deps.fs.freeBytes(dir);
|
|
20643
|
+
const needed = Math.ceil((deps.fs.stat(deps.store.databasePath)?.size ?? 0) * SNAPSHOT_HEADROOM);
|
|
20644
|
+
if (free !== null && free < needed) {
|
|
20645
|
+
throw new GatewayError("CONFLICT", "not enough free disk space to write a snapshot");
|
|
20646
|
+
}
|
|
20647
|
+
try {
|
|
20648
|
+
await deps.store.maintenance.snapshotTo(path);
|
|
20649
|
+
} catch (error51) {
|
|
20650
|
+
deps.fs.unlink(path);
|
|
20651
|
+
throw error51;
|
|
20652
|
+
}
|
|
20653
|
+
const info = {
|
|
20654
|
+
id: filename,
|
|
20655
|
+
filename,
|
|
20656
|
+
createdAt: at,
|
|
20657
|
+
sizeBytes: deps.fs.stat(path)?.size ?? 0,
|
|
20658
|
+
reason: input.reason
|
|
20659
|
+
};
|
|
20660
|
+
if (input.force !== true) {
|
|
20661
|
+
applyRetention(deps, retentionOf(await deps.store.config.getSettings()), at);
|
|
20662
|
+
}
|
|
20663
|
+
return info;
|
|
20664
|
+
}
|
|
20665
|
+
async function createSnapshot(deps, input) {
|
|
20666
|
+
return withExclusive("a snapshot", () => writeSnapshot(deps, input));
|
|
20667
|
+
}
|
|
20668
|
+
|
|
20669
|
+
class SwapFailedError extends Error {
|
|
20670
|
+
preRestoreSnapshotId;
|
|
20671
|
+
cause;
|
|
20672
|
+
reopened;
|
|
20673
|
+
constructor(preRestoreSnapshotId, cause, reopened) {
|
|
20674
|
+
super("the database swap failed and the database may be incomplete");
|
|
20675
|
+
this.preRestoreSnapshotId = preRestoreSnapshotId;
|
|
20676
|
+
this.cause = cause;
|
|
20677
|
+
this.reopened = reopened;
|
|
20678
|
+
this.name = "SwapFailedError";
|
|
20679
|
+
}
|
|
20680
|
+
}
|
|
20681
|
+
var NAMED_TABLES = 5;
|
|
20682
|
+
function describeTables(tables) {
|
|
20683
|
+
if (tables.length === 0)
|
|
20684
|
+
return "it has no tables at all";
|
|
20685
|
+
const named = tables.slice(0, NAMED_TABLES);
|
|
20686
|
+
const rest = tables.length - named.length;
|
|
20687
|
+
return `it has ${named.join(", ")}${rest > 0 ? ` and ${rest} more` : ""}`;
|
|
20688
|
+
}
|
|
20689
|
+
async function swapIn(deps, candidate) {
|
|
20690
|
+
const inspection = await deps.store.maintenance.inspect(candidate.path);
|
|
20691
|
+
if (!inspection.ok) {
|
|
20692
|
+
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}`);
|
|
20693
|
+
}
|
|
20694
|
+
const preRestoreSnapshot = await writeSnapshot(deps, { reason: "preRestore", force: true });
|
|
20695
|
+
const adminHashBefore = await deps.store.config.getAdminPasswordHash();
|
|
20696
|
+
const live = deps.store.databasePath;
|
|
20697
|
+
const staged = `${live}.incoming`;
|
|
20698
|
+
try {
|
|
20699
|
+
if (candidate.consume)
|
|
20700
|
+
deps.fs.rename(candidate.path, staged);
|
|
20701
|
+
else
|
|
20702
|
+
deps.fs.copyFile(candidate.path, staged);
|
|
20703
|
+
} catch (error51) {
|
|
20704
|
+
deps.fs.unlink(staged);
|
|
20705
|
+
throw error51;
|
|
20706
|
+
}
|
|
20707
|
+
try {
|
|
20708
|
+
deps.store.close();
|
|
20709
|
+
deps.fs.unlink(`${live}-wal`);
|
|
20710
|
+
deps.fs.unlink(`${live}-shm`);
|
|
20711
|
+
deps.fs.rename(staged, live);
|
|
20712
|
+
await deps.store.reopen();
|
|
20713
|
+
} catch (error51) {
|
|
20714
|
+
let reopened = true;
|
|
20715
|
+
try {
|
|
20716
|
+
await deps.store.reopen();
|
|
20717
|
+
} catch {
|
|
20718
|
+
reopened = false;
|
|
20719
|
+
}
|
|
20720
|
+
throw new SwapFailedError(preRestoreSnapshot.id, error51, reopened);
|
|
20721
|
+
}
|
|
20722
|
+
const adminHashAfter = await deps.store.config.getAdminPasswordHash();
|
|
20723
|
+
try {
|
|
20724
|
+
await deps.store.usage.rebuildRollup();
|
|
20725
|
+
} catch (error51) {
|
|
20726
|
+
(deps.logger ?? noopLogger).warn("usage rollup not rebuilt after the swap; run omni doctor", {
|
|
20727
|
+
reason: error51 instanceof Error ? error51.message : "unknown"
|
|
20728
|
+
});
|
|
20729
|
+
}
|
|
20730
|
+
return {
|
|
20731
|
+
ok: true,
|
|
20732
|
+
counts: inspection.counts,
|
|
20733
|
+
preRestoreSnapshot,
|
|
20734
|
+
adminPasswordChanged: adminHashAfter !== adminHashBefore
|
|
20735
|
+
};
|
|
20736
|
+
}
|
|
20737
|
+
async function restoreSnapshot(deps, id) {
|
|
20738
|
+
const path = snapshotPath(deps, id);
|
|
20739
|
+
if (deps.fs.stat(path) === null)
|
|
20740
|
+
throw new GatewayError("BAD_REQUEST", "no such snapshot");
|
|
20741
|
+
return withExclusive("a restore", () => swapIn(deps, { path, consume: false }));
|
|
20742
|
+
}
|
|
20743
|
+
var MAX_IMPORT_BYTES = 2 * 1024 * 1024 * 1024;
|
|
20744
|
+
async function vacuum(deps) {
|
|
20745
|
+
return withExclusive("a vacuum", async () => {
|
|
20746
|
+
const before = deps.fs.stat(deps.store.databasePath)?.size ?? 0;
|
|
20747
|
+
const startedAt = deps.now();
|
|
20748
|
+
await deps.store.maintenance.vacuum();
|
|
20749
|
+
const after = deps.fs.stat(deps.store.databasePath)?.size ?? 0;
|
|
20750
|
+
return {
|
|
20751
|
+
reclaimedBytes: Math.max(0, before - after),
|
|
20752
|
+
durationMs: Math.max(0, deps.now() - startedAt)
|
|
20753
|
+
};
|
|
20754
|
+
});
|
|
20755
|
+
}
|
|
20756
|
+
async function getDatabaseOverview(deps) {
|
|
20757
|
+
const live = deps.store.databasePath;
|
|
20758
|
+
const stats = await deps.store.maintenance.stats();
|
|
20759
|
+
const settings = await deps.store.config.getSettings();
|
|
20760
|
+
const snapshots = listSnapshots(deps);
|
|
20761
|
+
return {
|
|
20762
|
+
stats,
|
|
20763
|
+
fileBytes: deps.fs.stat(live)?.size ?? 0,
|
|
20764
|
+
walBytes: deps.fs.stat(`${live}-wal`)?.size ?? 0,
|
|
20765
|
+
bodiesBytes: deps.fs.dirBytes(bodiesDirFor(live)),
|
|
20766
|
+
logicalBytes: stats.pageSize * stats.pageCount,
|
|
20767
|
+
freePageBytes: stats.pageSize * stats.freelistCount,
|
|
20768
|
+
freeDiskBytes: deps.fs.freeBytes(dirname2(live)),
|
|
20769
|
+
retention: retentionOf(settings),
|
|
20770
|
+
snapshots: {
|
|
20771
|
+
count: snapshots.length,
|
|
20772
|
+
totalBytes: snapshots.reduce((sum, snapshot) => sum + snapshot.sizeBytes, 0),
|
|
20773
|
+
latestAt: snapshots[0]?.createdAt ?? null
|
|
20774
|
+
}
|
|
20775
|
+
};
|
|
20776
|
+
}
|
|
19595
20777
|
// packages/control/src/dryRun.ts
|
|
19596
20778
|
async function dryRun(deps, modelId, input) {
|
|
19597
20779
|
const need = parseOrThrow(dryRunSchema, input);
|
|
@@ -19636,17 +20818,67 @@ async function dryRun(deps, modelId, input) {
|
|
|
19636
20818
|
};
|
|
19637
20819
|
}
|
|
19638
20820
|
// packages/control/src/keys.ts
|
|
19639
|
-
|
|
20821
|
+
function configuredWindows(limits) {
|
|
20822
|
+
const needed = new Set;
|
|
20823
|
+
for (const dimension of ["requests", "tokens", "spend"]) {
|
|
20824
|
+
const windows = limits[dimension];
|
|
20825
|
+
if (windows === undefined)
|
|
20826
|
+
continue;
|
|
20827
|
+
for (const window of WINDOWS) {
|
|
20828
|
+
const limit = windows[window];
|
|
20829
|
+
if (limit !== undefined && limit !== null)
|
|
20830
|
+
needed.add(window);
|
|
20831
|
+
}
|
|
20832
|
+
}
|
|
20833
|
+
return [...needed];
|
|
20834
|
+
}
|
|
20835
|
+
async function readLimitUsage(store, id, limits, now) {
|
|
20836
|
+
if (limits === null)
|
|
20837
|
+
return [];
|
|
20838
|
+
const sums = new Map;
|
|
20839
|
+
for (const window of configuredWindows(limits)) {
|
|
20840
|
+
sums.set(window, await store.usage.sumSince(id, now - WINDOW_MS[window]));
|
|
20841
|
+
}
|
|
20842
|
+
const readings = [];
|
|
20843
|
+
for (const dimension of ["requests", "tokens", "spend"]) {
|
|
20844
|
+
const windows = limits[dimension];
|
|
20845
|
+
if (windows === undefined)
|
|
20846
|
+
continue;
|
|
20847
|
+
for (const window of WINDOWS) {
|
|
20848
|
+
const limit = windows[window];
|
|
20849
|
+
if (limit === undefined || limit === null)
|
|
20850
|
+
continue;
|
|
20851
|
+
const sum = sums.get(window);
|
|
20852
|
+
const used = sum === undefined ? null : dimension === "requests" ? sum.requests : dimension === "tokens" ? sum.tokens : sum.costUsd;
|
|
20853
|
+
readings.push({ dimension, window, limit, used });
|
|
20854
|
+
}
|
|
20855
|
+
}
|
|
20856
|
+
if (limits.concurrency !== undefined && limits.concurrency !== null) {
|
|
20857
|
+
readings.push({
|
|
20858
|
+
dimension: "concurrency",
|
|
20859
|
+
window: null,
|
|
20860
|
+
limit: limits.concurrency,
|
|
20861
|
+
used: null
|
|
20862
|
+
});
|
|
20863
|
+
}
|
|
20864
|
+
return readings;
|
|
20865
|
+
}
|
|
20866
|
+
async function toSummary(store, key, now) {
|
|
20867
|
+
return {
|
|
20868
|
+
id: key.id,
|
|
20869
|
+
label: key.label,
|
|
20870
|
+
prefix: key.prefix,
|
|
20871
|
+
modelAllowlist: key.modelAllowlist,
|
|
20872
|
+
limits: key.limits,
|
|
20873
|
+
limitUsage: await readLimitUsage(store, key.id, key.limits, now),
|
|
20874
|
+
bodyLoggingOptOut: key.bodyLoggingOptOut,
|
|
20875
|
+
createdAt: key.createdAt,
|
|
20876
|
+
revokedAt: key.revokedAt
|
|
20877
|
+
};
|
|
20878
|
+
}
|
|
20879
|
+
async function listKeys(store, now = Date.now()) {
|
|
19640
20880
|
const keys = await store.keys.list();
|
|
19641
|
-
return keys.map((
|
|
19642
|
-
id: k.id,
|
|
19643
|
-
label: k.label,
|
|
19644
|
-
prefix: k.prefix,
|
|
19645
|
-
modelAllowlist: k.modelAllowlist,
|
|
19646
|
-
rateLimitPerMin: k.rateLimitPerMin,
|
|
19647
|
-
createdAt: k.createdAt,
|
|
19648
|
-
revokedAt: k.revokedAt
|
|
19649
|
-
}));
|
|
20881
|
+
return Promise.all(keys.map((key) => toSummary(store, key, now)));
|
|
19650
20882
|
}
|
|
19651
20883
|
async function createKey(store, input) {
|
|
19652
20884
|
const body2 = parseOrThrow(keyCreateSchema, input);
|
|
@@ -19657,10 +20889,19 @@ async function createKey(store, input) {
|
|
|
19657
20889
|
prefix: raw.slice(0, 12),
|
|
19658
20890
|
hash: await hashApiKey(raw),
|
|
19659
20891
|
modelAllowlist: body2.modelAllowlist,
|
|
19660
|
-
|
|
20892
|
+
limits: body2.limits,
|
|
20893
|
+
bodyLoggingOptOut: body2.bodyLoggingOptOut
|
|
19661
20894
|
});
|
|
19662
20895
|
return { id: created.id, label: created.label, prefix: created.prefix, key: raw };
|
|
19663
20896
|
}
|
|
20897
|
+
async function setKeyLimits(store, id, input, now = Date.now()) {
|
|
20898
|
+
const body2 = parseOrThrow(keyLimitsSchema, input);
|
|
20899
|
+
const key = (await store.keys.list()).find((entry) => entry.id === id);
|
|
20900
|
+
if (key === undefined)
|
|
20901
|
+
throw new GatewayError("BAD_REQUEST", "no such api key");
|
|
20902
|
+
await store.keys.setLimits(id, body2.limits);
|
|
20903
|
+
return toSummary(store, { ...key, limits: body2.limits }, now);
|
|
20904
|
+
}
|
|
19664
20905
|
async function revokeKey(store, id) {
|
|
19665
20906
|
await store.keys.revoke(id);
|
|
19666
20907
|
}
|
|
@@ -19782,6 +21023,81 @@ async function putModel(store, id, input) {
|
|
|
19782
21023
|
async function removeModel(store, id) {
|
|
19783
21024
|
await store.config.removeModel(id);
|
|
19784
21025
|
}
|
|
21026
|
+
// packages/control/src/nodeFs.ts
|
|
21027
|
+
import {
|
|
21028
|
+
copyFileSync,
|
|
21029
|
+
mkdirSync,
|
|
21030
|
+
readdirSync,
|
|
21031
|
+
realpathSync,
|
|
21032
|
+
renameSync,
|
|
21033
|
+
rmSync,
|
|
21034
|
+
statfsSync,
|
|
21035
|
+
statSync
|
|
21036
|
+
} from "fs";
|
|
21037
|
+
import { join as join3 } from "path";
|
|
21038
|
+
function nodeDatabaseFs() {
|
|
21039
|
+
const dirBytes = (dir) => {
|
|
21040
|
+
let entries;
|
|
21041
|
+
try {
|
|
21042
|
+
entries = readdirSync(dir, { withFileTypes: true, encoding: "utf8" });
|
|
21043
|
+
} catch {
|
|
21044
|
+
return 0;
|
|
21045
|
+
}
|
|
21046
|
+
let total = 0;
|
|
21047
|
+
for (const entry of entries) {
|
|
21048
|
+
const path = join3(dir, entry.name);
|
|
21049
|
+
if (entry.isDirectory()) {
|
|
21050
|
+
total += dirBytes(path);
|
|
21051
|
+
continue;
|
|
21052
|
+
}
|
|
21053
|
+
try {
|
|
21054
|
+
total += statSync(path).size;
|
|
21055
|
+
} catch {}
|
|
21056
|
+
}
|
|
21057
|
+
return total;
|
|
21058
|
+
};
|
|
21059
|
+
return {
|
|
21060
|
+
readdir: (dir) => {
|
|
21061
|
+
try {
|
|
21062
|
+
return readdirSync(dir);
|
|
21063
|
+
} catch {
|
|
21064
|
+
return [];
|
|
21065
|
+
}
|
|
21066
|
+
},
|
|
21067
|
+
stat: (path) => {
|
|
21068
|
+
try {
|
|
21069
|
+
const stat = statSync(path);
|
|
21070
|
+
return { size: stat.size, mtimeMs: stat.mtimeMs };
|
|
21071
|
+
} catch {
|
|
21072
|
+
return null;
|
|
21073
|
+
}
|
|
21074
|
+
},
|
|
21075
|
+
unlink: (path) => {
|
|
21076
|
+
rmSync(path, { force: true });
|
|
21077
|
+
},
|
|
21078
|
+
rename: (from, to) => renameSync(from, to),
|
|
21079
|
+
copyFile: (from, to) => copyFileSync(from, to),
|
|
21080
|
+
mkdir: (dir) => {
|
|
21081
|
+
mkdirSync(dir, { recursive: true });
|
|
21082
|
+
},
|
|
21083
|
+
realpath: (path) => {
|
|
21084
|
+
try {
|
|
21085
|
+
return realpathSync(path);
|
|
21086
|
+
} catch {
|
|
21087
|
+
return null;
|
|
21088
|
+
}
|
|
21089
|
+
},
|
|
21090
|
+
freeBytes: (dir) => {
|
|
21091
|
+
try {
|
|
21092
|
+
const stat = statfsSync(dir);
|
|
21093
|
+
return Number(stat.bavail) * Number(stat.bsize);
|
|
21094
|
+
} catch {
|
|
21095
|
+
return null;
|
|
21096
|
+
}
|
|
21097
|
+
},
|
|
21098
|
+
dirBytes
|
|
21099
|
+
};
|
|
21100
|
+
}
|
|
19785
21101
|
// packages/control/src/oauth/pkce.ts
|
|
19786
21102
|
function randomToken() {
|
|
19787
21103
|
return Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("base64url");
|
|
@@ -20692,8 +22008,8 @@ function createRefresher(deps) {
|
|
|
20692
22008
|
};
|
|
20693
22009
|
}
|
|
20694
22010
|
// packages/control/src/quota/history.ts
|
|
20695
|
-
var
|
|
20696
|
-
var
|
|
22011
|
+
var DAY_MS2 = 24 * 60 * 60 * 1000;
|
|
22012
|
+
var HOUR_MS3 = 3600000;
|
|
20697
22013
|
async function gatewayRatesFor(store, windows) {
|
|
20698
22014
|
const rates = [];
|
|
20699
22015
|
for (const window of windows) {
|
|
@@ -20717,7 +22033,7 @@ async function gatewayRatesFor(store, windows) {
|
|
|
20717
22033
|
rates.push({
|
|
20718
22034
|
credentialId: window.credentialId,
|
|
20719
22035
|
windowType: window.windowType,
|
|
20720
|
-
gatewayRatePerHour: tokens2 / ((window.observedAt - since) /
|
|
22036
|
+
gatewayRatePerHour: tokens2 / ((window.observedAt - since) / HOUR_MS3)
|
|
20721
22037
|
});
|
|
20722
22038
|
}
|
|
20723
22039
|
return rates;
|
|
@@ -20725,7 +22041,7 @@ async function gatewayRatesFor(store, windows) {
|
|
|
20725
22041
|
async function quotaHistory(deps, input) {
|
|
20726
22042
|
const now = deps.now();
|
|
20727
22043
|
const settings = await deps.store.config.getSettings();
|
|
20728
|
-
const oldest = now - settings.logRetentionDays *
|
|
22044
|
+
const oldest = now - settings.logRetentionDays * DAY_MS2;
|
|
20729
22045
|
const since = Math.max(optionalNumber(input.since, oldest), oldest);
|
|
20730
22046
|
const until = Math.min(optionalNumber(input.until, now), now);
|
|
20731
22047
|
const raw = input.credentialId?.trim();
|
|
@@ -20748,9 +22064,13 @@ async function getSettings(store) {
|
|
|
20748
22064
|
return store.config.getSettings();
|
|
20749
22065
|
}
|
|
20750
22066
|
async function putSettings(store, input) {
|
|
20751
|
-
const
|
|
20752
|
-
|
|
20753
|
-
|
|
22067
|
+
const { snapshotKeepLatest, snapshotMaxAgeDays, ...rest } = parseOrThrow(settingsSchema, input);
|
|
22068
|
+
const patch = {
|
|
22069
|
+
...rest,
|
|
22070
|
+
...snapshotKeepLatest === undefined ? {} : { snapshotKeepLatest },
|
|
22071
|
+
...snapshotMaxAgeDays === undefined ? {} : { snapshotMaxAgeDays }
|
|
22072
|
+
};
|
|
22073
|
+
return store.config.putSettings(patch);
|
|
20754
22074
|
}
|
|
20755
22075
|
// packages/control/src/setup.ts
|
|
20756
22076
|
var KEY_PLACEHOLDER = "<your OmniGateway key>";
|
|
@@ -20863,7 +22183,7 @@ function opencodeConfig(described, input, mapping) {
|
|
|
20863
22183
|
return { path: "opencode.json", contents };
|
|
20864
22184
|
}
|
|
20865
22185
|
// packages/control/src/tail.ts
|
|
20866
|
-
import { closeSync, fstatSync, openSync, readSync, statSync } from "fs";
|
|
22186
|
+
import { closeSync, fstatSync, openSync, readSync, statSync as statSync2 } from "fs";
|
|
20867
22187
|
var CHUNK = 64 * 1024;
|
|
20868
22188
|
var MAX_BYTES = 8 * 1024 * 1024;
|
|
20869
22189
|
function tailFile(path, lines) {
|
|
@@ -20901,7 +22221,7 @@ function tailFile(path, lines) {
|
|
|
20901
22221
|
}
|
|
20902
22222
|
function fileExists(path) {
|
|
20903
22223
|
try {
|
|
20904
|
-
return
|
|
22224
|
+
return statSync2(path).isFile();
|
|
20905
22225
|
} catch {
|
|
20906
22226
|
return false;
|
|
20907
22227
|
}
|
|
@@ -20987,7 +22307,7 @@ function requirePositional(parsed, index, name) {
|
|
|
20987
22307
|
// apps/cli/src/context.ts
|
|
20988
22308
|
import { existsSync, readFileSync } from "fs";
|
|
20989
22309
|
import { homedir } from "os";
|
|
20990
|
-
import { isAbsolute, join, resolve } from "path";
|
|
22310
|
+
import { isAbsolute, join as join4, resolve as resolve2 } from "path";
|
|
20991
22311
|
class CliError extends Error {
|
|
20992
22312
|
exitCode;
|
|
20993
22313
|
constructor(message, exitCode = 1) {
|
|
@@ -20996,21 +22316,21 @@ class CliError extends Error {
|
|
|
20996
22316
|
this.exitCode = exitCode;
|
|
20997
22317
|
}
|
|
20998
22318
|
}
|
|
20999
|
-
var DEFAULT_ROOT =
|
|
22319
|
+
var DEFAULT_ROOT = join4(homedir(), ".config", "omnigateway");
|
|
21000
22320
|
function looksLikeRoot(dir) {
|
|
21001
|
-
return existsSync(
|
|
22321
|
+
return existsSync(join4(dir, ".env")) || existsSync(join4(dir, "omnigateway.db")) || existsSync(join4(dir, "apps", "gateway", "src", "index.ts"));
|
|
21002
22322
|
}
|
|
21003
22323
|
function resolveRoot(flags, env2, cwd) {
|
|
21004
22324
|
const withEnvFile = (root, source) => {
|
|
21005
|
-
const envFile =
|
|
22325
|
+
const envFile = join4(root, ".env");
|
|
21006
22326
|
return { root, source, envFile: existsSync(envFile) ? envFile : null };
|
|
21007
22327
|
};
|
|
21008
22328
|
if (flags.root !== undefined && flags.root.length > 0) {
|
|
21009
|
-
return withEnvFile(
|
|
22329
|
+
return withEnvFile(resolve2(cwd, flags.root), "flag");
|
|
21010
22330
|
}
|
|
21011
22331
|
const fromEnv = env2.OMNI_ROOT;
|
|
21012
22332
|
if (typeof fromEnv === "string" && fromEnv.length > 0) {
|
|
21013
|
-
return withEnvFile(
|
|
22333
|
+
return withEnvFile(resolve2(cwd, fromEnv), "env");
|
|
21014
22334
|
}
|
|
21015
22335
|
if (looksLikeRoot(cwd))
|
|
21016
22336
|
return withEnvFile(cwd, "cwd");
|
|
@@ -21057,7 +22377,7 @@ function createContext(parsed, options = {}) {
|
|
|
21057
22377
|
configError = error51 instanceof Error ? error51.message : "invalid configuration";
|
|
21058
22378
|
}
|
|
21059
22379
|
const configuredPath = dbFlag ?? config2?.databasePath ?? "omnigateway.db";
|
|
21060
|
-
const databasePath = isAbsolute(configuredPath) ? configuredPath :
|
|
22380
|
+
const databasePath = isAbsolute(configuredPath) ? configuredPath : resolve2(root.root, configuredPath);
|
|
21061
22381
|
const warnings = suppressedDbPath === null ? [] : [
|
|
21062
22382
|
`ignoring OMNI_DB_PATH=${suppressedDbPath} from the environment because --root was given; using ${databasePath}`
|
|
21063
22383
|
];
|
|
@@ -21099,6 +22419,143 @@ function createContext(parsed, options = {}) {
|
|
|
21099
22419
|
};
|
|
21100
22420
|
}
|
|
21101
22421
|
|
|
22422
|
+
// apps/cli/src/command.ts
|
|
22423
|
+
var PROVIDER_TONE = {
|
|
22424
|
+
anthropic: "magenta",
|
|
22425
|
+
openai: "green",
|
|
22426
|
+
kimi: "blue",
|
|
22427
|
+
kilo: "orange",
|
|
22428
|
+
grok: "yellow",
|
|
22429
|
+
custom: "cyan"
|
|
22430
|
+
};
|
|
22431
|
+
function provider(ctx, id) {
|
|
22432
|
+
return paint(ctx, PROVIDER_TONE[id], id);
|
|
22433
|
+
}
|
|
22434
|
+
function state(ctx, ok, text) {
|
|
22435
|
+
return paint(ctx, ok ? "green" : "red", text);
|
|
22436
|
+
}
|
|
22437
|
+
|
|
22438
|
+
// apps/cli/src/commands/bodies.ts
|
|
22439
|
+
var ABSENCE = {
|
|
22440
|
+
none: [
|
|
22441
|
+
"not captured",
|
|
22442
|
+
"Body capture was not running for this request. It needs OMNI_BODY_LOGGING_ALLOWED in the environment and bodyLoggingEnabled turned on, and the calling key must not have opted out."
|
|
22443
|
+
],
|
|
22444
|
+
missing: [
|
|
22445
|
+
"captured, then lost",
|
|
22446
|
+
"This request was captured, but its artifact is no longer on disk. Retention or the row cap has since pruned it, or something removed the file underneath the gateway."
|
|
22447
|
+
],
|
|
22448
|
+
corrupt: [
|
|
22449
|
+
"captured, but unreadable",
|
|
22450
|
+
"This request was captured and the artifact is still on disk, but it failed its checksum or would not decrypt. Changing OMNI_ENCRYPTION_KEY invalidates every artifact written under the old one."
|
|
22451
|
+
]
|
|
22452
|
+
};
|
|
22453
|
+
var encoder3 = new TextEncoder;
|
|
22454
|
+
function payloadBytes(value) {
|
|
22455
|
+
if (value === null || value === undefined)
|
|
22456
|
+
return 0;
|
|
22457
|
+
return encoder3.encode(JSON.stringify(value)).length;
|
|
22458
|
+
}
|
|
22459
|
+
function frame(read) {
|
|
22460
|
+
return fields([
|
|
22461
|
+
["STATE", read.detailState],
|
|
22462
|
+
["CAPTURED", formatTime(read.at)],
|
|
22463
|
+
["SIZE", `${formatBytes(read.sizeBytes)} on disk`],
|
|
22464
|
+
["TRUNCATED", read.truncated ? "yes" : "no"]
|
|
22465
|
+
]);
|
|
22466
|
+
}
|
|
22467
|
+
function pairs(ctx, artifact) {
|
|
22468
|
+
const frames = artifact.attempts.some((attempt) => attempt.streamChunks !== null);
|
|
22469
|
+
const label2 = (text, truncated) => truncated ? `${text} (truncated)` : text;
|
|
22470
|
+
return table([
|
|
22471
|
+
{ header: "PAIR" },
|
|
22472
|
+
{ header: "PROVIDER" },
|
|
22473
|
+
{ header: "REQUEST", align: "right" },
|
|
22474
|
+
{ header: "RESPONSE", align: "right" },
|
|
22475
|
+
...frames ? [{ header: "FRAMES", align: "right" }] : [],
|
|
22476
|
+
{ header: "REQUEST IS" }
|
|
22477
|
+
], [
|
|
22478
|
+
[
|
|
22479
|
+
label2("CLIENT", artifact.client.truncated),
|
|
22480
|
+
"",
|
|
22481
|
+
formatBytes(payloadBytes(artifact.client.request)),
|
|
22482
|
+
formatBytes(payloadBytes(artifact.client.response)),
|
|
22483
|
+
...frames ? ["\u2014"] : [],
|
|
22484
|
+
"pre-RTK"
|
|
22485
|
+
],
|
|
22486
|
+
...artifact.attempts.map((attempt) => [
|
|
22487
|
+
label2(`ATTEMPT ${attempt.attempt}`, attempt.truncated),
|
|
22488
|
+
provider(ctx, attempt.provider),
|
|
22489
|
+
formatBytes(payloadBytes(attempt.request)),
|
|
22490
|
+
formatBytes(payloadBytes(attempt.response)),
|
|
22491
|
+
...frames ? [attempt.streamChunks === null ? "\u2014" : String(attempt.streamChunks.length)] : [],
|
|
22492
|
+
"post-RTK"
|
|
22493
|
+
])
|
|
22494
|
+
]);
|
|
22495
|
+
}
|
|
22496
|
+
function block(ctx, heading, value) {
|
|
22497
|
+
const body2 = value === null || value === undefined ? "(nothing recorded for this half \u2014 it never happened, or never completed)" : JSON.stringify(value, null, 2);
|
|
22498
|
+
return `${paint(ctx, "dim", heading)}
|
|
22499
|
+
${body2}`;
|
|
22500
|
+
}
|
|
22501
|
+
function full(ctx, artifact) {
|
|
22502
|
+
const sections = [
|
|
22503
|
+
block(ctx, "CLIENT REQUEST (pre-RTK)", artifact.client.request),
|
|
22504
|
+
block(ctx, "CLIENT RESPONSE", artifact.client.response)
|
|
22505
|
+
];
|
|
22506
|
+
for (const attempt of artifact.attempts) {
|
|
22507
|
+
const name = `ATTEMPT ${attempt.attempt} ${provider(ctx, attempt.provider)}`;
|
|
22508
|
+
sections.push(block(ctx, `${name} REQUEST (post-RTK)`, attempt.request));
|
|
22509
|
+
sections.push(block(ctx, `${name} RESPONSE`, attempt.response));
|
|
22510
|
+
if (attempt.streamChunks !== null) {
|
|
22511
|
+
sections.push(block(ctx, `${name} STREAM FRAMES (${attempt.streamChunks.length})`, attempt.streamChunks));
|
|
22512
|
+
}
|
|
22513
|
+
}
|
|
22514
|
+
if (artifact.error !== null && artifact.error !== undefined) {
|
|
22515
|
+
sections.push(block(ctx, "ERROR", artifact.error));
|
|
22516
|
+
}
|
|
22517
|
+
return sections.join(`
|
|
22518
|
+
|
|
22519
|
+
`);
|
|
22520
|
+
}
|
|
22521
|
+
var bodies = {
|
|
22522
|
+
usage: "bodies <request-id> [--full]",
|
|
22523
|
+
summary: "Show captured bodies for one request; withheld unless --full",
|
|
22524
|
+
options: { full: { type: "boolean" } },
|
|
22525
|
+
async run(args, { ctx, writer }) {
|
|
22526
|
+
const requestId = requirePositional(args, 0, "request id");
|
|
22527
|
+
const read = await readRequestBody(await ctx.store(), requestId);
|
|
22528
|
+
emit(ctx, writer, read, () => {
|
|
22529
|
+
const artifact = read.artifact;
|
|
22530
|
+
if (artifact === null) {
|
|
22531
|
+
const [legend, message] = ABSENCE[read.detailState === "ready" ? "corrupt" : read.detailState];
|
|
22532
|
+
return [frame(read), "", legend, message].join(`
|
|
22533
|
+
`);
|
|
22534
|
+
}
|
|
22535
|
+
const lines = [
|
|
22536
|
+
frame(read),
|
|
22537
|
+
"",
|
|
22538
|
+
pairs(ctx, artifact),
|
|
22539
|
+
"",
|
|
22540
|
+
paint(ctx, "dim", "REQUEST and RESPONSE are the stored payload, after masking and bounding; neither is the size sent over the wire")
|
|
22541
|
+
];
|
|
22542
|
+
if (artifact.attempts.length > 0) {
|
|
22543
|
+
lines.push("", paint(ctx, "dim", "the client request is pre-RTK and every attempt request is post-RTK; they are not the same payload"));
|
|
22544
|
+
}
|
|
22545
|
+
if (artifact.error !== null && artifact.error !== undefined) {
|
|
22546
|
+
lines.push("", "an error was recorded for this request");
|
|
22547
|
+
}
|
|
22548
|
+
if (boolFlag(args.values, "full")) {
|
|
22549
|
+
lines.push("", full(ctx, artifact));
|
|
22550
|
+
} else {
|
|
22551
|
+
lines.push("", "bodies withheld; pass --full to print them, --json for the artifact");
|
|
22552
|
+
}
|
|
22553
|
+
return lines.join(`
|
|
22554
|
+
`);
|
|
22555
|
+
});
|
|
22556
|
+
}
|
|
22557
|
+
};
|
|
22558
|
+
|
|
21102
22559
|
// apps/cli/src/commands/connect.ts
|
|
21103
22560
|
var DEVICE_TIMEOUT_MS = 600000;
|
|
21104
22561
|
var connect = {
|
|
@@ -21146,21 +22603,21 @@ async function pollUntilAuthorized(flows, start, deps) {
|
|
|
21146
22603
|
}
|
|
21147
22604
|
|
|
21148
22605
|
// apps/cli/src/service.ts
|
|
21149
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
|
|
22606
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync } from "fs";
|
|
21150
22607
|
import { homedir as homedir2 } from "os";
|
|
21151
|
-
import { dirname, join as
|
|
22608
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
21152
22609
|
var UNIT_NAME2 = "omnigateway.service";
|
|
21153
22610
|
function unitPath(scope) {
|
|
21154
|
-
return scope === "system" ?
|
|
22611
|
+
return scope === "system" ? join5("/etc/systemd/system", UNIT_NAME2) : join5(homedir2(), ".config", "systemd", "user", UNIT_NAME2);
|
|
21155
22612
|
}
|
|
21156
22613
|
function systemctlArgs(scope, args) {
|
|
21157
22614
|
return scope === "system" ? ["systemctl", ...args] : ["systemctl", "--user", ...args];
|
|
21158
22615
|
}
|
|
21159
22616
|
function pidFile(stateDir) {
|
|
21160
|
-
return
|
|
22617
|
+
return join5(stateDir, "gateway.pid");
|
|
21161
22618
|
}
|
|
21162
22619
|
function logFile(stateDir) {
|
|
21163
|
-
return
|
|
22620
|
+
return join5(stateDir, "gateway.log");
|
|
21164
22621
|
}
|
|
21165
22622
|
function supervisedLogFile(deps) {
|
|
21166
22623
|
const configured = deps.logFile?.trim();
|
|
@@ -21168,8 +22625,8 @@ function supervisedLogFile(deps) {
|
|
|
21168
22625
|
}
|
|
21169
22626
|
function defaultStateDir(env2) {
|
|
21170
22627
|
const xdg = env2.XDG_STATE_HOME;
|
|
21171
|
-
const base = typeof xdg === "string" && xdg.length > 0 ? xdg :
|
|
21172
|
-
return
|
|
22628
|
+
const base = typeof xdg === "string" && xdg.length > 0 ? xdg : join5(homedir2(), ".local", "state");
|
|
22629
|
+
return join5(base, "omnigateway");
|
|
21173
22630
|
}
|
|
21174
22631
|
function unitFile(input) {
|
|
21175
22632
|
return `[Unit]
|
|
@@ -21180,7 +22637,7 @@ Wants=network-online.target
|
|
|
21180
22637
|
[Service]
|
|
21181
22638
|
Type=simple
|
|
21182
22639
|
WorkingDirectory=${input.root}
|
|
21183
|
-
EnvironmentFile=${
|
|
22640
|
+
EnvironmentFile=${join5(input.root, ".env")}
|
|
21184
22641
|
ExecStart=${input.bun} ${input.entrypoint}
|
|
21185
22642
|
Restart=on-failure
|
|
21186
22643
|
RestartSec=2
|
|
@@ -21205,20 +22662,20 @@ function livePid(deps) {
|
|
|
21205
22662
|
return null;
|
|
21206
22663
|
if (deps.alive(pid))
|
|
21207
22664
|
return pid;
|
|
21208
|
-
|
|
22665
|
+
rmSync2(pidFile(deps.stateDir), { force: true });
|
|
21209
22666
|
return null;
|
|
21210
22667
|
}
|
|
21211
22668
|
async function status(deps) {
|
|
21212
22669
|
if (unitInstalled(deps)) {
|
|
21213
22670
|
const active = await deps.run(systemctlArgs(deps.scope, ["is-active", UNIT_NAME2]));
|
|
21214
|
-
const
|
|
22671
|
+
const state2 = active.stdout.trim() || active.stderr.trim() || "unknown";
|
|
21215
22672
|
const shown = await deps.run(systemctlArgs(deps.scope, ["show", UNIT_NAME2, "--property=MainPID", "--value"]));
|
|
21216
22673
|
const pid2 = Number(shown.stdout.trim());
|
|
21217
22674
|
return {
|
|
21218
22675
|
supervisor: "systemd",
|
|
21219
|
-
running:
|
|
22676
|
+
running: state2 === "active",
|
|
21220
22677
|
pid: Number.isInteger(pid2) && pid2 > 0 ? pid2 : null,
|
|
21221
|
-
state,
|
|
22678
|
+
state: state2,
|
|
21222
22679
|
unitPath: deps.unitPath,
|
|
21223
22680
|
logFile: null
|
|
21224
22681
|
};
|
|
@@ -21253,9 +22710,9 @@ async function start(deps, input) {
|
|
|
21253
22710
|
healthy: await waitForHealth(deps, input.baseUrl)
|
|
21254
22711
|
};
|
|
21255
22712
|
}
|
|
21256
|
-
|
|
22713
|
+
mkdirSync2(deps.stateDir, { recursive: true });
|
|
21257
22714
|
const file2 = supervisedLogFile(deps);
|
|
21258
|
-
|
|
22715
|
+
mkdirSync2(dirname3(file2), { recursive: true });
|
|
21259
22716
|
const pid = deps.spawn({
|
|
21260
22717
|
argv: input.argv,
|
|
21261
22718
|
cwd: deps.root,
|
|
@@ -21299,7 +22756,7 @@ async function stop(deps) {
|
|
|
21299
22756
|
}
|
|
21300
22757
|
await deps.sleep(STOP_INTERVAL_MS);
|
|
21301
22758
|
}
|
|
21302
|
-
|
|
22759
|
+
rmSync2(pidFile(deps.stateDir), { force: true });
|
|
21303
22760
|
return { supervisor: "pidfile", stopped: true };
|
|
21304
22761
|
}
|
|
21305
22762
|
async function install(deps, input) {
|
|
@@ -21307,7 +22764,7 @@ async function install(deps, input) {
|
|
|
21307
22764
|
if (existsSync2(path) && !input.force) {
|
|
21308
22765
|
throw new Error(`${path} already exists; pass --force to replace it`);
|
|
21309
22766
|
}
|
|
21310
|
-
|
|
22767
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
21311
22768
|
writeFileSync(path, unitFile({ root: deps.root, bun: input.bun, entrypoint: input.entrypoint }));
|
|
21312
22769
|
const reload = await deps.run(systemctlArgs(deps.scope, ["daemon-reload"]));
|
|
21313
22770
|
let enabled = false;
|
|
@@ -21322,7 +22779,7 @@ async function uninstall(deps) {
|
|
|
21322
22779
|
if (!existsSync2(path))
|
|
21323
22780
|
return { path, removed: false };
|
|
21324
22781
|
await deps.run(systemctlArgs(deps.scope, ["disable", "--now", UNIT_NAME2]));
|
|
21325
|
-
|
|
22782
|
+
rmSync2(path, { force: true });
|
|
21326
22783
|
await deps.run(systemctlArgs(deps.scope, ["daemon-reload"]));
|
|
21327
22784
|
return { path, removed: true };
|
|
21328
22785
|
}
|
|
@@ -21417,22 +22874,6 @@ var console_ = {
|
|
|
21417
22874
|
}
|
|
21418
22875
|
};
|
|
21419
22876
|
|
|
21420
|
-
// apps/cli/src/command.ts
|
|
21421
|
-
var PROVIDER_TONE = {
|
|
21422
|
-
anthropic: "magenta",
|
|
21423
|
-
openai: "green",
|
|
21424
|
-
kimi: "blue",
|
|
21425
|
-
kilo: "orange",
|
|
21426
|
-
grok: "yellow",
|
|
21427
|
-
custom: "cyan"
|
|
21428
|
-
};
|
|
21429
|
-
function provider(ctx, id) {
|
|
21430
|
-
return paint(ctx, PROVIDER_TONE[id], id);
|
|
21431
|
-
}
|
|
21432
|
-
function state(ctx, ok, text) {
|
|
21433
|
-
return paint(ctx, ok ? "green" : "red", text);
|
|
21434
|
-
}
|
|
21435
|
-
|
|
21436
22877
|
// apps/cli/src/commands/credentials.ts
|
|
21437
22878
|
function condition(credential) {
|
|
21438
22879
|
if (!credential.enabled)
|
|
@@ -21658,6 +23099,9 @@ var credentialsHealth = {
|
|
|
21658
23099
|
|
|
21659
23100
|
// apps/cli/src/commands/db.ts
|
|
21660
23101
|
import { existsSync as existsSync3 } from "fs";
|
|
23102
|
+
async function database(env2) {
|
|
23103
|
+
return { store: await env2.ctx.store(), fs: nodeDatabaseFs(), now: env2.ctx.now };
|
|
23104
|
+
}
|
|
21661
23105
|
var dbMigrate = {
|
|
21662
23106
|
usage: "db migrate",
|
|
21663
23107
|
summary: "Create or upgrade the database schema",
|
|
@@ -21671,8 +23115,234 @@ var dbMigrate = {
|
|
|
21671
23115
|
]));
|
|
21672
23116
|
}
|
|
21673
23117
|
};
|
|
23118
|
+
var dbStats = {
|
|
23119
|
+
usage: "db stats",
|
|
23120
|
+
summary: "Show database size, free pages, and what snapshots are held",
|
|
23121
|
+
async run(_args, env2) {
|
|
23122
|
+
const { ctx, writer } = env2;
|
|
23123
|
+
const overview = await getDatabaseOverview(await database(env2));
|
|
23124
|
+
emit(ctx, writer, overview, () => fields([
|
|
23125
|
+
["database", ctx.databasePath],
|
|
23126
|
+
["size", formatBytes(overview.fileBytes)],
|
|
23127
|
+
["write-ahead log", formatBytes(overview.walBytes)],
|
|
23128
|
+
["captured bodies", `${formatBytes(overview.bodiesBytes)} (never snapshotted)`],
|
|
23129
|
+
["free pages", `${formatBytes(overview.freePageBytes)} \u2014 reclaimed by omni db vacuum`],
|
|
23130
|
+
["schema version", String(overview.stats.schemaVersion)],
|
|
23131
|
+
[
|
|
23132
|
+
"snapshots",
|
|
23133
|
+
`${overview.snapshots.count} (${formatBytes(overview.snapshots.totalBytes)}), latest ${formatTime(overview.snapshots.latestAt)}`
|
|
23134
|
+
],
|
|
23135
|
+
[
|
|
23136
|
+
"retention",
|
|
23137
|
+
`keep ${overview.retention.keepLatest}, up to ${overview.retention.maxAgeDays} days`
|
|
23138
|
+
],
|
|
23139
|
+
[
|
|
23140
|
+
"free disk",
|
|
23141
|
+
overview.freeDiskBytes === null ? "unknown" : formatBytes(overview.freeDiskBytes)
|
|
23142
|
+
]
|
|
23143
|
+
]));
|
|
23144
|
+
}
|
|
23145
|
+
};
|
|
23146
|
+
var dbSnapshots = {
|
|
23147
|
+
usage: "db snapshots",
|
|
23148
|
+
summary: "List the snapshots held for this installation",
|
|
23149
|
+
async run(_args, env2) {
|
|
23150
|
+
const { ctx, writer } = env2;
|
|
23151
|
+
const deps = await database(env2);
|
|
23152
|
+
const snapshots = listSnapshots(deps);
|
|
23153
|
+
emit(ctx, writer, { snapshots }, () => {
|
|
23154
|
+
if (snapshots.length === 0) {
|
|
23155
|
+
return `no snapshots in ${snapshotsDir(deps)}; take one with omni db backup`;
|
|
23156
|
+
}
|
|
23157
|
+
return table([
|
|
23158
|
+
{ header: "ID" },
|
|
23159
|
+
{ header: "TAKEN" },
|
|
23160
|
+
{ header: "SIZE", align: "right" },
|
|
23161
|
+
{ header: "REASON" }
|
|
23162
|
+
], snapshots.map((snapshot) => [
|
|
23163
|
+
snapshot.id,
|
|
23164
|
+
formatTime(snapshot.createdAt),
|
|
23165
|
+
formatBytes(snapshot.sizeBytes),
|
|
23166
|
+
snapshot.reason
|
|
23167
|
+
]));
|
|
23168
|
+
});
|
|
23169
|
+
}
|
|
23170
|
+
};
|
|
23171
|
+
var dbBackup = {
|
|
23172
|
+
usage: "db backup",
|
|
23173
|
+
summary: "Take a snapshot of the database, pruning by the retention policy",
|
|
23174
|
+
async run(_args, env2) {
|
|
23175
|
+
const { ctx, writer } = env2;
|
|
23176
|
+
const snapshot = await createSnapshot(await database(env2), { reason: "manual" });
|
|
23177
|
+
emit(ctx, writer, snapshot, () => {
|
|
23178
|
+
note(ctx, writer, "a snapshot carries encrypted credentials and key hashes; captured bodies are not in it");
|
|
23179
|
+
return fields([
|
|
23180
|
+
["snapshot", snapshot.id],
|
|
23181
|
+
["size", formatBytes(snapshot.sizeBytes)],
|
|
23182
|
+
["taken", formatTime(snapshot.createdAt)]
|
|
23183
|
+
]);
|
|
23184
|
+
});
|
|
23185
|
+
}
|
|
23186
|
+
};
|
|
23187
|
+
var dbVacuum = {
|
|
23188
|
+
usage: "db vacuum",
|
|
23189
|
+
summary: "Rewrite the database, reclaiming the pages deletion left free",
|
|
23190
|
+
async run(_args, env2) {
|
|
23191
|
+
const { ctx, writer } = env2;
|
|
23192
|
+
note(ctx, writer, "compacting; the gateway's writes will block until this finishes\u2026");
|
|
23193
|
+
const result = await vacuum(await database(env2));
|
|
23194
|
+
emit(ctx, writer, result, () => fields([
|
|
23195
|
+
["reclaimed", formatBytes(result.reclaimedBytes)],
|
|
23196
|
+
["took", `${result.durationMs} ms`]
|
|
23197
|
+
]));
|
|
23198
|
+
}
|
|
23199
|
+
};
|
|
23200
|
+
var dbRestore = {
|
|
23201
|
+
usage: "db restore <id>",
|
|
23202
|
+
summary: "Replace the database with a snapshot, keeping a copy of what was there",
|
|
23203
|
+
async run(args, env2) {
|
|
23204
|
+
const { ctx, writer, prompt } = env2;
|
|
23205
|
+
const id = requirePositional(args, 0, "snapshot id");
|
|
23206
|
+
const running = await status(env2.service());
|
|
23207
|
+
if (running.running) {
|
|
23208
|
+
const who = running.pid === null ? running.supervisor : `${running.supervisor}, pid ${running.pid}`;
|
|
23209
|
+
throw new CliError(`a gateway is running (${who}) against this installation; run omni stop first, ` + "or restore from the dashboard, which swaps the file behind its own quiesce latch");
|
|
23210
|
+
}
|
|
23211
|
+
if (!await prompt.confirm(`replace ${ctx.databasePath} with ${id}?`)) {
|
|
23212
|
+
throw new CliError("cancelled");
|
|
23213
|
+
}
|
|
23214
|
+
const result = await restoreSnapshot(await database(env2), id);
|
|
23215
|
+
emit(ctx, writer, result, () => fields([
|
|
23216
|
+
["restored", id],
|
|
23217
|
+
["undo", result.preRestoreSnapshot.id],
|
|
23218
|
+
...Object.entries(result.counts).map(([table2, count]) => [table2, String(count)])
|
|
23219
|
+
]));
|
|
23220
|
+
}
|
|
23221
|
+
};
|
|
21674
23222
|
|
|
21675
23223
|
// apps/cli/src/commands/keys.ts
|
|
23224
|
+
var WINDOWED = new Set(["requests", "tokens", "spend"]);
|
|
23225
|
+
var PROTO = "__proto__";
|
|
23226
|
+
function rejectProto(flag, name) {
|
|
23227
|
+
if (name === PROTO)
|
|
23228
|
+
throw new UsageError(`${flag} cannot name "${PROTO}"`);
|
|
23229
|
+
}
|
|
23230
|
+
function parseLimitFlags(entries, flag, into = {}) {
|
|
23231
|
+
const limits = into;
|
|
23232
|
+
for (const entry of entries) {
|
|
23233
|
+
const equals = entry.indexOf("=");
|
|
23234
|
+
if (equals <= 0) {
|
|
23235
|
+
throw new UsageError(`${flag} must be dimension:window=value, got "${entry}"`);
|
|
23236
|
+
}
|
|
23237
|
+
const pair = entry.slice(0, equals);
|
|
23238
|
+
const raw = entry.slice(equals + 1);
|
|
23239
|
+
const value = Number(raw);
|
|
23240
|
+
if (raw.trim().length === 0 || !Number.isFinite(value)) {
|
|
23241
|
+
throw new UsageError(`${flag} ${pair} must be a number, got "${raw}"`);
|
|
23242
|
+
}
|
|
23243
|
+
const colon = pair.indexOf(":");
|
|
23244
|
+
if (colon < 0) {
|
|
23245
|
+
if (WINDOWED.has(pair)) {
|
|
23246
|
+
throw new UsageError(`${flag} ${pair} needs a window, e.g. ${flag} ${pair}:1m=60`);
|
|
23247
|
+
}
|
|
23248
|
+
rejectProto(flag, pair);
|
|
23249
|
+
limits[pair] = value;
|
|
23250
|
+
continue;
|
|
23251
|
+
}
|
|
23252
|
+
const dimension = pair.slice(0, colon);
|
|
23253
|
+
const window = pair.slice(colon + 1);
|
|
23254
|
+
if (dimension.length === 0 || window.length === 0) {
|
|
23255
|
+
throw new UsageError(`${flag} must be dimension:window=value, got "${entry}"`);
|
|
23256
|
+
}
|
|
23257
|
+
rejectProto(flag, dimension);
|
|
23258
|
+
rejectProto(flag, window);
|
|
23259
|
+
const existing = limits[dimension];
|
|
23260
|
+
const windows = typeof existing === "object" ? existing : {};
|
|
23261
|
+
windows[window] = value;
|
|
23262
|
+
limits[dimension] = windows;
|
|
23263
|
+
}
|
|
23264
|
+
return limits;
|
|
23265
|
+
}
|
|
23266
|
+
function toLoose(limits) {
|
|
23267
|
+
const loose = {};
|
|
23268
|
+
for (const dimension of ["requests", "tokens", "spend"]) {
|
|
23269
|
+
const windows = limits[dimension];
|
|
23270
|
+
if (windows === undefined)
|
|
23271
|
+
continue;
|
|
23272
|
+
const kept = {};
|
|
23273
|
+
for (const [window, value] of Object.entries(windows)) {
|
|
23274
|
+
if (value === null || value === undefined)
|
|
23275
|
+
continue;
|
|
23276
|
+
kept[window] = value;
|
|
23277
|
+
}
|
|
23278
|
+
if (Object.keys(kept).length > 0)
|
|
23279
|
+
loose[dimension] = kept;
|
|
23280
|
+
}
|
|
23281
|
+
if (limits.concurrency !== undefined && limits.concurrency !== null) {
|
|
23282
|
+
loose.concurrency = limits.concurrency;
|
|
23283
|
+
}
|
|
23284
|
+
return loose;
|
|
23285
|
+
}
|
|
23286
|
+
function applyUnsetFlags(limits, entries) {
|
|
23287
|
+
for (const entry of entries) {
|
|
23288
|
+
if (entry.includes("=")) {
|
|
23289
|
+
throw new UsageError(`--unset names a limit to remove, not a value, got "${entry}"`);
|
|
23290
|
+
}
|
|
23291
|
+
const colon = entry.indexOf(":");
|
|
23292
|
+
if (colon < 0) {
|
|
23293
|
+
if (WINDOWED.has(entry)) {
|
|
23294
|
+
throw new UsageError(`--unset ${entry} needs a window, e.g. --unset ${entry}:1m`);
|
|
23295
|
+
}
|
|
23296
|
+
rejectProto("--unset", entry);
|
|
23297
|
+
if (limits[entry] === undefined)
|
|
23298
|
+
throw new UsageError(`this key has no ${entry} limit`);
|
|
23299
|
+
delete limits[entry];
|
|
23300
|
+
continue;
|
|
23301
|
+
}
|
|
23302
|
+
const dimension = entry.slice(0, colon);
|
|
23303
|
+
const window = entry.slice(colon + 1);
|
|
23304
|
+
if (dimension.length === 0 || window.length === 0) {
|
|
23305
|
+
throw new UsageError(`--unset must be dimension:window, got "${entry}"`);
|
|
23306
|
+
}
|
|
23307
|
+
rejectProto("--unset", dimension);
|
|
23308
|
+
rejectProto("--unset", window);
|
|
23309
|
+
const windows = limits[dimension];
|
|
23310
|
+
if (typeof windows !== "object" || windows[window] === undefined) {
|
|
23311
|
+
throw new UsageError(`this key has no ${entry} limit`);
|
|
23312
|
+
}
|
|
23313
|
+
delete windows[window];
|
|
23314
|
+
if (Object.keys(windows).length === 0)
|
|
23315
|
+
delete limits[dimension];
|
|
23316
|
+
}
|
|
23317
|
+
}
|
|
23318
|
+
function limitRow(reading) {
|
|
23319
|
+
const money = reading.dimension === "spend";
|
|
23320
|
+
const amount = (value) => money ? formatUsd(value) : String(value);
|
|
23321
|
+
return [
|
|
23322
|
+
reading.dimension,
|
|
23323
|
+
reading.window ?? "\u2014",
|
|
23324
|
+
amount(reading.limit),
|
|
23325
|
+
reading.used === null ? "\u2014" : amount(reading.used),
|
|
23326
|
+
reading.used === null ? "\u2014" : `${Math.round(reading.used / reading.limit * 100)}%`
|
|
23327
|
+
];
|
|
23328
|
+
}
|
|
23329
|
+
function summarizeLimits(limits) {
|
|
23330
|
+
const parts = [];
|
|
23331
|
+
for (const dimension of ["requests", "tokens", "spend"]) {
|
|
23332
|
+
const windows = limits[dimension];
|
|
23333
|
+
if (windows === undefined)
|
|
23334
|
+
continue;
|
|
23335
|
+
for (const [window, value] of Object.entries(windows)) {
|
|
23336
|
+
if (value === null || value === undefined)
|
|
23337
|
+
continue;
|
|
23338
|
+
parts.push(`${dimension}:${window}=${value}`);
|
|
23339
|
+
}
|
|
23340
|
+
}
|
|
23341
|
+
if (limits.concurrency !== undefined && limits.concurrency !== null) {
|
|
23342
|
+
parts.push(`concurrency=${limits.concurrency}`);
|
|
23343
|
+
}
|
|
23344
|
+
return parts.length === 0 ? "\u2014" : parts.join(" ");
|
|
23345
|
+
}
|
|
21676
23346
|
var keysList = {
|
|
21677
23347
|
usage: "keys list",
|
|
21678
23348
|
summary: "List gateway API keys",
|
|
@@ -21686,7 +23356,8 @@ var keysList = {
|
|
|
21686
23356
|
{ header: "LABEL" },
|
|
21687
23357
|
{ header: "PREFIX" },
|
|
21688
23358
|
{ header: "MODELS" },
|
|
21689
|
-
{ header: "
|
|
23359
|
+
{ header: "LIMITS" },
|
|
23360
|
+
{ header: "BODY CAPTURE" },
|
|
21690
23361
|
{ header: "STATE" },
|
|
21691
23362
|
{ header: "CREATED" }
|
|
21692
23363
|
], keys.map((key) => [
|
|
@@ -21694,7 +23365,8 @@ var keysList = {
|
|
|
21694
23365
|
key.label,
|
|
21695
23366
|
key.prefix,
|
|
21696
23367
|
key.modelAllowlist === null ? "all" : key.modelAllowlist.length === 0 ? "none" : key.modelAllowlist.join(","),
|
|
21697
|
-
key.
|
|
23368
|
+
key.limits === null ? paint(ctx, "red", "unreadable") : summarizeLimits(key.limits),
|
|
23369
|
+
key.bodyLoggingOptOut ? "no bodies" : "\u2014",
|
|
21698
23370
|
state(ctx, key.revokedAt === null, key.revokedAt === null ? "active" : "revoked"),
|
|
21699
23371
|
formatTime(key.createdAt)
|
|
21700
23372
|
]));
|
|
@@ -21702,19 +23374,21 @@ var keysList = {
|
|
|
21702
23374
|
}
|
|
21703
23375
|
};
|
|
21704
23376
|
var keysCreate = {
|
|
21705
|
-
usage: "keys create [--label L] [--allow <model> ...] [--
|
|
23377
|
+
usage: "keys create [--label L] [--allow <model> ...] [--limit <d>:<w>=N ...] [--no-bodies]",
|
|
21706
23378
|
summary: "Mint a gateway API key, printed once",
|
|
21707
23379
|
options: {
|
|
21708
23380
|
label: { type: "string" },
|
|
21709
23381
|
allow: { type: "string", multiple: true },
|
|
21710
|
-
|
|
23382
|
+
limit: { type: "string", multiple: true },
|
|
23383
|
+
"no-bodies": { type: "boolean" }
|
|
21711
23384
|
},
|
|
21712
23385
|
async run(args, { ctx, writer }) {
|
|
21713
23386
|
const allow = listFlag(args.values, "allow");
|
|
21714
23387
|
const created = await createKey(await ctx.store(), {
|
|
21715
23388
|
...stringFlag(args.values, "label") === undefined ? {} : { label: stringFlag(args.values, "label") },
|
|
21716
23389
|
modelAllowlist: allow ?? null,
|
|
21717
|
-
|
|
23390
|
+
limits: parseLimitFlags(listFlag(args.values, "limit") ?? [], "--limit"),
|
|
23391
|
+
bodyLoggingOptOut: boolFlag(args.values, "no-bodies")
|
|
21718
23392
|
});
|
|
21719
23393
|
emit(ctx, writer, created, () => {
|
|
21720
23394
|
note(ctx, writer, paint(ctx, "yellow", "this key is shown once and stored only as a hash"));
|
|
@@ -21722,6 +23396,63 @@ var keysCreate = {
|
|
|
21722
23396
|
});
|
|
21723
23397
|
}
|
|
21724
23398
|
};
|
|
23399
|
+
var keysLimits = {
|
|
23400
|
+
usage: "keys limits <id> [--set <d>:<w>=N ...] [--unset <d>:<w> ...]",
|
|
23401
|
+
summary: "Show or edit one key's limits, with what has been used against them",
|
|
23402
|
+
options: {
|
|
23403
|
+
set: { type: "string", multiple: true },
|
|
23404
|
+
unset: { type: "string", multiple: true }
|
|
23405
|
+
},
|
|
23406
|
+
async run(args, { ctx, writer }) {
|
|
23407
|
+
const id = requirePositional(args, 0, "key id");
|
|
23408
|
+
const sets = listFlag(args.values, "set") ?? [];
|
|
23409
|
+
const unsets = listFlag(args.values, "unset") ?? [];
|
|
23410
|
+
const store = await ctx.store();
|
|
23411
|
+
const existing = (await listKeys(store)).find((entry) => entry.id === id);
|
|
23412
|
+
if (existing === undefined)
|
|
23413
|
+
throw new CliError(`no api key "${id}"`);
|
|
23414
|
+
let key = existing;
|
|
23415
|
+
if (sets.length > 0 || unsets.length > 0) {
|
|
23416
|
+
if (existing.limits === null) {
|
|
23417
|
+
if (unsets.length > 0) {
|
|
23418
|
+
throw new CliError(`the stored limits for "${id}" cannot be read, so there is nothing to unset; ` + "replace them with --set instead");
|
|
23419
|
+
}
|
|
23420
|
+
note(ctx, writer, paint(ctx, "yellow", "the stored limits could not be read and are being replaced"));
|
|
23421
|
+
}
|
|
23422
|
+
const next = existing.limits === null ? {} : toLoose(existing.limits);
|
|
23423
|
+
applyUnsetFlags(next, unsets);
|
|
23424
|
+
parseLimitFlags(sets, "--set", next);
|
|
23425
|
+
key = await setKeyLimits(store, id, { limits: next });
|
|
23426
|
+
}
|
|
23427
|
+
emit(ctx, writer, key, () => {
|
|
23428
|
+
const head = fields([
|
|
23429
|
+
["id", key.id],
|
|
23430
|
+
["label", key.label],
|
|
23431
|
+
["prefix", `${key.prefix}\u2026`]
|
|
23432
|
+
]);
|
|
23433
|
+
if (key.limits === null) {
|
|
23434
|
+
return `${head}
|
|
23435
|
+
|
|
23436
|
+
${paint(ctx, "red", "limits unreadable")}: this key is refused at /v1 until they are replaced with --set`;
|
|
23437
|
+
}
|
|
23438
|
+
if (key.limitUsage.length === 0) {
|
|
23439
|
+
return `${head}
|
|
23440
|
+
|
|
23441
|
+
no limits configured; this key is unlimited`;
|
|
23442
|
+
}
|
|
23443
|
+
return `${head}
|
|
23444
|
+
|
|
23445
|
+
${table([
|
|
23446
|
+
{ header: "DIMENSION" },
|
|
23447
|
+
{ header: "WINDOW" },
|
|
23448
|
+
{ header: "LIMIT", align: "right" },
|
|
23449
|
+
{ header: "USED", align: "right" },
|
|
23450
|
+
{ header: "USE%", align: "right" }
|
|
23451
|
+
], key.limitUsage.map(limitRow))}
|
|
23452
|
+
${paint(ctx, "dim", "usage is counted from completed requests still inside each window")}`;
|
|
23453
|
+
});
|
|
23454
|
+
}
|
|
23455
|
+
};
|
|
21725
23456
|
var keysRevoke = {
|
|
21726
23457
|
usage: "keys revoke <id>",
|
|
21727
23458
|
summary: "Revoke a gateway API key, keeping its usage history",
|
|
@@ -22089,7 +23820,7 @@ import { existsSync as existsSync5 } from "fs";
|
|
|
22089
23820
|
|
|
22090
23821
|
// apps/cli/src/runtime.ts
|
|
22091
23822
|
import { closeSync as closeSync2, existsSync as existsSync4, openSync as openSync2 } from "fs";
|
|
22092
|
-
import { join as
|
|
23823
|
+
import { join as join6 } from "path";
|
|
22093
23824
|
var runCommand = async (argv) => {
|
|
22094
23825
|
const proc = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe" });
|
|
22095
23826
|
const [stdout, stderr, code] = await Promise.all([
|
|
@@ -22146,9 +23877,9 @@ function processAlive(pid) {
|
|
|
22146
23877
|
}
|
|
22147
23878
|
function gatewayEntrypoint(root, cliDir = import.meta.dir) {
|
|
22148
23879
|
const candidates = [
|
|
22149
|
-
|
|
22150
|
-
|
|
22151
|
-
|
|
23880
|
+
join6(root, "apps", "gateway", "src", "index.ts"),
|
|
23881
|
+
join6(cliDir, "..", "gateway.js"),
|
|
23882
|
+
join6(cliDir, "gateway.js")
|
|
22152
23883
|
];
|
|
22153
23884
|
return candidates.find((path) => existsSync4(path)) ?? null;
|
|
22154
23885
|
}
|
|
@@ -22275,6 +24006,16 @@ var serviceUninstall = {
|
|
|
22275
24006
|
emit(ctx, writer, result, () => result.removed ? `removed ${result.path}` : `no unit at ${result.path}`);
|
|
22276
24007
|
}
|
|
22277
24008
|
};
|
|
24009
|
+
async function rollupState(ctx) {
|
|
24010
|
+
if (ctx.configError !== null || !existsSync5(ctx.databasePath))
|
|
24011
|
+
return null;
|
|
24012
|
+
try {
|
|
24013
|
+
const audit = await (await ctx.store()).usage.auditRollup();
|
|
24014
|
+
return audit.ok ? `ok (${audit.buckets} hourly buckets)` : `${audit.mismatched} of ${audit.buckets} hourly buckets disagree with request_logs`;
|
|
24015
|
+
} catch {
|
|
24016
|
+
return null;
|
|
24017
|
+
}
|
|
24018
|
+
}
|
|
22278
24019
|
var doctor = {
|
|
22279
24020
|
usage: "doctor",
|
|
22280
24021
|
summary: "Check what this CLI resolved, and whether it can do anything with it",
|
|
@@ -22284,6 +24025,7 @@ var doctor = {
|
|
|
22284
24025
|
const key = ctx.env.OMNI_ENCRYPTION_KEY;
|
|
22285
24026
|
const unit = unitInstalled(deps);
|
|
22286
24027
|
const status2 = await status(deps);
|
|
24028
|
+
const usageRollup = await rollupState(ctx);
|
|
22287
24029
|
const checks3 = {
|
|
22288
24030
|
root: deps.root,
|
|
22289
24031
|
rootSource: ctx.root.source,
|
|
@@ -22293,6 +24035,7 @@ var doctor = {
|
|
|
22293
24035
|
encryptionKey: typeof key === "string" ? `present (${key.length} chars)` : "missing",
|
|
22294
24036
|
configError: ctx.configError,
|
|
22295
24037
|
gatewayEntrypoint: gatewayEntrypoint(deps.root),
|
|
24038
|
+
usageRollup,
|
|
22296
24039
|
unitInstalled: unit,
|
|
22297
24040
|
supervisor: status2.supervisor,
|
|
22298
24041
|
running: status2.running,
|
|
@@ -22311,6 +24054,10 @@ var doctor = {
|
|
|
22311
24054
|
["encryption key", checks3.encryptionKey],
|
|
22312
24055
|
["config", checks3.configError === null ? ok(true, "ok") : ok(false, checks3.configError)],
|
|
22313
24056
|
["entrypoint", checks3.gatewayEntrypoint ?? ok(false, "not found")],
|
|
24057
|
+
[
|
|
24058
|
+
"usage rollup",
|
|
24059
|
+
checks3.usageRollup === null ? paint(ctx, "dim", "not checked") : ok(checks3.usageRollup.startsWith("ok"), checks3.usageRollup)
|
|
24060
|
+
],
|
|
22314
24061
|
["systemd unit", unit ? deps.scope : paint(ctx, "dim", "none")],
|
|
22315
24062
|
["gateway", ok(status2.running, status2.running ? "running" : "stopped")],
|
|
22316
24063
|
["console log", sourceHint(checks3.consoleSource)],
|
|
@@ -22342,31 +24089,59 @@ var settingsGet = {
|
|
|
22342
24089
|
emit(ctx, writer, { settings }, () => fields(flatten(settings)));
|
|
22343
24090
|
}
|
|
22344
24091
|
};
|
|
24092
|
+
function has(value, key) {
|
|
24093
|
+
return Object.hasOwn(value, key);
|
|
24094
|
+
}
|
|
24095
|
+
function currentValue(settings, head, tail) {
|
|
24096
|
+
if (!has(settings, head))
|
|
24097
|
+
return;
|
|
24098
|
+
const top = settings[head];
|
|
24099
|
+
if (tail === undefined)
|
|
24100
|
+
return top;
|
|
24101
|
+
if (top === null || typeof top !== "object")
|
|
24102
|
+
return;
|
|
24103
|
+
if (!has(top, tail))
|
|
24104
|
+
return;
|
|
24105
|
+
return top[tail];
|
|
24106
|
+
}
|
|
24107
|
+
function asNumber(path, raw) {
|
|
24108
|
+
const value = raw.trim().length === 0 ? Number.NaN : Number(raw);
|
|
24109
|
+
if (!Number.isFinite(value))
|
|
24110
|
+
throw new UsageError(`${path} must be a number, got "${raw}"`);
|
|
24111
|
+
return value;
|
|
24112
|
+
}
|
|
24113
|
+
function asBoolean(path, raw) {
|
|
24114
|
+
const value = raw.trim().toLowerCase();
|
|
24115
|
+
if (value === "true")
|
|
24116
|
+
return true;
|
|
24117
|
+
if (value === "false")
|
|
24118
|
+
return false;
|
|
24119
|
+
throw new UsageError(`${path} must be true or false, got "${raw}"`);
|
|
24120
|
+
}
|
|
22345
24121
|
var settingsSet = {
|
|
22346
24122
|
usage: "settings set <path> <value>",
|
|
22347
|
-
summary: "Change one setting, e.g. weights.cost 0.4",
|
|
24123
|
+
summary: "Change one setting, e.g. weights.cost 0.4 or rtkEnabled true",
|
|
22348
24124
|
async run(args, { ctx, writer }) {
|
|
22349
24125
|
const path = requirePositional(args, 0, "setting path");
|
|
22350
24126
|
const raw = requirePositional(args, 1, "value");
|
|
22351
24127
|
const store = await ctx.store();
|
|
22352
24128
|
const current = await getSettings(store);
|
|
22353
|
-
const value = raw.trim().length === 0 ? Number.NaN : Number(raw);
|
|
22354
|
-
if (!Number.isFinite(value))
|
|
22355
|
-
throw new UsageError(`${path} must be a number, got "${raw}"`);
|
|
22356
24129
|
const [head, tail] = path.split(".");
|
|
22357
|
-
if (head === undefined || !(head
|
|
24130
|
+
if (head === undefined || !has(current, head))
|
|
22358
24131
|
throw new UsageError(`no setting "${path}"`);
|
|
22359
|
-
const next = tail === undefined ? { ...current, [head]: value } : { ...current, weights: { ...current.weights, [tail]: value } };
|
|
22360
24132
|
if (tail !== undefined && head !== "weights") {
|
|
22361
24133
|
throw new UsageError(`"${head}" has no sub-settings`);
|
|
22362
24134
|
}
|
|
24135
|
+
const existing = currentValue(current, head, tail);
|
|
24136
|
+
const value = typeof existing === "boolean" ? asBoolean(path, raw) : asNumber(path, raw);
|
|
24137
|
+
const next = tail === undefined ? { ...current, [head]: value } : { ...current, weights: { ...current.weights, [tail]: value } };
|
|
22363
24138
|
const saved = await putSettings(store, next);
|
|
22364
24139
|
emit(ctx, writer, { settings: saved }, () => `${path} = ${value}`);
|
|
22365
24140
|
}
|
|
22366
24141
|
};
|
|
22367
24142
|
|
|
22368
24143
|
// apps/cli/src/commands/setup.ts
|
|
22369
|
-
import { join as
|
|
24144
|
+
import { join as join7 } from "path";
|
|
22370
24145
|
var OPTIONS = {
|
|
22371
24146
|
dir: { type: "string" },
|
|
22372
24147
|
key: { type: "string" },
|
|
@@ -22398,7 +24173,7 @@ ${f.contents}`).join(`
|
|
|
22398
24173
|
}
|
|
22399
24174
|
}
|
|
22400
24175
|
function at(dir, file2) {
|
|
22401
|
-
return { path:
|
|
24176
|
+
return { path: join7(dir, file2.path), contents: file2.contents };
|
|
22402
24177
|
}
|
|
22403
24178
|
async function promptMapping(models, prompt) {
|
|
22404
24179
|
const choices = models.map(({ model }) => model.id).join(", ");
|
|
@@ -22432,11 +24207,11 @@ var setupClaude = {
|
|
|
22432
24207
|
options: OPTIONS,
|
|
22433
24208
|
async run(args, { ctx, writer, prompt, setupFs }) {
|
|
22434
24209
|
const models = await described(ctx);
|
|
22435
|
-
const dir = stringFlag(args.values, "dir") ??
|
|
24210
|
+
const dir = stringFlag(args.values, "dir") ?? join7(setupFs.homeDir, ".claude");
|
|
22436
24211
|
const key = stringFlag(args.values, "key");
|
|
22437
24212
|
const dryRun2 = boolFlag(args.values, "dry-run");
|
|
22438
24213
|
const mapping = await promptMapping(models, prompt);
|
|
22439
|
-
const path =
|
|
24214
|
+
const path = join7(dir, "settings.json");
|
|
22440
24215
|
const file2 = claudeSettings(models, {
|
|
22441
24216
|
baseUrl: baseUrl(ctx),
|
|
22442
24217
|
discoveryMirrors: ctx.config().exposeClaudeCodeAliases,
|
|
@@ -22665,6 +24440,7 @@ var COMMANDS = {
|
|
|
22665
24440
|
restart,
|
|
22666
24441
|
doctor,
|
|
22667
24442
|
logs,
|
|
24443
|
+
bodies,
|
|
22668
24444
|
console: console_,
|
|
22669
24445
|
usage,
|
|
22670
24446
|
quota,
|
|
@@ -22688,13 +24464,19 @@ var COMMANDS = {
|
|
|
22688
24464
|
"models catalog": modelsCatalog,
|
|
22689
24465
|
"keys list": keysList,
|
|
22690
24466
|
"keys create": keysCreate,
|
|
24467
|
+
"keys limits": keysLimits,
|
|
22691
24468
|
"keys revoke": keysRevoke,
|
|
22692
24469
|
"settings get": settingsGet,
|
|
22693
24470
|
"settings set": settingsSet,
|
|
22694
24471
|
"setup claude": setupClaude,
|
|
22695
24472
|
"setup opencode": setupOpencode,
|
|
22696
24473
|
"admin set-password": adminSetPassword,
|
|
22697
|
-
"db migrate": dbMigrate
|
|
24474
|
+
"db migrate": dbMigrate,
|
|
24475
|
+
"db stats": dbStats,
|
|
24476
|
+
"db snapshots": dbSnapshots,
|
|
24477
|
+
"db backup": dbBackup,
|
|
24478
|
+
"db restore": dbRestore,
|
|
24479
|
+
"db vacuum": dbVacuum
|
|
22698
24480
|
};
|
|
22699
24481
|
var VALUED_GLOBALS = new Set(["--root", "--db"]);
|
|
22700
24482
|
function resolveCommand(argv) {
|
|
@@ -22733,7 +24515,7 @@ function resolveCommand(argv) {
|
|
|
22733
24515
|
var GROUPS = [
|
|
22734
24516
|
{
|
|
22735
24517
|
title: "Gateway",
|
|
22736
|
-
prefixes: ["status", "start", "stop", "restart", "doctor", "logs", "console"]
|
|
24518
|
+
prefixes: ["status", "start", "stop", "restart", "doctor", "logs", "bodies", "console"]
|
|
22737
24519
|
},
|
|
22738
24520
|
{ title: "Service", prefixes: ["service "] },
|
|
22739
24521
|
{ title: "Accounts", prefixes: ["connect", "credentials "] },
|
|
@@ -22795,10 +24577,10 @@ async function readHidden() {
|
|
|
22795
24577
|
stdin.setRawMode?.(true);
|
|
22796
24578
|
stdin.resume();
|
|
22797
24579
|
try {
|
|
22798
|
-
const
|
|
24580
|
+
const decoder2 = new TextDecoder;
|
|
22799
24581
|
let line = "";
|
|
22800
24582
|
for await (const chunk of stdin) {
|
|
22801
|
-
const text =
|
|
24583
|
+
const text = decoder2.decode(chunk, { stream: true });
|
|
22802
24584
|
for (const char of text) {
|
|
22803
24585
|
if (char === "\r" || char === `
|
|
22804
24586
|
`)
|
|
@@ -22819,10 +24601,10 @@ async function readHidden() {
|
|
|
22819
24601
|
}
|
|
22820
24602
|
}
|
|
22821
24603
|
async function readLine() {
|
|
22822
|
-
const
|
|
24604
|
+
const decoder2 = new TextDecoder;
|
|
22823
24605
|
let line = "";
|
|
22824
24606
|
for await (const chunk of process.stdin) {
|
|
22825
|
-
const text =
|
|
24607
|
+
const text = decoder2.decode(chunk, { stream: true });
|
|
22826
24608
|
const newline = text.indexOf(`
|
|
22827
24609
|
`);
|
|
22828
24610
|
if (newline >= 0)
|
|
@@ -22862,10 +24644,10 @@ function createPrompt(ctx, writer) {
|
|
|
22862
24644
|
}
|
|
22863
24645
|
|
|
22864
24646
|
// apps/cli/src/setupFs.ts
|
|
22865
|
-
import { dirname as
|
|
24647
|
+
import { dirname as dirname4 } from "path";
|
|
22866
24648
|
function atomicWriteFile(path, contents, ops) {
|
|
22867
24649
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
22868
|
-
ops.mkdir(
|
|
24650
|
+
ops.mkdir(dirname4(path));
|
|
22869
24651
|
try {
|
|
22870
24652
|
ops.write(temporary, contents);
|
|
22871
24653
|
ops.rename(temporary, path);
|
|
@@ -22920,10 +24702,10 @@ async function run(argv, writer, options = {}) {
|
|
|
22920
24702
|
cwd: process.cwd(),
|
|
22921
24703
|
read: (path) => existsSync6(path) ? readFileSync3(path, "utf8") : null,
|
|
22922
24704
|
write: (path, contents) => atomicWriteFile(path, contents, {
|
|
22923
|
-
mkdir: (directory) =>
|
|
24705
|
+
mkdir: (directory) => mkdirSync3(directory, { recursive: true }),
|
|
22924
24706
|
write: (temporary, data) => writeFileSync2(temporary, data),
|
|
22925
|
-
rename:
|
|
22926
|
-
remove: (temporary) =>
|
|
24707
|
+
rename: renameSync2,
|
|
24708
|
+
remove: (temporary) => rmSync3(temporary, { force: true })
|
|
22927
24709
|
})
|
|
22928
24710
|
},
|
|
22929
24711
|
service: () => options.service?.({ root: ctx.root.root, env: ctx.env }) ?? createServiceDeps({ root: ctx.root.root, env: ctx.env, scope, now: ctx.now }),
|