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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmeter",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Local telemetry dashboard for Codex CLI usage",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 (startDay === endDay) {
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
- const dk = toDayKeyInTz(s.started_at * 1000);
318
- if (!dayMap.has(dk)) dayMap.set(dk, { tokens: 0, cost: 0, elapsed: 0, sessions: 0 });
319
- const d = dayMap.get(dk);
320
- d.tokens += s.tokens_used;
321
- if (s.cost !== null) d.cost += s.cost;
322
- d.sessions++;
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();
@@ -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);
@@ -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 (startDay === endDay) {
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.started_at) {
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;
@@ -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: usage.input_tokens || 0,
63
- cached_input_tokens: usage.cached_input_tokens || 0,
64
- output_tokens: usage.output_tokens || 0,
65
- reasoning_output_tokens: usage.reasoning_output_tokens || 0,
66
- total_tokens: usage.total_tokens || 0,
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
+ }