omnigateway 0.10.3 → 0.11.1

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 (43) hide show
  1. package/README.md +19 -2
  2. package/bin/omni.js +621 -333
  3. package/gateway.js +715 -395
  4. package/package.json +2 -2
  5. package/public/assets/{Chip-BeKVtewW.js → Chip-BBNaA-sL.js} +1 -1
  6. package/public/assets/{CopyValue-NTHhiCuT.js → CopyValue-DQMqneTT.js} +1 -1
  7. package/public/assets/{Lamp-C2Q3t5Mn.js → Lamp-CosPf-gw.js} +7 -7
  8. package/public/assets/Rack-BGrdPQRN.js +476 -0
  9. package/public/assets/RequestTable-B5pOnwDe.js +11 -0
  10. package/public/assets/SummaryDeck-DAvCcP_N.js +15 -0
  11. package/public/assets/{Toggle-DwKz1aVq.js → Toggle-ZS3zIMNF.js} +1 -1
  12. package/public/assets/{TokenBreakdown-DM-1HvOr.js → TokenBreakdown-VV2U0Fgv.js} +1 -1
  13. package/public/assets/{WindowChart-BsonO5Py.js → WindowChart-eqPzFE06.js} +2 -2
  14. package/public/assets/_app.accounts-wJdiS7Ue.js +59 -0
  15. package/public/assets/{_app.console-BQJMLRv4.js → _app.console-DPuEIJl7.js} +2 -2
  16. package/public/assets/{_app.database-BIiajZm6.js → _app.database-BWxxqrNy.js} +4 -4
  17. package/public/assets/_app.index-C_jGPDHc.js +67 -0
  18. package/public/assets/_app.keys-vLOZaX9s.js +92 -0
  19. package/public/assets/{_app.logs-AyVq3lLb.js → _app.logs-4fRTXz5H.js} +3 -3
  20. package/public/assets/{_app.models-CwlkGPrx.js → _app.models-BFWk7FBf.js} +4 -4
  21. package/public/assets/{_app.plugins._pluginId-BhvEnawY.js → _app.plugins._pluginId-B2R5TEp9.js} +1 -1
  22. package/public/assets/_app.settings-BKUhzFsw.js +88 -0
  23. package/public/assets/_app.usage-Dme4K70C.js +76 -0
  24. package/public/assets/{chevron-right-S84SGCvk.js → chevron-right-CL2vJUko.js} +1 -1
  25. package/public/assets/client-DIfYMjPd.js +39 -0
  26. package/public/assets/index-teScS3wF.js +13 -0
  27. package/public/assets/{login-CitmheGz.js → login-CCLJgPoH.js} +2 -2
  28. package/public/assets/plus-C3pkG04C.js +1 -0
  29. package/public/assets/{preload-helper-BC9bKUg1.js → preload-helper-Dv9CFok1.js} +1 -1
  30. package/public/assets/{shared-C47BfV_u.js → shared-5YRtk6PC.js} +12 -12
  31. package/public/assets/{trash-2-DmtElUHr.js → trash-2-CLOg_lJ7.js} +1 -1
  32. package/public/index.html +4 -4
  33. package/public/assets/Rack-sUGRoZUx.js +0 -476
  34. package/public/assets/RequestTable-C2RLARWy.js +0 -11
  35. package/public/assets/SummaryDeck-BHXcb_Ji.js +0 -15
  36. package/public/assets/_app.accounts-DUYxItf0.js +0 -59
  37. package/public/assets/_app.index-DagK0Pqp.js +0 -62
  38. package/public/assets/_app.keys-DLAG_bmt.js +0 -82
  39. package/public/assets/_app.settings-BKA-1YLs.js +0 -88
  40. package/public/assets/_app.usage-C7JEWKZM.js +0 -76
  41. package/public/assets/client-DCvpw0LS.js +0 -39
  42. package/public/assets/index-pZbadEun.js +0 -13
  43. package/public/assets/plus-E7qLz9CF.js +0 -1
package/gateway.js CHANGED
@@ -24443,13 +24443,13 @@ function prepareArtifact(input2) {
24443
24443
  error: error61.value
24444
24444
  };
24445
24445
  const json2 = JSON.stringify(bounded);
24446
- const size = encoder2.encode(json2).length;
24446
+ const size = Buffer.byteLength(json2);
24447
24447
  if (size <= MAX_ARTIFACT_BYTES)
24448
24448
  return { artifact: bounded, json: json2 };
24449
24449
  const marker = omission(size);
24450
24450
  const omitted = omitBodies(bounded, marker);
24451
24451
  const omittedJson = JSON.stringify(omitted);
