letmecode 0.1.1 → 0.1.3

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.
@@ -4,31 +4,34 @@ import path from "node:path";
4
4
  import readline from "node:readline";
5
5
  import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "./contract.js";
6
6
  import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregates, numberOrZero } from "./limits.js";
7
+ import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "./daily.js";
7
8
  const RATE_CARD = {
8
- "claude-opus-4-8": { input: 500, cacheWrite5m: 625, cacheWrite1h: 1000, cacheRead: 50, output: 2500 },
9
- "claude-opus-4-7": { input: 500, cacheWrite5m: 625, cacheWrite1h: 1000, cacheRead: 50, output: 2500 },
10
- "claude-opus-4-6": { input: 500, cacheWrite5m: 625, cacheWrite1h: 1000, cacheRead: 50, output: 2500 },
11
- "claude-opus-4-5": { input: 500, cacheWrite5m: 625, cacheWrite1h: 1000, cacheRead: 50, output: 2500 },
12
- "claude-opus-4-1": { input: 1500, cacheWrite5m: 1875, cacheWrite1h: 3000, cacheRead: 150, output: 7500 },
13
- "claude-opus-4": { input: 1500, cacheWrite5m: 1875, cacheWrite1h: 3000, cacheRead: 150, output: 7500 },
14
- "claude-sonnet-4-6": { input: 300, cacheWrite5m: 375, cacheWrite1h: 600, cacheRead: 30, output: 1500 },
15
- "claude-sonnet-4-5": { input: 300, cacheWrite5m: 375, cacheWrite1h: 600, cacheRead: 30, output: 1500 },
16
- "claude-sonnet-4": { input: 300, cacheWrite5m: 375, cacheWrite1h: 600, cacheRead: 30, output: 1500 },
17
- "claude-haiku-4-5": { input: 100, cacheWrite5m: 125, cacheWrite1h: 200, cacheRead: 10, output: 500 },
18
- "claude-haiku-3-5": { input: 80, cacheWrite5m: 100, cacheWrite1h: 160, cacheRead: 8, output: 400 }
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 }
19
20
  };
