codexmeter 1.0.0 → 1.0.1
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/package.json +1 -1
- package/server/aggregator.js +79 -7
- package/server/cost-catalog.js +1 -1
- package/server/ingest.js +22 -1
- package/server/live-state.js +61 -2
- package/server/rollout-reader.js +83 -5
package/package.json
CHANGED
package/server/aggregator.js
CHANGED
|
@@ -240,7 +240,10 @@ function buildDaily(rawSessions, groupedSessions, tz) {
|
|
|
240
240
|
const startDay = toDayKeyInTz(startMs);
|
|
241
241
|
const endDay = toDayKeyInTz(endMs - 1);
|
|
242
242
|
|
|
243
|
-
if (
|
|
243
|
+
if (s.has_usage_by_day) {
|
|
244
|
+
addSessionPresence(dayMap, startDay, endDay, startMs, endMs, totalDur, toDayKeyInTz, s);
|
|
245
|
+
addUsageByDay(dayMap, s);
|
|
246
|
+
} else if (startDay === endDay) {
|
|
244
247
|
addToDay(dayMap, startDay, s, 1.0);
|
|
245
248
|
} else {
|
|
246
249
|
let cursor = dayStartMs(startDay);
|
|
@@ -314,12 +317,25 @@ function buildHeatmap(rawSessions, groupedSessions, tz) {
|
|
|
314
317
|
const dayMap = new Map();
|
|
315
318
|
for (const s of rawSessions) {
|
|
316
319
|
if (!s.started_at) continue;
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
320
|
+
if (s.has_usage_by_day) {
|
|
321
|
+
for (const usageDay of s.usage_by_day || []) {
|
|
322
|
+
const dayKey = usageDay.day;
|
|
323
|
+
if (!dayMap.has(dayKey)) dayMap.set(dayKey, { tokens: 0, cost: 0, elapsed: 0, sessions: 0 });
|
|
324
|
+
const d = dayMap.get(dayKey);
|
|
325
|
+
d.tokens += usageDay.tokens || 0;
|
|
326
|
+
if (usageDay.cost !== null) d.cost += usageDay.cost;
|
|
327
|
+
}
|
|
328
|
+
const startDay = toDayKeyInTz(s.started_at * 1000);
|
|
329
|
+
if (!dayMap.has(startDay)) dayMap.set(startDay, { tokens: 0, cost: 0, elapsed: 0, sessions: 0 });
|
|
330
|
+
dayMap.get(startDay).sessions++;
|
|
331
|
+
} else {
|
|
332
|
+
const dk = toDayKeyInTz(s.started_at * 1000);
|
|
333
|
+
if (!dayMap.has(dk)) dayMap.set(dk, { tokens: 0, cost: 0, elapsed: 0, sessions: 0 });
|
|
334
|
+
const d = dayMap.get(dk);
|
|
335
|
+
d.tokens += s.tokens_used;
|
|
336
|
+
if (s.cost !== null) d.cost += s.cost;
|
|
337
|
+
d.sessions++;
|
|
338
|
+
}
|
|
323
339
|
}
|
|
324
340
|
|
|
325
341
|
for (const s of groupedSessions) {
|
|
@@ -332,6 +348,62 @@ function buildHeatmap(rawSessions, groupedSessions, tz) {
|
|
|
332
348
|
return Object.fromEntries(dayMap);
|
|
333
349
|
}
|
|
334
350
|
|
|
351
|
+
function addSessionPresence(dayMap, startDay, endDay, startMs, endMs, totalDur, toDayKeyInTz, session) {
|
|
352
|
+
if (startDay === endDay) {
|
|
353
|
+
addPresenceToDay(dayMap, startDay, session, 1.0);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let cursor = dayStartMs(startDay);
|
|
358
|
+
while (cursor < endMs) {
|
|
359
|
+
const nextDay = cursor + 86400000;
|
|
360
|
+
const overlapStart = Math.max(cursor, startMs);
|
|
361
|
+
const overlapEnd = Math.min(nextDay, endMs);
|
|
362
|
+
const fraction = (overlapEnd - overlapStart) / totalDur;
|
|
363
|
+
if (fraction > 0) addPresenceToDay(dayMap, toDayKeyInTz(cursor), session, fraction);
|
|
364
|
+
cursor = nextDay;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function addPresenceToDay(dayMap, dayKey, session, fraction) {
|
|
369
|
+
if (!dayMap.has(dayKey)) dayMap.set(dayKey, { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0, by_model: {}, by_family: {}, by_repo: {} });
|
|
370
|
+
const d = dayMap.get(dayKey);
|
|
371
|
+
if (fraction > 0.001) d.sessions++;
|
|
372
|
+
|
|
373
|
+
const fKey = session.agent_family;
|
|
374
|
+
if (!d.by_family[fKey]) d.by_family[fKey] = { tokens: 0, cost: 0, sessions: 0 };
|
|
375
|
+
if (fraction > 0.001) d.by_family[fKey].sessions++;
|
|
376
|
+
|
|
377
|
+
const rKey = session.repo_label || 'unknown';
|
|
378
|
+
if (!d.by_repo[rKey]) d.by_repo[rKey] = { tokens: 0, cost: 0, sessions: 0 };
|
|
379
|
+
if (fraction > 0.001) d.by_repo[rKey].sessions++;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function addUsageByDay(dayMap, session) {
|
|
383
|
+
for (const usageDay of session.usage_by_day || []) {
|
|
384
|
+
const dayKey = usageDay.day;
|
|
385
|
+
if (!dayMap.has(dayKey)) dayMap.set(dayKey, { tokens: 0, cost: 0, elapsed_seconds: 0, sessions: 0, by_model: {}, by_family: {}, by_repo: {} });
|
|
386
|
+
const d = dayMap.get(dayKey);
|
|
387
|
+
d.tokens += usageDay.tokens || 0;
|
|
388
|
+
if (usageDay.cost !== null) d.cost += usageDay.cost;
|
|
389
|
+
|
|
390
|
+
const mKey = session.model_name || 'unknown';
|
|
391
|
+
if (!d.by_model[mKey]) d.by_model[mKey] = { tokens: 0, cost: 0, elapsed_seconds: 0 };
|
|
392
|
+
d.by_model[mKey].tokens += usageDay.tokens || 0;
|
|
393
|
+
if (usageDay.cost !== null) d.by_model[mKey].cost += usageDay.cost;
|
|
394
|
+
|
|
395
|
+
const fKey = session.agent_family;
|
|
396
|
+
if (!d.by_family[fKey]) d.by_family[fKey] = { tokens: 0, cost: 0, sessions: 0 };
|
|
397
|
+
d.by_family[fKey].tokens += usageDay.tokens || 0;
|
|
398
|
+
if (usageDay.cost !== null) d.by_family[fKey].cost += usageDay.cost;
|
|
399
|
+
|
|
400
|
+
const rKey = session.repo_label || 'unknown';
|
|
401
|
+
if (!d.by_repo[rKey]) d.by_repo[rKey] = { tokens: 0, cost: 0, sessions: 0 };
|
|
402
|
+
d.by_repo[rKey].tokens += usageDay.tokens || 0;
|
|
403
|
+
if (usageDay.cost !== null) d.by_repo[rKey].cost += usageDay.cost;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
335
407
|
function dayStartMs(dayKey) {
|
|
336
408
|
const [y, m, d] = dayKey.split('-').map(Number);
|
|
337
409
|
return new Date(y, m - 1, d).getTime();
|
package/server/cost-catalog.js
CHANGED
|
@@ -32,7 +32,7 @@ export function getCacheAwareRate(modelName) {
|
|
|
32
32
|
return INPUT_FRACTION * effectiveInputRate + OUTPUT_FRACTION * entry.output;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function calculateCostFromUsage(modelName, usage) {
|
|
35
|
+
export function calculateCostFromUsage(modelName, usage) {
|
|
36
36
|
if (!modelName || !usage) return null;
|
|
37
37
|
const entry = getPricing()[modelName];
|
|
38
38
|
if (!entry) return null;
|
package/server/ingest.js
CHANGED
|
@@ -4,7 +4,7 @@ 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';
|
|
@@ -98,6 +98,8 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
98
98
|
model_name: null,
|
|
99
99
|
reasoning_effort: null,
|
|
100
100
|
usage_total: null,
|
|
101
|
+
usage_by_day: null,
|
|
102
|
+
has_usage_by_day: false,
|
|
101
103
|
active_by_day: null,
|
|
102
104
|
agent_role: t.agent_role,
|
|
103
105
|
agent_nickname: t.agent_nickname,
|
|
@@ -160,6 +162,10 @@ export async function runIngest(codexHome, state, opts = {}) {
|
|
|
160
162
|
if (data.reasoning_effort) s.reasoning_effort = data.reasoning_effort;
|
|
161
163
|
if (data.parent_thread_id) s.parent_thread_id = data.parent_thread_id;
|
|
162
164
|
if (data.usage_total) s.usage_total = data.usage_total;
|
|
165
|
+
if (data.usage_by_day) {
|
|
166
|
+
s.usage_by_day = buildUsageByDayMetrics(s.model_name, data.usage_by_day);
|
|
167
|
+
s.has_usage_by_day = s.usage_by_day.length > 0;
|
|
168
|
+
}
|
|
163
169
|
if (data.active_seconds && data.active_seconds > 0) {
|
|
164
170
|
s.elapsed_seconds = data.active_seconds;
|
|
165
171
|
s.active_by_day = data.active_by_day || null;
|
|
@@ -352,6 +358,21 @@ function finalizeSessionMetrics(session, toDayKey) {
|
|
|
352
358
|
}
|
|
353
359
|
}
|
|
354
360
|
|
|
361
|
+
function buildUsageByDayMetrics(modelName, usageByDay) {
|
|
362
|
+
const entries = [];
|
|
363
|
+
for (const [dayKey, usage] of Object.entries(usageByDay || {})) {
|
|
364
|
+
const tokens = (usage?.input_tokens || 0) + (usage?.output_tokens || 0);
|
|
365
|
+
const cost = calculateCostFromUsage(modelName, usage);
|
|
366
|
+
entries.push({
|
|
367
|
+
day: dayKey,
|
|
368
|
+
tokens,
|
|
369
|
+
cost,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
entries.sort((a, b) => a.day.localeCompare(b.day));
|
|
373
|
+
return entries;
|
|
374
|
+
}
|
|
375
|
+
|
|
355
376
|
function queueLiveProgress(state) {
|
|
356
377
|
state.live_progress_dirty = true;
|
|
357
378
|
ensureLivePump(state);
|
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
|
@@ -18,6 +18,7 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
|
|
|
18
18
|
last_timestamp: null,
|
|
19
19
|
active_seconds: null,
|
|
20
20
|
active_by_day: null,
|
|
21
|
+
usage_by_day: null,
|
|
21
22
|
parent_thread_id: null,
|
|
22
23
|
usage_total: null,
|
|
23
24
|
};
|
|
@@ -28,6 +29,11 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
|
|
|
28
29
|
let prevTimestamp = null;
|
|
29
30
|
let activeMs = 0;
|
|
30
31
|
const activeByDay = new Map();
|
|
32
|
+
const usageByDay = new Map();
|
|
33
|
+
let lastInputTokens = 0;
|
|
34
|
+
let lastCachedInputTokens = 0;
|
|
35
|
+
let lastOutputTokens = 0;
|
|
36
|
+
let hasSeenUsage = false;
|
|
31
37
|
for (const line of content.split(/\r?\n/)) {
|
|
32
38
|
if (!line.trim()) continue;
|
|
33
39
|
|
|
@@ -58,13 +64,43 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
|
|
|
58
64
|
if (obj.type === 'event_msg' && obj.payload?.type === 'token_count') {
|
|
59
65
|
const usage = obj.payload.info?.total_token_usage;
|
|
60
66
|
if (usage) {
|
|
67
|
+
const inputTokens = usage.input_tokens || 0;
|
|
68
|
+
const cachedInputTokens = usage.cached_input_tokens || 0;
|
|
69
|
+
const outputTokens = usage.output_tokens || 0;
|
|
70
|
+
const reasoningOutputTokens = usage.reasoning_output_tokens || 0;
|
|
71
|
+
const totalTokens = usage.total_tokens || 0;
|
|
72
|
+
|
|
61
73
|
result.usage_total = {
|
|
62
|
-
input_tokens:
|
|
63
|
-
cached_input_tokens:
|
|
64
|
-
output_tokens:
|
|
65
|
-
reasoning_output_tokens:
|
|
66
|
-
total_tokens:
|
|
74
|
+
input_tokens: inputTokens,
|
|
75
|
+
cached_input_tokens: cachedInputTokens,
|
|
76
|
+
output_tokens: outputTokens,
|
|
77
|
+
reasoning_output_tokens: reasoningOutputTokens,
|
|
78
|
+
total_tokens: totalTokens,
|
|
67
79
|
};
|
|
80
|
+
|
|
81
|
+
const usageDelta = hasSeenUsage
|
|
82
|
+
? {
|
|
83
|
+
input_tokens: Math.max(inputTokens - lastInputTokens, 0),
|
|
84
|
+
cached_input_tokens: Math.max(cachedInputTokens - lastCachedInputTokens, 0),
|
|
85
|
+
output_tokens: Math.max(outputTokens - lastOutputTokens, 0),
|
|
86
|
+
}
|
|
87
|
+
: {
|
|
88
|
+
input_tokens: inputTokens,
|
|
89
|
+
cached_input_tokens: cachedInputTokens,
|
|
90
|
+
output_tokens: outputTokens,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
hasSeenUsage = true;
|
|
94
|
+
lastInputTokens = inputTokens;
|
|
95
|
+
lastCachedInputTokens = cachedInputTokens;
|
|
96
|
+
lastOutputTokens = outputTokens;
|
|
97
|
+
if (obj.timestamp && hasUsage(usageDelta)) {
|
|
98
|
+
const ts = new Date(obj.timestamp).getTime();
|
|
99
|
+
if (!isNaN(ts)) {
|
|
100
|
+
const dayKey = toDayKey(ts);
|
|
101
|
+
mergeUsageTotals(usageByDay, dayKey, usageDelta);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
68
104
|
}
|
|
69
105
|
}
|
|
70
106
|
|
|
@@ -100,9 +136,51 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
|
|
|
100
136
|
result.active_by_day = activeByDaySeconds;
|
|
101
137
|
result.active_seconds = Object.values(activeByDaySeconds).reduce((sum, seconds) => sum + seconds, 0);
|
|
102
138
|
}
|
|
139
|
+
if (usageByDay.size > 0) {
|
|
140
|
+
result.usage_by_day = Object.fromEntries(usageByDay);
|
|
141
|
+
}
|
|
103
142
|
} catch {
|
|
104
143
|
return null;
|
|
105
144
|
}
|
|
106
145
|
|
|
107
146
|
return result;
|
|
108
147
|
}
|
|
148
|
+
|
|
149
|
+
function normalizeUsageTotals(usage) {
|
|
150
|
+
return {
|
|
151
|
+
input_tokens: usage.input_tokens || 0,
|
|
152
|
+
cached_input_tokens: usage.cached_input_tokens || 0,
|
|
153
|
+
output_tokens: usage.output_tokens || 0,
|
|
154
|
+
reasoning_output_tokens: usage.reasoning_output_tokens || 0,
|
|
155
|
+
total_tokens: usage.total_tokens || 0,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function subtractUsageTotals(current, previous) {
|
|
160
|
+
return {
|
|
161
|
+
input_tokens: Math.max((current.input_tokens || 0) - (previous.input_tokens || 0), 0),
|
|
162
|
+
cached_input_tokens: Math.max((current.cached_input_tokens || 0) - (previous.cached_input_tokens || 0), 0),
|
|
163
|
+
output_tokens: Math.max((current.output_tokens || 0) - (previous.output_tokens || 0), 0),
|
|
164
|
+
reasoning_output_tokens: Math.max((current.reasoning_output_tokens || 0) - (previous.reasoning_output_tokens || 0), 0),
|
|
165
|
+
total_tokens: Math.max((current.total_tokens || 0) - (previous.total_tokens || 0), 0),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function hasUsage(usage) {
|
|
170
|
+
return !!usage && (
|
|
171
|
+
usage.input_tokens > 0 ||
|
|
172
|
+
usage.cached_input_tokens > 0 ||
|
|
173
|
+
usage.output_tokens > 0
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function mergeUsageTotals(target, dayKey, usage) {
|
|
178
|
+
let previous = target.get(dayKey);
|
|
179
|
+
if (!previous) {
|
|
180
|
+
previous = { input_tokens: 0, cached_input_tokens: 0, output_tokens: 0 };
|
|
181
|
+
target.set(dayKey, previous);
|
|
182
|
+
}
|
|
183
|
+
previous.input_tokens += usage.input_tokens || 0;
|
|
184
|
+
previous.cached_input_tokens += usage.cached_input_tokens || 0;
|
|
185
|
+
previous.output_tokens += usage.output_tokens || 0;
|
|
186
|
+
}
|