codexmeter 1.0.0 → 1.0.2
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/bin/codexmeter.js +1 -0
- package/dist/assets/index-C0qLYGUY.css +1 -0
- package/dist/assets/index-M87czq50.js +113 -0
- package/dist/index.html +2 -2
- package/package.json +5 -2
- package/server/aggregator.js +79 -7
- package/server/cost-catalog.js +1 -1
- package/server/export-replay.js +80 -0
- package/server/export-video.js +313 -0
- package/server/index.js +68 -1
- package/server/ingest.js +147 -42
- package/server/live-state.js +61 -2
- package/server/rollout-reader.js +87 -5
- package/src/utils/animationsDefault.js +244 -0
- package/dist/assets/index-DJWqyRDh.css +0 -1
- package/dist/assets/index-DZKogILW.js +0 -113
package/server/ingest.js
CHANGED
|
@@ -4,22 +4,19 @@ import {
|
|
|
4
4
|
normalizeCwd, deriveRepoKey, deriveRepoLabel,
|
|
5
5
|
classifyAgentFamily, isSubagent, normalizeModelName,
|
|
6
6
|
} from './normalize.js';
|
|
7
|
-
import { initPricing, priceSession } from './cost-catalog.js';
|
|
7
|
+
import { calculateCostFromUsage, initPricing, priceSession } from './cost-catalog.js';
|
|
8
8
|
import { buildAggregates, buildSessionView } from './aggregator.js';
|
|
9
9
|
import { createDayKeyFormatter } from './day-key.js';
|
|
10
10
|
import { createLiveAggregateState, createEmptyLivePatch, applySessionToLiveState, buildLiveBootstrap, buildLivePatch } from './live-state.js';
|
|
11
11
|
import { createRolloutWorkerPool } from './rollout-worker-pool.js';
|
|
12
|
+
import { beginReplayCapture, createReplayCaptureState, failReplayCapture, getReplaySnapshot, recordReplayEvent, resetReplayCapture } from './export-replay.js';
|
|
13
|
+
import { OVERVIEW_INGEST_ANIMATION } from '../src/utils/animationsDefault.js';
|
|
12
14
|
|
|
13
15
|
const LIVE_FRAME_INTERVAL_MS = 50;
|
|
14
16
|
const LIVE_DAYS_PER_SECOND = 6;
|
|
15
|
-
const
|
|
17
|
+
const LIVE_OVERVIEW_CADENCE_MS = Math.round(1000 / 10);
|
|
16
18
|
const LIVE_DAY_KEYS_PER_EMIT = 1;
|
|
17
|
-
const
|
|
18
|
-
overview: LIVE_FRAME_INTERVAL_MS * 2,
|
|
19
|
-
rankings: LIVE_FRAME_INTERVAL_MS * 3,
|
|
20
|
-
daily: LIVE_DAY_CADENCE_MS,
|
|
21
|
-
heatmap: LIVE_DAY_CADENCE_MS,
|
|
22
|
-
};
|
|
19
|
+
const LIVE_SESSION_REORDER_BUFFER = 160;
|
|
23
20
|
|
|
24
21
|
export function createIngestState() {
|
|
25
22
|
return {
|
|
@@ -43,9 +40,11 @@ export function createIngestState() {
|
|
|
43
40
|
live_subscribers: new Set(),
|
|
44
41
|
live_pump_timer: null,
|
|
45
42
|
live_pending_patch: createEmptyLivePatch(),
|
|
43
|
+
live_session_buffer: [],
|
|
46
44
|
live_progress_dirty: false,
|
|
47
45
|
live_last_emit_at: 0,
|
|
48
|
-
|
|
46
|
+
live_last_overview_emit_at: 0,
|
|
47
|
+
replay_capture: createReplayCaptureState(),
|
|
49
48
|
};
|
|
50
49
|
}
|
|
51
50
|
|
|
@@ -60,6 +59,12 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
60
59
|
state.live_state = createLiveAggregateState(tz);
|
|
61
60
|
state.phase = 'inventory';
|
|
62
61
|
state.percent = 0;
|
|
62
|
+
beginReplayCapture(state.replay_capture, state.ingest_id, {
|
|
63
|
+
ingest_id: state.ingest_id,
|
|
64
|
+
seq: 0,
|
|
65
|
+
progress: progressPayload(state),
|
|
66
|
+
data: buildLiveBootstrap(state.live_state),
|
|
67
|
+
});
|
|
63
68
|
queueLiveProgress(state);
|
|
64
69
|
broadcastBootstrap(state);
|
|
65
70
|
|
|
@@ -98,6 +103,9 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
98
103
|
model_name: null,
|
|
99
104
|
reasoning_effort: null,
|
|
100
105
|
usage_total: null,
|
|
106
|
+
usage_by_day: null,
|
|
107
|
+
has_usage_by_day: false,
|
|
108
|
+
live_sort_ts: null,
|
|
101
109
|
active_by_day: null,
|
|
102
110
|
agent_role: t.agent_role,
|
|
103
111
|
agent_nickname: t.agent_nickname,
|
|
@@ -132,7 +140,7 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
132
140
|
|
|
133
141
|
state.needs_enrichment = candidates.length;
|
|
134
142
|
state.percent = candidates.length > 0 ? 0.08 : 0.90;
|
|
135
|
-
const BATCH_SIZE = opts.batchSize ||
|
|
143
|
+
const BATCH_SIZE = opts.batchSize || 40;
|
|
136
144
|
const ROOT_REFRESH_EVERY = opts.rootRefreshEvery || 1000;
|
|
137
145
|
let lastRootRefreshCount = 0;
|
|
138
146
|
|
|
@@ -160,12 +168,20 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
160
168
|
if (data.reasoning_effort) s.reasoning_effort = data.reasoning_effort;
|
|
161
169
|
if (data.parent_thread_id) s.parent_thread_id = data.parent_thread_id;
|
|
162
170
|
if (data.usage_total) s.usage_total = data.usage_total;
|
|
171
|
+
if (data.usage_by_day) {
|
|
172
|
+
s.usage_by_day = buildUsageByDayMetrics(s.model_name, data.usage_by_day);
|
|
173
|
+
s.has_usage_by_day = s.usage_by_day.length > 0;
|
|
174
|
+
}
|
|
175
|
+
if (data.first_usage_timestamp) {
|
|
176
|
+
s.live_sort_ts = Math.floor(data.first_usage_timestamp / 1000);
|
|
177
|
+
}
|
|
163
178
|
if (data.active_seconds && data.active_seconds > 0) {
|
|
164
179
|
s.elapsed_seconds = data.active_seconds;
|
|
165
180
|
s.active_by_day = data.active_by_day || null;
|
|
166
181
|
}
|
|
167
182
|
}
|
|
168
183
|
finalizeSessionMetrics(s, toDayKey);
|
|
184
|
+
s.live_sort_day = deriveLiveSortDay(s, toDayKey);
|
|
169
185
|
s.materialized = true;
|
|
170
186
|
}
|
|
171
187
|
|
|
@@ -177,8 +193,14 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
177
193
|
assignRootThreadIds(sessions);
|
|
178
194
|
lastRootRefreshCount = state.enriched;
|
|
179
195
|
}
|
|
196
|
+
const liveReady = bufferLiveSessionsForPresentation(state, batch);
|
|
197
|
+
if (state.live_session_buffer[0]?.live_sort_day) {
|
|
198
|
+
state.current_date_bucket = state.live_session_buffer[0].live_sort_day;
|
|
199
|
+
} else if (liveReady[0]?.live_sort_day) {
|
|
200
|
+
state.current_date_bucket = liveReady[0].live_sort_day;
|
|
201
|
+
}
|
|
180
202
|
const livePatch = createEmptyLivePatch();
|
|
181
|
-
for (const session of
|
|
203
|
+
for (const session of liveReady) {
|
|
182
204
|
applySessionToLiveState(state.live_state, session, livePatch);
|
|
183
205
|
}
|
|
184
206
|
queueLivePatch(state, livePatch);
|
|
@@ -197,6 +219,11 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
197
219
|
}
|
|
198
220
|
|
|
199
221
|
assignRootThreadIds(sessions);
|
|
222
|
+
const finalLivePatch = createEmptyLivePatch();
|
|
223
|
+
for (const session of flushBufferedLiveSessions(state)) {
|
|
224
|
+
applySessionToLiveState(state.live_state, session, finalLivePatch);
|
|
225
|
+
}
|
|
226
|
+
queueLivePatch(state, finalLivePatch);
|
|
200
227
|
|
|
201
228
|
state.phase = 'aggregation';
|
|
202
229
|
state.percent = state.needs_enrichment > 0 ? 0.95 : 0.90;
|
|
@@ -216,6 +243,7 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
216
243
|
if (!isCurrentRun()) return;
|
|
217
244
|
state.error = err.message;
|
|
218
245
|
state.phase = 'error';
|
|
246
|
+
failReplayCapture(state.replay_capture);
|
|
219
247
|
console.error('Ingest error:', err);
|
|
220
248
|
flushLive(state, 'ingest-error');
|
|
221
249
|
} finally {
|
|
@@ -247,9 +275,11 @@ export function restartIngest(codexHome, state, opts = {}) {
|
|
|
247
275
|
state.live_state = null;
|
|
248
276
|
state.live_seq = 0;
|
|
249
277
|
state.live_pending_patch = createEmptyLivePatch();
|
|
278
|
+
state.live_session_buffer = [];
|
|
250
279
|
state.live_progress_dirty = false;
|
|
251
280
|
state.live_last_emit_at = 0;
|
|
252
|
-
state.
|
|
281
|
+
state.live_last_overview_emit_at = 0;
|
|
282
|
+
resetReplayCapture(state.replay_capture);
|
|
253
283
|
|
|
254
284
|
return runIngest(codexHome, state, opts);
|
|
255
285
|
}
|
|
@@ -352,6 +382,64 @@ function finalizeSessionMetrics(session, toDayKey) {
|
|
|
352
382
|
}
|
|
353
383
|
}
|
|
354
384
|
|
|
385
|
+
function buildUsageByDayMetrics(modelName, usageByDay) {
|
|
386
|
+
const entries = [];
|
|
387
|
+
for (const [dayKey, usage] of Object.entries(usageByDay || {})) {
|
|
388
|
+
const tokens = (usage?.input_tokens || 0) + (usage?.output_tokens || 0);
|
|
389
|
+
const cost = calculateCostFromUsage(modelName, usage);
|
|
390
|
+
entries.push({
|
|
391
|
+
day: dayKey,
|
|
392
|
+
tokens,
|
|
393
|
+
cost,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
entries.sort((a, b) => a.day.localeCompare(b.day));
|
|
397
|
+
return entries;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function deriveLiveSortDay(session, toDayKey) {
|
|
401
|
+
if (session.has_usage_by_day && session.usage_by_day?.length) {
|
|
402
|
+
let best = session.usage_by_day[0];
|
|
403
|
+
for (const entry of session.usage_by_day) {
|
|
404
|
+
if ((entry.tokens || 0) > (best.tokens || 0)) {
|
|
405
|
+
best = entry;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if ((entry.tokens || 0) === (best.tokens || 0) && String(entry.day) > String(best.day)) {
|
|
409
|
+
best = entry;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return best?.day || null;
|
|
413
|
+
}
|
|
414
|
+
if (session.started_at) {
|
|
415
|
+
return toDayKey(session.started_at * 1000);
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function compareLiveSessionOrder(a, b) {
|
|
421
|
+
const dayCompare = String(a.live_sort_day || '9999-12-31').localeCompare(String(b.live_sort_day || '9999-12-31'));
|
|
422
|
+
if (dayCompare !== 0) return dayCompare;
|
|
423
|
+
const liveTsCompare = (a.live_sort_ts || a.started_at || 0) - (b.live_sort_ts || b.started_at || 0);
|
|
424
|
+
if (liveTsCompare !== 0) return liveTsCompare;
|
|
425
|
+
return String(a.thread_id || '').localeCompare(String(b.thread_id || ''));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function bufferLiveSessionsForPresentation(state, sessions) {
|
|
429
|
+
if (!sessions.length) return [];
|
|
430
|
+
state.live_session_buffer.push(...sessions);
|
|
431
|
+
state.live_session_buffer.sort(compareLiveSessionOrder);
|
|
432
|
+
const flushCount = Math.max(0, state.live_session_buffer.length - LIVE_SESSION_REORDER_BUFFER);
|
|
433
|
+
if (flushCount <= 0) return [];
|
|
434
|
+
return state.live_session_buffer.splice(0, flushCount);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function flushBufferedLiveSessions(state) {
|
|
438
|
+
if (!state.live_session_buffer.length) return [];
|
|
439
|
+
state.live_session_buffer.sort(compareLiveSessionOrder);
|
|
440
|
+
return state.live_session_buffer.splice(0, state.live_session_buffer.length);
|
|
441
|
+
}
|
|
442
|
+
|
|
355
443
|
function queueLiveProgress(state) {
|
|
356
444
|
state.live_progress_dirty = true;
|
|
357
445
|
ensureLivePump(state);
|
|
@@ -364,13 +452,14 @@ function queueLivePatch(state, patch) {
|
|
|
364
452
|
}
|
|
365
453
|
|
|
366
454
|
function ensureLivePump(state) {
|
|
367
|
-
if (state.live_pump_timer
|
|
455
|
+
if (state.live_pump_timer) return;
|
|
456
|
+
if (!state.live_subscribers.size && !state.replay_capture.active) return;
|
|
368
457
|
state.live_pump_timer = setInterval(() => flushLive(state), LIVE_FRAME_INTERVAL_MS);
|
|
369
458
|
}
|
|
370
459
|
|
|
371
460
|
function stopLivePumpIfIdle(state) {
|
|
372
461
|
if (!state.live_pump_timer) return;
|
|
373
|
-
if (state.live_subscribers.size) return;
|
|
462
|
+
if (state.live_subscribers.size || state.replay_capture.active) return;
|
|
374
463
|
clearInterval(state.live_pump_timer);
|
|
375
464
|
state.live_pump_timer = null;
|
|
376
465
|
finalizeWithoutSubscribers(state);
|
|
@@ -378,6 +467,7 @@ function stopLivePumpIfIdle(state) {
|
|
|
378
467
|
|
|
379
468
|
function finalizeWithoutSubscribers(state) {
|
|
380
469
|
if (state.live_subscribers.size) return;
|
|
470
|
+
if (state.replay_capture.active) return;
|
|
381
471
|
if (!state.presentation_complete_pending) return;
|
|
382
472
|
state.phase = 'complete';
|
|
383
473
|
state.percent = 1;
|
|
@@ -388,7 +478,7 @@ function finalizeWithoutSubscribers(state) {
|
|
|
388
478
|
}
|
|
389
479
|
|
|
390
480
|
function flushLive(state, forcedEvent = null) {
|
|
391
|
-
if (!state.live_subscribers.size) {
|
|
481
|
+
if (!state.live_subscribers.size && !state.replay_capture.active) {
|
|
392
482
|
state.live_pending_patch = createEmptyLivePatch();
|
|
393
483
|
state.live_progress_dirty = false;
|
|
394
484
|
stopLivePumpIfIdle(state);
|
|
@@ -428,12 +518,16 @@ function flushLive(state, forcedEvent = null) {
|
|
|
428
518
|
payload.data = buildLiveBootstrap(state.live_state);
|
|
429
519
|
}
|
|
430
520
|
|
|
431
|
-
|
|
521
|
+
recordReplayEvent(state.replay_capture, event, payload);
|
|
522
|
+
if (state.live_subscribers.size) {
|
|
523
|
+
broadcastLive(state, event, payload);
|
|
524
|
+
}
|
|
432
525
|
state.live_last_emit_at = Date.now();
|
|
433
526
|
state.live_progress_dirty = shouldEmitComplete ? false : (event === 'progress' ? false : state.live_progress_dirty);
|
|
434
527
|
if (forcedEvent) {
|
|
435
528
|
state.live_pending_patch = createEmptyLivePatch();
|
|
436
529
|
}
|
|
530
|
+
stopLivePumpIfIdle(state);
|
|
437
531
|
}
|
|
438
532
|
|
|
439
533
|
function broadcastLive(state, event, payload) {
|
|
@@ -475,41 +569,48 @@ function takeFlushablePatch(state) {
|
|
|
475
569
|
const now = Date.now();
|
|
476
570
|
const sent = createEmptyLivePatch();
|
|
477
571
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
state.live_last_surface_emit_at.overview = now;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const rankingsDirty =
|
|
572
|
+
const overviewDirty =
|
|
573
|
+
state.live_pending_patch.overview.size > 0 ||
|
|
484
574
|
state.live_pending_patch.repos.total.size > 0 || state.live_pending_patch.repos.d7.size > 0 || state.live_pending_patch.repos.d30.size > 0 ||
|
|
485
575
|
state.live_pending_patch.models.total.size > 0 || state.live_pending_patch.models.d7.size > 0 || state.live_pending_patch.models.d30.size > 0 ||
|
|
486
|
-
state.live_pending_patch.families.total.size > 0 || state.live_pending_patch.families.d7.size > 0 || state.live_pending_patch.families.d30.size > 0
|
|
576
|
+
state.live_pending_patch.families.total.size > 0 || state.live_pending_patch.families.d7.size > 0 || state.live_pending_patch.families.d30.size > 0 ||
|
|
577
|
+
state.live_pending_patch.daily.size > 0 || state.live_pending_patch.heatmap.size > 0;
|
|
487
578
|
|
|
488
|
-
if (
|
|
489
|
-
|
|
490
|
-
moveRangeSets(state.live_pending_patch.models, sent.models);
|
|
491
|
-
moveRangeSets(state.live_pending_patch.families, sent.families);
|
|
492
|
-
state.live_last_surface_emit_at.rankings = now;
|
|
579
|
+
if (!overviewDirty || !readyForOverview(state, now)) {
|
|
580
|
+
return sent;
|
|
493
581
|
}
|
|
494
582
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
583
|
+
moveSet(state.live_pending_patch.overview, sent.overview);
|
|
584
|
+
moveRangeSets(state.live_pending_patch.repos, sent.repos);
|
|
585
|
+
moveRangeSets(state.live_pending_patch.models, sent.models);
|
|
586
|
+
moveRangeSets(state.live_pending_patch.families, sent.families);
|
|
587
|
+
|
|
588
|
+
const nextDayKeys = takeNextChronologicalDayKeys(
|
|
589
|
+
state.live_pending_patch.daily,
|
|
590
|
+
state.live_pending_patch.heatmap,
|
|
591
|
+
LIVE_DAY_KEYS_PER_EMIT
|
|
592
|
+
);
|
|
593
|
+
moveSpecificKeys(state.live_pending_patch.daily, sent.daily, nextDayKeys);
|
|
594
|
+
moveSpecificKeys(state.live_pending_patch.heatmap, sent.heatmap, nextDayKeys);
|
|
595
|
+
state.live_last_overview_emit_at = now;
|
|
507
596
|
|
|
508
597
|
return sent;
|
|
509
598
|
}
|
|
510
599
|
|
|
511
|
-
function
|
|
512
|
-
return (now - state.
|
|
600
|
+
function readyForOverview(state, now) {
|
|
601
|
+
return (now - state.live_last_overview_emit_at) >= getOverviewCadenceMs(state);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function getOverviewCadenceMs(state) {
|
|
605
|
+
const tail = OVERVIEW_INGEST_ANIMATION.tail;
|
|
606
|
+
const tailStartPercent = Math.min(Math.max(tail?.startPercent ?? 0.95, 0), 0.999);
|
|
607
|
+
const tailHz = Number.isFinite(tail?.overviewHz) && tail.overviewHz > 0 ? tail.overviewHz : 5;
|
|
608
|
+
const tailCadenceMs = Math.round(1000 / tailHz);
|
|
609
|
+
|
|
610
|
+
if (tail?.enabled && (state.presentation_complete_pending || (state.percent || 0) >= tailStartPercent)) {
|
|
611
|
+
return tailCadenceMs;
|
|
612
|
+
}
|
|
613
|
+
return LIVE_OVERVIEW_CADENCE_MS;
|
|
513
614
|
}
|
|
514
615
|
|
|
515
616
|
function moveSet(from, to) {
|
|
@@ -581,6 +682,10 @@ export function detachLiveSubscriber(state, res) {
|
|
|
581
682
|
stopLivePumpIfIdle(state);
|
|
582
683
|
}
|
|
583
684
|
|
|
685
|
+
export function getLatestReplay(state) {
|
|
686
|
+
return getReplaySnapshot(state.replay_capture);
|
|
687
|
+
}
|
|
688
|
+
|
|
584
689
|
function broadcastBootstrap(state) {
|
|
585
690
|
if (!state.live_subscribers.size) return;
|
|
586
691
|
const payload = {
|
package/server/live-state.js
CHANGED
|
@@ -217,7 +217,10 @@ function applyDailyBucket(dayMap, session, toDayKey, dirtySet) {
|
|
|
217
217
|
const startDay = toDayKey(startMs);
|
|
218
218
|
const endDay = toDayKey(endMs - 1);
|
|
219
219
|
|
|
220
|
-
if (
|
|
220
|
+
if (session.has_usage_by_day) {
|
|
221
|
+
addSessionPresence(dayMap, startDay, endDay, startMs, endMs, totalDur, toDayKey, session, dirtySet);
|
|
222
|
+
addUsageByDay(dayMap, session, dirtySet);
|
|
223
|
+
} else if (startDay === endDay) {
|
|
221
224
|
addToDay(dayMap, startDay, session, 1.0);
|
|
222
225
|
dirtySet.add(startDay);
|
|
223
226
|
} else {
|
|
@@ -247,7 +250,21 @@ function applyDailyBucket(dayMap, session, toDayKey, dirtySet) {
|
|
|
247
250
|
}
|
|
248
251
|
|
|
249
252
|
function applyHeatmapBucket(dayMap, session, toDayKey, dirtySet) {
|
|
250
|
-
if (session.
|
|
253
|
+
if (session.has_usage_by_day) {
|
|
254
|
+
for (const usageDay of session.usage_by_day || []) {
|
|
255
|
+
const dayKey = usageDay.day;
|
|
256
|
+
const day = ensureHeatmapDay(dayMap, dayKey);
|
|
257
|
+
day.tokens += usageDay.tokens || 0;
|
|
258
|
+
if (usageDay.cost !== null) day.cost += usageDay.cost;
|
|
259
|
+
dirtySet.add(dayKey);
|
|
260
|
+
}
|
|
261
|
+
if (session.started_at) {
|
|
262
|
+
const startDay = toDayKey(session.started_at * 1000);
|
|
263
|
+
const day = ensureHeatmapDay(dayMap, startDay);
|
|
264
|
+
day.sessions += 1;
|
|
265
|
+
dirtySet.add(startDay);
|
|
266
|
+
}
|
|
267
|
+
} else if (session.started_at) {
|
|
251
268
|
const dayKey = toDayKey(session.started_at * 1000);
|
|
252
269
|
const day = ensureHeatmapDay(dayMap, dayKey);
|
|
253
270
|
day.tokens += session.tokens_used || 0;
|
|
@@ -265,6 +282,48 @@ function applyHeatmapBucket(dayMap, session, toDayKey, dirtySet) {
|
|
|
265
282
|
}
|
|
266
283
|
}
|
|
267
284
|
|
|
285
|
+
function addSessionPresence(dayMap, startDay, endDay, startMs, endMs, totalDur, toDayKey, session, dirtySet) {
|
|
286
|
+
if (startDay === endDay) {
|
|
287
|
+
addPresenceToDay(dayMap, startDay, session, 1.0);
|
|
288
|
+
dirtySet.add(startDay);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let cursor = dayStartMs(startDay);
|
|
293
|
+
while (cursor < endMs) {
|
|
294
|
+
const nextDay = cursor + 86400000;
|
|
295
|
+
const overlapStart = Math.max(cursor, startMs);
|
|
296
|
+
const overlapEnd = Math.min(nextDay, endMs);
|
|
297
|
+
const fraction = (overlapEnd - overlapStart) / totalDur;
|
|
298
|
+
if (fraction > 0) {
|
|
299
|
+
const dayKey = toDayKey(cursor);
|
|
300
|
+
addPresenceToDay(dayMap, dayKey, session, fraction);
|
|
301
|
+
dirtySet.add(dayKey);
|
|
302
|
+
}
|
|
303
|
+
cursor = nextDay;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function addPresenceToDay(dayMap, dayKey, session, fraction) {
|
|
308
|
+
const day = ensureDay(dayMap, dayKey);
|
|
309
|
+
if (fraction > 0.001) day.sessions += 1;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function addUsageByDay(dayMap, session, dirtySet) {
|
|
313
|
+
for (const usageDay of session.usage_by_day || []) {
|
|
314
|
+
const dayKey = usageDay.day;
|
|
315
|
+
const day = ensureDay(dayMap, dayKey);
|
|
316
|
+
day.tokens += usageDay.tokens || 0;
|
|
317
|
+
if (usageDay.cost !== null) day.cost += usageDay.cost;
|
|
318
|
+
|
|
319
|
+
const modelKey = session.model_name || 'unknown';
|
|
320
|
+
if (!day.by_model[modelKey]) day.by_model[modelKey] = { tokens: 0, cost: 0, elapsed_seconds: 0 };
|
|
321
|
+
day.by_model[modelKey].tokens += usageDay.tokens || 0;
|
|
322
|
+
if (usageDay.cost !== null) day.by_model[modelKey].cost += usageDay.cost;
|
|
323
|
+
dirtySet.add(dayKey);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
268
327
|
function addToDay(dayMap, dayKey, session, fraction) {
|
|
269
328
|
const day = ensureDay(dayMap, dayKey);
|
|
270
329
|
day.tokens += (session.tokens_used || 0) * fraction;
|
package/server/rollout-reader.js
CHANGED
|
@@ -15,9 +15,11 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
|
|
|
15
15
|
model_name: null,
|
|
16
16
|
reasoning_effort: null,
|
|
17
17
|
first_timestamp: null,
|
|
18
|
+
first_usage_timestamp: null,
|
|
18
19
|
last_timestamp: null,
|
|
19
20
|
active_seconds: null,
|
|
20
21
|
active_by_day: null,
|
|
22
|
+
usage_by_day: null,
|
|
21
23
|
parent_thread_id: null,
|
|
22
24
|
usage_total: null,
|
|
23
25
|
};
|
|
@@ -28,6 +30,11 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
|
|
|
28
30
|
let prevTimestamp = null;
|
|
29
31
|
let activeMs = 0;
|
|
30
32
|
const activeByDay = new Map();
|
|
33
|
+
const usageByDay = new Map();
|
|
34
|
+
let lastInputTokens = 0;
|
|
35
|
+
let lastCachedInputTokens = 0;
|
|
36
|
+
let lastOutputTokens = 0;
|
|
37
|
+
let hasSeenUsage = false;
|
|
31
38
|
for (const line of content.split(/\r?\n/)) {
|
|
32
39
|
if (!line.trim()) continue;
|
|
33
40
|
|
|
@@ -58,13 +65,46 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
|
|
|
58
65
|
if (obj.type === 'event_msg' && obj.payload?.type === 'token_count') {
|
|
59
66
|
const usage = obj.payload.info?.total_token_usage;
|
|
60
67
|
if (usage) {
|
|
68
|
+
const inputTokens = usage.input_tokens || 0;
|
|
69
|
+
const cachedInputTokens = usage.cached_input_tokens || 0;
|
|
70
|
+
const outputTokens = usage.output_tokens || 0;
|
|
71
|
+
const reasoningOutputTokens = usage.reasoning_output_tokens || 0;
|
|
72
|
+
const totalTokens = usage.total_tokens || 0;
|
|
73
|
+
|
|
61
74
|
result.usage_total = {
|
|
62
|
-
input_tokens:
|
|
63
|
-
cached_input_tokens:
|
|
64
|
-
output_tokens:
|
|
65
|
-
reasoning_output_tokens:
|
|
66
|
-
total_tokens:
|
|
75
|
+
input_tokens: inputTokens,
|
|
76
|
+
cached_input_tokens: cachedInputTokens,
|
|
77
|
+
output_tokens: outputTokens,
|
|
78
|
+
reasoning_output_tokens: reasoningOutputTokens,
|
|
79
|
+
total_tokens: totalTokens,
|
|
67
80
|
};
|
|
81
|
+
|
|
82
|
+
const usageDelta = hasSeenUsage
|
|
83
|
+
? {
|
|
84
|
+
input_tokens: Math.max(inputTokens - lastInputTokens, 0),
|
|
85
|
+
cached_input_tokens: Math.max(cachedInputTokens - lastCachedInputTokens, 0),
|
|
86
|
+
output_tokens: Math.max(outputTokens - lastOutputTokens, 0),
|
|
87
|
+
}
|
|
88
|
+
: {
|
|
89
|
+
input_tokens: inputTokens,
|
|
90
|
+
cached_input_tokens: cachedInputTokens,
|
|
91
|
+
output_tokens: outputTokens,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
hasSeenUsage = true;
|
|
95
|
+
lastInputTokens = inputTokens;
|
|
96
|
+
lastCachedInputTokens = cachedInputTokens;
|
|
97
|
+
lastOutputTokens = outputTokens;
|
|
98
|
+
if (obj.timestamp && hasUsage(usageDelta)) {
|
|
99
|
+
const ts = new Date(obj.timestamp).getTime();
|
|
100
|
+
if (!isNaN(ts)) {
|
|
101
|
+
if (!result.first_usage_timestamp || ts < result.first_usage_timestamp) {
|
|
102
|
+
result.first_usage_timestamp = ts;
|
|
103
|
+
}
|
|
104
|
+
const dayKey = toDayKey(ts);
|
|
105
|
+
mergeUsageTotals(usageByDay, dayKey, usageDelta);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
68
108
|
}
|
|
69
109
|
}
|
|
70
110
|
|
|
@@ -100,9 +140,51 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
|
|
|
100
140
|
result.active_by_day = activeByDaySeconds;
|
|
101
141
|
result.active_seconds = Object.values(activeByDaySeconds).reduce((sum, seconds) => sum + seconds, 0);
|
|
102
142
|
}
|
|
143
|
+
if (usageByDay.size > 0) {
|
|
144
|
+
result.usage_by_day = Object.fromEntries(usageByDay);
|
|
145
|
+
}
|
|
103
146
|
} catch {
|
|
104
147
|
return null;
|
|
105
148
|
}
|
|
106
149
|
|
|
107
150
|
return result;
|
|
108
151
|
}
|
|
152
|
+
|
|
153
|
+
function normalizeUsageTotals(usage) {
|
|
154
|
+
return {
|
|
155
|
+
input_tokens: usage.input_tokens || 0,
|
|
156
|
+
cached_input_tokens: usage.cached_input_tokens || 0,
|
|
157
|
+
output_tokens: usage.output_tokens || 0,
|
|
158
|
+
reasoning_output_tokens: usage.reasoning_output_tokens || 0,
|
|
159
|
+
total_tokens: usage.total_tokens || 0,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function subtractUsageTotals(current, previous) {
|
|
164
|
+
return {
|
|
165
|
+
input_tokens: Math.max((current.input_tokens || 0) - (previous.input_tokens || 0), 0),
|
|
166
|
+
cached_input_tokens: Math.max((current.cached_input_tokens || 0) - (previous.cached_input_tokens || 0), 0),
|
|
167
|
+
output_tokens: Math.max((current.output_tokens || 0) - (previous.output_tokens || 0), 0),
|
|
168
|
+
reasoning_output_tokens: Math.max((current.reasoning_output_tokens || 0) - (previous.reasoning_output_tokens || 0), 0),
|
|
169
|
+
total_tokens: Math.max((current.total_tokens || 0) - (previous.total_tokens || 0), 0),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function hasUsage(usage) {
|
|
174
|
+
return !!usage && (
|
|
175
|
+
usage.input_tokens > 0 ||
|
|
176
|
+
usage.cached_input_tokens > 0 ||
|
|
177
|
+
usage.output_tokens > 0
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function mergeUsageTotals(target, dayKey, usage) {
|
|
182
|
+
let previous = target.get(dayKey);
|
|
183
|
+
if (!previous) {
|
|
184
|
+
previous = { input_tokens: 0, cached_input_tokens: 0, output_tokens: 0 };
|
|
185
|
+
target.set(dayKey, previous);
|
|
186
|
+
}
|
|
187
|
+
previous.input_tokens += usage.input_tokens || 0;
|
|
188
|
+
previous.cached_input_tokens += usage.cached_input_tokens || 0;
|
|
189
|
+
previous.output_tokens += usage.output_tokens || 0;
|
|
190
|
+
}
|