21
+ const USD_TO_CREDITS = 100;
20
22
  export class ClaudeUsageProvider extends UsageProviderBase {
21
23
  constructor(options = {}) {
22
24
  super("claude", "Claude");
23
25
  this.root = path.resolve(options.root ?? os.homedir());
24
26
  }
25
- async getStats() {
27
+ async getStats(options = {}) {
26
28
  const sessionsRoot = path.join(this.root, ".claude", "projects");
27
29
  const byModel = new Map();
30
+ const byDay = createDailyUsageAggregates();
28
31
  const windows = createLimitWindowAggregates();
29
32
  const planTypes = new Set();
30
33
  const warnings = [];
31
- const seenUsageEvents = new Set();
34
+ const parsedEvents = createParsedUsageEventAccumulator();
32
35
  const parseTotals = {
33
36
  filesScanned: 0,
34
37
  linesRead: 0,
@@ -37,14 +40,34 @@ export class ClaudeUsageProvider extends UsageProviderBase {
37
40
  };
38
41
  for await (const file of walkSessionFiles(sessionsRoot)) {
39
42
  parseTotals.filesScanned += 1;
40
- const fileStats = await parseSessionFile(file, byModel, windows, planTypes, seenUsageEvents);
43
+ const fileStats = await parseSessionFile(file, parsedEvents);
41
44
  parseTotals.linesRead += fileStats.linesRead;
42
- parseTotals.tokenEvents += fileStats.tokenEvents;
43
45
  parseTotals.malformedLines += fileStats.malformedLines;
44
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;
45
59
  if (parseTotals.malformedLines > 0) {
46
60
  warnings.push(`Skipped ${parseTotals.malformedLines} malformed JSONL line(s).`);
47
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
+ }
48
71
  const modelUsage = [...byModel.entries()]
49
72
  .map(([modelId, totals]) => ({ modelId, totals }))
50
73
  .sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
@@ -58,6 +81,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
58
81
  warnings.push(`No Claude session files found under ${sessionsRoot}.`);
59
82
  }
60
83
  const summaryTotals = sumUsageTotals(modelUsage.map((row) => row.totals));
84
+ const dayUsage = buildDailyUsageRows(byDay);
61
85
  const [primaryLimitWindows, secondaryLimitWindows] = buildWindowLists(windows);
62
86
  if (parseTotals.filesScanned > 0 &&
63
87
  parseTotals.tokenEvents > 0 &&
@@ -79,6 +103,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
79
103
  rootPath: sessionsRoot
80
104
  },
81
105
  modelUsage,
106
+ dayUsage,
82
107
  primaryLimitWindows,
83
108
  secondaryLimitWindows,
84
109
  warnings
@@ -115,37 +140,53 @@ function creditsFor(modelId, usage) {
115
140
  if (!rate) {
116
141
  return 0;
117
142
  }
118
- const cacheWriteKnownTokens = usage.cacheCreation5mInputTokens + usage.cacheCreation1hInputTokens;
119
- const cacheWriteFallbackTokens = Math.max(0, usage.cacheCreationInputTokens - cacheWriteKnownTokens);
143
+ const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
120
144
  const inferenceMultiplier = usage.inferenceGeo === "us" ? 1.1 : 1;
121
145
  return (((usage.inputTokens / 1000000) * rate.input +
122
146
  (usage.cacheReadInputTokens / 1000000) * rate.cacheRead +
123
- (usage.cacheCreation5mInputTokens / 1000000) * rate.cacheWrite5m +
124
- (usage.cacheCreation1hInputTokens / 1000000) * rate.cacheWrite1h +
125
- (cacheWriteFallbackTokens / 1000000) * rate.cacheWrite5m +
147
+ (cacheWriteBreakdown.cacheWrite5mInputTokens / 1000000) * rate.cacheWrite5m +
148
+ (cacheWriteBreakdown.cacheWrite1hInputTokens / 1000000) * rate.cacheWrite1h +
126
149
  (usage.outputTokens / 1000000) * rate.output) *
127
- inferenceMultiplier);
150
+ inferenceMultiplier *
151
+ USD_TO_CREDITS);
128
152
  }
129
153
  function usageToTotals(modelId, usage) {
130
- const nonCachedInputTokens = usage.inputTokens + usage.cacheCreationInputTokens;
131
- const cachedInputTokens = usage.cacheReadInputTokens;
154
+ const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
155
+ const inputTotalTokens = usage.inputTokens +
156
+ cacheWriteBreakdown.cacheWrite5mInputTokens +
157
+ cacheWriteBreakdown.cacheWrite1hInputTokens +
158
+ usage.cacheReadInputTokens;
132
159
  return {
133
- inputTokens: nonCachedInputTokens + cachedInputTokens,
134
- cachedInputTokens,
135
- nonCachedInputTokens,
160
+ inputTotalTokens,
136
161
  outputTokens: usage.outputTokens,
137
162
  reasoningOutputTokens: 0,
138
- totalTokens: nonCachedInputTokens + cachedInputTokens + usage.outputTokens,
163
+ totalTokens: inputTotalTokens + usage.outputTokens,
139
164
  estimatedCredits: creditsFor(modelId, usage),
140
- eventCount: 1
165
+ eventCount: 1,
166
+ tokenBreakdown: {
167
+ schema: "anthropic",
168
+ inputTokens: usage.inputTokens,
169
+ cacheWrite5mInputTokens: cacheWriteBreakdown.cacheWrite5mInputTokens,
170
+ cacheWrite1hInputTokens: cacheWriteBreakdown.cacheWrite1hInputTokens,
171
+ cacheReadInputTokens: usage.cacheReadInputTokens,
172
+ outputTokens: usage.outputTokens
173
+ }
141
174
  };
142
175
  }
143
176
  function addModelUsage(byModel, modelId, deltaTotals) {
144
177
  const resolvedModelId = modelId || "unknown";
145
- const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
178
+ const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals("anthropic");
146
179
  addUsageTotals(totals, deltaTotals);
147
180
  byModel.set(resolvedModelId, totals);
148
181
  }
182
+ function resolveClaudeCacheWriteBreakdown(usage) {
183
+ const cacheWriteKnownTokens = usage.cacheCreation5mInputTokens + usage.cacheCreation1hInputTokens;
184
+ const cacheWriteFallbackTokens = Math.max(0, usage.cacheCreationInputTokens - cacheWriteKnownTokens);
185
+ return {
186
+ cacheWrite5mInputTokens: usage.cacheCreation5mInputTokens + cacheWriteFallbackTokens,
187
+ cacheWrite1hInputTokens: usage.cacheCreation1hInputTokens
188
+ };
189
+ }
149
190
  function isSessionFile(filePath) {
150
191
  return filePath.endsWith(".jsonl") && filePath.includes(`${path.sep}.claude${path.sep}projects${path.sep}`);
151
192
  }
@@ -167,11 +208,10 @@ async function* walkSessionFiles(directory) {
167
208
  }
168
209
  }
169
210
  }
170
- async function parseSessionFile(filePath, byModel, windows, planTypes, seenUsageEvents) {
211
+ async function parseSessionFile(filePath, parsedEvents) {
171
212
  const stream = fs.createReadStream(filePath, { encoding: "utf8" });
172
213
  const lineReader = readline.createInterface({ input: stream, crlfDelay: Infinity });
173
214
  let linesRead = 0;
174
- let tokenEvents = 0;
175
215
  let malformedLines = 0;
176
216
  for await (const line of lineReader) {
177
217
  linesRead += 1;
@@ -194,22 +234,23 @@ async function parseSessionFile(filePath, byModel, windows, planTypes, seenUsage
194
234
  if (!usage) {
195
235
  continue;
196
236
  }
197
- const usageKey = buildUsageEventKey(payloadObject, message);
198
- if (usageKey && seenUsageEvents.has(usageKey)) {
199
- continue;
200
- }
201
- if (usageKey) {
202
- seenUsageEvents.add(usageKey);
203
- }
204
237
  const modelId = String(message?.model ?? "unknown");
205
- const deltaTotals = usageToTotals(modelId, normalizeUsage(usage));
206
- addModelUsage(byModel, modelId, deltaTotals);
207
- tokenEvents += 1;
208
238
  const eventTimeMs = Date.parse(String(payloadObject.timestamp ?? ""));
209
- const safeEventTimeMs = Number.isFinite(eventTimeMs) ? eventTimeMs : 0;
210
- applyRateLimits(windows, extractRateLimits(payloadObject, message), safeEventTimeMs, deltaTotals, planTypes);
239
+ const rateLimits = extractRateLimits(payloadObject, message);
240
+ const normalizedUsage = normalizeUsage(usage);
241
+ const usageKey = buildUsageEventKey(payloadObject, message);
242
+ const usageSignature = buildUsageSignature(payloadObject, modelId, normalizedUsage);
243
+ const parsedEvent = {
244
+ usageKey,
245
+ usageSignature,
246
+ timestampMs: eventTimeMs,
247
+ modelId,
248
+ totals: usageToTotals(modelId, normalizedUsage),
249
+ rateLimits
250
+ };
251
+ recordParsedUsageEvent(parsedEvents, parsedEvent);
211
252
  }
212
- return { linesRead, tokenEvents, malformedLines };
253
+ return { linesRead, malformedLines };
213
254
  }
214
255
  function buildUsageEventKey(payloadObject, message) {
215
256
  const sessionId = String(payloadObject.sessionId ?? "");
@@ -220,6 +261,66 @@ function buildUsageEventKey(payloadObject, message) {
220
261
  }
221
262
  return `${sessionId}|${requestId || messageId}`;
222
263
  }
264
+ function buildUsageSignature(payloadObject, modelId, usage) {
265
+ return [
266
+ String(payloadObject.sessionId ?? ""),
267
+ modelId,
268
+ usage.inputTokens,
269
+ usage.cacheCreationInputTokens,
270
+ usage.cacheCreation5mInputTokens,
271
+ usage.cacheCreation1hInputTokens,
272
+ usage.cacheReadInputTokens,
273
+ usage.outputTokens,
274
+ usage.inferenceGeo
275
+ ].join("|");
276
+ }
277
+ function createParsedUsageEventAccumulator() {
278
+ return {
279
+ keyedEvents: new Map(),
280
+ unkeyedEvents: new Map(),
281
+ duplicateUsageKeys: 0,
282
+ duplicateUsageKeyCollisions: 0,
283
+ duplicateUnkeyedEvents: 0
284
+ };
285
+ }
286
+ function recordParsedUsageEvent(parsedEvents, event) {
287
+ if (event.usageKey) {
288
+ const previous = parsedEvents.keyedEvents.get(event.usageKey);
289
+ if (!previous) {
290
+ parsedEvents.keyedEvents.set(event.usageKey, event);
291
+ return;
292
+ }
293
+ parsedEvents.duplicateUsageKeys += 1;
294
+ if (previous.usageSignature !== event.usageSignature) {
295
+ parsedEvents.duplicateUsageKeyCollisions += 1;
296
+ }
297
+ if (shouldReplaceUsageEvent(previous, event)) {
298
+ parsedEvents.keyedEvents.set(event.usageKey, event);
299
+ }
300
+ return;
301
+ }
302
+ const previous = parsedEvents.unkeyedEvents.get(event.usageSignature);
303
+ if (!previous) {
304
+ parsedEvents.unkeyedEvents.set(event.usageSignature, event);
305
+ return;
306
+ }
307
+ parsedEvents.duplicateUnkeyedEvents += 1;
308
+ if (shouldReplaceUsageEvent(previous, event)) {
309
+ parsedEvents.unkeyedEvents.set(event.usageSignature, event);
310
+ }
311
+ }
312
+ function shouldReplaceUsageEvent(previous, next) {
313
+ if (next.totals.estimatedCredits > previous.totals.estimatedCredits) {
314
+ return true;
315
+ }
316
+ if (next.totals.estimatedCredits === previous.totals.estimatedCredits) {
317
+ return normalizeTimestamp(next.timestampMs) > normalizeTimestamp(previous.timestampMs);
318
+ }
319
+ return false;
320
+ }
321
+ function normalizeTimestamp(value) {
322
+ return Number.isFinite(value) ? value : Number.NEGATIVE_INFINITY;
323
+ }
223
324
  function extractRateLimits(payloadObject, message) {
224
325
  return asRecord(payloadObject.rate_limits) ?? asRecord(message?.rate_limits);
225
326
  }
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import readline from "node:readline";
5
5
  import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "./contract.js";
6
6
  import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregates, numberOrZero } from "./limits.js";
7
+ import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "./daily.js";
7
8
  const RATE_CARD = {
8
9
  "gpt-5.5": { input: 125, cachedInput: 12.5, output: 750 },
9
10
  "gpt-5.4": { input: 62.5, cachedInput: 6.25, output: 375 },
@@ -14,9 +15,10 @@ export class CodexUsageProvider extends UsageProviderBase {
14
15
  super("codex", "Codex");
15
16
  this.root = path.resolve(options.root ?? os.homedir());
16
17
  }
17
- async getStats() {
18
+ async getStats(_options = {}) {
18
19
  const sessionsRoot = path.join(this.root, ".codex", "sessions");
19
20
  const byModel = new Map();
21
+ const byDay = createDailyUsageAggregates();
20
22
  const windows = createLimitWindowAggregates();
21
23
  const planTypes = new Set();
22
24
  const warnings = [];
@@ -28,7 +30,7 @@ export class CodexUsageProvider extends UsageProviderBase {
28
30
  };
29
31
  for await (const file of walkSessionFiles(sessionsRoot)) {
30
32
  parseTotals.filesScanned += 1;
31
- const fileStats = await parseSessionFile(file, byModel, windows, planTypes);
33
+ const fileStats = await parseSessionFile(file, byModel, byDay, windows, planTypes);
32
34
  parseTotals.linesRead += fileStats.linesRead;
33
35
  parseTotals.tokenEvents += fileStats.tokenEvents;
34
36
  parseTotals.malformedLines += fileStats.malformedLines;
@@ -49,6 +51,7 @@ export class CodexUsageProvider extends UsageProviderBase {
49
51
  warnings.push(`No Codex session files found under ${sessionsRoot}.`);
50
52
  }
51
53
  const summaryTotals = sumUsageTotals(modelUsage.map((row) => row.totals));
54
+ const dayUsage = buildDailyUsageRows(byDay);
52
55
  const [primaryLimitWindows, secondaryLimitWindows] = buildWindowLists(windows);
53
56
  return {
54
57
  providerId: this.id,
@@ -64,6 +67,7 @@ export class CodexUsageProvider extends UsageProviderBase {
64
67
  rootPath: sessionsRoot
65
68
  },
66
69
  modelUsage,
70
+ dayUsage,
67
71
  primaryLimitWindows,
68
72
  secondaryLimitWindows,
69
73
  warnings
@@ -103,23 +107,25 @@ function creditsFor(modelId, usage) {
103
107
  if (!rate) {
104
108
  return 0;
105
109
  }
106
- const cachedInputTokens = Math.min(usage.cachedInputTokens, usage.inputTokens);
107
- const nonCachedInputTokens = Math.max(0, usage.inputTokens - cachedInputTokens);
108
- return ((nonCachedInputTokens / 1000000) * rate.input +
109
- (cachedInputTokens / 1000000) * rate.cachedInput +
110
+ return ((usage.inputTokens / 1000000) * rate.input +
111
+ (usage.cachedInputTokens / 1000000) * rate.cachedInput +
110
112
  (usage.outputTokens / 1000000) * rate.output);
111
113
  }
112
114
  function rawUsageToTotals(usage) {
113
- const cachedInputTokens = Math.min(usage.cachedInputTokens, usage.inputTokens);
115
+ const inputTotalTokens = usage.inputTokens + usage.cachedInputTokens;
114
116
  return {
115
- inputTokens: usage.inputTokens,
116
- cachedInputTokens,
117
- nonCachedInputTokens: Math.max(0, usage.inputTokens - cachedInputTokens),
117
+ inputTotalTokens,
118
118
  outputTokens: usage.outputTokens,
119
119
  reasoningOutputTokens: usage.reasoningOutputTokens,
120
- totalTokens: usage.totalTokens,
120
+ totalTokens: inputTotalTokens + usage.outputTokens,
121
121
  estimatedCredits: 0,
122
- eventCount: 0
122
+ eventCount: 0,
123
+ tokenBreakdown: {
124
+ schema: "openai",
125
+ nonCachedInputTokens: usage.inputTokens,
126
+ cachedInputTokens: usage.cachedInputTokens,
127
+ outputTokens: usage.outputTokens
128
+ }
123
129
  };
124
130
  }
125
131
  function createUsageTotalsForModel(modelId, usage) {
@@ -131,7 +137,7 @@ function createUsageTotalsForModel(modelId, usage) {
131
137
  }
132
138
  function addModelUsage(byModel, modelId, deltaTotals) {
133
139
  const resolvedModelId = modelId || "unknown";
134
- const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
140
+ const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals("openai");
135
141
  addUsageTotals(totals, deltaTotals);
136
142
  byModel.set(resolvedModelId, totals);
137
143
  }
@@ -156,7 +162,7 @@ async function* walkSessionFiles(directory) {
156
162
  }
157
163
  }
158
164
  }
159
- async function parseSessionFile(filePath, byModel, windows, planTypes) {
165
+ async function parseSessionFile(filePath, byModel, byDay, windows, planTypes) {
160
166
  const stream = fs.createReadStream(filePath, { encoding: "utf8" });
161
167
  const lineReader = readline.createInterface({ input: stream, crlfDelay: Infinity });
162
168
  let currentModel = "unknown";
@@ -202,7 +208,10 @@ async function parseSessionFile(filePath, byModel, windows, planTypes) {
202
208
  addModelUsage(byModel, resolvedModelId, deltaTotals);
203
209
  const eventTimeMs = Date.parse(String(payloadObject.timestamp ?? ""));
204
210
  const safeEventTimeMs = Number.isFinite(eventTimeMs) ? eventTimeMs : 0;
205
- applyRateLimits(windows, asRecord(payload.rate_limits), safeEventTimeMs, deltaTotals, planTypes);
211
+ const rateLimits = asRecord(payload.rate_limits);
212
+ const planType = typeof rateLimits?.plan_type === "string" ? rateLimits.plan_type : undefined;
213
+ addDailyUsage(byDay, eventTimeMs, resolvedModelId, planType, deltaTotals);
214
+ applyRateLimits(windows, rateLimits, safeEventTimeMs, deltaTotals, planTypes);
206
215
  }
207
216
  return { linesRead, tokenEvents, malformedLines };
208
217
  }
@@ -4,32 +4,77 @@ export class UsageProviderBase {
4
4
  this.label = label;
5
5
  }
6
6
  }
7
- export function createEmptyUsageTotals() {
7
+ export function createEmptyUsageTotals(schema = "openai") {
8
8
  return {
9
- inputTokens: 0,
10
- cachedInputTokens: 0,
11
- nonCachedInputTokens: 0,
9
+ inputTotalTokens: 0,
12
10
  outputTokens: 0,
13
11
  reasoningOutputTokens: 0,
14
12
  totalTokens: 0,
15
13
  estimatedCredits: 0,
16
- eventCount: 0
14
+ eventCount: 0,
15
+ tokenBreakdown: createEmptyUsageTokenBreakdown(schema)
16
+ };
17
+ }
18
+ export function cloneUsageTotals(totals) {
19
+ return {
20
+ ...totals,
21
+ tokenBreakdown: { ...totals.tokenBreakdown }
17
22
  };
18
23
  }
19
24
  export function addUsageTotals(target, source) {
20
- target.inputTokens += source.inputTokens;
21
- target.cachedInputTokens += source.cachedInputTokens;
22
- target.nonCachedInputTokens += source.nonCachedInputTokens;
25
+ target.inputTotalTokens += source.inputTotalTokens;
23
26
  target.outputTokens += source.outputTokens;
24
27
  target.reasoningOutputTokens += source.reasoningOutputTokens;
25
28
  target.totalTokens += source.totalTokens;
26
29
  target.estimatedCredits += source.estimatedCredits;
27
30
  target.eventCount += source.eventCount;
31
+ addUsageTokenBreakdown(target.tokenBreakdown, source.tokenBreakdown);
32
+ if (source.cacheStatus === "unavailable") {
33
+ target.cacheStatus = "unavailable";
34
+ }
35
+ if (source.estimatedCreditsStatus === "unavailable") {
36
+ target.estimatedCreditsStatus = "unavailable";
37
+ }
28
38
  }
29
39
  export function sumUsageTotals(rows) {
30
- const totals = createEmptyUsageTotals();
40
+ const totals = createEmptyUsageTotals(rows[0]?.tokenBreakdown.schema ?? "openai");
31
41
  for (const row of rows) {
32
42
  addUsageTotals(totals, row);
33
43
  }
34
44
  return totals;
35
45
  }
46
+ function createEmptyUsageTokenBreakdown(schema) {
47
+ if (schema === "anthropic") {
48
+ return {
49
+ schema,
50
+ inputTokens: 0,
51
+ cacheWrite5mInputTokens: 0,
52
+ cacheWrite1hInputTokens: 0,
53
+ cacheReadInputTokens: 0,
54
+ outputTokens: 0
55
+ };
56
+ }
57
+ return {
58
+ schema,
59
+ nonCachedInputTokens: 0,
60
+ cachedInputTokens: 0,
61
+ outputTokens: 0
62
+ };
63
+ }
64
+ function addUsageTokenBreakdown(target, source) {
65
+ if (target.schema !== source.schema) {
66
+ throw new Error(`Cannot merge ${source.schema} usage totals into ${target.schema} totals.`);
67
+ }
68
+ target.outputTokens += source.outputTokens;
69
+ if (target.schema === "anthropic" && source.schema === "anthropic") {
70
+ target.inputTokens += source.inputTokens;
71
+ target.cacheWrite5mInputTokens += source.cacheWrite5mInputTokens;
72
+ target.cacheWrite1hInputTokens += source.cacheWrite1hInputTokens;
73
+ target.cacheReadInputTokens += source.cacheReadInputTokens;
74
+ return;
75
+ }
76
+ if (target.schema === "openai" && source.schema === "openai") {
77
+ target.nonCachedInputTokens += source.nonCachedInputTokens;
78
+ target.cachedInputTokens += source.cachedInputTokens;
79
+ }
80
+ }