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/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
|
|
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:
|
|
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
|
|
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 (!
|
|
34222
|
+
if (!present2 && tables.length === 0) {
|
|
33772
34223
|
throw new GatewayError("BAD_REQUEST", `no plugin "${id}" installed under ${pluginsDir(root)}`);
|
|
33773
34224
|
}
|
|
33774
|
-
if (
|
|
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:
|
|
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.
|
|
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
|
|
37422
|
-
|
|
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]
|
|
38114
|
+
let seen = first[0] === undefined ? null : cursorOf(first[0]);
|
|
37426
38115
|
for (;; ) {
|
|
37427
38116
|
await Bun.sleep(FOLLOW_INTERVAL_MS);
|
|
37428
|
-
const
|
|
37429
|
-
|
|
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
|
-
|
|
37432
|
-
|
|
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,
|