omnigateway 0.11.0 → 0.11.2

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 (47) hide show
  1. package/README.md +16 -17
  2. package/bin/omni.js +184 -35
  3. package/gateway.js +139 -40
  4. package/package.json +2 -2
  5. package/public/assets/{Chip-CmL-9rw7.js → Chip-77Q04h7S.js} +2 -1
  6. package/public/assets/{CopyValue-CTVbU0O_.js → CopyValue-DDJId-Zo.js} +1 -1
  7. package/public/assets/{Field-DcNu4yw-.js → Field-56pmSGrh.js} +1 -1
  8. package/public/assets/{Lamp-DtHKJH6P.js → Lamp-Drd1fFwQ.js} +7 -7
  9. package/public/assets/{Panel-BlNr9j6-.js → Panel-BV18WuL1.js} +1 -0
  10. package/public/assets/Rack-D3iFlR6e.js +482 -0
  11. package/public/assets/{Readout-D8Ofl3Gz.js → Readout-Dxwka1JI.js} +1 -1
  12. package/public/assets/RequestTable-I395emqH.js +11 -0
  13. package/public/assets/SummaryDeck-pAZ93jBW.js +15 -0
  14. package/public/assets/{Toggle-ChjN-tw1.js → Toggle-y17mtXCV.js} +1 -1
  15. package/public/assets/{TokenBreakdown-BPoXM3JM.js → TokenBreakdown-BIwuEVs8.js} +2 -1
  16. package/public/assets/{WindowChart-BPEsEGFL.js → WindowChart-DWZtz4Ps.js} +2 -2
  17. package/public/assets/{_app-CWXzHdhc.js → _app-0fHLZIC7.js} +1 -1
  18. package/public/assets/_app.accounts-DBlImnPw.js +59 -0
  19. package/public/assets/{_app.console-Bkdallia.js → _app.console-in_iVnep.js} +2 -2
  20. package/public/assets/{_app.database-DxqN1395.js → _app.database-CPj-Juag.js} +4 -4
  21. package/public/assets/_app.index-DGz9njXx.js +67 -0
  22. package/public/assets/_app.keys-C-Fk39-U.js +92 -0
  23. package/public/assets/{_app.logs-DGQD8ZAq.js → _app.logs-C7BhBRjg.js} +3 -3
  24. package/public/assets/{_app.models-D6SV_GVk.js → _app.models-BuiRrOrz.js} +4 -4
  25. package/public/assets/{_app.plugins._pluginId-CJ7ObXXe.js → _app.plugins._pluginId-Bx1LgcDj.js} +1 -1
  26. package/public/assets/_app.settings-s6K98CA2.js +88 -0
  27. package/public/assets/_app.usage-Bp45JtBe.js +76 -0
  28. package/public/assets/{chevron-right-BfnK63GB.js → chevron-right-BE-3EQR5.js} +1 -1
  29. package/public/assets/client-Bl_FA5jI.js +39 -0
  30. package/public/assets/index-nvJxJ9Gp.js +13 -0
  31. package/public/assets/{login-CrJK9wRA.js → login-Qc9iIepG.js} +2 -2
  32. package/public/assets/plus-DhyoE7aH.js +1 -0
  33. package/public/assets/{preload-helper-BknjH67x.js → preload-helper-DYShfVEU.js} +1 -1
  34. package/public/assets/{shared-oTMOLWMT.js → shared-ChaZCaBz.js} +12 -12
  35. package/public/assets/{trash-2-CSkf6ME_.js → trash-2-zGGJ7VE_.js} +1 -1
  36. package/public/index.html +5 -5
  37. package/public/assets/Rack-_A_3zqxC.js +0 -476
  38. package/public/assets/RequestTable-Cokk7wKP.js +0 -11
  39. package/public/assets/SummaryDeck-C0IbqnLz.js +0 -15
  40. package/public/assets/_app.accounts-DKmxz7Tl.js +0 -59
  41. package/public/assets/_app.index-BUqor-qH.js +0 -62
  42. package/public/assets/_app.keys-DMCPEj1g.js +0 -82
  43. package/public/assets/_app.settings-BVimpngR.js +0 -88
  44. package/public/assets/_app.usage-Y2Nkaw0t.js +0 -76
  45. package/public/assets/client-WgQ5A2PD.js +0 -39
  46. package/public/assets/index-BTE8vyBz.js +0 -13
  47. package/public/assets/plus-Cv51cK5k.js +0 -1
package/gateway.js CHANGED
@@ -24927,14 +24927,14 @@ function estimateCachedInputTokens(request) {
24927
24927
  return cached2;
24928
24928
  }
24929
24929
  // packages/ir/src/validate.ts
