omnigateway 0.12.3 → 0.12.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -332,7 +332,10 @@ changed without a restart: the six routing weights, `maxAttempts`,
332
332
  `ponytailMode` (`off` | `lite` | `full` | `ultra`),
333
333
  and the two body-capture switches. Edit them with `omni settings set` or in the
334
334
  console. `quotaPollIntervalMs` is the one exception: the poller reads it once at
335
- boot, so a change to it takes a restart. Snapshot retention —
335
+ boot, so a change to it takes a restart. `requestDeadlineMs` defaults to `0`,
336
+ which imposes no deadline at all; what that means for whatever sits in front of
337
+ the gateway is
338
+ [Behind a reverse proxy](docs/deploying.md#behind-a-reverse-proxy). Snapshot retention —
336
339
  `snapshotKeepLatest` and `snapshotMaxAgeDays` — is stored alongside them but
337
340
  deliberately edited on the Database screen instead; see
338
341
  [Snapshots and restore](docs/operations.md#snapshots-and-restore) for why.
package/bin/omni.js CHANGED
@@ -19590,9 +19590,16 @@ function memoryCoord(options = {}) {
19590
19590
  const now = options.now ?? (() => Date.now());
19591
19591
  const windows = new Map;
19592
19592
  const gauges = new Map;
19593
+ const named = (holder) => {
19594
+ if (holder === undefined)
19595
+ return;
19596
+ if (holder === "")
19597
+ throw new TypeError("gauge holder must be a non-empty string");
19598
+ return holder;
19599
+ };
19593
19600
  const liveSlots = (key) => {
19594
19601
  const at = now();
19595
- const held = (gauges.get(key) ?? []).filter((expiresAt) => expiresAt > at);
19602
+ const held = (gauges.get(key) ?? []).filter((slot) => slot.expiresAt > at);
19596
19603
  if (held.length === 0)
19597
19604
  gauges.delete(key);
19598
19605
  else
@@ -19647,18 +19654,45 @@ function memoryCoord(options = {}) {
19647
19654
  }
19648
19655
  },
19649
19656
  gauge: {
19650
- acquire(key, ttlMs) {
19657
+ acquire(key, ttlMs, rawHolder) {
19658
+ const holder = named(rawHolder);
19651
19659
  const held = liveSlots(key);
19660
+ const expiresAt = now() + ttlMs;
19661
+ const mine = holder === undefined ? -1 : held.findIndex((s) => s.holder === holder);
19662
+ if (mine !== -1 && holder !== undefined) {
19663
+ held[mine] = { expiresAt, holder };
19664
+ gauges.set(key, held);
19665
+ return Promise.resolve(held.length - 1);
19666
+ }
19652
19667
  const before = held.length;
19653
- held.push(now() + ttlMs);
19668
+ held.push(holder === undefined ? { expiresAt } : { expiresAt, holder });
19654
19669
  gauges.set(key, held);
19655
19670
  return Promise.resolve(before);
19656
19671
  },
19657
- release(key) {
19672
+ renew(key, holder, ttlMs) {
19673
+ named(holder);
19674
+ const held = liveSlots(key);
19675
+ const mine = held.findIndex((slot) => slot.holder === holder);
19676
+ if (mine !== -1) {
19677
+ held[mine] = { expiresAt: now() + ttlMs, holder };
19678
+ gauges.set(key, held);
19679
+ }
19680
+ return Promise.resolve();
19681
+ },
19682
+ release(key, holder) {
19683
+ named(holder);
19658
19684
  const held = liveSlots(key);
19659
- held.sort((x, y) => x - y).shift();
19685
+ if (holder === undefined) {
19686
+ held.sort((x, y) => x.expiresAt - y.expiresAt).shift();
19687
+ } else {
19688
+ const mine = held.findIndex((slot) => slot.holder === holder);
19689
+ if (mine !== -1)
19690
+ held.splice(mine, 1);
19691
+ }
19660
19692
  if (held.length === 0)
19661
19693
  gauges.delete(key);
19694
+ else
19695
+ gauges.set(key, held);
19662
19696
  return Promise.resolve();
19663
19697
  },
19664
19698
  read(key) {
@@ -21194,7 +21228,7 @@ var NODE_GRACE_MS = 60000;
21194
21228
  var DEFAULT_SETTINGS = {
21195
21229
  weights: { tier: 10, health: 3, quota: 2, load: 2, cost: 1, latency: 1 },
21196
21230
  maxAttempts: 3,
21197
- requestDeadlineMs: 120000,
21231
+ requestDeadlineMs: 0,
21198
21232
  breakerThreshold: 3,
21199
21233
  breakerCooldownMs: 30000,
21200
21234
  logRetentionDays: 30,
@@ -37474,7 +37508,7 @@ function createServiceDeps(input2) {
37474
37508
  }
37475
37509
 
37476
37510
  // apps/cli/src/version.ts
37477
- var VERSION = "0.12.3";
37511
+ var VERSION = "0.12.4";
37478
37512
 
37479
37513
  // apps/cli/src/commands/service.ts
37480
37514
  function gatewayArgv(root) {
package/gateway.js CHANGED
@@ -24789,9 +24789,16 @@ function memoryCoord(options = {}) {
24789
24789
  const now = options.now ?? (() => Date.now());
24790
24790
  const windows = new Map;
24791
24791
  const gauges = new Map;
24792
+ const named = (holder) => {
24793
+ if (holder === undefined)
24794
+ return;
24795
+ if (holder === "")
24796
+ throw new TypeError("gauge holder must be a non-empty string");
24797
+ return holder;
24798
+ };
24792
24799
  const liveSlots = (key) => {
24793
24800
  const at = now();
24794
- const held = (gauges.get(key) ?? []).filter((expiresAt) => expiresAt > at);
24801
+ const held = (gauges.get(key) ?? []).filter((slot) => slot.expiresAt > at);
24795
24802
  if (held.length === 0)
24796
24803
  gauges.delete(key);
24797
24804
  else
@@ -24846,18 +24853,45 @@ function memoryCoord(options = {}) {
24846
24853
  }
24847
24854
  },
24848
24855
  gauge: {
24849
- acquire(key, ttlMs) {
24856
+ acquire(key, ttlMs, rawHolder) {
24857
+ const holder = named(rawHolder);
24850
24858
  const held = liveSlots(key);
24859
+ const expiresAt = now() + ttlMs;
24860
+ const mine = holder === undefined ? -1 : held.findIndex((s) => s.holder === holder);
24861
+ if (mine !== -1 && holder !== undefined) {
24862
+ held[mine] = { expiresAt, holder };
24863
+ gauges.set(key, held);
24864
+ return Promise.resolve(held.length - 1);
24865
+ }
24851
24866
  const before = held.length;
24852
- held.push(now() + ttlMs);
24867
+ held.push(holder === undefined ? { expiresAt } : { expiresAt, holder });
24853
24868
  gauges.set(key, held);
24854
24869
  return Promise.resolve(before);
24855
24870
  },
24856
- release(key) {
24871
+ renew(key, holder, ttlMs) {
24872
+ named(holder);
24873
+ const held = liveSlots(key);
24874
+ const mine = held.findIndex((slot) => slot.holder === holder);
24875
+ if (mine !== -1) {
24876
+ held[mine] = { expiresAt: now() + ttlMs, holder };
24877
+ gauges.set(key, held);
24878
+ }
24879
+ return Promise.resolve();
24880
+ },
24881
+ release(key, holder) {
24882
+ named(holder);
24857
24883
  const held = liveSlots(key);
24858
- held.sort((x, y) => x - y).shift();
24884
+ if (holder === undefined) {
24885
+ held.sort((x, y) => x.expiresAt - y.expiresAt).shift();
24886
+ } else {
24887
+ const mine = held.findIndex((slot) => slot.holder === holder);
24888
+ if (mine !== -1)
24889
+ held.splice(mine, 1);
24890
+ }
24859
24891
  if (held.length === 0)
24860
24892
  gauges.delete(key);
24893
+ else
24894
+ gauges.set(key, held);
24861
24895
  return Promise.resolve();
24862
24896
  },
24863
24897
  read(key) {
@@ -26626,7 +26660,7 @@ var NODE_GRACE_MS = 60000;
26626
26660
  var DEFAULT_SETTINGS = {
26627
26661
  weights: { tier: 10, health: 3, quota: 2, load: 2, cost: 1, latency: 1 },
26628
26662
  maxAttempts: 3,
26629
- requestDeadlineMs: 120000,
26663
+ requestDeadlineMs: 0,
26630
26664
  breakerThreshold: 3,
26631
26665
  breakerCooldownMs: 30000,
26632
26666
  logRetentionDays: 30,
@@ -54861,6 +54895,8 @@ var LONG_WINDOWS = ["5h", "1w"];
54861
54895
  var GRAIN_MS = { "5h": 60000, "1w": 3600000 };
54862
54896
  var SEED_LOCK_MS = 5000;
54863
54897
  var GAUGE_TTL_MS = 300000;
54898
+ var RENEW_INTERVAL_MS = 1e5;
54899
+ var MAX_RENEWED_MS = 86400000;
54864
54900
  function bucketKey(keyId, window2) {
54865
54901
  return `lim:${keyId}:${window2}`;
54866
54902
  }
@@ -54919,10 +54955,11 @@ class ApiKeyRateLimiter {
54919
54955
  }
54920
54956
  async admit(keyId, limits, requestId) {
54921
54957
  if (!anyLimit(limits))
54922
- return { release: () => {}, headroom: {} };
54958
+ return { release: () => {}, renew: async () => {}, headroom: {} };
54923
54959
  const now = this.now();
54924
54960
  this.limits.set(keyId, limits);
54925
- const claim2 = await this.claim(keyId, now, true);
54961
+ const holder = configured(limits.concurrency) ? crypto.randomUUID() : undefined;
54962
+ const claim2 = await this.claim(keyId, now, true, holder);
54926
54963
  let admitted = false;
54927
54964
  try {
54928
54965
  const counters = await this.counters(keyId, limits, claim2, now, requestId);
@@ -54930,13 +54967,29 @@ class ApiKeyRateLimiter {
54930
54967
  await this.refuse(keyId, decision, now, requestId);
54931
54968
  admitted = true;
54932
54969
  let released = false;
54970
+ const renewUntil = now + MAX_RENEWED_MS;
54971
+ const renew = async () => {
54972
+ if (released || holder === undefined)
54973
+ return;
54974
+ await this.coord.gauge.renew(keyId, holder, GAUGE_TTL_MS);
54975
+ };
54976
+ const renewal = holder === undefined ? null : setInterval(() => {
54977
+ if (this.now() >= renewUntil) {
54978
+ clearInterval(renewal ?? undefined);
54979
+ return;
54980
+ }
54981
+ renew();
54982
+ }, RENEW_INTERVAL_MS);
54983
+ renewal?.unref?.();
54933
54984
  const release = () => {
54934
54985
  if (released)
54935
54986
  return;
54936
54987
  released = true;
54937
- this.coord.gauge.release(keyId);
54988
+ if (renewal !== null)
54989
+ clearInterval(renewal);
54990
+ this.coord.gauge.release(keyId, holder);
54938
54991
  };
54939
- return { release, headroom: decision.headroom };
54992
+ return { release, renew, headroom: decision.headroom };
54940
54993
  } finally {
54941
54994
  if (!admitted)
54942
54995
  await this.rollback(keyId, claim2);
@@ -54959,15 +55012,15 @@ class ApiKeyRateLimiter {
54959
55012
  await this.rollback(keyId, claim2);
54960
55013
  }
54961
55014
  }
54962
- async claim(keyId, now, gauge) {
55015
+ async claim(keyId, now, gauge, holder) {
54963
55016
  const ring = await this.coord.window.claim(keyId, WINDOW_MS["1m"], now);
54964
- const concurrency = gauge ? await this.coord.gauge.acquire(keyId, GAUGE_TTL_MS) : await this.coord.gauge.read(keyId);
54965
- return { stamp: ring.stamp, gauge, before: { requests: ring.before, concurrency } };
55017
+ const concurrency = gauge ? await this.coord.gauge.acquire(keyId, GAUGE_TTL_MS, holder) : await this.coord.gauge.read(keyId);
55018
+ return { stamp: ring.stamp, gauge, holder, before: { requests: ring.before, concurrency } };
54966
55019
  }
54967
55020
  async rollback(keyId, claim2) {
54968
55021
  await this.coord.window.rollback(keyId, claim2.stamp);
54969
55022
  if (claim2.gauge)
54970
- await this.coord.gauge.release(keyId);
55023
+ await this.coord.gauge.release(keyId, claim2.holder);
54971
55024
  }
54972
55025
  debit(keyId, usage) {
54973
55026
  const limits = this.limits.get(keyId);
@@ -55094,6 +55147,8 @@ function withReset(headroom, window2, resetAt) {
55094
55147
  // apps/gateway/src/dispatch/loadRegistry.ts
55095
55148
  var PREFIX6 = "load:";
55096
55149
  var SLOT_TTL_MS = 300000;
55150
+ var RENEW_INTERVAL_MS2 = 1e5;
55151
+ var MAX_RENEWED_MS2 = 86400000;
55097
55152
  var EWMA_ALPHA = 0.3;
55098
55153
  function createLoadRegistry(coord = memoryCoord(), now = Date.now) {
55099
55154
  const local = new Map;
@@ -55106,12 +55161,23 @@ function createLoadRegistry(coord = memoryCoord(), now = Date.now) {
55106
55161
  local.set(key, (local.get(key) ?? 0) + 1);
55107
55162
  if (provider !== undefined)
55108
55163
  providers.set(provider, (providers.get(provider) ?? 0) + 1);
55109
- coord.gauge.acquire(PREFIX6 + key, SLOT_TTL_MS);
55164
+ const holder = crypto.randomUUID();
55165
+ coord.gauge.acquire(PREFIX6 + key, SLOT_TTL_MS, holder);
55166
+ const renewUntil = now() + MAX_RENEWED_MS2;
55167
+ const renewal = setInterval(() => {
55168
+ if (now() >= renewUntil) {
55169
+ clearInterval(renewal);
55170
+ return;
55171
+ }
55172
+ coord.gauge.renew(PREFIX6 + key, holder, SLOT_TTL_MS);
55173
+ }, RENEW_INTERVAL_MS2);
55174
+ renewal.unref?.();
55110
55175
  let released = false;
55111
55176
  return (ttftMs) => {
55112
55177
  if (released)
55113
55178
  return;
55114
55179
  released = true;
55180
+ clearInterval(renewal);
55115
55181
  const prior = history.get(key)?.ewmaTtftMs ?? null;
55116
55182
  history.set(key, {
55117
55183
  lastReleasedAt: now(),
@@ -55129,7 +55195,7 @@ function createLoadRegistry(coord = memoryCoord(), now = Date.now) {
55129
55195
  else
55130
55196
  providers.delete(provider);
55131
55197
  }
55132
- coord.gauge.release(PREFIX6 + key);
55198
+ coord.gauge.release(PREFIX6 + key, holder);
55133
55199
  };
55134
55200
  },
55135
55201
  counts() {
@@ -62302,7 +62368,7 @@ function createRing(limits = DEFAULT_RING_LIMITS) {
62302
62368
  }
62303
62369
 
62304
62370
  // apps/gateway/src/version.ts
62305
- var VERSION = "0.12.3";
62371
+ var VERSION = "0.12.4";
62306
62372
 
62307
62373
  // apps/gateway/src/app.ts
62308
62374
  var ADMIN_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
@@ -62580,19 +62646,35 @@ var WINDOW_ROLLBACK = `
62580
62646
  `;
62581
62647
  var GAUGE_ACQUIRE = `
62582
62648
  redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', ARGV[1])
62649
+ local mine = redis.call('ZSCORE', KEYS[1], ARGV[3])
62583
62650
  local before = redis.call('ZCARD', KEYS[1])
62651
+ if mine then before = before - 1 end
62584
62652
  redis.call('ZADD', KEYS[1], tonumber(ARGV[1]) + tonumber(ARGV[2]), ARGV[3])
62585
- redis.call('PEXPIRE', KEYS[1], ARGV[2])
62653
+ if redis.call('PTTL', KEYS[1]) < tonumber(ARGV[2]) then
62654
+ redis.call('PEXPIRE', KEYS[1], ARGV[2])
62655
+ end
62586
62656
  redis.call('SADD', KEYS[2], KEYS[1])
62587
62657
  return before
62588
62658
  `;
62659
+ var GAUGE_RENEW = `
62660
+ redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', ARGV[1])
62661
+ redis.call('ZADD', KEYS[1], 'XX', tonumber(ARGV[1]) + tonumber(ARGV[2]), ARGV[3])
62662
+ if redis.call('PTTL', KEYS[1]) < tonumber(ARGV[2]) then
62663
+ redis.call('PEXPIRE', KEYS[1], ARGV[2])
62664
+ end
62665
+ return 0
62666
+ `;
62589
62667
  var GAUGE_READ = `
62590
62668
  redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', ARGV[1])
62591
62669
  return redis.call('ZCARD', KEYS[1])
62592
62670
  `;
62593
62671
  var GAUGE_RELEASE = `
62594
62672
  redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', ARGV[1])
62595
- redis.call('ZPOPMIN', KEYS[1])
62673
+ if ARGV[2] == '' then
62674
+ redis.call('ZPOPMIN', KEYS[1])
62675
+ else
62676
+ redis.call('ZREM', KEYS[1], ARGV[2])
62677
+ end
62596
62678
  return 0
62597
62679
  `;
62598
62680
  var GAUGE_SNAPSHOT = `
@@ -62601,8 +62683,10 @@ var GAUGE_SNAPSHOT = `
62601
62683
  redis.call('ZREMRANGEBYSCORE', key, '-inf', ARGV[1])
62602
62684
  local count = redis.call('ZCARD', key)
62603
62685
  if count > 0 then
62604
- out[#out + 1] = key
62605
- out[#out + 1] = count
62686
+ if string.sub(key, 1, string.len(ARGV[2])) == ARGV[2] then
62687
+ out[#out + 1] = key
62688
+ out[#out + 1] = count
62689
+ end
62606
62690
  else
62607
62691
  redis.call('SREM', KEYS[1], key)
62608
62692
  end
@@ -62667,6 +62751,7 @@ var SCRIPTS = [
62667
62751
  WINDOW_CLAIM,
62668
62752
  WINDOW_ROLLBACK,
62669
62753
  GAUGE_ACQUIRE,
62754
+ GAUGE_RENEW,
62670
62755
  GAUGE_READ,
62671
62756
  GAUGE_RELEASE,
62672
62757
  GAUGE_SNAPSHOT,
@@ -62701,6 +62786,10 @@ function redisCoord(deps) {
62701
62786
  const colon = name.indexOf(":");
62702
62787
  return `${NS}gidx:${colon === -1 ? name : name.slice(0, colon + 1)}`;
62703
62788
  };
62789
+ const blankHolder = (holder) => {
62790
+ if (holder === "")
62791
+ throw new TypeError("gauge holder must be a non-empty string");
62792
+ };
62704
62793
  let faults = 0;
62705
62794
  const fault = (error62) => {
62706
62795
  healthy = false;
@@ -62836,20 +62925,28 @@ function redisCoord(deps) {
62836
62925
  }
62837
62926
  },
62838
62927
  gauge: {
62839
- acquire(name, ttlMs) {
62840
- return attempt2(async () => await evalScript(GAUGE_ACQUIRE, [key("g", name), indexOf2(name)], [now(), ttlMs, crypto.randomUUID()]), () => fallback.gauge.acquire(name, ttlMs));
62928
+ acquire(name, ttlMs, holder) {
62929
+ blankHolder(holder);
62930
+ return attempt2(async () => await evalScript(GAUGE_ACQUIRE, [key("g", name), indexOf2(name)], [now(), ttlMs, holder ?? crypto.randomUUID()]), () => fallback.gauge.acquire(name, ttlMs, holder));
62931
+ },
62932
+ renew(name, holder, ttlMs) {
62933
+ blankHolder(holder);
62934
+ return attempt2(async () => {
62935
+ await evalScript(GAUGE_RENEW, [key("g", name)], [now(), ttlMs, holder]);
62936
+ }, () => fallback.gauge.renew(name, holder, ttlMs));
62841
62937
  },
62842
- release(name) {
62938
+ release(name, holder) {
62939
+ blankHolder(holder);
62843
62940
  return attempt2(async () => {
62844
- await evalScript(GAUGE_RELEASE, [key("g", name)], [now()]);
62845
- }, () => fallback.gauge.release(name));
62941
+ await evalScript(GAUGE_RELEASE, [key("g", name)], [now(), holder ?? ""]);
62942
+ }, () => fallback.gauge.release(name, holder));
62846
62943
  },
62847
62944
  read(name) {
62848
62945
  return attempt2(async () => await evalScript(GAUGE_READ, [key("g", name)], [now()]), () => fallback.gauge.read(name));
62849
62946
  },
62850
62947
  snapshot(prefix2) {
62851
62948
  return attempt2(async () => {
62852
- const flat = await evalScript(GAUGE_SNAPSHOT, [indexOf2(prefix2)], [now()]);
62949
+ const flat = await evalScript(GAUGE_SNAPSHOT, [indexOf2(prefix2)], [now(), key("g", prefix2)]);
62853
62950
  const out = new Map;
62854
62951
  for (let i = 0;i + 1 < flat.length; i += 2) {
62855
62952
  out.set(String(flat[i]).slice(key("g", "").length), Number(flat[i + 1]));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnigateway",
3
- "version": "0.12.3",
3
+ "version": "0.12.4",
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>",