24452
- if (encoder2.encode(omittedJson).length <= MAX_ARTIFACT_BYTES) {
24452
+ if (Buffer.byteLength(omittedJson) <= MAX_ARTIFACT_BYTES) {
24453
24453
  return { artifact: omitted, json: omittedJson };
24454
24454
  }
24455
24455
  const stripped = { ...omitted, error: marker };
@@ -24630,6 +24630,7 @@ var FIELD_ORDER = [
24630
24630
  "sizeBytes",
24631
24631
  "host",
24632
24632
  "port",
24633
+ "dayOffsetMinutes",
24633
24634
  "path",
24634
24635
  "supervisor",
24635
24636
  "reason"
@@ -24926,14 +24927,14 @@ function estimateCachedInputTokens(request) {
24926
24927
  return cached2;
24927
24928
  }
24928
24929
  // packages/ir/src/validate.ts
24929
- function validateRequest(req) {
24930
+ function validateRequest(req, newToolUseId = () => `tu_${crypto.randomUUID()}`) {
24930
24931
  const seenToolUseIds = new Set;
24931
24932
  const cleaned = [];
24932
24933
  for (const message of req.messages) {
24933
24934
  const content = [];
24934
24935
  for (const block of message.content) {
24935
24936
  if (block.type === "toolUse") {
24936
- const id = block.id.length > 0 ? block.id : `tu_${crypto.randomUUID()}`;
24937
+ const id = block.id.length > 0 ? block.id : newToolUseId();
24937
24938
  seenToolUseIds.add(id);
24938
24939
  content.push({ ...block, id });
24939
24940
  continue;
@@ -24953,6 +24954,261 @@ function validateRequest(req) {
24953
24954
  }
24954
24955
  return { ...req, messages: cleaned };
24955
24956
  }
24957
+ // packages/store/src/sqlite/rollup.ts
24958
+ var HOUR_MS = 3600000;
24959
+ function hourOf(at) {
24960
+ return Math.floor(at / HOUR_MS);
24961
+ }
24962
+ var DAY_MS = 86400000;
24963
+ function hostDayOffsetMinutes() {
24964
+ return -new Date().getTimezoneOffset() || 0;
24965
+ }
24966
+ function startOfDay(at, offsetMinutes) {
24967
+ const offsetMs = offsetMinutes * 60000;
24968
+ return Math.floor((at + offsetMs) / DAY_MS) * DAY_MS - offsetMs;
24969
+ }
24970
+ var UPSERT = `
24971
+ INSERT INTO usage_daily
24972
+ (day, provider, credential_id, requested_model, resolved_model, api_key_id,
24973
+ requests, errors, input_tokens, output_tokens, cache_read_tokens,
24974
+ cache_write_tokens, rtk_saved_tokens, rtk_applied_requests, cost_usd, duration_ms_sum)
24975
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
24976
+ ON CONFLICT (day, provider, credential_id, requested_model, resolved_model, api_key_id)
24977
+ DO UPDATE SET
24978
+ requests = requests + excluded.requests,
24979
+ errors = errors + excluded.errors,
24980
+ input_tokens = input_tokens + excluded.input_tokens,
24981
+ output_tokens = output_tokens + excluded.output_tokens,
24982
+ cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
24983
+ cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
24984
+ rtk_saved_tokens = rtk_saved_tokens + excluded.rtk_saved_tokens,
24985
+ rtk_applied_requests = rtk_applied_requests + excluded.rtk_applied_requests,
24986
+ cost_usd = cost_usd + excluded.cost_usd,
24987
+ duration_ms_sum = duration_ms_sum + excluded.duration_ms_sum`;
24988
+ function keyOf(log, dayOffsetMinutes) {
24989
+ return [
24990
+ startOfDay(log.at, dayOffsetMinutes),
24991
+ log.resolvedProvider ?? "",
24992
+ log.credentialId ?? "",
24993
+ log.requestedModel,
24994
+ log.resolvedModel ?? "",
24995
+ log.apiKeyId ?? ""
24996
+ ];
24997
+ }
24998
+ function countersOf(log) {
24999
+ return {
25000
+ requests: 1,
25001
+ errors: log.status >= 400 ? 1 : 0,
25002
+ inputTokens: log.inputTokens,
25003
+ outputTokens: log.outputTokens,
25004
+ cacheReadTokens: log.cacheReadTokens,
25005
+ cacheWriteTokens: log.cacheWriteTokens,
25006
+ rtkSavedTokens: log.rtkEstimatedTokensSaved,
25007
+ rtkAppliedRequests: log.rtkApplied ? 1 : 0,
25008
+ costUsd: log.costUsd,
25009
+ durationMsSum: log.durationMs
25010
+ };
25011
+ }
25012
+ var BACKFILL_UPSERT = `
25013
+ INSERT INTO usage_daily
25014
+ (day, provider, credential_id, requested_model, resolved_model, api_key_id,
25015
+ requests, errors, input_tokens, output_tokens, cache_read_tokens,
25016
+ cache_write_tokens, cost_usd, duration_ms_sum)
25017
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
25018
+ ON CONFLICT (day, provider, credential_id, requested_model, resolved_model, api_key_id)
25019
+ DO UPDATE SET
25020
+ requests = requests + excluded.requests,
25021
+ errors = errors + excluded.errors,
25022
+ input_tokens = input_tokens + excluded.input_tokens,
25023
+ output_tokens = output_tokens + excluded.output_tokens,
25024
+ cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
25025
+ cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
25026
+ cost_usd = cost_usd + excluded.cost_usd,
25027
+ duration_ms_sum = duration_ms_sum + excluded.duration_ms_sum`;
25028
+ function upsertPreRtk(db, key, c) {
25029
+ db.run(BACKFILL_UPSERT, [
25030
+ ...key,
25031
+ c.requests,
25032
+ c.errors,
25033
+ c.inputTokens,
25034
+ c.outputTokens,
25035
+ c.cacheReadTokens,
25036
+ c.cacheWriteTokens,
25037
+ c.costUsd,
25038
+ c.durationMsSum
25039
+ ]);
25040
+ }
25041
+ function upsert(db, key, c) {
25042
+ db.run(UPSERT, [
25043
+ ...key,
25044
+ c.requests,
25045
+ c.errors,
25046
+ c.inputTokens,
25047
+ c.outputTokens,
25048
+ c.cacheReadTokens,
25049
+ c.cacheWriteTokens,
25050
+ c.rtkSavedTokens,
25051
+ c.rtkAppliedRequests,
25052
+ c.costUsd,
25053
+ c.durationMsSum
25054
+ ]);
25055
+ }
25056
+ function rollupLog(db, log, dayOffsetMinutes) {
25057
+ upsert(db, keyOf(log, dayOffsetMinutes), countersOf(log));
25058
+ }
25059
+ function backfillDaily(db, dayOffsetMinutes) {
25060
+ const groups = new Map;
25061
+ for (const row of db.query(`SELECT at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
25062
+ status, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
25063
+ duration_ms, cost_usd
25064
+ FROM request_logs`).all()) {
25065
+ const key = [
25066
+ startOfDay(row.at, dayOffsetMinutes),
25067
+ row.resolved_provider ?? "",
25068
+ row.credential_id ?? "",
25069
+ row.requested_model,
25070
+ row.resolved_model ?? "",
25071
+ row.api_key_id ?? ""
25072
+ ];
25073
+ const id = key.join("\x00");
25074
+ const seen = groups.get(id);
25075
+ const counters = seen?.counters ?? {
25076
+ requests: 0,
25077
+ errors: 0,
25078
+ inputTokens: 0,
25079
+ outputTokens: 0,
25080
+ cacheReadTokens: 0,
25081
+ cacheWriteTokens: 0,
25082
+ rtkSavedTokens: 0,
25083
+ rtkAppliedRequests: 0,
25084
+ costUsd: 0,
25085
+ durationMsSum: 0
25086
+ };
25087
+ counters.requests += 1;
25088
+ counters.errors += row.status >= 400 ? 1 : 0;
25089
+ counters.inputTokens += row.input_tokens;
25090
+ counters.outputTokens += row.output_tokens;
25091
+ counters.cacheReadTokens += row.cache_read_tokens;
25092
+ counters.cacheWriteTokens += row.cache_write_tokens;
25093
+ counters.costUsd += row.cost_usd;
25094
+ counters.durationMsSum += row.duration_ms;
25095
+ if (seen === undefined)
25096
+ groups.set(id, { key, counters });
25097
+ }
25098
+ for (const group of groups.values())
25099
+ upsertPreRtk(db, group.key, group.counters);
25100
+ return groups.size;
25101
+ }
25102
+ var HOURLY_UPSERT = `
25103
+ INSERT INTO usage_rollup
25104
+ (api_key_id, hour, requests, input_tokens, output_tokens,
25105
+ cache_read_tokens, cache_write_tokens, cost_usd)
25106
+ VALUES (?,?,?,?,?,?,?,?)
25107
+ ON CONFLICT (api_key_id, hour)
25108
+ DO UPDATE SET
25109
+ requests = requests + excluded.requests,
25110
+ input_tokens = input_tokens + excluded.input_tokens,
25111
+ output_tokens = output_tokens + excluded.output_tokens,
25112
+ cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
25113
+ cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
25114
+ cost_usd = cost_usd + excluded.cost_usd`;
25115
+ function rollupHour(db, log) {
25116
+ const apiKeyId = log.apiKeyId;
25117
+ if (apiKeyId === null)
25118
+ return;
25119
+ db.run(HOURLY_UPSERT, [
25120
+ apiKeyId,
25121
+ hourOf(log.at),
25122
+ 1,
25123
+ log.inputTokens,
25124
+ log.outputTokens,
25125
+ log.cacheReadTokens,
25126
+ log.cacheWriteTokens,
25127
+ log.costUsd
25128
+ ]);
25129
+ }
25130
+ var REBUILD = `
25131
+ INSERT INTO usage_rollup
25132
+ (api_key_id, hour, requests, input_tokens, output_tokens,
25133
+ cache_read_tokens, cache_write_tokens, cost_usd)
25134
+ SELECT api_key_id,
25135
+ CAST(at / 3600000 AS INTEGER),
25136
+ COUNT(*),
25137
+ COALESCE(SUM(input_tokens), 0),
25138
+ COALESCE(SUM(output_tokens), 0),
25139
+ COALESCE(SUM(cache_read_tokens), 0),
25140
+ COALESCE(SUM(cache_write_tokens), 0),
25141
+ COALESCE(SUM(cost_usd), 0)
25142
+ FROM request_logs
25143
+ WHERE state = 'done' AND api_key_id IS NOT NULL
25144
+ GROUP BY api_key_id, CAST(at / 3600000 AS INTEGER)`;
25145
+ function rebuildRollup(db) {
25146
+ db.run("DELETE FROM usage_rollup");
25147
+ db.run(REBUILD);
25148
+ }
25149
+ function auditRollup(db) {
25150
+ const row = db.query(`WITH truth AS (
25151
+ SELECT api_key_id AS k,
25152
+ CAST(at / 3600000 AS INTEGER) AS h,
25153
+ COUNT(*) AS requests,
25154
+ COALESCE(SUM(input_tokens), 0) AS input_tokens,
25155
+ COALESCE(SUM(output_tokens), 0) AS output_tokens,
25156
+ COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
25157
+ COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,
25158
+ COALESCE(SUM(cost_usd), 0) AS cost_usd
25159
+ FROM request_logs
25160
+ WHERE state = 'done' AND api_key_id IS NOT NULL
25161
+ GROUP BY k, h
25162
+ )
25163
+ SELECT (SELECT COUNT(*) FROM truth) AS buckets,
25164
+ (SELECT COUNT(*)
25165
+ FROM truth
25166
+ LEFT JOIN usage_rollup r ON r.api_key_id = truth.k AND r.hour = truth.h
25167
+ WHERE r.api_key_id IS NULL
25168
+ OR r.requests <> truth.requests
25169
+ OR r.input_tokens <> truth.input_tokens
25170
+ OR r.output_tokens <> truth.output_tokens
25171
+ OR r.cache_read_tokens <> truth.cache_read_tokens
25172
+ OR r.cache_write_tokens <> truth.cache_write_tokens
25173
+ OR ABS(r.cost_usd - truth.cost_usd) > 1e-9 + ABS(truth.cost_usd) * 1e-9
25174
+ ) AS mismatched,
25175
+ (SELECT COUNT(*)
25176
+ FROM usage_rollup r
25177
+ LEFT JOIN truth ON r.api_key_id = truth.k AND r.hour = truth.h
25178
+ WHERE truth.k IS NULL
25179
+ ) AS orphans`).get();
25180
+ const buckets = row?.buckets ?? 0;
25181
+ const mismatched = (row?.mismatched ?? 0) + (row?.orphans ?? 0);
25182
+ return { buckets, mismatched, ok: mismatched === 0 };
25183
+ }
25184
+ function backfillRtkUsage(db, dayOffsetMinutes) {
25185
+ const groups = new Map;
25186
+ for (const row of db.query(`SELECT at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
25187
+ rtk_applied, rtk_estimated_tokens_saved
25188
+ FROM request_logs
25189
+ WHERE state = 'done'`).all()) {
25190
+ const key = [
25191
+ startOfDay(row.at, dayOffsetMinutes),
25192
+ row.resolved_provider ?? "",
25193
+ row.credential_id ?? "",
25194
+ row.requested_model,
25195
+ row.resolved_model ?? "",
25196
+ row.api_key_id ?? ""
25197
+ ];
25198
+ const id = key.join("\x00");
25199
+ const group = groups.get(id) ?? { key, saved: 0, applied: 0 };
25200
+ group.saved += row.rtk_estimated_tokens_saved;
25201
+ group.applied += row.rtk_applied === 1 ? 1 : 0;
25202
+ groups.set(id, group);
25203
+ }
25204
+ for (const { key, saved, applied } of groups.values()) {
25205
+ db.run(`UPDATE usage_daily
25206
+ SET rtk_saved_tokens = ?, rtk_applied_requests = ?
25207
+ WHERE day = ? AND provider = ? AND credential_id = ? AND requested_model = ?
25208
+ AND resolved_model = ? AND api_key_id = ?`, [saved, applied, ...key]);
25209
+ }
25210
+ }
25211
+
24956
25212
  // packages/store/src/postgres/db.ts
24957
25213
  var {SQL } = globalThis.Bun;
24958
25214
 
@@ -25258,11 +25514,95 @@ CREATE TRIGGER api_keys_config_version
25258
25514
  FOR EACH STATEMENT EXECUTE FUNCTION bump_config_version();
25259
25515
  `;
25260
25516
 
25517
+ // packages/store/src/postgres/migrations/002_health_measurements.sql
25518
+ var _002_health_measurements_default = `-- DEPLOY HAZARD \u2014 read before applying to a fleet.
25519
+ --
25520
+ -- This migration drops two columns that every gateway before it writes on
25521
+ -- the success path (\`UPSERT_HEALTH\` names \`ewma_ttft_ms\` and \`last_used_at\`).
25522
+ -- The first new replica to boot applies it under the migration lock; from that
25523
+ -- moment every OLD replica still running throws on every successful request:
25524
+ -- the upstream call has completed and been billed, the client has received its
25525
+ -- content frames, and then the connection aborts with no terminal frame. The
25526
+ -- \`RollingUpdate\` in \`k8s/deployment.yaml\` does not protect against this \u2014 the
25527
+ -- "surviving" replica is the one that breaks.
25528
+ --
25529
+ -- Required: scale to ONE replica before deploying this release, or accept that
25530
+ -- every old replica fails every successful request until it is replaced.
25531
+ -- Rollback is unsafe: an old image cannot re-add the columns, and running it
25532
+ -- against this schema fails the same way. The only way back is a snapshot
25533
+ -- restore taken before the deploy. Deliberate: kept as a drop, not
25534
+ -- expand/contract, on the operator's decision \u2014 see the write-path spec.
25535
+ --
25536
+ -- \`credential_health\` keeps decisions, not measurements.
25537
+ --
25538
+ -- \`ewma_ttft_ms\` and \`last_used_at\` were written on every successful request,
25539
+ -- and a success that changes nothing else now writes nothing. Both live in the
25540
+ -- gateway's process-local \`loadRegistry\` instead. The console's "last used"
25541
+ -- reads \`request_logs\` through \`usage.lastUsedByCredential\`, served by
25542
+ -- \`idx_request_logs_cred\`.
25543
+ ALTER TABLE credential_health DROP COLUMN ewma_ttft_ms;
25544
+ ALTER TABLE credential_health DROP COLUMN last_used_at;
25545
+
25546
+ -- The statement-level trigger bumped \`config_version\` \u2014 one row, locked until
25547
+ -- commit, by every writer on every replica \u2014 on every health write, and every
25548
+ -- bump makes every replica rebuild its routing snapshot. Only a change to a
25549
+ -- column routing *decides* on needs that; a bare \`consecutive_failures\`
25550
+ -- increment is patched into the held snapshot from the \`healthSaved\` change.
25551
+ --
25552
+ -- The INSERT arm must stay \`FOR EACH ROW\`. The write is an upsert, and a
25553
+ -- statement-level INSERT trigger fires on an upsert whichever path each row
25554
+ -- took, which would bump on every write and make the WHEN clause below moot.
25555
+ -- Row-level \`AFTER INSERT\` fires only for rows actually inserted.
25556
+ DROP TRIGGER credential_health_config_version ON credential_health;
25557
+
25558
+ CREATE TRIGGER credential_health_config_version_upd
25559
+ AFTER UPDATE ON credential_health
25560
+ FOR EACH ROW
25561
+ WHEN (OLD.breaker_state IS DISTINCT FROM NEW.breaker_state
25562
+ OR OLD.rate_limited_until IS DISTINCT FROM NEW.rate_limited_until
25563
+ OR OLD.opened_at IS DISTINCT FROM NEW.opened_at)
25564
+ EXECUTE FUNCTION bump_config_version();
25565
+
25566
+ CREATE TRIGGER credential_health_config_version_ins
25567
+ AFTER INSERT ON credential_health
25568
+ FOR EACH ROW EXECUTE FUNCTION bump_config_version();
25569
+
25570
+ -- DELETE needs no WHEN, so it stays statement-level as in 001: a credential
25571
+ -- with M model rows bumps once, not M times.
25572
+ CREATE TRIGGER credential_health_config_version_del
25573
+ AFTER DELETE ON credential_health
25574
+ FOR EACH STATEMENT EXECUTE FUNCTION bump_config_version();
25575
+ `;
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
+
25261
25597
  // packages/store/src/postgres/db.ts
25262
25598
  var num = (v) => Number(v);
25263
25599
  var numOrNull = (v) => v === null ? null : Number(v);
25264
25600
  var MIGRATION_LOCK = 7140641;
25265
- var MIGRATIONS = [{ id: 1, sql: _001_init_default }];
25601
+ var MIGRATIONS = [
25602
+ { id: 1, sql: _001_init_default },
25603
+ { id: 2, sql: _002_health_measurements_default },
25604
+ { id: 3, sql: _003_key_expiry_default }
25605
+ ];
25266
25606
  async function openPg(url2) {
25267
25607
  const sql = new SQL({ url: url2 });
25268
25608
  const conn = await sql.reserve();
@@ -25447,6 +25787,9 @@ function resolvePin(target, accounts) {
25447
25787
  return;
25448
25788
  return accounts.find((account) => servesTarget(target, account));
25449
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;
25450
25793
  var NODE_GRACE_MS = 60000;
25451
25794
  var DEFAULT_SETTINGS = {
25452
25795
  weights: { tier: 10, health: 3, quota: 2, load: 2, cost: 1, latency: 1 },
@@ -25574,31 +25917,25 @@ function toHealth(r) {
25574
25917
  breakerState: r.breaker_state,
25575
25918
  consecutiveFailures: r.consecutive_failures,
25576
25919
  openedAt: numOrNull(r.opened_at),
25577
- rateLimitedUntil: numOrNull(r.rate_limited_until),
25578
- ewmaTtftMs: r.ewma_ttft_ms,
25579
- lastUsedAt: numOrNull(r.last_used_at)
25920
+ rateLimitedUntil: numOrNull(r.rate_limited_until)
25580
25921
  };
25581
25922
  }
25582
25923
  var UPSERT_HEALTH = `INSERT INTO credential_health
25583
25924
  (credential_id, model, breaker_state, consecutive_failures, opened_at,
25584
- rate_limited_until, ewma_ttft_ms, last_used_at)
25585
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
25925
+ rate_limited_until)
25926
+ VALUES ($1,$2,$3,$4,$5,$6)
25586
25927
  ON CONFLICT (credential_id, model) DO UPDATE SET
25587
25928
  breaker_state = EXCLUDED.breaker_state,
25588
25929
  consecutive_failures = EXCLUDED.consecutive_failures,
25589
25930
  opened_at = EXCLUDED.opened_at,
25590
- rate_limited_until = EXCLUDED.rate_limited_until,
25591
- ewma_ttft_ms = EXCLUDED.ewma_ttft_ms,
25592
- last_used_at = EXCLUDED.last_used_at`;
25931
+ rate_limited_until = EXCLUDED.rate_limited_until`;
25593
25932
  var healthValues = (r) => [
25594
25933
  r.credentialId,
25595
25934
  r.model,
25596
25935
  r.breakerState,
25597
25936
  r.consecutiveFailures,
25598
25937
  r.openedAt,
25599
- r.rateLimitedUntil,
25600
- r.ewmaTtftMs,
25601
- r.lastUsedAt
25938
+ r.rateLimitedUntil
25602
25939
  ];
25603
25940
  function createCredentialRepo(sql, key, emit) {
25604
25941
  const open2 = async (v) => v === null ? null : decrypt(key, v);
@@ -25791,13 +26128,6 @@ function createCredentialRepo(sql, key, emit) {
25791
26128
  const rows = await sql.unsafe("SELECT * FROM credential_health");
25792
26129
  return rows.map(toHealth);
25793
26130
  },
25794
- async saveHealth(rows) {
25795
- await sql.begin(async (tx) => {
25796
- for (const r of rows)
25797
- await tx.unsafe(UPSERT_HEALTH, healthValues(r));
25798
- });
25799
- emit({ type: "healthSaved", rows });
25800
- },
25801
26131
  async updateHealth(credentialId, model, apply) {
25802
26132
  const written = await sql.begin(async (tx) => {
25803
26133
  await tx.unsafe("SELECT pg_advisory_xact_lock(hashtext($1))", [`${credentialId}|${model}`]);
@@ -25939,7 +26269,8 @@ var toKey = (r, logger2) => ({
25939
26269
  limits: parseLimits(r.id, r.limits, logger2),
25940
26270
  bodyLoggingOptOut: r.body_logging_opt_out,
25941
26271
  createdAt: num(r.created_at),
25942
- revokedAt: numOrNull(r.revoked_at)
26272
+ revokedAt: numOrNull(r.revoked_at),
26273
+ expiresAt: numOrNull(r.expires_at)
25943
26274
  });
25944
26275
  function createKeyRepo(sql, logger2) {
25945
26276
  const one = async (where, arg) => {
@@ -25957,8 +26288,8 @@ function createKeyRepo(sql, logger2) {
25957
26288
  async create(input2) {
25958
26289
  const now = Date.now();
25959
26290
  await sql.unsafe(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
25960
- body_logging_opt_out, created_at, revoked_at)
25961
- 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)`, [
25962
26293
  input2.id,
25963
26294
  input2.label,
25964
26295
  input2.prefix,
@@ -25966,7 +26297,8 @@ function createKeyRepo(sql, logger2) {
25966
26297
  input2.modelAllowlist === null ? null : JSON.stringify(input2.modelAllowlist),
25967
26298
  JSON.stringify(parseLimitConfig(input2.limits)),
25968
26299
  input2.bodyLoggingOptOut,
25969
- now
26300
+ now,
26301
+ input2.expiresAt
25970
26302
  ]);
25971
26303
  return { ...input2, createdAt: now, revokedAt: null };
25972
26304
  },
@@ -25974,8 +26306,8 @@ function createKeyRepo(sql, logger2) {
25974
26306
  if (row.limits === null)
25975
26307
  throw new Error(`api key ${row.id} has unreadable limits`);
25976
26308
  await sql.unsafe(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
25977
- body_logging_opt_out, created_at, revoked_at)
25978
- 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)`, [
25979
26311
  row.id,
25980
26312
  row.label,
25981
26313
  row.prefix,
@@ -25984,7 +26316,8 @@ function createKeyRepo(sql, logger2) {
25984
26316
  JSON.stringify(parseLimitConfig(row.limits)),
25985
26317
  row.bodyLoggingOptOut,
25986
26318
  row.createdAt,
25987
- row.revokedAt
26319
+ row.revokedAt,
26320
+ row.expiresAt
25988
26321
  ]);
25989
26322
  },
25990
26323
  async setLimits(id, limits) {
@@ -25999,6 +26332,9 @@ function createKeyRepo(sql, logger2) {
25999
26332
  id
26000
26333
  ]);
26001
26334
  },
26335
+ async setExpiry(id, expiresAt) {
26336
+ await sql.unsafe("UPDATE api_keys SET expires_at = $1 WHERE id = $2", [expiresAt, id]);
26337
+ },
26002
26338
  async revoke(id) {
26003
26339
  await sql.unsafe("UPDATE api_keys SET revoked_at = $1 WHERE id = $2", [Date.now(), id]);
26004
26340
  }
@@ -26297,229 +26633,6 @@ function isRtkFilterId(value) {
26297
26633
  return FILTER_IDS.has(value);
26298
26634
  }
26299
26635
 
26300
- // packages/store/src/sqlite/rollup.ts
26301
- var HOUR_MS = 3600000;
26302
- function hourOf(at) {
26303
- return Math.floor(at / HOUR_MS);
26304
- }
26305
- function startOfLocalDay(at) {
26306
- const day = new Date(at);
26307
- day.setHours(0, 0, 0, 0);
26308
- return day.getTime();
26309
- }
26310
- var UPSERT = `
26311
- INSERT INTO usage_daily
26312
- (day, provider, credential_id, requested_model, resolved_model, api_key_id,
26313
- requests, errors, input_tokens, output_tokens, cache_read_tokens,
26314
- cache_write_tokens, rtk_saved_tokens, rtk_applied_requests, cost_usd, duration_ms_sum)
26315
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
26316
- ON CONFLICT (day, provider, credential_id, requested_model, resolved_model, api_key_id)
26317
- DO UPDATE SET
26318
- requests = requests + excluded.requests,
26319
- errors = errors + excluded.errors,
26320
- input_tokens = input_tokens + excluded.input_tokens,
26321
- output_tokens = output_tokens + excluded.output_tokens,
26322
- cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
26323
- cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
26324
- rtk_saved_tokens = rtk_saved_tokens + excluded.rtk_saved_tokens,
26325
- rtk_applied_requests = rtk_applied_requests + excluded.rtk_applied_requests,
26326
- cost_usd = cost_usd + excluded.cost_usd,
26327
- duration_ms_sum = duration_ms_sum + excluded.duration_ms_sum`;
26328
- function keyOf(log) {
26329
- return [
26330
- startOfLocalDay(log.at),
26331
- log.resolvedProvider ?? "",
26332
- log.credentialId ?? "",
26333
- log.requestedModel,
26334
- log.resolvedModel ?? "",
26335
- log.apiKeyId ?? ""
26336
- ];
26337
- }
26338
- function countersOf(log) {
26339
- return {
26340
- requests: 1,
26341
- errors: log.status >= 400 ? 1 : 0,
26342
- inputTokens: log.inputTokens,
26343
- outputTokens: log.outputTokens,
26344
- cacheReadTokens: log.cacheReadTokens,
26345
- cacheWriteTokens: log.cacheWriteTokens,
26346
- rtkSavedTokens: log.rtkEstimatedTokensSaved,
26347
- rtkAppliedRequests: log.rtkApplied ? 1 : 0,
26348
- costUsd: log.costUsd,
26349
- durationMsSum: log.durationMs
26350
- };
26351
- }
26352
- function upsert(db, key, c) {
26353
- db.run(UPSERT, [
26354
- ...key,
26355
- c.requests,
26356
- c.errors,
26357
- c.inputTokens,
26358
- c.outputTokens,
26359
- c.cacheReadTokens,
26360
- c.cacheWriteTokens,
26361
- c.rtkSavedTokens,
26362
- c.rtkAppliedRequests,
26363
- c.costUsd,
26364
- c.durationMsSum
26365
- ]);
26366
- }
26367
- function rollupLog(db, log) {
26368
- upsert(db, keyOf(log), countersOf(log));
26369
- }
26370
- function backfillDaily(db) {
26371
- const groups = new Map;
26372
- for (const row of db.query(`SELECT at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
26373
- status, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
26374
- duration_ms, cost_usd
26375
- FROM request_logs`).all()) {
26376
- const key = [
26377
- startOfLocalDay(row.at),
26378
- row.resolved_provider ?? "",
26379
- row.credential_id ?? "",
26380
- row.requested_model,
26381
- row.resolved_model ?? "",
26382
- row.api_key_id ?? ""
26383
- ];
26384
- const id = key.join("\x00");
26385
- const seen = groups.get(id);
26386
- const counters = seen?.counters ?? {
26387
- requests: 0,
26388
- errors: 0,
26389
- inputTokens: 0,
26390
- outputTokens: 0,
26391
- cacheReadTokens: 0,
26392
- cacheWriteTokens: 0,
26393
- rtkSavedTokens: 0,
26394
- rtkAppliedRequests: 0,
26395
- costUsd: 0,
26396
- durationMsSum: 0
26397
- };
26398
- counters.requests += 1;
26399
- counters.errors += row.status >= 400 ? 1 : 0;
26400
- counters.inputTokens += row.input_tokens;
26401
- counters.outputTokens += row.output_tokens;
26402
- counters.cacheReadTokens += row.cache_read_tokens;
26403
- counters.cacheWriteTokens += row.cache_write_tokens;
26404
- counters.costUsd += row.cost_usd;
26405
- counters.durationMsSum += row.duration_ms;
26406
- if (seen === undefined)
26407
- groups.set(id, { key, counters });
26408
- }
26409
- for (const group of groups.values())
26410
- upsert(db, group.key, group.counters);
26411
- return groups.size;
26412
- }
26413
- var HOURLY_UPSERT = `
26414
- INSERT INTO usage_rollup
26415
- (api_key_id, hour, requests, input_tokens, output_tokens,
26416
- cache_read_tokens, cache_write_tokens, cost_usd)
26417
- VALUES (?,?,?,?,?,?,?,?)
26418
- ON CONFLICT (api_key_id, hour)
26419
- DO UPDATE SET
26420
- requests = requests + excluded.requests,
26421
- input_tokens = input_tokens + excluded.input_tokens,
26422
- output_tokens = output_tokens + excluded.output_tokens,
26423
- cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
26424
- cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
26425
- cost_usd = cost_usd + excluded.cost_usd`;
26426
- function rollupHour(db, log) {
26427
- const apiKeyId = log.apiKeyId;
26428
- if (apiKeyId === null)
26429
- return;
26430
- db.run(HOURLY_UPSERT, [
26431
- apiKeyId,
26432
- hourOf(log.at),
26433
- 1,
26434
- log.inputTokens,
26435
- log.outputTokens,
26436
- log.cacheReadTokens,
26437
- log.cacheWriteTokens,
26438
- log.costUsd
26439
- ]);
26440
- }
26441
- var REBUILD = `
26442
- INSERT INTO usage_rollup
26443
- (api_key_id, hour, requests, input_tokens, output_tokens,
26444
- cache_read_tokens, cache_write_tokens, cost_usd)
26445
- SELECT api_key_id,
26446
- CAST(at / 3600000 AS INTEGER),
26447
- COUNT(*),
26448
- COALESCE(SUM(input_tokens), 0),
26449
- COALESCE(SUM(output_tokens), 0),
26450
- COALESCE(SUM(cache_read_tokens), 0),
26451
- COALESCE(SUM(cache_write_tokens), 0),
26452
- COALESCE(SUM(cost_usd), 0)
26453
- FROM request_logs
26454
- WHERE state = 'done' AND api_key_id IS NOT NULL
26455
- GROUP BY api_key_id, CAST(at / 3600000 AS INTEGER)`;
26456
- function rebuildRollup(db) {
26457
- db.run("DELETE FROM usage_rollup");
26458
- db.run(REBUILD);
26459
- }
26460
- function auditRollup(db) {
26461
- const row = db.query(`WITH truth AS (
26462
- SELECT api_key_id AS k,
26463
- CAST(at / 3600000 AS INTEGER) AS h,
26464
- COUNT(*) AS requests,
26465
- COALESCE(SUM(input_tokens), 0) AS input_tokens,
26466
- COALESCE(SUM(output_tokens), 0) AS output_tokens,
26467
- COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
26468
- COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,
26469
- COALESCE(SUM(cost_usd), 0) AS cost_usd
26470
- FROM request_logs
26471
- WHERE state = 'done' AND api_key_id IS NOT NULL
26472
- GROUP BY k, h
26473
- )
26474
- SELECT (SELECT COUNT(*) FROM truth) AS buckets,
26475
- (SELECT COUNT(*)
26476
- FROM truth
26477
- LEFT JOIN usage_rollup r ON r.api_key_id = truth.k AND r.hour = truth.h
26478
- WHERE r.api_key_id IS NULL
26479
- OR r.requests <> truth.requests
26480
- OR r.input_tokens <> truth.input_tokens
26481
- OR r.output_tokens <> truth.output_tokens
26482
- OR r.cache_read_tokens <> truth.cache_read_tokens
26483
- OR r.cache_write_tokens <> truth.cache_write_tokens
26484
- OR ABS(r.cost_usd - truth.cost_usd) > 1e-9 + ABS(truth.cost_usd) * 1e-9
26485
- ) AS mismatched,
26486
- (SELECT COUNT(*)
26487
- FROM usage_rollup r
26488
- LEFT JOIN truth ON r.api_key_id = truth.k AND r.hour = truth.h
26489
- WHERE truth.k IS NULL
26490
- ) AS orphans`).get();
26491
- const buckets = row?.buckets ?? 0;
26492
- const mismatched = (row?.mismatched ?? 0) + (row?.orphans ?? 0);
26493
- return { buckets, mismatched, ok: mismatched === 0 };
26494
- }
26495
- function backfillRtkUsage(db) {
26496
- const groups = new Map;
26497
- for (const row of db.query(`SELECT at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
26498
- rtk_applied, rtk_estimated_tokens_saved
26499
- FROM request_logs
26500
- WHERE state = 'done'`).all()) {
26501
- const key = [
26502
- startOfLocalDay(row.at),
26503
- row.resolved_provider ?? "",
26504
- row.credential_id ?? "",
26505
- row.requested_model,
26506
- row.resolved_model ?? "",
26507
- row.api_key_id ?? ""
26508
- ];
26509
- const id = key.join("\x00");
26510
- const group = groups.get(id) ?? { key, saved: 0, applied: 0 };
26511
- group.saved += row.rtk_estimated_tokens_saved;
26512
- group.applied += row.rtk_applied === 1 ? 1 : 0;
26513
- groups.set(id, group);
26514
- }
26515
- for (const { key, saved, applied } of groups.values()) {
26516
- db.run(`UPDATE usage_daily
26517
- SET rtk_saved_tokens = ?, rtk_applied_requests = ?
26518
- WHERE day = ? AND provider = ? AND credential_id = ? AND requested_model = ?
26519
- AND resolved_model = ? AND api_key_id = ?`, [saved, applied, ...key]);
26520
- }
26521
- }
26522
-
26523
26636
  // packages/store/src/postgres/rollup.ts
26524
26637
  var DAILY_UPSERT = `
26525
26638
  INSERT INTO usage_daily
@@ -26539,9 +26652,9 @@ var DAILY_UPSERT = `
26539
26652
  rtk_applied_requests = usage_daily.rtk_applied_requests + EXCLUDED.rtk_applied_requests,
26540
26653
  cost_usd = usage_daily.cost_usd + EXCLUDED.cost_usd,
26541
26654
  duration_ms_sum = usage_daily.duration_ms_sum + EXCLUDED.duration_ms_sum`;
26542
- async function rollupLog2(conn, log) {
26655
+ async function rollupLog2(conn, log, dayOffsetMinutes) {
26543
26656
  await conn.unsafe(DAILY_UPSERT, [
26544
- startOfLocalDay(log.at),
26657
+ startOfDay(log.at, dayOffsetMinutes),
26545
26658
  log.resolvedProvider ?? "",
26546
26659
  log.credentialId ?? "",
26547
26660
  log.requestedModel,
@@ -26785,7 +26898,7 @@ var sum = (row) => ({
26785
26898
  tokens: row === undefined ? 0 : num(row.tokens),
26786
26899
  costUsd: row === undefined ? 0 : row.cost_usd
26787
26900
  });
26788
- function createUsageRepo(sql, nodeId) {
26901
+ function createUsageRepo(sql, nodeId, dayOffsetMinutes) {
26789
26902
  const complete = async (conn, log) => {
26790
26903
  const written = (await conn.unsafe(COMPLETE, values(log, "done", nodeId))).count;
26791
26904
  if (written === 0)
@@ -26793,7 +26906,7 @@ function createUsageRepo(sql, nodeId) {
26793
26906
  const stored = (await conn.unsafe("SELECT * FROM request_logs WHERE id = $1", [log.id]))[0];
26794
26907
  if (stored !== undefined) {
26795
26908
  const restored = toLog(stored);
26796
- await rollupLog2(conn, restored);
26909
+ await rollupLog2(conn, restored, dayOffsetMinutes);
26797
26910
  await rollupHour2(conn, restored);
26798
26911
  }
26799
26912
  };
@@ -26824,6 +26937,10 @@ function createUsageRepo(sql, nodeId) {
26824
26937
  ]) : await sql.unsafe("SELECT * FROM request_logs WHERE api_key_id = $1 ORDER BY at DESC LIMIT $2", [apiKeyId, limit]);
26825
26938
  return rows.map(toLog);
26826
26939
  },
26940
+ async lastUsedByCredential(credentialId) {
26941
+ const rows = await sql.unsafe("SELECT MAX(at) AS at FROM request_logs WHERE credential_id = $1", [credentialId]);
26942
+ return numOrNull(rows[0]?.at ?? null);
26943
+ },
26827
26944
  async scan(cursor, limit) {
26828
26945
  const rows = cursor === null ? await sql.unsafe("SELECT * FROM request_logs ORDER BY at, id LIMIT $1", [
26829
26946
  limit
@@ -26886,7 +27003,7 @@ function createUsageRepo(sql, nodeId) {
26886
27003
  const daily = grain === "daily";
26887
27004
  const key = columnFor(grain, q.groupBy);
26888
27005
  const split = q.splitBy === undefined ? null : columnFor(grain, q.splitBy);
26889
- const since = daily ? startOfLocalDay(q.since) : q.since;
27006
+ const since = daily ? startOfDay(q.since, dayOffsetMinutes) : q.since;
26890
27007
  const until = q.until ?? Number.MAX_SAFE_INTEGER;
26891
27008
  const timeColumn = daily ? "day" : "at";
26892
27009
  const scope = q.apiKeyId;
@@ -26962,6 +27079,7 @@ function redacted(url2) {
26962
27079
  async function createPostgresStore(opts) {
26963
27080
  const logger2 = opts.logger ?? noopLogger;
26964
27081
  const nodeId = opts.nodeId ?? crypto.randomUUID();
27082
+ const dayOffsetMinutes = opts.dayOffsetMinutes ?? hostDayOffsetMinutes();
26965
27083
  const listeners = new Set;
26966
27084
  const emit = (change) => {
26967
27085
  for (const listener of listeners) {
@@ -26993,7 +27111,7 @@ async function createPostgresStore(opts) {
26993
27111
  credentials: createCredentialRepo(sql, opts.encryptionKey, emit),
26994
27112
  config: createConfigRepo(sql, emit),
26995
27113
  keys: createKeyRepo(sql, logger2),
26996
- usage: createUsageRepo(sql, nodeId),
27114
+ usage: createUsageRepo(sql, nodeId, dayOffsetMinutes),
26997
27115
  bodies: createBodyRepo(sql, opts.encryptionKey),
26998
27116
  maintenance: createMaintenanceRepo(sql, nodeId),
26999
27117
  plugins: createPluginRepo(sql),
@@ -27230,22 +27348,18 @@ function toHealth2(r) {
27230
27348
  breakerState: r.breaker_state,
27231
27349
  consecutiveFailures: r.consecutive_failures,
27232
27350
  openedAt: r.opened_at,
27233
- rateLimitedUntil: r.rate_limited_until,
27234
- ewmaTtftMs: r.ewma_ttft_ms,
27235
- lastUsedAt: r.last_used_at
27351
+ rateLimitedUntil: r.rate_limited_until
27236
27352
  };
27237
27353
  }
27238
27354
  var UPSERT_HEALTH2 = `INSERT INTO credential_health
27239
27355
  (credential_id, model, breaker_state, consecutive_failures, opened_at,
27240
- rate_limited_until, ewma_ttft_ms, last_used_at)
27241
- VALUES (?,?,?,?,?,?,?,?)
27356
+ rate_limited_until)
27357
+ VALUES (?,?,?,?,?,?)
27242
27358
  ON CONFLICT (credential_id, model) DO UPDATE SET
27243
27359
  breaker_state = excluded.breaker_state,
27244
27360
  consecutive_failures = excluded.consecutive_failures,
27245
27361
  opened_at = excluded.opened_at,
27246
- rate_limited_until = excluded.rate_limited_until,
27247
- ewma_ttft_ms = excluded.ewma_ttft_ms,
27248
- last_used_at = excluded.last_used_at`;
27362
+ rate_limited_until = excluded.rate_limited_until`;
27249
27363
  function createCredentialRepo2(db, key, emit = () => {}) {
27250
27364
  const open2 = async (v) => v === null ? null : decrypt(key, v);
27251
27365
  const secretsFrom = async (row) => ({
@@ -27428,22 +27542,13 @@ function createCredentialRepo2(db, key, emit = () => {}) {
27428
27542
  async listHealth() {
27429
27543
  return db.query("SELECT * FROM credential_health").all().map(toHealth2);
27430
27544
  },
27431
- async saveHealth(rows) {
27432
- const stmt = db.prepare(UPSERT_HEALTH2);
27433
- db.transaction(() => {
27434
- for (const r of rows) {
27435
- stmt.run(r.credentialId, r.model, r.breakerState, r.consecutiveFailures, r.openedAt, r.rateLimitedUntil, r.ewmaTtftMs, r.lastUsedAt);
27436
- }
27437
- })();
27438
- emit({ type: "healthSaved", rows });
27439
- },
27440
27545
  async updateHealth(credentialId, model, apply) {
27441
27546
  const current = db.prepare("SELECT * FROM credential_health WHERE credential_id = ? AND model = ?");
27442
27547
  const stmt = db.prepare(UPSERT_HEALTH2);
27443
27548
  const written = db.transaction(() => {
27444
27549
  const row = current.get(credentialId, model);
27445
27550
  const next = apply(row === null ? null : toHealth2(row));
27446
- stmt.run(next.credentialId, next.model, next.breakerState, next.consecutiveFailures, next.openedAt, next.rateLimitedUntil, next.ewmaTtftMs, next.lastUsedAt);
27551
+ stmt.run(next.credentialId, next.model, next.breakerState, next.consecutiveFailures, next.openedAt, next.rateLimitedUntil);
27447
27552
  return next;
27448
27553
  })();
27449
27554
  emit({ type: "healthSaved", rows: [written] });
@@ -28006,6 +28111,63 @@ CREATE TABLE nodes (
28006
28111
  ALTER TABLE credentials ADD COLUMN token_version INTEGER NOT NULL DEFAULT 0;
28007
28112
  `;
28008
28113
 
28114
+ // packages/store/src/sqlite/migrations/013_health_measurements.sql
28115
+ var _013_health_measurements_default = `-- DEPLOY HAZARD \u2014 read before upgrading an install that is still serving.
28116
+ --
28117
+ -- This migration drops two columns that every gateway before it writes on
28118
+ -- the success path (\`UPSERT_HEALTH\` names \`ewma_ttft_ms\` and \`last_used_at\`).
28119
+ -- SQLite is single-process, so the hazard is the rollback, not the rollout: an
28120
+ -- older gateway started against this database fails every successful request
28121
+ -- after the upstream call has completed and been billed \u2014 content frames out,
28122
+ -- then an aborted connection with no terminal frame. Rolling back the binary
28123
+ -- does not roll back the schema. The only way back is a snapshot taken before
28124
+ -- the upgrade. Deliberate: kept as a drop, not expand/contract, on the
28125
+ -- operator's decision \u2014 see the write-path spec.
28126
+ --
28127
+ -- \`credential_health\` keeps decisions, not measurements.
28128
+ --
28129
+ -- \`ewma_ttft_ms\` and \`last_used_at\` were written on every successful request,
28130
+ -- and a success that changes nothing else now writes nothing. Both live in the
28131
+ -- gateway's process-local \`loadRegistry\` instead: latency is a property of
28132
+ -- *this* process's path to the provider, and the round-robin tiebreak reads
28133
+ -- the value the process holds. The console's "last used" reads
28134
+ -- \`request_logs\` through \`usage.lastUsedByCredential\`, served by
28135
+ -- \`idx_request_logs_cred\`.
28136
+ --
28137
+ -- Neither column is indexed or constrained, so \`DROP COLUMN\` is permitted.
28138
+ ALTER TABLE credential_health DROP COLUMN ewma_ttft_ms;
28139
+ ALTER TABLE credential_health DROP COLUMN last_used_at;
28140
+ `;
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
+
28009
28171
  // packages/store/src/sqlite/db.ts
28010
28172
  var MIGRATIONS2 = [
28011
28173
  { id: 1, sql: _001_init_default2 },
@@ -28019,9 +28181,16 @@ var MIGRATIONS2 = [
28019
28181
  { id: 9, sql: _009_key_limits_default },
28020
28182
  { id: 10, sql: _010_usage_rollup_default, after: rebuildRollup },
28021
28183
  { id: 11, sql: _011_plugin_migrations_default },
28022
- { id: 12, sql: _012_nodes_default }
28184
+ { id: 12, sql: _012_nodes_default },
28185
+ { id: 13, sql: _013_health_measurements_default },
28186
+ { id: 14, sql: _014_key_expiry_default }
28023
28187
  ];
28024
- function openDb(path) {
28188
+ function statements(sql) {
28189
+ return sql.split(`
28190
+ `).filter((line) => !line.trimStart().startsWith("--")).join(`
28191
+ `).split(/;[ \t]*(?:\n|$)/).map((s) => s.trim()).filter((s) => s.length > 0);
28192
+ }
28193
+ function openDb(path, dayOffsetMinutes = hostDayOffsetMinutes()) {
28025
28194
  const db = new Database(path, { create: true });
28026
28195
  db.run("PRAGMA journal_mode = WAL");
28027
28196
  db.run("PRAGMA synchronous = NORMAL");
@@ -28033,8 +28202,9 @@ function openDb(path) {
28033
28202
  if (done.has(m.id))
28034
28203
  continue;
28035
28204
  db.transaction(() => {
28036
- db.run(m.sql);
28037
- m.after?.(db);
28205
+ for (const statement of statements(m.sql))
28206
+ db.run(statement);
28207
+ m.after?.(db, dayOffsetMinutes);
28038
28208
  db.run("INSERT INTO migrations (id, applied_at) VALUES (?, ?)", [m.id, Date.now()]);
28039
28209
  })();
28040
28210
  }
@@ -28069,7 +28239,8 @@ var toKey2 = (r, logger2) => ({
28069
28239
  limits: parseLimits2(r.id, r.limits, logger2),
28070
28240
  bodyLoggingOptOut: r.body_logging_opt_out === 1,
28071
28241
  createdAt: r.created_at,
28072
- revokedAt: r.revoked_at
28242
+ revokedAt: r.revoked_at,
28243
+ expiresAt: r.expires_at
28073
28244
  });
28074
28245
  function createKeyRepo2(db, logger2 = noopLogger) {
28075
28246
  return {
@@ -28087,8 +28258,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
28087
28258
  async create(input2) {
28088
28259
  const now = Date.now();
28089
28260
  db.run(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
28090
- body_logging_opt_out, created_at, revoked_at)
28091
- VALUES (?,?,?,?,?,?,?,?,NULL)`, [
28261
+ body_logging_opt_out, created_at, revoked_at, expires_at)
28262
+ VALUES (?,?,?,?,?,?,?,?,NULL,?)`, [
28092
28263
  input2.id,
28093
28264
  input2.label,
28094
28265
  input2.prefix,
@@ -28096,7 +28267,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
28096
28267
  input2.modelAllowlist === null ? null : JSON.stringify(input2.modelAllowlist),
28097
28268
  JSON.stringify(parseLimitConfig(input2.limits)),
28098
28269
  input2.bodyLoggingOptOut ? 1 : 0,
28099
- now
28270
+ now,
28271
+ input2.expiresAt
28100
28272
  ]);
28101
28273
  return { ...input2, createdAt: now, revokedAt: null };
28102
28274
  },
@@ -28104,8 +28276,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
28104
28276
  if (row.limits === null)
28105
28277
  throw new Error(`api key ${row.id} has unreadable limits`);
28106
28278
  db.run(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
28107
- body_logging_opt_out, created_at, revoked_at)
28108
- VALUES (?,?,?,?,?,?,?,?,?)`, [
28279
+ body_logging_opt_out, created_at, revoked_at, expires_at)
28280
+ VALUES (?,?,?,?,?,?,?,?,?,?)`, [
28109
28281
  row.id,
28110
28282
  row.label,
28111
28283
  row.prefix,
@@ -28114,7 +28286,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
28114
28286
  JSON.stringify(parseLimitConfig(row.limits)),
28115
28287
  row.bodyLoggingOptOut ? 1 : 0,
28116
28288
  row.createdAt,
28117
- row.revokedAt
28289
+ row.revokedAt,
28290
+ row.expiresAt
28118
28291
  ]);
28119
28292
  },
28120
28293
  async setLimits(id, limits) {
@@ -28129,6 +28302,9 @@ function createKeyRepo2(db, logger2 = noopLogger) {
28129
28302
  id
28130
28303
  ]);
28131
28304
  },
28305
+ async setExpiry(id, expiresAt) {
28306
+ db.run("UPDATE api_keys SET expires_at = ? WHERE id = ?", [expiresAt, id]);
28307
+ },
28132
28308
  async revoke(id) {
28133
28309
  db.run("UPDATE api_keys SET revoked_at = ? WHERE id = ?", [Date.now(), id]);
28134
28310
  }
@@ -28451,14 +28627,14 @@ var COMPLETE2 = `INSERT INTO request_logs ${COLUMNS2} VALUES ${PLACEHOLDERS2}
28451
28627
  rtk_estimated_tokens_saved = excluded.rtk_estimated_tokens_saved,
28452
28628
  rtk_filters = excluded.rtk_filters
28453
28629
  WHERE request_logs.state = 'pending'`;
28454
- function createUsageRepo2(db, nodeId) {
28630
+ function createUsageRepo2(db, nodeId, dayOffsetMinutes = hostDayOffsetMinutes()) {
28455
28631
  const complete = db.transaction((log) => {
28456
28632
  if (db.run(COMPLETE2, values2(log, "done", nodeId)).changes === 0)
28457
28633
  return;
28458
28634
  const stored = db.query("SELECT * FROM request_logs WHERE id = ?").get(log.id);
28459
28635
  if (stored !== null) {
28460
28636
  const restored = toLog2(stored);
28461
- rollupLog(db, restored);
28637
+ rollupLog(db, restored, dayOffsetMinutes);
28462
28638
  rollupHour(db, restored);
28463
28639
  }
28464
28640
  });
@@ -28508,6 +28684,10 @@ function createUsageRepo2(db, nodeId) {
28508
28684
  }
28509
28685
  return db.query("SELECT * FROM request_logs WHERE api_key_id = ? ORDER BY at DESC LIMIT ?").all(apiKeyId, limit).map(toLog2);
28510
28686
  },
28687
+ async lastUsedByCredential(credentialId) {
28688
+ const row = db.query("SELECT MAX(at) AS at FROM request_logs WHERE credential_id = ?").get(credentialId);
28689
+ return row?.at ?? null;
28690
+ },
28511
28691
  async scan(cursor, limit) {
28512
28692
  const rows = cursor === null ? db.query("SELECT * FROM request_logs ORDER BY at, id LIMIT ?").all(limit) : db.query(`SELECT * FROM request_logs
28513
28693
  WHERE at > ? OR (at = ? AND id > ?)
@@ -28569,7 +28749,7 @@ function createUsageRepo2(db, nodeId) {
28569
28749
  const daily = grain === "daily";
28570
28750
  const key = columnFor2(grain, q.groupBy);
28571
28751
  const split = q.splitBy === undefined ? null : columnFor2(grain, q.splitBy);
28572
- const since = daily ? startOfLocalDay(q.since) : q.since;
28752
+ const since = daily ? startOfDay(q.since, dayOffsetMinutes) : q.since;
28573
28753
  const until = q.until ?? Number.MAX_SAFE_INTEGER;
28574
28754
  const timeColumn = daily ? "day" : "at";
28575
28755
  const scope = q.apiKeyId;
@@ -28620,6 +28800,7 @@ function createUsageRepo2(db, nodeId) {
28620
28800
  async function createStore(opts) {
28621
28801
  const logger2 = opts.logger ?? noopLogger;
28622
28802
  const nodeId = opts.nodeId ?? crypto.randomUUID();
28803
+ const dayOffsetMinutes = opts.dayOffsetMinutes ?? hostDayOffsetMinutes();
28623
28804
  const listeners = new Set;
28624
28805
  const emit = (change) => {
28625
28806
  for (const listener of listeners) {
@@ -28629,14 +28810,14 @@ async function createStore(opts) {
28629
28810
  }
28630
28811
  };
28631
28812
  const open2 = () => {
28632
- const db = openDb(opts.path);
28813
+ const db = openDb(opts.path, dayOffsetMinutes);
28633
28814
  logger2.debug("store opened", { path: opts.path });
28634
28815
  return {
28635
28816
  db,
28636
28817
  credentials: createCredentialRepo2(db, opts.encryptionKey, emit),
28637
28818
  config: createConfigRepo2(db, emit),
28638
28819
  keys: createKeyRepo2(db, logger2),
28639
- usage: createUsageRepo2(db, nodeId),
28820
+ usage: createUsageRepo2(db, nodeId, dayOffsetMinutes),
28640
28821
  bodies: createBodyRepo2(db, opts.encryptionKey, bodiesDirFor(opts.path)),
28641
28822
  maintenance: createMaintenanceRepo2(db, nodeId),
28642
28823
  plugins: createPluginRepo2(db)
@@ -28662,7 +28843,6 @@ async function createStore(opts) {
28662
28843
  updateSecrets: (id, secrets, expiresAt, expectedVersion) => handle.credentials.updateSecrets(id, secrets, expiresAt, expectedVersion),
28663
28844
  remove: (id) => handle.credentials.remove(id),
28664
28845
  listHealth: () => handle.credentials.listHealth(),
28665
- saveHealth: (rows) => handle.credentials.saveHealth(rows),
28666
28846
  updateHealth: (id, model, apply) => handle.credentials.updateHealth(id, model, apply),
28667
28847
  listQuota: () => handle.credentials.listQuota(),
28668
28848
  saveQuota: (rows) => handle.credentials.saveQuota(rows),
@@ -28689,6 +28869,7 @@ async function createStore(opts) {
28689
28869
  importRow: (row) => handle.keys.importRow(row),
28690
28870
  setLimits: (id, limits) => handle.keys.setLimits(id, limits),
28691
28871
  setModelAllowlist: (id, modelAllowlist) => handle.keys.setModelAllowlist(id, modelAllowlist),
28872
+ setExpiry: (id, expiresAt) => handle.keys.setExpiry(id, expiresAt),
28692
28873
  revoke: (id) => handle.keys.revoke(id)
28693
28874
  },
28694
28875
  usage: {
@@ -28697,6 +28878,7 @@ async function createStore(opts) {
28697
28878
  append: (log) => handle.usage.append(log),
28698
28879
  sweepPending: (now) => handle.usage.sweepPending(now),
28699
28880
  recent: (limit, apiKeyId) => handle.usage.recent(limit, apiKeyId),
28881
+ lastUsedByCredential: (credentialId) => handle.usage.lastUsedByCredential(credentialId),
28700
28882
  scan: (cursor, limit) => handle.usage.scan(cursor, limit),
28701
28883
  aggregate: (q) => handle.usage.aggregate(q),
28702
28884
  sumSince: (apiKeyId, sinceMs) => handle.usage.sumSince(apiKeyId, sinceMs),
@@ -28796,7 +28978,7 @@ function createAdminAuth(store, opts) {
28796
28978
  };
28797
28979
  const keyStillValid = async (apiKeyId) => {
28798
28980
  const key = await store.keys.get(apiKeyId);
28799
- return key !== null && key.revokedAt === null;
28981
+ return key !== null && keyUsable(key, opts.now());
28800
28982
  };
28801
28983
  const applyPassword = async (password) => {
28802
28984
  if (password.length < MIN_PASSWORD_LENGTH) {
@@ -28850,7 +29032,7 @@ function createAdminAuth(store, opts) {
28850
29032
  },
28851
29033
  async loginClient(rawKey) {
28852
29034
  const key = await store.keys.findByHash(await hashApiKey(rawKey));
28853
- if (key === null || key.revokedAt !== null)
29035
+ if (key === null || !keyUsable(key, opts.now()))
28854
29036
  return null;
28855
29037
  return issue2({ kind: "client", apiKeyId: key.id });
28856
29038
  },
@@ -29689,6 +29871,7 @@ function toClientLog(log) {
29689
29871
  var MIN_KEY_LENGTH = 16;
29690
29872
  var TRUTHY = new Set(["1", "true", "yes", "on"]);
29691
29873
  var DECIMAL_INTEGER = /^\d+$/;
29874
+ var SIGNED_INTEGER = /^-?\d+$/;
29692
29875
  function optionalText(value, fallback) {
29693
29876
  return value?.trim() || fallback;
29694
29877
  }
@@ -29719,6 +29902,11 @@ function loadConfig(env) {
29719
29902
  } else if (databaseUrl !== undefined || redisUrl !== undefined) {
29720
29903
  throw new Error("OMNI_DATABASE_URL and OMNI_REDIS_URL are cluster-mode settings; set OMNI_CLUSTER_MODE=true or unset them");
29721
29904
  }
29905
+ const rawDayOffset = env.OMNI_DAY_OFFSET_MINUTES?.trim();
29906
+ const parsedDayOffset = rawDayOffset === undefined ? NaN : Number(rawDayOffset);
29907
+ const validDayOffset = rawDayOffset !== undefined && SIGNED_INTEGER.test(rawDayOffset) && Number.isInteger(parsedDayOffset) && parsedDayOffset >= -1440 && parsedDayOffset <= 1440;
29908
+ const dayOffsetMinutes = validDayOffset ? parsedDayOffset : hostDayOffsetMinutes();
29909
+ const dayOffsetFallbackFrom = rawDayOffset !== undefined && !validDayOffset ? rawDayOffset : null;
29722
29910
  const bodyLoggingAllowed = TRUTHY.has((env.OMNI_BODY_LOGGING_ALLOWED ?? "").trim().toLowerCase());
29723
29911
  const rawLogLevel = env.OMNI_LOG_LEVEL?.trim();
29724
29912
  const logLevel = parseLogLevel(rawLogLevel);
@@ -29749,6 +29937,8 @@ function loadConfig(env) {
29749
29937
  clusterMode,
29750
29938
  databaseUrl: clusterMode ? databaseUrl ?? null : null,
29751
29939
  redisUrl: clusterMode ? redisUrl ?? null : null,
29940
+ dayOffsetMinutes,
29941
+ dayOffsetFallbackFrom,
29752
29942
  metricsToken,
29753
29943
  metricsMaxSeries,
29754
29944
  otlpEndpoint,
@@ -36448,16 +36638,19 @@ var modelSchema = exports_external.object({
36448
36638
  isAlias: exports_external.boolean(),
36449
36639
  targets: exports_external.array(targetSchema).min(1, "a virtual model needs at least one target")
36450
36640
  });
36641
+ var MAX_INSTANT = 8640000000000000;
36451
36642
  var keyCreateSchema = exports_external.object({
36452
36643
  label: exports_external.string().min(1).default("api key"),
36453
36644
  modelAllowlist: exports_external.array(exports_external.string().min(1)).nullable().default(null),
36454
36645
  limits: limitConfigSchema.default({}),
36455
- 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)
36456
36648
  }).strict();
36457
36649
  var keyLimitsSchema = exports_external.object({ limits: limitConfigSchema }).strict();
36458
36650
  var keyModelsSchema = exports_external.object({
36459
36651
  modelAllowlist: exports_external.array(exports_external.string().min(1)).nullable()
36460
36652
  }).strict();
36653
+ var keyExpirySchema = exports_external.object({ expiresAt: exports_external.number().int().positive().max(MAX_INSTANT).nullable() }).strict();
36461
36654
  var retentionSchema = exports_external.object({
36462
36655
  keepLatest: exports_external.number().int().min(1).max(100),
36463
36656
  maxAgeDays: exports_external.number().int().min(1).max(3650)
@@ -36672,10 +36865,11 @@ function eligible(input2) {
36672
36865
  drop("disabled");
36673
36866
  continue;
36674
36867
  }
36675
- if (credential.authType === "oauth" && credential.expiresAt !== null && credential.expiresAt <= now && !credential.hasRefreshToken) {
36868
+ if (credentialExpired(credential, now)) {
36676
36869
  drop("expired");
36677
36870
  continue;
36678
36871
  }
36872
+ let probe = false;
36679
36873
  const h = snapshot.health.get(healthKey(credential.id, target.model));
36680
36874
  if (h !== undefined) {
36681
36875
  if (h.rateLimitedUntil !== null && h.rateLimitedUntil > now) {
@@ -36688,6 +36882,9 @@ function eligible(input2) {
36688
36882
  drop("breaker:open");
36689
36883
  continue;
36690
36884
  }
36885
+ probe = true;
36886
+ } else if (h.breakerState === "halfOpen") {
36887
+ probe = true;
36691
36888
  }
36692
36889
  }
36693
36890
  const spent = (snapshot.quota.get(credential.id) ?? []).find((w) => w.limit !== null && w.used >= w.limit && (w.resetsAt === null || w.resetsAt > now));
@@ -36695,7 +36892,7 @@ function eligible(input2) {
36695
36892
  drop(`quota:${spent.windowType}`);
36696
36893
  continue;
36697
36894
  }
36698
- pairs.push({ credential, target });
36895
+ pairs.push({ credential, target, probe });
36699
36896
  }
36700
36897
  if (target.credentialId === undefined && providerSeen && !servedAny) {
36701
36898
  excluded.push({
@@ -36772,7 +36969,7 @@ function healthScore(h) {
36772
36969
  return h?.breakerState === "open" || h?.breakerState === "halfOpen" ? base * 0.5 : base;
36773
36970
  }
36774
36971
  function score(pairs, input2) {
36775
- const { snapshot, now, load } = input2;
36972
+ const { snapshot, now, load, recent } = input2;
36776
36973
  const w = snapshot.settings.weights;
36777
36974
  const tiers = pairs.map((p) => p.target.tier);
36778
36975
  const minTier = Math.min(...tiers);
@@ -36780,9 +36977,10 @@ function score(pairs, input2) {
36780
36977
  const tokens2 = estimateTokens(input2.request);
36781
36978
  const costs = pairs.map((p) => requestCost(p.target, tokens2));
36782
36979
  const bestCost = bestPositive(costs);
36980
+ const ewmaOf = (key) => recent?.get(key)?.ewmaTtftMs ?? null;
36783
36981
  const bestLatency = bestPositive(pairs.flatMap((p) => {
36784
- const h = snapshot.health.get(healthKey(p.credential.id, p.target.model));
36785
- return h?.ewmaTtftMs != null ? [h.ewmaTtftMs] : [];
36982
+ const ewma = ewmaOf(healthKey(p.credential.id, p.target.model));
36983
+ return ewma === null ? [] : [ewma];
36786
36984
  }));
36787
36985
  return pairs.map((pair, i) => {
36788
36986
  const key = healthKey(pair.credential.id, pair.target.model);
@@ -36794,12 +36992,14 @@ function score(pairs, input2) {
36794
36992
  const quota = quotaHeadroom(pair.credential, snapshot.quota.get(pair.credential.id) ?? [], now, snapshot.settings.quotaPollIntervalMs);
36795
36993
  const ownCost = costs[i];
36796
36994
  const cost = ownCost <= 0 || bestCost === null ? UNKNOWN : ratio(ownCost, bestCost);
36797
- const latency = h?.ewmaTtftMs == null || h.ewmaTtftMs <= 0 || bestLatency === null ? UNKNOWN : ratio(h.ewmaTtftMs, bestLatency);
36995
+ const ewma = ewmaOf(key);
36996
+ const latency = ewma === null || ewma <= 0 || bestLatency === null ? UNKNOWN : ratio(ewma, bestLatency);
36798
36997
  const reasons = { tier, health, quota, cost, latency, load: loadTerm };
36799
36998
  const base = tier * w.tier + health * w.health + quota * w.quota + cost * w.cost + latency * w.latency + loadTerm * w.load;
36800
36999
  return {
36801
37000
  credential: pair.credential,
36802
37001
  target: pair.target,
37002
+ probe: pair.probe,
36803
37003
  score: base * pair.credential.weight * pair.target.weight,
36804
37004
  reasons
36805
37005
  };
@@ -36825,7 +37025,6 @@ var PENALTY = {
36825
37025
  ALL_CANDIDATES_FAILED: "none",
36826
37026
  INTERNAL: "none"
36827
37027
  };
36828
- var EWMA_ALPHA = 0.3;
36829
37028
  var DEFAULT_RATE_LIMIT_MS = 60000;
36830
37029
  var QUOTA_PARK_MS = 3600000;
36831
37030
  var MAX_JITTER_MS = 2000;
@@ -36836,21 +37035,28 @@ function blankHealth(credentialId, model) {
36836
37035
  breakerState: "closed",
36837
37036
  consecutiveFailures: 0,
36838
37037
  openedAt: null,
36839
- rateLimitedUntil: null,
36840
- ewmaTtftMs: null,
36841
- lastUsedAt: null
37038
+ rateLimitedUntil: null
36842
37039
  };
36843
37040
  }
36844
- function recordSuccess(current, opts) {
36845
- const ewma = opts.ttftMs === null ? current.ewmaTtftMs : current.ewmaTtftMs === null ? opts.ttftMs : current.ewmaTtftMs * (1 - EWMA_ALPHA) + opts.ttftMs * EWMA_ALPHA;
37041
+ var SUCCESS_RESETS = [
37042
+ "breakerState",
37043
+ "consecutiveFailures",
37044
+ "openedAt",
37045
+ "rateLimitedUntil"
37046
+ ];
37047
+ function successWouldChange(current) {
37048
+ if (current === undefined)
37049
+ return false;
37050
+ const blank = blankHealth(current.credentialId, current.model);
37051
+ return SUCCESS_RESETS.some((field) => current[field] !== blank[field]);
37052
+ }
37053
+ function recordSuccess(current) {
36846
37054
  return {
36847
37055
  ...current,
36848
37056
  breakerState: "closed",
36849
37057
  consecutiveFailures: 0,
36850
37058
  openedAt: null,
36851
- rateLimitedUntil: null,
36852
- ewmaTtftMs: ewma,
36853
- lastUsedAt: opts.now
37059
+ rateLimitedUntil: null
36854
37060
  };
36855
37061
  }
36856
37062
  function recordFailure(current, opts) {
@@ -36860,7 +37066,7 @@ function recordFailure(current, opts) {
36860
37066
  if (penalty === "soft") {
36861
37067
  const base = opts.code === "QUOTA_EXHAUSTED" ? QUOTA_PARK_MS : opts.retryAfterMs ?? DEFAULT_RATE_LIMIT_MS;
36862
37068
  const until = opts.now + base + Math.round((opts.jitter ?? 0) * MAX_JITTER_MS);
36863
- return { ...current, rateLimitedUntil: until, lastUsedAt: opts.now };
37069
+ return { ...current, rateLimitedUntil: until };
36864
37070
  }
36865
37071
  const failures = current.consecutiveFailures + 1;
36866
37072
  const open2 = opts.code === "AUTH" || current.breakerState === "halfOpen" || failures >= opts.settings.breakerThreshold;
@@ -36868,8 +37074,7 @@ function recordFailure(current, opts) {
36868
37074
  ...current,
36869
37075
  consecutiveFailures: failures,
36870
37076
  breakerState: open2 ? "open" : "closed",
36871
- openedAt: open2 ? opts.now : current.openedAt,
36872
- lastUsedAt: opts.now
37077
+ openedAt: open2 ? opts.now : current.openedAt
36873
37078
  };
36874
37079
  }
36875
37080
  // packages/router/src/resolve.ts
@@ -36950,8 +37155,8 @@ function rank(input2) {
36950
37155
  break;
36951
37156
  case "roundRobin": {
36952
37157
  const idle = (c) => {
36953
- const h = input2.snapshot.health.get(healthKey(c.credential.id, c.target.model));
36954
- return h?.lastUsedAt == null ? Number.POSITIVE_INFINITY : input2.now - h.lastUsedAt;
37158
+ const at = input2.recent?.get(healthKey(c.credential.id, c.target.model))?.lastReleasedAt;
37159
+ return at === undefined ? Number.POSITIVE_INFINITY : input2.now - at;
36955
37160
  };
36956
37161
  const spent = (c) => headroom(c) < QUOTA_FLOOR ? 1 : 0;
36957
37162
  scored.sort((a, b) => spent(a) - spent(b) || inflight(a) - inflight(b) || idle(b) - idle(a));
@@ -37107,16 +37312,23 @@ async function createApiKeyCredential(store, input2, logger2 = noopLogger, provi
37107
37312
  return summarizeCredential(created);
37108
37313
  }
37109
37314
  async function credentialHealth(deps) {
37110
- const [health, quota, settings] = await Promise.all([
37315
+ const [health, quota, settings, credentials] = await Promise.all([
37111
37316
  deps.store.credentials.listHealth(),
37112
37317
  deps.store.credentials.listQuota(),
37113
- deps.store.config.getSettings()
37318
+ deps.store.config.getSettings(),
37319
+ deps.store.credentials.list()
37114
37320
  ]);
37321
+ const lastUsed = {};
37322
+ for (const c of credentials) {
37323
+ const at = await deps.store.usage.lastUsedByCredential(c.id);
37324
+ if (at !== null)
37325
+ lastUsed[c.id] = at;
37326
+ }
37115
37327
  const burn = burnEstimates(quota, {
37116
37328
  now: deps.now(),
37117
37329
  pollIntervalMs: settings.quotaPollIntervalMs
37118
37330
  });
37119
- return { health, quota, burn };
37331
+ return { health, quota, burn, lastUsed };
37120
37332
  }
37121
37333
  async function patchCredential(deps, id, input2) {
37122
37334
  const patch = parseOrThrow(credentialPatchSchema, input2);
@@ -37228,14 +37440,14 @@ function listSnapshots(deps) {
37228
37440
  }
37229
37441
  return snapshots.sort((a, b) => b.createdAt - a.createdAt);
37230
37442
  }
37231
- var DAY_MS = 86400000;
37443
+ var DAY_MS2 = 86400000;
37232
37444
  var SNAPSHOT_HEADROOM = 1.1;
37233
37445
  function retentionOf(settings) {
37234
37446
  return { keepLatest: settings.snapshotKeepLatest, maxAgeDays: settings.snapshotMaxAgeDays };
37235
37447
  }
37236
37448
  function applyRetention(deps, policy, now) {
37237
37449
  const dir = snapshotsDir(deps);
37238
- const cutoff = now - policy.maxAgeDays * DAY_MS;
37450
+ const cutoff = now - policy.maxAgeDays * DAY_MS2;
37239
37451
  const snapshots = listSnapshots(deps);
37240
37452
  const undo = snapshots.find((snapshot) => snapshot.reason === "preRestore")?.id;
37241
37453
  let removed = 0;
@@ -37599,7 +37811,8 @@ async function toSummary(store, key, now) {
37599
37811
  limitUsage: await readLimitUsage(store, key.id, key.limits, now),
37600
37812
  bodyLoggingOptOut: key.bodyLoggingOptOut,
37601
37813
  createdAt: key.createdAt,
37602
- revokedAt: key.revokedAt
37814
+ revokedAt: key.revokedAt,
37815
+ expiresAt: key.expiresAt
37603
37816
  };
37604
37817
  }
37605
37818
  async function listKeys(store, now = Date.now()) {
@@ -37622,7 +37835,8 @@ async function createKey(store, input2) {
37622
37835
  hash: await hashApiKey(raw),
37623
37836
  modelAllowlist: body2.modelAllowlist,
37624
37837
  limits: body2.limits,
37625
- bodyLoggingOptOut: body2.bodyLoggingOptOut
37838
+ bodyLoggingOptOut: body2.bodyLoggingOptOut,
37839
+ expiresAt: body2.expiresAt
37626
37840
  });
37627
37841
  return { id: created.id, label: created.label, prefix: created.prefix, key: raw };
37628
37842
  }
@@ -37642,6 +37856,14 @@ async function setKeyModels(store, id, input2, now = Date.now()) {
37642
37856
  await store.keys.setModelAllowlist(id, body2.modelAllowlist);
37643
37857
  return toSummary(store, { ...key, modelAllowlist: body2.modelAllowlist }, now);
37644
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
+ }
37645
37867
  async function revokeKey(store, id) {
37646
37868
  await store.keys.revoke(id);
37647
37869
  }
@@ -38490,12 +38712,18 @@ async function accountQuota(deps) {
38490
38712
  }
38491
38713
 
38492
38714
  // packages/control/src/quota/history.ts
38493
- var DAY_MS2 = 24 * 60 * 60 * 1000;
38715
+ var DAY_MS3 = 24 * 60 * 60 * 1000;
38494
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
+ }
38495
38723
  async function retainedSpan(deps, input2, maxSpanMs) {
38496
38724
  const now = deps.now();
38497
38725
  const settings = await deps.store.config.getSettings();
38498
- const retained = now - settings.logRetentionDays * DAY_MS2;
38726
+ const retained = now - settings.logRetentionDays * DAY_MS3;
38499
38727
  const oldest = maxSpanMs === undefined ? retained : Math.max(retained, now - maxSpanMs);
38500
38728
  return {
38501
38729
  since: Math.max(optionalNumber(input2.since, oldest), oldest),
@@ -38538,26 +38766,26 @@ async function quotaHistory(deps, input2) {
38538
38766
  deps.store.credentials.listQuotaSamples({
38539
38767
  since,
38540
38768
  until,
38769
+ limit: SAMPLE_QUERY_LIMIT,
38541
38770
  ...credentialId === undefined ? {} : { credentialId }
38542
38771
  }),
38543
38772
  deps.store.credentials.listQuota()
38544
38773
  ]);
38545
38774
  const scoped = credentialId === undefined ? quota : quota.filter((w) => w.credentialId === credentialId);
38546
- 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) };
38547
38777
  }
38548
38778
 
38549
38779
  // packages/control/src/quota/clientHistory.ts
38550
38780
  var MAX_SPAN_MS = 16 * 24 * 60 * 60 * 1000;
38551
- var MAX_SAMPLES = 50000;
38552
38781
  async function accountQuotaHistory(deps, input2) {
38553
38782
  const { since, until } = await retainedSpan(deps, input2, MAX_SPAN_MS);
38554
38783
  const [samples, credentials] = await Promise.all([
38555
- deps.store.credentials.listQuotaSamples({ since, until, limit: MAX_SAMPLES + 1 }),
38784
+ deps.store.credentials.listQuotaSamples({ since, until, limit: SAMPLE_QUERY_LIMIT }),
38556
38785
  deps.store.credentials.list()
38557
38786
  ]);
38558
38787
  const byId = new Map(credentials.map((credential) => [credential.id, credential]));
38559
- const truncated = samples.length > MAX_SAMPLES;
38560
- const page = truncated ? samples.slice(0, MAX_SAMPLES) : samples;
38788
+ const { page, truncated } = pageSamples(samples);
38561
38789
  const out = [];
38562
38790
  for (const sample of page) {
38563
38791
  const credential = byId.get(sample.credentialId);
@@ -53559,9 +53787,11 @@ function withReset(headroom, window2, resetAt) {
53559
53787
  // apps/gateway/src/dispatch/loadRegistry.ts
53560
53788
  var PREFIX6 = "load:";
53561
53789
  var SLOT_TTL_MS = 300000;
53562
- function createLoadRegistry(coord = memoryCoord()) {
53790
+ var EWMA_ALPHA = 0.3;
53791
+ function createLoadRegistry(coord = memoryCoord(), now = Date.now) {
53563
53792
  const local = new Map;
53564
53793
  const providers = new Map;
53794
+ const history = new Map;
53565
53795
  let remote = new Map;
53566
53796
  return {
53567
53797
  acquire(credentialId, model, provider) {
@@ -53571,10 +53801,15 @@ function createLoadRegistry(coord = memoryCoord()) {
53571
53801
  providers.set(provider, (providers.get(provider) ?? 0) + 1);
53572
53802
  coord.gauge.acquire(PREFIX6 + key, SLOT_TTL_MS);
53573
53803
  let released = false;
53574
- return () => {
53804
+ return (ttftMs) => {
53575
53805
  if (released)
53576
53806
  return;
53577
53807
  released = true;
53808
+ const prior = history.get(key)?.ewmaTtftMs ?? null;
53809
+ history.set(key, {
53810
+ lastReleasedAt: now(),
53811
+ ewmaTtftMs: ttftMs === undefined || ttftMs === null ? prior : prior === null ? ttftMs : prior * (1 - EWMA_ALPHA) + ttftMs * EWMA_ALPHA
53812
+ });
53578
53813
  const next = (local.get(key) ?? 0) - 1;
53579
53814
  if (next > 0)
53580
53815
  local.set(key, next);
@@ -53601,6 +53836,9 @@ function createLoadRegistry(coord = memoryCoord()) {
53601
53836
  localCounts() {
53602
53837
  return new Map(providers);
53603
53838
  },
53839
+ recent() {
53840
+ return history;
53841
+ },
53604
53842
  async refresh() {
53605
53843
  const held = await coord.gauge.snapshot(PREFIX6);
53606
53844
  const sample = new Map;
@@ -54969,6 +55207,11 @@ function adminRoutes(deps) {
54969
55207
  const key = await setKeyModels(deps.store, params.id, await readJson(request2));
54970
55208
  changed("res:keys");
54971
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;
54972
55215
  }).delete("/api/keys/:id", async ({ request: request2, params }) => {
54973
55216
  await requireAdmin(request2, deps.admin);
54974
55217
  await revokeKey(deps.store, params.id);
@@ -54979,6 +55222,7 @@ function adminRoutes(deps) {
54979
55222
  return {
54980
55223
  settings: await getSettings(deps.store),
54981
55224
  bodyLoggingAllowed: deps.bodyLoggingAllowed === true,
55225
+ dayOffsetMinutes: deps.dayOffsetMinutes ?? hostDayOffsetMinutes(),
54982
55226
  viewerConfigured: await deps.admin.isViewerConfigured()
54983
55227
  };
54984
55228
  }).put("/api/settings/password", async ({ request: request2 }) => {
@@ -55113,7 +55357,10 @@ function clientRoutes(deps) {
55113
55357
  return { ok: true };
55114
55358
  }).get("/api/client/summary", async ({ request: request2 }) => {
55115
55359
  const apiKeyId = await requireClient(request2, deps.admin);
55116
- return readOwnKey(deps.store, apiKeyId, now());
55360
+ return {
55361
+ ...await readOwnKey(deps.store, apiKeyId, now()),
55362
+ dayOffsetMinutes: deps.dayOffsetMinutes ?? hostDayOffsetMinutes()
55363
+ };
55117
55364
  }).get("/api/client/usage", async ({ request: request2, query }) => {
55118
55365
  const apiKeyId = await requireClient(request2, deps.admin);
55119
55366
  return queryUsage({ store: deps.store, now }, {
@@ -55513,12 +55760,12 @@ function injectPonytail(request2, opts) {
55513
55760
  }
55514
55761
 
55515
55762
  // apps/gateway/src/auth/apiKey.ts
55516
- async function authenticateApiKey(store, header, logger2 = noopLogger) {
55763
+ async function authenticateApiKey(store, header, now, logger2 = noopLogger) {
55517
55764
  const raw = extractToken(header);
55518
55765
  if (raw === null)
55519
55766
  throw new GatewayError("AUTH", "missing or malformed Authorization header");
55520
55767
  const key = await store.keys.findByHash(await hashApiKey(raw));
55521
- if (key === null || key.revokedAt !== null) {
55768
+ if (key === null || !keyUsable(key, now)) {
55522
55769
  throw new GatewayError("AUTH", "invalid API key");
55523
55770
  }
55524
55771
  const limits = key.limits;
@@ -57826,6 +58073,7 @@ function priceOf(prices, usage, provider, providers = PROVIDER_DESCRIPTORS) {
57826
58073
  }
57827
58074
 
57828
58075
  // apps/gateway/src/dispatch/index.ts
58076
+ var PROBE_TTL_MS = 180000;
57829
58077
  async function dispatch(request2, deps, signal, requestId) {
57830
58078
  const logger2 = deps.logger ?? noopLogger;
57831
58079
  const startedAt = deps.now();
@@ -57919,6 +58167,7 @@ async function dispatch(request2, deps, signal, requestId) {
57919
58167
  now: startedAt,
57920
58168
  rand: deps.rand(),
57921
58169
  load: deps.loadRegistry.counts(),
58170
+ recent: deps.loadRegistry.recent(),
57922
58171
  providers: deps.providers
57923
58172
  });
57924
58173
  if (deps.trace !== null && deps.trace !== undefined) {
@@ -57980,8 +58229,11 @@ async function dispatch(request2, deps, signal, requestId) {
57980
58229
  async function* run() {
57981
58230
  try {
57982
58231
  let lastError = null;
58232
+ let attempts = 0;
57983
58233
  candidateLoop:
57984
- for (let i = 0;i < maxAttempts; i++) {
58234
+ for (const candidate of candidates) {
58235
+ if (attempts >= maxAttempts)
58236
+ break;
57985
58237
  try {
57986
58238
  checkCancellation();
57987
58239
  } catch (error61) {
@@ -57990,22 +58242,45 @@ async function dispatch(request2, deps, signal, requestId) {
57990
58242
  lastError = error61 instanceof GatewayError ? error61 : new GatewayError("TIMEOUT", "request deadline exceeded");
57991
58243
  break;
57992
58244
  }
57993
- const candidate = candidates[i];
57994
58245
  const adoptable = eagerHeld && eager.credentialId === candidate.credential.id && eager.model === candidate.target.model;
57995
58246
  if (adoptable)
57996
58247
  eagerHeld = false;
57997
58248
  const releaseSlot = adoptable ? eager.release : deps.loadRegistry.acquire(candidate.credential.id, candidate.target.model, candidate.target.provider);
58249
+ const successWrites = successWouldChange(snapshot.health.get(healthKey(candidate.credential.id, candidate.target.model)));
57998
58250
  const attemptStartedAt = deps.now();
57999
58251
  let attemptCode;
58000
- if (deps.trace !== null && deps.trace !== undefined) {
58001
- deps.trace.activeAttempt = addSpan(deps.trace, "dispatch.attempt", 0, attemptStartedAt - startedAt, attemptStartedAt - startedAt, {
58002
- attempt: i + 1,
58003
- provider: candidate.target.provider,
58004
- model: candidate.target.model,
58005
- credential_id: candidate.credential.id
58006
- });
58007
- }
58252
+ let releaseProbe;
58253
+ let probing = false;
58008
58254
  try {
58255
+ if (candidate.probe) {
58256
+ const key = `probe:${healthKey(candidate.credential.id, candidate.target.model)}`;
58257
+ const held = await deps.coord.gauge.acquire(key, PROBE_TTL_MS);
58258
+ releaseProbe = () => deps.coord.gauge.release(key);
58259
+ if (held > 0) {
58260
+ noteDegradations([`excluded:${candidate.credential.id}:breaker:probing`]);
58261
+ logger2.debug("routing candidate excluded", {
58262
+ requestId,
58263
+ credentialId: candidate.credential.id,
58264
+ reason: "breaker:probing"
58265
+ });
58266
+ continue;
58267
+ }
58268
+ await persistHealth(candidate, (current) => {
58269
+ if (current.breakerState === "closed")
58270
+ return current;
58271
+ probing = true;
58272
+ return { ...current, breakerState: "halfOpen" };
58273
+ });
58274
+ }
58275
+ const i = attempts++;
58276
+ if (deps.trace !== null && deps.trace !== undefined) {
58277
+ deps.trace.activeAttempt = addSpan(deps.trace, "dispatch.attempt", 0, attemptStartedAt - startedAt, attemptStartedAt - startedAt, {
58278
+ attempt: i + 1,
58279
+ provider: candidate.target.provider,
58280
+ model: candidate.target.model,
58281
+ credential_id: candidate.credential.id
58282
+ });
58283
+ }
58009
58284
  log.attempts = i + 1;
58010
58285
  log.credentialId = candidate.credential.id;
58011
58286
  log.resolvedProvider = candidate.target.provider;
@@ -58028,6 +58303,7 @@ async function dispatch(request2, deps, signal, requestId) {
58028
58303
  log.cacheWriteTokens = 0;
58029
58304
  log.ttftMs = null;
58030
58305
  let committed = false;
58306
+ let closedAtCommit = false;
58031
58307
  let authRefreshRetried = false;
58032
58308
  let retrySecrets;
58033
58309
  const attemptNow = deps.now();
@@ -58082,6 +58358,11 @@ async function dispatch(request2, deps, signal, requestId) {
58082
58358
  for (const buffered of pending)
58083
58359
  yield buffered;
58084
58360
  pending.length = 0;
58361
+ if (probing) {
58362
+ await persistHealth(candidate, recordSuccess);
58363
+ probing = false;
58364
+ closedAtCommit = true;
58365
+ }
58085
58366
  }
58086
58367
  if (event.type === "end") {
58087
58368
  terminal = true;
@@ -58139,11 +58420,8 @@ async function dispatch(request2, deps, signal, requestId) {
58139
58420
  if (!terminal) {
58140
58421
  throw new GatewayError("UPSTREAM", "upstream stream ended without a terminal event");
58141
58422
  }
58142
- await persistHealth(candidate, (current) => recordSuccess(current, {
58143
- settings: snapshot.settings,
58144
- now: deps.now(),
58145
- ttftMs: log.ttftMs
58146
- }));
58423
+ if (successWrites && !closedAtCommit)
58424
+ await persistHealth(candidate, recordSuccess);
58147
58425
  log.status = 200;
58148
58426
  log.errorCode = null;
58149
58427
  log.durationMs = deps.now() - startedAt;
@@ -58196,7 +58474,7 @@ async function dispatch(request2, deps, signal, requestId) {
58196
58474
  ...failure.retryAfterMs === undefined ? {} : { retryAfterMs: failure.retryAfterMs },
58197
58475
  jitter: deps.rand()
58198
58476
  }));
58199
- if (!committed && RETRYABLE[failure.code] && i + 1 < maxAttempts) {
58477
+ if (!committed && RETRYABLE[failure.code] && attempts < maxAttempts) {
58200
58478
  logger2.warn("attempt failed; retrying", {
58201
58479
  requestId,
58202
58480
  provider: candidate.target.provider,
@@ -58236,14 +58514,16 @@ async function dispatch(request2, deps, signal, requestId) {
58236
58514
  }
58237
58515
  deps.trace.activeAttempt = null;
58238
58516
  }
58239
- releaseSlot();
58517
+ releaseSlot(log.ttftMs);
58518
+ releaseProbe?.();
58240
58519
  }
58241
58520
  }
58242
- const code = lastError?.code === "TIMEOUT" ? "TIMEOUT" : lastError !== null && !RETRYABLE[lastError.code] ? lastError.code : "ALL_CANDIDATES_FAILED";
58521
+ const untried = attempts === 0 && lastError === null;
58522
+ const code = lastError?.code === "TIMEOUT" ? "TIMEOUT" : lastError !== null && !RETRYABLE[lastError.code] ? lastError.code : untried ? "NO_CANDIDATES" : "ALL_CANDIDATES_FAILED";
58243
58523
  log.status = HTTP_STATUS[code];
58244
58524
  log.errorCode = code;
58245
58525
  log.durationMs = deps.now() - startedAt;
58246
- const message = lastError?.message ?? "all candidates failed";
58526
+ const message = lastError?.message ?? (untried ? `no eligible credential for model "${safeToken(request2.model)}"` : "all candidates failed");
58247
58527
  reject(lastError?.provider === undefined ? new GatewayError(code, message) : new GatewayError(code, message, {
58248
58528
  provider: lastError.provider,
58249
58529
  ...lastError.gatewayAuthored ? { gatewayAuthored: true } : {}
@@ -59549,7 +59829,7 @@ async function handle(deps, rateLimiter, surface, request2) {
59549
59829
  try {
59550
59830
  let key;
59551
59831
  try {
59552
- key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers), deps.logger);
59832
+ key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers), startedAt, deps.logger);
59553
59833
  } catch (error61) {
59554
59834
  if (error61 instanceof GatewayError && error61.code === "AUTH") {
59555
59835
  deps.logger.warn("authentication rejected", {
@@ -59706,17 +59986,14 @@ async function handle(deps, rateLimiter, surface, request2) {
59706
59986
  }
59707
59987
  function proxyRoutes(deps) {
59708
59988
  const logger2 = deps.logger ?? noopLogger;
59709
- const rateLimiter = deps.rateLimiter ?? new ApiKeyRateLimiter({
59710
- store: deps.store,
59711
- now: deps.now,
59712
- logger: logger2,
59713
- ...deps.coord === undefined ? {} : { coord: deps.coord }
59714
- });
59989
+ const coord = deps.coord ?? memoryCoord();
59990
+ const rateLimiter = deps.rateLimiter ?? new ApiKeyRateLimiter({ store: deps.store, now: deps.now, logger: logger2, coord });
59715
59991
  const dispatchDeps = {
59716
59992
  ...deps,
59717
59993
  logger: logger2,
59994
+ coord,
59718
59995
  snapshots: deps.snapshots ?? createRoutingSnapshotCache(deps.store, logger2),
59719
- loadRegistry: deps.loadRegistry ?? createLoadRegistry(deps.coord),
59996
+ loadRegistry: deps.loadRegistry ?? createLoadRegistry(coord),
59720
59997
  keepaliveMs: deps.keepaliveMs ?? KEEPALIVE_MS
59721
59998
  };
59722
59999
  return new Elysia().post("/v1/messages", ({ request: request2, server }) => {
@@ -59730,7 +60007,7 @@ function proxyRoutes(deps) {
59730
60007
  return handle(dispatchDeps, rateLimiter, "openai", request2);
59731
60008
  }).get("/v1/models", async ({ request: request2 }) => {
59732
60009
  try {
59733
- const key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers), logger2);
60010
+ const key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers), deps.now(), logger2);
59734
60011
  const snapshot = await dispatchDeps.snapshots.get(deps.now());
59735
60012
  const models = [...snapshot.models.values()];
59736
60013
  const visibleModels = key.modelAllowlist === null ? models : models.filter((model) => key.modelAllowlist?.includes(model.id));
@@ -59746,7 +60023,7 @@ function proxyRoutes(deps) {
59746
60023
  }
59747
60024
  }).post("/v1/messages/count_tokens", async ({ request: request2 }) => {
59748
60025
  try {
59749
- const key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers), logger2);
60026
+ const key = await authenticateApiKey(deps.store, apiKeyHeader(request2.headers), deps.now(), logger2);
59750
60027
  await rateLimiter.consume(key.id, key.limits, deps.requestId());
59751
60028
  const body2 = await request2.json();
59752
60029
  const chatRequest = parseAnthropicRequest(body2, request2.headers);
@@ -60597,7 +60874,8 @@ function sizeOf(payload) {
60597
60874
  return 0;
60598
60875
  }
60599
60876
  }
60600
- function createRing(limits) {
60877
+ var DEFAULT_RING_LIMITS = { frames: 500, bytes: 2 * 1024 * 1024 };
60878
+ function createRing(limits = DEFAULT_RING_LIMITS) {
60601
60879
  const topics = new Map;
60602
60880
  const stateFor = (topic) => {
60603
60881
  const existing = topics.get(topic);
@@ -60660,7 +60938,7 @@ function createRing(limits) {
60660
60938
  }
60661
60939
 
60662
60940
  // apps/gateway/src/version.ts
60663
- var VERSION = "0.10.3";
60941
+ var VERSION = "0.11.1";
60664
60942
 
60665
60943
  // apps/gateway/src/app.ts
60666
60944
  var ADMIN_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
@@ -60747,7 +61025,7 @@ function createApp(deps) {
60747
61025
  now,
60748
61026
  onDetach: (id, topics) => channelsRef?.closed(id, topics)
60749
61027
  });
60750
- const ring = deps.ring ?? createRing({ frames: 500, bytes: 2 * 1024 * 1024 });
61028
+ const ring = deps.ring ?? createRing();
60751
61029
  const broadcaster = deps.broadcaster ?? createBroadcaster({ registry: registry2, ring, coord, nodeId, now });
60752
61030
  const channels = deps.channels ?? createChannelRegistry({
60753
61031
  sockets: registry2,
@@ -60788,7 +61066,7 @@ function createApp(deps) {
60788
61066
  return new Response(null, { status: 401 });
60789
61067
  }
60790
61068
  const coordFallback = deps.coordHealthy?.() === false;
60791
- return new Response(telemetry.scrape(loadRegistry.localCounts?.() ?? new Map, registry2.stats(), coordFallback), { headers: { "content-type": "text/plain; version=0.0.4; charset=utf-8" } });
61069
+ return new Response(telemetry.scrape(loadRegistry.localCounts?.() ?? new Map, registry2.stats(), coordFallback, deps.coordFaults?.() ?? 0), { headers: { "content-type": "text/plain; version=0.0.4; charset=utf-8" } });
60792
61070
  })).use(proxyRoutes({
60793
61071
  store: deps.store,
60794
61072
  snapshots,
@@ -60811,6 +61089,7 @@ function createApp(deps) {
60811
61089
  admin,
60812
61090
  baseUrl: deps.baseUrl,
60813
61091
  bodyLoggingAllowed: deps.bodyLoggingAllowed === true,
61092
+ ...deps.dayOffsetMinutes === undefined ? {} : { dayOffsetMinutes: deps.dayOffsetMinutes },
60814
61093
  now,
60815
61094
  sessionTtlMs: ADMIN_SESSION_TTL_MS,
60816
61095
  logger: logger2,
@@ -60822,6 +61101,7 @@ function createApp(deps) {
60822
61101
  store: deps.store,
60823
61102
  admin,
60824
61103
  sessionTtlMs: ADMIN_SESSION_TTL_MS,
61104
+ ...deps.dayOffsetMinutes === undefined ? {} : { dayOffsetMinutes: deps.dayOffsetMinutes },
60825
61105
  now,
60826
61106
  logger: logger2
60827
61107
  })).use(databaseRoutes({
@@ -60917,6 +61197,7 @@ var CHANNEL2 = `${NS}pubsub`;
60917
61197
  var LOCK_POLL_MS = 50;
60918
61198
  var FAULT_LOG_INTERVAL_MS = 30000;
60919
61199
  var CONNECT_WAIT_MS = 2000;
61200
+ var COMMAND_TIMEOUT_MS = 1000;
60920
61201
  var RECONNECTED2 = "coord:reconnected";
60921
61202
  var WINDOW_CLAIM = `
60922
61203
  redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', tonumber(ARGV[1]) - tonumber(ARGV[2]))
@@ -61030,10 +61311,17 @@ var SCRIPTS = [
61030
61311
  LEASE_ACQUIRE,
61031
61312
  PUBLISH_SEQUENCED
61032
61313
  ];
61314
+
61315
+ class TimedOut extends Error {
61316
+ constructor() {
61317
+ super("command timed out");
61318
+ }
61319
+ }
61033
61320
  function redisCoord(deps) {
61034
61321
  const logger2 = deps.logger ?? noopLogger;
61035
61322
  const now = deps.now ?? (() => Date.now());
61036
61323
  const faultInterval = deps.faultLogIntervalMs ?? FAULT_LOG_INTERVAL_MS;
61324
+ const commandTimeout = deps.commandTimeoutMs ?? COMMAND_TIMEOUT_MS;
61037
61325
  const fallback = memoryCoord({ now });
61038
61326
  const client = new RedisClient(deps.url, {
61039
61327
  connectionTimeout: 2000,
@@ -61047,17 +61335,15 @@ function redisCoord(deps) {
61047
61335
  const colon = name.indexOf(":");
61048
61336
  return `${NS}gidx:${colon === -1 ? name : name.slice(0, colon + 1)}`;
61049
61337
  };
61338
+ let faults = 0;
61050
61339
  const fault = (error61) => {
61051
61340
  healthy = false;
61341
+ faults++;
61052
61342
  const at = now();
61053
61343
  if (at - lastFault < faultInterval)
61054
61344
  return;
61055
61345
  lastFault = at;
61056
- logger2.warn("coordinator unreachable; serving from memory", {
61057
- coord: "redis",
61058
- coordFallback: true,
61059
- reason: describeError(error61, "unknown")
61060
- });
61346
+ logger2.warn(error61 instanceof TimedOut ? "coordinator slow; serving from memory" : "coordinator unreachable; serving from memory", { coord: "redis", coordFallback: true, reason: describeError(error61, "unknown") });
61061
61347
  };
61062
61348
  const shas = new Map;
61063
61349
  const loadScripts = async () => {
@@ -61085,22 +61371,39 @@ function redisCoord(deps) {
61085
61371
  }, (error61) => fault(error61)),
61086
61372
  Bun.sleep(CONNECT_WAIT_MS)
61087
61373
  ]);
61374
+ let lostDebit = false;
61088
61375
  const reseedAfterOutage = async () => {
61089
61376
  const keys = await scan(client, `${key("b", "")}*`);
61090
61377
  if (keys.length > 0)
61091
61378
  await client.send("UNLINK", keys);
61092
61379
  };
61380
+ const deadline = async (redis) => {
61381
+ let timer;
61382
+ try {
61383
+ return await Promise.race([
61384
+ redis(),
61385
+ new Promise((_resolve, reject) => {
61386
+ timer = setTimeout(() => reject(new TimedOut), commandTimeout);
61387
+ })
61388
+ ]);
61389
+ } finally {
61390
+ clearTimeout(timer);
61391
+ }
61392
+ };
61093
61393
  const attempt2 = async (redis, instead) => {
61094
61394
  try {
61095
61395
  await ready;
61096
61396
  if (!client.connected)
61097
61397
  throw new Error("not connected");
61098
- const out = await redis();
61099
- if (!healthy && lastFault !== Number.NEGATIVE_INFINITY) {
61100
- healthy = true;
61101
- reseedAfterOutage().catch(fault);
61102
- }
61398
+ const out = await deadline(redis);
61103
61399
  healthy = true;
61400
+ if (lostDebit) {
61401
+ lostDebit = false;
61402
+ reseedAfterOutage().catch((error61) => {
61403
+ lostDebit = true;
61404
+ fault(error61);
61405
+ });
61406
+ }
61104
61407
  return out;
61105
61408
  } catch (error61) {
61106
61409
  fault(error61);
@@ -61146,6 +61449,7 @@ function redisCoord(deps) {
61146
61449
  subscriber.connect().catch(fault);
61147
61450
  return {
61148
61451
  healthy: () => healthy,
61452
+ faults: () => faults,
61149
61453
  close() {
61150
61454
  subscriber.close();
61151
61455
  client.close();
@@ -61192,7 +61496,10 @@ function redisCoord(deps) {
61192
61496
  add(name, grainMs, windowMs, at, delta2) {
61193
61497
  return attempt2(async () => {
61194
61498
  await evalScript(BUCKETS_ADD, [key("b", name)], [at, grainMs, windowMs, delta2.requests, delta2.tokens, delta2.costUsd]);
61195
- }, () => fallback.buckets.add(name, grainMs, windowMs, at, delta2));
61499
+ }, () => {
61500
+ lostDebit = true;
61501
+ return fallback.buckets.add(name, grainMs, windowMs, at, delta2);
61502
+ });
61196
61503
  },
61197
61504
  sum(name, grainMs, windowMs, at) {
61198
61505
  return attempt2(async () => {
@@ -61231,14 +61538,15 @@ function redisCoord(deps) {
61231
61538
  async withLock(name, ttlMs, waitMs, fn) {
61232
61539
  const full = key("lock", name);
61233
61540
  const token = crypto.randomUUID();
61234
- const deadline = performance.now() + waitMs;
61541
+ const deadline2 = performance.now() + waitMs;
61235
61542
  for (;; ) {
61236
61543
  const taken = await attempt2(async () => await client.set(full, token, "PX", String(ttlMs), "NX") === "OK", async () => {
61544
+ evalScript(MUTEX_RELEASE, [full], [token]).catch(() => {});
61237
61545
  throw new LockUnavailable(name);
61238
61546
  });
61239
61547
  if (taken)
61240
61548
  break;
61241
- if (performance.now() >= deadline)
61549
+ if (performance.now() >= deadline2)
61242
61550
  throw new LockUnavailable(name);
61243
61551
  await Bun.sleep(LOCK_POLL_MS);
61244
61552
  }
@@ -61274,17 +61582,17 @@ function redisCoord(deps) {
61274
61582
  }
61275
61583
  },
61276
61584
  pubsub: {
61277
- publish(topic, payload) {
61585
+ async publish(topic, payload) {
61586
+ await listened;
61278
61587
  return attempt2(async () => {
61279
- await listened;
61280
61588
  if (!listening)
61281
61589
  throw new Error("not listening");
61282
61590
  await client.publish(CHANNEL2, JSON.stringify({ topic, payload }));
61283
61591
  }, () => fallback.pubsub.publish(topic, payload));
61284
61592
  },
61285
- publishSequenced(topic, payload, seqKey) {
61593
+ async publishSequenced(topic, payload, seqKey) {
61594
+ await listened;
61286
61595
  return attempt2(async () => {
61287
- await listened;
61288
61596
  if (!listening)
61289
61597
  throw new Error("not listening");
61290
61598
  return await evalScript(PUBLISH_SEQUENCED, [key("seq", seqKey)], [CHANNEL2, topic, payload]);
@@ -61378,13 +61686,13 @@ async function underLease(lease, name, ttlMs, fn) {
61378
61686
 
61379
61687
  // apps/gateway/src/maintenance.ts
61380
61688
  var SWEEP_INTERVAL_MS2 = 60 * 60 * 1000;
61381
- var DAY_MS3 = 24 * 60 * 60 * 1000;
61689
+ var DAY_MS4 = 24 * 60 * 60 * 1000;
61382
61690
  var ROLLUP_RETENTION_DAYS = 400;
61383
61691
  async function pruneLogs(store, now) {
61384
61692
  const settings = await store.config.getSettings();
61385
- const rawHorizon = now - settings.logRetentionDays * DAY_MS3;
61693
+ const rawHorizon = now - settings.logRetentionDays * DAY_MS4;
61386
61694
  const raw = await store.usage.prune(rawHorizon);
61387
- const daily = await store.usage.pruneDaily(now - ROLLUP_RETENTION_DAYS * DAY_MS3);
61695
+ const daily = await store.usage.pruneDaily(now - ROLLUP_RETENTION_DAYS * DAY_MS4);
61388
61696
  const quotaSamples = await store.credentials.pruneQuotaSamples(rawHorizon);
61389
61697
  const bodies = await store.bodies.prune(rawHorizon);
61390
61698
  const bodiesOverCap = await store.bodies.pruneToCap();
@@ -61455,7 +61763,7 @@ async function sweep(deps) {
61455
61763
  let touched = 0;
61456
61764
  for (const credential of due(credentials, deps.now())) {
61457
61765
  if (!credential.hasRefreshToken) {
61458
- if (credential.expiresAt !== null && credential.expiresAt <= deps.now()) {
61766
+ if (credentialExpired(credential, deps.now())) {
61459
61767
  await deps.store.credentials.update(credential.id, {
61460
61768
  enabled: false,
61461
61769
  disabledReason: "expiredNoRefresh",
@@ -62147,6 +62455,10 @@ var DEFINITIONS = {
62147
62455
  help: "Whether this process is using coordination fallback.",
62148
62456
  type: "gauge"
62149
62457
  },
62458
+ omni_coord_faults_total: {
62459
+ help: "Coordinator calls this process answered from memory, timeouts included.",
62460
+ type: "counter"
62461
+ },
62150
62462
  omni_metrics_series_folded_total: {
62151
62463
  help: "Metric series folded into api_key_id other.",
62152
62464
  type: "counter"
@@ -62392,13 +62704,14 @@ function createTelemetry(opts) {
62392
62704
  if (opts.metricsEnabled)
62393
62705
  registry2.add("omni_ratelimit_rejected_total", { dimension, window: window2 ?? "none" });
62394
62706
  },
62395
- scrape(localInflight, streams, coordFallback) {
62707
+ scrape(localInflight, streams, coordFallback, coordFaults) {
62396
62708
  for (const [provider, value] of localInflight)
62397
62709
  registry2.set("omni_inflight", { provider }, value);
62398
62710
  registry2.set("omni_stream_connections", {}, streams.connections);
62399
62711
  registry2.set("omni_stream_queued", {}, streams.queued);
62400
62712
  registry2.set("omni_stream_dropped_total", {}, streams.dropped);
62401
62713
  registry2.set("omni_coord_fallback", {}, coordFallback ? 1 : 0);
62714
+ registry2.set("omni_coord_faults_total", {}, coordFaults);
62402
62715
  return renderPrometheus(registry2.snapshot());
62403
62716
  },
62404
62717
  stop() {
@@ -62450,7 +62763,12 @@ async function main() {
62450
62763
  if (config2.logLevelFallbackFrom !== null) {
62451
62764
  logger2.info("invalid log level; using info", { reason: config2.logLevelFallbackFrom });
62452
62765
  }
62453
- logger2.info("omnigateway booting", { host: config2.host, port: config2.port });
62766
+ logger2.info(config2.dayOffsetFallbackFrom === null ? "omnigateway booting" : "omnigateway booting; invalid day offset, using host offset", {
62767
+ host: config2.host,
62768
+ port: config2.port,
62769
+ dayOffsetMinutes: config2.dayOffsetMinutes,
62770
+ ...config2.dayOffsetFallbackFrom === null ? {} : { reason: config2.dayOffsetFallbackFrom }
62771
+ });
62454
62772
  function dashboardDir() {
62455
62773
  if (config2.staticDir !== null)
62456
62774
  return config2.staticDir;
@@ -62467,7 +62785,8 @@ async function main() {
62467
62785
  ...config2.clusterMode && config2.databaseUrl !== null ? { url: config2.databaseUrl } : { path: config2.databasePath },
62468
62786
  encryptionKey,
62469
62787
  logger: logger2,
62470
- nodeId
62788
+ nodeId,
62789
+ dayOffsetMinutes: config2.dayOffsetMinutes
62471
62790
  });
62472
62791
  await store.maintenance.heartbeat(now());
62473
62792
  const swept = await store.usage.sweepPending(now());
@@ -62507,7 +62826,7 @@ async function main() {
62507
62826
  now,
62508
62827
  onDetach: (id, topics) => pluginChannelsRef?.closed(id, topics)
62509
62828
  });
62510
- const streamRing = createRing({ frames: 500, bytes: 2 * 1024 * 1024 });
62829
+ const streamRing = createRing();
62511
62830
  const broadcaster = createBroadcaster({
62512
62831
  registry: streamRegistry,
62513
62832
  ring: streamRing,
@@ -62544,7 +62863,7 @@ async function main() {
62544
62863
  coord,
62545
62864
  nodeId,
62546
62865
  mode: config2.clusterMode ? "cluster" : "single",
62547
- ...shared === null ? {} : { coordHealthy: shared.healthy },
62866
+ ...shared === null ? {} : { coordHealthy: shared.healthy, coordFaults: shared.faults },
62548
62867
  baseUrl: config2.baseUrl,
62549
62868
  http,
62550
62869
  now,
@@ -62559,6 +62878,7 @@ async function main() {
62559
62878
  telemetry,
62560
62879
  ...config2.metricsToken === null ? {} : { metricsToken: config2.metricsToken },
62561
62880
  bodyLoggingAllowed: config2.bodyLoggingAllowed,
62881
+ dayOffsetMinutes: config2.dayOffsetMinutes,
62562
62882
  plugins: loadedPlugins.plugins.map((plugin) => ({ id: plugin.id, routes: plugin.routes })),
62563
62883
  pluginUi: loadedPlugins.plugins,
62564
62884
  ...loadedPlugins.plugins.length === 0 ? {} : { emit: (event) => pluginEvents.emitRequestCompleted(event) },