letmecode 0.1.0 → 0.1.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.
@@ -0,0 +1,311 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import readline from "node:readline";
5
+ import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "./contract.js";
6
+ import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregates, numberOrZero } from "./limits.js";
7
+ import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "./daily.js";
8
+ const RATE_CARD = {
9
+ "claude-opus-4-8": { input: 5, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5, output: 25 },
10
+ "claude-opus-4-7": { input: 5, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5, output: 25 },
11
+ "claude-opus-4-6": { input: 5, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5, output: 25 },
12
+ "claude-opus-4-5": { input: 5, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5, output: 25 },
13
+ "claude-opus-4-1": { input: 15, cacheWrite5m: 18.75, cacheWrite1h: 30, cacheRead: 1.5, output: 75 },
14
+ "claude-opus-4": { input: 15, cacheWrite5m: 18.75, cacheWrite1h: 30, cacheRead: 1.5, output: 75 },
15
+ "claude-sonnet-4-6": { input: 3, cacheWrite5m: 3.75, cacheWrite1h: 6, cacheRead: 0.3, output: 15 },
16
+ "claude-sonnet-4-5": { input: 3, cacheWrite5m: 3.75, cacheWrite1h: 6, cacheRead: 0.3, output: 15 },
17
+ "claude-sonnet-4": { input: 3, cacheWrite5m: 3.75, cacheWrite1h: 6, cacheRead: 0.3, output: 15 },
18
+ "claude-haiku-4-5": { input: 1, cacheWrite5m: 1.25, cacheWrite1h: 2, cacheRead: 0.1, output: 5 },
19
+ "claude-haiku-3-5": { input: 0.8, cacheWrite5m: 1, cacheWrite1h: 1.6, cacheRead: 0.08, output: 4 }
20
+ };
21
+ const USD_TO_CREDITS = 100;
22
+ export class ClaudeUsageProvider extends UsageProviderBase {
23
+ constructor(options = {}) {
24
+ super("claude", "Claude");
25
+ this.root = path.resolve(options.root ?? os.homedir());
26
+ }
27
+ async getStats(options = {}) {
28
+ const sessionsRoot = path.join(this.root, ".claude", "projects");
29
+ const byModel = new Map();
30
+ const byDay = createDailyUsageAggregates();
31
+ const windows = createLimitWindowAggregates();
32
+ const planTypes = new Set();
33
+ const warnings = [];
34
+ const parsedEvents = createParsedUsageEventAccumulator();
35
+ const parseTotals = {
36
+ filesScanned: 0,
37
+ linesRead: 0,
38
+ tokenEvents: 0,
39
+ malformedLines: 0
40
+ };
41
+ for await (const file of walkSessionFiles(sessionsRoot)) {
42
+ parseTotals.filesScanned += 1;
43
+ const fileStats = await parseSessionFile(file, parsedEvents);
44
+ parseTotals.linesRead += fileStats.linesRead;
45
+ parseTotals.malformedLines += fileStats.malformedLines;
46
+ }
47
+ const selectedEvents = [
48
+ ...parsedEvents.keyedEvents.values(),
49
+ ...parsedEvents.unkeyedEvents.values()
50
+ ];
51
+ for (const event of selectedEvents) {
52
+ addModelUsage(byModel, event.modelId, event.totals);
53
+ const planType = typeof event.rateLimits?.plan_type === "string" ? event.rateLimits.plan_type : undefined;
54
+ const safeEventTimeMs = Number.isFinite(event.timestampMs) ? event.timestampMs : 0;
55
+ addDailyUsage(byDay, event.timestampMs, event.modelId, planType, event.totals);
56
+ applyRateLimits(windows, event.rateLimits, safeEventTimeMs, event.totals, planTypes);
57
+ }
58
+ parseTotals.tokenEvents = selectedEvents.length;
59
+ if (parseTotals.malformedLines > 0) {
60
+ warnings.push(`Skipped ${parseTotals.malformedLines} malformed JSONL line(s).`);
61
+ }
62
+ if (options.verbose && parsedEvents.duplicateUsageKeys > 0) {
63
+ warnings.push(`Collapsed ${parsedEvents.duplicateUsageKeys} duplicate Claude usage event(s) by request/message key.`);
64
+ }
65
+ if (options.verbose && parsedEvents.duplicateUsageKeyCollisions > 0) {
66
+ warnings.push(`Detected ${parsedEvents.duplicateUsageKeyCollisions} Claude usage key collision(s) with different token usage; keeping the highest-cost/latest event per key.`);
67
+ }
68
+ if (options.verbose && parsedEvents.duplicateUnkeyedEvents > 0) {
69
+ warnings.push(`Collapsed ${parsedEvents.duplicateUnkeyedEvents} duplicate unkeyed Claude usage event(s) by usage signature.`);
70
+ }
71
+ const modelUsage = [...byModel.entries()]
72
+ .map(([modelId, totals]) => ({ modelId, totals }))
73
+ .sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
74
+ const unknownPricedModels = modelUsage
75
+ .map((row) => row.modelId)
76
+ .filter((modelId) => !resolveRate(modelId));
77
+ if (unknownPricedModels.length > 0) {
78
+ warnings.push(`No credit rate configured for: ${unknownPricedModels.join(", ")}.`);
79
+ }
80
+ if (parseTotals.filesScanned === 0) {
81
+ warnings.push(`No Claude session files found under ${sessionsRoot}.`);
82
+ }
83
+ const summaryTotals = sumUsageTotals(modelUsage.map((row) => row.totals));
84
+ const dayUsage = buildDailyUsageRows(byDay);
85
+ const [primaryLimitWindows, secondaryLimitWindows] = buildWindowLists(windows);
86
+ if (parseTotals.filesScanned > 0 &&
87
+ parseTotals.tokenEvents > 0 &&
88
+ primaryLimitWindows.length === 0 &&
89
+ secondaryLimitWindows.length === 0) {
90
+ warnings.push("Claude transcripts did not expose rate-limit windows in the local logs.");
91
+ }
92
+ return {
93
+ providerId: this.id,
94
+ providerLabel: this.label,
95
+ summary: {
96
+ filesScanned: parseTotals.filesScanned,
97
+ linesRead: parseTotals.linesRead,
98
+ tokenEvents: parseTotals.tokenEvents,
99
+ totals: summaryTotals,
100
+ distinctModels: modelUsage.map((row) => row.modelId),
101
+ distinctPlanTypes: [...planTypes].sort(),
102
+ rootLabel: "~/.claude/projects",
103
+ rootPath: sessionsRoot
104
+ },
105
+ modelUsage,
106
+ dayUsage,
107
+ primaryLimitWindows,
108
+ secondaryLimitWindows,
109
+ warnings
110
+ };
111
+ }
112
+ }
113
+ function normalizeUsage(value) {
114
+ const usage = asRecord(value) ?? {};
115
+ const cacheCreation = asRecord(usage.cache_creation);
116
+ const cacheCreation5mInputTokens = numberOrZero(cacheCreation?.ephemeral_5m_input_tokens);
117
+ const cacheCreation1hInputTokens = numberOrZero(cacheCreation?.ephemeral_1h_input_tokens);
118
+ const cacheCreationInputTokens = Math.max(numberOrZero(usage.cache_creation_input_tokens), cacheCreation5mInputTokens + cacheCreation1hInputTokens);
119
+ return {
120
+ inputTokens: numberOrZero(usage.input_tokens),
121
+ cacheReadInputTokens: numberOrZero(usage.cache_read_input_tokens),
122
+ cacheCreationInputTokens,
123
+ cacheCreation5mInputTokens,
124
+ cacheCreation1hInputTokens,
125
+ outputTokens: numberOrZero(usage.output_tokens),
126
+ inferenceGeo: String(usage.inference_geo ?? "")
127
+ };
128
+ }
129
+ function resolveRate(modelId) {
130
+ const candidates = Object.keys(RATE_CARD).sort((left, right) => right.length - left.length);
131
+ for (const candidate of candidates) {
132
+ if (modelId === candidate || modelId.startsWith(`${candidate}-`)) {
133
+ return RATE_CARD[candidate];
134
+ }
135
+ }
136
+ return undefined;
137
+ }
138
+ function creditsFor(modelId, usage) {
139
+ const rate = resolveRate(modelId);
140
+ if (!rate) {
141
+ return 0;
142
+ }
143
+ const cacheWriteKnownTokens = usage.cacheCreation5mInputTokens + usage.cacheCreation1hInputTokens;
144
+ const cacheWriteFallbackTokens = Math.max(0, usage.cacheCreationInputTokens - cacheWriteKnownTokens);
145
+ const inferenceMultiplier = usage.inferenceGeo === "us" ? 1.1 : 1;
146
+ return (((usage.inputTokens / 1000000) * rate.input +
147
+ (usage.cacheReadInputTokens / 1000000) * rate.cacheRead +
148
+ (usage.cacheCreation5mInputTokens / 1000000) * rate.cacheWrite5m +
149
+ (usage.cacheCreation1hInputTokens / 1000000) * rate.cacheWrite1h +
150
+ (cacheWriteFallbackTokens / 1000000) * rate.cacheWrite5m +
151
+ (usage.outputTokens / 1000000) * rate.output) *
152
+ inferenceMultiplier *
153
+ USD_TO_CREDITS);
154
+ }
155
+ function usageToTotals(modelId, usage) {
156
+ const nonCachedInputTokens = usage.inputTokens + usage.cacheCreationInputTokens;
157
+ const cachedInputTokens = usage.cacheReadInputTokens;
158
+ return {
159
+ inputTokens: nonCachedInputTokens + cachedInputTokens,
160
+ cachedInputTokens,
161
+ nonCachedInputTokens,
162
+ outputTokens: usage.outputTokens,
163
+ reasoningOutputTokens: 0,
164
+ totalTokens: nonCachedInputTokens + cachedInputTokens + usage.outputTokens,
165
+ estimatedCredits: creditsFor(modelId, usage),
166
+ eventCount: 1
167
+ };
168
+ }
169
+ function addModelUsage(byModel, modelId, deltaTotals) {
170
+ const resolvedModelId = modelId || "unknown";
171
+ const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
172
+ addUsageTotals(totals, deltaTotals);
173
+ byModel.set(resolvedModelId, totals);
174
+ }
175
+ function isSessionFile(filePath) {
176
+ return filePath.endsWith(".jsonl") && filePath.includes(`${path.sep}.claude${path.sep}projects${path.sep}`);
177
+ }
178
+ async function* walkSessionFiles(directory) {
179
+ let entries;
180
+ try {
181
+ entries = await fs.promises.readdir(directory, { withFileTypes: true });
182
+ }
183
+ catch {
184
+ return;
185
+ }
186
+ for (const entry of entries) {
187
+ const fullPath = path.join(directory, entry.name);
188
+ if (entry.isDirectory()) {
189
+ yield* walkSessionFiles(fullPath);
190
+ }
191
+ else if (entry.isFile() && isSessionFile(fullPath)) {
192
+ yield fullPath;
193
+ }
194
+ }
195
+ }
196
+ async function parseSessionFile(filePath, parsedEvents) {
197
+ const stream = fs.createReadStream(filePath, { encoding: "utf8" });
198
+ const lineReader = readline.createInterface({ input: stream, crlfDelay: Infinity });
199
+ let linesRead = 0;
200
+ let malformedLines = 0;
201
+ for await (const line of lineReader) {
202
+ linesRead += 1;
203
+ if (!line.trim()) {
204
+ continue;
205
+ }
206
+ let payloadObject;
207
+ try {
208
+ payloadObject = JSON.parse(line);
209
+ }
210
+ catch {
211
+ malformedLines += 1;
212
+ continue;
213
+ }
214
+ if (payloadObject.type !== "assistant") {
215
+ continue;
216
+ }
217
+ const message = asRecord(payloadObject.message);
218
+ const usage = asRecord(message?.usage);
219
+ if (!usage) {
220
+ continue;
221
+ }
222
+ const modelId = String(message?.model ?? "unknown");
223
+ const eventTimeMs = Date.parse(String(payloadObject.timestamp ?? ""));
224
+ const rateLimits = extractRateLimits(payloadObject, message);
225
+ const normalizedUsage = normalizeUsage(usage);
226
+ const usageKey = buildUsageEventKey(payloadObject, message);
227
+ const usageSignature = buildUsageSignature(payloadObject, modelId, normalizedUsage);
228
+ const parsedEvent = {
229
+ usageKey,
230
+ usageSignature,
231
+ timestampMs: eventTimeMs,
232
+ modelId,
233
+ totals: usageToTotals(modelId, normalizedUsage),
234
+ rateLimits
235
+ };
236
+ recordParsedUsageEvent(parsedEvents, parsedEvent);
237
+ }
238
+ return { linesRead, malformedLines };
239
+ }
240
+ function buildUsageEventKey(payloadObject, message) {
241
+ const sessionId = String(payloadObject.sessionId ?? "");
242
+ const requestId = typeof payloadObject.requestId === "string" ? payloadObject.requestId : "";
243
+ const messageId = typeof message?.id === "string" ? message.id : "";
244
+ if (!requestId && !messageId) {
245
+ return null;
246
+ }
247
+ return `${sessionId}|${requestId || messageId}`;
248
+ }
249
+ function buildUsageSignature(payloadObject, modelId, usage) {
250
+ return [
251
+ String(payloadObject.sessionId ?? ""),
252
+ modelId,
253
+ usage.inputTokens,
254
+ usage.cacheCreationInputTokens,
255
+ usage.cacheCreation5mInputTokens,
256
+ usage.cacheCreation1hInputTokens,
257
+ usage.cacheReadInputTokens,
258
+ usage.outputTokens,
259
+ usage.inferenceGeo
260
+ ].join("|");
261
+ }
262
+ function createParsedUsageEventAccumulator() {
263
+ return {
264
+ keyedEvents: new Map(),
265
+ unkeyedEvents: new Map(),
266
+ duplicateUsageKeys: 0,
267
+ duplicateUsageKeyCollisions: 0,
268
+ duplicateUnkeyedEvents: 0
269
+ };
270
+ }
271
+ function recordParsedUsageEvent(parsedEvents, event) {
272
+ if (event.usageKey) {
273
+ const previous = parsedEvents.keyedEvents.get(event.usageKey);
274
+ if (!previous) {
275
+ parsedEvents.keyedEvents.set(event.usageKey, event);
276
+ return;
277
+ }
278
+ parsedEvents.duplicateUsageKeys += 1;
279
+ if (previous.usageSignature !== event.usageSignature) {
280
+ parsedEvents.duplicateUsageKeyCollisions += 1;
281
+ }
282
+ if (shouldReplaceUsageEvent(previous, event)) {
283
+ parsedEvents.keyedEvents.set(event.usageKey, event);
284
+ }
285
+ return;
286
+ }
287
+ const previous = parsedEvents.unkeyedEvents.get(event.usageSignature);
288
+ if (!previous) {
289
+ parsedEvents.unkeyedEvents.set(event.usageSignature, event);
290
+ return;
291
+ }
292
+ parsedEvents.duplicateUnkeyedEvents += 1;
293
+ if (shouldReplaceUsageEvent(previous, event)) {
294
+ parsedEvents.unkeyedEvents.set(event.usageSignature, event);
295
+ }
296
+ }
297
+ function shouldReplaceUsageEvent(previous, next) {
298
+ if (next.totals.estimatedCredits > previous.totals.estimatedCredits) {
299
+ return true;
300
+ }
301
+ if (next.totals.estimatedCredits === previous.totals.estimatedCredits) {
302
+ return normalizeTimestamp(next.timestampMs) > normalizeTimestamp(previous.timestampMs);
303
+ }
304
+ return false;
305
+ }
306
+ function normalizeTimestamp(value) {
307
+ return Number.isFinite(value) ? value : Number.NEGATIVE_INFINITY;
308
+ }
309
+ function extractRateLimits(payloadObject, message) {
310
+ return asRecord(payloadObject.rate_limits) ?? asRecord(message?.rate_limits);
311
+ }
@@ -2,7 +2,9 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import readline from "node:readline";
5
- import { UsageProviderBase, createEmptyUsageTotals } from "./contract.js";
5
+ import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "./contract.js";
6
+ import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregates, numberOrZero } from "./limits.js";
7
+ import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "./daily.js";
6
8
  const RATE_CARD = {
7
9
  "gpt-5.5": { input: 125, cachedInput: 12.5, output: 750 },
8
10
  "gpt-5.4": { input: 62.5, cachedInput: 6.25, output: 375 },
@@ -13,10 +15,11 @@ export class CodexUsageProvider extends UsageProviderBase {
13
15
  super("codex", "Codex");
14
16
  this.root = path.resolve(options.root ?? os.homedir());
15
17
  }
16
- async getStats() {
18
+ async getStats(_options = {}) {
17
19
  const sessionsRoot = path.join(this.root, ".codex", "sessions");
18
20
  const byModel = new Map();
19
- const windows = new Map();
21
+ const byDay = createDailyUsageAggregates();
22
+ const windows = createLimitWindowAggregates();
20
23
  const planTypes = new Set();
21
24
  const warnings = [];
22
25
  const parseTotals = {
@@ -27,7 +30,7 @@ export class CodexUsageProvider extends UsageProviderBase {
27
30
  };
28
31
  for await (const file of walkSessionFiles(sessionsRoot)) {
29
32
  parseTotals.filesScanned += 1;
30
- const fileStats = await parseSessionFile(file, byModel, windows, planTypes);
33
+ const fileStats = await parseSessionFile(file, byModel, byDay, windows, planTypes);
31
34
  parseTotals.linesRead += fileStats.linesRead;
32
35
  parseTotals.tokenEvents += fileStats.tokenEvents;
33
36
  parseTotals.malformedLines += fileStats.malformedLines;
@@ -48,6 +51,7 @@ export class CodexUsageProvider extends UsageProviderBase {
48
51
  warnings.push(`No Codex session files found under ${sessionsRoot}.`);
49
52
  }
50
53
  const summaryTotals = sumUsageTotals(modelUsage.map((row) => row.totals));
54
+ const dayUsage = buildDailyUsageRows(byDay);
51
55
  const [primaryLimitWindows, secondaryLimitWindows] = buildWindowLists(windows);
52
56
  return {
53
57
  providerId: this.id,
@@ -63,6 +67,7 @@ export class CodexUsageProvider extends UsageProviderBase {
63
67
  rootPath: sessionsRoot
64
68
  },
65
69
  modelUsage,
70
+ dayUsage,
66
71
  primaryLimitWindows,
67
72
  secondaryLimitWindows,
68
73
  warnings
@@ -78,9 +83,6 @@ function createEmptyRawUsage() {
78
83
  totalTokens: 0
79
84
  };
80
85
  }
81
- function numberOrZero(value) {
82
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
83
- }
84
86
  function normalizeRawUsage(value) {
85
87
  const usage = value && typeof value === "object" ? value : {};
86
88
  return {
@@ -124,104 +126,19 @@ function rawUsageToTotals(usage) {
124
126
  eventCount: 0
125
127
  };
126
128
  }
127
- function addUsageTotals(target, source) {
128
- target.inputTokens += source.inputTokens;
129
- target.cachedInputTokens += source.cachedInputTokens;
130
- target.nonCachedInputTokens += source.nonCachedInputTokens;
131
- target.outputTokens += source.outputTokens;
132
- target.reasoningOutputTokens += source.reasoningOutputTokens;
133
- target.totalTokens += source.totalTokens;
134
- target.estimatedCredits += source.estimatedCredits;
135
- target.eventCount += source.eventCount;
136
- }
137
- function addModelUsage(byModel, modelId, usage) {
129
+ function createUsageTotalsForModel(modelId, usage) {
138
130
  const resolvedModelId = modelId || "unknown";
139
- const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
140
131
  const deltaTotals = rawUsageToTotals(usage);
141
132
  deltaTotals.estimatedCredits = creditsFor(resolvedModelId, usage);
142
133
  deltaTotals.eventCount = 1;
134
+ return deltaTotals;
135
+ }
136
+ function addModelUsage(byModel, modelId, deltaTotals) {
137
+ const resolvedModelId = modelId || "unknown";
138
+ const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
143
139
  addUsageTotals(totals, deltaTotals);
144
140
  byModel.set(resolvedModelId, totals);
145
141
  }
146
- function sumUsageTotals(rows) {
147
- const totals = createEmptyUsageTotals();
148
- for (const row of rows) {
149
- addUsageTotals(totals, row);
150
- }
151
- return totals;
152
- }
153
- function formatIsoFromSeconds(seconds) {
154
- return new Date(seconds * 1000).toISOString().replace(".000Z", "Z");
155
- }
156
- function formatIsoFromMilliseconds(milliseconds) {
157
- return new Date(milliseconds).toISOString().replace(".000Z", "Z");
158
- }
159
- function makeWindowKey(scope, rateLimits, window) {
160
- return [
161
- scope,
162
- String(rateLimits.limit_id ?? "unknown"),
163
- String(rateLimits.plan_type ?? "unknown"),
164
- numberOrZero(window.window_minutes),
165
- Math.round(numberOrZero(window.resets_at) / 60)
166
- ].join("|");
167
- }
168
- function upsertWindow(windows, scope, rateLimits, window, eventTimeMs) {
169
- if (!window) {
170
- return;
171
- }
172
- const windowMinutes = numberOrZero(window.window_minutes);
173
- const resetsAt = numberOrZero(window.resets_at);
174
- if (!windowMinutes || !resetsAt) {
175
- return;
176
- }
177
- const startsAt = resetsAt - windowMinutes * 60;
178
- const usedPercent = numberOrZero(window.used_percent);
179
- const key = makeWindowKey(scope, rateLimits, window);
180
- const existing = windows.get(key);
181
- if (!existing) {
182
- windows.set(key, {
183
- scope,
184
- limitId: String(rateLimits.limit_id ?? "unknown"),
185
- planType: String(rateLimits.plan_type ?? "unknown"),
186
- windowMinutes,
187
- minStartsAt: startsAt,
188
- maxResetsAt: resetsAt,
189
- firstSeenMs: eventTimeMs,
190
- lastSeenMs: eventTimeMs,
191
- minUsedPercent: usedPercent,
192
- maxUsedPercent: usedPercent,
193
- eventCount: 1
194
- });
195
- return;
196
- }
197
- existing.minStartsAt = Math.min(existing.minStartsAt, startsAt);
198
- existing.maxResetsAt = Math.max(existing.maxResetsAt, resetsAt);
199
- existing.firstSeenMs = Math.min(existing.firstSeenMs, eventTimeMs);
200
- existing.lastSeenMs = Math.max(existing.lastSeenMs, eventTimeMs);
201
- existing.minUsedPercent = Math.min(existing.minUsedPercent, usedPercent);
202
- existing.maxUsedPercent = Math.max(existing.maxUsedPercent, usedPercent);
203
- existing.eventCount += 1;
204
- }
205
- function buildWindowLists(windows) {
206
- const rows = [...windows.values()]
207
- .map((window) => ({
208
- scope: window.scope,
209
- planType: window.planType,
210
- limitId: window.limitId,
211
- windowMinutes: window.windowMinutes,
212
- startTimeIso: formatIsoFromSeconds(window.minStartsAt),
213
- endTimeIso: formatIsoFromSeconds(window.maxResetsAt),
214
- firstSeenIso: formatIsoFromMilliseconds(window.firstSeenMs),
215
- lastSeenIso: formatIsoFromMilliseconds(window.lastSeenMs),
216
- minUsedPercent: window.minUsedPercent,
217
- maxUsedPercent: window.maxUsedPercent,
218
- eventCount: window.eventCount
219
- }))
220
- .sort((left, right) => right.endTimeIso.localeCompare(left.endTimeIso));
221
- const primary = rows.filter((row) => row.scope === "primary").slice(0, 5);
222
- const secondary = rows.filter((row) => row.scope === "secondary").slice(0, 5);
223
- return [primary, secondary];
224
- }
225
142
  function isSessionFile(filePath) {
226
143
  return filePath.endsWith(".jsonl") && filePath.includes(`${path.sep}.codex${path.sep}sessions${path.sep}`);
227
144
  }
@@ -243,7 +160,7 @@ async function* walkSessionFiles(directory) {
243
160
  }
244
161
  }
245
162
  }
246
- async function parseSessionFile(filePath, byModel, windows, planTypes) {
163
+ async function parseSessionFile(filePath, byModel, byDay, windows, planTypes) {
247
164
  const stream = fs.createReadStream(filePath, { encoding: "utf8" });
248
165
  const lineReader = readline.createInterface({ input: stream, crlfDelay: Infinity });
249
166
  let currentModel = "unknown";
@@ -279,23 +196,20 @@ async function parseSessionFile(filePath, byModel, windows, planTypes) {
279
196
  continue;
280
197
  }
281
198
  const info = payload.info;
282
- const rateLimits = payload.rate_limits;
283
199
  const totalUsage = normalizeRawUsage(info?.total_token_usage);
284
200
  const lastUsage = info?.last_token_usage;
285
201
  const usage = lastUsage ? normalizeRawUsage(lastUsage) : previousTotal ? subtractRawUsage(totalUsage, previousTotal) : totalUsage;
286
202
  previousTotal = totalUsage;
203
+ const resolvedModelId = currentModel || "unknown";
204
+ const deltaTotals = createUsageTotalsForModel(resolvedModelId, usage);
287
205
  tokenEvents += 1;
288
- addModelUsage(byModel, currentModel, usage);
289
- if (typeof rateLimits?.plan_type === "string") {
290
- planTypes.add(rateLimits.plan_type);
291
- }
206
+ addModelUsage(byModel, resolvedModelId, deltaTotals);
292
207
  const eventTimeMs = Date.parse(String(payloadObject.timestamp ?? ""));
293
208
  const safeEventTimeMs = Number.isFinite(eventTimeMs) ? eventTimeMs : 0;
294
- upsertWindow(windows, "primary", rateLimits ?? {}, asRecord(rateLimits?.primary), safeEventTimeMs);
295
- upsertWindow(windows, "secondary", rateLimits ?? {}, asRecord(rateLimits?.secondary), safeEventTimeMs);
209
+ const rateLimits = asRecord(payload.rate_limits);
210
+ const planType = typeof rateLimits?.plan_type === "string" ? rateLimits.plan_type : undefined;
211
+ addDailyUsage(byDay, eventTimeMs, resolvedModelId, planType, deltaTotals);
212
+ applyRateLimits(windows, rateLimits, safeEventTimeMs, deltaTotals, planTypes);
296
213
  }
297
214
  return { linesRead, tokenEvents, malformedLines };
298
215
  }
299
- function asRecord(value) {
300
- return value && typeof value === "object" ? value : null;
301
- }
@@ -16,3 +16,26 @@ export function createEmptyUsageTotals() {
16
16
  eventCount: 0
17
17
  };
18
18
  }
19
+ export function addUsageTotals(target, source) {
20
+ target.inputTokens += source.inputTokens;
21
+ target.cachedInputTokens += source.cachedInputTokens;
22
+ target.nonCachedInputTokens += source.nonCachedInputTokens;
23
+ target.outputTokens += source.outputTokens;
24
+ target.reasoningOutputTokens += source.reasoningOutputTokens;
25
+ target.totalTokens += source.totalTokens;
26
+ target.estimatedCredits += source.estimatedCredits;
27
+ target.eventCount += source.eventCount;
28
+ if (source.cacheStatus === "unavailable") {
29
+ target.cacheStatus = "unavailable";
30
+ }
31
+ if (source.estimatedCreditsStatus === "unavailable") {
32
+ target.estimatedCreditsStatus = "unavailable";
33
+ }
34
+ }
35
+ export function sumUsageTotals(rows) {
36
+ const totals = createEmptyUsageTotals();
37
+ for (const row of rows) {
38
+ addUsageTotals(totals, row);
39
+ }
40
+ return totals;
41
+ }