omnigateway 0.11.4 → 0.12.0

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 +670 -91
  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));
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 };
39657
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,
@@ -55834,6 +56359,7 @@ function createQuiesceLatch() {
55834
56359
  }
55835
56360
 
55836
56361
  // apps/gateway/src/routes/admin.ts
56362
+ var EXPORT_ENCODER = new TextEncoder;
55837
56363
  function adminRoutes(deps) {
55838
56364
  const logger2 = deps.logger ?? noopLogger;
55839
56365
  const reportedCatalogProblems = new Set;
@@ -55924,6 +56450,17 @@ function adminRoutes(deps) {
55924
56450
  until: query.until,
55925
56451
  credentialId: query.credentialId
55926
56452
  });
56453
+ }).post("/api/credentials/quota/refresh", async ({ request: request2 }) => {
56454
+ await requireAdmin(request2, deps.admin);
56455
+ const quota = deps.quota;
56456
+ if (quota === undefined) {
56457
+ throw new GatewayError("INTERNAL", "quota refresh is not available on this process");
56458
+ }
56459
+ const result = await quota.refresh(parseOrThrow(quotaRefreshSchema, await readJson(request2)));
56460
+ if (result.outcomes.some((o) => o.kind === "refreshed" || o.kind === "coalesced")) {
56461
+ changed("res:quota");
56462
+ }
56463
+ return result;
55927
56464
  }).patch("/api/credentials/:id", async ({ request: request2, params }) => {
55928
56465
  await requireAdmin(request2, deps.admin);
55929
56466
  await patchCredential(deps, params.id, await readJson(request2));
@@ -56066,7 +56603,33 @@ function adminRoutes(deps) {
56066
56603
  };
56067
56604
  }).get("/api/logs", async ({ request: request2, query }) => {
56068
56605
  await requireReader(request2, deps.admin);
56069
- return { logs: await recentLogs(deps.store, query.limit) };
56606
+ return pageLogs(deps.store, query);
56607
+ }).get("/api/logs/export", async ({ request: request2, query }) => {
56608
+ await requireReader(request2, deps.admin);
56609
+ const format = query.format === "jsonl" ? "jsonl" : "csv";
56610
+ const rows = exportLogs(deps.store, query);
56611
+ const head = await rows.next();
56612
+ const body2 = new ReadableStream({
56613
+ start(controller) {
56614
+ if (head.done !== true)
56615
+ controller.enqueue(EXPORT_ENCODER.encode(head.value));
56616
+ },
56617
+ async pull(controller) {
56618
+ const next = await rows.next();
56619
+ if (next.done === true)
56620
+ controller.close();
56621
+ else
56622
+ controller.enqueue(EXPORT_ENCODER.encode(next.value));
56623
+ },
56624
+ cancel: (reason) => void rows.return(reason)
56625
+ });
56626
+ return new Response(body2, {
56627
+ headers: {
56628
+ "content-type": EXPORT_CONTENT_TYPE[format],
56629
+ "content-disposition": `attachment; filename="${exportFilename(format, deps.now())}"`,
56630
+ "cache-control": "no-store"
56631
+ }
56632
+ });
56070
56633
  }).get("/api/requests/:id/body", async ({ request: request2, params }) => {
56071
56634
  await requireAdmin(request2, deps.admin);
56072
56635
  return readRequestBody(deps.store, params.id);
@@ -56154,8 +56717,19 @@ function clientRoutes(deps) {
56154
56717
  }, scopeOf({ kind: "client", apiKeyId }));
56155
56718
  }).get("/api/client/logs", async ({ request: request2, query }) => {
56156
56719
  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) };
56720
+ const page = await pageLogs(deps.store, {
56721
+ limit: query.limit,
56722
+ cursor: query.cursor,
56723
+ since: query.since,
56724
+ until: query.until,
56725
+ state: query.state,
56726
+ failed: query.failed,
56727
+ provider: query.provider,
56728
+ requestedModel: query.requestedModel,
56729
+ resolvedModel: query.resolvedModel,
56730
+ errorCode: query.errorCode
56731
+ }, scopeOf({ kind: "client", apiKeyId }));
56732
+ return { logs: page.logs.map(toClientLog), nextCursor: page.nextCursor };
56159
56733
  }).get("/api/client/quota", async ({ request: request2 }) => {
56160
56734
  await requireClient(request2, deps.admin);
56161
56735
  return { accounts: await accountQuota({ store: deps.store, now }) };
@@ -61720,7 +62294,7 @@ function createRing(limits = DEFAULT_RING_LIMITS) {
61720
62294
  }
61721
62295
 
61722
62296
  // apps/gateway/src/version.ts
61723
- var VERSION = "0.11.4";
62297
+ var VERSION = "0.12.0";
61724
62298
 
61725
62299
  // apps/gateway/src/app.ts
61726
62300
  var ADMIN_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
@@ -61878,6 +62452,7 @@ function createApp(deps) {
61878
62452
  broadcaster,
61879
62453
  nodeId,
61880
62454
  consoleFleet,
62455
+ ...deps.quota === undefined ? {} : { quota: deps.quota },
61881
62456
  ...deps.console === undefined ? {} : { console: deps.console }
61882
62457
  })).use(clientRoutes({
61883
62458
  store: deps.store,
@@ -62962,12 +63537,13 @@ async function startQuotaPoller(deps) {
62962
63537
  const { quotaPollIntervalMs } = await deps.store.config.getSettings();
62963
63538
  if (quotaPollIntervalMs <= 0)
62964
63539
  return () => {};
63540
+ const quota = deps.quota ?? quotaOps(deps);
62965
63541
  let running = false;
62966
63542
  const pass = () => {
62967
63543
  if (running)
62968
63544
  return;
62969
63545
  running = true;
62970
- underLease(deps.lease, "quota-poll", 2 * quotaPollIntervalMs, () => poll(deps).then()).catch((error62) => {
63546
+ underLease(deps.lease, "quota-poll", 2 * quotaPollIntervalMs, () => quota.poll().then()).catch((error62) => {
62971
63547
  logger2.error("quota poll failed", {
62972
63548
  reason: describeError(error62, "unknown")
62973
63549
  });
@@ -63591,6 +64167,7 @@ async function main() {
63591
64167
  const coord = shared ?? memoryCoord({ now });
63592
64168
  const lease = { coord, nodeId };
63593
64169
  const refresh = createRefresher({ store, providers: OAUTH_PROVIDERS, http, now, logger: logger2, coord });
64170
+ const quota = quotaOps({ store, coord, providers: OAUTH_PROVIDERS, http, refresh, now, logger: logger2 });
63594
64171
  const staticDir = dashboardDir();
63595
64172
  logger2.info(existsSync2(staticDir) ? "dashboard directory resolved" : "dashboard directory absent", {
63596
64173
  path: staticDir
@@ -63650,6 +64227,7 @@ async function main() {
63650
64227
  http,
63651
64228
  now,
63652
64229
  refresh,
64230
+ quota,
63653
64231
  staticDir,
63654
64232
  logger: logger2,
63655
64233
  console: console2,
@@ -63698,6 +64276,7 @@ async function main() {
63698
64276
  providers: OAUTH_PROVIDERS,
63699
64277
  http,
63700
64278
  refresh,
64279
+ quota,
63701
64280
  now,
63702
64281
  logger: logger2,
63703
64282
  broadcaster