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.
- package/README.md +35 -9
- package/ink-app/dist/index.js +323 -35
- package/ink-app/dist/providers/claude.js +311 -0
- package/ink-app/dist/providers/codex.js +23 -109
- package/ink-app/dist/providers/contract.js +23 -0
- package/ink-app/dist/providers/copilot.js +380 -0
- package/ink-app/dist/providers/daily.js +64 -0
- package/ink-app/dist/providers/index.js +5 -1
- package/ink-app/dist/providers/limits.js +146 -0
- package/package.json +2 -1
|
@@ -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
|
|
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
|
|
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,
|
|
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
|
-
|
|
295
|
-
|
|
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
|
+
}
|