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