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/bin/omni.js CHANGED
@@ -37,6 +37,9 @@ var consoleWriter = {
37
37
  err: (line) => {
38
38
  process.stderr.write(`${line}
39
39
  `);
40
+ },
41
+ raw: (chunk) => {
42
+ process.stdout.write(chunk);
40
43
  }
41
44
  };
42
45
  function paint(ctx, tone, text) {
@@ -20929,6 +20932,54 @@ var _003_key_expiry_default = `-- The Postgres half of the sqlite migration of t
20929
20932
  ALTER TABLE api_keys ADD COLUMN expires_at BIGINT;
20930
20933
  `;
20931
20934
 
20935
+ // packages/store/src/postgres/migrations/004_log_keyset.sql
20936
+ var _004_log_keyset_default = `-- Indexes for the keyset traversal of \`request_logs\`. The SQLite copy,
20937
+ -- \`015_log_keyset.sql\`, carries the reasoning: \`id\` in the index is what makes
20938
+ -- "the row after this one" a question with one answer when \`at\` repeats, and
20939
+ -- the three shorter indexes are replaced rather than kept because a redundant
20940
+ -- prefix costs a write per row and answers nothing the longer one does not.
20941
+ DROP INDEX IF EXISTS idx_request_logs_at;
20942
+ DROP INDEX IF EXISTS idx_request_logs_key_at;
20943
+ DROP INDEX IF EXISTS idx_request_logs_cred;
20944
+
20945
+ CREATE INDEX idx_request_logs_at ON request_logs (at DESC, id DESC);
20946
+ CREATE INDEX idx_request_logs_key_at ON request_logs (api_key_id, at DESC, id DESC);
20947
+ CREATE INDEX idx_request_logs_cred ON request_logs (credential_id, at DESC, id DESC);
20948
+ `;
20949
+
20950
+ // packages/store/src/postgres/migrations/005_log_filter_indexes.sql
20951
+ var _005_log_filter_indexes_default = `-- An index for the provider filter on \`request_logs\`. Filtering by a provider
20952
+ -- with no traffic has nothing to seek, so proving the absence reads every row,
20953
+ -- and the console offers exactly that query because its dropdown is built from
20954
+ -- the provider catalog rather than from traffic. Measured here at 500,000 rows,
20955
+ -- and separately in the SQLite copy \`016_log_filter_indexes.sql\`:
20956
+ --
20957
+ -- resolved_provider = without index with index
20958
+ -- a provider with no rows 17.17ms 0.01ms
20959
+ -- a minority provider 0.04ms 0.02ms
20960
+ -- the dominant provider 0.02ms 0.02ms
20961
+ --
20962
+ -- Only the first row justifies the index, and it justifies it on both backends:
20963
+ -- without it the planner falls back to a sequential scan with a sort on top,
20964
+ -- because there is no prefix to seek and every row has to be rejected before the
20965
+ -- absence is proven. The other two answer off \`idx_request_logs_at\` either way \u2014
20966
+ -- a provider that has traffic fills a page near the head immediately.
20967
+ --
20968
+ -- Leading column then the keyset pair, so one seek answers the filter and the
20969
+ -- walk from it is already in page order.
20970
+ --
20971
+ -- **This is the only log-filter index on this backend, and the SQLite copy has
20972
+ -- one more.** There, \`017_log_text_search.sql\` moves \`requested_model\`,
20973
+ -- \`resolved_model\` and \`error_code\` into the keyset index so a substring scan
20974
+ -- tests the match against entries it is already walking. Do not mirror that
20975
+ -- here: this planner reads a leading-wildcard \`ILIKE\` as unindexable, so a
20976
+ -- substring miss plans as a parallel sequential scan with a sort on top and
20977
+ -- \`idx_request_logs_at\` is never opened. Measured at 500,000 rows, widening it
20978
+ -- cost 15MB to 58MB and a wider row per insert, and bought 0.25ms of 86ms. The
20979
+ -- backends share the predicate in \`logPage.ts\`, not the index shape.
20980
+ CREATE INDEX idx_request_logs_provider ON request_logs (resolved_provider, at DESC, id DESC);
20981
+ `;
20982
+
20932
20983
  // packages/store/src/postgres/db.ts
20933
20984
  var num = (v) => Number(v);
20934
20985
  var numOrNull = (v) => v === null ? null : Number(v);
@@ -20936,7 +20987,9 @@ var MIGRATION_LOCK = 7140641;
20936
20987
  var MIGRATIONS = [
20937
20988
  { id: 1, sql: _001_init_default },
20938
20989
  { id: 2, sql: _002_health_measurements_default },
20939
- { id: 3, sql: _003_key_expiry_default }
20990
+ { id: 3, sql: _003_key_expiry_default },
20991
+ { id: 4, sql: _004_log_keyset_default },
20992
+ { id: 5, sql: _005_log_filter_indexes_default }
20940
20993
  ];
20941
20994
  async function openPg(url2) {
20942
20995
  const sql = new SQL({ url: url2 });
@@ -21980,6 +22033,57 @@ function isRtkFilterId(value) {
21980
22033
  return FILTER_IDS.has(value);
21981
22034
  }
21982
22035
 
22036
+ // packages/store/src/logPage.ts
22037
+ function logPageClauses(query, placeholder, like = "LIKE") {
22038
+ const clauses = [];
22039
+ const bindings = [];
22040
+ const bind = (value) => {
22041
+ bindings.push(value);
22042
+ return placeholder(bindings.length);
22043
+ };
22044
+ const contains = (column, value) => {
22045
+ if (value === undefined)
22046
+ return;
22047
+ const escaped = value.replace(/[\\%_]/g, "\\$&");
22048
+ clauses.push(`${column} ${like} ${bind(`%${escaped}%`)} ESCAPE '\\'`);
22049
+ };
22050
+ const eq = (column, value) => {
22051
+ if (value !== undefined)
22052
+ clauses.push(`${column} = ${bind(value)}`);
22053
+ };
22054
+ if (query.cursor !== null) {
22055
+ const cursor = query.cursor;
22056
+ clauses.push(`(at < ${bind(cursor.at)} OR (at = ${bind(cursor.at)} AND id < ${bind(cursor.id)}))`);
22057
+ }
22058
+ eq("api_key_id", query.apiKeyId);
22059
+ eq("credential_id", query.credentialId);
22060
+ eq("resolved_provider", query.provider);
22061
+ eq("state", query.state);
22062
+ contains("requested_model", query.requestedModel);
22063
+ contains("resolved_model", query.resolvedModel);
22064
+ contains("error_code", query.errorCode);
22065
+ if (query.model !== undefined) {
22066
+ const before = clauses.length;
22067
+ contains("requested_model", query.model);
22068
+ contains("resolved_model", query.model);
22069
+ clauses.push(`(${clauses.splice(before).join(" OR ")})`);
22070
+ }
22071
+ if (query.since !== undefined)
22072
+ clauses.push(`at >= ${bind(query.since)}`);
22073
+ if (query.until !== undefined)
22074
+ clauses.push(`at <= ${bind(query.until)}`);
22075
+ if (query.failed === true)
22076
+ clauses.push("state = 'done' AND status >= 400");
22077
+ return { where: clauses.length === 0 ? "" : `WHERE ${clauses.join(" AND ")}`, bindings };
22078
+ }
22079
+ function splitPage(rows, limit) {
22080
+ if (rows.length <= limit)
22081
+ return { logs: rows, next: null };
22082
+ const logs = rows.slice(0, limit);
22083
+ const last = logs[logs.length - 1];
22084
+ return { logs, next: last === undefined ? null : { at: last.at, id: last.id } };
22085
+ }
22086
+
21983
22087
  // packages/store/src/postgres/rollup.ts
21984
22088
  var DAILY_UPSERT = `
21985
22089
  INSERT INTO usage_daily
@@ -22284,6 +22388,12 @@ function createUsageRepo(sql, nodeId, dayOffsetMinutes) {
22284
22388
  ]) : await sql.unsafe("SELECT * FROM request_logs WHERE api_key_id = $1 ORDER BY at DESC LIMIT $2", [apiKeyId, limit]);
22285
22389
  return rows.map(toLog);
22286
22390
  },
22391
+ async page(query) {
22392
+ const { where, bindings } = logPageClauses(query, (n) => `$${n}`, "ILIKE");
22393
+ const rows = (await sql.unsafe(`SELECT * FROM request_logs ${where}
22394
+ ORDER BY at DESC, id DESC LIMIT $${bindings.length + 1}`, [...bindings, query.limit + 1])).map(toLog);
22395
+ return splitPage(rows, query.limit);
22396
+ },
22287
22397
  async lastUsedByCredential(credentialId) {
22288
22398
  const rows = await sql.unsafe("SELECT MAX(at) AS at FROM request_logs WHERE credential_id = $1", [credentialId]);
22289
22399
  return numOrNull(rows[0]?.at ?? null);
@@ -23515,6 +23625,114 @@ var _014_key_expiry_default = `-- An optional expiry on a gateway API key: an ab
23515
23625
  ALTER TABLE api_keys ADD COLUMN expires_at INTEGER;
23516
23626
  `;
23517
23627
 
23628
+ // packages/store/src/sqlite/migrations/015_log_keyset.sql
23629
+ 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";
23630
+
23631
+ // packages/store/src/sqlite/migrations/016_log_filter_indexes.sql
23632
+ var _016_log_filter_indexes_default = `-- An index for the provider filter on \`request_logs\`, which \`015_log_keyset.sql\`
23633
+ -- deliberately left out pending a measurement. This is that measurement.
23634
+ --
23635
+ -- Without it, filtering by a provider that has *no* traffic scans the whole
23636
+ -- table: there is nothing to seek, so proving the absence means reading every
23637
+ -- row. \`bun:sqlite\` is synchronous, so that scan is the event loop \u2014 time in
23638
+ -- which no proxied request moves. Measured at 500,000 rows:
23639
+ --
23640
+ -- resolved_provider = without index with index
23641
+ -- a provider with no rows 84.0ms 0.0ms
23642
+ -- a minority provider 0.19ms 0.10ms
23643
+ -- the dominant provider 0.09ms 0.08ms
23644
+ --
23645
+ -- Only the first row justifies the index, and it justifies it completely. It is
23646
+ -- also the case the console invites: the filter is a dropdown built from the
23647
+ -- provider catalog rather than from traffic, so every provider this gateway
23648
+ -- *could* route to is offered, including the ones an operator has no account
23649
+ -- for. Selecting one is a normal thing to do and it was the most expensive
23650
+ -- query on the surface.
23651
+ --
23652
+ -- Leading column then the keyset pair, so one seek answers the filter and the
23653
+ -- walk from it is already in page order.
23654
+ --
23655
+ -- \`state\` gets nothing: \`idx_request_logs_pending\` already covers the only
23656
+ -- selective half, and \`state = 'done'\` is every row, so the keyset index fills a
23657
+ -- page immediately. Nothing per-column for \`failed\` either, whose predicate is
23658
+ -- \`state = 'done' AND status >= 400\` and which reads at 1.0ms off the keyset
23659
+ -- index, because failures cluster near the head an operator is looking at.
23660
+ --
23661
+ -- The filters an operator *types* are not here, and not by omission: they match
23662
+ -- substrings, which no B-tree can seek. \`017_log_text_search.sql\` carries them,
23663
+ -- and carries them inside the keyset index rather than beside it.
23664
+ CREATE INDEX idx_request_logs_provider ON request_logs (resolved_provider, at DESC, id DESC);
23665
+ `;
23666
+
23667
+ // packages/store/src/sqlite/migrations/017_log_text_search.sql
23668
+ var _017_log_text_search_default = `-- Indexes the typed log filters, which match substrings rather than values.
23669
+ --
23670
+ -- \`requested_model\`, \`resolved_model\`, \`error_code\`, and the \`model\` filter that
23671
+ -- reads the first two, became \`LIKE '%value%'\`, because an exact match on a name
23672
+ -- nobody recalls exactly answered "no such traffic" for every spelling but one. A
23673
+ -- leading wildcard cannot seek a B-tree: there is no prefix to range over.
23674
+ --
23675
+ -- The three \`DROP\`s below are **upgrade-only cleanup and no-ops on a fresh
23676
+ -- install.** An earlier draft of 016 created a per-column index for each of these
23677
+ -- three filters, back when they were equalities. Nothing released ever ran it, so
23678
+ -- 016 now creates only the provider index it still justifies \u2014 but a database
23679
+ -- that applied the draft is carrying three indexes that can no longer be seeked
23680
+ -- and still cost a write per row, so they are named here rather than left to rot.
23681
+ -- Building them only to drop them cost an upgrading install 1,334ms at 1,000,000
23682
+ -- rows, which is why the fix was to stop creating them rather than to keep the
23683
+ -- symmetry.
23684
+ --
23685
+ -- What the typed filters get instead is those same columns moved *into* the keyset
23686
+ -- index. The scan is going to walk \`(at DESC, id DESC)\` regardless \u2014 that is where
23687
+ -- the ordering comes from \u2014 and carrying the three text columns in that index lets
23688
+ -- it test the match against index entries rather than fetching each row to do it.
23689
+ -- Only the page that survives is fetched.
23690
+ --
23691
+ -- Measured at 1,000,000 rows of 300 bytes, filter matching nothing, which is the
23692
+ -- case that has to scan to the end to prove it:
23693
+ --
23694
+ -- keyset index model filter error filter head read index size
23695
+ -- (at, id) 204.4ms 160.1ms 0.08ms 44.2MB
23696
+ -- (at, id, + 3 columns) 78.3ms 33.0ms 0.08ms 87.0MB
23697
+ --
23698
+ -- So: 2.6x on the model filter, 4.8x on the error one, no cost to the unfiltered
23699
+ -- head read every board opens with, and double the index. The size is the price
23700
+ -- and it is worth naming \u2014 at a million rows this index is 87MB against 44MB \u2014
23701
+ -- but it is one index instead of three, and it is the only shape a substring can
23702
+ -- use at all.
23703
+ --
23704
+ -- A second, separate covering index does **not** work: SQLite prefers the
23705
+ -- narrower index that supplies the ordering and ignores the wider one, measured
23706
+ -- at 198ms against 202ms. The columns have to be in the index the scan already
23707
+ -- wants, which is why this replaces \`idx_request_logs_at\` rather than joining it.
23708
+ --
23709
+ -- \`resolved_provider\` keeps 016's index and its \`=\`: it arrives from a dropdown
23710
+ -- of ids the gateway supplied, so it is never half-typed, a substring would only
23711
+ -- widen it, and an equality can still seek \u2014 which is worth 84ms to 0 on a
23712
+ -- provider with no traffic.
23713
+ --
23714
+ -- The \`ANALYZE\` trap an earlier draft of 016 documented is gone with the plan
23715
+ -- that had it. There is one plan for a leading wildcard, so statistics have
23716
+ -- nothing to choose between: measured identical, 22.5ms either way at 300,000
23717
+ -- rows.
23718
+ --
23719
+ -- A time bound still collapses all of this \u2014 the same 22.5ms miss is 0.23ms with
23720
+ -- \`since\` set to the newest 1% \u2014 because \`at\` still leads the index. That remains
23721
+ -- the cheapest thing an operator can do, and the reason export requires both
23722
+ -- bounds. If a substring miss on a very large log ever needs to be free rather
23723
+ -- than linear, the next step is FTS5's trigram tokenizer, which is a shadow
23724
+ -- table and a trigger to keep in sync and a \`pg_trgm\` GIN index to match it on
23725
+ -- the other backend. Not worth it for one query per typed name, behind a
23726
+ -- debounce.
23727
+ DROP INDEX IF EXISTS idx_request_logs_resolved_model;
23728
+ DROP INDEX IF EXISTS idx_request_logs_requested_model;
23729
+ DROP INDEX IF EXISTS idx_request_logs_error_code;
23730
+
23731
+ DROP INDEX IF EXISTS idx_request_logs_at;
23732
+ CREATE INDEX idx_request_logs_at
23733
+ ON request_logs (at DESC, id DESC, requested_model, resolved_model, error_code);
23734
+ `;
23735
+
23518
23736
  // packages/store/src/sqlite/db.ts
23519
23737
  var MIGRATIONS2 = [
23520
23738
  { id: 1, sql: _001_init_default2 },
@@ -23530,7 +23748,10 @@ var MIGRATIONS2 = [
23530
23748
  { id: 11, sql: _011_plugin_migrations_default },
23531
23749
  { id: 12, sql: _012_nodes_default },
23532
23750
  { id: 13, sql: _013_health_measurements_default },
23533
- { id: 14, sql: _014_key_expiry_default }
23751
+ { id: 14, sql: _014_key_expiry_default },
23752
+ { id: 15, sql: _015_log_keyset_default },
23753
+ { id: 16, sql: _016_log_filter_indexes_default },
23754
+ { id: 17, sql: _017_log_text_search_default }
23534
23755
  ];
23535
23756
  function statements(sql) {
23536
23757
  return sql.split(`
@@ -24031,6 +24252,11 @@ function createUsageRepo2(db, nodeId, dayOffsetMinutes = hostDayOffsetMinutes())
24031
24252
  }
24032
24253
  return db.query("SELECT * FROM request_logs WHERE api_key_id = ? ORDER BY at DESC LIMIT ?").all(apiKeyId, limit).map(toLog2);
24033
24254
  },
24255
+ async page(query) {
24256
+ const { where, bindings } = logPageClauses(query, () => "?");
24257
+ const rows = db.query(`SELECT * FROM request_logs ${where} ORDER BY at DESC, id DESC LIMIT ?`).all(...bindings, query.limit + 1).map(toLog2);
24258
+ return splitPage(rows, query.limit);
24259
+ },
24034
24260
  async lastUsedByCredential(credentialId) {
24035
24261
  const row = db.query("SELECT MAX(at) AS at FROM request_logs WHERE credential_id = ?").get(credentialId);
24036
24262
  return row?.at ?? null;
@@ -24225,6 +24451,7 @@ async function createStore(opts) {
24225
24451
  append: (log) => handle.usage.append(log),
24226
24452
  sweepPending: (now) => handle.usage.sweepPending(now),
24227
24453
  recent: (limit, apiKeyId) => handle.usage.recent(limit, apiKeyId),
24454
+ page: (query) => handle.usage.page(query),
24228
24455
  lastUsedByCredential: (credentialId) => handle.usage.lastUsedByCredential(credentialId),
24229
24456
  scan: (cursor, limit) => handle.usage.scan(cursor, limit),
24230
24457
  aggregate: (q) => handle.usage.aggregate(q),
@@ -31894,6 +32121,13 @@ var credentialPatchSchema = exports_external.object({
31894
32121
  tier: exports_external.number().int().min(1).optional(),
31895
32122
  weight: exports_external.number().positive().optional()
31896
32123
  }).strict();
32124
+ var quotaRefreshSchema = exports_external.discriminatedUnion("kind", [
32125
+ exports_external.object({
32126
+ kind: exports_external.literal("one"),
32127
+ credentialId: exports_external.string().trim().min(1).max(64).regex(/^[A-Za-z0-9_-]+$/, "credentialId must be an account id")
32128
+ }).strict(),
32129
+ exports_external.object({ kind: exports_external.literal("all") }).strict()
32130
+ ]);
31897
32131
  var dimensionSchema = exports_external.enum([
31898
32132
  "credential",
31899
32133
  "model",
@@ -33012,6 +33246,223 @@ async function setKeyExpiry(store, id, input2, now = Date.now()) {
33012
33246
  async function revokeKey(store, id) {
33013
33247
  await store.keys.revoke(id);
33014
33248
  }
33249
+ // packages/control/src/principal.ts
33250
+ var ALL = { kind: "all" };
33251
+ function scopeKey(scope) {
33252
+ return scope.kind === "key" ? scope.apiKeyId : undefined;
33253
+ }
33254
+ function readsNothing(scope) {
33255
+ return scope.kind === "none" || scope.kind === "key" && scope.apiKeyId === "";
33256
+ }
33257
+
33258
+ // packages/control/src/usage.ts
33259
+ var MAX_LOG_LIMIT = 500;
33260
+ var DEFAULT_LOG_LIMIT = 100;
33261
+ async function queryUsage(deps, input2, scope = ALL) {
33262
+ const grain = parseOrThrow(grainSchema, input2.grain ?? "raw");
33263
+ const groupBy = requireDimension(grain, parseOrThrow(dimensionSchema, input2.groupBy ?? "model"));
33264
+ const splitBy = input2.splitBy === undefined ? undefined : requireDimension(grain, parseOrThrow(dimensionSchema, input2.splitBy));
33265
+ if (scope.kind !== "all") {
33266
+ for (const dimension of [groupBy, splitBy]) {
33267
+ if (dimension === "credential") {
33268
+ throw new GatewayError("BAD_REQUEST", "this dimension is not available on this surface");
33269
+ }
33270
+ }
33271
+ }
33272
+ if (readsNothing(scope))
33273
+ return [];
33274
+ const apiKeyId = scopeKey(scope);
33275
+ return deps.store.usage.aggregate({
33276
+ grain,
33277
+ groupBy,
33278
+ ...splitBy === undefined ? {} : { splitBy },
33279
+ ...apiKeyId === undefined ? {} : { apiKeyId },
33280
+ since: optionalNumber(input2.since, 0),
33281
+ until: optionalNumber(input2.until, deps.now())
33282
+ });
33283
+ }
33284
+ function logLimit(requested) {
33285
+ const value = optionalNumber(requested, DEFAULT_LOG_LIMIT);
33286
+ return Math.floor(Math.min(Math.max(1, value), MAX_LOG_LIMIT));
33287
+ }
33288
+
33289
+ // packages/control/src/logs.ts
33290
+ var CURSOR_VERSION = 1;
33291
+ var MAX_ID_LENGTH = 128;
33292
+ var MAX_FILTER_LENGTH = 256;
33293
+ var idSchema = exports_external.string().min(1).max(MAX_ID_LENGTH);
33294
+ var textSchema = exports_external.string().min(1).max(MAX_FILTER_LENGTH);
33295
+ var instantSchema = exports_external.union([
33296
+ exports_external.number(),
33297
+ exports_external.string().regex(/^-?\d+$/, "must be epoch milliseconds").transform(Number)
33298
+ ]).pipe(exports_external.number().int());
33299
+ var logFilterSchema = exports_external.object({
33300
+ cursor: exports_external.string().max(512).optional(),
33301
+ limit: exports_external.union([exports_external.string(), exports_external.number()]).optional(),
33302
+ since: instantSchema.optional(),
33303
+ until: instantSchema.optional(),
33304
+ state: exports_external.enum(["pending", "done"]).optional(),
33305
+ failed: exports_external.enum(["true", "false"]).optional(),
33306
+ provider: providerIdSchema.optional(),
33307
+ requestedModel: textSchema.optional(),
33308
+ resolvedModel: textSchema.optional(),
33309
+ model: textSchema.optional(),
33310
+ credentialId: idSchema.optional(),
33311
+ apiKeyId: idSchema.optional(),
33312
+ errorCode: textSchema.optional()
33313
+ }).strict();
33314
+ function present(input2) {
33315
+ const out = {};
33316
+ for (const [key, value] of Object.entries(input2)) {
33317
+ if (value === undefined || value === "")
33318
+ continue;
33319
+ out[key] = typeof value === "boolean" ? String(value) : value;
33320
+ }
33321
+ return out;
33322
+ }
33323
+ function encodeLogCursor(cursor) {
33324
+ const json10 = JSON.stringify({ v: CURSOR_VERSION, at: cursor.at, id: cursor.id });
33325
+ return Buffer.from(json10, "utf8").toString("base64url");
33326
+ }
33327
+ function decodeLogCursor(raw) {
33328
+ const reject = () => {
33329
+ throw new GatewayError("BAD_REQUEST", "cursor: malformed");
33330
+ };
33331
+ let parsed2;
33332
+ try {
33333
+ parsed2 = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
33334
+ } catch {
33335
+ return reject();
33336
+ }
33337
+ if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2))
33338
+ return reject();
33339
+ const record2 = parsed2;
33340
+ const keys = Object.keys(record2);
33341
+ if (keys.length !== 3)
33342
+ return reject();
33343
+ if (record2.v !== CURSOR_VERSION) {
33344
+ throw new GatewayError("BAD_REQUEST", "cursor: unknown version");
33345
+ }
33346
+ const { at, id } = record2;
33347
+ if (typeof at !== "number" || !Number.isSafeInteger(at))
33348
+ return reject();
33349
+ if (typeof id !== "string" || id.length === 0 || id.length > MAX_ID_LENGTH)
33350
+ return reject();
33351
+ const cursor = { at, id };
33352
+ if (encodeLogCursor(cursor) !== raw)
33353
+ return reject();
33354
+ return cursor;
33355
+ }
33356
+ function toQuery(input2, scope) {
33357
+ const filters = parseOrThrow(logFilterSchema, present(input2));
33358
+ const { since, until } = filters;
33359
+ if (since !== undefined && until !== undefined && since > until) {
33360
+ throw new GatewayError("BAD_REQUEST", "since: must not be after until");
33361
+ }
33362
+ const scoped = scopeKey(scope);
33363
+ return {
33364
+ limit: logLimit(filters.limit),
33365
+ cursor: filters.cursor === undefined ? null : decodeLogCursor(filters.cursor),
33366
+ ...filters.apiKeyId === undefined ? {} : { apiKeyId: filters.apiKeyId },
33367
+ ...scoped === undefined ? {} : { apiKeyId: scoped },
33368
+ ...filters.credentialId === undefined ? {} : { credentialId: filters.credentialId },
33369
+ ...filters.provider === undefined ? {} : { provider: filters.provider },
33370
+ ...filters.requestedModel === undefined ? {} : { requestedModel: filters.requestedModel },
33371
+ ...filters.resolvedModel === undefined ? {} : { resolvedModel: filters.resolvedModel },
33372
+ ...filters.model === undefined ? {} : { model: filters.model },
33373
+ ...filters.errorCode === undefined ? {} : { errorCode: filters.errorCode },
33374
+ ...filters.state === undefined ? {} : { state: filters.state },
33375
+ ...filters.failed === "true" ? { failed: true } : {},
33376
+ ...since === undefined ? {} : { since },
33377
+ ...until === undefined ? {} : { until }
33378
+ };
33379
+ }
33380
+ async function pageLogs(store, input2 = {}, scope = ALL) {
33381
+ const query = toQuery(input2, scope);
33382
+ if (readsNothing(scope))
33383
+ return { logs: [], nextCursor: null };
33384
+ const page = await store.usage.page(query);
33385
+ return {
33386
+ logs: page.logs,
33387
+ nextCursor: page.next === null ? null : encodeLogCursor(page.next)
33388
+ };
33389
+ }
33390
+ var EXPORT_BATCH = 500;
33391
+ var EXPORT_COLUMNS = [
33392
+ "id",
33393
+ "state",
33394
+ "at",
33395
+ "apiKeyId",
33396
+ "requestedModel",
33397
+ "resolvedProvider",
33398
+ "resolvedModel",
33399
+ "credentialId",
33400
+ "attempts",
33401
+ "status",
33402
+ "errorCode",
33403
+ "inputTokens",
33404
+ "outputTokens",
33405
+ "cacheReadTokens",
33406
+ "cacheWriteTokens",
33407
+ "ttftMs",
33408
+ "durationMs",
33409
+ "costUsd",
33410
+ "degradations",
33411
+ "rtkApplied",
33412
+ "rtkFilterHits",
33413
+ "rtkOriginalCodeUnits",
33414
+ "rtkCompressedCodeUnits",
33415
+ "rtkEstimatedTokensSaved",
33416
+ "rtkFilters"
33417
+ ];
33418
+ function csvCell(value) {
33419
+ if (value === null || value === undefined)
33420
+ return "";
33421
+ const text = Array.isArray(value) ? JSON.stringify(value) : String(value);
33422
+ const first = text.trimStart().charAt(0);
33423
+ const guarded = "=+-@".includes(first) && first !== "" ? `'${text}` : text;
33424
+ return /[",\r\n]/.test(guarded) ? `"${guarded.replaceAll('"', '""')}"` : guarded;
33425
+ }
33426
+ var csvRow = (cells) => `${cells.map(csvCell).join(",")}\r
33427
+ `;
33428
+ function exportHeader(format) {
33429
+ return format === "csv" ? csvRow(EXPORT_COLUMNS) : "";
33430
+ }
33431
+ function exportRow(format, log) {
33432
+ if (format === "jsonl") {
33433
+ const ordered = {};
33434
+ for (const column of EXPORT_COLUMNS)
33435
+ ordered[column] = log[column];
33436
+ return `${JSON.stringify(ordered)}
33437
+ `;
33438
+ }
33439
+ return csvRow(EXPORT_COLUMNS.map((column) => log[column]));
33440
+ }
33441
+ var formatSchema = exports_external.enum(["csv", "jsonl"]);
33442
+ async function* exportLogs(store, input2, scope = ALL) {
33443
+ const { format: requested, ...filters } = input2;
33444
+ const format = parseOrThrow(formatSchema, requested ?? "csv");
33445
+ if (input2.since === undefined || input2.since === "") {
33446
+ throw new GatewayError("BAD_REQUEST", "since: required for export");
33447
+ }
33448
+ if (input2.until === undefined || input2.until === "") {
33449
+ throw new GatewayError("BAD_REQUEST", "until: required for export");
33450
+ }
33451
+ const base = toQuery({ ...filters, limit: EXPORT_BATCH, cursor: undefined }, scope);
33452
+ if (readsNothing(scope)) {
33453
+ yield exportHeader(format);
33454
+ return;
33455
+ }
33456
+ let page = await store.usage.page(base);
33457
+ yield exportHeader(format);
33458
+ for (;; ) {
33459
+ for (const log of page.logs)
33460
+ yield exportRow(format, log);
33461
+ if (page.next === null)
33462
+ return;
33463
+ page = await store.usage.page({ ...base, cursor: page.next });
33464
+ }
33465
+ }
33015
33466
  // packages/control/src/modelLimits.ts
33016
33467
  function narrower(a, b) {
33017
33468
  const context = [a.contextWindow, b.contextWindow].filter((n) => n !== undefined);
@@ -33170,7 +33621,7 @@ var CAPABILITIES = [
33170
33621
  "provider"
33171
33622
  ];
33172
33623
  var ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
33173
- var idSchema = exports_external.string().regex(ID_PATTERN, "id must match /^[a-z][a-z0-9-]{0,31}$/");
33624
+ var idSchema2 = exports_external.string().regex(ID_PATTERN, "id must match /^[a-z][a-z0-9-]{0,31}$/");
33174
33625
  var entrySchema = exports_external.string().min(1).refine((p) => !p.startsWith("/") && !p.split("/").includes(".."), {
33175
33626
  message: "entry must be a relative path with no '..' segment"
33176
33627
  });
@@ -33194,7 +33645,7 @@ var navSchema = exports_external.object({
33194
33645
  }).strict();
33195
33646
  var ORIGIN_CAPABILITIES = ["net:outbound", "provider"];
33196
33647
  var manifestSchema = exports_external.object({
33197
- id: idSchema,
33648
+ id: idSchema2,
33198
33649
  name: exports_external.string().min(1).max(64),
33199
33650
  version: exports_external.string().min(1).max(32),
33200
33651
  api: exports_external.number().int().nonnegative(),
@@ -33762,20 +34213,20 @@ async function removePlugin(deps, root, id, options = {}) {
33762
34213
  throw new GatewayError("BAD_REQUEST", `"${id}" is not a valid plugin id`);
33763
34214
  }
33764
34215
  const target = join3(pluginsDir(root), id);
33765
- const present = deps.fs.isDirectory(target);
34216
+ const present2 = deps.fs.isDirectory(target);
33766
34217
  const purge = options.purge === true;
33767
34218
  if (purge && deps.store === undefined) {
33768
34219
  throw new GatewayError("INTERNAL", "purging a plugin's data needs a database");
33769
34220
  }
33770
34221
  const tables = purge && deps.store !== undefined ? await deps.store.plugins.listTables(id) : [];
33771
- if (!present && tables.length === 0) {
34222
+ if (!present2 && tables.length === 0) {
33772
34223
  throw new GatewayError("BAD_REQUEST", `no plugin "${id}" installed under ${pluginsDir(root)}`);
33773
34224
  }
33774
- if (present)
34225
+ if (present2)
33775
34226
  deps.fs.rm(target);
33776
34227
  if (purge && deps.store !== undefined)
33777
34228
  await deps.store.plugins.dropAll(id);
33778
- return { id, removed: present, droppedTables: tables };
34229
+ return { id, removed: present2, droppedTables: tables };
33779
34230
  }
33780
34231
  function orphanPluginTables(deps, root, store) {
33781
34232
  const installed = listPlugins(deps, root).map((plugin) => plugin.id);
@@ -34147,6 +34598,8 @@ function registerOAuthProvider(id, provider, registry2 = OAUTH_PROVIDERS) {
34147
34598
  }
34148
34599
  registry2[id] = provider;
34149
34600
  }
34601
+ // packages/control/src/oauth/lead.ts
34602
+ var SCHEDULER_REFRESH_LEAD_MS = 300000;
34150
34603
  // packages/control/src/oauth/refresh.ts
34151
34604
  var REFRESH_LOCK_MS = 30000;
34152
34605
  function createRefresher(deps) {
@@ -34468,14 +34921,6 @@ async function readPluginProviders(plugins, importer, timeoutMs = PLUGIN_IMPORT_
34468
34921
  }
34469
34922
  return { descriptors: descriptors2, oauth, failures };
34470
34923
  }
34471
- // packages/control/src/principal.ts
34472
- var ALL = { kind: "all" };
34473
- function scopeKey(scope) {
34474
- return scope.kind === "key" ? scope.apiKeyId : undefined;
34475
- }
34476
- function readsNothing(scope) {
34477
- return scope.kind === "none" || scope.kind === "key" && scope.apiKeyId === "";
34478
- }
34479
34924
  // packages/control/src/quota/history.ts
34480
34925
  var DAY_MS3 = 24 * 60 * 60 * 1000;
34481
34926
  var HOUR_MS3 = 3600000;
@@ -34543,6 +34988,186 @@ async function quotaHistory(deps, input2) {
34543
34988
 
34544
34989
  // packages/control/src/quota/clientHistory.ts
34545
34990
  var MAX_SPAN_MS = 16 * 24 * 60 * 60 * 1000;
34991
+ // packages/control/src/quota/poll.ts
34992
+ var CONCURRENCY = 4;
34993
+ var RATE_LIMIT_COOLDOWN_MS = 180000;
34994
+ var COOLDOWN_PREFIX = "quota:cooldown:";
34995
+ var PROBE_LOCK_PREFIX = "quota:probe:";
34996
+ var PROBE_LOCK_TTL_MS = 60000;
34997
+ var PROBE_LOCK_WAIT_MS = 30000;
34998
+ async function probe(deps, credential) {
34999
+ if (credential.authType !== "oauth")
35000
+ return null;
35001
+ const provider = deps.providers[credential.provider];
35002
+ if (provider?.usage === undefined)
35003
+ return null;
35004
+ const refreshed = credential.hasRefreshToken && credential.expiresAt !== null && credential.expiresAt - SCHEDULER_REFRESH_LEAD_MS <= deps.now() ? await deps.refresh(credential) : null;
35005
+ const secrets = refreshed === null ? await credential.openForUsage() : { accessToken: refreshed.accessToken };
35006
+ const report = await provider.usage(secrets, { http: deps.http, now: deps.now }, credential.providerData);
35007
+ if (report === null || report.windows.length === 0)
35008
+ return null;
35009
+ const observedAt = deps.now();
35010
+ const rows = report.windows.map((w) => ({
35011
+ credentialId: credential.id,
35012
+ windowType: w.windowType,
35013
+ startsAt: observedAt,
35014
+ used: w.used,
35015
+ limit: w.limit,
35016
+ resetsAt: w.resetsAt,
35017
+ observedAt,
35018
+ windowMs: w.windowMs
35019
+ }));
35020
+ await deps.store.credentials.saveQuota(rows);
35021
+ (deps.logger ?? noopLogger).debug("quota snapshot written", {
35022
+ provider: credential.provider,
35023
+ credentialId: credential.id,
35024
+ count: rows.length
35025
+ });
35026
+ return rows;
35027
+ }
35028
+ async function inOrder(items, job) {
35029
+ const results = new Array(items.length).fill(undefined);
35030
+ let next = 0;
35031
+ const worker = async () => {
35032
+ while (next < items.length) {
35033
+ const index = next++;
35034
+ const item = items[index];
35035
+ if (item === undefined)
35036
+ return;
35037
+ results[index] = await job(item);
35038
+ }
35039
+ };
35040
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, items.length) }, () => worker()));
35041
+ return results;
35042
+ }
35043
+ function quotaOps(deps) {
35044
+ const logger2 = deps.logger ?? noopLogger;
35045
+ const inFlight = new Map;
35046
+ async function coordinated(credential, fallback, fn) {
35047
+ try {
35048
+ return await fn();
35049
+ } catch (error62) {
35050
+ logger2.warn("quota probe coordination unavailable", {
35051
+ provider: credential.provider,
35052
+ credentialId: credential.id,
35053
+ coordFallback: true,
35054
+ reason: describeError(error62, "unknown")
35055
+ });
35056
+ return fallback;
35057
+ }
35058
+ }
35059
+ function reportFailure(credential, error62) {
35060
+ const rateLimited = error62 instanceof GatewayError && error62.code === "RATE_LIMIT";
35061
+ logger2.warn(rateLimited ? "quota probe rate limited" : "quota probe failed", {
35062
+ provider: credential.provider,
35063
+ credentialId: credential.id,
35064
+ code: error62 instanceof GatewayError ? error62.code : "INTERNAL",
35065
+ ...rateLimited ? { retryAfterMs: RATE_LIMIT_COOLDOWN_MS } : {},
35066
+ reason: describeError(error62, "unknown")
35067
+ });
35068
+ return error62 instanceof GatewayError ? error62.code : "INTERNAL";
35069
+ }
35070
+ async function runProbe(credential) {
35071
+ const credentialId = credential.id;
35072
+ try {
35073
+ const rows = await probe(deps, credential);
35074
+ if (rows === null)
35075
+ return { outcome: { kind: "noData", credentialId }, wrote: false };
35076
+ return { outcome: { kind: "refreshed", credentialId, windows: rows.length }, wrote: true };
35077
+ } catch (error62) {
35078
+ if (error62 instanceof GatewayError && error62.code === "RATE_LIMIT") {
35079
+ await coordinated(credential, undefined, () => deps.coord.kv.set(COOLDOWN_PREFIX + credentialId, "1", RATE_LIMIT_COOLDOWN_MS));
35080
+ }
35081
+ return {
35082
+ outcome: { kind: "failed", credentialId, code: reportFailure(credential, error62) },
35083
+ wrote: false
35084
+ };
35085
+ }
35086
+ }
35087
+ async function runGuarded(credential, startedAt) {
35088
+ const credentialId = credential.id;
35089
+ let entered = false;
35090
+ const body2 = async () => {
35091
+ entered = true;
35092
+ const rows = (await deps.store.credentials.listQuota()).filter((row) => row.credentialId === credentialId);
35093
+ if (rows.some((row) => row.observedAt >= startedAt)) {
35094
+ return { outcome: { kind: "coalesced", credentialId, windows: rows.length }, wrote: false };
35095
+ }
35096
+ return runProbe(credential);
35097
+ };
35098
+ try {
35099
+ return await deps.coord.mutex.withLock(PROBE_LOCK_PREFIX + credentialId, PROBE_LOCK_TTL_MS, PROBE_LOCK_WAIT_MS, body2);
35100
+ } catch (error62) {
35101
+ if (entered)
35102
+ return { outcome: { kind: "failed", credentialId, code: "INTERNAL" }, wrote: false };
35103
+ logger2.warn("quota probe lock unavailable", {
35104
+ provider: credential.provider,
35105
+ credentialId,
35106
+ coordFallback: true,
35107
+ reason: describeError(error62, "unknown")
35108
+ });
35109
+ return runProbe(credential);
35110
+ }
35111
+ }
35112
+ function attempt(credential) {
35113
+ const credentialId = credential.id;
35114
+ if (!credential.enabled) {
35115
+ return Promise.resolve({ outcome: { kind: "disabled", credentialId }, wrote: false });
35116
+ }
35117
+ if (credential.authType !== "oauth" || deps.providers[credential.provider]?.usage === undefined) {
35118
+ return Promise.resolve({ outcome: { kind: "unsupported", credentialId }, wrote: false });
35119
+ }
35120
+ const running = inFlight.get(credentialId);
35121
+ if (running !== undefined) {
35122
+ return running.then((shared) => shared.outcome.kind === "refreshed" ? {
35123
+ outcome: {
35124
+ kind: "coalesced",
35125
+ credentialId,
35126
+ windows: shared.outcome.windows
35127
+ },
35128
+ wrote: false
35129
+ } : { outcome: shared.outcome, wrote: false });
35130
+ }
35131
+ const startedAt = deps.now();
35132
+ const started = (async () => {
35133
+ const cooling = await coordinated(credential, null, () => deps.coord.kv.get(COOLDOWN_PREFIX + credentialId));
35134
+ if (cooling !== null) {
35135
+ return { outcome: { kind: "cooldown", credentialId }, wrote: false };
35136
+ }
35137
+ return runGuarded(credential, startedAt);
35138
+ })();
35139
+ inFlight.set(credentialId, started);
35140
+ return started.finally(() => {
35141
+ inFlight.delete(credentialId);
35142
+ });
35143
+ }
35144
+ return {
35145
+ async poll() {
35146
+ const credentials = await deps.store.credentials.list();
35147
+ const attempts = await inOrder(credentials, attempt);
35148
+ return attempts.filter((a) => a?.wrote === true).length;
35149
+ },
35150
+ async refresh(request2) {
35151
+ if (request2.kind === "one") {
35152
+ const credentialId = request2.credentialId.trim();
35153
+ if (credentialId === "")
35154
+ throw new GatewayError("BAD_REQUEST", "no such credential");
35155
+ const credential = await deps.store.credentials.get(credentialId);
35156
+ if (credential === null)
35157
+ throw new GatewayError("BAD_REQUEST", "no such credential");
35158
+ return { outcomes: [(await attempt(credential)).outcome] };
35159
+ }
35160
+ const credentials = await deps.store.credentials.list();
35161
+ const attempts = await inOrder(credentials, attempt);
35162
+ const outcomes = [];
35163
+ for (const [index, credential] of credentials.entries()) {
35164
+ const result = attempts[index];
35165
+ outcomes.push(result?.outcome ?? { kind: "failed", credentialId: credential.id, code: "INTERNAL" });
35166
+ }
35167
+ return { outcomes };
35168
+ }
35169
+ };
35170
+ }
34546
35171
  // packages/control/src/settings.ts
34547
35172
  async function getSettings(store) {
34548
35173
  return store.config.getSettings();
@@ -34711,41 +35336,6 @@ function fileExists(path) {
34711
35336
  return false;
34712
35337
  }
34713
35338
  }
34714
- // packages/control/src/usage.ts
34715
- var MAX_LOG_LIMIT = 500;
34716
- var DEFAULT_LOG_LIMIT = 100;
34717
- async function queryUsage(deps, input2, scope = ALL) {
34718
- const grain = parseOrThrow(grainSchema, input2.grain ?? "raw");
34719
- const groupBy = requireDimension(grain, parseOrThrow(dimensionSchema, input2.groupBy ?? "model"));
34720
- const splitBy = input2.splitBy === undefined ? undefined : requireDimension(grain, parseOrThrow(dimensionSchema, input2.splitBy));
34721
- if (scope.kind !== "all") {
34722
- for (const dimension of [groupBy, splitBy]) {
34723
- if (dimension === "credential") {
34724
- throw new GatewayError("BAD_REQUEST", "this dimension is not available on this surface");
34725
- }
34726
- }
34727
- }
34728
- if (readsNothing(scope))
34729
- return [];
34730
- const apiKeyId = scopeKey(scope);
34731
- return deps.store.usage.aggregate({
34732
- grain,
34733
- groupBy,
34734
- ...splitBy === undefined ? {} : { splitBy },
34735
- ...apiKeyId === undefined ? {} : { apiKeyId },
34736
- since: optionalNumber(input2.since, 0),
34737
- until: optionalNumber(input2.until, deps.now())
34738
- });
34739
- }
34740
- function logLimit(requested) {
34741
- const value = optionalNumber(requested, DEFAULT_LOG_LIMIT);
34742
- return Math.floor(Math.min(Math.max(1, value), MAX_LOG_LIMIT));
34743
- }
34744
- async function recentLogs(store, requested, scope = ALL) {
34745
- if (readsNothing(scope))
34746
- return [];
34747
- return store.usage.recent(logLimit(requested), scopeKey(scope));
34748
- }
34749
35339
  // apps/cli/src/args.ts
34750
35340
  import { parseArgs } from "util";
34751
35341
  class UsageError extends Error {
@@ -36734,6 +37324,66 @@ var quota = {
36734
37324
  });
36735
37325
  }
36736
37326
  };
37327
+ var OUTCOME_NOTE = {
37328
+ refreshed: "refreshed",
37329
+ coalesced: "refreshed by a concurrent probe",
37330
+ noData: "provider reported nothing",
37331
+ cooldown: "cooling down after a rate limit",
37332
+ unsupported: "no usage endpoint",
37333
+ disabled: "disabled",
37334
+ failed: "failed"
37335
+ };
37336
+ function unmet(outcome) {
37337
+ return outcome.kind === "failed" || outcome.kind === "cooldown";
37338
+ }
37339
+ var quotaRefresh = {
37340
+ usage: "quota refresh <id> | --all",
37341
+ summary: "Read provider quota now, for one account or for every account",
37342
+ options: { all: { type: "boolean" } },
37343
+ async run(args, { ctx, writer }) {
37344
+ const all = args.values.all === true;
37345
+ const id = args.positionals[0];
37346
+ if (all === (id !== undefined)) {
37347
+ throw new UsageError("give an account id or --all, not both");
37348
+ }
37349
+ const store = await ctx.store();
37350
+ const credentials = await listCredentials(store);
37351
+ const wanted = id === undefined ? credentials : credentials.filter((c) => c.id === id);
37352
+ const providers = wanted.every((c) => Object.hasOwn(PROVIDER_DESCRIPTORS, c.provider)) ? OAUTH_PROVIDERS : (await connectRegistryFor(ctx.root.root)).providers;
37353
+ const http = nodeHttpClient();
37354
+ const ops = quotaOps({
37355
+ store,
37356
+ coord: memoryCoord({ now: ctx.now }),
37357
+ providers,
37358
+ http,
37359
+ refresh: createRefresher({ store, providers, http, now: ctx.now }),
37360
+ now: ctx.now
37361
+ });
37362
+ note(ctx, writer, id === undefined ? "refreshing every account\u2026" : `refreshing ${id}\u2026`);
37363
+ const result = await ops.refresh(id === undefined ? { kind: "all" } : { kind: "one", credentialId: id });
37364
+ const labels = new Map(credentials.map((c) => [c.id, c.label]));
37365
+ emit(ctx, writer, result, () => {
37366
+ const rows = result.outcomes.map((outcome) => [
37367
+ `${labels.get(outcome.credentialId) ?? outcome.credentialId}`,
37368
+ OUTCOME_NOTE[outcome.kind],
37369
+ outcome.kind === "refreshed" || outcome.kind === "coalesced" ? `${outcome.windows} window${outcome.windows === 1 ? "" : "s"}` : outcome.kind === "failed" ? outcome.code : dash
37370
+ ]);
37371
+ const refreshed = result.outcomes.filter((o) => o.kind === "refreshed" || o.kind === "coalesced").length;
37372
+ const failed = result.outcomes.filter((o) => o.kind === "failed").length;
37373
+ const skipped = result.outcomes.length - refreshed - failed;
37374
+ return [
37375
+ table([{ header: "ACCOUNT" }, { header: "OUTCOME" }, { header: "DETAIL", align: "right" }], rows),
37376
+ "",
37377
+ `refreshed ${refreshed}; failed ${failed}; skipped ${skipped}`
37378
+ ].join(`
37379
+ `);
37380
+ });
37381
+ const bad = result.outcomes.filter(unmet);
37382
+ if (bad.length > 0) {
37383
+ throw new CliError(bad.length === result.outcomes.length ? "quota refresh did not run" : `quota refresh did not run for ${bad.length} of ${result.outcomes.length} accounts`);
37384
+ }
37385
+ }
37386
+ };
36737
37387
 
36738
37388
  // apps/cli/src/commands/service.ts
36739
37389
  import { existsSync as existsSync5 } from "fs";
@@ -36824,7 +37474,7 @@ function createServiceDeps(input2) {
36824
37474
  }
36825
37475
 
36826
37476
  // apps/cli/src/version.ts
36827
- var VERSION = "0.11.4";
37477
+ var VERSION = "0.12.0";
36828
37478
 
36829
37479
  // apps/cli/src/commands/service.ts
36830
37480
  function gatewayArgv(root) {
@@ -37384,12 +38034,44 @@ var usage = {
37384
38034
  });
37385
38035
  }
37386
38036
  };
38037
+ var LOG_FILTER_OPTIONS = {
38038
+ since: { type: "string" },
38039
+ until: { type: "string" },
38040
+ state: { type: "string" },
38041
+ failed: { type: "boolean" },
38042
+ provider: { type: "string" },
38043
+ model: { type: "string" },
38044
+ "requested-model": { type: "string" },
38045
+ "resolved-model": { type: "string" },
38046
+ account: { type: "string" },
38047
+ key: { type: "string" },
38048
+ "error-code": { type: "string" }
38049
+ };
38050
+ function logFilters(values3) {
38051
+ const since = stringFlag(values3, "since");
38052
+ const until = stringFlag(values3, "until");
38053
+ return {
38054
+ ...since === undefined ? {} : { since: instant(since, 0) },
38055
+ ...until === undefined ? {} : { until: instant(until, 0) },
38056
+ state: stringFlag(values3, "state"),
38057
+ ...boolFlag(values3, "failed") ? { failed: "true" } : {},
38058
+ provider: stringFlag(values3, "provider"),
38059
+ model: stringFlag(values3, "model"),
38060
+ requestedModel: stringFlag(values3, "requested-model"),
38061
+ resolvedModel: stringFlag(values3, "resolved-model"),
38062
+ credentialId: stringFlag(values3, "account"),
38063
+ apiKeyId: stringFlag(values3, "key"),
38064
+ errorCode: stringFlag(values3, "error-code")
38065
+ };
38066
+ }
37387
38067
  var logs = {
37388
- usage: "logs [-n N] [--follow] [--service]",
38068
+ usage: "logs [-n N] [--follow] [--cursor C] [filters] [--service]",
37389
38069
  summary: "Show recent requests (--service is an alias for `omni console`)",
37390
38070
  options: {
38071
+ ...LOG_FILTER_OPTIONS,
37391
38072
  number: { type: "string", short: "n" },
37392
38073
  follow: { type: "boolean" },
38074
+ cursor: { type: "string" },
37393
38075
  service: { type: "boolean" },
37394
38076
  system: { type: "boolean" }
37395
38077
  },
@@ -37401,6 +38083,7 @@ var logs = {
37401
38083
  return;
37402
38084
  }
37403
38085
  const store = await ctx.store();
38086
+ const filters = logFilters(args.values);
37404
38087
  const render2 = (rows) => table([
37405
38088
  { header: "AT" },
37406
38089
  { header: "MODEL" },
@@ -37418,18 +38101,66 @@ var logs = {
37418
38101
  formatUsd(row.costUsd),
37419
38102
  String(row.durationMs)
37420
38103
  ]));
37421
- const first = await recentLogs(store, limit);
37422
- emit(ctx, writer, { logs: first }, () => first.length === 0 ? "no requests logged yet" : render2(first));
38104
+ const page = await pageLogs(store, {
38105
+ ...filters,
38106
+ limit,
38107
+ cursor: stringFlag(args.values, "cursor")
38108
+ });
38109
+ const first = page.logs;
38110
+ emit(ctx, writer, { logs: first, nextCursor: page.nextCursor }, () => first.length === 0 ? "no requests match" : page.nextCursor === null ? render2(first) : `${render2(first)}
38111
+ ${paint(ctx, "dim", `older: --cursor ${page.nextCursor}`)}`);
37423
38112
  if (args.values.follow !== true || ctx.json)
37424
38113
  return;
37425
- let seen = first[0]?.at ?? 0;
38114
+ let seen = first[0] === undefined ? null : cursorOf(first[0]);
37426
38115
  for (;; ) {
37427
38116
  await Bun.sleep(FOLLOW_INTERVAL_MS);
37428
- const next = (await recentLogs(store, limit)).filter((row) => row.at > seen);
37429
- if (next.length === 0)
38117
+ const mark = seen;
38118
+ const { rows, skipped } = await newerThan((cursor) => pageLogs(store, { ...filters, limit, cursor }), mark);
38119
+ if (rows.length === 0)
37430
38120
  continue;
37431
- seen = next[0]?.at ?? seen;
37432
- writer.out(render2(next.reverse()));
38121
+ const newest = rows[0];
38122
+ if (newest !== undefined)
38123
+ seen = cursorOf(newest);
38124
+ if (skipped) {
38125
+ writer.out(paint(ctx, "dim", `more than ${FOLLOW_MAX_PAGES * limit} requests arrived since the last poll; older ones are not shown`));
38126
+ }
38127
+ writer.out(render2(rows.reverse()));
38128
+ }
38129
+ }
38130
+ };
38131
+ var cursorOf = (row) => ({ at: row.at, id: row.id });
38132
+ var FOLLOW_MAX_PAGES = 10;
38133
+ async function newerThan(read, mark) {
38134
+ const rows = [];
38135
+ let cursor;
38136
+ for (let page = 0;page < FOLLOW_MAX_PAGES; page++) {
38137
+ const got = await read(cursor);
38138
+ if (mark === null)
38139
+ return { rows: got.logs, skipped: false };
38140
+ const fresh = got.logs.filter((row) => isAfter(row, mark));
38141
+ rows.push(...fresh);
38142
+ if (fresh.length < got.logs.length || got.nextCursor === null) {
38143
+ return { rows, skipped: false };
38144
+ }
38145
+ cursor = got.nextCursor;
38146
+ }
38147
+ return { rows, skipped: true };
38148
+ }
38149
+ var isAfter = (row, seen) => row.at > seen.at || row.at === seen.at && row.id > seen.id;
38150
+ var logsExport = {
38151
+ usage: "logs export --since T --until T [--format csv|jsonl] [filters]",
38152
+ summary: "Write matching request metadata to stdout as CSV or JSONL",
38153
+ options: {
38154
+ ...LOG_FILTER_OPTIONS,
38155
+ format: { type: "string" }
38156
+ },
38157
+ async run(args, { ctx, writer }) {
38158
+ const store = await ctx.store();
38159
+ for await (const chunk of exportLogs(store, {
38160
+ ...logFilters(args.values),
38161
+ format: stringFlag(args.values, "format") ?? "csv"
38162
+ })) {
38163
+ writer.raw(chunk);
37433
38164
  }
37434
38165
  }
37435
38166
  };
@@ -37447,6 +38178,7 @@ var COMMANDS = {
37447
38178
  usage,
37448
38179
  quota,
37449
38180
  connect,
38181
+ "logs export": logsExport,
37450
38182
  "service install": serviceInstall,
37451
38183
  "service uninstall": serviceUninstall,
37452
38184
  "credentials list": credentialsList,
@@ -37458,6 +38190,7 @@ var COMMANDS = {
37458
38190
  "credentials refresh": credentialsRefresh,
37459
38191
  "credentials add-key": credentialsAddKey,
37460
38192
  "credentials health": credentialsHealth,
38193
+ "quota refresh": quotaRefresh,
37461
38194
  "models list": modelsList,
37462
38195
  "models show": modelsShow,
37463
38196
  "models put": modelsPut,