omnigateway 0.11.4 → 0.12.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 (46) hide show
  1. package/README.md +3 -1
  2. package/bin/omni.js +793 -60
  3. package/gateway.js +686 -98
  4. package/package.json +1 -1
  5. package/public/assets/{Chip-roGX6zyg.js → Chip-D4qvhu1D.js} +1 -1
  6. package/public/assets/{CopyValue-FO5yNkRC.js → CopyValue-SnsT2LML.js} +1 -1
  7. package/public/assets/Lamp-DkvXKB-B.js +26 -0
  8. package/public/assets/Rack-CEuCdVXc.js +483 -0
  9. package/public/assets/RequestTable-CSQGt9W_.js +93 -0
  10. package/public/assets/{SummaryDeck-DwRGp7mB.js → SummaryDeck-7ivuca39.js} +1 -1
  11. package/public/assets/Toggle-t82IhqEt.js +39 -0
  12. package/public/assets/{TokenBreakdown-DT7or6C9.js → TokenBreakdown-D1Wtza1y.js} +1 -1
  13. package/public/assets/{WindowChart-C-oJo3fW.js → WindowChart-BeGPsccS.js} +1 -1
  14. package/public/assets/_app.accounts-W1USUo2Q.js +70 -0
  15. package/public/assets/{_app.console-DbM7Zbgu.js → _app.console-BvIGqIe7.js} +2 -2
  16. package/public/assets/{_app.database-Bj4UJUBZ.js → _app.database-CtFJLcfE.js} +4 -4
  17. package/public/assets/_app.index-DigYfob3.js +67 -0
  18. package/public/assets/{_app.keys-BYk7HsiW.js → _app.keys-B1fRMyU-.js} +6 -6
  19. package/public/assets/_app.logs-D73Qcx9S.js +60 -0
  20. package/public/assets/{_app.models-BqdGSdg4.js → _app.models-Dpl1O2RE.js} +9 -9
  21. package/public/assets/{_app.plugins._pluginId-Bf9D8Qiz.js → _app.plugins._pluginId-BNrRTn9l.js} +1 -1
  22. package/public/assets/{_app.settings-TzCc7Sd3.js → _app.settings-tRLrVJoq.js} +4 -4
  23. package/public/assets/_app.usage-D2qlcEp4.js +76 -0
  24. package/public/assets/chevron-down-Bs0tSOyA.js +1 -0
  25. package/public/assets/chevron-right-DSaI1kFo.js +1 -0
  26. package/public/assets/client-BZ_ev12J.js +39 -0
  27. package/public/assets/dist-D03q-60i.js +1 -0
  28. package/public/assets/index-89mqAVmK.js +14 -0
  29. package/public/assets/{login-Dgd5jBoq.js → login-75MYwRdC.js} +2 -2
  30. package/public/assets/plus-BCfjC6nk.js +1 -0
  31. package/public/assets/{preload-helper-CvG2RSKd.js → preload-helper-BPaHpbO_.js} +1 -1
  32. package/public/assets/{shared-wEQ9LHZa.js → shared-Dtkw6SE0.js} +1 -1
  33. package/public/assets/{trash-IE0YvGCJ.js → trash-D4QWzEK_.js} +1 -1
  34. package/public/index.html +4 -4
  35. package/public/assets/Lamp-DsUSnxN4.js +0 -26
  36. package/public/assets/Rack-CSpeYQ1D.js +0 -483
  37. package/public/assets/RequestTable-xpG3dPST.js +0 -11
  38. package/public/assets/Toggle-BTJVC9Se.js +0 -39
  39. package/public/assets/_app.accounts-D8eynNaM.js +0 -59
  40. package/public/assets/_app.index-2hdPR1Le.js +0 -67
  41. package/public/assets/_app.logs-sG1w9y1i.js +0 -58
  42. package/public/assets/_app.usage-XQHmzx-j.js +0 -76
  43. package/public/assets/chevron-right-DgdKbG-g.js +0 -1
  44. package/public/assets/client-B_g7bcxd.js +0 -39
  45. package/public/assets/index-C_8ChaGY.js +0 -14
  46. package/public/assets/plus-UgaZwucC.js +0 -1
package/gateway.js CHANGED
@@ -26376,6 +26376,54 @@ var _003_key_expiry_default = `-- The Postgres half of the sqlite migration of t
26376
26376
  ALTER TABLE api_keys ADD COLUMN expires_at BIGINT;
26377
26377
  `;
26378
26378
 
26379
+ // packages/store/src/postgres/migrations/004_log_keyset.sql
26380
+ var _004_log_keyset_default = `-- Indexes for the keyset traversal of \`request_logs\`. The SQLite copy,
26381
+ -- \`015_log_keyset.sql\`, carries the reasoning: \`id\` in the index is what makes
26382
+ -- "the row after this one" a question with one answer when \`at\` repeats, and
26383
+ -- the three shorter indexes are replaced rather than kept because a redundant
26384
+ -- prefix costs a write per row and answers nothing the longer one does not.
26385
+ DROP INDEX IF EXISTS idx_request_logs_at;
26386
+ DROP INDEX IF EXISTS idx_request_logs_key_at;
26387
+ DROP INDEX IF EXISTS idx_request_logs_cred;
26388
+
26389
+ CREATE INDEX idx_request_logs_at ON request_logs (at DESC, id DESC);
26390
+ CREATE INDEX idx_request_logs_key_at ON request_logs (api_key_id, at DESC, id DESC);
26391
+ CREATE INDEX idx_request_logs_cred ON request_logs (credential_id, at DESC, id DESC);
26392
+ `;
26393
+
26394
+ // packages/store/src/postgres/migrations/005_log_filter_indexes.sql
26395
+ var _005_log_filter_indexes_default = `-- An index for the provider filter on \`request_logs\`. Filtering by a provider
26396
+ -- with no traffic has nothing to seek, so proving the absence reads every row,
26397
+ -- and the console offers exactly that query because its dropdown is built from
26398
+ -- the provider catalog rather than from traffic. Measured here at 500,000 rows,
26399
+ -- and separately in the SQLite copy \`016_log_filter_indexes.sql\`:
26400
+ --
26401
+ -- resolved_provider = without index with index
26402
+ -- a provider with no rows 17.17ms 0.01ms
26403
+ -- a minority provider 0.04ms 0.02ms
26404
+ -- the dominant provider 0.02ms 0.02ms
26405
+ --
26406
+ -- Only the first row justifies the index, and it justifies it on both backends:
26407
+ -- without it the planner falls back to a sequential scan with a sort on top,
26408
+ -- because there is no prefix to seek and every row has to be rejected before the
26409
+ -- absence is proven. The other two answer off \`idx_request_logs_at\` either way \u2014
26410
+ -- a provider that has traffic fills a page near the head immediately.
26411
+ --
26412
+ -- Leading column then the keyset pair, so one seek answers the filter and the
26413
+ -- walk from it is already in page order.
26414
+ --
26415
+ -- **This is the only log-filter index on this backend, and the SQLite copy has
26416
+ -- one more.** There, \`017_log_text_search.sql\` moves \`requested_model\`,
26417
+ -- \`resolved_model\` and \`error_code\` into the keyset index so a substring scan
26418
+ -- tests the match against entries it is already walking. Do not mirror that
26419
+ -- here: this planner reads a leading-wildcard \`ILIKE\` as unindexable, so a
26420
+ -- substring miss plans as a parallel sequential scan with a sort on top and
26421
+ -- \`idx_request_logs_at\` is never opened. Measured at 500,000 rows, widening it
26422
+ -- cost 15MB to 58MB and a wider row per insert, and bought 0.25ms of 86ms. The
26423
+ -- backends share the predicate in \`logPage.ts\`, not the index shape.
26424
+ CREATE INDEX idx_request_logs_provider ON request_logs (resolved_provider, at DESC, id DESC);
26425
+ `;
26426
+
26379
26427
  // packages/store/src/postgres/db.ts