24930
- function validateRequest(req) {
24930
+ function validateRequest(req, newToolUseId = () => `tu_${crypto.randomUUID()}`) {
24931
24931
  const seenToolUseIds = new Set;
24932
24932
  const cleaned = [];
24933
24933
  for (const message of req.messages) {
24934
24934
  const content = [];
24935
24935
  for (const block of message.content) {
24936
24936
  if (block.type === "toolUse") {
24937
- const id = block.id.length > 0 ? block.id : `tu_${crypto.randomUUID()}`;
24937
+ const id = block.id.length > 0 ? block.id : newToolUseId();
24938
24938
  seenToolUseIds.add(id);
24939
24939
  content.push({ ...block, id });
24940
24940
  continue;
@@ -25574,13 +25574,34 @@ CREATE TRIGGER credential_health_config_version_del
25574
25574
  FOR EACH STATEMENT EXECUTE FUNCTION bump_config_version();
25575
25575
  `;
25576
25576
 
25577
+ // packages/store/src/postgres/migrations/003_key_expiry.sql
25578
+ var _003_key_expiry_default = `-- The Postgres half of the sqlite migration of the same name. \`BIGINT\`, like
25579
+ -- every other epoch-ms column in this schema, and read back through \`numOrNull\`.
25580
+ --
25581
+ -- Nullable rather than \`NOT NULL DEFAULT\`: the column is a deadline, and every
25582
+ -- finite default is a date on which somebody's client stops working. NULL is
25583
+ -- the only value that says nothing was decided, and every reader maps it to
25584
+ -- "never expires".
25585
+ --
25586
+ -- No backfill, deliberately. Every key on every existing installation was
25587
+ -- minted on the promise that it works until revoked, so adding the column and
25588
+ -- stopping is what keeps that promise. The absence of an \`UPDATE\` here is the
25589
+ -- substance of the migration, not an omission.
25590
+ --
25591
+ -- Safe to apply under a rolling deploy, unlike migration 2: this adds a
25592
+ -- nullable column and drops nothing, so a replica running the previous image
25593
+ -- keeps serving \u2014 \`SELECT *\` gains a field its mapper ignores.
25594
+ ALTER TABLE api_keys ADD COLUMN expires_at BIGINT;
25595
+ `;
25596
+
25577
25597
  // packages/store/src/postgres/db.ts
25578
25598
  var num = (v) => Number(v);
25579
25599
  var numOrNull = (v) => v === null ? null : Number(v);
25580
25600
  var MIGRATION_LOCK = 7140641;
25581
25601
  var MIGRATIONS = [
25582
25602
  { id: 1, sql: _001_init_default },
25583
- { id: 2, sql: _002_health_measurements_default }
25603
+ { id: 2, sql: _002_health_measurements_default },
25604
+ { id: 3, sql: _003_key_expiry_default }
25584
25605
  ];
25585
25606
  async function openPg(url2) {
25586
25607
  const sql = new SQL({ url: url2 });
@@ -25766,6 +25787,9 @@ function resolvePin(target, accounts) {
25766
25787
  return;
25767
25788
  return accounts.find((account) => servesTarget(target, account));
25768
25789
  }
25790
+ var keyUsable = (key, now) => key.revokedAt === null && (key.expiresAt === null || key.expiresAt > now);
25791
+ var credentialPastExpiry = (credential, now) => credential.authType === "oauth" && credential.expiresAt !== null && credential.expiresAt <= now;
25792
+ var credentialExpired = (credential, now) => credentialPastExpiry(credential, now) && !credential.hasRefreshToken;
25769
25793
  var NODE_GRACE_MS = 60000;
25770
25794
  var DEFAULT_SETTINGS = {
25771
25795
  weights: { tier: 10, health: 3, quota: 2, load: 2, cost: 1, latency: 1 },
@@ -26245,7 +26269,8 @@ var toKey = (r, logger2) => ({
26245
26269
  limits: parseLimits(r.id, r.limits, logger2),
26246
26270
  bodyLoggingOptOut: r.body_logging_opt_out,
26247
26271
  createdAt: num(r.created_at),
26248
- revokedAt: numOrNull(r.revoked_at)
26272
+ revokedAt: numOrNull(r.revoked_at),
26273
+ expiresAt: numOrNull(r.expires_at)
26249
26274
  });
26250
26275
  function createKeyRepo(sql, logger2) {
26251
26276
  const one = async (where, arg) => {
@@ -26263,8 +26288,8 @@ function createKeyRepo(sql, logger2) {
26263
26288
  async create(input2) {
26264
26289
  const now = Date.now();
26265
26290
  await sql.unsafe(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
26266
- body_logging_opt_out, created_at, revoked_at)
26267
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NULL)`, [
26291
+ body_logging_opt_out, created_at, revoked_at, expires_at)
26292
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NULL,$9)`, [
26268
26293
  input2.id,
26269
26294
  input2.label,
26270
26295
  input2.prefix,
@@ -26272,7 +26297,8 @@ function createKeyRepo(sql, logger2) {
26272
26297
  input2.modelAllowlist === null ? null : JSON.stringify(input2.modelAllowlist),
26273
26298
  JSON.stringify(parseLimitConfig(input2.limits)),
26274
26299
  input2.bodyLoggingOptOut,
26275
- now
26300
+ now,
26301
+ input2.expiresAt
26276
26302
  ]);
26277
26303
  return { ...input2, createdAt: now, revokedAt: null };
26278
26304
  },
@@ -26280,8 +26306,8 @@ function createKeyRepo(sql, logger2) {
26280
26306
  if (row.limits === null)
26281
26307
  throw new Error(`api key ${row.id} has unreadable limits`);
26282
26308
  await sql.unsafe(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
26283
- body_logging_opt_out, created_at, revoked_at)
26284
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`, [
26309
+ body_logging_opt_out, created_at, revoked_at, expires_at)
26310
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`, [
26285
26311
  row.id,
26286
26312
  row.label,
26287
26313
  row.prefix,
@@ -26290,7 +26316,8 @@ function createKeyRepo(sql, logger2) {
26290
26316
  JSON.stringify(parseLimitConfig(row.limits)),
26291
26317
  row.bodyLoggingOptOut,
26292
26318
  row.createdAt,
26293
- row.revokedAt
26319
+ row.revokedAt,
26320
+ row.expiresAt
26294
26321
  ]);
26295
26322
  },
26296
26323
  async setLimits(id, limits) {
@@ -26305,6 +26332,9 @@ function createKeyRepo(sql, logger2) {
26305
26332
  id
26306
26333
  ]);
26307
26334
  },
26335
+ async setExpiry(id, expiresAt) {
26336
+ await sql.unsafe("UPDATE api_keys SET expires_at = $1 WHERE id = $2", [expiresAt, id]);
26337
+ },
26308
26338
  async revoke(id) {
26309
26339
  await sql.unsafe("UPDATE api_keys SET revoked_at = $1 WHERE id = $2", [Date.now(), id]);
26310
26340
  }
@@ -28109,6 +28139,35 @@ ALTER TABLE credential_health DROP COLUMN ewma_ttft_ms;
28109
28139
  ALTER TABLE credential_health DROP COLUMN last_used_at;
28110
28140
  `;
28111
28141
 
28142
+ // packages/store/src/sqlite/migrations/014_key_expiry.sql
28143
+ var _014_key_expiry_default = `-- An optional expiry on a gateway API key: an absolute epoch-ms instant after
28144
+ -- which the key is refused, spelled exactly as \`credentials.expires_at\` is.
28145
+ --
28146
+ -- Nullable rather than \`NOT NULL DEFAULT\`, which is the opposite of the choice
28147
+ -- migration 9 made for \`limits\`, and for the opposite reason. There, every
28148
+ -- possible value is a policy and \`{}\` is the one that means "no ceiling", so a
28149
+ -- default gives "unlimited" a single spelling. Here the column is a deadline,
28150
+ -- and no number means "no deadline" \u2014 every finite default is a date on which
28151
+ -- somebody's client stops working. NULL is the only value that says nothing was
28152
+ -- decided, so it is the column's own default and every reader maps it to
28153
+ -- "never expires".
28154
+ --
28155
+ -- No backfill, deliberately, and this is the whole substance of the migration.
28156
+ -- Every key on every existing installation was minted on the promise that it
28157
+ -- works until revoked; an upgrade that attaches an expiry to those rows breaks
28158
+ -- clients on a date the operator never chose, and the failure surfaces days
28159
+ -- later as an authentication error with nothing in it that names this change.
28160
+ -- Adding the column and stopping is what keeps that promise, so the absence of
28161
+ -- an \`UPDATE\` here is load-bearing rather than an omission \u2014
28162
+ -- \`packages/store/test/migrations.test.ts\` pins it.
28163
+ --
28164
+ -- No index. The column is read only after a row has already been found by
28165
+ -- \`hash\` or by \`id\`, both of which are unique, so there is no scan for an index
28166
+ -- to help; and nothing sweeps expired keys, because an expired key stays listed
28167
+ -- exactly as a revoked one does so its usage history keeps its attribution.
28168
+ ALTER TABLE api_keys ADD COLUMN expires_at INTEGER;
28169
+ `;
28170
+
28112
28171
  // packages/store/src/sqlite/db.ts
28113
28172
  var MIGRATIONS2 = [
28114
28173
  { id: 1, sql: _001_init_default2 },
@@ -28123,7 +28182,8 @@ var MIGRATIONS2 = [
28123
28182
  { id: 10, sql: _010_usage_rollup_default, after: rebuildRollup },
28124
28183
  { id: 11, sql: _011_plugin_migrations_default },
28125
28184
  { id: 12, sql: _012_nodes_default },
28126
- { id: 13, sql: _013_health_measurements_default }
28185
+ { id: 13, sql: _013_health_measurements_default },
28186
+ { id: 14, sql: _014_key_expiry_default }
28127
28187
  ];
28128
28188
  function statements(sql) {
28129
28189
  return sql.split(`
@@ -28179,7 +28239,8 @@ var toKey2 = (r, logger2) => ({
28179
28239
  limits: parseLimits2(r.id, r.limits, logger2),
28180
28240
  bodyLoggingOptOut: r.body_logging_opt_out === 1,
28181
28241
  createdAt: r.created_at,
28182
- revokedAt: r.revoked_at
28242
+ revokedAt: r.revoked_at,
28243
+ expiresAt: r.expires_at
28183
28244
  });
28184
28245
  function createKeyRepo2(db, logger2 = noopLogger) {
28185
28246
  return {
@@ -28197,8 +28258,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
28197
28258
  async create(input2) {
28198
28259
  const now = Date.now();
28199
28260
  db.run(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
28200
- body_logging_opt_out, created_at, revoked_at)
28201
- VALUES (?,?,?,?,?,?,?,?,NULL)`, [
28261
+ body_logging_opt_out, created_at, revoked_at, expires_at)
28262
+ VALUES (?,?,?,?,?,?,?,?,NULL,?)`, [
28202
28263
  input2.id,
28203
28264
  input2.label,
28204
28265
  input2.prefix,
@@ -28206,7 +28267,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
28206
28267
  input2.modelAllowlist === null ? null : JSON.stringify(input2.modelAllowlist),
28207
28268
  JSON.stringify(parseLimitConfig(input2.limits)),
28208
28269
  input2.bodyLoggingOptOut ? 1 : 0,
28209
- now
28270
+ now,
28271
+ input2.expiresAt
28210
28272
  ]);
28211
28273
  return { ...input2, createdAt: now, revokedAt: null };
28212
28274
  },
@@ -28214,8 +28276,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
28214
28276
  if (row.limits === null)
28215
28277
  throw new Error(`api key ${row.id} has unreadable limits`);
28216
28278
  db.run(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
28217
- body_logging_opt_out, created_at, revoked_at)
28218
- VALUES (?,?,?,?,?,?,?,?,?)`, [
28279
+ body_logging_opt_out, created_at, revoked_at, expires_at)
28280
+ VALUES (?,?,?,?,?,?,?,?,?,?)`, [
28219
28281
  row.id,
28220
28282
  row.label,
28221
28283
  row.prefix,
@@ -28224,7 +28286,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
28224
28286
  JSON.stringify(parseLimitConfig(row.limits)),
28225
28287
  row.bodyLoggingOptOut ? 1 : 0,
28226
28288
  row.createdAt,
28227
- row.revokedAt
28289
+ row.revokedAt,
28290
+ row.expiresAt
28228
28291
  ]);
28229
28292
  },
28230
28293
  async setLimits(id, limits) {
@@ -28239,6 +28302,9 @@ function createKeyRepo2(db, logger2 = noopLogger) {
28239
28302
  id
28240
28303
  ]);
28241
28304
  },
28305
+ async setExpiry(id, expiresAt) {
28306
+ db.run("UPDATE api_keys SET expires_at = ? WHERE id = ?", [expiresAt, id]);
28307
+ },
28242
28308
  async revoke(id) {
28243
28309
  db.run("UPDATE api_keys SET revoked_at = ? WHERE id = ?", [Date.now(), id]);
28244
28310
  }
@@ -28803,6 +28869,7 @@ async function createStore(opts) {
28803
28869
  importRow: (row) => handle.keys.importRow(row),
28804
28870
  setLimits: (id, limits) => handle.keys.setLimits(id, limits),
28805
28871
  setModelAllowlist: (id, modelAllowlist) => handle.keys.setModelAllowlist(id, modelAllowlist),
28872
+ setExpiry: (id, expiresAt) => handle.keys.setExpiry(id, expiresAt),
28806
28873
  revoke: (id) => handle.keys.revoke(id)
28807
28874
  },
28808
28875
  usage: {
@@ -28911,7 +28978,7 @@ function createAdminAuth(store, opts) {
28911
28978
  };
28912
28979
  const keyStillValid = async (apiKeyId) => {
28913
28980
  const key = await store.keys.get(apiKeyId);
28914
- return key !== null && key.revokedAt === null;
28981
+ return key !== null && keyUsable(key, opts.now());
28915
28982
  };
28916
28983
  const applyPassword = async (password) => {
28917
28984
  if (password.length < MIN_PASSWORD_LENGTH) {
@@ -28965,7 +29032,7 @@ function createAdminAuth(store, opts) {
28965
29032
  },
28966
29033
  async loginClient(rawKey) {
28967
29034
  const key = await store.keys.findByHash(await hashApiKey(rawKey));
28968
- if (key === null || key.revokedAt !== null)
29035
+ if (key === null || !keyUsable(key, opts.now()))
28969
29036
  return null;
28970
29037
  return issue2({ kind: "client", apiKeyId: key.id });
28971
29038
  },
@@ -36571,16 +36638,19 @@ var modelSchema = exports_external.object({
36571
36638
  isAlias: exports_external.boolean(),
36572
36639
  targets: exports_external.array(targetSchema).min(1, "a virtual model needs at least one target")
36573
36640
  });
36641
+ var MAX_INSTANT = 8640000000000000;
36574
36642
  var keyCreateSchema = exports_external.object({
36575
36643
  label: exports_external.string().min(1).default("api key"),
36576
36644
  modelAllowlist: exports_external.array(exports_external.string().min(1)).nullable().default(null),
36577
36645
  limits: limitConfigSchema.default({}),
36578
- bodyLoggingOptOut: exports_external.boolean().default(false)
36646
+ bodyLoggingOptOut: exports_external.boolean().default(false),
36647
+ expiresAt: exports_external.number().int().positive().max(MAX_INSTANT).nullable().default(null)
36579
36648
  }).strict();
36580
36649
  var keyLimitsSchema = exports_external.object({ limits: limitConfigSchema }).strict();
36581
36650
  var keyModelsSchema = exports_external.object({
36582
36651
  modelAllowlist: exports_external.array(exports_external.string().min(1)).nullable()
36583
36652
  }).strict();
36653
+ var keyExpirySchema = exports_external.object({ expiresAt: exports_external.number().int().positive().max(MAX_INSTANT).nullable() }).strict();
36584
36654
  var retentionSchema = exports_external.object({
36585
36655
  keepLatest: exports_external.number().int().min(1).max(100),
36586
36656
  maxAgeDays: exports_external.number().int().min(1).max(3650)
@@ -36795,7 +36865,7 @@ function eligible(input2) {
36795
36865
  drop("disabled");
36796
36866
  continue;
36797
36867
  }
36798
- if (credential.authType === "oauth" && credential.expiresAt !== null && credential.expiresAt <= now && !credential.hasRefreshToken) {
36868
+ if (credentialExpired(credential, now)) {
36799
36869
  drop("expired");
36800
36870
  continue;
36801
36871
  }
@@ -37741,7 +37811,8 @@ async function toSummary(store, key, now) {
37741
37811
  limitUsage: await readLimitUsage(store, key.id, key.limits, now),
37742
37812
  bodyLoggingOptOut: key.bodyLoggingOptOut,
37743
37813
  createdAt: key.createdAt,
37744
- revokedAt: key.revokedAt
37814
+ revokedAt: key.revokedAt,
37815
+ expiresAt: key.expiresAt
37745
37816
  };
37746
37817
  }
37747
37818
  async function listKeys(store, now = Date.now()) {
@@ -37764,7 +37835,8 @@ async function createKey(store, input2) {
37764
37835
  hash: await hashApiKey(raw),
37765
37836
  modelAllowlist: body2.modelAllowlist,
37766
37837
  limits: body2.limits,
37767
- bodyLoggingOptOut: body2.bodyLoggingOptOut
37838
+ bodyLoggingOptOut: body2.bodyLoggingOptOut,
37839
+ expiresAt: body2.expiresAt
37768
37840
  });
37769
37841
  return { id: created.id, label: created.label, prefix: created.prefix, key: raw };
37770
37842
  }
@@ -37784,6 +37856,14 @@ async function setKeyModels(store, id, input2, now = Date.now()) {
37784
37856
  await store.keys.setModelAllowlist(id, body2.modelAllowlist);
37785
37857
  return toSummary(store, { ...key, modelAllowlist: body2.modelAllowlist }, now);
37786
37858
  }
37859
+ async function setKeyExpiry(store, id, input2, now = Date.now()) {
37860
+ const body2 = parseOrThrow(keyExpirySchema, input2);
37861
+ const key = await store.keys.get(id);
37862
+ if (key === null)
37863
+ throw new GatewayError("BAD_REQUEST", "no such api key");
37864
+ await store.keys.setExpiry(id, body2.expiresAt);
37865
+ return toSummary(store, { ...key, expiresAt: body2.expiresAt }, now);
37866
+ }
37787
37867
  async function revokeKey(store, id) {
37788
37868
  await store.keys.revoke(id);
37789
37869
  }
@@ -38634,6 +38714,12 @@ async function accountQuota(deps) {
38634
38714
  // packages/control/src/quota/history.ts
38635
38715
  var DAY_MS3 = 24 * 60 * 60 * 1000;
38636
38716
  var HOUR_MS3 = 3600000;
38717
+ var MAX_SAMPLES = 50000;
38718
+ var SAMPLE_QUERY_LIMIT = MAX_SAMPLES + 1;
38719
+ function pageSamples(samples) {
38720
+ const truncated = samples.length > MAX_SAMPLES;
38721
+ return { page: truncated ? samples.slice(0, MAX_SAMPLES) : samples, truncated };
38722
+ }
38637
38723
  async function retainedSpan(deps, input2, maxSpanMs) {
38638
38724
  const now = deps.now();
38639
38725
  const settings = await deps.store.config.getSettings();
@@ -38680,26 +38766,26 @@ async function quotaHistory(deps, input2) {
38680
38766
  deps.store.credentials.listQuotaSamples({
38681
38767
  since,
38682
38768
  until,
38769
+ limit: SAMPLE_QUERY_LIMIT,
38683
38770
  ...credentialId === undefined ? {} : { credentialId }
38684
38771
  }),
38685
38772
  deps.store.credentials.listQuota()
38686
38773
  ]);
38687
38774
  const scoped = credentialId === undefined ? quota : quota.filter((w) => w.credentialId === credentialId);
38688
- return { samples, gatewayRates: await gatewayRatesFor(deps.store, scoped) };
38775
+ const { page, truncated } = pageSamples(samples);
38776
+ return { samples: page, truncated, gatewayRates: await gatewayRatesFor(deps.store, scoped) };
38689
38777
  }
38690
38778
 
38691
38779
  // packages/control/src/quota/clientHistory.ts
38692
38780
  var MAX_SPAN_MS = 16 * 24 * 60 * 60 * 1000;
38693
- var MAX_SAMPLES = 50000;
38694
38781
  async function accountQuotaHistory(deps, input2) {
38695
38782
  const { since, until } = await retainedSpan(deps, input2, MAX_SPAN_MS);
38696
38783
  const [samples, credentials] = await Promise.all([
38697
- deps.store.credentials.listQuotaSamples({ since, until, limit: MAX_SAMPLES + 1 }),
38784
+ deps.store.credentials.listQuotaSamples({ since, until, limit: SAMPLE_QUERY_LIMIT }),
38698
38785
  deps.store.credentials.list()
38699
38786
  ]);
38700
38787
  const byId = new Map(credentials.map((credential) => [credential.id, credential]));
38701
- const truncated = samples.length > MAX_SAMPLES;
38702
- const page = truncated ? samples.slice(0, MAX_SAMPLES) : samples;
38788
+ const { page, truncated } = pageSamples(samples);
38703
38789
  const out = [];
38704
38790
  for (const sample of page) {
38705
38791
  const credential = byId.get(sample.credentialId);
@@ -55121,6 +55207,11 @@ function adminRoutes(deps) {
55121
55207
  const key = await setKeyModels(deps.store, params.id, await readJson(request2));
55122
55208
  changed("res:keys");
55123
55209
  return key;
55210
+ }).put("/api/keys/:id/expiry", async ({ request: request2, params }) => {
55211
+ await requireAdmin(request2, deps.admin);
55212
+ const key = await setKeyExpiry(deps.store, params.id, await readJson(request2));
55213
+ changed("res:keys");
55214
+ return key;
55124
55215
  }).delete("/api/keys/:id", async ({ request: request2, params }) => {
55125
55216
  await requireAdmin(request2, deps.admin);
55126
55217
  await revokeKey(deps.store, params.id);
@@ -55131,6 +55222,7 @@ function adminRoutes(deps) {
55131
55222
  return {
55132
55223
  settings: await getSettings(deps.store),
55133
55224
  bodyLoggingAllowed: deps.bodyLoggingAllowed === true,
55225
+ dayOffsetMinutes: deps.dayOffsetMinutes ?? hostDayOffsetMinutes(),
55134
55226
  viewerConfigured: await deps.admin.isViewerConfigured()
55135
55227
  };
55136
55228
  }).put("/api/settings/password", async ({ request: request2 }) => {
@@ -55265,7 +55357,10 @@ function clientRoutes(deps) {
55265
55357
  return { ok: true };
55266
55358
  }).get("/api/client/summary", async ({ request: request2 }) => {
55267
55359
  const apiKeyId = await requireClient(request2, deps.admin);
55268
- return readOwnKey(deps.store, apiKeyId, now());
55360
+ return {
55361
+ ...await readOwnKey(deps.store, apiKeyId, now()),
55362
+ dayOffsetMinutes: deps.dayOffsetMinutes ?? hostDayOffsetMinutes()
55363
+ };
55269
55364
  }).get("/api/client/usage", async ({ request: request2, query }) => {
55270
55365
  const apiKeyId = await requireClient(request2, deps.admin);
55271
55366
  return queryUsage({ store: deps.store, now }, {
@@ -55665,12 +55760,12 @@ function injectPonytail(request2, opts) {
55665
55760
  }
55666
55761
 
55667
55762
  // apps/gateway/src/auth/apiKey.ts
55668
- async function authenticateApiKey(store, header, logger2 = noopLogger) {
55763
+ async function authenticateApiKey(store, header, now, logger2 = noopLogger) {
55669
55764
  const raw = extractToken(header);
55670
55765
  if (raw === null)
55671
55766
  throw new GatewayError("AUTH", "missing or malformed Authorization header");
55672
55767
  const key = await store.keys.findByHash(await hashApiKey(raw));
55673
- if (key === null || key.revokedAt !== null) {
55768
+ if (key === null || !keyUsable(key, now)) {
55674
55769
  throw new GatewayError("AUTH", "invalid API key");
55675
55770
  }
55676
55771
  const limits = key.limits;
@@ -59734,7 +59829,7 @@ async function handle(deps, rateLimiter, surface, request2) {
59734
59829
  try {
59735
59830
  let key;
59736
59831
  try {
59737
- key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers), deps.logger);
59832
+ key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers), startedAt, deps.logger);
59738
59833
  } catch (error61) {
59739
59834
  if (error61 instanceof GatewayError && error61.code === "AUTH") {
59740
59835
  deps.logger.warn("authentication rejected", {
@@ -59912,7 +60007,7 @@ function proxyRoutes(deps) {
59912
60007
  return handle(dispatchDeps, rateLimiter, "openai", request2);
59913
60008
  }).get("/v1/models", async ({ request: request2 }) => {
59914
60009
  try {
59915
- const key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers), logger2);
60010
+ const key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers), deps.now(), logger2);
59916
60011
  const snapshot = await dispatchDeps.snapshots.get(deps.now());
59917
60012
  const models = [...snapshot.models.values()];
59918
60013
  const visibleModels = key.modelAllowlist === null ? models : models.filter((model) => key.modelAllowlist?.includes(model.id));
@@ -59928,7 +60023,7 @@ function proxyRoutes(deps) {
59928
60023
  }
59929
60024
  }).post("/v1/messages/count_tokens", async ({ request: request2 }) => {
59930
60025
  try {
59931
- const key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers), logger2);
60026
+ const key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers), deps.now(), logger2);
59932
60027
  await rateLimiter.consume(key.id, key.limits, deps.requestId());
59933
60028
  const body2 = await request2.json();
59934
60029
  const chatRequest = parseAnthropicRequest(body2, request2.headers);
@@ -60779,7 +60874,8 @@ function sizeOf(payload) {
60779
60874
  return 0;
60780
60875
  }
60781
60876
  }
60782
- function createRing(limits) {
60877
+ var DEFAULT_RING_LIMITS = { frames: 500, bytes: 2 * 1024 * 1024 };
60878
+ function createRing(limits = DEFAULT_RING_LIMITS) {
60783
60879
  const topics = new Map;
60784
60880
  const stateFor = (topic) => {
60785
60881
  const existing = topics.get(topic);
@@ -60842,7 +60938,7 @@ function createRing(limits) {
60842
60938
  }
60843
60939
 
60844
60940
  // apps/gateway/src/version.ts
60845
- var VERSION = "0.11.0";
60941
+ var VERSION = "0.11.2";
60846
60942
 
60847
60943
  // apps/gateway/src/app.ts
60848
60944
  var ADMIN_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
@@ -60929,7 +61025,7 @@ function createApp(deps) {
60929
61025
  now,
60930
61026
  onDetach: (id, topics) => channelsRef?.closed(id, topics)
60931
61027
  });
60932
- const ring = deps.ring ?? createRing({ frames: 500, bytes: 2 * 1024 * 1024 });
61028
+ const ring = deps.ring ?? createRing();
60933
61029
  const broadcaster = deps.broadcaster ?? createBroadcaster({ registry: registry2, ring, coord, nodeId, now });
60934
61030
  const channels = deps.channels ?? createChannelRegistry({
60935
61031
  sockets: registry2,
@@ -60993,6 +61089,7 @@ function createApp(deps) {
60993
61089
  admin,
60994
61090
  baseUrl: deps.baseUrl,
60995
61091
  bodyLoggingAllowed: deps.bodyLoggingAllowed === true,
61092
+ ...deps.dayOffsetMinutes === undefined ? {} : { dayOffsetMinutes: deps.dayOffsetMinutes },
60996
61093
  now,
60997
61094
  sessionTtlMs: ADMIN_SESSION_TTL_MS,
60998
61095
  logger: logger2,
@@ -61004,6 +61101,7 @@ function createApp(deps) {
61004
61101
  store: deps.store,
61005
61102
  admin,
61006
61103
  sessionTtlMs: ADMIN_SESSION_TTL_MS,
61104
+ ...deps.dayOffsetMinutes === undefined ? {} : { dayOffsetMinutes: deps.dayOffsetMinutes },
61007
61105
  now,
61008
61106
  logger: logger2
61009
61107
  })).use(databaseRoutes({
@@ -61665,7 +61763,7 @@ async function sweep(deps) {
61665
61763
  let touched = 0;
61666
61764
  for (const credential of due(credentials, deps.now())) {
61667
61765
  if (!credential.hasRefreshToken) {
61668
- if (credential.expiresAt !== null && credential.expiresAt <= deps.now()) {
61766
+ if (credentialExpired(credential, deps.now())) {
61669
61767
  await deps.store.credentials.update(credential.id, {
61670
61768
  enabled: false,
61671
61769
  disabledReason: "expiredNoRefresh",
@@ -62728,7 +62826,7 @@ async function main() {
62728
62826
  now,
62729
62827
  onDetach: (id, topics) => pluginChannelsRef?.closed(id, topics)
62730
62828
  });
62731
- const streamRing = createRing({ frames: 500, bytes: 2 * 1024 * 1024 });
62829
+ const streamRing = createRing();
62732
62830
  const broadcaster = createBroadcaster({
62733
62831
  registry: streamRegistry,
62734
62832
  ring: streamRing,
@@ -62780,6 +62878,7 @@ async function main() {
62780
62878
  telemetry,
62781
62879
  ...config2.metricsToken === null ? {} : { metricsToken: config2.metricsToken },
62782
62880
  bodyLoggingAllowed: config2.bodyLoggingAllowed,
62881
+ dayOffsetMinutes: config2.dayOffsetMinutes,
62783
62882
  plugins: loadedPlugins.plugins.map((plugin) => ({ id: plugin.id, routes: plugin.routes })),
62784
62883
  pluginUi: loadedPlugins.plugins,
62785
62884
  ...loadedPlugins.plugins.length === 0 ? {} : { emit: (event) => pluginEvents.emitRequestCompleted(event) },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnigateway",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "Self-hosted AI gateway with Anthropic- and OpenAI-compatible APIs, an admin console, and a CLI",
5
5
  "license": "MIT",
6
6
  "author": "Harismawan <mail@harismawan.com>",
@@ -16,7 +16,7 @@
16
16
  "LICENSE"
17
17
  ],
18
18
  "dependencies": {
19
- "@node-rs/argon2": "2.0.2"
19
+ "@node-rs/argon2": "2.2.0"
20
20
  },
21
21
  "engines": {
22
22
  "bun": ">=1.3.0"
@@ -1,6 +1,7 @@
1
- import{m as e}from"./Rack-_A_3zqxC.js";import{jsx as t}from"react/jsx-runtime";import n from"styled-components";var r={neutral:{fg:`var(--ink-dim)`,bg:`transparent`},ok:{fg:`var(--ok)`,bg:`var(--ok-wash)`},warn:{fg:`var(--warn)`,bg:`var(--warn-wash)`},down:{fg:`var(--down)`,bg:`var(--down-wash)`},accent:{fg:`var(--accent)`,bg:`var(--accent-wash)`}},i=n.span`
1
+ import{m as e}from"./Rack-D3iFlR6e.js";import{jsx as t}from"react/jsx-runtime";import n from"styled-components";var r={neutral:{fg:`var(--ink-dim)`,bg:`transparent`},ok:{fg:`var(--ok)`,bg:`var(--ok-wash)`},warn:{fg:`var(--warn)`,bg:`var(--warn-wash)`},down:{fg:`var(--down)`,bg:`var(--down-wash)`},accent:{fg:`var(--accent)`,bg:`var(--accent-wash)`}},i=n.span`
2
2
  display: inline-flex;
3
3
  align-items: center;
4
+ line-height: 1;
4
5
  gap: 4px;
5
6
  height: 18px;
6
7
  padding: 0 6px;
@@ -1,4 +1,4 @@
1
- import{C as e,O as t}from"./Panel-BlNr9j6-.js";import{b as n}from"./Rack-_A_3zqxC.js";import{useCallback as r,useEffect as i,useRef as a,useState as o}from"react";import{jsx as s,jsxs as c}from"react/jsx-runtime";import l from"styled-components";var u=n(`check`,[[`path`,{d:`M20 6 9 17l-5-5`,key:`1gmf2c`}]]),d=n(`copy`,[[`rect`,{width:`14`,height:`14`,x:`8`,y:`8`,rx:`2`,ry:`2`,key:`17jyea`}],[`path`,{d:`M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2`,key:`zix9uf`}]]);function f(e){if(e===null||typeof getSelection!=`function`)return!1;let t=getSelection();if(t===null)return!1;try{let n=document.createRange();return n.selectNodeContents(e),t.removeAllRanges(),t.addRange(n),!0}catch{return!1}}async function p(e,t){if(typeof navigator<`u`&&navigator.clipboard!==void 0)try{return await navigator.clipboard.writeText(e),`copied`}catch{}let n=f(t??null);if(n&&typeof document.execCommand==`function`)try{if(document.execCommand(`copy`))return`copied`}catch{}return n?`selected`:`failed`}var m=l.div`
1
+ import{C as e,O as t}from"./Panel-BV18WuL1.js";import{b as n}from"./Rack-D3iFlR6e.js";import{useCallback as r,useEffect as i,useRef as a,useState as o}from"react";import{jsx as s,jsxs as c}from"react/jsx-runtime";import l from"styled-components";var u=n(`check`,[[`path`,{d:`M20 6 9 17l-5-5`,key:`1gmf2c`}]]),d=n(`copy`,[[`rect`,{width:`14`,height:`14`,x:`8`,y:`8`,rx:`2`,ry:`2`,key:`17jyea`}],[`path`,{d:`M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2`,key:`zix9uf`}]]);function f(e){if(e===null||typeof getSelection!=`function`)return!1;let t=getSelection();if(t===null)return!1;try{let n=document.createRange();return n.selectNodeContents(e),t.removeAllRanges(),t.addRange(n),!0}catch{return!1}}async function p(e,t){if(typeof navigator<`u`&&navigator.clipboard!==void 0)try{return await navigator.clipboard.writeText(e),`copied`}catch{}let n=f(t??null);if(n&&typeof document.execCommand==`function`)try{if(document.execCommand(`copy`))return`copied`}catch{}return n?`selected`:`failed`}var m=l.div`
2
2
  display: flex;
3
3
  align-items: stretch;
4
4
  gap: 0;
@@ -1,4 +1,4 @@
1
- import{_ as e}from"./Panel-BlNr9j6-.js";import{useId as t}from"react";import{jsx as n,jsxs as r}from"react/jsx-runtime";import i from"styled-components";var a=`
1
+ import{_ as e}from"./Panel-BV18WuL1.js";import{useId as t}from"react";import{jsx as n,jsxs as r}from"react/jsx-runtime";import i from"styled-components";var a=`
2
2
  width: 100%;
3
3
  border-radius: 3px;
4
4
  transition: border-color 120ms ease;