@krtclcdy/tersio 2.25.5 → 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 +17 -36
- package/dashboard/dist/index.html +15 -15
- package/dist/cli/backups.js +7 -14
- package/dist/cli/banner.js +4 -3
- package/dist/cli/common.js +28 -12
- package/dist/cli/currency.js +1 -1
- package/dist/cli/dashboard.js +66 -35
- package/dist/cli/doctor-fix.js +204 -19
- package/dist/cli/doctor.js +226 -39
- package/dist/cli/hosts.js +25 -5
- package/dist/cli/install.js +144 -48
- package/dist/cli/interactive.js +3 -3
- package/dist/cli/manifest.js +87 -7
- package/dist/cli/profile.js +13 -8
- package/dist/cli/rtk-wiring.js +1 -1
- package/dist/cli/settings.js +20 -5
- package/dist/cli/uninstall.js +3 -3
- package/dist/cli/update.js +6 -7
- package/dist/cli/usage.js +6 -9
- package/dist/extensions/caveman/index.js +4 -2
- package/dist/extensions/caveman/rule-megacave.md +5 -3
- package/dist/extensions/caveman/rule-ultra.md +4 -4
- package/dist/extensions/caveman/rule.md +55 -45
- package/dist/extensions/combo/index.js +62 -21
- package/dist/extensions/commands/index.js +1 -1
- package/dist/extensions/pstack/index.js +119 -0
- package/dist/extensions/pstack/rule.md +65 -0
- package/dist/extensions/rtk/filter.js +0 -1
- package/dist/extensions/rtk/session.js +4 -4
- package/dist/extensions/server.js +90 -6
- package/dist/extensions/shared/agent-stats.js +214 -278
- package/dist/extensions/shared/brand-marks.js +33 -34
- package/dist/extensions/shared/carbon.js +1 -1
- package/dist/extensions/shared/plugin-settings.js +11 -6
- package/dist/extensions/shared/pricing.js +8 -16
- 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 +53 -41
- package/dist/extensions/shared/usage-store.js +111 -52
- package/dist/extensions/shared/utils.js +5 -5
- package/dist/extensions/timing/index.js +31 -0
- package/dist/extensions/updater/index.js +3 -6
- package/dist/tersio.js +8 -3
- package/extensions/caveman/index.ts +4 -2
- package/extensions/caveman/rule-megacave.md +5 -3
- package/extensions/caveman/rule-ultra.md +4 -4
- package/extensions/caveman/rule.md +55 -45
- package/extensions/combo/index.ts +62 -19
- package/extensions/commands/index.ts +1 -1
- package/extensions/pstack/index.ts +128 -0
- package/extensions/pstack/rule.md +65 -0
- package/extensions/rtk/filter.ts +0 -1
- package/extensions/rtk/session.ts +4 -4
- package/extensions/server.ts +72 -7
- package/extensions/shared/agent-stats.ts +226 -278
- package/extensions/shared/brand-marks.ts +34 -34
- package/extensions/shared/carbon.ts +1 -1
- package/extensions/shared/plugin-settings.ts +12 -6
- package/extensions/shared/pricing.ts +10 -26
- 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 +60 -47
- package/extensions/shared/usage-store.ts +120 -52
- package/extensions/shared/utils.ts +5 -5
- package/extensions/timing/index.ts +45 -0
- package/extensions/updater/index.ts +3 -8
- package/package.json +17 -8
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
// Shared OMP extension host types. Structural — the real `pi` object comes from the OMP runtime
|
|
1
|
+
// Shared OMP extension host types. Structural — the real `pi` object comes from the OMP runtime.
|
|
2
2
|
|
|
3
3
|
export interface ComboState {
|
|
4
4
|
level: string;
|
|
5
5
|
caveman: string;
|
|
6
6
|
rtk: string;
|
|
7
7
|
ponytail: string;
|
|
8
|
+
pstack: string;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export type ComboLevel = 'off' | 'medium' | 'balanced' | 'max' | 'custom';
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Usage ledger: append-only JSON lines, read by the CLI, `/tersio`, and the Dashboard. Corrupt lines are skipped.
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { homeDir, isPiProcess, piAgentDir, resolveRtkBinary, tersioDataPath } from './utils.ts';
|
|
5
4
|
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { homeDir, isPiProcess, piAgentDir, resolveRtkBinary, tersioDataPath } from './utils.ts';
|
|
6
|
+
import { sqliteRead } from './sqlite.ts';
|
|
6
7
|
|
|
7
8
|
export type UsageKind = 'command' | 'toggle' | 'install' | 'update' | 'rtk-audit';
|
|
8
9
|
|
|
@@ -50,7 +51,7 @@ export function appendUsage(kind: UsageKind, detail: string): void {
|
|
|
50
51
|
} catch { /* ledger is best-effort; never break the caller */ }
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
//
|
|
54
|
+
// Session tokens, tokscale-style: assistant messages in the host transcripts carry usage, model, and a timestamp.
|
|
54
55
|
export interface TokenBreakdown {
|
|
55
56
|
input: number;
|
|
56
57
|
output: number;
|
|
@@ -67,6 +68,8 @@ export interface RecentRequest {
|
|
|
67
68
|
o: number;
|
|
68
69
|
t: number;
|
|
69
70
|
d?: number;
|
|
71
|
+
/** First-token latency, when the host recorded one. */
|
|
72
|
+
tf?: number;
|
|
70
73
|
/** Which agent wrote the session: pi, omp, or codex. */
|
|
71
74
|
h?: string;
|
|
72
75
|
cr?: number;
|
|
@@ -127,7 +130,7 @@ export function durOf(v: unknown): number | undefined {
|
|
|
127
130
|
return typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : undefined;
|
|
128
131
|
}
|
|
129
132
|
|
|
130
|
-
// Measured cost: usage.cost.total, or a bare number in old fixtures. Anything else means unrecorded,
|
|
133
|
+
// Measured cost: usage.cost.total, or a bare number in old fixtures. Anything else means unrecorded, not 0.
|
|
131
134
|
export function costOf(usage: Record<string, unknown>): number | undefined {
|
|
132
135
|
const c = usage.cost;
|
|
133
136
|
if (typeof c === 'number') return Number.isFinite(c) ? c : undefined;
|
|
@@ -162,7 +165,7 @@ function addInto(into: TokenBreakdown, u: { input?: unknown; output?: unknown; c
|
|
|
162
165
|
export function sessionsDirs(): string[] {
|
|
163
166
|
const override = process.env.TERSIO_SESSIONS_DIR;
|
|
164
167
|
if (override) return [override];
|
|
165
|
-
// Both hosts, not just the running one: a session
|
|
168
|
+
// Both hosts, not just the running one: a pi session is missing if we stop at the first existing directory.
|
|
166
169
|
const dirs = isPiProcess()
|
|
167
170
|
? [path.join(piAgentDir(), 'sessions'), path.join(homeDir(), '.omp', 'agent', 'sessions')]
|
|
168
171
|
: [path.join(homeDir(), '.omp', 'agent', 'sessions'), path.join(piAgentDir(), 'sessions')];
|
|
@@ -204,55 +207,55 @@ export interface OpencodeDbRow {
|
|
|
204
207
|
id: string;
|
|
205
208
|
created: number;
|
|
206
209
|
row: OpencodeMessage;
|
|
210
|
+
/** Tool names this message called, comma-joined. Empty when it called none. */
|
|
211
|
+
tools?: string;
|
|
207
212
|
}
|
|
208
213
|
|
|
209
|
-
//
|
|
210
|
-
// current `session_message` (role in the `type` column, model under data.model).
|
|
211
|
-
// Scalars only: the `data` blob holds newlines and tabs, which the sqlite3 CLI would split.
|
|
214
|
+
// `message` vs `session_message`: read scalars only, since the blob's newlines break the row-per-line output.
|
|
212
215
|
const OC_DB_ORDER = ' ORDER BY time_created;';
|
|
213
216
|
const OC_DB_COLUMNS =
|
|
214
|
-
'json_extract(data,\'$.time.created\'), json_extract(data,\'$.time.completed\'),' +
|
|
217
|
+
'json_extract(data,\'$.time.created\'), json_extract(data,\'$.time.streamed\'), json_extract(data,\'$.time.completed\'),' +
|
|
215
218
|
' json_extract(data,\'$.tokens.input\'), json_extract(data,\'$.tokens.output\'),' +
|
|
216
219
|
' json_extract(data,\'$.tokens.cache.read\'), json_extract(data,\'$.tokens.cache.write\'), json_extract(data,\'$.cost\')';
|
|
220
|
+
const OC_DB_TOOLS =
|
|
221
|
+
'(SELECT group_concat(json_extract(j.value,\'$.name\')) FROM json_each(data,\'$.content\') j' +
|
|
222
|
+
' WHERE json_extract(j.value,\'$.type\')=\'tool\')';
|
|
217
223
|
const ocDbLegacy = (since: number): string =>
|
|
218
|
-
`SELECT id, time_created, json_extract(data,'$.role'), json_extract(data,'$.providerID'), json_extract(data,'$.modelID'), ${OC_DB_COLUMNS}` +
|
|
224
|
+
`SELECT id, time_created, json_extract(data,'$.role'), json_extract(data,'$.providerID'), json_extract(data,'$.modelID'), ${OC_DB_COLUMNS}, ${OC_DB_TOOLS}` +
|
|
219
225
|
` FROM message WHERE json_extract(data,'$.role')='assistant' AND time_created > ${since}${OC_DB_ORDER}`;
|
|
220
226
|
const ocDbCurrent = (since: number): string =>
|
|
221
|
-
`SELECT id, time_created, 'assistant', json_extract(data,'$.model.providerID'), json_extract(data,'$.model.id'), ${OC_DB_COLUMNS}` +
|
|
227
|
+
`SELECT id, time_created, 'assistant', json_extract(data,'$.model.providerID'), json_extract(data,'$.model.id'), ${OC_DB_COLUMNS}, ${OC_DB_TOOLS}` +
|
|
222
228
|
` FROM session_message WHERE type='assistant' AND time_created > ${since}${OC_DB_ORDER}`;
|
|
223
229
|
|
|
224
230
|
// A re-scan window covers tokens that land after the row was created, so a late fill is not lost.
|
|
225
231
|
export const OPENCODE_DB_OVERLAP_MS = 3_600_000;
|
|
226
232
|
|
|
227
|
-
//
|
|
228
|
-
export const SQLITE_READ_BUFFER = 50 * 1024 * 1024;
|
|
229
|
-
|
|
230
|
-
// Empty when sqlite3 is absent or the db is unreadable; that is a sync skip, not a failure.
|
|
233
|
+
// Empty when no SQLite is available or the db is unreadable; that is a sync skip, not a failure.
|
|
231
234
|
export function readOpencodeDbRows(db: string, sinceMs: number): OpencodeDbRow[] {
|
|
232
235
|
if (!Number.isFinite(sinceMs)) return [];
|
|
233
236
|
try {
|
|
234
237
|
if (!fs.existsSync(db)) return [];
|
|
235
238
|
const since = Math.max(0, Math.floor(sinceMs));
|
|
236
239
|
// Separate calls: a db holding only one table must not cancel the other.
|
|
237
|
-
const run = (sql: string): string =>
|
|
238
|
-
execFileSync('sqlite3', ['-separator', '\t', '-readonly', db, sql], { encoding: 'utf8', timeout: 30000, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: SQLITE_READ_BUFFER });
|
|
240
|
+
const run = (sql: string): string => sqliteRead(db, sql);
|
|
239
241
|
let out = '';
|
|
240
242
|
try { out += run(ocDbLegacy(since)); } catch { /* no legacy table or unreadable */ }
|
|
241
243
|
try { out += run(ocDbCurrent(since)); } catch { /* no session_message table or unreadable */ }
|
|
242
244
|
const rows: OpencodeDbRow[] = [];
|
|
243
245
|
for (const line of out.split('\n')) {
|
|
244
246
|
if (!line.trim()) continue;
|
|
245
|
-
const [id, created, role, providerID, modelID, tCreated, tCompleted, input, output, cacheRead, cacheWrite, cost] = line.split('\t');
|
|
247
|
+
const [id, created, role, providerID, modelID, tCreated, tStreamed, tCompleted, input, output, cacheRead, cacheWrite, cost, tools] = line.split('\t');
|
|
246
248
|
const n = (v: string | undefined): unknown => (v !== undefined && v !== '' && Number.isFinite(Number(v)) ? Number(v) : undefined);
|
|
247
249
|
rows.push({
|
|
248
250
|
id,
|
|
249
251
|
created: Number(created),
|
|
252
|
+
tools: tools ?? '',
|
|
250
253
|
row: {
|
|
251
254
|
role,
|
|
252
255
|
modelID,
|
|
253
256
|
providerID,
|
|
254
257
|
tokens: { input: n(input), output: n(output), cache: { read: n(cacheRead), write: n(cacheWrite) } },
|
|
255
|
-
time: { created: n(tCreated), completed: n(tCompleted) },
|
|
258
|
+
time: { created: n(tCreated), streamed: n(tStreamed), completed: n(tCompleted) },
|
|
256
259
|
cost: n(cost),
|
|
257
260
|
},
|
|
258
261
|
});
|
|
@@ -318,7 +321,7 @@ export interface SessionAccum {
|
|
|
318
321
|
export function newSessionAccum(): SessionAccum {
|
|
319
322
|
return { byModel: {}, byDay: {}, byDayModel: {}, byTool: {}, byModelMessages: {}, byHost: {}, byAgent: {}, byAgentMessages: {}, byModelMeasured: {}, byModelCostMessages: {}, totals: zeroBreakdown(), messages: 0, costMeasured: 0, recent: [], recentFloor: Number.POSITIVE_INFINITY };
|
|
320
323
|
}
|
|
321
|
-
export function ingestSessionRow(accum: SessionAccum, model: string, usage: Record<string, unknown>, ts: string | number | undefined, durMs?: unknown, run?: { st: RunStatus; code?: number; note?: string }, toolNames?: string[], host?: string, id?: string, agent?: string): boolean {
|
|
324
|
+
export function ingestSessionRow(accum: SessionAccum, model: string, usage: Record<string, unknown>, ts: string | number | undefined, durMs?: unknown, run?: { st: RunStatus; code?: number; note?: string }, toolNames?: string[], host?: string, id?: string, agent?: string, ttftMs?: unknown): boolean {
|
|
322
325
|
const watermark = readResetWatermark();
|
|
323
326
|
const ms = ts === undefined ? NaN : typeof ts === 'number' ? ts : Date.parse(ts);
|
|
324
327
|
if (watermark > 0 && (!Number.isFinite(ms) || ms < watermark)) return false;
|
|
@@ -342,11 +345,9 @@ export function ingestSessionRow(accum: SessionAccum, model: string, usage: Reco
|
|
|
342
345
|
const num = (v: unknown): number => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0);
|
|
343
346
|
if (Number.isFinite(ms)) {
|
|
344
347
|
const outcome = run ?? { st: 'completed' as RunStatus };
|
|
345
|
-
//
|
|
346
|
-
// anything older than the floor, and re-read the floor when the list is full, so a 50k-row
|
|
347
|
-
// store costs 2000 rows of memory instead of 50k.
|
|
348
|
+
// Only the newest rows are read: skip below the floor, and a 50k-row store costs 2000 rows of memory.
|
|
348
349
|
if (accum.recent.length < RECENT_LIMIT || ms >= accum.recentFloor) {
|
|
349
|
-
accum.recent.push({ m: key, i: num(usage.input), o: num(usage.output), t: ms, d: durOf(durMs), h: host, cr: num(usage.cacheRead), cw: num(usage.cacheWrite), usd: costOf(usage), st: outcome.st, code: outcome.code, note: outcome.note, id });
|
|
350
|
+
accum.recent.push({ m: key, i: num(usage.input), o: num(usage.output), t: ms, d: durOf(durMs), tf: durOf(ttftMs), h: host, cr: num(usage.cacheRead), cw: num(usage.cacheWrite), usd: costOf(usage), st: outcome.st, code: outcome.code, note: outcome.note, id });
|
|
350
351
|
if (accum.recent.length > RECENT_LIMIT) {
|
|
351
352
|
let drop = 0;
|
|
352
353
|
for (let k = 1; k < accum.recent.length; k += 1) if (accum.recent[k].t < accum.recent[drop].t) drop = k;
|
|
@@ -378,7 +379,7 @@ export function ingestSessionRow(accum: SessionAccum, model: string, usage: Reco
|
|
|
378
379
|
return true;
|
|
379
380
|
}
|
|
380
381
|
export type SessionLineKind = 'codex_provider' | 'codex_model' | 'token_row' | 'assistant_row' | 'skip';
|
|
381
|
-
export function classifySessionLine(row: { timestamp?: string | number; type?: unknown; message?: { role?: unknown; model?: unknown; usage?: Record<string, unknown>; duration?: unknown; completedAt?: unknown; timestamp?: unknown; stopReason?: unknown; errorStatus?: unknown; errorMessage?: unknown; isError?: unknown; content?: Array<{ type?: unknown; name?: unknown; arguments?: unknown }> }; payload?: { type?: unknown; model?: unknown; model_provider?: unknown; info?: { last_token_usage?: Record<string, unknown> } } }): { kind: SessionLineKind; provider?: string; model?: string; usage?: Record<string, unknown>; durMs?: number; run?: { st: RunStatus; code?: number; note?: string }; tools?: string[]; ms?: number } {
|
|
382
|
+
export function classifySessionLine(row: { timestamp?: string | number; type?: unknown; message?: { role?: unknown; model?: unknown; usage?: Record<string, unknown>; duration?: unknown; ttft?: unknown; completedAt?: unknown; timestamp?: unknown; stopReason?: unknown; errorStatus?: unknown; errorMessage?: unknown; isError?: unknown; content?: Array<{ type?: unknown; name?: unknown; arguments?: unknown }> }; payload?: { type?: unknown; model?: unknown; model_provider?: unknown; info?: { last_token_usage?: Record<string, unknown> } } }): { kind: SessionLineKind; provider?: string; model?: string; usage?: Record<string, unknown>; durMs?: number; ttftMs?: number; run?: { st: RunStatus; code?: number; note?: string }; tools?: string[]; ms?: number } {
|
|
382
383
|
const payload = row.payload;
|
|
383
384
|
if (payload && typeof payload === 'object') {
|
|
384
385
|
if (row.type === 'session_meta' && typeof payload.model_provider === 'string') return { kind: 'codex_provider', provider: payload.model_provider };
|
|
@@ -412,7 +413,7 @@ export function classifySessionLine(row: { timestamp?: string | number; type?: u
|
|
|
412
413
|
const command = (part.arguments as { command?: unknown } | null)?.command;
|
|
413
414
|
tools.push(`bash:${leadBinary(command)}`);
|
|
414
415
|
}
|
|
415
|
-
return { kind: 'assistant_row', model, usage: msg.usage, durMs: typeof msg.duration === 'number' ? msg.duration : computedDur, run: statusOf(msg), tools };
|
|
416
|
+
return { kind: 'assistant_row', model, usage: msg.usage, durMs: typeof msg.duration === 'number' ? msg.duration : computedDur, ttftMs: typeof msg.ttft === 'number' ? msg.ttft : undefined, run: statusOf(msg), tools };
|
|
416
417
|
}
|
|
417
418
|
// True when at least one token bucket holds a positive finite count.
|
|
418
419
|
export function hasPositiveUsage(usage: Record<string, unknown>): boolean {
|
|
@@ -428,13 +429,30 @@ export interface OpencodeMessage {
|
|
|
428
429
|
tokens?: { input?: unknown; output?: unknown; reasoning?: unknown; cache?: { read?: unknown; write?: unknown } } | null;
|
|
429
430
|
modelID?: unknown;
|
|
430
431
|
providerID?: unknown;
|
|
431
|
-
time?: { created?: unknown; completed?: unknown } | null;
|
|
432
|
+
time?: { created?: unknown; streamed?: unknown; completed?: unknown } | null;
|
|
432
433
|
cost?: unknown;
|
|
433
434
|
finish?: unknown;
|
|
435
|
+
/** The message's parts. Tool calls sit here, named under a part of type `tool`. */
|
|
436
|
+
content?: unknown;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Tool names an OpenCode message called; v2 passes SQL's comma-joined string as the blob holds newlines. */
|
|
440
|
+
export function opencodeToolNames(message: OpencodeMessage, joined?: string): string[] {
|
|
441
|
+
if (joined !== undefined) return joined === '' ? [] : joined.split(',');
|
|
442
|
+
const parts = message?.content;
|
|
443
|
+
if (!Array.isArray(parts)) return [];
|
|
444
|
+
const names: string[] = [];
|
|
445
|
+
for (const part of parts) {
|
|
446
|
+
if (!part || typeof part !== 'object') continue;
|
|
447
|
+
if ((part as Record<string, unknown>).type !== 'tool') continue;
|
|
448
|
+
const name = (part as Record<string, unknown>).name;
|
|
449
|
+
if (typeof name === 'string' && name) names.push(name);
|
|
450
|
+
}
|
|
451
|
+
return names;
|
|
434
452
|
}
|
|
435
453
|
|
|
436
454
|
// One OpenCode message file; assistant rows carry the token buckets.
|
|
437
|
-
export function classifyOpencodeMessage(obj: OpencodeMessage | null | undefined): { model: string; usage: Record<string, unknown>; ms: number | undefined; durMs: number | undefined } | null {
|
|
455
|
+
export function classifyOpencodeMessage(obj: OpencodeMessage | null | undefined): { model: string; usage: Record<string, unknown>; ms: number | undefined; durMs: number | undefined; ttftMs: number | undefined } | null {
|
|
438
456
|
if (!obj || typeof obj !== 'object' || obj.role !== 'assistant') return null;
|
|
439
457
|
const t = obj.tokens;
|
|
440
458
|
if (!t || typeof t !== 'object') return null;
|
|
@@ -451,15 +469,17 @@ export function classifyOpencodeMessage(obj: OpencodeMessage | null | undefined)
|
|
|
451
469
|
const num = (v: unknown): number | undefined => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : undefined);
|
|
452
470
|
const created = num(obj.time?.created);
|
|
453
471
|
const completed = num(obj.time?.completed);
|
|
472
|
+
// `streamed` is when the first token arrived, so it is the host's own first-token latency.
|
|
473
|
+
const streamed = num(obj.time?.streamed);
|
|
454
474
|
return {
|
|
455
475
|
model: `opencode/${provider}/${model}`,
|
|
456
476
|
usage,
|
|
457
477
|
ms: created,
|
|
458
478
|
durMs: created !== undefined && completed !== undefined && completed > created ? completed - created : undefined,
|
|
479
|
+
ttftMs: created !== undefined && streamed !== undefined && streamed > created ? streamed - created : undefined,
|
|
459
480
|
};
|
|
460
481
|
}
|
|
461
482
|
|
|
462
|
-
// One OpenCode message file into the shared accum. The msg id is the file name.
|
|
463
483
|
export function processOpencodeFile(accum: SessionAccum, text: string, file?: string): void {
|
|
464
484
|
let obj: unknown;
|
|
465
485
|
try {
|
|
@@ -467,17 +487,18 @@ export function processOpencodeFile(accum: SessionAccum, text: string, file?: st
|
|
|
467
487
|
} catch {
|
|
468
488
|
return;
|
|
469
489
|
}
|
|
470
|
-
const
|
|
490
|
+
const message = obj as OpencodeMessage;
|
|
491
|
+
const parsed = classifyOpencodeMessage(message);
|
|
471
492
|
if (!parsed) return;
|
|
472
493
|
const id = file ? path.basename(file).replace(/\.[^.]+$/, '') || undefined : undefined;
|
|
473
|
-
ingestSessionRow(accum, parsed.model, parsed.usage, parsed.ms, parsed.durMs, { st: 'completed' as RunStatus },
|
|
494
|
+
ingestSessionRow(accum, parsed.model, parsed.usage, parsed.ms, parsed.durMs, { st: 'completed' as RunStatus }, opencodeToolNames(message), 'opencode', id, 'main', parsed.ttftMs);
|
|
474
495
|
}
|
|
475
496
|
|
|
476
497
|
// One v2 message row into the shared accum. The msg id is the row primary key.
|
|
477
498
|
export function ingestOpencodeDbRow(accum: SessionAccum, row: OpencodeDbRow): void {
|
|
478
499
|
const parsed = classifyOpencodeMessage(row.row);
|
|
479
500
|
if (!parsed) return;
|
|
480
|
-
ingestSessionRow(accum, parsed.model, parsed.usage, parsed.ms, parsed.durMs, { st: 'completed' as RunStatus },
|
|
501
|
+
ingestSessionRow(accum, parsed.model, parsed.usage, parsed.ms, parsed.durMs, { st: 'completed' as RunStatus }, opencodeToolNames(row.row, row.tools), 'opencode', row.id, 'main', parsed.ttftMs);
|
|
481
502
|
}
|
|
482
503
|
|
|
483
504
|
// Fold `:free`/`-free` suffixes and case variants into one chart key.
|
|
@@ -490,8 +511,7 @@ export function messageRowId(row: unknown): string | undefined {
|
|
|
490
511
|
return typeof id === 'string' && id ? id : undefined;
|
|
491
512
|
}
|
|
492
513
|
|
|
493
|
-
// Vendor names mirror the dashboard map;
|
|
494
|
-
// gateway segments shadow it (minimaxai holds xai, gemma rides nvidia).
|
|
514
|
+
// Vendor names mirror the dashboard map; family first, so gateway segments shadow it (minimaxai holds xai).
|
|
495
515
|
const VENDOR_RES: Array<[RegExp, string]> = [
|
|
496
516
|
[/inclusionai|ling-/i, 'InclusionAI'],
|
|
497
517
|
[/minimax/i, 'MiniMax'],
|
|
@@ -617,9 +637,7 @@ export function hostOfSessionFile(file: string): string {
|
|
|
617
637
|
return 'pi';
|
|
618
638
|
}
|
|
619
639
|
|
|
620
|
-
//
|
|
621
|
-
// session is a subagent's, and everything else is the main agent. The row is the host's own
|
|
622
|
-
// record, so a mirrored transcript carries the same answer as the stats database.
|
|
640
|
+
// A transcript with a parent session ran as a subagent; the host's own row, so a mirror answers the same.
|
|
623
641
|
export function agentOfSessionText(text: string): string {
|
|
624
642
|
if (!text.includes('parentSession')) return 'main';
|
|
625
643
|
for (const line of text.split('\n')) {
|
|
@@ -725,7 +743,7 @@ export function processSessionText(accum: SessionAccum, state: { codexProvider:
|
|
|
725
743
|
continue;
|
|
726
744
|
}
|
|
727
745
|
if (parsed.kind !== 'assistant_row' || !parsed.model || !parsed.usage) continue;
|
|
728
|
-
ingestSessionRow(accum, parsed.model, parsed.usage, row.timestamp, parsed.durMs, parsed.run, parsed.tools, host, rid, agent);
|
|
746
|
+
ingestSessionRow(accum, parsed.model, parsed.usage, row.timestamp, parsed.durMs, parsed.run, parsed.tools, host, rid, agent, parsed.ttftMs);
|
|
729
747
|
} catch { /* skip corrupt lines */ }
|
|
730
748
|
}
|
|
731
749
|
}
|
|
@@ -762,7 +780,7 @@ export function leadBinary(command: unknown): string {
|
|
|
762
780
|
return 'bash';
|
|
763
781
|
}
|
|
764
782
|
|
|
765
|
-
//
|
|
783
|
+
// Pricing lives in ./pricing.ts (live LiteLLM cache + fallback table).
|
|
766
784
|
import { DEFAULT_PRICE, PRICE_TIERS_FIRM, canonicalPriceId, priceFor, pricesOverridePath, refreshPricesIfStale, type PriceHit, type PriceTier } from './pricing.ts';
|
|
767
785
|
import { co2GramsFor, energyWhFor } from './carbon.ts';
|
|
768
786
|
import type { ModelPrice } from './pricing.ts';
|
|
@@ -771,11 +789,7 @@ export { DEFAULT_PRICE, PRICE_TIERS_FIRM, priceFor, pricesOverridePath, refreshP
|
|
|
771
789
|
export { co2GramsFor, energyWhFor };
|
|
772
790
|
export type { PriceHit, PriceTier, ModelPrice };
|
|
773
791
|
|
|
774
|
-
/**
|
|
775
|
-
* Price one model's tokens. `measured` is the cost the host reported for those tokens; it outranks
|
|
776
|
-
* every rate card, because it is the only figure from the party that billed. Pass undefined rather
|
|
777
|
-
* than 0 for a cost nobody reported: `costOf` keeps the two apart.
|
|
778
|
-
*/
|
|
792
|
+
/** Price one model's tokens; `measured` outranks every rate card, so pass undefined when unreported. */
|
|
779
793
|
export function usdCost(t: TokenBreakdown, model?: string, measured?: number): { usd: number; priced: boolean; tier: PriceTier; buckets: { input: number; output: number; cacheRead: number; cacheWrite: number } } {
|
|
780
794
|
const m = 1 / 1_000_000;
|
|
781
795
|
if (measured !== undefined) {
|
|
@@ -797,8 +811,7 @@ export function usdCost(t: TokenBreakdown, model?: string, measured?: number): {
|
|
|
797
811
|
};
|
|
798
812
|
return {
|
|
799
813
|
usd: buckets.input + buckets.output + buckets.cacheRead + buckets.cacheWrite,
|
|
800
|
-
//
|
|
801
|
-
// and a report that sums them beside measured cost reads as a bill it is not.
|
|
814
|
+
// `tail`/`default` are estimates; summing them beside measured cost reads as a bill it is not.
|
|
802
815
|
priced: PRICE_TIERS_FIRM.includes(hit.tier),
|
|
803
816
|
tier: hit.tier,
|
|
804
817
|
buckets,
|
|
@@ -829,7 +842,7 @@ export function readUsage(): UsageRow[] {
|
|
|
829
842
|
return rows;
|
|
830
843
|
}
|
|
831
844
|
|
|
832
|
-
// Delete the ledger file and return the rows cleared. Tersio-owned data only: transcripts and the RTK database
|
|
845
|
+
// Delete the ledger file and return the rows cleared. Tersio-owned data only: transcripts and the RTK database stay.
|
|
833
846
|
export function clearUsageLedger(): number {
|
|
834
847
|
const rows = readUsage().length;
|
|
835
848
|
try {
|