26380
26428
  var num = (v) => Number(v);
26381
26429
  var numOrNull = (v) => v === null ? null : Number(v);
@@ -26383,7 +26431,9 @@ var MIGRATION_LOCK = 7140641;
26383
26431
  var MIGRATIONS = [
26384
26432
  { id: 1, sql: _001_init_default },
26385
26433
  { id: 2, sql: _002_health_measurements_default },
26386
- { id: 3, sql: _003_key_expiry_default }
26434
+ { id: 3, sql: _003_key_expiry_default },
26435
+ { id: 4, sql: _004_log_keyset_default },
26436
+ { id: 5, sql: _005_log_filter_indexes_default }
26387
26437
  ];
26388
26438
  async function openPg(url2) {
26389
26439
  const sql = new SQL({ url: url2 });
@@ -27415,6 +27465,57 @@ function isRtkFilterId(value) {
27415
27465
  return FILTER_IDS.has(value);
27416
27466
  }
27417
27467
 
27468
+ // packages/store/src/logPage.ts
27469
+ function logPageClauses(query, placeholder, like = "LIKE") {
27470
+ const clauses = [];
27471
+ const bindings = [];
27472
+ const bind = (value) => {
27473
+ bindings.push(value);
27474
+ return placeholder(bindings.length);
27475
+ };
27476
+ const contains = (column, value) => {
27477
+ if (value === undefined)
27478
+ return;
27479
+ const escaped = value.replace(/[\\%_]/g, "\\$&");
27480
+ clauses.push(`${column} ${like} ${bind(`%${escaped}%`)} ESCAPE '\\'`);
27481
+ };
27482
+ const eq = (column, value) => {
27483
+ if (value !== undefined)
27484
+ clauses.push(`${column} = ${bind(value)}`);
27485
+ };
27486
+ if (query.cursor !== null) {
27487
+ const cursor = query.cursor;
27488
+ clauses.push(`(at < ${bind(cursor.at)} OR (at = ${bind(cursor.at)} AND id < ${bind(cursor.id)}))`);
27489
+ }
27490
+ eq("api_key_id", query.apiKeyId);
27491
+ eq("credential_id", query.credentialId);
27492
+ eq("resolved_provider", query.provider);
27493
+ eq("state", query.state);
27494
+ contains("requested_model", query.requestedModel);
27495
+ contains("resolved_model", query.resolvedModel);
27496
+ contains("error_code", query.errorCode);
27497
+ if (query.model !== undefined) {
27498
+ const before = clauses.length;
27499
+ contains("requested_model", query.model);
27500
+ contains("resolved_model", query.model);
27501
+ clauses.push(`(${clauses.splice(before).join(" OR ")})`);
27502
+ }
27503
+ if (query.since !== undefined)
27504
+ clauses.push(`at >= ${bind(query.since)}`);
27505
+ if (query.until !== undefined)
27506
+ clauses.push(`at <= ${bind(query.until)}`);
27507
+ if (query.failed === true)
27508
+ clauses.push("state = 'done' AND status >= 400");
27509
+ return { where: clauses.length === 0 ? "" : `WHERE ${clauses.join(" AND ")}`, bindings };
27510
+ }
27511
+ function splitPage(rows, limit) {
27512
+ if (rows.length <= limit)
27513
+ return { logs: rows, next: null };
27514
+ const logs = rows.slice(0, limit);
27515
+ const last = logs[logs.length - 1];
27516
+ return { logs, next: last === undefined ? null : { at: last.at, id: last.id } };
27517
+ }
27518
+
27418
27519
  // packages/store/src/postgres/rollup.ts
27419
27520
  var DAILY_UPSERT = `
27420
27521
  INSERT INTO usage_daily
@@ -27719,6 +27820,12 @@ function createUsageRepo(sql, nodeId, dayOffsetMinutes) {
27719
27820
  ]) : await sql.unsafe("SELECT * FROM request_logs WHERE api_key_id = $1 ORDER BY at DESC LIMIT $2", [apiKeyId, limit]);
27720
27821
  return rows.map(toLog);
27721
27822
  },
27823
+ async page(query) {
27824
+ const { where, bindings } = logPageClauses(query, (n) => `$${n}`, "ILIKE");
27825
+ const rows = (await sql.unsafe(`SELECT * FROM request_logs ${where}
27826
+ ORDER BY at DESC, id DESC LIMIT $${bindings.length + 1}`, [...bindings, query.limit + 1])).map(toLog);
27827
+ return splitPage(rows, query.limit);
27828
+ },
27722
27829
  async lastUsedByCredential(credentialId) {
27723
27830
  const rows = await sql.unsafe("SELECT MAX(at) AS at FROM request_logs WHERE credential_id = $1", [credentialId]);
27724
27831
  return numOrNull(rows[0]?.at ?? null);
@@ -28950,6 +29057,114 @@ var _014_key_expiry_default = `-- An optional expiry on a gateway API key: an ab
28950
29057
  ALTER TABLE api_keys ADD COLUMN expires_at INTEGER;
28951
29058
  `;
28952
29059
 
29060
+ // packages/store/src/sqlite/migrations/015_log_keyset.sql
29061
+ var _015_log_keyset_default = "-- Indexes for the keyset traversal of `request_logs`.\n--\n-- Ordering by `at DESC, id DESC` is what makes a page boundary exact: `at`\n-- repeats within a millisecond, so an index on `at` alone leaves SQLite to\n-- resolve the tie however the scan happens to reach it, and the resolution can\n-- differ between two reads of the same table. The `id` column in the index is\n-- therefore not a tie-break for display \u2014 it is what makes \"the row after this\n-- one\" a question with one answer.\n--\n-- Each of the three replaces a shorter index with the same leading columns, so\n-- they are dropped rather than kept beside the new ones. A redundant prefix\n-- index costs a write on every row inserted and answers no query the longer one\n-- does not: `(at DESC, id DESC)` seeks and scans exactly as `(at DESC)` did for\n-- every reader that never mentions `id`, including `prune`'s range delete and\n-- `aggregate`'s window.\n--\n-- The key and credential indexes are the two scoped reads that must stay\n-- bounded. `api_key_id` is every client page and every scoped operator read,\n-- and it is also the sliding-window limiter's edge scan, which runs on the\n-- request hot path. `credential_id` is `lastUsedByCredential` \u2014 one seek to a\n-- head \u2014 and the account investigation an operator reaches for first.\n--\n-- Deliberately no index for `provider`, `resolved_model`, `state` or\n-- `error_code`. One index per filter is a combinatorial answer to a question\n-- nobody has measured yet, paid for on every insert; the cursor and the time\n-- bounds already keep a filtered page's scan to the rows above it. A production\n-- measurement of the query distribution is what should buy the next index.\nDROP INDEX IF EXISTS idx_request_logs_at;\nDROP INDEX IF EXISTS idx_request_logs_key_at;\nDROP INDEX IF EXISTS idx_request_logs_cred;\n\nCREATE INDEX idx_request_logs_at ON request_logs (at DESC, id DESC);\nCREATE INDEX idx_request_logs_key_at ON request_logs (api_key_id, at DESC, id DESC);\nCREATE INDEX idx_request_logs_cred ON request_logs (credential_id, at DESC, id DESC);\n";
29062
+
29063
+ // packages/store/src/sqlite/migrations/016_log_filter_indexes.sql
29064
+ var _016_log_filter_indexes_default = `-- An index for the provider filter on \`request_logs\`, which \`015_log_keyset.sql\`
29065
+ -- deliberately left out pending a measurement. This is that measurement.
29066
+ --
29067
+ -- Without it, filtering by a provider that has *no* traffic scans the whole
29068
+ -- table: there is nothing to seek, so proving the absence means reading every
29069
+ -- row. \`bun:sqlite\` is synchronous, so that scan is the event loop \u2014 time in
29070
+ -- which no proxied request moves. Measured at 500,000 rows:
29071
+ --
29072
+ -- resolved_provider = without index with index
29073
+ -- a provider with no rows 84.0ms 0.0ms
29074
+ -- a minority provider 0.19ms 0.10ms
29075
+ -- the dominant provider 0.09ms 0.08ms
29076
+ --
29077
+ -- Only the first row justifies the index, and it justifies it completely. It is
29078
+ -- also the case the console invites: the filter is a dropdown built from the
29079
+ -- provider catalog rather than from traffic, so every provider this gateway
29080
+ -- *could* route to is offered, including the ones an operator has no account
29081
+ -- for. Selecting one is a normal thing to do and it was the most expensive
29082
+ -- query on the surface.
29083
+ --
29084
+ -- Leading column then the keyset pair, so one seek answers the filter and the
29085
+ -- walk from it is already in page order.
29086
+ --
29087
+ -- \`state\` gets nothing: \`idx_request_logs_pending\` already covers the only
29088
+ -- selective half, and \`state = 'done'\` is every row, so the keyset index fills a
29089
+ -- page immediately. Nothing per-column for \`failed\` either, whose predicate is
29090
+ -- \`state = 'done' AND status >= 400\` and which reads at 1.0ms off the keyset
29091
+ -- index, because failures cluster near the head an operator is looking at.
29092
+ --
29093
+ -- The filters an operator *types* are not here, and not by omission: they match
29094
+ -- substrings, which no B-tree can seek. \`017_log_text_search.sql\` carries them,
29095
+ -- and carries them inside the keyset index rather than beside it.
29096
+ CREATE INDEX idx_request_logs_provider ON request_logs (resolved_provider, at DESC, id DESC);
29097
+ `;
29098
+
29099
+ // packages/store/src/sqlite/migrations/017_log_text_search.sql
29100
+ var _017_log_text_search_default = `-- Indexes the typed log filters, which match substrings rather than values.
29101
+ --
29102
+ -- \`requested_model\`, \`resolved_model\`, \`error_code\`, and the \`model\` filter that
29103
+ -- reads the first two, became \`LIKE '%value%'\`, because an exact match on a name
29104
+ -- nobody recalls exactly answered "no such traffic" for every spelling but one. A
29105
+ -- leading wildcard cannot seek a B-tree: there is no prefix to range over.
29106
+ --
29107
+ -- The three \`DROP\`s below are **upgrade-only cleanup and no-ops on a fresh
29108
+ -- install.** An earlier draft of 016 created a per-column index for each of these
29109
+ -- three filters, back when they were equalities. Nothing released ever ran it, so
29110
+ -- 016 now creates only the provider index it still justifies \u2014 but a database
29111
+ -- that applied the draft is carrying three indexes that can no longer be seeked
29112
+ -- and still cost a write per row, so they are named here rather than left to rot.
29113
+ -- Building them only to drop them cost an upgrading install 1,334ms at 1,000,000
29114
+ -- rows, which is why the fix was to stop creating them rather than to keep the
29115
+ -- symmetry.
29116
+ --
29117
+ -- What the typed filters get instead is those same columns moved *into* the keyset
29118
+ -- index. The scan is going to walk \`(at DESC, id DESC)\` regardless \u2014 that is where
29119
+ -- the ordering comes from \u2014 and carrying the three text columns in that index lets
29120
+ -- it test the match against index entries rather than fetching each row to do it.
29121
+ -- Only the page that survives is fetched.
29122
+ --
29123
+ -- Measured at 1,000,000 rows of 300 bytes, filter matching nothing, which is the
29124
+ -- case that has to scan to the end to prove it:
29125
+ --
29126
+ -- keyset index model filter error filter head read index size
29127
+ -- (at, id) 204.4ms 160.1ms 0.08ms 44.2MB
29128
+ -- (at, id, + 3 columns) 78.3ms 33.0ms 0.08ms 87.0MB
29129
+ --
29130
+ -- So: 2.6x on the model filter, 4.8x on the error one, no cost to the unfiltered
29131
+ -- head read every board opens with, and double the index. The size is the price
29132
+ -- and it is worth naming \u2014 at a million rows this index is 87MB against 44MB \u2014
29133
+ -- but it is one index instead of three, and it is the only shape a substring can
29134
+ -- use at all.
29135
+ --
29136
+ -- A second, separate covering index does **not** work: SQLite prefers the
29137
+ -- narrower index that supplies the ordering and ignores the wider one, measured
29138
+ -- at 198ms against 202ms. The columns have to be in the index the scan already
29139
+ -- wants, which is why this replaces \`idx_request_logs_at\` rather than joining it.
29140
+ --
29141
+ -- \`resolved_provider\` keeps 016's index and its \`=\`: it arrives from a dropdown
29142
+ -- of ids the gateway supplied, so it is never half-typed, a substring would only
29143
+ -- widen it, and an equality can still seek \u2014 which is worth 84ms to 0 on a
29144
+ -- provider with no traffic.
29145
+ --
29146
+ -- The \`ANALYZE\` trap an earlier draft of 016 documented is gone with the plan
29147
+ -- that had it. There is one plan for a leading wildcard, so statistics have
29148
+ -- nothing to choose between: measured identical, 22.5ms either way at 300,000
29149
+ -- rows.
29150
+ --
29151
+ -- A time bound still collapses all of this \u2014 the same 22.5ms miss is 0.23ms with
29152
+ -- \`since\` set to the newest 1% \u2014 because \`at\` still leads the index. That remains
29153
+ -- the cheapest thing an operator can do, and the reason export requires both
29154
+ -- bounds. If a substring miss on a very large log ever needs to be free rather
29155
+ -- than linear, the next step is FTS5's trigram tokenizer, which is a shadow
29156
+ -- table and a trigger to keep in sync and a \`pg_trgm\` GIN index to match it on
29157
+ -- the other backend. Not worth it for one query per typed name, behind a
29158
+ -- debounce.
29159
+ DROP INDEX IF EXISTS idx_request_logs_resolved_model;
29160
+ DROP INDEX IF EXISTS idx_request_logs_requested_model;
29161
+ DROP INDEX IF EXISTS idx_request_logs_error_code;
29162
+
29163
+ DROP INDEX IF EXISTS idx_request_logs_at;
29164
+ CREATE INDEX idx_request_logs_at
29165
+ ON request_logs (at DESC, id DESC, requested_model, resolved_model, error_code);
29166
+ `;
29167
+
28953
29168
  // packages/store/src/sqlite/db.ts
28954
29169
  var MIGRATIONS2 = [
28955
29170
  { id: 1, sql: _001_init_default2 },
@@ -28965,7 +29180,10 @@ var MIGRATIONS2 = [
28965
29180
  { id: 11, sql: _011_plugin_migrations_default },
28966
29181
  { id: 12, sql: _012_nodes_default },
28967
29182
  { id: 13, sql: _013_health_measurements_default },
28968
- { id: 14, sql: _014_key_expiry_default }
29183
+ { id: 14, sql: _014_key_expiry_default },
29184
+ { id: 15, sql: _015_log_keyset_default },
29185
+ { id: 16, sql: _016_log_filter_indexes_default },
29186
+ { id: 17, sql: _017_log_text_search_default }
28969
29187
  ];
28970
29188
  function statements(sql) {
28971
29189
  return sql.split(`
@@ -29466,6 +29684,11 @@ function createUsageRepo2(db, nodeId, dayOffsetMinutes = hostDayOffsetMinutes())
29466
29684
  }
29467
29685
  return db.query("SELECT * FROM request_logs WHERE api_key_id = ? ORDER BY at DESC LIMIT ?").all(apiKeyId, limit).map(toLog2);
29468
29686
  },
29687
+ async page(query) {
29688
+ const { where, bindings } = logPageClauses(query, () => "?");
29689
+ const rows = db.query(`SELECT * FROM request_logs ${where} ORDER BY at DESC, id DESC LIMIT ?`).all(...bindings, query.limit + 1).map(toLog2);
29690
+ return splitPage(rows, query.limit);
29691
+ },
29469
29692
  async lastUsedByCredential(credentialId) {
29470
29693
  const row = db.query("SELECT MAX(at) AS at FROM request_logs WHERE credential_id = ?").get(credentialId);
29471
29694
  return row?.at ?? null;
@@ -29660,6 +29883,7 @@ async function createStore(opts) {
29660
29883
  append: (log) => handle.usage.append(log),
29661
29884
  sweepPending: (now) => handle.usage.sweepPending(now),
29662
29885
  recent: (limit, apiKeyId) => handle.usage.recent(limit, apiKeyId),
29886
+ page: (query) => handle.usage.page(query),
29663
29887
  lastUsedByCredential: (credentialId) => handle.usage.lastUsedByCredential(credentialId),
29664
29888
  scan: (cursor, limit) => handle.usage.scan(cursor, limit),
29665
29889
  aggregate: (q) => handle.usage.aggregate(q),
@@ -37466,6 +37690,13 @@ var credentialPatchSchema = exports_external.object({
37466
37690
  tier: exports_external.number().int().min(1).optional(),
37467
37691
  weight: exports_external.number().positive().optional()
37468
37692
  }).strict();
37693
+ var quotaRefreshSchema = exports_external.discriminatedUnion("kind", [
37694
+ exports_external.object({
37695
+ kind: exports_external.literal("one"),
37696
+ credentialId: exports_external.string().trim().min(1).max(64).regex(/^[A-Za-z0-9_-]+$/, "credentialId must be an account id")
37697
+ }).strict(),
37698
+ exports_external.object({ kind: exports_external.literal("all") }).strict()
37699
+ ]);
37469
37700
  var dimensionSchema = exports_external.enum([
37470
37701
  "credential",
37471
37702
  "model",
@@ -38697,6 +38928,243 @@ async function requestRestart(deps) {
38697
38928
  async function requestShutdown(deps) {
38698
38929
  deps.stop(0);
38699
38930
  }
38931
+ // packages/control/src/principal.ts
38932
+ var ALL = { kind: "all" };
38933
+ var NONE2 = { kind: "none" };
38934
+ function scopeOf(principal) {
38935
+ switch (principal.kind) {
38936
+ case "admin":
38937
+ case "viewer":
38938
+ return ALL;
38939
+ case "client":
38940
+ return principal.apiKeyId === "" ? NONE2 : { kind: "key", apiKeyId: principal.apiKeyId };
38941
+ case "machine":
38942
+ return NONE2;
38943
+ }
38944
+ }
38945
+ function scopeKey(scope) {
38946
+ return scope.kind === "key" ? scope.apiKeyId : undefined;
38947
+ }
38948
+ function readsNothing(scope) {
38949
+ return scope.kind === "none" || scope.kind === "key" && scope.apiKeyId === "";
38950
+ }
38951
+
38952
+ // packages/control/src/usage.ts
38953
+ var MAX_LOG_LIMIT = 500;
38954
+ var DEFAULT_LOG_LIMIT = 100;
38955
+ async function queryUsage(deps, input2, scope = ALL) {
38956
+ const grain = parseOrThrow(grainSchema, input2.grain ?? "raw");
38957
+ const groupBy = requireDimension(grain, parseOrThrow(dimensionSchema, input2.groupBy ?? "model"));
38958
+ const splitBy = input2.splitBy === undefined ? undefined : requireDimension(grain, parseOrThrow(dimensionSchema, input2.splitBy));
38959
+ if (scope.kind !== "all") {
38960
+ for (const dimension of [groupBy, splitBy]) {
38961
+ if (dimension === "credential") {
38962
+ throw new GatewayError("BAD_REQUEST", "this dimension is not available on this surface");
38963
+ }
38964
+ }
38965
+ }
38966
+ if (readsNothing(scope))
38967
+ return [];
38968
+ const apiKeyId = scopeKey(scope);
38969
+ return deps.store.usage.aggregate({
38970
+ grain,
38971
+ groupBy,
38972
+ ...splitBy === undefined ? {} : { splitBy },
38973
+ ...apiKeyId === undefined ? {} : { apiKeyId },
38974
+ since: optionalNumber(input2.since, 0),
38975
+ until: optionalNumber(input2.until, deps.now())
38976
+ });
38977
+ }
38978
+ function logLimit(requested) {
38979
+ const value = optionalNumber(requested, DEFAULT_LOG_LIMIT);
38980
+ return Math.floor(Math.min(Math.max(1, value), MAX_LOG_LIMIT));
38981
+ }
38982
+
38983
+ // packages/control/src/logs.ts
38984
+ var CURSOR_VERSION = 1;
38985
+ var MAX_ID_LENGTH = 128;
38986
+ var MAX_FILTER_LENGTH = 256;
38987
+ var idSchema = exports_external.string().min(1).max(MAX_ID_LENGTH);
38988
+ var textSchema = exports_external.string().min(1).max(MAX_FILTER_LENGTH);
38989
+ var instantSchema = exports_external.union([
38990
+ exports_external.number(),
38991
+ exports_external.string().regex(/^-?\d+$/, "must be epoch milliseconds").transform(Number)
38992
+ ]).pipe(exports_external.number().int());
38993
+ var logFilterSchema = exports_external.object({
38994
+ cursor: exports_external.string().max(512).optional(),
38995
+ limit: exports_external.union([exports_external.string(), exports_external.number()]).optional(),
38996
+ since: instantSchema.optional(),
38997
+ until: instantSchema.optional(),
38998
+ state: exports_external.enum(["pending", "done"]).optional(),
38999
+ failed: exports_external.enum(["true", "false"]).optional(),
39000
+ provider: providerIdSchema.optional(),
39001
+ requestedModel: textSchema.optional(),
39002
+ resolvedModel: textSchema.optional(),
39003
+ model: textSchema.optional(),
39004
+ credentialId: idSchema.optional(),
39005
+ apiKeyId: idSchema.optional(),
39006
+ errorCode: textSchema.optional()
39007
+ }).strict();
39008
+ function present(input2) {
39009
+ const out = {};
39010
+ for (const [key, value] of Object.entries(input2)) {
39011
+ if (value === undefined || value === "")
39012
+ continue;
39013
+ out[key] = typeof value === "boolean" ? String(value) : value;
39014
+ }
39015
+ return out;
39016
+ }
39017
+ function encodeLogCursor(cursor) {
39018
+ const json10 = JSON.stringify({ v: CURSOR_VERSION, at: cursor.at, id: cursor.id });
39019
+ return Buffer.from(json10, "utf8").toString("base64url");
39020
+ }
39021
+ function decodeLogCursor(raw) {
39022
+ const reject = () => {
39023
+ throw new GatewayError("BAD_REQUEST", "cursor: malformed");
39024
+ };
39025
+ let parsed2;
39026
+ try {
39027
+ parsed2 = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
39028
+ } catch {
39029
+ return reject();
39030
+ }
39031
+ if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2))
39032
+ return reject();
39033
+ const record2 = parsed2;
39034
+ const keys = Object.keys(record2);
39035
+ if (keys.length !== 3)
39036
+ return reject();
39037
+ if (record2.v !== CURSOR_VERSION) {
39038
+ throw new GatewayError("BAD_REQUEST", "cursor: unknown version");
39039
+ }
39040
+ const { at, id } = record2;
39041
+ if (typeof at !== "number" || !Number.isSafeInteger(at))
39042
+ return reject();
39043
+ if (typeof id !== "string" || id.length === 0 || id.length > MAX_ID_LENGTH)
39044
+ return reject();
39045
+ const cursor = { at, id };
39046
+ if (encodeLogCursor(cursor) !== raw)
39047
+ return reject();
39048
+ return cursor;
39049
+ }
39050
+ function toQuery(input2, scope) {
39051
+ const filters = parseOrThrow(logFilterSchema, present(input2));
39052
+ const { since, until } = filters;
39053
+ if (since !== undefined && until !== undefined && since > until) {
39054
+ throw new GatewayError("BAD_REQUEST", "since: must not be after until");
39055
+ }
39056
+ const scoped = scopeKey(scope);
39057
+ return {
39058
+ limit: logLimit(filters.limit),
39059
+ cursor: filters.cursor === undefined ? null : decodeLogCursor(filters.cursor),
39060
+ ...filters.apiKeyId === undefined ? {} : { apiKeyId: filters.apiKeyId },
39061
+ ...scoped === undefined ? {} : { apiKeyId: scoped },
39062
+ ...filters.credentialId === undefined ? {} : { credentialId: filters.credentialId },
39063
+ ...filters.provider === undefined ? {} : { provider: filters.provider },
39064
+ ...filters.requestedModel === undefined ? {} : { requestedModel: filters.requestedModel },
39065
+ ...filters.resolvedModel === undefined ? {} : { resolvedModel: filters.resolvedModel },
39066
+ ...filters.model === undefined ? {} : { model: filters.model },
39067
+ ...filters.errorCode === undefined ? {} : { errorCode: filters.errorCode },
39068
+ ...filters.state === undefined ? {} : { state: filters.state },
39069
+ ...filters.failed === "true" ? { failed: true } : {},
39070
+ ...since === undefined ? {} : { since },
39071
+ ...until === undefined ? {} : { until }
39072
+ };
39073
+ }
39074
+ async function pageLogs(store, input2 = {}, scope = ALL) {
39075
+ const query = toQuery(input2, scope);
39076
+ if (readsNothing(scope))
39077
+ return { logs: [], nextCursor: null };
39078
+ const page = await store.usage.page(query);
39079
+ return {
39080
+ logs: page.logs,
39081
+ nextCursor: page.next === null ? null : encodeLogCursor(page.next)
39082
+ };
39083
+ }
39084
+ var EXPORT_BATCH = 500;
39085
+ var EXPORT_COLUMNS = [
39086
+ "id",
39087
+ "state",
39088
+ "at",
39089
+ "apiKeyId",
39090
+ "requestedModel",
39091
+ "resolvedProvider",
39092
+ "resolvedModel",
39093
+ "credentialId",
39094
+ "attempts",
39095
+ "status",
39096
+ "errorCode",
39097
+ "inputTokens",
39098
+ "outputTokens",
39099
+ "cacheReadTokens",
39100
+ "cacheWriteTokens",
39101
+ "ttftMs",
39102
+ "durationMs",
39103
+ "costUsd",
39104
+ "degradations",
39105
+ "rtkApplied",
39106
+ "rtkFilterHits",
39107
+ "rtkOriginalCodeUnits",
39108
+ "rtkCompressedCodeUnits",
39109
+ "rtkEstimatedTokensSaved",
39110
+ "rtkFilters"
39111
+ ];
39112
+ function csvCell(value) {
39113
+ if (value === null || value === undefined)
39114
+ return "";
39115
+ const text = Array.isArray(value) ? JSON.stringify(value) : String(value);
39116
+ const first = text.trimStart().charAt(0);
39117
+ const guarded = "=+-@".includes(first) && first !== "" ? `'${text}` : text;
39118
+ return /[",\r\n]/.test(guarded) ? `"${guarded.replaceAll('"', '""')}"` : guarded;
39119
+ }
39120
+ var csvRow = (cells) => `${cells.map(csvCell).join(",")}\r
39121
+ `;
39122
+ function exportHeader(format) {
39123
+ return format === "csv" ? csvRow(EXPORT_COLUMNS) : "";
39124
+ }
39125
+ function exportRow(format, log) {
39126
+ if (format === "jsonl") {
39127
+ const ordered = {};
39128
+ for (const column of EXPORT_COLUMNS)
39129
+ ordered[column] = log[column];
39130
+ return `${JSON.stringify(ordered)}
39131
+ `;
39132
+ }
39133
+ return csvRow(EXPORT_COLUMNS.map((column) => log[column]));
39134
+ }
39135
+ var EXPORT_CONTENT_TYPE = {
39136
+ csv: "text/csv; charset=utf-8",
39137
+ jsonl: "application/x-ndjson"
39138
+ };
39139
+ function exportFilename(format, now) {
39140
+ const stamp = new Date(now).toISOString().replaceAll(/[:.]/g, "-");
39141
+ return `omnigateway-logs-${stamp}.${format}`;
39142
+ }
39143
+ var formatSchema = exports_external.enum(["csv", "jsonl"]);
39144
+ async function* exportLogs(store, input2, scope = ALL) {
39145
+ const { format: requested, ...filters } = input2;
39146
+ const format = parseOrThrow(formatSchema, requested ?? "csv");
39147
+ if (input2.since === undefined || input2.since === "") {
39148
+ throw new GatewayError("BAD_REQUEST", "since: required for export");
39149
+ }
39150
+ if (input2.until === undefined || input2.until === "") {
39151
+ throw new GatewayError("BAD_REQUEST", "until: required for export");
39152
+ }
39153
+ const base = toQuery({ ...filters, limit: EXPORT_BATCH, cursor: undefined }, scope);
39154
+ if (readsNothing(scope)) {
39155
+ yield exportHeader(format);
39156
+ return;
39157
+ }
39158
+ let page = await store.usage.page(base);
39159
+ yield exportHeader(format);
39160
+ for (;; ) {
39161
+ for (const log of page.logs)
39162
+ yield exportRow(format, log);
39163
+ if (page.next === null)
39164
+ return;
39165
+ page = await store.usage.page({ ...base, cursor: page.next });
39166
+ }
39167
+ }
38700
39168
  // packages/control/src/modelLimits.ts
38701
39169
  function narrower(a, b) {
38702
39170
  const context = [a.contextWindow, b.contextWindow].filter((n) => n !== undefined);
@@ -38845,7 +39313,7 @@ var CAPABILITIES = [
38845
39313
  "provider"
38846
39314
  ];
38847
39315
  var ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
38848
- var idSchema = exports_external.string().regex(ID_PATTERN, "id must match /^[a-z][a-z0-9-]{0,31}$/");
39316
+ var idSchema2 = exports_external.string().regex(ID_PATTERN, "id must match /^[a-z][a-z0-9-]{0,31}$/");
38849
39317
  var entrySchema = exports_external.string().min(1).refine((p) => !p.startsWith("/") && !p.split("/").includes(".."), {
38850
39318
  message: "entry must be a relative path with no '..' segment"
38851
39319
  });
@@ -38869,7 +39337,7 @@ var navSchema = exports_external.object({
38869
39337
  }).strict();
38870
39338
  var ORIGIN_CAPABILITIES = ["net:outbound", "provider"];
38871
39339
  var manifestSchema = exports_external.object({
38872
- id: idSchema,
39340
+ id: idSchema2,
38873
39341
  name: exports_external.string().min(1).max(64),
38874
39342
  version: exports_external.string().min(1).max(32),
38875
39343
  api: exports_external.number().int().nonnegative(),
@@ -39433,26 +39901,6 @@ function readProviders(pluginId, manifest, definition) {
39433
39901
  }
39434
39902
  return declared.map((entry) => validateRegistration(pluginId, entry, manifest.origins));
39435
39903
  }
39436
- // packages/control/src/principal.ts
39437
- var ALL = { kind: "all" };
39438
- var NONE2 = { kind: "none" };
39439
- function scopeOf(principal) {
39440
- switch (principal.kind) {
39441
- case "admin":
39442
- case "viewer":
39443
- return ALL;
39444
- case "client":
39445
- return principal.apiKeyId === "" ? NONE2 : { kind: "key", apiKeyId: principal.apiKeyId };
39446
- case "machine":
39447
- return NONE2;
39448
- }
39449
- }
39450
- function scopeKey(scope) {
39451
- return scope.kind === "key" ? scope.apiKeyId : undefined;
39452
- }
39453
- function readsNothing(scope) {
39454
- return scope.kind === "none" || scope.kind === "key" && scope.apiKeyId === "";
39455
- }
39456
39904
  // packages/control/src/quota/headroom.ts
39457
39905
  function usedRatioOf(used, limit) {
39458
39906
  if (limit === null || limit <= 0)
@@ -39596,6 +40044,9 @@ async function accountQuotaHistory(deps, input2) {
39596
40044
  var CONCURRENCY = 4;
39597
40045
  var RATE_LIMIT_COOLDOWN_MS = 180000;
39598
40046
  var COOLDOWN_PREFIX = "quota:cooldown:";
40047
+ var PROBE_LOCK_PREFIX = "quota:probe:";
40048
+ var PROBE_LOCK_TTL_MS = 60000;
40049
+ var PROBE_LOCK_WAIT_MS = 30000;
39599
40050
  async function probe(deps, credential) {
39600
40051
  if (credential.authType !== "oauth")
39601
40052
  return null;
@@ -39626,39 +40077,148 @@ async function probe(deps, credential) {
39626
40077
  });
39627
40078
  return rows;
39628
40079
  }
39629
- async function poll(deps) {
39630
- const logger2 = deps.logger ?? noopLogger;
39631
- const _now = deps.now();
39632
- const eligible2 = (await deps.store.credentials.list()).filter((c) => c.enabled && c.authType === "oauth" && deps.providers[c.provider]?.usage !== undefined);
39633
- const cooling = await Promise.all(eligible2.map((c) => deps.coord.kv.get(COOLDOWN_PREFIX + c.id)));
39634
- const credentials = eligible2.filter((_, index) => cooling[index] === null);
39635
- let written = 0;
40080
+ async function inOrder(items, job) {
40081
+ const results = new Array(items.length).fill(undefined);
39636
40082
  let next = 0;
39637
40083
  const worker = async () => {
39638
- while (next < credentials.length) {
39639
- const credential = credentials[next++];
39640
- if (credential === undefined)
40084
+ while (next < items.length) {
40085
+ const index = next++;
40086
+ const item = items[index];
40087
+ if (item === undefined)
39641
40088
  return;
39642
- try {
39643
- if (await probe(deps, credential) !== null)
39644
- written += 1;
39645
- } catch (error62) {
39646
- const rateLimited = error62 instanceof GatewayError && error62.code === "RATE_LIMIT";
39647
- if (rateLimited) {
39648
- await deps.coord.kv.set(COOLDOWN_PREFIX + credential.id, "1", RATE_LIMIT_COOLDOWN_MS);
39649
- }
39650
- logger2.warn(rateLimited ? "quota probe rate limited" : "quota probe failed", {
39651
- provider: credential.provider,
39652
- credentialId: credential.id,
39653
- code: error62 instanceof GatewayError ? error62.code : "INTERNAL",
39654
- ...rateLimited ? { retryAfterMs: RATE_LIMIT_COOLDOWN_MS } : {},
39655
- reason: describeError(error62, "unknown")
39656
- });
40089
+ results[index] = await job(item);
40090
+ }
40091
+ };
40092
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, items.length) }, () => worker()));
40093
+ return results;
40094
+ }
40095
+ function quotaOps(deps) {
40096
+ const logger2 = deps.logger ?? noopLogger;
40097
+ const inFlight = new Map;
40098
+ async function coordinated(credential, fallback, fn) {
40099
+ try {
40100
+ return await fn();
40101
+ } catch (error62) {
40102
+ logger2.warn("quota probe coordination unavailable", {
40103
+ provider: credential.provider,
40104
+ credentialId: credential.id,
40105
+ coordFallback: true,
40106
+ reason: describeError(error62, "unknown")
40107
+ });
40108
+ return fallback;
40109
+ }
40110
+ }
40111
+ function reportFailure(credential, error62) {
40112
+ const rateLimited = error62 instanceof GatewayError && error62.code === "RATE_LIMIT";
40113
+ logger2.warn(rateLimited ? "quota probe rate limited" : "quota probe failed", {
40114
+ provider: credential.provider,
40115
+ credentialId: credential.id,
40116
+ code: error62 instanceof GatewayError ? error62.code : "INTERNAL",
40117
+ ...rateLimited ? { retryAfterMs: RATE_LIMIT_COOLDOWN_MS } : {},
40118
+ reason: describeError(error62, "unknown")
40119
+ });
40120
+ return error62 instanceof GatewayError ? error62.code : "INTERNAL";
40121
+ }
40122
+ async function runProbe(credential) {
40123
+ const credentialId = credential.id;
40124
+ try {
40125
+ const rows = await probe(deps, credential);
40126
+ if (rows === null)
40127
+ return { outcome: { kind: "noData", credentialId }, wrote: false };
40128
+ return { outcome: { kind: "refreshed", credentialId, windows: rows.length }, wrote: true };
40129
+ } catch (error62) {
40130
+ if (error62 instanceof GatewayError && error62.code === "RATE_LIMIT") {
40131
+ await coordinated(credential, undefined, () => deps.coord.kv.set(COOLDOWN_PREFIX + credentialId, "1", RATE_LIMIT_COOLDOWN_MS));
39657
40132
  }
40133
+ return {
40134
+ outcome: { kind: "failed", credentialId, code: reportFailure(credential, error62) },
40135
+ wrote: false
40136
+ };
40137
+ }
40138
+ }
40139
+ async function runGuarded(credential, startedAt) {
40140
+ const credentialId = credential.id;
40141
+ let entered = false;
40142
+ const body2 = async () => {
40143
+ entered = true;
40144
+ const rows = (await deps.store.credentials.listQuota()).filter((row) => row.credentialId === credentialId);
40145
+ if (rows.some((row) => row.observedAt >= startedAt)) {
40146
+ return { outcome: { kind: "coalesced", credentialId, windows: rows.length }, wrote: false };
40147
+ }
40148
+ return runProbe(credential);
40149
+ };
40150
+ try {
40151
+ return await deps.coord.mutex.withLock(PROBE_LOCK_PREFIX + credentialId, PROBE_LOCK_TTL_MS, PROBE_LOCK_WAIT_MS, body2);
40152
+ } catch (error62) {
40153
+ if (entered)
40154
+ return { outcome: { kind: "failed", credentialId, code: "INTERNAL" }, wrote: false };
40155
+ logger2.warn("quota probe lock unavailable", {
40156
+ provider: credential.provider,
40157
+ credentialId,
40158
+ coordFallback: true,
40159
+ reason: describeError(error62, "unknown")
40160
+ });
40161
+ return runProbe(credential);
40162
+ }
40163
+ }
40164
+ function attempt(credential) {
40165
+ const credentialId = credential.id;
40166
+ if (!credential.enabled) {
40167
+ return Promise.resolve({ outcome: { kind: "disabled", credentialId }, wrote: false });
40168
+ }
40169
+ if (credential.authType !== "oauth" || deps.providers[credential.provider]?.usage === undefined) {
40170
+ return Promise.resolve({ outcome: { kind: "unsupported", credentialId }, wrote: false });
40171
+ }
40172
+ const running = inFlight.get(credentialId);
40173
+ if (running !== undefined) {
40174
+ return running.then((shared) => shared.outcome.kind === "refreshed" ? {
40175
+ outcome: {
40176
+ kind: "coalesced",
40177
+ credentialId,
40178
+ windows: shared.outcome.windows
40179
+ },
40180
+ wrote: false
40181
+ } : { outcome: shared.outcome, wrote: false });
40182
+ }
40183
+ const startedAt = deps.now();
40184
+ const started = (async () => {
40185
+ const cooling = await coordinated(credential, null, () => deps.coord.kv.get(COOLDOWN_PREFIX + credentialId));
40186
+ if (cooling !== null) {
40187
+ return { outcome: { kind: "cooldown", credentialId }, wrote: false };
40188
+ }
40189
+ return runGuarded(credential, startedAt);
40190
+ })();
40191
+ inFlight.set(credentialId, started);
40192
+ return started.finally(() => {
40193
+ inFlight.delete(credentialId);
40194
+ });
40195
+ }
40196
+ return {
40197
+ async poll() {
40198
+ const credentials = await deps.store.credentials.list();
40199
+ const attempts = await inOrder(credentials, attempt);
40200
+ return attempts.filter((a) => a?.wrote === true).length;
40201
+ },
40202
+ async refresh(request2) {
40203
+ if (request2.kind === "one") {
40204
+ const credentialId = request2.credentialId.trim();
40205
+ if (credentialId === "")
40206
+ throw new GatewayError("BAD_REQUEST", "no such credential");
40207
+ const credential = await deps.store.credentials.get(credentialId);
40208
+ if (credential === null)
40209
+ throw new GatewayError("BAD_REQUEST", "no such credential");
40210
+ return { outcomes: [(await attempt(credential)).outcome] };
40211
+ }
40212
+ const credentials = await deps.store.credentials.list();
40213
+ const attempts = await inOrder(credentials, attempt);
40214
+ const outcomes = [];
40215
+ for (const [index, credential] of credentials.entries()) {
40216
+ const result = attempts[index];
40217
+ outcomes.push(result?.outcome ?? { kind: "failed", credentialId: credential.id, code: "INTERNAL" });
40218
+ }
40219
+ return { outcomes };
39658
40220
  }
39659
40221
  };
39660
- await Promise.all(Array.from({ length: Math.min(CONCURRENCY, credentials.length) }, () => worker()));
39661
- return written;
39662
40222
  }
39663
40223
  // packages/control/src/settings.ts
39664
40224
  async function getSettings(store) {
@@ -39864,41 +40424,6 @@ function readFrom(path, offset) {
39864
40424
  closeSync(fd);
39865
40425
  }
39866
40426
  }
39867
- // packages/control/src/usage.ts
39868
- var MAX_LOG_LIMIT = 500;
39869
- var DEFAULT_LOG_LIMIT = 100;
39870
- async function queryUsage(deps, input2, scope = ALL) {
39871
- const grain = parseOrThrow(grainSchema, input2.grain ?? "raw");
39872
- const groupBy = requireDimension(grain, parseOrThrow(dimensionSchema, input2.groupBy ?? "model"));
39873
- const splitBy = input2.splitBy === undefined ? undefined : requireDimension(grain, parseOrThrow(dimensionSchema, input2.splitBy));
39874
- if (scope.kind !== "all") {
39875
- for (const dimension of [groupBy, splitBy]) {
39876
- if (dimension === "credential") {
39877
- throw new GatewayError("BAD_REQUEST", "this dimension is not available on this surface");
39878
- }
39879
- }
39880
- }
39881
- if (readsNothing(scope))
39882
- return [];
39883
- const apiKeyId = scopeKey(scope);
39884
- return deps.store.usage.aggregate({
39885
- grain,
39886
- groupBy,
39887
- ...splitBy === undefined ? {} : { splitBy },
39888
- ...apiKeyId === undefined ? {} : { apiKeyId },
39889
- since: optionalNumber(input2.since, 0),
39890
- until: optionalNumber(input2.until, deps.now())
39891
- });
39892
- }
39893
- function logLimit(requested) {
39894
- const value = optionalNumber(requested, DEFAULT_LOG_LIMIT);
39895
- return Math.floor(Math.min(Math.max(1, value), MAX_LOG_LIMIT));
39896
- }
39897
- async function recentLogs(store, requested, scope = ALL) {
39898
- if (readsNothing(scope))
39899
- return [];
39900
- return store.usage.recent(logLimit(requested), scopeKey(scope));
39901
- }
39902
40427
  // packages/plugin-api/src/events.ts
39903
40428
  var WINDOW_MS2 = {
39904
40429
  "1m": 60000,
@@ -55556,8 +56081,16 @@ function readCookie(request2, name) {
55556
56081
  }
55557
56082
  return null;
55558
56083
  }
55559
- function sessionCookie(request2, token, maxAge) {
55560
- const secure = new URL(request2.url).protocol === "https:" ? ["Secure"] : [];
56084
+ function forwardedProto(request2) {
56085
+ const header = request2.headers.get("x-forwarded-proto");
56086
+ if (header === null)
56087
+ return null;
56088
+ return header.split(",")[0]?.trim().toLowerCase() ?? null;
56089
+ }
56090
+ function sessionCookie(request2, baseUrl, token, maxAge) {
56091
+ const configured2 = URL.canParse(baseUrl) ? new URL(baseUrl).protocol === "https:" : false;
56092
+ const https = configured2 || forwardedProto(request2) === "https" || new URL(request2.url).protocol === "https:";
56093
+ const secure = https ? ["Secure"] : [];
55561
56094
  return [
55562
56095
  `${ADMIN_COOKIE}=${token}`,
55563
56096
  "Path=/",
@@ -55834,6 +56367,7 @@ function createQuiesceLatch() {
55834
56367
  }
55835
56368
 
55836
56369
  // apps/gateway/src/routes/admin.ts
56370
+ var EXPORT_ENCODER = new TextEncoder;
55837
56371
  function adminRoutes(deps) {
55838
56372
  const logger2 = deps.logger ?? noopLogger;
55839
56373
  const reportedCatalogProblems = new Set;
@@ -55883,7 +56417,7 @@ function adminRoutes(deps) {
55883
56417
  if (token === null)
55884
56418
  throw new GatewayError("INTERNAL", "could not create admin session");
55885
56419
  logger2.info("admin setup completed");
55886
- set2.headers["set-cookie"] = sessionCookie(request2, token, Math.floor(deps.sessionTtlMs / 1000));
56420
+ set2.headers["set-cookie"] = sessionCookie(request2, deps.baseUrl, token, Math.floor(deps.sessionTtlMs / 1000));
55887
56421
  return { ok: true };
55888
56422
  }).post("/api/login", async ({ request: request2, set: set2 }) => {
55889
56423
  const body2 = await readJsonRecord(request2);
@@ -55895,13 +56429,13 @@ function adminRoutes(deps) {
55895
56429
  throw new GatewayError("AUTH", "invalid password");
55896
56430
  }
55897
56431
  logger2.info("admin login succeeded");
55898
- set2.headers["set-cookie"] = sessionCookie(request2, token, Math.floor(deps.sessionTtlMs / 1000));
56432
+ set2.headers["set-cookie"] = sessionCookie(request2, deps.baseUrl, token, Math.floor(deps.sessionTtlMs / 1000));
55899
56433
  return { ok: true };
55900
56434
  }).post("/api/logout", async ({ request: request2, set: set2 }) => {
55901
56435
  const token = readCookie(request2, ADMIN_COOKIE);
55902
56436
  if (token !== null)
55903
56437
  await deps.admin.logout(token);
55904
- set2.headers["set-cookie"] = sessionCookie(request2, "", 0);
56438
+ set2.headers["set-cookie"] = sessionCookie(request2, deps.baseUrl, "", 0);
55905
56439
  return { ok: true };
55906
56440
  }).get("/api/credentials", async ({ request: request2 }) => {
55907
56441
  await requireReader(request2, deps.admin);
@@ -55924,6 +56458,17 @@ function adminRoutes(deps) {
55924
56458
  until: query.until,
55925
56459
  credentialId: query.credentialId
55926
56460
  });
56461
+ }).post("/api/credentials/quota/refresh", async ({ request: request2 }) => {
56462
+ await requireAdmin(request2, deps.admin);
56463
+ const quota = deps.quota;
56464
+ if (quota === undefined) {
56465
+ throw new GatewayError("INTERNAL", "quota refresh is not available on this process");
56466
+ }
56467
+ const result = await quota.refresh(parseOrThrow(quotaRefreshSchema, await readJson(request2)));
56468
+ if (result.outcomes.some((o) => o.kind === "refreshed" || o.kind === "coalesced")) {
56469
+ changed("res:quota");
56470
+ }
56471
+ return result;
55927
56472
  }).patch("/api/credentials/:id", async ({ request: request2, params }) => {
55928
56473
  await requireAdmin(request2, deps.admin);
55929
56474
  await patchCredential(deps, params.id, await readJson(request2));
@@ -56066,7 +56611,33 @@ function adminRoutes(deps) {
56066
56611
  };
56067
56612
  }).get("/api/logs", async ({ request: request2, query }) => {
56068
56613
  await requireReader(request2, deps.admin);
56069
- return { logs: await recentLogs(deps.store, query.limit) };
56614
+ return pageLogs(deps.store, query);
56615
+ }).get("/api/logs/export", async ({ request: request2, query }) => {
56616
+ await requireReader(request2, deps.admin);
56617
+ const format = query.format === "jsonl" ? "jsonl" : "csv";
56618
+ const rows = exportLogs(deps.store, query);
56619
+ const head = await rows.next();
56620
+ const body2 = new ReadableStream({
56621
+ start(controller) {
56622
+ if (head.done !== true)
56623
+ controller.enqueue(EXPORT_ENCODER.encode(head.value));
56624
+ },
56625
+ async pull(controller) {
56626
+ const next = await rows.next();
56627
+ if (next.done === true)
56628
+ controller.close();
56629
+ else
56630
+ controller.enqueue(EXPORT_ENCODER.encode(next.value));
56631
+ },
56632
+ cancel: (reason) => void rows.return(reason)
56633
+ });
56634
+ return new Response(body2, {
56635
+ headers: {
56636
+ "content-type": EXPORT_CONTENT_TYPE[format],
56637
+ "content-disposition": `attachment; filename="${exportFilename(format, deps.now())}"`,
56638
+ "cache-control": "no-store"
56639
+ }
56640
+ });
56070
56641
  }).get("/api/requests/:id/body", async ({ request: request2, params }) => {
56071
56642
  await requireAdmin(request2, deps.admin);
56072
56643
  return readRequestBody(deps.store, params.id);
@@ -56129,13 +56700,13 @@ function clientRoutes(deps) {
56129
56700
  throw new GatewayError("AUTH", "invalid api key");
56130
56701
  }
56131
56702
  logger2.info("client login succeeded");
56132
- set2.headers["set-cookie"] = sessionCookie(request2, token, Math.floor(deps.sessionTtlMs / 1000));
56703
+ set2.headers["set-cookie"] = sessionCookie(request2, deps.baseUrl, token, Math.floor(deps.sessionTtlMs / 1000));
56133
56704
  return { ok: true };
56134
56705
  }).post("/api/client/logout", async ({ request: request2, set: set2 }) => {
56135
56706
  const token = readCookie(request2, ADMIN_COOKIE);
56136
56707
  if (token !== null)
56137
56708
  await deps.admin.logout(token);
56138
- set2.headers["set-cookie"] = sessionCookie(request2, "", 0);
56709
+ set2.headers["set-cookie"] = sessionCookie(request2, deps.baseUrl, "", 0);
56139
56710
  return { ok: true };
56140
56711
  }).get("/api/client/summary", async ({ request: request2 }) => {
56141
56712
  const apiKeyId = await requireClient(request2, deps.admin);
@@ -56154,8 +56725,19 @@ function clientRoutes(deps) {
56154
56725
  }, scopeOf({ kind: "client", apiKeyId }));
56155
56726
  }).get("/api/client/logs", async ({ request: request2, query }) => {
56156
56727
  const apiKeyId = await requireClient(request2, deps.admin);
56157
- const rows = await recentLogs(deps.store, logLimit(query.limit), scopeOf({ kind: "client", apiKeyId }));
56158
- return { logs: rows.map(toClientLog) };
56728
+ const page = await pageLogs(deps.store, {
56729
+ limit: query.limit,
56730
+ cursor: query.cursor,
56731
+ since: query.since,
56732
+ until: query.until,
56733
+ state: query.state,
56734
+ failed: query.failed,
56735
+ provider: query.provider,
56736
+ requestedModel: query.requestedModel,
56737
+ resolvedModel: query.resolvedModel,
56738
+ errorCode: query.errorCode
56739
+ }, scopeOf({ kind: "client", apiKeyId }));
56740
+ return { logs: page.logs.map(toClientLog), nextCursor: page.nextCursor };
56159
56741
  }).get("/api/client/quota", async ({ request: request2 }) => {
56160
56742
  await requireClient(request2, deps.admin);
56161
56743
  return { accounts: await accountQuota({ store: deps.store, now }) };
@@ -61720,7 +62302,7 @@ function createRing(limits = DEFAULT_RING_LIMITS) {
61720
62302
  }
61721
62303
 
61722
62304
  // apps/gateway/src/version.ts
61723
- var VERSION = "0.11.4";
62305
+ var VERSION = "0.12.1";
61724
62306
 
61725
62307
  // apps/gateway/src/app.ts
61726
62308
  var ADMIN_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
@@ -61878,11 +62460,13 @@ function createApp(deps) {
61878
62460
  broadcaster,
61879
62461
  nodeId,
61880
62462
  consoleFleet,
62463
+ ...deps.quota === undefined ? {} : { quota: deps.quota },
61881
62464
  ...deps.console === undefined ? {} : { console: deps.console }
61882
62465
  })).use(clientRoutes({
61883
62466
  store: deps.store,
61884
62467
  admin,
61885
62468
  sessionTtlMs: ADMIN_SESSION_TTL_MS,
62469
+ baseUrl: deps.baseUrl,
61886
62470
  ...deps.dayOffsetMinutes === undefined ? {} : { dayOffsetMinutes: deps.dayOffsetMinutes },
61887
62471
  now,
61888
62472
  logger: logger2
@@ -62962,12 +63546,13 @@ async function startQuotaPoller(deps) {
62962
63546
  const { quotaPollIntervalMs } = await deps.store.config.getSettings();
62963
63547
  if (quotaPollIntervalMs <= 0)
62964
63548
  return () => {};
63549
+ const quota = deps.quota ?? quotaOps(deps);
62965
63550
  let running = false;
62966
63551
  const pass = () => {
62967
63552
  if (running)
62968
63553
  return;
62969
63554
  running = true;
62970
- underLease(deps.lease, "quota-poll", 2 * quotaPollIntervalMs, () => poll(deps).then()).catch((error62) => {
63555
+ underLease(deps.lease, "quota-poll", 2 * quotaPollIntervalMs, () => quota.poll().then()).catch((error62) => {
62971
63556
  logger2.error("quota poll failed", {
62972
63557
  reason: describeError(error62, "unknown")
62973
63558
  });
@@ -63591,6 +64176,7 @@ async function main() {
63591
64176
  const coord = shared ?? memoryCoord({ now });
63592
64177
  const lease = { coord, nodeId };
63593
64178
  const refresh = createRefresher({ store, providers: OAUTH_PROVIDERS, http, now, logger: logger2, coord });
64179
+ const quota = quotaOps({ store, coord, providers: OAUTH_PROVIDERS, http, refresh, now, logger: logger2 });
63594
64180
  const staticDir = dashboardDir();
63595
64181
  logger2.info(existsSync2(staticDir) ? "dashboard directory resolved" : "dashboard directory absent", {
63596
64182
  path: staticDir
@@ -63650,6 +64236,7 @@ async function main() {
63650
64236
  http,
63651
64237
  now,
63652
64238
  refresh,
64239
+ quota,
63653
64240
  staticDir,
63654
64241
  logger: logger2,
63655
64242
  console: console2,
@@ -63698,6 +64285,7 @@ async function main() {
63698
64285
  providers: OAUTH_PROVIDERS,
63699
64286
  http,
63700
64287
  refresh,
64288
+ quota,
63701
64289
  now,
63702
64290
  logger: logger2,
63703
64291
  broadcaster