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/bin/omni.js CHANGED
@@ -75,7 +75,10 @@ function note(ctx, writer, message) {
75
75
  function formatTime(at) {
76
76
  if (at === null || !Number.isFinite(at))
77
77
  return "\u2014";
78
- return new Date(at).toISOString().replace("T", " ").slice(0, 19);
78
+ const date = new Date(at);
79
+ if (Number.isNaN(date.getTime()))
80
+ return "out of range";
81
+ return date.toISOString().replace("T", " ").slice(0, 19);
79
82
  }
80
83
  function formatAge(from, now) {
81
84
  if (from === null || !Number.isFinite(from))
@@ -19238,13 +19241,13 @@ function prepareArtifact(input2) {
19238
19241
  error: error61.value
19239
19242
  };
19240
19243
  const json2 = JSON.stringify(bounded);
19241
- const size = encoder2.encode(json2).length;
19244
+ const size = Buffer.byteLength(json2);
19242
19245
  if (size <= MAX_ARTIFACT_BYTES)
19243
19246
  return { artifact: bounded, json: json2 };
19244
19247
  const marker = omission(size);
19245
19248
  const omitted = omitBodies(bounded, marker);
19246
19249
  const omittedJson = JSON.stringify(omitted);
19247
- if (encoder2.encode(omittedJson).length <= MAX_ARTIFACT_BYTES) {
19250
+ if (Buffer.byteLength(omittedJson) <= MAX_ARTIFACT_BYTES) {
19248
19251
  return { artifact: omitted, json: omittedJson };
19249
19252
  }
19250
19253
  const stripped = { ...omitted, error: marker };
@@ -19504,6 +19507,261 @@ function estimateCachedInputTokens(request) {
19504
19507
  }
19505
19508
  return cached2;
19506
19509
  }
19510
+ // packages/store/src/sqlite/rollup.ts
19511
+ var HOUR_MS = 3600000;
19512
+ function hourOf(at) {
19513
+ return Math.floor(at / HOUR_MS);
19514
+ }
19515
+ var DAY_MS = 86400000;
19516
+ function hostDayOffsetMinutes() {
19517
+ return -new Date().getTimezoneOffset() || 0;
19518
+ }
19519
+ function startOfDay(at, offsetMinutes) {
19520
+ const offsetMs = offsetMinutes * 60000;
19521
+ return Math.floor((at + offsetMs) / DAY_MS) * DAY_MS - offsetMs;
19522
+ }
19523
+ var UPSERT = `
19524
+ INSERT INTO usage_daily
19525
+ (day, provider, credential_id, requested_model, resolved_model, api_key_id,
19526
+ requests, errors, input_tokens, output_tokens, cache_read_tokens,
19527
+ cache_write_tokens, rtk_saved_tokens, rtk_applied_requests, cost_usd, duration_ms_sum)
19528
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
19529
+ ON CONFLICT (day, provider, credential_id, requested_model, resolved_model, api_key_id)
19530
+ DO UPDATE SET
19531
+ requests = requests + excluded.requests,
19532
+ errors = errors + excluded.errors,
19533
+ input_tokens = input_tokens + excluded.input_tokens,
19534
+ output_tokens = output_tokens + excluded.output_tokens,
19535
+ cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
19536
+ cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
19537
+ rtk_saved_tokens = rtk_saved_tokens + excluded.rtk_saved_tokens,
19538
+ rtk_applied_requests = rtk_applied_requests + excluded.rtk_applied_requests,
19539
+ cost_usd = cost_usd + excluded.cost_usd,
19540
+ duration_ms_sum = duration_ms_sum + excluded.duration_ms_sum`;
19541
+ function keyOf(log, dayOffsetMinutes) {
19542
+ return [
19543
+ startOfDay(log.at, dayOffsetMinutes),
19544
+ log.resolvedProvider ?? "",
19545
+ log.credentialId ?? "",
19546
+ log.requestedModel,
19547
+ log.resolvedModel ?? "",
19548
+ log.apiKeyId ?? ""
19549
+ ];
19550
+ }
19551
+ function countersOf(log) {
19552
+ return {
19553
+ requests: 1,
19554
+ errors: log.status >= 400 ? 1 : 0,
19555
+ inputTokens: log.inputTokens,
19556
+ outputTokens: log.outputTokens,
19557
+ cacheReadTokens: log.cacheReadTokens,
19558
+ cacheWriteTokens: log.cacheWriteTokens,
19559
+ rtkSavedTokens: log.rtkEstimatedTokensSaved,
19560
+ rtkAppliedRequests: log.rtkApplied ? 1 : 0,
19561
+ costUsd: log.costUsd,
19562
+ durationMsSum: log.durationMs
19563
+ };
19564
+ }
19565
+ var BACKFILL_UPSERT = `
19566
+ INSERT INTO usage_daily
19567
+ (day, provider, credential_id, requested_model, resolved_model, api_key_id,
19568
+ requests, errors, input_tokens, output_tokens, cache_read_tokens,
19569
+ cache_write_tokens, cost_usd, duration_ms_sum)
19570
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
19571
+ ON CONFLICT (day, provider, credential_id, requested_model, resolved_model, api_key_id)
19572
+ DO UPDATE SET
19573
+ requests = requests + excluded.requests,
19574
+ errors = errors + excluded.errors,
19575
+ input_tokens = input_tokens + excluded.input_tokens,
19576
+ output_tokens = output_tokens + excluded.output_tokens,
19577
+ cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
19578
+ cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
19579
+ cost_usd = cost_usd + excluded.cost_usd,
19580
+ duration_ms_sum = duration_ms_sum + excluded.duration_ms_sum`;
19581
+ function upsertPreRtk(db, key, c) {
19582
+ db.run(BACKFILL_UPSERT, [
19583
+ ...key,
19584
+ c.requests,
19585
+ c.errors,
19586
+ c.inputTokens,
19587
+ c.outputTokens,
19588
+ c.cacheReadTokens,
19589
+ c.cacheWriteTokens,
19590
+ c.costUsd,
19591
+ c.durationMsSum
19592
+ ]);
19593
+ }
19594
+ function upsert(db, key, c) {
19595
+ db.run(UPSERT, [
19596
+ ...key,
19597
+ c.requests,
19598
+ c.errors,
19599
+ c.inputTokens,
19600
+ c.outputTokens,
19601
+ c.cacheReadTokens,
19602
+ c.cacheWriteTokens,
19603
+ c.rtkSavedTokens,
19604
+ c.rtkAppliedRequests,
19605
+ c.costUsd,
19606
+ c.durationMsSum
19607
+ ]);
19608
+ }
19609
+ function rollupLog(db, log, dayOffsetMinutes) {
19610
+ upsert(db, keyOf(log, dayOffsetMinutes), countersOf(log));
19611
+ }
19612
+ function backfillDaily(db, dayOffsetMinutes) {
19613
+ const groups = new Map;
19614
+ for (const row of db.query(`SELECT at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
19615
+ status, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
19616
+ duration_ms, cost_usd
19617
+ FROM request_logs`).all()) {
19618
+ const key = [
19619
+ startOfDay(row.at, dayOffsetMinutes),
19620
+ row.resolved_provider ?? "",
19621
+ row.credential_id ?? "",
19622
+ row.requested_model,
19623
+ row.resolved_model ?? "",
19624
+ row.api_key_id ?? ""
19625
+ ];
19626
+ const id = key.join("\x00");
19627
+ const seen = groups.get(id);
19628
+ const counters = seen?.counters ?? {
19629
+ requests: 0,
19630
+ errors: 0,
19631
+ inputTokens: 0,
19632
+ outputTokens: 0,
19633
+ cacheReadTokens: 0,
19634
+ cacheWriteTokens: 0,
19635
+ rtkSavedTokens: 0,
19636
+ rtkAppliedRequests: 0,
19637
+ costUsd: 0,
19638
+ durationMsSum: 0
19639
+ };
19640
+ counters.requests += 1;
19641
+ counters.errors += row.status >= 400 ? 1 : 0;
19642
+ counters.inputTokens += row.input_tokens;
19643
+ counters.outputTokens += row.output_tokens;
19644
+ counters.cacheReadTokens += row.cache_read_tokens;
19645
+ counters.cacheWriteTokens += row.cache_write_tokens;
19646
+ counters.costUsd += row.cost_usd;
19647
+ counters.durationMsSum += row.duration_ms;
19648
+ if (seen === undefined)
19649
+ groups.set(id, { key, counters });
19650
+ }
19651
+ for (const group of groups.values())
19652
+ upsertPreRtk(db, group.key, group.counters);
19653
+ return groups.size;
19654
+ }
19655
+ var HOURLY_UPSERT = `
19656
+ INSERT INTO usage_rollup
19657
+ (api_key_id, hour, requests, input_tokens, output_tokens,
19658
+ cache_read_tokens, cache_write_tokens, cost_usd)
19659
+ VALUES (?,?,?,?,?,?,?,?)
19660
+ ON CONFLICT (api_key_id, hour)
19661
+ DO UPDATE SET
19662
+ requests = requests + excluded.requests,
19663
+ input_tokens = input_tokens + excluded.input_tokens,
19664
+ output_tokens = output_tokens + excluded.output_tokens,
19665
+ cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
19666
+ cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
19667
+ cost_usd = cost_usd + excluded.cost_usd`;
19668
+ function rollupHour(db, log) {
19669
+ const apiKeyId = log.apiKeyId;
19670
+ if (apiKeyId === null)
19671
+ return;
19672
+ db.run(HOURLY_UPSERT, [
19673
+ apiKeyId,
19674
+ hourOf(log.at),
19675
+ 1,
19676
+ log.inputTokens,
19677
+ log.outputTokens,
19678
+ log.cacheReadTokens,
19679
+ log.cacheWriteTokens,
19680
+ log.costUsd
19681
+ ]);
19682
+ }
19683
+ var REBUILD = `
19684
+ INSERT INTO usage_rollup
19685
+ (api_key_id, hour, requests, input_tokens, output_tokens,
19686
+ cache_read_tokens, cache_write_tokens, cost_usd)
19687
+ SELECT api_key_id,
19688
+ CAST(at / 3600000 AS INTEGER),
19689
+ COUNT(*),
19690
+ COALESCE(SUM(input_tokens), 0),
19691
+ COALESCE(SUM(output_tokens), 0),
19692
+ COALESCE(SUM(cache_read_tokens), 0),
19693
+ COALESCE(SUM(cache_write_tokens), 0),
19694
+ COALESCE(SUM(cost_usd), 0)
19695
+ FROM request_logs
19696
+ WHERE state = 'done' AND api_key_id IS NOT NULL
19697
+ GROUP BY api_key_id, CAST(at / 3600000 AS INTEGER)`;
19698
+ function rebuildRollup(db) {
19699
+ db.run("DELETE FROM usage_rollup");
19700
+ db.run(REBUILD);
19701
+ }
19702
+ function auditRollup(db) {
19703
+ const row = db.query(`WITH truth AS (
19704
+ SELECT api_key_id AS k,
19705
+ CAST(at / 3600000 AS INTEGER) AS h,
19706
+ COUNT(*) AS requests,
19707
+ COALESCE(SUM(input_tokens), 0) AS input_tokens,
19708
+ COALESCE(SUM(output_tokens), 0) AS output_tokens,
19709
+ COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
19710
+ COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,
19711
+ COALESCE(SUM(cost_usd), 0) AS cost_usd
19712
+ FROM request_logs
19713
+ WHERE state = 'done' AND api_key_id IS NOT NULL
19714
+ GROUP BY k, h
19715
+ )
19716
+ SELECT (SELECT COUNT(*) FROM truth) AS buckets,
19717
+ (SELECT COUNT(*)
19718
+ FROM truth
19719
+ LEFT JOIN usage_rollup r ON r.api_key_id = truth.k AND r.hour = truth.h
19720
+ WHERE r.api_key_id IS NULL
19721
+ OR r.requests <> truth.requests
19722
+ OR r.input_tokens <> truth.input_tokens
19723
+ OR r.output_tokens <> truth.output_tokens
19724
+ OR r.cache_read_tokens <> truth.cache_read_tokens
19725
+ OR r.cache_write_tokens <> truth.cache_write_tokens
19726
+ OR ABS(r.cost_usd - truth.cost_usd) > 1e-9 + ABS(truth.cost_usd) * 1e-9
19727
+ ) AS mismatched,
19728
+ (SELECT COUNT(*)
19729
+ FROM usage_rollup r
19730
+ LEFT JOIN truth ON r.api_key_id = truth.k AND r.hour = truth.h
19731
+ WHERE truth.k IS NULL
19732
+ ) AS orphans`).get();
19733
+ const buckets = row?.buckets ?? 0;
19734
+ const mismatched = (row?.mismatched ?? 0) + (row?.orphans ?? 0);
19735
+ return { buckets, mismatched, ok: mismatched === 0 };
19736
+ }
19737
+ function backfillRtkUsage(db, dayOffsetMinutes) {
19738
+ const groups = new Map;
19739
+ for (const row of db.query(`SELECT at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
19740
+ rtk_applied, rtk_estimated_tokens_saved
19741
+ FROM request_logs
19742
+ WHERE state = 'done'`).all()) {
19743
+ const key = [
19744
+ startOfDay(row.at, dayOffsetMinutes),
19745
+ row.resolved_provider ?? "",
19746
+ row.credential_id ?? "",
19747
+ row.requested_model,
19748
+ row.resolved_model ?? "",
19749
+ row.api_key_id ?? ""
19750
+ ];
19751
+ const id = key.join("\x00");
19752
+ const group = groups.get(id) ?? { key, saved: 0, applied: 0 };
19753
+ group.saved += row.rtk_estimated_tokens_saved;
19754
+ group.applied += row.rtk_applied === 1 ? 1 : 0;
19755
+ groups.set(id, group);
19756
+ }
19757
+ for (const { key, saved, applied } of groups.values()) {
19758
+ db.run(`UPDATE usage_daily
19759
+ SET rtk_saved_tokens = ?, rtk_applied_requests = ?
19760
+ WHERE day = ? AND provider = ? AND credential_id = ? AND requested_model = ?
19761
+ AND resolved_model = ? AND api_key_id = ?`, [saved, applied, ...key]);
19762
+ }
19763
+ }
19764
+
19507
19765
  // packages/store/src/postgres/db.ts
19508
19766
  var {SQL } = globalThis.Bun;
19509
19767
 
@@ -19809,11 +20067,95 @@ CREATE TRIGGER api_keys_config_version
19809
20067
  FOR EACH STATEMENT EXECUTE FUNCTION bump_config_version();
19810
20068
  `;
19811
20069
 
20070
+ // packages/store/src/postgres/migrations/002_health_measurements.sql
20071
+ var _002_health_measurements_default = `-- DEPLOY HAZARD \u2014 read before applying to a fleet.
20072
+ --
20073
+ -- This migration drops two columns that every gateway before it writes on
20074
+ -- the success path (\`UPSERT_HEALTH\` names \`ewma_ttft_ms\` and \`last_used_at\`).
20075
+ -- The first new replica to boot applies it under the migration lock; from that
20076
+ -- moment every OLD replica still running throws on every successful request:
20077
+ -- the upstream call has completed and been billed, the client has received its
20078
+ -- content frames, and then the connection aborts with no terminal frame. The
20079
+ -- \`RollingUpdate\` in \`k8s/deployment.yaml\` does not protect against this \u2014 the
20080
+ -- "surviving" replica is the one that breaks.
20081
+ --
20082
+ -- Required: scale to ONE replica before deploying this release, or accept that
20083
+ -- every old replica fails every successful request until it is replaced.
20084
+ -- Rollback is unsafe: an old image cannot re-add the columns, and running it
20085
+ -- against this schema fails the same way. The only way back is a snapshot
20086
+ -- restore taken before the deploy. Deliberate: kept as a drop, not
20087
+ -- expand/contract, on the operator's decision \u2014 see the write-path spec.
20088
+ --
20089
+ -- \`credential_health\` keeps decisions, not measurements.
20090
+ --
20091
+ -- \`ewma_ttft_ms\` and \`last_used_at\` were written on every successful request,
20092
+ -- and a success that changes nothing else now writes nothing. Both live in the
20093
+ -- gateway's process-local \`loadRegistry\` instead. The console's "last used"
20094
+ -- reads \`request_logs\` through \`usage.lastUsedByCredential\`, served by
20095
+ -- \`idx_request_logs_cred\`.
20096
+ ALTER TABLE credential_health DROP COLUMN ewma_ttft_ms;
20097
+ ALTER TABLE credential_health DROP COLUMN last_used_at;
20098
+
20099
+ -- The statement-level trigger bumped \`config_version\` \u2014 one row, locked until
20100
+ -- commit, by every writer on every replica \u2014 on every health write, and every
20101
+ -- bump makes every replica rebuild its routing snapshot. Only a change to a
20102
+ -- column routing *decides* on needs that; a bare \`consecutive_failures\`
20103
+ -- increment is patched into the held snapshot from the \`healthSaved\` change.
20104
+ --
20105
+ -- The INSERT arm must stay \`FOR EACH ROW\`. The write is an upsert, and a
20106
+ -- statement-level INSERT trigger fires on an upsert whichever path each row
20107
+ -- took, which would bump on every write and make the WHEN clause below moot.
20108
+ -- Row-level \`AFTER INSERT\` fires only for rows actually inserted.
20109
+ DROP TRIGGER credential_health_config_version ON credential_health;
20110
+
20111
+ CREATE TRIGGER credential_health_config_version_upd
20112
+ AFTER UPDATE ON credential_health
20113
+ FOR EACH ROW
20114
+ WHEN (OLD.breaker_state IS DISTINCT FROM NEW.breaker_state
20115
+ OR OLD.rate_limited_until IS DISTINCT FROM NEW.rate_limited_until
20116
+ OR OLD.opened_at IS DISTINCT FROM NEW.opened_at)
20117
+ EXECUTE FUNCTION bump_config_version();
20118
+
20119
+ CREATE TRIGGER credential_health_config_version_ins
20120
+ AFTER INSERT ON credential_health
20121
+ FOR EACH ROW EXECUTE FUNCTION bump_config_version();
20122
+
20123
+ -- DELETE needs no WHEN, so it stays statement-level as in 001: a credential
20124
+ -- with M model rows bumps once, not M times.
20125
+ CREATE TRIGGER credential_health_config_version_del
20126
+ AFTER DELETE ON credential_health
20127
+ FOR EACH STATEMENT EXECUTE FUNCTION bump_config_version();
20128
+ `;
20129
+
20130
+ // packages/store/src/postgres/migrations/003_key_expiry.sql
20131
+ var _003_key_expiry_default = `-- The Postgres half of the sqlite migration of the same name. \`BIGINT\`, like
20132
+ -- every other epoch-ms column in this schema, and read back through \`numOrNull\`.
20133
+ --
20134
+ -- Nullable rather than \`NOT NULL DEFAULT\`: the column is a deadline, and every
20135
+ -- finite default is a date on which somebody's client stops working. NULL is
20136
+ -- the only value that says nothing was decided, and every reader maps it to
20137
+ -- "never expires".
20138
+ --
20139
+ -- No backfill, deliberately. Every key on every existing installation was
20140
+ -- minted on the promise that it works until revoked, so adding the column and
20141
+ -- stopping is what keeps that promise. The absence of an \`UPDATE\` here is the
20142
+ -- substance of the migration, not an omission.
20143
+ --
20144
+ -- Safe to apply under a rolling deploy, unlike migration 2: this adds a
20145
+ -- nullable column and drops nothing, so a replica running the previous image
20146
+ -- keeps serving \u2014 \`SELECT *\` gains a field its mapper ignores.
20147
+ ALTER TABLE api_keys ADD COLUMN expires_at BIGINT;
20148
+ `;
20149
+
19812
20150
  // packages/store/src/postgres/db.ts
19813
20151
  var num = (v) => Number(v);
19814
20152
  var numOrNull = (v) => v === null ? null : Number(v);
19815
20153
  var MIGRATION_LOCK = 7140641;
19816
- var MIGRATIONS = [{ id: 1, sql: _001_init_default }];
20154
+ var MIGRATIONS = [
20155
+ { id: 1, sql: _001_init_default },
20156
+ { id: 2, sql: _002_health_measurements_default },
20157
+ { id: 3, sql: _003_key_expiry_default }
20158
+ ];
19817
20159
  async function openPg(url2) {
19818
20160
  const sql = new SQL({ url: url2 });
19819
20161
  const conn = await sql.reserve();
@@ -20010,6 +20352,9 @@ function resolvePin(target, accounts) {
20010
20352
  return;
20011
20353
  return accounts.find((account) => servesTarget(target, account));
20012
20354
  }
20355
+ var keyUsable = (key, now) => key.revokedAt === null && (key.expiresAt === null || key.expiresAt > now);
20356
+ var credentialPastExpiry = (credential, now) => credential.authType === "oauth" && credential.expiresAt !== null && credential.expiresAt <= now;
20357
+ var credentialExpired = (credential, now) => credentialPastExpiry(credential, now) && !credential.hasRefreshToken;
20013
20358
  var NODE_GRACE_MS = 60000;
20014
20359
  var DEFAULT_SETTINGS = {
20015
20360
  weights: { tier: 10, health: 3, quota: 2, load: 2, cost: 1, latency: 1 },
@@ -20137,31 +20482,25 @@ function toHealth(r) {
20137
20482
  breakerState: r.breaker_state,
20138
20483
  consecutiveFailures: r.consecutive_failures,
20139
20484
  openedAt: numOrNull(r.opened_at),
20140
- rateLimitedUntil: numOrNull(r.rate_limited_until),
20141
- ewmaTtftMs: r.ewma_ttft_ms,
20142
- lastUsedAt: numOrNull(r.last_used_at)
20485
+ rateLimitedUntil: numOrNull(r.rate_limited_until)
20143
20486
  };
20144
20487
  }
20145
20488
  var UPSERT_HEALTH = `INSERT INTO credential_health
20146
20489
  (credential_id, model, breaker_state, consecutive_failures, opened_at,
20147
- rate_limited_until, ewma_ttft_ms, last_used_at)
20148
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
20490
+ rate_limited_until)
20491
+ VALUES ($1,$2,$3,$4,$5,$6)
20149
20492
  ON CONFLICT (credential_id, model) DO UPDATE SET
20150
20493
  breaker_state = EXCLUDED.breaker_state,
20151
20494
  consecutive_failures = EXCLUDED.consecutive_failures,
20152
20495
  opened_at = EXCLUDED.opened_at,
20153
- rate_limited_until = EXCLUDED.rate_limited_until,
20154
- ewma_ttft_ms = EXCLUDED.ewma_ttft_ms,
20155
- last_used_at = EXCLUDED.last_used_at`;
20496
+ rate_limited_until = EXCLUDED.rate_limited_until`;
20156
20497
  var healthValues = (r) => [
20157
20498
  r.credentialId,
20158
20499
  r.model,
20159
20500
  r.breakerState,
20160
20501
  r.consecutiveFailures,
20161
20502
  r.openedAt,
20162
- r.rateLimitedUntil,
20163
- r.ewmaTtftMs,
20164
- r.lastUsedAt
20503
+ r.rateLimitedUntil
20165
20504
  ];
20166
20505
  function createCredentialRepo(sql, key, emit2) {
20167
20506
  const open2 = async (v) => v === null ? null : decrypt(key, v);
@@ -20354,13 +20693,6 @@ function createCredentialRepo(sql, key, emit2) {
20354
20693
  const rows = await sql.unsafe("SELECT * FROM credential_health");
20355
20694
  return rows.map(toHealth);
20356
20695
  },
20357
- async saveHealth(rows) {
20358
- await sql.begin(async (tx) => {
20359
- for (const r of rows)
20360
- await tx.unsafe(UPSERT_HEALTH, healthValues(r));
20361
- });
20362
- emit2({ type: "healthSaved", rows });
20363
- },
20364
20696
  async updateHealth(credentialId, model, apply) {
20365
20697
  const written = await sql.begin(async (tx) => {
20366
20698
  await tx.unsafe("SELECT pg_advisory_xact_lock(hashtext($1))", [`${credentialId}|${model}`]);
@@ -20502,7 +20834,8 @@ var toKey = (r, logger2) => ({
20502
20834
  limits: parseLimits(r.id, r.limits, logger2),
20503
20835
  bodyLoggingOptOut: r.body_logging_opt_out,
20504
20836
  createdAt: num(r.created_at),
20505
- revokedAt: numOrNull(r.revoked_at)
20837
+ revokedAt: numOrNull(r.revoked_at),
20838
+ expiresAt: numOrNull(r.expires_at)
20506
20839
  });
20507
20840
  function createKeyRepo(sql, logger2) {
20508
20841
  const one = async (where, arg) => {
@@ -20520,8 +20853,8 @@ function createKeyRepo(sql, logger2) {
20520
20853
  async create(input2) {
20521
20854
  const now = Date.now();
20522
20855
  await sql.unsafe(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
20523
- body_logging_opt_out, created_at, revoked_at)
20524
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NULL)`, [
20856
+ body_logging_opt_out, created_at, revoked_at, expires_at)
20857
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NULL,$9)`, [
20525
20858
  input2.id,
20526
20859
  input2.label,
20527
20860
  input2.prefix,
@@ -20529,7 +20862,8 @@ function createKeyRepo(sql, logger2) {
20529
20862
  input2.modelAllowlist === null ? null : JSON.stringify(input2.modelAllowlist),
20530
20863
  JSON.stringify(parseLimitConfig(input2.limits)),
20531
20864
  input2.bodyLoggingOptOut,
20532
- now
20865
+ now,
20866
+ input2.expiresAt
20533
20867
  ]);
20534
20868
  return { ...input2, createdAt: now, revokedAt: null };
20535
20869
  },
@@ -20537,8 +20871,8 @@ function createKeyRepo(sql, logger2) {
20537
20871
  if (row.limits === null)
20538
20872
  throw new Error(`api key ${row.id} has unreadable limits`);
20539
20873
  await sql.unsafe(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
20540
- body_logging_opt_out, created_at, revoked_at)
20541
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`, [
20874
+ body_logging_opt_out, created_at, revoked_at, expires_at)
20875
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`, [
20542
20876
  row.id,
20543
20877
  row.label,
20544
20878
  row.prefix,
@@ -20547,7 +20881,8 @@ function createKeyRepo(sql, logger2) {
20547
20881
  JSON.stringify(parseLimitConfig(row.limits)),
20548
20882
  row.bodyLoggingOptOut,
20549
20883
  row.createdAt,
20550
- row.revokedAt
20884
+ row.revokedAt,
20885
+ row.expiresAt
20551
20886
  ]);
20552
20887
  },
20553
20888
  async setLimits(id, limits) {
@@ -20562,6 +20897,9 @@ function createKeyRepo(sql, logger2) {
20562
20897
  id
20563
20898
  ]);
20564
20899
  },
20900
+ async setExpiry(id, expiresAt) {
20901
+ await sql.unsafe("UPDATE api_keys SET expires_at = $1 WHERE id = $2", [expiresAt, id]);
20902
+ },
20565
20903
  async revoke(id) {
20566
20904
  await sql.unsafe("UPDATE api_keys SET revoked_at = $1 WHERE id = $2", [Date.now(), id]);
20567
20905
  }
@@ -20860,229 +21198,6 @@ function isRtkFilterId(value) {
20860
21198
  return FILTER_IDS.has(value);
20861
21199
  }
20862
21200
 
20863
- // packages/store/src/sqlite/rollup.ts
20864
- var HOUR_MS = 3600000;
20865
- function hourOf(at) {
20866
- return Math.floor(at / HOUR_MS);
20867
- }
20868
- function startOfLocalDay(at) {
20869
- const day = new Date(at);
20870
- day.setHours(0, 0, 0, 0);
20871
- return day.getTime();
20872
- }
20873
- var UPSERT = `
20874
- INSERT INTO usage_daily
20875
- (day, provider, credential_id, requested_model, resolved_model, api_key_id,
20876
- requests, errors, input_tokens, output_tokens, cache_read_tokens,
20877
- cache_write_tokens, rtk_saved_tokens, rtk_applied_requests, cost_usd, duration_ms_sum)
20878
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
20879
- ON CONFLICT (day, provider, credential_id, requested_model, resolved_model, api_key_id)
20880
- DO UPDATE SET
20881
- requests = requests + excluded.requests,
20882
- errors = errors + excluded.errors,
20883
- input_tokens = input_tokens + excluded.input_tokens,
20884
- output_tokens = output_tokens + excluded.output_tokens,
20885
- cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
20886
- cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
20887
- rtk_saved_tokens = rtk_saved_tokens + excluded.rtk_saved_tokens,
20888
- rtk_applied_requests = rtk_applied_requests + excluded.rtk_applied_requests,
20889
- cost_usd = cost_usd + excluded.cost_usd,
20890
- duration_ms_sum = duration_ms_sum + excluded.duration_ms_sum`;
20891
- function keyOf(log) {
20892
- return [
20893
- startOfLocalDay(log.at),
20894
- log.resolvedProvider ?? "",
20895
- log.credentialId ?? "",
20896
- log.requestedModel,
20897
- log.resolvedModel ?? "",
20898
- log.apiKeyId ?? ""
20899
- ];
20900
- }
20901
- function countersOf(log) {
20902
- return {
20903
- requests: 1,
20904
- errors: log.status >= 400 ? 1 : 0,
20905
- inputTokens: log.inputTokens,
20906
- outputTokens: log.outputTokens,
20907
- cacheReadTokens: log.cacheReadTokens,
20908
- cacheWriteTokens: log.cacheWriteTokens,
20909
- rtkSavedTokens: log.rtkEstimatedTokensSaved,
20910
- rtkAppliedRequests: log.rtkApplied ? 1 : 0,
20911
- costUsd: log.costUsd,
20912
- durationMsSum: log.durationMs
20913
- };
20914
- }
20915
- function upsert(db, key, c) {
20916
- db.run(UPSERT, [
20917
- ...key,
20918
- c.requests,
20919
- c.errors,
20920
- c.inputTokens,
20921
- c.outputTokens,
20922
- c.cacheReadTokens,
20923
- c.cacheWriteTokens,
20924
- c.rtkSavedTokens,
20925
- c.rtkAppliedRequests,
20926
- c.costUsd,
20927
- c.durationMsSum
20928
- ]);
20929
- }
20930
- function rollupLog(db, log) {
20931
- upsert(db, keyOf(log), countersOf(log));
20932
- }
20933
- function backfillDaily(db) {
20934
- const groups = new Map;
20935
- for (const row of db.query(`SELECT at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
20936
- status, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
20937
- duration_ms, cost_usd
20938
- FROM request_logs`).all()) {
20939
- const key = [
20940
- startOfLocalDay(row.at),
20941
- row.resolved_provider ?? "",
20942
- row.credential_id ?? "",
20943
- row.requested_model,
20944
- row.resolved_model ?? "",
20945
- row.api_key_id ?? ""
20946
- ];
20947
- const id = key.join("\x00");
20948
- const seen = groups.get(id);
20949
- const counters = seen?.counters ?? {
20950
- requests: 0,
20951
- errors: 0,
20952
- inputTokens: 0,
20953
- outputTokens: 0,
20954
- cacheReadTokens: 0,
20955
- cacheWriteTokens: 0,
20956
- rtkSavedTokens: 0,
20957
- rtkAppliedRequests: 0,
20958
- costUsd: 0,
20959
- durationMsSum: 0
20960
- };
20961
- counters.requests += 1;
20962
- counters.errors += row.status >= 400 ? 1 : 0;
20963
- counters.inputTokens += row.input_tokens;
20964
- counters.outputTokens += row.output_tokens;
20965
- counters.cacheReadTokens += row.cache_read_tokens;
20966
- counters.cacheWriteTokens += row.cache_write_tokens;
20967
- counters.costUsd += row.cost_usd;
20968
- counters.durationMsSum += row.duration_ms;
20969
- if (seen === undefined)
20970
- groups.set(id, { key, counters });
20971
- }
20972
- for (const group of groups.values())
20973
- upsert(db, group.key, group.counters);
20974
- return groups.size;
20975
- }
20976
- var HOURLY_UPSERT = `
20977
- INSERT INTO usage_rollup
20978
- (api_key_id, hour, requests, input_tokens, output_tokens,
20979
- cache_read_tokens, cache_write_tokens, cost_usd)
20980
- VALUES (?,?,?,?,?,?,?,?)
20981
- ON CONFLICT (api_key_id, hour)
20982
- DO UPDATE SET
20983
- requests = requests + excluded.requests,
20984
- input_tokens = input_tokens + excluded.input_tokens,
20985
- output_tokens = output_tokens + excluded.output_tokens,
20986
- cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
20987
- cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
20988
- cost_usd = cost_usd + excluded.cost_usd`;
20989
- function rollupHour(db, log) {
20990
- const apiKeyId = log.apiKeyId;
20991
- if (apiKeyId === null)
20992
- return;
20993
- db.run(HOURLY_UPSERT, [
20994
- apiKeyId,
20995
- hourOf(log.at),
20996
- 1,
20997
- log.inputTokens,
20998
- log.outputTokens,
20999
- log.cacheReadTokens,
21000
- log.cacheWriteTokens,
21001
- log.costUsd
21002
- ]);
21003
- }
21004
- var REBUILD = `
21005
- INSERT INTO usage_rollup
21006
- (api_key_id, hour, requests, input_tokens, output_tokens,
21007
- cache_read_tokens, cache_write_tokens, cost_usd)
21008
- SELECT api_key_id,
21009
- CAST(at / 3600000 AS INTEGER),
21010
- COUNT(*),
21011
- COALESCE(SUM(input_tokens), 0),
21012
- COALESCE(SUM(output_tokens), 0),
21013
- COALESCE(SUM(cache_read_tokens), 0),
21014
- COALESCE(SUM(cache_write_tokens), 0),
21015
- COALESCE(SUM(cost_usd), 0)
21016
- FROM request_logs
21017
- WHERE state = 'done' AND api_key_id IS NOT NULL
21018
- GROUP BY api_key_id, CAST(at / 3600000 AS INTEGER)`;
21019
- function rebuildRollup(db) {
21020
- db.run("DELETE FROM usage_rollup");
21021
- db.run(REBUILD);
21022
- }
21023
- function auditRollup(db) {
21024
- const row = db.query(`WITH truth AS (
21025
- SELECT api_key_id AS k,
21026
- CAST(at / 3600000 AS INTEGER) AS h,
21027
- COUNT(*) AS requests,
21028
- COALESCE(SUM(input_tokens), 0) AS input_tokens,
21029
- COALESCE(SUM(output_tokens), 0) AS output_tokens,
21030
- COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
21031
- COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,
21032
- COALESCE(SUM(cost_usd), 0) AS cost_usd
21033
- FROM request_logs
21034
- WHERE state = 'done' AND api_key_id IS NOT NULL
21035
- GROUP BY k, h
21036
- )
21037
- SELECT (SELECT COUNT(*) FROM truth) AS buckets,
21038
- (SELECT COUNT(*)
21039
- FROM truth
21040
- LEFT JOIN usage_rollup r ON r.api_key_id = truth.k AND r.hour = truth.h
21041
- WHERE r.api_key_id IS NULL
21042
- OR r.requests <> truth.requests
21043
- OR r.input_tokens <> truth.input_tokens
21044
- OR r.output_tokens <> truth.output_tokens
21045
- OR r.cache_read_tokens <> truth.cache_read_tokens
21046
- OR r.cache_write_tokens <> truth.cache_write_tokens
21047
- OR ABS(r.cost_usd - truth.cost_usd) > 1e-9 + ABS(truth.cost_usd) * 1e-9
21048
- ) AS mismatched,
21049
- (SELECT COUNT(*)
21050
- FROM usage_rollup r
21051
- LEFT JOIN truth ON r.api_key_id = truth.k AND r.hour = truth.h
21052
- WHERE truth.k IS NULL
21053
- ) AS orphans`).get();
21054
- const buckets = row?.buckets ?? 0;
21055
- const mismatched = (row?.mismatched ?? 0) + (row?.orphans ?? 0);
21056
- return { buckets, mismatched, ok: mismatched === 0 };
21057
- }
21058
- function backfillRtkUsage(db) {
21059
- const groups = new Map;
21060
- for (const row of db.query(`SELECT at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
21061
- rtk_applied, rtk_estimated_tokens_saved
21062
- FROM request_logs
21063
- WHERE state = 'done'`).all()) {
21064
- const key = [
21065
- startOfLocalDay(row.at),
21066
- row.resolved_provider ?? "",
21067
- row.credential_id ?? "",
21068
- row.requested_model,
21069
- row.resolved_model ?? "",
21070
- row.api_key_id ?? ""
21071
- ];
21072
- const id = key.join("\x00");
21073
- const group = groups.get(id) ?? { key, saved: 0, applied: 0 };
21074
- group.saved += row.rtk_estimated_tokens_saved;
21075
- group.applied += row.rtk_applied === 1 ? 1 : 0;
21076
- groups.set(id, group);
21077
- }
21078
- for (const { key, saved, applied } of groups.values()) {
21079
- db.run(`UPDATE usage_daily
21080
- SET rtk_saved_tokens = ?, rtk_applied_requests = ?
21081
- WHERE day = ? AND provider = ? AND credential_id = ? AND requested_model = ?
21082
- AND resolved_model = ? AND api_key_id = ?`, [saved, applied, ...key]);
21083
- }
21084
- }
21085
-
21086
21201
  // packages/store/src/postgres/rollup.ts
21087
21202
  var DAILY_UPSERT = `
21088
21203
  INSERT INTO usage_daily
@@ -21102,9 +21217,9 @@ var DAILY_UPSERT = `
21102
21217
  rtk_applied_requests = usage_daily.rtk_applied_requests + EXCLUDED.rtk_applied_requests,
21103
21218
  cost_usd = usage_daily.cost_usd + EXCLUDED.cost_usd,
21104
21219
  duration_ms_sum = usage_daily.duration_ms_sum + EXCLUDED.duration_ms_sum`;
21105
- async function rollupLog2(conn, log) {
21220
+ async function rollupLog2(conn, log, dayOffsetMinutes) {
21106
21221
  await conn.unsafe(DAILY_UPSERT, [
21107
- startOfLocalDay(log.at),
21222
+ startOfDay(log.at, dayOffsetMinutes),
21108
21223
  log.resolvedProvider ?? "",
21109
21224
  log.credentialId ?? "",
21110
21225
  log.requestedModel,
@@ -21348,7 +21463,7 @@ var sum = (row) => ({
21348
21463
  tokens: row === undefined ? 0 : num(row.tokens),
21349
21464
  costUsd: row === undefined ? 0 : row.cost_usd
21350
21465
  });
21351
- function createUsageRepo(sql, nodeId) {
21466
+ function createUsageRepo(sql, nodeId, dayOffsetMinutes) {
21352
21467
  const complete = async (conn, log) => {
21353
21468
  const written = (await conn.unsafe(COMPLETE, values(log, "done", nodeId))).count;
21354
21469
  if (written === 0)
@@ -21356,7 +21471,7 @@ function createUsageRepo(sql, nodeId) {
21356
21471
  const stored = (await conn.unsafe("SELECT * FROM request_logs WHERE id = $1", [log.id]))[0];
21357
21472
  if (stored !== undefined) {
21358
21473
  const restored = toLog(stored);
21359
- await rollupLog2(conn, restored);
21474
+ await rollupLog2(conn, restored, dayOffsetMinutes);
21360
21475
  await rollupHour2(conn, restored);
21361
21476
  }
21362
21477
  };
@@ -21387,6 +21502,10 @@ function createUsageRepo(sql, nodeId) {
21387
21502
  ]) : await sql.unsafe("SELECT * FROM request_logs WHERE api_key_id = $1 ORDER BY at DESC LIMIT $2", [apiKeyId, limit]);
21388
21503
  return rows.map(toLog);
21389
21504
  },
21505
+ async lastUsedByCredential(credentialId) {
21506
+ const rows = await sql.unsafe("SELECT MAX(at) AS at FROM request_logs WHERE credential_id = $1", [credentialId]);
21507
+ return numOrNull(rows[0]?.at ?? null);
21508
+ },
21390
21509
  async scan(cursor, limit) {
21391
21510
  const rows = cursor === null ? await sql.unsafe("SELECT * FROM request_logs ORDER BY at, id LIMIT $1", [
21392
21511
  limit
@@ -21449,7 +21568,7 @@ function createUsageRepo(sql, nodeId) {
21449
21568
  const daily = grain === "daily";
21450
21569
  const key = columnFor(grain, q.groupBy);
21451
21570
  const split = q.splitBy === undefined ? null : columnFor(grain, q.splitBy);
21452
- const since = daily ? startOfLocalDay(q.since) : q.since;
21571
+ const since = daily ? startOfDay(q.since, dayOffsetMinutes) : q.since;
21453
21572
  const until = q.until ?? Number.MAX_SAFE_INTEGER;
21454
21573
  const timeColumn = daily ? "day" : "at";
21455
21574
  const scope = q.apiKeyId;
@@ -21525,6 +21644,7 @@ function redacted(url2) {
21525
21644
  async function createPostgresStore(opts) {
21526
21645
  const logger2 = opts.logger ?? noopLogger;
21527
21646
  const nodeId = opts.nodeId ?? crypto.randomUUID();
21647
+ const dayOffsetMinutes = opts.dayOffsetMinutes ?? hostDayOffsetMinutes();
21528
21648
  const listeners = new Set;
21529
21649
  const emit2 = (change) => {
21530
21650
  for (const listener of listeners) {
@@ -21556,7 +21676,7 @@ async function createPostgresStore(opts) {
21556
21676
  credentials: createCredentialRepo(sql, opts.encryptionKey, emit2),
21557
21677
  config: createConfigRepo(sql, emit2),
21558
21678
  keys: createKeyRepo(sql, logger2),
21559
- usage: createUsageRepo(sql, nodeId),
21679
+ usage: createUsageRepo(sql, nodeId, dayOffsetMinutes),
21560
21680
  bodies: createBodyRepo(sql, opts.encryptionKey),
21561
21681
  maintenance: createMaintenanceRepo(sql, nodeId),
21562
21682
  plugins: createPluginRepo(sql),
@@ -21793,22 +21913,18 @@ function toHealth2(r) {
21793
21913
  breakerState: r.breaker_state,
21794
21914
  consecutiveFailures: r.consecutive_failures,
21795
21915
  openedAt: r.opened_at,
21796
- rateLimitedUntil: r.rate_limited_until,
21797
- ewmaTtftMs: r.ewma_ttft_ms,
21798
- lastUsedAt: r.last_used_at
21916
+ rateLimitedUntil: r.rate_limited_until
21799
21917
  };
21800
21918
  }
21801
21919
  var UPSERT_HEALTH2 = `INSERT INTO credential_health
21802
21920
  (credential_id, model, breaker_state, consecutive_failures, opened_at,
21803
- rate_limited_until, ewma_ttft_ms, last_used_at)
21804
- VALUES (?,?,?,?,?,?,?,?)
21921
+ rate_limited_until)
21922
+ VALUES (?,?,?,?,?,?)
21805
21923
  ON CONFLICT (credential_id, model) DO UPDATE SET
21806
21924
  breaker_state = excluded.breaker_state,
21807
21925
  consecutive_failures = excluded.consecutive_failures,
21808
21926
  opened_at = excluded.opened_at,
21809
- rate_limited_until = excluded.rate_limited_until,
21810
- ewma_ttft_ms = excluded.ewma_ttft_ms,
21811
- last_used_at = excluded.last_used_at`;
21927
+ rate_limited_until = excluded.rate_limited_until`;
21812
21928
  function createCredentialRepo2(db, key, emit2 = () => {}) {
21813
21929
  const open2 = async (v) => v === null ? null : decrypt(key, v);
21814
21930
  const secretsFrom = async (row) => ({
@@ -21991,22 +22107,13 @@ function createCredentialRepo2(db, key, emit2 = () => {}) {
21991
22107
  async listHealth() {
21992
22108
  return db.query("SELECT * FROM credential_health").all().map(toHealth2);
21993
22109
  },
21994
- async saveHealth(rows) {
21995
- const stmt = db.prepare(UPSERT_HEALTH2);
21996
- db.transaction(() => {
21997
- for (const r of rows) {
21998
- stmt.run(r.credentialId, r.model, r.breakerState, r.consecutiveFailures, r.openedAt, r.rateLimitedUntil, r.ewmaTtftMs, r.lastUsedAt);
21999
- }
22000
- })();
22001
- emit2({ type: "healthSaved", rows });
22002
- },
22003
22110
  async updateHealth(credentialId, model, apply) {
22004
22111
  const current = db.prepare("SELECT * FROM credential_health WHERE credential_id = ? AND model = ?");
22005
22112
  const stmt = db.prepare(UPSERT_HEALTH2);
22006
22113
  const written = db.transaction(() => {
22007
22114
  const row = current.get(credentialId, model);
22008
22115
  const next = apply(row === null ? null : toHealth2(row));
22009
- stmt.run(next.credentialId, next.model, next.breakerState, next.consecutiveFailures, next.openedAt, next.rateLimitedUntil, next.ewmaTtftMs, next.lastUsedAt);
22116
+ stmt.run(next.credentialId, next.model, next.breakerState, next.consecutiveFailures, next.openedAt, next.rateLimitedUntil);
22010
22117
  return next;
22011
22118
  })();
22012
22119
  emit2({ type: "healthSaved", rows: [written] });
@@ -22569,6 +22676,63 @@ CREATE TABLE nodes (
22569
22676
  ALTER TABLE credentials ADD COLUMN token_version INTEGER NOT NULL DEFAULT 0;
22570
22677
  `;
22571
22678
 
22679
+ // packages/store/src/sqlite/migrations/013_health_measurements.sql
22680
+ var _013_health_measurements_default = `-- DEPLOY HAZARD \u2014 read before upgrading an install that is still serving.
22681
+ --
22682
+ -- This migration drops two columns that every gateway before it writes on
22683
+ -- the success path (\`UPSERT_HEALTH\` names \`ewma_ttft_ms\` and \`last_used_at\`).
22684
+ -- SQLite is single-process, so the hazard is the rollback, not the rollout: an
22685
+ -- older gateway started against this database fails every successful request
22686
+ -- after the upstream call has completed and been billed \u2014 content frames out,
22687
+ -- then an aborted connection with no terminal frame. Rolling back the binary
22688
+ -- does not roll back the schema. The only way back is a snapshot taken before
22689
+ -- the upgrade. Deliberate: kept as a drop, not expand/contract, on the
22690
+ -- operator's decision \u2014 see the write-path spec.
22691
+ --
22692
+ -- \`credential_health\` keeps decisions, not measurements.
22693
+ --
22694
+ -- \`ewma_ttft_ms\` and \`last_used_at\` were written on every successful request,
22695
+ -- and a success that changes nothing else now writes nothing. Both live in the
22696
+ -- gateway's process-local \`loadRegistry\` instead: latency is a property of
22697
+ -- *this* process's path to the provider, and the round-robin tiebreak reads
22698
+ -- the value the process holds. The console's "last used" reads
22699
+ -- \`request_logs\` through \`usage.lastUsedByCredential\`, served by
22700
+ -- \`idx_request_logs_cred\`.
22701
+ --
22702
+ -- Neither column is indexed or constrained, so \`DROP COLUMN\` is permitted.
22703
+ ALTER TABLE credential_health DROP COLUMN ewma_ttft_ms;
22704
+ ALTER TABLE credential_health DROP COLUMN last_used_at;
22705
+ `;
22706
+
22707
+ // packages/store/src/sqlite/migrations/014_key_expiry.sql
22708
+ var _014_key_expiry_default = `-- An optional expiry on a gateway API key: an absolute epoch-ms instant after
22709
+ -- which the key is refused, spelled exactly as \`credentials.expires_at\` is.
22710
+ --
22711
+ -- Nullable rather than \`NOT NULL DEFAULT\`, which is the opposite of the choice
22712
+ -- migration 9 made for \`limits\`, and for the opposite reason. There, every
22713
+ -- possible value is a policy and \`{}\` is the one that means "no ceiling", so a
22714
+ -- default gives "unlimited" a single spelling. Here the column is a deadline,
22715
+ -- and no number means "no deadline" \u2014 every finite default is a date on which
22716
+ -- somebody's client stops working. NULL is the only value that says nothing was
22717
+ -- decided, so it is the column's own default and every reader maps it to
22718
+ -- "never expires".
22719
+ --
22720
+ -- No backfill, deliberately, and this is the whole substance of the migration.
22721
+ -- Every key on every existing installation was minted on the promise that it
22722
+ -- works until revoked; an upgrade that attaches an expiry to those rows breaks
22723
+ -- clients on a date the operator never chose, and the failure surfaces days
22724
+ -- later as an authentication error with nothing in it that names this change.
22725
+ -- Adding the column and stopping is what keeps that promise, so the absence of
22726
+ -- an \`UPDATE\` here is load-bearing rather than an omission \u2014
22727
+ -- \`packages/store/test/migrations.test.ts\` pins it.
22728
+ --
22729
+ -- No index. The column is read only after a row has already been found by
22730
+ -- \`hash\` or by \`id\`, both of which are unique, so there is no scan for an index
22731
+ -- to help; and nothing sweeps expired keys, because an expired key stays listed
22732
+ -- exactly as a revoked one does so its usage history keeps its attribution.
22733
+ ALTER TABLE api_keys ADD COLUMN expires_at INTEGER;
22734
+ `;
22735
+
22572
22736
  // packages/store/src/sqlite/db.ts
22573
22737
  var MIGRATIONS2 = [
22574
22738
  { id: 1, sql: _001_init_default2 },
@@ -22582,9 +22746,16 @@ var MIGRATIONS2 = [
22582
22746
  { id: 9, sql: _009_key_limits_default },
22583
22747
  { id: 10, sql: _010_usage_rollup_default, after: rebuildRollup },
22584
22748
  { id: 11, sql: _011_plugin_migrations_default },
22585
- { id: 12, sql: _012_nodes_default }
22749
+ { id: 12, sql: _012_nodes_default },
22750
+ { id: 13, sql: _013_health_measurements_default },
22751
+ { id: 14, sql: _014_key_expiry_default }
22586
22752
  ];
22587
- function openDb(path) {
22753
+ function statements(sql) {
22754
+ return sql.split(`
22755
+ `).filter((line) => !line.trimStart().startsWith("--")).join(`
22756
+ `).split(/;[ \t]*(?:\n|$)/).map((s) => s.trim()).filter((s) => s.length > 0);
22757
+ }
22758
+ function openDb(path, dayOffsetMinutes = hostDayOffsetMinutes()) {
22588
22759
  const db = new Database(path, { create: true });
22589
22760
  db.run("PRAGMA journal_mode = WAL");
22590
22761
  db.run("PRAGMA synchronous = NORMAL");
@@ -22596,8 +22767,9 @@ function openDb(path) {
22596
22767
  if (done.has(m.id))
22597
22768
  continue;
22598
22769
  db.transaction(() => {
22599
- db.run(m.sql);
22600
- m.after?.(db);
22770
+ for (const statement of statements(m.sql))
22771
+ db.run(statement);
22772
+ m.after?.(db, dayOffsetMinutes);
22601
22773
  db.run("INSERT INTO migrations (id, applied_at) VALUES (?, ?)", [m.id, Date.now()]);
22602
22774
  })();
22603
22775
  }
@@ -22632,7 +22804,8 @@ var toKey2 = (r, logger2) => ({
22632
22804
  limits: parseLimits2(r.id, r.limits, logger2),
22633
22805
  bodyLoggingOptOut: r.body_logging_opt_out === 1,
22634
22806
  createdAt: r.created_at,
22635
- revokedAt: r.revoked_at
22807
+ revokedAt: r.revoked_at,
22808
+ expiresAt: r.expires_at
22636
22809
  });
22637
22810
  function createKeyRepo2(db, logger2 = noopLogger) {
22638
22811
  return {
@@ -22650,8 +22823,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
22650
22823
  async create(input2) {
22651
22824
  const now = Date.now();
22652
22825
  db.run(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
22653
- body_logging_opt_out, created_at, revoked_at)
22654
- VALUES (?,?,?,?,?,?,?,?,NULL)`, [
22826
+ body_logging_opt_out, created_at, revoked_at, expires_at)
22827
+ VALUES (?,?,?,?,?,?,?,?,NULL,?)`, [
22655
22828
  input2.id,
22656
22829
  input2.label,
22657
22830
  input2.prefix,
@@ -22659,7 +22832,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
22659
22832
  input2.modelAllowlist === null ? null : JSON.stringify(input2.modelAllowlist),
22660
22833
  JSON.stringify(parseLimitConfig(input2.limits)),
22661
22834
  input2.bodyLoggingOptOut ? 1 : 0,
22662
- now
22835
+ now,
22836
+ input2.expiresAt
22663
22837
  ]);
22664
22838
  return { ...input2, createdAt: now, revokedAt: null };
22665
22839
  },
@@ -22667,8 +22841,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
22667
22841
  if (row.limits === null)
22668
22842
  throw new Error(`api key ${row.id} has unreadable limits`);
22669
22843
  db.run(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
22670
- body_logging_opt_out, created_at, revoked_at)
22671
- VALUES (?,?,?,?,?,?,?,?,?)`, [
22844
+ body_logging_opt_out, created_at, revoked_at, expires_at)
22845
+ VALUES (?,?,?,?,?,?,?,?,?,?)`, [
22672
22846
  row.id,
22673
22847
  row.label,
22674
22848
  row.prefix,
@@ -22677,7 +22851,8 @@ function createKeyRepo2(db, logger2 = noopLogger) {
22677
22851
  JSON.stringify(parseLimitConfig(row.limits)),
22678
22852
  row.bodyLoggingOptOut ? 1 : 0,
22679
22853
  row.createdAt,
22680
- row.revokedAt
22854
+ row.revokedAt,
22855
+ row.expiresAt
22681
22856
  ]);
22682
22857
  },
22683
22858
  async setLimits(id, limits) {
@@ -22692,6 +22867,9 @@ function createKeyRepo2(db, logger2 = noopLogger) {
22692
22867
  id
22693
22868
  ]);
22694
22869
  },
22870
+ async setExpiry(id, expiresAt) {
22871
+ db.run("UPDATE api_keys SET expires_at = ? WHERE id = ?", [expiresAt, id]);
22872
+ },
22695
22873
  async revoke(id) {
22696
22874
  db.run("UPDATE api_keys SET revoked_at = ? WHERE id = ?", [Date.now(), id]);
22697
22875
  }
@@ -23014,14 +23192,14 @@ var COMPLETE2 = `INSERT INTO request_logs ${COLUMNS2} VALUES ${PLACEHOLDERS2}
23014
23192
  rtk_estimated_tokens_saved = excluded.rtk_estimated_tokens_saved,
23015
23193
  rtk_filters = excluded.rtk_filters
23016
23194
  WHERE request_logs.state = 'pending'`;
23017
- function createUsageRepo2(db, nodeId) {
23195
+ function createUsageRepo2(db, nodeId, dayOffsetMinutes = hostDayOffsetMinutes()) {
23018
23196
  const complete = db.transaction((log) => {
23019
23197
  if (db.run(COMPLETE2, values2(log, "done", nodeId)).changes === 0)
23020
23198
  return;
23021
23199
  const stored = db.query("SELECT * FROM request_logs WHERE id = ?").get(log.id);
23022
23200
  if (stored !== null) {
23023
23201
  const restored = toLog2(stored);
23024
- rollupLog(db, restored);
23202
+ rollupLog(db, restored, dayOffsetMinutes);
23025
23203
  rollupHour(db, restored);
23026
23204
  }
23027
23205
  });
@@ -23071,6 +23249,10 @@ function createUsageRepo2(db, nodeId) {
23071
23249
  }
23072
23250
  return db.query("SELECT * FROM request_logs WHERE api_key_id = ? ORDER BY at DESC LIMIT ?").all(apiKeyId, limit).map(toLog2);
23073
23251
  },
23252
+ async lastUsedByCredential(credentialId) {
23253
+ const row = db.query("SELECT MAX(at) AS at FROM request_logs WHERE credential_id = ?").get(credentialId);
23254
+ return row?.at ?? null;
23255
+ },
23074
23256
  async scan(cursor, limit) {
23075
23257
  const rows = cursor === null ? db.query("SELECT * FROM request_logs ORDER BY at, id LIMIT ?").all(limit) : db.query(`SELECT * FROM request_logs
23076
23258
  WHERE at > ? OR (at = ? AND id > ?)
@@ -23132,7 +23314,7 @@ function createUsageRepo2(db, nodeId) {
23132
23314
  const daily = grain === "daily";
23133
23315
  const key = columnFor2(grain, q.groupBy);
23134
23316
  const split = q.splitBy === undefined ? null : columnFor2(grain, q.splitBy);
23135
- const since = daily ? startOfLocalDay(q.since) : q.since;
23317
+ const since = daily ? startOfDay(q.since, dayOffsetMinutes) : q.since;
23136
23318
  const until = q.until ?? Number.MAX_SAFE_INTEGER;
23137
23319
  const timeColumn = daily ? "day" : "at";
23138
23320
  const scope = q.apiKeyId;
@@ -23183,6 +23365,7 @@ function createUsageRepo2(db, nodeId) {
23183
23365
  async function createStore(opts) {
23184
23366
  const logger2 = opts.logger ?? noopLogger;
23185
23367
  const nodeId = opts.nodeId ?? crypto.randomUUID();
23368
+ const dayOffsetMinutes = opts.dayOffsetMinutes ?? hostDayOffsetMinutes();
23186
23369
  const listeners = new Set;
23187
23370
  const emit2 = (change) => {
23188
23371
  for (const listener of listeners) {
@@ -23192,14 +23375,14 @@ async function createStore(opts) {
23192
23375
  }
23193
23376
  };
23194
23377
  const open2 = () => {
23195
- const db = openDb(opts.path);
23378
+ const db = openDb(opts.path, dayOffsetMinutes);
23196
23379
  logger2.debug("store opened", { path: opts.path });
23197
23380
  return {
23198
23381
  db,
23199
23382
  credentials: createCredentialRepo2(db, opts.encryptionKey, emit2),
23200
23383
  config: createConfigRepo2(db, emit2),
23201
23384
  keys: createKeyRepo2(db, logger2),
23202
- usage: createUsageRepo2(db, nodeId),
23385
+ usage: createUsageRepo2(db, nodeId, dayOffsetMinutes),
23203
23386
  bodies: createBodyRepo2(db, opts.encryptionKey, bodiesDirFor(opts.path)),
23204
23387
  maintenance: createMaintenanceRepo2(db, nodeId),
23205
23388
  plugins: createPluginRepo2(db)
@@ -23225,7 +23408,6 @@ async function createStore(opts) {
23225
23408
  updateSecrets: (id, secrets, expiresAt, expectedVersion) => handle.credentials.updateSecrets(id, secrets, expiresAt, expectedVersion),
23226
23409
  remove: (id) => handle.credentials.remove(id),
23227
23410
  listHealth: () => handle.credentials.listHealth(),
23228
- saveHealth: (rows) => handle.credentials.saveHealth(rows),
23229
23411
  updateHealth: (id, model, apply) => handle.credentials.updateHealth(id, model, apply),
23230
23412
  listQuota: () => handle.credentials.listQuota(),
23231
23413
  saveQuota: (rows) => handle.credentials.saveQuota(rows),
@@ -23252,6 +23434,7 @@ async function createStore(opts) {
23252
23434
  importRow: (row) => handle.keys.importRow(row),
23253
23435
  setLimits: (id, limits) => handle.keys.setLimits(id, limits),
23254
23436
  setModelAllowlist: (id, modelAllowlist) => handle.keys.setModelAllowlist(id, modelAllowlist),
23437
+ setExpiry: (id, expiresAt) => handle.keys.setExpiry(id, expiresAt),
23255
23438
  revoke: (id) => handle.keys.revoke(id)
23256
23439
  },
23257
23440
  usage: {
@@ -23260,6 +23443,7 @@ async function createStore(opts) {
23260
23443
  append: (log) => handle.usage.append(log),
23261
23444
  sweepPending: (now) => handle.usage.sweepPending(now),
23262
23445
  recent: (limit, apiKeyId) => handle.usage.recent(limit, apiKeyId),
23446
+ lastUsedByCredential: (credentialId) => handle.usage.lastUsedByCredential(credentialId),
23263
23447
  scan: (cursor, limit) => handle.usage.scan(cursor, limit),
23264
23448
  aggregate: (q) => handle.usage.aggregate(q),
23265
23449
  sumSince: (apiKeyId, sinceMs) => handle.usage.sumSince(apiKeyId, sinceMs),
@@ -23353,7 +23537,7 @@ function createAdminAuth(store, opts) {
23353
23537
  };
23354
23538
  const keyStillValid = async (apiKeyId) => {
23355
23539
  const key = await store.keys.get(apiKeyId);
23356
- return key !== null && key.revokedAt === null;
23540
+ return key !== null && keyUsable(key, opts.now());
23357
23541
  };
23358
23542
  const applyPassword = async (password) => {
23359
23543
  if (password.length < MIN_PASSWORD_LENGTH) {
@@ -23407,7 +23591,7 @@ function createAdminAuth(store, opts) {
23407
23591
  },
23408
23592
  async loginClient(rawKey) {
23409
23593
  const key = await store.keys.findByHash(await hashApiKey(rawKey));
23410
- if (key === null || key.revokedAt !== null)
23594
+ if (key === null || !keyUsable(key, opts.now()))
23411
23595
  return null;
23412
23596
  return issue2({ kind: "client", apiKeyId: key.id });
23413
23597
  },
@@ -24138,6 +24322,7 @@ var PROVIDER_IDS = Object.keys(PROVIDER_DESCRIPTORS);
24138
24322
  var MIN_KEY_LENGTH = 16;
24139
24323
  var TRUTHY = new Set(["1", "true", "yes", "on"]);
24140
24324
  var DECIMAL_INTEGER = /^\d+$/;
24325
+ var SIGNED_INTEGER = /^-?\d+$/;
24141
24326
  function optionalText(value, fallback) {
24142
24327
  return value?.trim() || fallback;
24143
24328
  }
@@ -24168,6 +24353,11 @@ function loadConfig(env) {
24168
24353
  } else if (databaseUrl !== undefined || redisUrl !== undefined) {
24169
24354
  throw new Error("OMNI_DATABASE_URL and OMNI_REDIS_URL are cluster-mode settings; set OMNI_CLUSTER_MODE=true or unset them");
24170
24355
  }
24356
+ const rawDayOffset = env.OMNI_DAY_OFFSET_MINUTES?.trim();
24357
+ const parsedDayOffset = rawDayOffset === undefined ? NaN : Number(rawDayOffset);
24358
+ const validDayOffset = rawDayOffset !== undefined && SIGNED_INTEGER.test(rawDayOffset) && Number.isInteger(parsedDayOffset) && parsedDayOffset >= -1440 && parsedDayOffset <= 1440;
24359
+ const dayOffsetMinutes = validDayOffset ? parsedDayOffset : hostDayOffsetMinutes();
24360
+ const dayOffsetFallbackFrom = rawDayOffset !== undefined && !validDayOffset ? rawDayOffset : null;
24171
24361
  const bodyLoggingAllowed = TRUTHY.has((env.OMNI_BODY_LOGGING_ALLOWED ?? "").trim().toLowerCase());
24172
24362
  const rawLogLevel = env.OMNI_LOG_LEVEL?.trim();
24173
24363
  const logLevel = parseLogLevel(rawLogLevel);
@@ -24198,6 +24388,8 @@ function loadConfig(env) {
24198
24388
  clusterMode,
24199
24389
  databaseUrl: clusterMode ? databaseUrl ?? null : null,
24200
24390
  redisUrl: clusterMode ? redisUrl ?? null : null,
24391
+ dayOffsetMinutes,
24392
+ dayOffsetFallbackFrom,
24201
24393
  metricsToken,
24202
24394
  metricsMaxSeries,
24203
24395
  otlpEndpoint,
@@ -30874,16 +31066,19 @@ var modelSchema = exports_external.object({
30874
31066
  isAlias: exports_external.boolean(),
30875
31067
  targets: exports_external.array(targetSchema).min(1, "a virtual model needs at least one target")
30876
31068
  });
31069
+ var MAX_INSTANT = 8640000000000000;
30877
31070
  var keyCreateSchema = exports_external.object({
30878
31071
  label: exports_external.string().min(1).default("api key"),
30879
31072
  modelAllowlist: exports_external.array(exports_external.string().min(1)).nullable().default(null),
30880
31073
  limits: limitConfigSchema.default({}),
30881
- bodyLoggingOptOut: exports_external.boolean().default(false)
31074
+ bodyLoggingOptOut: exports_external.boolean().default(false),
31075
+ expiresAt: exports_external.number().int().positive().max(MAX_INSTANT).nullable().default(null)
30882
31076
  }).strict();
30883
31077
  var keyLimitsSchema = exports_external.object({ limits: limitConfigSchema }).strict();
30884
31078
  var keyModelsSchema = exports_external.object({
30885
31079
  modelAllowlist: exports_external.array(exports_external.string().min(1)).nullable()
30886
31080
  }).strict();
31081
+ var keyExpirySchema = exports_external.object({ expiresAt: exports_external.number().int().positive().max(MAX_INSTANT).nullable() }).strict();
30887
31082
  var retentionSchema = exports_external.object({
30888
31083
  keepLatest: exports_external.number().int().min(1).max(100),
30889
31084
  maxAgeDays: exports_external.number().int().min(1).max(3650)
@@ -31181,10 +31376,11 @@ function eligible(input2) {
31181
31376
  drop("disabled");
31182
31377
  continue;
31183
31378
  }
31184
- if (credential.authType === "oauth" && credential.expiresAt !== null && credential.expiresAt <= now && !credential.hasRefreshToken) {
31379
+ if (credentialExpired(credential, now)) {
31185
31380
  drop("expired");
31186
31381
  continue;
31187
31382
  }
31383
+ let probe = false;
31188
31384
  const h = snapshot.health.get(healthKey(credential.id, target.model));
31189
31385
  if (h !== undefined) {
31190
31386
  if (h.rateLimitedUntil !== null && h.rateLimitedUntil > now) {
@@ -31197,6 +31393,9 @@ function eligible(input2) {
31197
31393
  drop("breaker:open");
31198
31394
  continue;
31199
31395
  }
31396
+ probe = true;
31397
+ } else if (h.breakerState === "halfOpen") {
31398
+ probe = true;
31200
31399
  }
31201
31400
  }
31202
31401
  const spent = (snapshot.quota.get(credential.id) ?? []).find((w) => w.limit !== null && w.used >= w.limit && (w.resetsAt === null || w.resetsAt > now));
@@ -31204,7 +31403,7 @@ function eligible(input2) {
31204
31403
  drop(`quota:${spent.windowType}`);
31205
31404
  continue;
31206
31405
  }
31207
- pairs.push({ credential, target });
31406
+ pairs.push({ credential, target, probe });
31208
31407
  }
31209
31408
  if (target.credentialId === undefined && providerSeen && !servedAny) {
31210
31409
  excluded.push({
@@ -31281,7 +31480,7 @@ function healthScore(h) {
31281
31480
  return h?.breakerState === "open" || h?.breakerState === "halfOpen" ? base * 0.5 : base;
31282
31481
  }
31283
31482
  function score(pairs, input2) {
31284
- const { snapshot, now, load } = input2;
31483
+ const { snapshot, now, load, recent } = input2;
31285
31484
  const w = snapshot.settings.weights;
31286
31485
  const tiers = pairs.map((p) => p.target.tier);
31287
31486
  const minTier = Math.min(...tiers);
@@ -31289,9 +31488,10 @@ function score(pairs, input2) {
31289
31488
  const tokens2 = estimateTokens(input2.request);
31290
31489
  const costs = pairs.map((p) => requestCost(p.target, tokens2));
31291
31490
  const bestCost = bestPositive(costs);
31491
+ const ewmaOf = (key) => recent?.get(key)?.ewmaTtftMs ?? null;
31292
31492
  const bestLatency = bestPositive(pairs.flatMap((p) => {
31293
- const h = snapshot.health.get(healthKey(p.credential.id, p.target.model));
31294
- return h?.ewmaTtftMs != null ? [h.ewmaTtftMs] : [];
31493
+ const ewma = ewmaOf(healthKey(p.credential.id, p.target.model));
31494
+ return ewma === null ? [] : [ewma];
31295
31495
  }));
31296
31496
  return pairs.map((pair, i) => {
31297
31497
  const key = healthKey(pair.credential.id, pair.target.model);
@@ -31303,12 +31503,14 @@ function score(pairs, input2) {
31303
31503
  const quota = quotaHeadroom(pair.credential, snapshot.quota.get(pair.credential.id) ?? [], now, snapshot.settings.quotaPollIntervalMs);
31304
31504
  const ownCost = costs[i];
31305
31505
  const cost = ownCost <= 0 || bestCost === null ? UNKNOWN : ratio(ownCost, bestCost);
31306
- const latency = h?.ewmaTtftMs == null || h.ewmaTtftMs <= 0 || bestLatency === null ? UNKNOWN : ratio(h.ewmaTtftMs, bestLatency);
31506
+ const ewma = ewmaOf(key);
31507
+ const latency = ewma === null || ewma <= 0 || bestLatency === null ? UNKNOWN : ratio(ewma, bestLatency);
31307
31508
  const reasons = { tier, health, quota, cost, latency, load: loadTerm };
31308
31509
  const base = tier * w.tier + health * w.health + quota * w.quota + cost * w.cost + latency * w.latency + loadTerm * w.load;
31309
31510
  return {
31310
31511
  credential: pair.credential,
31311
31512
  target: pair.target,
31513
+ probe: pair.probe,
31312
31514
  score: base * pair.credential.weight * pair.target.weight,
31313
31515
  reasons
31314
31516
  };
@@ -31348,8 +31550,8 @@ function rank(input2) {
31348
31550
  break;
31349
31551
  case "roundRobin": {
31350
31552
  const idle = (c) => {
31351
- const h = input2.snapshot.health.get(healthKey(c.credential.id, c.target.model));
31352
- return h?.lastUsedAt == null ? Number.POSITIVE_INFINITY : input2.now - h.lastUsedAt;
31553
+ const at = input2.recent?.get(healthKey(c.credential.id, c.target.model))?.lastReleasedAt;
31554
+ return at === undefined ? Number.POSITIVE_INFINITY : input2.now - at;
31353
31555
  };
31354
31556
  const spent = (c) => headroom(c) < QUOTA_FLOOR ? 1 : 0;
31355
31557
  scored.sort((a, b) => spent(a) - spent(b) || inflight(a) - inflight(b) || idle(b) - idle(a));
@@ -31521,16 +31723,23 @@ async function refreshCredential(deps, id) {
31521
31723
  return getCredential(deps.store, id);
31522
31724
  }
31523
31725
  async function credentialHealth(deps) {
31524
- const [health, quota, settings] = await Promise.all([
31726
+ const [health, quota, settings, credentials] = await Promise.all([
31525
31727
  deps.store.credentials.listHealth(),
31526
31728
  deps.store.credentials.listQuota(),
31527
- deps.store.config.getSettings()
31729
+ deps.store.config.getSettings(),
31730
+ deps.store.credentials.list()
31528
31731
  ]);
31732
+ const lastUsed = {};
31733
+ for (const c of credentials) {
31734
+ const at = await deps.store.usage.lastUsedByCredential(c.id);
31735
+ if (at !== null)
31736
+ lastUsed[c.id] = at;
31737
+ }
31529
31738
  const burn = burnEstimates(quota, {
31530
31739
  now: deps.now(),
31531
31740
  pollIntervalMs: settings.quotaPollIntervalMs
31532
31741
  });
31533
- return { health, quota, burn };
31742
+ return { health, quota, burn, lastUsed };
31534
31743
  }
31535
31744
  async function credentialStatus(store, options) {
31536
31745
  const [credentials, quota, adminConfigured] = await Promise.all([
@@ -31644,14 +31853,14 @@ function listSnapshots(deps) {
31644
31853
  }
31645
31854
  return snapshots.sort((a, b) => b.createdAt - a.createdAt);
31646
31855
  }
31647
- var DAY_MS = 86400000;
31856
+ var DAY_MS2 = 86400000;
31648
31857
  var SNAPSHOT_HEADROOM = 1.1;
31649
31858
  function retentionOf(settings) {
31650
31859
  return { keepLatest: settings.snapshotKeepLatest, maxAgeDays: settings.snapshotMaxAgeDays };
31651
31860
  }
31652
31861
  function applyRetention(deps, policy, now) {
31653
31862
  const dir = snapshotsDir(deps);
31654
- const cutoff = now - policy.maxAgeDays * DAY_MS;
31863
+ const cutoff = now - policy.maxAgeDays * DAY_MS2;
31655
31864
  const snapshots = listSnapshots(deps);
31656
31865
  const undo = snapshots.find((snapshot) => snapshot.reason === "preRestore")?.id;
31657
31866
  let removed = 0;
@@ -31971,7 +32180,8 @@ async function toSummary(store, key, now) {
31971
32180
  limitUsage: await readLimitUsage(store, key.id, key.limits, now),
31972
32181
  bodyLoggingOptOut: key.bodyLoggingOptOut,
31973
32182
  createdAt: key.createdAt,
31974
- revokedAt: key.revokedAt
32183
+ revokedAt: key.revokedAt,
32184
+ expiresAt: key.expiresAt
31975
32185
  };
31976
32186
  }
31977
32187
  async function listKeys(store, now = Date.now()) {
@@ -31988,7 +32198,8 @@ async function createKey(store, input2) {
31988
32198
  hash: await hashApiKey(raw),
31989
32199
  modelAllowlist: body2.modelAllowlist,
31990
32200
  limits: body2.limits,
31991
- bodyLoggingOptOut: body2.bodyLoggingOptOut
32201
+ bodyLoggingOptOut: body2.bodyLoggingOptOut,
32202
+ expiresAt: body2.expiresAt
31992
32203
  });
31993
32204
  return { id: created.id, label: created.label, prefix: created.prefix, key: raw };
31994
32205
  }
@@ -32008,6 +32219,14 @@ async function setKeyModels(store, id, input2, now = Date.now()) {
32008
32219
  await store.keys.setModelAllowlist(id, body2.modelAllowlist);
32009
32220
  return toSummary(store, { ...key, modelAllowlist: body2.modelAllowlist }, now);
32010
32221
  }
32222
+ async function setKeyExpiry(store, id, input2, now = Date.now()) {
32223
+ const body2 = parseOrThrow(keyExpirySchema, input2);
32224
+ const key = await store.keys.get(id);
32225
+ if (key === null)
32226
+ throw new GatewayError("BAD_REQUEST", "no such api key");
32227
+ await store.keys.setExpiry(id, body2.expiresAt);
32228
+ return toSummary(store, { ...key, expiresAt: body2.expiresAt }, now);
32229
+ }
32011
32230
  async function revokeKey(store, id) {
32012
32231
  await store.keys.revoke(id);
32013
32232
  }
@@ -33476,12 +33695,18 @@ function readsNothing(scope) {
33476
33695
  return scope.kind === "none" || scope.kind === "key" && scope.apiKeyId === "";
33477
33696
  }
33478
33697
  // packages/control/src/quota/history.ts
33479
- var DAY_MS2 = 24 * 60 * 60 * 1000;
33698
+ var DAY_MS3 = 24 * 60 * 60 * 1000;
33480
33699
  var HOUR_MS3 = 3600000;
33700
+ var MAX_SAMPLES = 50000;
33701
+ var SAMPLE_QUERY_LIMIT = MAX_SAMPLES + 1;
33702
+ function pageSamples(samples) {
33703
+ const truncated = samples.length > MAX_SAMPLES;
33704
+ return { page: truncated ? samples.slice(0, MAX_SAMPLES) : samples, truncated };
33705
+ }
33481
33706
  async function retainedSpan(deps, input2, maxSpanMs) {
33482
33707
  const now = deps.now();
33483
33708
  const settings = await deps.store.config.getSettings();
33484
- const retained = now - settings.logRetentionDays * DAY_MS2;
33709
+ const retained = now - settings.logRetentionDays * DAY_MS3;
33485
33710
  const oldest = maxSpanMs === undefined ? retained : Math.max(retained, now - maxSpanMs);
33486
33711
  return {
33487
33712
  since: Math.max(optionalNumber(input2.since, oldest), oldest),
@@ -33524,12 +33749,14 @@ async function quotaHistory(deps, input2) {
33524
33749
  deps.store.credentials.listQuotaSamples({
33525
33750
  since,
33526
33751
  until,
33752
+ limit: SAMPLE_QUERY_LIMIT,
33527
33753
  ...credentialId === undefined ? {} : { credentialId }
33528
33754
  }),
33529
33755
  deps.store.credentials.listQuota()
33530
33756
  ]);
33531
33757
  const scoped = credentialId === undefined ? quota : quota.filter((w) => w.credentialId === credentialId);
33532
- return { samples, gatewayRates: await gatewayRatesFor(deps.store, scoped) };
33758
+ const { page, truncated } = pageSamples(samples);
33759
+ return { samples: page, truncated, gatewayRates: await gatewayRatesFor(deps.store, scoped) };
33533
33760
  }
33534
33761
 
33535
33762
  // packages/control/src/quota/clientHistory.ts
@@ -33924,7 +34151,11 @@ function createContext(parsed2, options = {}) {
33924
34151
  const current = config2;
33925
34152
  if (current === null)
33926
34153
  throw new CliError(configError ?? "invalid configuration");
33927
- opened = deriveKey(current.encryptionKey).then((key) => createStore({ path: databasePath, encryptionKey: key })).then((created) => {
34154
+ opened = deriveKey(current.encryptionKey).then((key) => createStore({
34155
+ path: databasePath,
34156
+ encryptionKey: key,
34157
+ dayOffsetMinutes: current.dayOffsetMinutes
34158
+ })).then((created) => {
33928
34159
  store = created;
33929
34160
  return created;
33930
34161
  });
@@ -34575,12 +34806,13 @@ var console_ = {
34575
34806
  };
34576
34807
 
34577
34808
  // apps/cli/src/commands/credentials.ts
34578
- function condition(credential) {
34809
+ function condition(credential, now) {
34579
34810
  if (!credential.enabled)
34580
34811
  return { ok: false, text: credential.disabledReason ?? "disabled" };
34581
- if (credential.expiresAt !== null && credential.expiresAt <= Date.now()) {
34582
- return credential.hasRefreshToken ? { ok: true, text: "expired (refreshable)" } : { ok: false, text: "expired" };
34583
- }
34812
+ if (credentialExpired(credential, now))
34813
+ return { ok: false, text: "expired" };
34814
+ if (credentialPastExpiry(credential, now))
34815
+ return { ok: true, text: "expired (refreshable)" };
34584
34816
  return { ok: true, text: "enabled" };
34585
34817
  }
34586
34818
  var credentialsList = {
@@ -34601,7 +34833,7 @@ var credentialsList = {
34601
34833
  { header: "STATE" },
34602
34834
  { header: "EXPIRES" }
34603
34835
  ], credentials.map((c) => {
34604
- const status2 = condition(c);
34836
+ const status2 = condition(c, ctx.now());
34605
34837
  return [
34606
34838
  c.id,
34607
34839
  provider(ctx, c.provider),
@@ -34625,7 +34857,7 @@ var credentialsShow = {
34625
34857
  async run(args, { ctx, writer }) {
34626
34858
  const id = requirePositional(args, 0, "credential id");
34627
34859
  const credential = await findCredential(ctx, id);
34628
- const status2 = condition(credential);
34860
+ const status2 = condition(credential, ctx.now());
34629
34861
  const data = {
34630
34862
  id: credential.id,
34631
34863
  provider: credential.provider,
@@ -34775,17 +35007,17 @@ var credentialsAddKey = {
34775
35007
  };
34776
35008
  var credentialsHealth = {
34777
35009
  usage: "credentials health",
34778
- summary: "Show breaker state and latency per credential and model",
35010
+ summary: "Show breaker state per credential and model",
34779
35011
  options: { all: { type: "boolean" } },
34780
35012
  async run(args, { ctx, writer }) {
34781
35013
  const store = await ctx.store();
34782
- const [{ health: rows }, credentials] = await Promise.all([
35014
+ const [{ health: rows, lastUsed }, credentials] = await Promise.all([
34783
35015
  credentialHealth({ store, now: ctx.now }),
34784
35016
  listCredentials(store)
34785
35017
  ]);
34786
35018
  const labels = new Map(credentials.map((c) => [c.id, c.label]));
34787
35019
  const shown = boolFlag(args.values, "all") ? rows : rows.filter((r) => r.breakerState !== "closed" || r.consecutiveFailures > 0);
34788
- emit(ctx, writer, { health: rows }, () => {
35020
+ emit(ctx, writer, { health: rows, lastUsed }, () => {
34789
35021
  if (shown.length === 0)
34790
35022
  return "every credential is healthy";
34791
35023
  return table([
@@ -34793,15 +35025,13 @@ var credentialsHealth = {
34793
35025
  { header: "MODEL" },
34794
35026
  { header: "BREAKER" },
34795
35027
  { header: "FAILS", align: "right" },
34796
- { header: "TTFT", align: "right" },
34797
35028
  { header: "LAST USED" }
34798
35029
  ], shown.map((row) => [
34799
35030
  labels.get(row.credentialId) ?? row.credentialId,
34800
35031
  row.model,
34801
35032
  state(ctx, row.breakerState === "closed", row.breakerState),
34802
35033
  String(row.consecutiveFailures),
34803
- row.ewmaTtftMs === null ? "\u2014" : `${Math.round(row.ewmaTtftMs)}ms`,
34804
- paint(ctx, "dim", formatTime(row.lastUsedAt))
35034
+ paint(ctx, "dim", formatTime(lastUsed[row.credentialId] ?? null))
34805
35035
  ]));
34806
35036
  });
34807
35037
  }
@@ -34842,7 +35072,8 @@ var dbMigrate = {
34842
35072
  const source = await ctx.store();
34843
35073
  const target = await createPostgresStore({
34844
35074
  url: to,
34845
- encryptionKey: await deriveKey(ctx.config().encryptionKey)
35075
+ encryptionKey: await deriveKey(ctx.config().encryptionKey),
35076
+ dayOffsetMinutes: ctx.config().dayOffsetMinutes
34846
35077
  });
34847
35078
  try {
34848
35079
  const report = await copyStore(source, target);
@@ -35148,11 +35379,24 @@ function summarizeLimits(limits) {
35148
35379
  }
35149
35380
  return parts.length === 0 ? "\u2014" : parts.join(" ");
35150
35381
  }
35382
+ function parseWhen(flag, raw) {
35383
+ const at = Date.parse(raw);
35384
+ if (!Number.isFinite(at)) {
35385
+ throw new UsageError(`${flag} must be a date, e.g. 2030-01-01, got "${raw}"`);
35386
+ }
35387
+ return at;
35388
+ }
35389
+ function keyState(key, now) {
35390
+ if (key.revokedAt !== null)
35391
+ return "revoked";
35392
+ return keyUsable(key, now) ? "active" : "expired";
35393
+ }
35151
35394
  var keysList = {
35152
35395
  usage: "keys list",
35153
35396
  summary: "List gateway API keys",
35154
35397
  async run(_args, { ctx, writer }) {
35155
35398
  const keys = await listKeys(await ctx.store());
35399
+ const now = Date.now();
35156
35400
  emit(ctx, writer, { keys }, () => {
35157
35401
  if (keys.length === 0)
35158
35402
  return "no api keys; create one with: omni keys create";
@@ -35164,6 +35408,7 @@ var keysList = {
35164
35408
  { header: "LIMITS" },
35165
35409
  { header: "BODY CAPTURE" },
35166
35410
  { header: "STATE" },
35411
+ { header: "EXPIRES" },
35167
35412
  { header: "CREATED" }
35168
35413
  ], keys.map((key) => [
35169
35414
  key.id,
@@ -35172,28 +35417,32 @@ var keysList = {
35172
35417
  key.modelAllowlist === null ? "all" : key.modelAllowlist.length === 0 ? "none" : key.modelAllowlist.join(","),
35173
35418
  key.limits === null ? paint(ctx, "red", "unreadable") : summarizeLimits(key.limits),
35174
35419
  key.bodyLoggingOptOut ? "no bodies" : "\u2014",
35175
- state(ctx, key.revokedAt === null, key.revokedAt === null ? "active" : "revoked"),
35420
+ ((verdict) => state(ctx, verdict === "active", verdict))(keyState(key, now)),
35421
+ formatTime(key.expiresAt),
35176
35422
  formatTime(key.createdAt)
35177
35423
  ]));
35178
35424
  });
35179
35425
  }
35180
35426
  };
35181
35427
  var keysCreate = {
35182
- usage: "keys create [--label L] [--allow <model> ...] [--limit <d>:<w>=N ...] [--no-bodies]",
35428
+ usage: "keys create [--label L] [--allow <model> ...] [--limit <d>:<w>=N ...] [--expires <when>] [--no-bodies]",
35183
35429
  summary: "Mint a gateway API key, printed once",
35184
35430
  options: {
35185
35431
  label: { type: "string" },
35186
35432
  allow: { type: "string", multiple: true },
35187
35433
  limit: { type: "string", multiple: true },
35434
+ expires: { type: "string" },
35188
35435
  "no-bodies": { type: "boolean" }
35189
35436
  },
35190
35437
  async run(args, { ctx, writer }) {
35191
35438
  const allow = listFlag(args.values, "allow");
35439
+ const expires = stringFlag(args.values, "expires");
35192
35440
  const created = await createKey(await ctx.store(), {
35193
35441
  ...stringFlag(args.values, "label") === undefined ? {} : { label: stringFlag(args.values, "label") },
35194
35442
  modelAllowlist: allow ?? null,
35195
35443
  limits: parseLimitFlags(listFlag(args.values, "limit") ?? [], "--limit"),
35196
- bodyLoggingOptOut: boolFlag(args.values, "no-bodies")
35444
+ bodyLoggingOptOut: boolFlag(args.values, "no-bodies"),
35445
+ expiresAt: expires === undefined ? null : parseWhen("--expires", expires)
35197
35446
  });
35198
35447
  emit(ctx, writer, created, () => {
35199
35448
  note(ctx, writer, paint(ctx, "yellow", "this key is shown once and stored only as a hash"));
@@ -35260,6 +35509,44 @@ ${paint(ctx, "dim", "usage is counted from completed requests still inside each
35260
35509
  });
35261
35510
  }
35262
35511
  };
35512
+ var keysExpiry = {
35513
+ usage: "keys expiry <id> [--at <when>] [--never]",
35514
+ summary: "Show or replace one key's expiry",
35515
+ options: {
35516
+ at: { type: "string" },
35517
+ never: { type: "boolean" }
35518
+ },
35519
+ async run(args, { ctx, writer }) {
35520
+ const id = requirePositional(args, 0, "key id");
35521
+ const at = stringFlag(args.values, "at");
35522
+ const never2 = boolFlag(args.values, "never");
35523
+ const store = await ctx.store();
35524
+ const existing = (await listKeys(store)).find((entry) => entry.id === id);
35525
+ if (existing === undefined)
35526
+ throw new CliError(`no api key "${id}"`);
35527
+ let key = existing;
35528
+ if (never2 || at !== undefined) {
35529
+ if (never2 && at !== undefined) {
35530
+ throw new UsageError("--at and --never cannot be combined");
35531
+ }
35532
+ const next = never2 ? null : parseWhen("--at", at);
35533
+ key = await setKeyExpiry(store, id, { expiresAt: next });
35534
+ }
35535
+ emit(ctx, writer, key, () => {
35536
+ const head = fields([
35537
+ ["id", key.id],
35538
+ ["label", key.label],
35539
+ ["prefix", `${key.prefix}\u2026`]
35540
+ ]);
35541
+ if (key.expiresAt === null)
35542
+ return `${head}
35543
+ expires: never`;
35544
+ const verdict = keyState(key, Date.now());
35545
+ return `${head}
35546
+ expires: ${formatTime(key.expiresAt)}${verdict === "expired" ? ` ${paint(ctx, "red", "(expired)")}` : ""}`;
35547
+ });
35548
+ }
35549
+ };
35263
35550
  var keysRevoke = {
35264
35551
  usage: "keys revoke <id>",
35265
35552
  summary: "Revoke a gateway API key, keeping its usage history",
@@ -35755,7 +36042,7 @@ function createServiceDeps(input2) {
35755
36042
  }
35756
36043
 
35757
36044
  // apps/cli/src/version.ts
35758
- var VERSION = "0.10.3";
36045
+ var VERSION = "0.11.1";
35759
36046
 
35760
36047
  // apps/cli/src/commands/service.ts
35761
36048
  function gatewayArgv(root) {
@@ -36355,7 +36642,7 @@ var logs = {
36355
36642
  return;
36356
36643
  let seen = first[0]?.at ?? 0;
36357
36644
  for (;; ) {
36358
- await Bun.sleep(2000);
36645
+ await Bun.sleep(FOLLOW_INTERVAL_MS);
36359
36646
  const next = (await recentLogs(store, limit)).filter((row) => row.at > seen);
36360
36647
  if (next.length === 0)
36361
36648
  continue;
@@ -36399,6 +36686,7 @@ var COMMANDS = {
36399
36686
  "keys create": keysCreate,
36400
36687
  "keys limits": keysLimits,
36401
36688
  "keys models": keysModels,
36689
+ "keys expiry": keysExpiry,
36402
36690
  "keys revoke": keysRevoke,
36403
36691
  "plugin list": pluginList,
36404
36692
  "plugin verify": pluginVerify,