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.
- package/README.md +35 -9
- package/ink-app/dist/index.js +253 -32
- package/ink-app/dist/providers/claude.js +145 -44
- package/ink-app/dist/providers/codex.js +24 -15
- package/ink-app/dist/providers/contract.js +54 -9
- package/ink-app/dist/providers/copilot.js +388 -0
- package/ink-app/dist/providers/daily.js +64 -0
- package/ink-app/dist/providers/index.js +3 -1
- package/ink-app/dist/providers/limits.js +5 -5
- package/package.json +14 -11
|
@@ -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:
|
|
9
|
-
"claude-opus-4-7": { input:
|
|
10
|
-
"claude-opus-4-6": { input:
|
|
11
|
-
"claude-opus-4-5": { input:
|
|
12
|
-
"claude-opus-4-1": { input:
|
|
13
|
-
"claude-opus-4": { input:
|
|
14
|
-
"claude-sonnet-4-6": { input:
|
|
15
|
-
"claude-sonnet-4-5": { input:
|
|
16
|
-
"claude-sonnet-4": { input:
|
|
17
|
-
"claude-haiku-4-5": { input:
|
|
18
|
-
"claude-haiku-3-5": { input:
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
(
|
|
124
|
-
(
|
|
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
|
|
131
|
-
const
|
|
154
|
+
const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
|
|
155
|
+
const inputTotalTokens = usage.inputTokens +
|
|
156
|
+
cacheWriteBreakdown.cacheWrite5mInputTokens +
|
|
157
|
+
cacheWriteBreakdown.cacheWrite1hInputTokens +
|
|
158
|
+
usage.cacheReadInputTokens;
|
|
132
159
|
return {
|
|
133
|
-
|
|
134
|
-
cachedInputTokens,
|
|
135
|
-
nonCachedInputTokens,
|
|
160
|
+
inputTotalTokens,
|
|
136
161
|
outputTokens: usage.outputTokens,
|
|
137
162
|
reasoningOutputTokens: 0,
|
|
138
|
-
totalTokens:
|
|
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,
|
|
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
|
|
210
|
-
|
|
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,
|
|
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
|
-
|
|
107
|
-
|
|
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
|
|
115
|
+
const inputTotalTokens = usage.inputTokens + usage.cachedInputTokens;
|
|
114
116
|
return {
|
|
115
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|