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.
- package/README.md +3 -1
- package/bin/omni.js +793 -60
- package/gateway.js +670 -91
- package/package.json +1 -1
- package/public/assets/{Chip-roGX6zyg.js → Chip-D4qvhu1D.js} +1 -1
- package/public/assets/{CopyValue-FO5yNkRC.js → CopyValue-SnsT2LML.js} +1 -1
- package/public/assets/Lamp-DkvXKB-B.js +26 -0
- package/public/assets/Rack-CEuCdVXc.js +483 -0
- package/public/assets/RequestTable-CSQGt9W_.js +93 -0
- package/public/assets/{SummaryDeck-DwRGp7mB.js → SummaryDeck-7ivuca39.js} +1 -1
- package/public/assets/Toggle-t82IhqEt.js +39 -0
- package/public/assets/{TokenBreakdown-DT7or6C9.js → TokenBreakdown-D1Wtza1y.js} +1 -1
- package/public/assets/{WindowChart-C-oJo3fW.js → WindowChart-BeGPsccS.js} +1 -1
- package/public/assets/_app.accounts-W1USUo2Q.js +70 -0
- package/public/assets/{_app.console-DbM7Zbgu.js → _app.console-BvIGqIe7.js} +2 -2
- package/public/assets/{_app.database-Bj4UJUBZ.js → _app.database-CtFJLcfE.js} +4 -4
- package/public/assets/_app.index-DigYfob3.js +67 -0
- package/public/assets/{_app.keys-BYk7HsiW.js → _app.keys-B1fRMyU-.js} +6 -6
- package/public/assets/_app.logs-D73Qcx9S.js +60 -0
- package/public/assets/{_app.models-BqdGSdg4.js → _app.models-Dpl1O2RE.js} +9 -9
- package/public/assets/{_app.plugins._pluginId-Bf9D8Qiz.js → _app.plugins._pluginId-BNrRTn9l.js} +1 -1
- package/public/assets/{_app.settings-TzCc7Sd3.js → _app.settings-tRLrVJoq.js} +4 -4
- package/public/assets/_app.usage-D2qlcEp4.js +76 -0
- package/public/assets/chevron-down-Bs0tSOyA.js +1 -0
- package/public/assets/chevron-right-DSaI1kFo.js +1 -0
- package/public/assets/client-BZ_ev12J.js +39 -0
- package/public/assets/dist-D03q-60i.js +1 -0
- package/public/assets/index-89mqAVmK.js +14 -0
- package/public/assets/{login-Dgd5jBoq.js → login-75MYwRdC.js} +2 -2
- package/public/assets/plus-BCfjC6nk.js +1 -0
- package/public/assets/{preload-helper-CvG2RSKd.js → preload-helper-BPaHpbO_.js} +1 -1
- package/public/assets/{shared-wEQ9LHZa.js → shared-Dtkw6SE0.js} +1 -1
- package/public/assets/{trash-IE0YvGCJ.js → trash-D4QWzEK_.js} +1 -1
- package/public/index.html +4 -4
- package/public/assets/Lamp-DsUSnxN4.js +0 -26
- package/public/assets/Rack-CSpeYQ1D.js +0 -483
- package/public/assets/RequestTable-xpG3dPST.js +0 -11
- package/public/assets/Toggle-BTJVC9Se.js +0 -39
- package/public/assets/_app.accounts-D8eynNaM.js +0 -59
- package/public/assets/_app.index-2hdPR1Le.js +0 -67
- package/public/assets/_app.logs-sG1w9y1i.js +0 -58
- package/public/assets/_app.usage-XQHmzx-j.js +0 -76
- package/public/assets/chevron-right-DgdKbG-g.js +0 -1
- package/public/assets/client-B_g7bcxd.js +0 -39
- package/public/assets/index-C_8ChaGY.js +0 -14
- 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
|
|
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:
|
|
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
|
|
39630
|
-
const
|
|
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 <
|
|
39639
|
-
const
|
|
39640
|
-
|
|
40084
|
+
while (next < items.length) {
|
|
40085
|
+
const index = next++;
|
|
40086
|
+
const item = items[index];
|
|
40087
|
+
if (item === undefined)
|
|
39641
40088
|
return;
|
|
39642
|
-
|
|
39643
|
-
|
|
39644
|
-
|
|
39645
|
-
|
|
39646
|
-
|
|
39647
|
-
|
|
39648
|
-
|
|
39649
|
-
|
|
39650
|
-
|
|
39651
|
-
|
|
39652
|
-
|
|
39653
|
-
|
|
39654
|
-
|
|
39655
|
-
|
|
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
|
|
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
|
|
56158
|
-
|
|
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.
|
|
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(
|
|
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
|