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.
Files changed (54) hide show
  1. package/README.md +334 -8
  2. package/bin/omni.js +1929 -147
  3. package/gateway.js +3129 -555
  4. package/package.json +1 -1
  5. package/public/assets/{Chip-DJwp7gBu.js → Chip-DbKk1ExR.js} +1 -1
  6. package/public/assets/Confirm-C5DYoI93.js +4 -0
  7. package/public/assets/CopyValue-CXjietiZ.js +27 -0
  8. package/public/assets/{Field-BDtp692L.js → Field-B7GOsJf8.js} +1 -1
  9. package/public/assets/Lamp-QRSv-SqM.js +25 -0
  10. package/public/assets/{Meter-BREBUFGX.js → Meter-Dngwbpjo.js} +1 -1
  11. package/public/assets/Modal-DfltaSCz.js +82 -0
  12. package/public/assets/Rack-D1zBw9SW.js +151 -0
  13. package/public/assets/{Readout-CEAoN5hu.js → Readout--CQnPjoO.js} +2 -2
  14. package/public/assets/{States-BEKLros0.js → States-CQFkw1UZ.js} +3 -3
  15. package/public/assets/{Table-BBypbVWj.js → Table-Ba58Wg4-.js} +1 -1
  16. package/public/assets/Toggle-DWrWc9-9.js +39 -0
  17. package/public/assets/TokenBreakdown-CyHJv0O5.js +31 -0
  18. package/public/assets/_app-CTDgfstQ.js +1 -0
  19. package/public/assets/_app.accounts-DP6T8z7I.js +64 -0
  20. package/public/assets/{_app.console-DKyGL9xx.js → _app.console-DYr7TCAl.js} +10 -10
  21. package/public/assets/_app.database-BzO1S_VR.js +39 -0
  22. package/public/assets/_app.index-CBt6b-lA.js +62 -0
  23. package/public/assets/_app.keys-B8HGsPPS.js +76 -0
  24. package/public/assets/_app.logs-DKsxa2Su.js +68 -0
  25. package/public/assets/{_app.models-CDqVrAeF.js → _app.models-CZD2UpM-.js} +28 -28
  26. package/public/assets/_app.settings-B2C1LQIF.js +46 -0
  27. package/public/assets/_app.usage-CDWUXtZy.js +83 -0
  28. package/public/assets/dist-IkJ0qg-4.js +1 -0
  29. package/public/assets/index-ClU3Xqui.js +186 -0
  30. package/public/assets/login-E3gh2_Cl.js +36 -0
  31. package/public/assets/plus-DkO4vG-T.js +1 -0
  32. package/public/assets/queries-BJHk_wso.js +144 -0
  33. package/public/assets/reasons-Cb8yF4Oy.js +1 -0
  34. package/public/assets/{shared-CIhZ6FCO.js → shared-Bp35cgFf.js} +7 -7
  35. package/public/assets/{trash-2-CLYNAAJA.js → trash-2-DTWWI3vA.js} +1 -1
  36. package/public/index.html +2 -2
  37. package/public/assets/Confirm-CVLz9F7a.js +0 -4
  38. package/public/assets/CopyValue-CNZbWS59.js +0 -27
  39. package/public/assets/Lamp-B65uWKbW.js +0 -25
  40. package/public/assets/Modal-DbydSAlp.js +0 -82
  41. package/public/assets/Rack-Bbo4JfEX.js +0 -151
  42. package/public/assets/Toggle-BUr71O4J.js +0 -39
  43. package/public/assets/TokenBreakdown-C1ziK9d1.js +0 -31
  44. package/public/assets/_app-ByQ1bPbe.js +0 -1
  45. package/public/assets/_app.accounts-BTug_Xfo.js +0 -64
  46. package/public/assets/_app.index-CSw9gmMf.js +0 -62
  47. package/public/assets/_app.keys-07Kbw4RS.js +0 -39
  48. package/public/assets/_app.logs-C9QiV39p.js +0 -32
  49. package/public/assets/_app.settings-DmUEeTaP.js +0 -38
  50. package/public/assets/_app.usage-B1WuMGRr.js +0 -83
  51. package/public/assets/dist-Bka7ErcH.js +0 -1
  52. package/public/assets/index-DJfb1i4P.js +0 -186
  53. package/public/assets/login-DKZYqRh0.js +0 -33
  54. 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 mkdirSync2, readFileSync as readFileSync3, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
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/o\u200Dpencode",
517
- "o\u200Dpencode.ai/docs",
518
- "github.com/c\u200Dline/c\u200Dline",
519
- "github.com/getc\u200Dursor/c\u200Dursor",
520
- "c\u200Dontinue.dev"
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 O\u200DpenCode honestly", "if the assistant honestly"],
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("You are O\u200DpenCode")).filter((p) => !p.includes(BILLING_PREFIX)).filter((p) => p.trim() !== AGENT_PREAMBLE).join(`
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
- rateLimitPerMin: exports_external.number().int().positive().nullable().default(null)
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((r) => ({
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(`INSERT INTO credential_health
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
- var toKey = (r) => ({
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
- rateLimitPerMin: r.rate_limit_per_min,
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, rate_limit_per_min, created_at, revoked_at)
19107
- VALUES (?,?,?,?,?,?,?,NULL)`, [
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.rateLimitPerMin,
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
- rollupLog(db, toLog(stored));
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
- db.run("DELETE FROM request_logs WHERE at < ?", [olderThan]);
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
- credentials: createCredentialRepo(db, opts.encryptionKey, emit2),
19376
- config: createConfigRepo(db, emit2),
19377
- keys: createKeyRepo(db),
19378
- usage: createUsageRepo(db),
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
- close: () => db.close()
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 HOUR_MS = 3600000;
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 / HOUR_MS;
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 * HOUR_MS;
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
- async function listKeys(store) {
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((k) => ({
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
- rateLimitPerMin: body2.rateLimitPerMin
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 DAY_MS = 24 * 60 * 60 * 1000;
20696
- var HOUR_MS2 = 3600000;
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) / HOUR_MS2)
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 * DAY_MS;
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 settings = parseOrThrow(settingsSchema, input);
20752
- await store.config.putSettings(settings);
20753
- return settings;
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 statSync(path).isFile();
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 = join(homedir(), ".config", "omnigateway");
22319
+ var DEFAULT_ROOT = join4(homedir(), ".config", "omnigateway");
21000
22320
  function looksLikeRoot(dir) {
21001
- return existsSync(join(dir, ".env")) || existsSync(join(dir, "omnigateway.db")) || existsSync(join(dir, "apps", "gateway", "src", "index.ts"));
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 = join(root, ".env");
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(resolve(cwd, flags.root), "flag");
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(resolve(cwd, fromEnv), "env");
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 : resolve(root.root, 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 join2 } from "path";
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" ? join2("/etc/systemd/system", UNIT_NAME2) : join2(homedir2(), ".config", "systemd", "user", UNIT_NAME2);
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 join2(stateDir, "gateway.pid");
22617
+ return join5(stateDir, "gateway.pid");
21161
22618
  }
21162
22619
  function logFile(stateDir) {
21163
- return join2(stateDir, "gateway.log");
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 : join2(homedir2(), ".local", "state");
21172
- return join2(base, "omnigateway");
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=${join2(input.root, ".env")}
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
- rmSync(pidFile(deps.stateDir), { force: true });
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 state = active.stdout.trim() || active.stderr.trim() || "unknown";
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: state === "active",
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
- mkdirSync(deps.stateDir, { recursive: true });
22713
+ mkdirSync2(deps.stateDir, { recursive: true });
21257
22714
  const file2 = supervisedLogFile(deps);
21258
- mkdirSync(dirname(file2), { recursive: true });
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
- rmSync(pidFile(deps.stateDir), { force: true });
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
- mkdirSync(dirname(path), { recursive: true });
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
- rmSync(path, { force: true });
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: "RATE/MIN", align: "right" },
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.rateLimitPerMin === null ? "\u2014" : String(key.rateLimitPerMin),
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> ...] [--rate-limit N]",
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
- "rate-limit": { type: "string" }
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
- rateLimitPerMin: numberFlag(args.values, "rate-limit") ?? null
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 join3 } from "path";
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
- join3(root, "apps", "gateway", "src", "index.ts"),
22150
- join3(cliDir, "..", "gateway.js"),
22151
- join3(cliDir, "gateway.js")
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 in current))
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 join4 } from "path";
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: join4(dir, file2.path), contents: file2.contents };
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") ?? join4(setupFs.homeDir, ".claude");
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 = join4(dir, "settings.json");
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 decoder = new TextDecoder;
24580
+ const decoder2 = new TextDecoder;
22799
24581
  let line = "";
22800
24582
  for await (const chunk of stdin) {
22801
- const text = decoder.decode(chunk, { stream: true });
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 decoder = new TextDecoder;
24604
+ const decoder2 = new TextDecoder;
22823
24605
  let line = "";
22824
24606
  for await (const chunk of process.stdin) {
22825
- const text = decoder.decode(chunk, { stream: true });
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 dirname2 } from "path";
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(dirname2(path));
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) => mkdirSync2(directory, { recursive: true }),
24705
+ mkdir: (directory) => mkdirSync3(directory, { recursive: true }),
22924
24706
  write: (temporary, data) => writeFileSync2(temporary, data),
22925
- rename: renameSync,
22926
- remove: (temporary) => rmSync2(temporary, { force: true })
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 }),