@krtclcdy/tersio 2.25.4 → 2.25.6
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 +27 -63
- package/dashboard/dist/index.html +18 -16
- package/dist/cli/backups.js +187 -0
- package/dist/cli/banner.js +4 -3
- package/dist/cli/common.js +29 -13
- package/dist/cli/currency.js +1 -1
- package/dist/cli/dashboard.js +168 -110
- package/dist/cli/doctor-fix.js +232 -50
- package/dist/cli/doctor.js +281 -32
- package/dist/cli/hosts.js +29 -9
- package/dist/cli/install.js +204 -69
- package/dist/cli/interactive.js +3 -3
- package/dist/cli/manifest.js +125 -17
- package/dist/cli/profile.js +14 -9
- package/dist/cli/rtk-wiring.js +1 -1
- package/dist/cli/settings.js +20 -5
- package/dist/cli/uninstall.js +24 -8
- package/dist/cli/update.js +9 -10
- package/dist/cli/usage.js +75 -7
- package/dist/extensions/{caveman-session → caveman}/index.js +4 -2
- package/{extensions/caveman-session → dist/extensions/caveman}/rule-megacave.md +5 -3
- package/{extensions/caveman-session → dist/extensions/caveman}/rule-ultra.md +4 -4
- package/dist/extensions/caveman/rule.md +89 -0
- package/dist/extensions/{combo-toggle → combo}/index.js +62 -21
- package/dist/extensions/{tersio-commands → commands}/index.js +2 -2
- package/dist/extensions/pstack/index.js +119 -0
- package/dist/extensions/pstack/rule.md +65 -0
- package/dist/extensions/{rtk-filter/index.js → rtk/filter.js} +1 -2
- package/dist/extensions/{rtk-session/index.js → rtk/session.js} +5 -5
- package/dist/extensions/{opencode/server.js → server.js} +98 -14
- package/dist/extensions/shared/agent-stats.js +1198 -0
- package/dist/extensions/shared/brand-marks.js +153 -0
- package/dist/extensions/shared/carbon.js +1 -1
- package/dist/extensions/shared/plugin-settings.js +12 -7
- package/dist/extensions/shared/pricing.js +86 -30
- package/dist/extensions/shared/rtk-gain.js +4 -9
- package/dist/extensions/shared/session-state.js +15 -10
- package/dist/extensions/shared/sqlite.js +168 -0
- package/dist/extensions/shared/status.js +8 -8
- package/dist/extensions/shared/types.js +1 -1
- package/dist/extensions/shared/usage-ledger.js +113 -39
- package/dist/extensions/shared/usage-store.js +191 -48
- package/dist/extensions/{lib → shared}/utils.js +6 -6
- package/dist/extensions/timing/index.js +31 -0
- package/dist/extensions/{ai-addons-updater → updater}/index.js +13 -6
- package/dist/probe-catalog.js +48 -0
- package/dist/probe-catalog188.js +44 -0
- package/dist/tersio.js +9 -4
- package/extensions/{caveman-session → caveman}/index.ts +4 -2
- package/{dist/extensions/caveman-session → extensions/caveman}/rule-megacave.md +5 -3
- package/{dist/extensions/caveman-session → extensions/caveman}/rule-ultra.md +4 -4
- package/extensions/caveman/rule.md +89 -0
- package/extensions/{combo-toggle → combo}/index.ts +62 -19
- package/extensions/{tersio-commands → commands}/index.ts +2 -2
- package/extensions/pstack/index.ts +128 -0
- package/extensions/pstack/rule.md +65 -0
- package/extensions/{rtk-filter/index.ts → rtk/filter.ts} +1 -2
- package/extensions/{rtk-session/index.ts → rtk/session.ts} +5 -5
- package/extensions/{opencode/server.ts → server.ts} +81 -16
- package/extensions/shared/agent-stats.ts +1605 -0
- package/extensions/shared/brand-marks.ts +163 -0
- package/extensions/shared/carbon.ts +1 -1
- package/extensions/shared/plugin-settings.ts +13 -7
- package/extensions/shared/pricing.ts +95 -32
- package/extensions/shared/rtk-gain.ts +4 -9
- package/extensions/shared/session-state.ts +18 -13
- package/extensions/shared/sqlite.ts +168 -0
- package/extensions/shared/status.ts +9 -8
- package/extensions/shared/types.ts +2 -1
- package/extensions/shared/usage-ledger.ts +134 -46
- package/extensions/shared/usage-store.ts +202 -50
- package/extensions/{lib → shared}/utils.ts +6 -6
- package/extensions/timing/index.ts +45 -0
- package/extensions/{ai-addons-updater → updater}/index.ts +15 -9
- package/package.json +27 -18
- package/dist/extensions/caveman-session/rule.md +0 -79
- package/extensions/caveman-session/rule.md +0 -79
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
//
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
1
|
+
// usage-store.ts — tersio-owned usage.db. A cache of the live parse, not a fork; no SQLite → sync false / read null.
|
|
3
2
|
import fs from 'node:fs';
|
|
4
3
|
import path from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { sqliteExec, sqliteRead } from './sqlite.js';
|
|
5
|
+
import { RECENT_LIMIT, canonicalModelId, classifySessionLine, classifyOpencodeMessage, costOf, durOf, agentOfSessionText, hostOfSessionFile, ingestSessionRow, messageRowId, OPENCODE_DB_OVERLAP_MS, opencodeDbPath, opencodeSessionsDir, newSessionAccum, opencodeToolNames, readOpencodeDbRows, sessionsDirs, walkJsonl, } from './usage-ledger.js';
|
|
6
|
+
import { tersioDataPath } from './utils.js';
|
|
7
7
|
import { MODEL_ALIASES } from './pricing.js';
|
|
8
8
|
import { tersioSettingsFile } from './plugin-settings.js';
|
|
9
9
|
export function usageDbPath() {
|
|
@@ -24,25 +24,21 @@ function nullNum(v) {
|
|
|
24
24
|
function nullStr(v) {
|
|
25
25
|
return v === undefined ? 'NULL' : esc(v);
|
|
26
26
|
}
|
|
27
|
+
// A failed INSERT stops before COMMIT, so it leaves no DELETEs behind.
|
|
27
28
|
function run(db, sql) {
|
|
28
|
-
|
|
29
|
-
execFileSync('sqlite3', ['-bail', db, sql], { stdio: ['ignore', 'ignore', 'ignore'], timeout: 30000 });
|
|
29
|
+
sqliteExec(db, sql);
|
|
30
30
|
}
|
|
31
31
|
function query(db, sql) {
|
|
32
|
-
|
|
33
|
-
encoding: 'utf8',
|
|
34
|
-
timeout: 30000,
|
|
35
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
36
|
-
maxBuffer: SQLITE_READ_BUFFER,
|
|
37
|
-
});
|
|
38
|
-
return out
|
|
32
|
+
return sqliteRead(db, sql)
|
|
39
33
|
.split('\n')
|
|
40
34
|
.filter((line) => line.trim())
|
|
41
35
|
.map((line) => line.split('\t'));
|
|
42
36
|
}
|
|
43
37
|
// Bump on a parse change: unchanged transcripts are never re-read.
|
|
44
|
-
const PARSER_VERSION = '
|
|
45
|
-
|
|
38
|
+
const PARSER_VERSION = '13';
|
|
39
|
+
/** OpenCode rows gained tool names; re-read by id, since a PARSER_VERSION bump drops rotated history. */
|
|
40
|
+
const OPENCODE_TOOLS_VERSION = '1';
|
|
41
|
+
// A re-parse keeping under half the rows means transcripts vanished mid-migration; the backup is restored instead.
|
|
46
42
|
const REPARSE_GUARD_RATIO = 0.5;
|
|
47
43
|
let guardReport = null;
|
|
48
44
|
/** Why the last sync refused to publish a re-parse, if it did. */
|
|
@@ -52,9 +48,22 @@ export function reparseGuardReport() {
|
|
|
52
48
|
const SCHEMA_SQL = `CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY, v TEXT NOT NULL);` +
|
|
53
49
|
`CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT UNIQUE NOT NULL, mtime REAL NOT NULL, size INTEGER NOT NULL);` +
|
|
54
50
|
`CREATE TABLE IF NOT EXISTS messages (file_id INTEGER NOT NULL REFERENCES files(id), t REAL, model TEXT NOT NULL,` +
|
|
55
|
-
` i INTEGER NOT NULL, o INTEGER NOT NULL, d REAL, cr INTEGER NOT NULL, cw INTEGER NOT NULL,` +
|
|
56
|
-
` usd REAL, st TEXT NOT NULL, code REAL, note TEXT, tools TEXT NOT NULL DEFAULT '[]', h TEXT, id TEXT);` +
|
|
57
|
-
|
|
51
|
+
` i INTEGER NOT NULL, o INTEGER NOT NULL, d REAL, tf REAL, cr INTEGER NOT NULL, cw INTEGER NOT NULL,` +
|
|
52
|
+
` usd REAL, st TEXT NOT NULL, code REAL, note TEXT, tools TEXT NOT NULL DEFAULT '[]', h TEXT, a TEXT, id TEXT);` +
|
|
53
|
+
// Every read filters or orders by one of these three; without them each sync and read scans the table.
|
|
54
|
+
`CREATE INDEX IF NOT EXISTS idx_messages_file ON messages(file_id);` +
|
|
55
|
+
`CREATE INDEX IF NOT EXISTS idx_messages_t ON messages(t);` +
|
|
56
|
+
`CREATE INDEX IF NOT EXISTS idx_messages_h_t ON messages(h, t);` +
|
|
57
|
+
`CREATE INDEX IF NOT EXISTS idx_messages_model ON messages(model);`;
|
|
58
|
+
/** WAL + a checkpoint: the write path avoids an fsync per commit, and usage.db alone holds every row. */
|
|
59
|
+
function pragmas(db) {
|
|
60
|
+
run(db, 'PRAGMA journal_mode=WAL;' +
|
|
61
|
+
'PRAGMA synchronous=NORMAL;' +
|
|
62
|
+
'PRAGMA wal_autocheckpoint=256;' +
|
|
63
|
+
'PRAGMA cache_size=-8000;' +
|
|
64
|
+
'PRAGMA temp_store=MEMORY;' +
|
|
65
|
+
'PRAGMA mmap_size=268435456;');
|
|
66
|
+
}
|
|
58
67
|
function storedParserVersion(db) {
|
|
59
68
|
try {
|
|
60
69
|
const stored = query(db, `SELECT v FROM meta WHERE k='parser_version';`);
|
|
@@ -64,6 +73,14 @@ function storedParserVersion(db) {
|
|
|
64
73
|
return null;
|
|
65
74
|
}
|
|
66
75
|
}
|
|
76
|
+
// A store written by a newer tersio: no forward migration, so a re-parse would swap rotated history for what is left.
|
|
77
|
+
function isDowngrade(stored) {
|
|
78
|
+
if (stored === null)
|
|
79
|
+
return false;
|
|
80
|
+
const storedNum = Number(stored);
|
|
81
|
+
const currentNum = Number(PARSER_VERSION);
|
|
82
|
+
return Number.isFinite(storedNum) && Number.isFinite(currentNum) && storedNum > currentNum;
|
|
83
|
+
}
|
|
67
84
|
function countMessages(db) {
|
|
68
85
|
try {
|
|
69
86
|
const rows = query(db, `SELECT COUNT(*) FROM messages;`);
|
|
@@ -84,6 +101,16 @@ function newestOpencodeMs(db) {
|
|
|
84
101
|
return null;
|
|
85
102
|
}
|
|
86
103
|
}
|
|
104
|
+
/** Whether the OpenCode tools backfill ran; unfilled rows stay empty, so this widens the scan once. */
|
|
105
|
+
function opencodeToolsBackfilled(db) {
|
|
106
|
+
try {
|
|
107
|
+
const rows = query(db, `SELECT v FROM meta WHERE k='opencode_tools';`);
|
|
108
|
+
return rows.length > 0 && rows[0][0] === OPENCODE_TOOLS_VERSION;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
87
114
|
// Same pre-migration state as the newest backup: another copy churns the 3-slot rotation for nothing.
|
|
88
115
|
function newestBackupMatches(stored) {
|
|
89
116
|
if (stored === null)
|
|
@@ -99,7 +126,7 @@ function newestBackupMatches(stored) {
|
|
|
99
126
|
return false;
|
|
100
127
|
}
|
|
101
128
|
}
|
|
102
|
-
// v9 shape (path-keyed rows) migrates in place with zero re-parse, so history survives even
|
|
129
|
+
// v9 shape (path-keyed rows) migrates in place with zero re-parse, so history survives even without transcripts.
|
|
103
130
|
function canMigrateInPlace(db) {
|
|
104
131
|
try {
|
|
105
132
|
const names = query(db, `PRAGMA table_info(messages);`).map((r) => r[1]);
|
|
@@ -116,8 +143,8 @@ function migrateInPlace(db, preCount) {
|
|
|
116
143
|
`CREATE TABLE files_new (id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT UNIQUE NOT NULL, mtime REAL NOT NULL, size INTEGER NOT NULL);` +
|
|
117
144
|
`INSERT INTO files_new (path, mtime, size) SELECT path, mtime, size FROM files;` +
|
|
118
145
|
`CREATE TABLE messages_new (file_id INTEGER NOT NULL REFERENCES files_new(id), t REAL, model TEXT NOT NULL,` +
|
|
119
|
-
` i INTEGER NOT NULL, o INTEGER NOT NULL, d REAL, cr INTEGER NOT NULL, cw INTEGER NOT NULL,` +
|
|
120
|
-
` usd REAL, st TEXT NOT NULL, code REAL, note TEXT, tools TEXT NOT NULL DEFAULT '[]', h TEXT, id TEXT);` +
|
|
146
|
+
` i INTEGER NOT NULL, o INTEGER NOT NULL, d REAL, tf REAL, cr INTEGER NOT NULL, cw INTEGER NOT NULL,` +
|
|
147
|
+
` usd REAL, st TEXT NOT NULL, code REAL, note TEXT, tools TEXT NOT NULL DEFAULT '[]', h TEXT, a TEXT, id TEXT);` +
|
|
121
148
|
`INSERT INTO messages_new (file_id, ${cols}) SELECT (SELECT id FROM files_new WHERE files_new.path = messages.file), ${cols} FROM messages;` +
|
|
122
149
|
`DROP TABLE messages;DROP TABLE files;` +
|
|
123
150
|
`ALTER TABLE files_new RENAME TO files;ALTER TABLE messages_new RENAME TO messages;` +
|
|
@@ -135,6 +162,63 @@ function migrateInPlace(db, preCount) {
|
|
|
135
162
|
return false;
|
|
136
163
|
}
|
|
137
164
|
}
|
|
165
|
+
// v12 adds the agent kind with a bare ALTER; existing rows read NULL and report under the main agent.
|
|
166
|
+
function canAddAgent(db) {
|
|
167
|
+
try {
|
|
168
|
+
const names = query(db, `PRAGMA table_info(messages);`).map((r) => r[1]);
|
|
169
|
+
return names.includes('file_id') && !names.includes('a');
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function addAgentColumn(db) {
|
|
176
|
+
try {
|
|
177
|
+
run(db, `ALTER TABLE messages ADD COLUMN a TEXT;`);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
// v13 shape gains the first-token latency with a bare ALTER, so no row moves and no wipe runs.
|
|
185
|
+
function canAddTtft(db) {
|
|
186
|
+
try {
|
|
187
|
+
const names = query(db, `PRAGMA table_info(messages);`).map((r) => r[1]);
|
|
188
|
+
return names.includes('file_id') && !names.includes('tf');
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// The column is additive, and a rotated transcript cannot be re-parsed, so the ALTER precedes the backfill.
|
|
195
|
+
function addTtftColumn(db) {
|
|
196
|
+
try {
|
|
197
|
+
run(db, `ALTER TABLE messages ADD COLUMN tf REAL;`);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
return !canAddTtft(db);
|
|
203
|
+
}
|
|
204
|
+
/** First-token-latency marker; unchanged transcripts are never revisited, so old stores stay null. */
|
|
205
|
+
const TTFT_VERSION = '1';
|
|
206
|
+
function ttftBackfilled(db) {
|
|
207
|
+
try {
|
|
208
|
+
const rows = query(db, `SELECT v FROM meta WHERE k='ttft_backfill';`);
|
|
209
|
+
return rows.length > 0 && rows[0][0] === TTFT_VERSION;
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** Clears ledger mtimes so every transcript re-reads; synthetic `db#row-id` OpenCode keys stay untouched. */
|
|
216
|
+
function markTranscriptsDirty(db) {
|
|
217
|
+
try {
|
|
218
|
+
run(db, `UPDATE files SET mtime = 0 WHERE path NOT LIKE '%#%';`);
|
|
219
|
+
}
|
|
220
|
+
catch { /* a store with no files table yet, or a read-only file: nothing to backfill */ }
|
|
221
|
+
}
|
|
138
222
|
// v10 shape gains the message id with a bare ALTER: no rows move, so no re-parse and no guard trip.
|
|
139
223
|
function canAlterInPlace(db) {
|
|
140
224
|
try {
|
|
@@ -154,15 +238,21 @@ function alterInPlace(db) {
|
|
|
154
238
|
}
|
|
155
239
|
return !canAlterInPlace(db);
|
|
156
240
|
}
|
|
157
|
-
function ensureSchema(db) {
|
|
241
|
+
function ensureSchema(db, forceWipe) {
|
|
158
242
|
fs.mkdirSync(path.dirname(db), { recursive: true });
|
|
159
243
|
// Fold in rows stored under a pre-alias spelling; the mtime ledger will not re-read those transcripts.
|
|
160
244
|
const rekeys = MODEL_ALIASES.map((a) => `UPDATE messages SET model='${a.id}' WHERE model='${a.feed}';`).join('');
|
|
161
245
|
const stored = storedParserVersion(db);
|
|
246
|
+
pragmas(db);
|
|
162
247
|
if (stored === PARSER_VERSION) {
|
|
163
248
|
run(db, SCHEMA_SQL + rekeys);
|
|
164
249
|
return;
|
|
165
250
|
}
|
|
251
|
+
// A newer store keeps its rows: only an explicit force re-parse may wipe it.
|
|
252
|
+
if (!forceWipe && isDowngrade(stored)) {
|
|
253
|
+
run(db, SCHEMA_SQL + rekeys);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
166
256
|
// A version bump rebuilds both tables, so no ALTER patchwork survives a migration.
|
|
167
257
|
try {
|
|
168
258
|
if (countMessages(db) > 0 && !newestBackupMatches(stored))
|
|
@@ -179,6 +269,16 @@ function ensureSchema(db) {
|
|
|
179
269
|
run(db, `INSERT OR REPLACE INTO meta (k,v) VALUES ('parser_version','${PARSER_VERSION}');`);
|
|
180
270
|
return;
|
|
181
271
|
}
|
|
272
|
+
// The agent column is additive: every row survives and unchanged transcripts read NULL until a re-parse.
|
|
273
|
+
if (stored !== null && canAddAgent(db) && addAgentColumn(db)) {
|
|
274
|
+
run(db, `INSERT OR REPLACE INTO meta (k, v) VALUES ('parser_version','${PARSER_VERSION}');`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
// The ttft column is additive too; its values come from the backfill in `syncUsageDb`.
|
|
278
|
+
if (stored !== null && canAddTtft(db) && addTtftColumn(db)) {
|
|
279
|
+
run(db, `INSERT OR REPLACE INTO meta (k, v) VALUES ('parser_version','${PARSER_VERSION}');`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
182
282
|
run(db, `DROP TABLE IF EXISTS messages;DROP TABLE IF EXISTS files;` + SCHEMA_SQL +
|
|
183
283
|
`INSERT OR REPLACE INTO meta (k,v) VALUES ('parser_version','${PARSER_VERSION}');`);
|
|
184
284
|
}
|
|
@@ -287,37 +387,40 @@ function readFiles(db) {
|
|
|
287
387
|
catch { /* fresh db */ }
|
|
288
388
|
return known;
|
|
289
389
|
}
|
|
290
|
-
// OpenCode rows carry
|
|
291
|
-
function opencodeStoredRow(oc, host, id) {
|
|
390
|
+
// OpenCode rows carry only tokens, msg id, and tools; they record as the main agent, so no NULL kind.
|
|
391
|
+
function opencodeStoredRow(oc, host, id, tools) {
|
|
292
392
|
return {
|
|
293
393
|
t: oc.ms ?? null,
|
|
294
394
|
model: canonicalModelId(oc.model),
|
|
295
395
|
i: intOf(oc.usage.input),
|
|
296
396
|
o: intOf(oc.usage.output),
|
|
297
397
|
d: durOf(oc.durMs),
|
|
398
|
+
tf: durOf(oc.ttftMs),
|
|
298
399
|
cr: intOf(oc.usage.cacheRead),
|
|
299
400
|
cw: intOf(oc.usage.cacheWrite),
|
|
300
401
|
usd: costOf(oc.usage),
|
|
301
402
|
st: 'completed',
|
|
302
403
|
code: undefined,
|
|
303
404
|
note: undefined,
|
|
304
|
-
tools
|
|
405
|
+
tools,
|
|
305
406
|
host,
|
|
407
|
+
agent: 'main',
|
|
306
408
|
id,
|
|
307
409
|
};
|
|
308
410
|
}
|
|
309
|
-
function parseFile(text, host, file) {
|
|
411
|
+
function parseFile(text, host, file, agent) {
|
|
310
412
|
const rows = [];
|
|
311
413
|
// Guarded: a JSONL transcript must fall through, not abort the sync.
|
|
312
414
|
let oc = null;
|
|
313
415
|
try {
|
|
314
|
-
|
|
416
|
+
const message = JSON.parse(text);
|
|
417
|
+
oc = classifyOpencodeMessage(message);
|
|
418
|
+
if (oc) {
|
|
419
|
+
rows.push(opencodeStoredRow(oc, host, file ? path.basename(file).replace(/\.[^.]+$/, '') : undefined, opencodeToolNames(message)));
|
|
420
|
+
return rows;
|
|
421
|
+
}
|
|
315
422
|
}
|
|
316
423
|
catch { /* JSONL */ }
|
|
317
|
-
if (oc) {
|
|
318
|
-
rows.push(opencodeStoredRow(oc, host, file ? path.basename(file).replace(/\.[^.]+$/, '') : undefined));
|
|
319
|
-
return rows;
|
|
320
|
-
}
|
|
321
424
|
let codexProvider = null;
|
|
322
425
|
let codexModel = null;
|
|
323
426
|
for (const line of text.split('\n')) {
|
|
@@ -349,6 +452,7 @@ function parseFile(text, host, file) {
|
|
|
349
452
|
i: intOf(usage.input),
|
|
350
453
|
o: intOf(usage.output),
|
|
351
454
|
d: undefined,
|
|
455
|
+
tf: undefined,
|
|
352
456
|
cr: intOf(usage.cacheRead),
|
|
353
457
|
cw: intOf(usage.cacheWrite),
|
|
354
458
|
usd: costOf(usage),
|
|
@@ -357,6 +461,7 @@ function parseFile(text, host, file) {
|
|
|
357
461
|
note: undefined,
|
|
358
462
|
tools: [],
|
|
359
463
|
host,
|
|
464
|
+
agent,
|
|
360
465
|
id: rid,
|
|
361
466
|
});
|
|
362
467
|
continue;
|
|
@@ -373,6 +478,7 @@ function parseFile(text, host, file) {
|
|
|
373
478
|
i: intOf(usage.input),
|
|
374
479
|
o: intOf(usage.output),
|
|
375
480
|
d: dur,
|
|
481
|
+
tf: durOf(parsed.ttftMs),
|
|
376
482
|
cr: intOf(usage.cacheRead),
|
|
377
483
|
cw: intOf(usage.cacheWrite),
|
|
378
484
|
usd: costOf(usage),
|
|
@@ -381,6 +487,7 @@ function parseFile(text, host, file) {
|
|
|
381
487
|
note,
|
|
382
488
|
tools: parsed.tools ?? [],
|
|
383
489
|
host,
|
|
490
|
+
agent,
|
|
384
491
|
id: rid,
|
|
385
492
|
});
|
|
386
493
|
}
|
|
@@ -389,12 +496,12 @@ function parseFile(text, host, file) {
|
|
|
389
496
|
return rows;
|
|
390
497
|
}
|
|
391
498
|
function insertSql(file, r) {
|
|
392
|
-
return `INSERT INTO messages (file_id, t, model, i, o, d, cr, cw, usd, st, code, note, tools, h, id) VALUES (` +
|
|
499
|
+
return `INSERT INTO messages (file_id, t, model, i, o, d, tf, cr, cw, usd, st, code, note, tools, h, a, id) VALUES (` +
|
|
393
500
|
`(SELECT id FROM files WHERE path=${esc(file)}),${r.t === null ? 'NULL' : String(r.t)},${esc(r.model)},${r.i},${r.o},` +
|
|
394
|
-
`${nullNum(r.d)},${r.cr},${r.cw},${nullNum(r.usd)},${esc(r.st)},` +
|
|
395
|
-
`${nullNum(r.code)},${nullStr(r.note)},${esc(JSON.stringify(r.tools))},${nullStr(r.host)},${nullStr(r.id)});`;
|
|
501
|
+
`${nullNum(r.d)},${nullNum(r.tf)},${r.cr},${r.cw},${nullNum(r.usd)},${esc(r.st)},` +
|
|
502
|
+
`${nullNum(r.code)},${nullStr(r.note)},${esc(JSON.stringify(r.tools))},${nullStr(r.host)},${nullStr(r.agent ?? undefined)},${nullStr(r.id)});`;
|
|
396
503
|
}
|
|
397
|
-
// Unchanged transcripts are skipped via mtime+size; rows for deleted ones are kept so rotation never erases history.
|
|
504
|
+
// Unchanged transcripts are skipped via mtime+size; rows for deleted ones are kept so rotation never erases history.
|
|
398
505
|
export function syncUsageDb() {
|
|
399
506
|
let db;
|
|
400
507
|
guardReport = null;
|
|
@@ -403,12 +510,19 @@ export function syncUsageDb() {
|
|
|
403
510
|
try {
|
|
404
511
|
db = usageDbPath();
|
|
405
512
|
const stored = storedParserVersion(db);
|
|
406
|
-
migrated = stored !== null && stored !== PARSER_VERSION;
|
|
407
513
|
// The operator deleted sessions on purpose and wants the mirror to match: bypass the guard.
|
|
408
514
|
const forced = process.env.TERSIO_FORCE_REPARSE === '1';
|
|
515
|
+
// A downgrade keeps its rows: the wipe below cannot rebuild them from rotated transcripts.
|
|
516
|
+
const downgrade = !forced && isDowngrade(stored);
|
|
517
|
+
migrated = stored !== null && stored !== PARSER_VERSION && !downgrade;
|
|
518
|
+
if (downgrade)
|
|
519
|
+
guardReport = `store was written by a newer tersio (parser ${stored} > ${PARSER_VERSION}); kept its rows and skipped the re-parse`;
|
|
409
520
|
if (migrated && !forced)
|
|
410
521
|
preWipe = countMessages(db);
|
|
411
|
-
ensureSchema(db);
|
|
522
|
+
ensureSchema(db, forced);
|
|
523
|
+
// One rewrite so the additive column's NULL rows gain latency; runs while the marker is absent.
|
|
524
|
+
if (!downgrade && !ttftBackfilled(db))
|
|
525
|
+
markTranscriptsDirty(db);
|
|
412
526
|
maybeScheduledBackup(db);
|
|
413
527
|
}
|
|
414
528
|
catch {
|
|
@@ -433,8 +547,10 @@ export function syncUsageDb() {
|
|
|
433
547
|
const ocDbPath = opencodeDbPath();
|
|
434
548
|
if (process.env.TERSIO_SESSIONS_DIR === undefined || process.env.TERSIO_OPENCODE_DB !== undefined) {
|
|
435
549
|
const newest = newestOpencodeMs(db);
|
|
550
|
+
// A store predating the tool names re-reads the whole table once; the marker closes the window.
|
|
551
|
+
const since = newest === null || !opencodeToolsBackfilled(db) ? 0 : newest - OPENCODE_DB_OVERLAP_MS;
|
|
436
552
|
if (newest !== null || fs.existsSync(ocDbPath)) {
|
|
437
|
-
ocDbRows.push(...readOpencodeDbRows(ocDbPath,
|
|
553
|
+
ocDbRows.push(...readOpencodeDbRows(ocDbPath, since));
|
|
438
554
|
}
|
|
439
555
|
}
|
|
440
556
|
const known = readFiles(db);
|
|
@@ -454,6 +570,7 @@ export function syncUsageDb() {
|
|
|
454
570
|
if (!prev || prev.mtime !== entry.mtime || prev.size !== entry.size)
|
|
455
571
|
changed.push(file);
|
|
456
572
|
}
|
|
573
|
+
// `ocDbRows` is non-empty whenever the backfill window is open, so this never skips it.
|
|
457
574
|
if (!changed.length && !ocDbRows.length)
|
|
458
575
|
return true;
|
|
459
576
|
const chunks = [];
|
|
@@ -489,7 +606,7 @@ export function syncUsageDb() {
|
|
|
489
606
|
try {
|
|
490
607
|
for (const file of changed) {
|
|
491
608
|
const text = fs.readFileSync(file, 'utf8');
|
|
492
|
-
for (const r of parseFile(text, hostOfSessionFile(file), file)) {
|
|
609
|
+
for (const r of parseFile(text, hostOfSessionFile(file), file, agentOfSessionText(text))) {
|
|
493
610
|
if (!push(insertSql(file, r)))
|
|
494
611
|
return false;
|
|
495
612
|
}
|
|
@@ -503,7 +620,7 @@ export function syncUsageDb() {
|
|
|
503
620
|
if (!push(`INSERT INTO files (path, mtime, size) VALUES (${esc(key)},${row.created},0)` +
|
|
504
621
|
` ON CONFLICT(path) DO UPDATE SET mtime=excluded.mtime;` +
|
|
505
622
|
`DELETE FROM messages WHERE file_id=(SELECT id FROM files WHERE path=${esc(key)});` +
|
|
506
|
-
insertSql(key, opencodeStoredRow(parsed, 'opencode', row.id))))
|
|
623
|
+
insertSql(key, opencodeStoredRow(parsed, 'opencode', row.id, opencodeToolNames(row.row, row.tools)))))
|
|
507
624
|
return false;
|
|
508
625
|
}
|
|
509
626
|
}
|
|
@@ -512,6 +629,16 @@ export function syncUsageDb() {
|
|
|
512
629
|
}
|
|
513
630
|
if (!push(`INSERT OR REPLACE INTO meta (k, v) VALUES ('last_sync',${esc(String(Date.now()))});`))
|
|
514
631
|
return false;
|
|
632
|
+
// Only once the rows are written, so a sync that fails midway widens the scan again.
|
|
633
|
+
if (ocDbRows.length > 0) {
|
|
634
|
+
if (!push(`INSERT OR REPLACE INTO meta (k, v) VALUES ('opencode_tools','${OPENCODE_TOOLS_VERSION}');`))
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
// Set only after the rewrite commits: a crash mid-backfill leaves mtimes cleared and the marker absent.
|
|
638
|
+
if (changed.length > 0) {
|
|
639
|
+
if (!push(`INSERT OR REPLACE INTO meta (k, v) VALUES ('ttft_backfill','${TTFT_VERSION}');`))
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
515
642
|
if (!flush())
|
|
516
643
|
return false;
|
|
517
644
|
if (preWipe > 0) {
|
|
@@ -531,6 +658,11 @@ export function syncUsageDb() {
|
|
|
531
658
|
}
|
|
532
659
|
catch { /* best-effort */ }
|
|
533
660
|
}
|
|
661
|
+
// Fold the WAL back into the main file: a backup copies one file, and a checkpoint keeps it whole.
|
|
662
|
+
try {
|
|
663
|
+
run(db, `PRAGMA wal_checkpoint(TRUNCATE);`);
|
|
664
|
+
}
|
|
665
|
+
catch { /* best-effort */ }
|
|
534
666
|
return true;
|
|
535
667
|
}
|
|
536
668
|
export function readUsageDb() {
|
|
@@ -544,21 +676,28 @@ export function readUsageDb() {
|
|
|
544
676
|
}
|
|
545
677
|
let rows;
|
|
546
678
|
try {
|
|
547
|
-
|
|
679
|
+
// The wide `tools` column is read as one string and skipped in JS; a pre-agent store is read without it.
|
|
680
|
+
const names = query(db, `PRAGMA table_info(messages);`).map((r) => r[1]);
|
|
681
|
+
const agentCol = names.includes('a') ? 'a' : "''";
|
|
682
|
+
// A store predating first-token latency has no `tf` column; read NULL so the row still loads.
|
|
683
|
+
const tfCol = names.includes('tf') ? 'tf' : 'NULL';
|
|
684
|
+
rows = query(db, `SELECT t, model, i, o, d, cr, cw, usd, st, code, note, tools, h, ${agentCol}, id, ${tfCol} FROM messages;`);
|
|
548
685
|
}
|
|
549
686
|
catch {
|
|
550
687
|
return null;
|
|
551
688
|
}
|
|
552
689
|
const accum = newSessionAccum();
|
|
553
|
-
for (const [t, model, i, o, d, cr, cw, usd, st, code, note, tools, h, id] of rows) {
|
|
690
|
+
for (const [t, model, i, o, d, cr, cw, usd, st, code, note, tools, h, a, id, tf] of rows) {
|
|
554
691
|
const ts = t === '' ? undefined : Number(t);
|
|
555
692
|
let toolNames = [];
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
693
|
+
if (tools && tools !== '[]' && tools !== '') {
|
|
694
|
+
try {
|
|
695
|
+
const parsed = JSON.parse(tools);
|
|
696
|
+
if (Array.isArray(parsed))
|
|
697
|
+
toolNames = parsed.filter((x) => typeof x === 'string');
|
|
698
|
+
}
|
|
699
|
+
catch { /* keep empty */ }
|
|
560
700
|
}
|
|
561
|
-
catch { /* keep empty */ }
|
|
562
701
|
const usage = {
|
|
563
702
|
input: Number(i) || 0,
|
|
564
703
|
output: Number(o) || 0,
|
|
@@ -574,7 +713,7 @@ export function readUsageDb() {
|
|
|
574
713
|
st: st === 'error' || st === 'aborted' ? st : 'completed',
|
|
575
714
|
code: code === '' ? undefined : Number(code),
|
|
576
715
|
note: note === '' ? undefined : note,
|
|
577
|
-
}, toolNames, h === '' ? undefined : h, id === '' || id === undefined ? undefined : id);
|
|
716
|
+
}, toolNames, h === '' ? undefined : h, id === '' || id === undefined ? undefined : id, a === '' ? undefined : a, tf === undefined || tf === '' ? undefined : Number(tf));
|
|
578
717
|
}
|
|
579
718
|
accum.recent.sort((a, b) => b.t - a.t);
|
|
580
719
|
let syncedAt = 0;
|
|
@@ -594,6 +733,10 @@ export function readUsageDb() {
|
|
|
594
733
|
byTool: accum.byTool,
|
|
595
734
|
byModelMessages: accum.byModelMessages,
|
|
596
735
|
byHost: accum.byHost,
|
|
736
|
+
byAgent: accum.byAgent,
|
|
737
|
+
byAgentMessages: accum.byAgentMessages,
|
|
738
|
+
byModelMeasured: accum.byModelMeasured,
|
|
739
|
+
byModelCostMessages: accum.byModelCostMessages,
|
|
597
740
|
costMeasured: accum.costMeasured,
|
|
598
741
|
recent: accum.recent.slice(0, RECENT_LIMIT),
|
|
599
742
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Shared helpers for the installer and
|
|
1
|
+
// Shared helpers for the installer and AI add-ons updater. Node built-ins only — must stay dependency-free.
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { accessSync, constants, createWriteStream, existsSync, mkdirSync, renameSync, statSync } from 'node:fs';
|
|
4
4
|
import fs from 'node:fs/promises';
|
|
@@ -6,7 +6,7 @@ import https from 'node:https';
|
|
|
6
6
|
import os from 'node:os';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
// Promise.withResolvers is Node 22+; the package supports Node 18+.
|
|
9
|
-
|
|
9
|
+
function withResolvers() {
|
|
10
10
|
let resolve;
|
|
11
11
|
let reject;
|
|
12
12
|
const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
|
|
@@ -87,7 +87,7 @@ function redirectNext(res, url) {
|
|
|
87
87
|
}
|
|
88
88
|
return null;
|
|
89
89
|
}
|
|
90
|
-
// pi sets PI_CODING_AGENT; OMP does not.
|
|
90
|
+
// pi sets PI_CODING_AGENT; OMP does not. It reads PI_CODING_AGENT_DIR to relocate, so the override only counts there.
|
|
91
91
|
export function isPiProcess() {
|
|
92
92
|
return process.env.PI_CODING_AGENT === 'true';
|
|
93
93
|
}
|
|
@@ -95,7 +95,7 @@ export function piAgentDir() {
|
|
|
95
95
|
const override = process.env.PI_CODING_AGENT_DIR;
|
|
96
96
|
return isPiProcess() && override ? override : path.join(homeDir(), '.pi', 'agent');
|
|
97
97
|
}
|
|
98
|
-
// Ponytail is
|
|
98
|
+
// Ponytail is hoisted, so it sits in Tersio's node_modules, a host's plugin dir, or beside it; walking up covers all.
|
|
99
99
|
export function findHoistedPackage(pkg, fromDir, ...relInside) {
|
|
100
100
|
let dir = fromDir;
|
|
101
101
|
for (;;) {
|
|
@@ -235,7 +235,7 @@ export async function readTextIfExists(p) {
|
|
|
235
235
|
return null;
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
|
-
// Chunked
|
|
238
|
+
// Chunked: a 200 MiB binary never fits memory. `tail` keeps needle.length-1 bytes in case a match spans chunks.
|
|
239
239
|
export async function fileContains(filePath, needleText) {
|
|
240
240
|
const needle = Buffer.from(needleText, 'utf8');
|
|
241
241
|
if (needle.length === 0)
|
|
@@ -266,7 +266,7 @@ export async function fileContains(filePath, needleText) {
|
|
|
266
266
|
await handle?.close().catch(() => { });
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
|
-
// Tersio
|
|
269
|
+
// Tersio data home: ~/.tersio. First use moves legacy ~/.omp/plugins/tersio-* files over so installs keep history.
|
|
270
270
|
const migratedTersioFiles = new Set();
|
|
271
271
|
export function tersioHome() {
|
|
272
272
|
// Matches the other TERSIO_* overrides so a test never reads the real home.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export default function timingExtension(pi) {
|
|
2
|
+
// First token per request, keyed by the request's own start stamp, which survives every stream event.
|
|
3
|
+
const firstTokenAt = new Map();
|
|
4
|
+
pi.on('message_update', (event) => {
|
|
5
|
+
const message = event.message;
|
|
6
|
+
if (!message || message.role !== 'assistant')
|
|
7
|
+
return;
|
|
8
|
+
const anchor = typeof message.timestamp === 'number' ? message.timestamp : 0;
|
|
9
|
+
if (anchor <= 0 || firstTokenAt.has(anchor))
|
|
10
|
+
return;
|
|
11
|
+
// `start` opens the stream with no token yet; the first delta is the first token.
|
|
12
|
+
if (!String(event.assistantMessageEvent?.type ?? '').endsWith('_delta'))
|
|
13
|
+
return;
|
|
14
|
+
firstTokenAt.set(anchor, Date.now());
|
|
15
|
+
});
|
|
16
|
+
pi.on('message_end', (event) => {
|
|
17
|
+
const message = event.message;
|
|
18
|
+
if (!message || message.role !== 'assistant')
|
|
19
|
+
return;
|
|
20
|
+
const anchor = typeof message.timestamp === 'number' ? message.timestamp : 0;
|
|
21
|
+
const at = anchor > 0 ? firstTokenAt.get(anchor) : undefined;
|
|
22
|
+
if (anchor > 0)
|
|
23
|
+
firstTokenAt.delete(anchor);
|
|
24
|
+
// A host that recorded its own latency wins; this only fills the gap.
|
|
25
|
+
if (at === undefined || message.ttft !== undefined)
|
|
26
|
+
return;
|
|
27
|
+
const ttft = at - anchor;
|
|
28
|
+
if (ttft > 0)
|
|
29
|
+
message.ttft = ttft;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -1,18 +1,26 @@
|
|
|
1
|
-
// /ai-addons manual updater. Node built-ins only. RTK ships SHA256 checksums
|
|
1
|
+
// /ai-addons manual updater. Node built-ins only. RTK ships unsigned SHA256 checksums, so checksum-only it is.
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
+
import fsSync from 'node:fs';
|
|
4
5
|
import fs from 'node:fs/promises';
|
|
5
6
|
import os from 'node:os';
|
|
6
7
|
import path from 'node:path';
|
|
7
8
|
import { fileURLToPath } from 'node:url';
|
|
8
9
|
import { setExtensionLabel } from '../shared/host.js';
|
|
9
|
-
import { CAVEMAN_REMOTE_ULTRA, CAVEMAN_REMOTE_MEGACAVE, RTK_RELEASE_API, fetchJson, findFile, findHoistedPackage, httpsGet, httpsDownload, sha256Hex, parseChecksum, normalizeRtkVersion, readTextIfExists, rtkPlatformSpec, resolveRtkBinary, } from '../
|
|
10
|
+
import { CAVEMAN_REMOTE_ULTRA, CAVEMAN_REMOTE_MEGACAVE, RTK_RELEASE_API, fetchJson, findFile, findHoistedPackage, httpsGet, httpsDownload, sha256Hex, parseChecksum, normalizeRtkVersion, readTextIfExists, rtkPlatformSpec, resolveRtkBinary, } from '../shared/utils.js';
|
|
10
11
|
const IS_WINDOWS = process.platform === 'win32';
|
|
11
12
|
const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const PONYTAIL_REMOTE = 'https://raw.githubusercontent.com/DietrichGebert/ponytail/main/package.json';
|
|
13
14
|
const RTK_BINARY = resolveRtkBinary();
|
|
14
|
-
// Beside this file
|
|
15
|
-
const
|
|
15
|
+
// Beside this file: a retired agent-tree copy has no `caveman` sibling, so fall back to the OMP plugin.
|
|
16
|
+
const CAVEMAN_SIBLING = (name) => path.join(EXTENSION_DIR, '..', 'caveman', name);
|
|
17
|
+
function cavemanRule(name) {
|
|
18
|
+
const beside = CAVEMAN_SIBLING(name);
|
|
19
|
+
if (fsSync.existsSync(beside))
|
|
20
|
+
return beside;
|
|
21
|
+
const plugin = path.join(os.homedir(), '.omp', 'plugins', 'node_modules', '@krtclcdy', 'tersio', 'extensions', 'caveman', name);
|
|
22
|
+
return fsSync.existsSync(plugin) ? plugin : beside;
|
|
23
|
+
}
|
|
16
24
|
const CAVEMAN_LOCAL = cavemanRule('rule.md');
|
|
17
25
|
const CAVEMAN_TRACKED = [
|
|
18
26
|
{ file: cavemanRule('rule-ultra.md'), remote: CAVEMAN_REMOTE_ULTRA },
|
|
@@ -20,7 +28,7 @@ const CAVEMAN_TRACKED = [
|
|
|
20
28
|
];
|
|
21
29
|
// Host-neutral: this updater runs inside pi, OMP, and OpenCode alike.
|
|
22
30
|
const RELOAD_MSG = 'Reminder: restart your agent (or reload extensions) for updates to take effect.';
|
|
23
|
-
// Ponytail is
|
|
31
|
+
// Ponytail is hoisted, so the installed copy is found by walking up, not by naming a host directory.
|
|
24
32
|
function ponytailLocal() {
|
|
25
33
|
return findHoistedPackage('@dietrichgebert/ponytail', EXTENSION_DIR, 'package.json');
|
|
26
34
|
}
|
|
@@ -139,7 +147,6 @@ async function checkAddons(ctx) {
|
|
|
139
147
|
}
|
|
140
148
|
return lines.join('\n');
|
|
141
149
|
}
|
|
142
|
-
// Ponytail is a hoisted dependency; replace its package dir from the npm tarball.
|
|
143
150
|
async function updatePonytail(pi, ctx, dryRun = false) {
|
|
144
151
|
void pi;
|
|
145
152
|
const localPkg = ponytailLocal();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Throwaway: read the real rate cards out of the pi-catalog omp already ships.
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
const root = process.env.HOME + "/.bun/install/global/node_modules/@oh-my-pi/pi-catalog/src";
|
|
4
|
+
const catalog = JSON.parse(readFileSync(root + "/models.json", "utf8"));
|
|
5
|
+
const wanted = [
|
|
6
|
+
"space-bunny",
|
|
7
|
+
"muse-spark-1.3-contributor",
|
|
8
|
+
"glm-5.3-flash",
|
|
9
|
+
"qwen38-flash-next",
|
|
10
|
+
"muse-spark-1.2-contributor",
|
|
11
|
+
"qwen3.8-27b",
|
|
12
|
+
"m3",
|
|
13
|
+
"ox-alpha",
|
|
14
|
+
"deepseek-v4-flash",
|
|
15
|
+
"ling-3.0-flash-sante",
|
|
16
|
+
"qwen3.6-35b-a3b-fp8",
|
|
17
|
+
"mimo-v2.6-flash",
|
|
18
|
+
"deepseek-v4.1-flash",
|
|
19
|
+
"step-5-preview",
|
|
20
|
+
"ling-3.1-flash",
|
|
21
|
+
"longcat-2.5-preview",
|
|
22
|
+
];
|
|
23
|
+
let providers = 0;
|
|
24
|
+
let rows = 0;
|
|
25
|
+
const hits = new Map();
|
|
26
|
+
for (const [provider, models] of Object.entries(catalog)) {
|
|
27
|
+
if (typeof models !== "object" || models === null)
|
|
28
|
+
continue;
|
|
29
|
+
providers += 1;
|
|
30
|
+
for (const [id, row] of Object.entries(models)) {
|
|
31
|
+
rows += 1;
|
|
32
|
+
const bare = id.toLowerCase().split("/").pop();
|
|
33
|
+
if (wanted.includes(bare)) {
|
|
34
|
+
const list = hits.get(bare) ?? [];
|
|
35
|
+
list.push({ provider, id, cost: row.cost, status: row.pricingStatus });
|
|
36
|
+
hits.set(bare, list);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
console.log("providers:", providers, "model rows:", rows, "\n");
|
|
41
|
+
for (const w of wanted) {
|
|
42
|
+
const list = hits.get(w) ?? [];
|
|
43
|
+
console.log(w.padEnd(26) + (list.length === 0 ? " — NOT IN CATALOG —" : " " + list.length + " row(s)"));
|
|
44
|
+
for (const h of list.slice(0, 4)) {
|
|
45
|
+
const c = h.cost;
|
|
46
|
+
console.log(" " + (h.provider + "/" + h.id).padEnd(52) + " in=" + String(c?.input ?? "?") + " out=" + String(c?.output ?? "?") + " status=" + JSON.stringify(h.status));
|
|
47
|
+
}
|
|
48
|
+
}
|