letmecode 0.1.8 → 0.1.10
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 +2 -0
- package/ink-app/dist/cli-options.js +2 -1
- package/ink-app/dist/providers/antigravity.js +498 -10
- package/ink-app/dist/providers/claude.js +133 -15
- package/ink-app/dist/providers/codex.js +1 -1
- package/ink-app/dist/providers/index.js +1 -1
- package/ink-app/dist/providers/limits.js +57 -23
- package/ink-app/dist/reporting.js +11 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ npx -y letmecode -- -h
|
|
|
8
8
|
npx -y letmecode -- --log-to log.txt
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
`--log-to` now records Claude binary discovery, session-root selection, parsed transcript file summaries, entrypoint matching, raw `/usage` output, and live-window event matching so zero-token windows are diagnosable.
|
|
12
|
+
|
|
11
13
|
<img width="2308" height="1491" alt="image" src="https://github.com/user-attachments/assets/f3f52d79-00e3-4ff5-bf2f-65f8be632aaa" />
|
|
12
14
|
|
|
13
15
|
## Controls
|
|
@@ -62,8 +62,9 @@ export function buildHelpText() {
|
|
|
62
62
|
"",
|
|
63
63
|
"Trace logging:",
|
|
64
64
|
" --log-to PATH writes Claude CLI SDK and Claude VSCode detection details,",
|
|
65
|
+
" session root selection, parsed session file summaries, entrypoint matching,",
|
|
65
66
|
" every candidate binary path check, the final found/not-found result,",
|
|
66
|
-
" and the raw /usage command output."
|
|
67
|
+
" and the raw /usage command output plus live window matching details."
|
|
67
68
|
].join("\n");
|
|
68
69
|
}
|
|
69
70
|
export function createFileTraceLogger(logPath) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import https from "node:https";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
4
|
import fs from "node:fs";
|
|
4
5
|
import os from "node:os";
|
|
@@ -66,6 +67,21 @@ const RATE_CARD = {
|
|
|
66
67
|
const UNPRICED_MODELS = new Set([
|
|
67
68
|
"gpt-oss-120b"
|
|
68
69
|
]);
|
|
70
|
+
const ANTIGRAVITY_PRIMARY_WINDOW_MINUTES = 5 * 60;
|
|
71
|
+
const ANTIGRAVITY_WEEKLY_WINDOW_MINUTES = 7 * 24 * 60;
|
|
72
|
+
const ANTIGRAVITY_QUOTA_SUMMARY_PATH = "/exa.language_server_pb.LanguageServerService/RetrieveUserQuotaSummary";
|
|
73
|
+
const ANTIGRAVITY_DEBUG_LOG_PATH = process.env.LETMECODE_ANTIGRAVITY_DEBUG_LOG ??
|
|
74
|
+
path.join(os.tmpdir(), "letmecode-antigravity-debug.jsonl");
|
|
75
|
+
const GEMINI_QUOTA_MODELS = [
|
|
76
|
+
"gemini-3.5-flash",
|
|
77
|
+
"gemini-3.1-pro",
|
|
78
|
+
"gemini-3-flash"
|
|
79
|
+
];
|
|
80
|
+
const THIRD_PARTY_QUOTA_MODELS = [
|
|
81
|
+
"claude-opus-4-6",
|
|
82
|
+
"claude-sonnet-4-6",
|
|
83
|
+
"gpt-oss-120b"
|
|
84
|
+
];
|
|
69
85
|
const MODEL_ALIASES = {
|
|
70
86
|
"gemini-3-flash-a": "gemini-3-flash",
|
|
71
87
|
"gemini-3-flash-preview": "gemini-3-flash",
|
|
@@ -80,15 +96,33 @@ export class AntigravityUsageProvider extends UsageProviderBase {
|
|
|
80
96
|
this.collectUsage =
|
|
81
97
|
options.collectUsage ??
|
|
82
98
|
collectAntigravityUsageFromTokscale;
|
|
99
|
+
this.collectQuota =
|
|
100
|
+
options.collectQuota ??
|
|
101
|
+
collectAntigravityQuotaFromLocalRpc;
|
|
83
102
|
}
|
|
84
103
|
async getStats(_options = {}) {
|
|
85
104
|
const warnings = [];
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
105
|
+
const [usageResult, quotaResult] = await Promise.allSettled([
|
|
106
|
+
this.collectUsage(),
|
|
107
|
+
this.collectQuota()
|
|
108
|
+
]);
|
|
109
|
+
const records = usageResult.status === "fulfilled"
|
|
110
|
+
? usageResult.value
|
|
111
|
+
: [];
|
|
112
|
+
const quotaSnapshot = quotaResult.status === "fulfilled"
|
|
113
|
+
? quotaResult.value
|
|
114
|
+
: null;
|
|
115
|
+
if (usageResult.status === "rejected") {
|
|
116
|
+
warnings.push("Could not synchronize Antigravity token usage through Tokscale.");
|
|
117
|
+
}
|
|
118
|
+
if (quotaResult.status === "rejected") {
|
|
119
|
+
warnings.push("Live Antigravity quota is unavailable. Ensure the Antigravity IDE is running.");
|
|
89
120
|
}
|
|
90
|
-
|
|
91
|
-
warnings.push("
|
|
121
|
+
else if (quotaResult.value.entries.length === 0) {
|
|
122
|
+
warnings.push("Antigravity local quota RPC responded, but no recognized model quota windows were found.");
|
|
123
|
+
}
|
|
124
|
+
if (isAntigravityDebugEnabled()) {
|
|
125
|
+
warnings.push(`Antigravity debug log: ${ANTIGRAVITY_DEBUG_LOG_PATH}`);
|
|
92
126
|
}
|
|
93
127
|
const selectedRecords = deduplicateRecords(records);
|
|
94
128
|
const duplicateEvents = records.length - selectedRecords.length;
|
|
@@ -116,6 +150,7 @@ export class AntigravityUsageProvider extends UsageProviderBase {
|
|
|
116
150
|
if (unknownPricedModels.length > 0) {
|
|
117
151
|
warnings.push(`No Antigravity estimated API-equivalent rate configured for: ${unknownPricedModels.join(", ")}.`);
|
|
118
152
|
}
|
|
153
|
+
const limitWindows = quotaSnapshot?.entries.map((quota) => buildAntigravityLimitWindow(quota, selectedRecords, quotaSnapshot.fetchedAt)) ?? [];
|
|
119
154
|
return {
|
|
120
155
|
providerId: this.id,
|
|
121
156
|
providerLabel: this.label,
|
|
@@ -125,14 +160,16 @@ export class AntigravityUsageProvider extends UsageProviderBase {
|
|
|
125
160
|
tokenEvents: selectedRecords.length,
|
|
126
161
|
totals: sumUsageTotals(modelUsage.map((row) => row.totals)),
|
|
127
162
|
distinctModels: modelUsage.map((row) => row.modelId),
|
|
128
|
-
distinctPlanTypes: [
|
|
129
|
-
|
|
130
|
-
|
|
163
|
+
distinctPlanTypes: [
|
|
164
|
+
...new Set(limitWindows.map((window) => window.planType))
|
|
165
|
+
],
|
|
166
|
+
rootLabel: "Tokscale usage + Antigravity local quota",
|
|
167
|
+
rootPath: getAntigravityCacheRoot()
|
|
131
168
|
},
|
|
132
169
|
modelUsage,
|
|
133
170
|
dayUsage: buildDailyUsageRows(byDay),
|
|
134
|
-
primaryLimitWindows:
|
|
135
|
-
secondaryLimitWindows:
|
|
171
|
+
primaryLimitWindows: limitWindows.filter((window) => window.scope === "primary"),
|
|
172
|
+
secondaryLimitWindows: limitWindows.filter((window) => window.scope === "secondary"),
|
|
136
173
|
warnings
|
|
137
174
|
};
|
|
138
175
|
}
|
|
@@ -140,6 +177,9 @@ export class AntigravityUsageProvider extends UsageProviderBase {
|
|
|
140
177
|
export async function collectAntigravityUsage() {
|
|
141
178
|
return collectAntigravityUsageFromTokscale();
|
|
142
179
|
}
|
|
180
|
+
export async function collectAntigravityQuota() {
|
|
181
|
+
return collectAntigravityQuotaFromLocalRpc();
|
|
182
|
+
}
|
|
143
183
|
async function collectAntigravityUsageFromTokscale() {
|
|
144
184
|
await runTokscale([
|
|
145
185
|
"antigravity",
|
|
@@ -153,11 +193,459 @@ async function runTokscale(args) {
|
|
|
153
193
|
maxBuffer: 32 * 1024 * 1024
|
|
154
194
|
});
|
|
155
195
|
}
|
|
196
|
+
async function collectAntigravityQuotaFromLocalRpc() {
|
|
197
|
+
const server = await findAntigravityLocalServer();
|
|
198
|
+
if (!server) {
|
|
199
|
+
throw new Error("Antigravity local language server was not found.");
|
|
200
|
+
}
|
|
201
|
+
const fetchedAt = Date.now();
|
|
202
|
+
const payload = await readAntigravityQuotaSummary(server);
|
|
203
|
+
const entries = parseAntigravityQuotaEntries(payload);
|
|
204
|
+
await writeAntigravityDebugEvent("quota-rpc-response", {
|
|
205
|
+
port: server.port,
|
|
206
|
+
path: ANTIGRAVITY_QUOTA_SUMMARY_PATH,
|
|
207
|
+
entries: entries.map((entry) => ({
|
|
208
|
+
limitId: entry.limitId,
|
|
209
|
+
remainingFraction: entry.remainingFraction,
|
|
210
|
+
resetAt: new Date(entry.resetAt).toISOString(),
|
|
211
|
+
windowMinutes: entry.windowMinutes,
|
|
212
|
+
scope: entry.scope,
|
|
213
|
+
modelIds: entry.modelIds
|
|
214
|
+
})),
|
|
215
|
+
...(isAntigravityRawDebugEnabled() ? { payload } : {})
|
|
216
|
+
});
|
|
217
|
+
return {
|
|
218
|
+
entries,
|
|
219
|
+
fetchedAt
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function recordsForQuotaWindow(quota, records) {
|
|
223
|
+
if (quota.modelIds.length === 0) {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
const endMs = quota.resetAt;
|
|
227
|
+
const startMs = endMs - quota.windowMinutes * 60000;
|
|
228
|
+
const modelIds = new Set(quota.modelIds.map(resolveModelId));
|
|
229
|
+
return records.filter((record) => {
|
|
230
|
+
const modelId = resolveModelId(record.modelId);
|
|
231
|
+
return (record.timestamp >= startMs &&
|
|
232
|
+
record.timestamp < endMs &&
|
|
233
|
+
modelIds.has(modelId));
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
function buildAntigravityLimitWindow(quota, records, fetchedAt) {
|
|
237
|
+
const matchingRecords = recordsForQuotaWindow(quota, records);
|
|
238
|
+
const byModel = new Map();
|
|
239
|
+
for (const record of matchingRecords) {
|
|
240
|
+
const modelId = resolveModelId(record.modelId);
|
|
241
|
+
addModelUsage(byModel, modelId, usageRecordToTotals(modelId, record));
|
|
242
|
+
}
|
|
243
|
+
const modelUsage = [...byModel.entries()]
|
|
244
|
+
.map(([modelId, totals]) => ({
|
|
245
|
+
modelId,
|
|
246
|
+
totals
|
|
247
|
+
}))
|
|
248
|
+
.sort((left, right) => right.totals.estimatedCredits -
|
|
249
|
+
left.totals.estimatedCredits);
|
|
250
|
+
const totals = sumUsageTotals(modelUsage.map((row) => row.totals));
|
|
251
|
+
const usedPercent = clampPercent((1 - quota.remainingFraction) * 100);
|
|
252
|
+
// Quota percentage is authoritative from Antigravity RPC. Token totals are
|
|
253
|
+
// reconstructed from locally available Tokscale events inside the same time
|
|
254
|
+
// window and may not match Antigravity's internal quota accounting exactly.
|
|
255
|
+
return {
|
|
256
|
+
scope: quota.scope,
|
|
257
|
+
planType: quota.planType,
|
|
258
|
+
limitId: quota.limitId,
|
|
259
|
+
windowMinutes: quota.windowMinutes,
|
|
260
|
+
startTimeUtcIso: new Date(quota.resetAt - quota.windowMinutes * 60000).toISOString(),
|
|
261
|
+
endTimeUtcIso: new Date(quota.resetAt).toISOString(),
|
|
262
|
+
firstSeenUtcIso: new Date(fetchedAt).toISOString(),
|
|
263
|
+
lastSeenUtcIso: new Date(fetchedAt).toISOString(),
|
|
264
|
+
minUsedPercent: usedPercent,
|
|
265
|
+
maxUsedPercent: usedPercent,
|
|
266
|
+
totals,
|
|
267
|
+
modelUsage,
|
|
268
|
+
eventCount: totals.eventCount
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const antigravityHttpsAgent = new https.Agent({
|
|
272
|
+
rejectUnauthorized: false
|
|
273
|
+
});
|
|
274
|
+
async function findAntigravityLocalServer() {
|
|
275
|
+
const process = await findAntigravityProcess();
|
|
276
|
+
if (!process) {
|
|
277
|
+
await writeAntigravityDebugEvent("process-not-found", {});
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
const ports = await findListeningLoopbackPorts(process.pid);
|
|
281
|
+
await writeAntigravityDebugEvent("process-found", {
|
|
282
|
+
pid: process.pid,
|
|
283
|
+
ports
|
|
284
|
+
});
|
|
285
|
+
return probeAntigravityPorts(ports, process.csrfToken);
|
|
286
|
+
}
|
|
287
|
+
async function findAntigravityProcess() {
|
|
288
|
+
const fromProc = await findAntigravityProcessFromProc();
|
|
289
|
+
if (fromProc) {
|
|
290
|
+
return fromProc;
|
|
291
|
+
}
|
|
292
|
+
return findAntigravityProcessFromPs();
|
|
293
|
+
}
|
|
294
|
+
async function findAntigravityProcessFromProc() {
|
|
295
|
+
let entries;
|
|
296
|
+
try {
|
|
297
|
+
entries = await fs.promises.readdir("/proc", { withFileTypes: true });
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
for (const entry of entries) {
|
|
303
|
+
if (!entry.isDirectory() || !/^\d+$/.test(entry.name)) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const pid = Number(entry.name);
|
|
307
|
+
const cmdline = await readProcCmdline(path.join("/proc", entry.name, "cmdline"));
|
|
308
|
+
const process = parseAntigravityProcessFromArgs(pid, cmdline);
|
|
309
|
+
if (process) {
|
|
310
|
+
return process;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
async function readProcCmdline(filePath) {
|
|
316
|
+
try {
|
|
317
|
+
const content = await fs.promises.readFile(filePath);
|
|
318
|
+
return content
|
|
319
|
+
.toString("utf8")
|
|
320
|
+
.split("\0")
|
|
321
|
+
.filter(Boolean);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function findAntigravityProcessFromPs() {
|
|
328
|
+
try {
|
|
329
|
+
const { stdout } = await execFileAsync("ps", ["-eo", "pid=,args="], {
|
|
330
|
+
encoding: "utf8",
|
|
331
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
332
|
+
timeout: 5000
|
|
333
|
+
});
|
|
334
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
335
|
+
const match = line.match(/^\s*(\d+)\s+(.+)$/);
|
|
336
|
+
if (!match) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const process = parseAntigravityProcessFromArgs(Number(match[1]), splitCommandLineForDiscovery(match[2]));
|
|
340
|
+
if (process) {
|
|
341
|
+
return process;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
function splitCommandLineForDiscovery(value) {
|
|
351
|
+
return value.match(/"[^"]*"|'[^']*'|\S+/g)?.map((part) => part.replace(/^['"]|['"]$/g, "")) ?? [];
|
|
352
|
+
}
|
|
353
|
+
function parseAntigravityProcessFromArgs(pid, args) {
|
|
354
|
+
if (!Number.isInteger(pid) || pid <= 0 || !isAntigravityLanguageServerCommand(args)) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
const csrfToken = readNamedArg(args, "--csrf_token")?.trim();
|
|
358
|
+
if (!csrfToken) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
return { pid, csrfToken };
|
|
362
|
+
}
|
|
363
|
+
function isAntigravityLanguageServerCommand(args) {
|
|
364
|
+
const normalized = args.join(" ").toLowerCase();
|
|
365
|
+
return (normalized.includes("antigravity") &&
|
|
366
|
+
(normalized.includes("language-server") ||
|
|
367
|
+
normalized.includes("language_server") ||
|
|
368
|
+
normalized.includes("extension-server") ||
|
|
369
|
+
normalized.includes("extension_server")));
|
|
370
|
+
}
|
|
371
|
+
function readNamedArg(args, name) {
|
|
372
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
373
|
+
const arg = args[index];
|
|
374
|
+
if (arg === name) {
|
|
375
|
+
return args[index + 1] ?? null;
|
|
376
|
+
}
|
|
377
|
+
if (arg.startsWith(`${name}=`)) {
|
|
378
|
+
return arg.slice(name.length + 1);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
async function findListeningLoopbackPorts(pid) {
|
|
384
|
+
const parsers = [
|
|
385
|
+
() => findListeningLoopbackPortsWithSs(pid),
|
|
386
|
+
() => findListeningLoopbackPortsWithLsof(pid)
|
|
387
|
+
];
|
|
388
|
+
for (const parse of parsers) {
|
|
389
|
+
const ports = await parse();
|
|
390
|
+
if (ports.length > 0) {
|
|
391
|
+
return ports;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return [];
|
|
395
|
+
}
|
|
396
|
+
async function findListeningLoopbackPortsWithSs(pid) {
|
|
397
|
+
try {
|
|
398
|
+
const { stdout } = await execFileAsync("ss", ["-H", "-ltnp"], {
|
|
399
|
+
encoding: "utf8",
|
|
400
|
+
maxBuffer: 1024 * 1024,
|
|
401
|
+
timeout: 5000
|
|
402
|
+
});
|
|
403
|
+
return uniquePorts(stdout
|
|
404
|
+
.split(/\r?\n/)
|
|
405
|
+
.filter((line) => line.includes(`pid=${pid},`) && isLoopbackListenLine(line))
|
|
406
|
+
.map(extractPortFromListenLine));
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return [];
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async function findListeningLoopbackPortsWithLsof(pid) {
|
|
413
|
+
try {
|
|
414
|
+
const { stdout } = await execFileAsync("lsof", ["-Pan", "-p", String(pid), "-iTCP", "-sTCP:LISTEN"], {
|
|
415
|
+
encoding: "utf8",
|
|
416
|
+
maxBuffer: 1024 * 1024,
|
|
417
|
+
timeout: 5000
|
|
418
|
+
});
|
|
419
|
+
return uniquePorts(stdout
|
|
420
|
+
.split(/\r?\n/)
|
|
421
|
+
.filter(isLoopbackListenLine)
|
|
422
|
+
.map(extractPortFromListenLine));
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
return [];
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function isLoopbackListenLine(line) {
|
|
429
|
+
return /(?:127\.0\.0\.1|localhost|\[::1\]|::1):\d+\b/.test(line);
|
|
430
|
+
}
|
|
431
|
+
function extractPortFromListenLine(line) {
|
|
432
|
+
const matches = [...line.matchAll(/(?:127\.0\.0\.1|localhost|\[::1\]|::1):(\d+)/g)];
|
|
433
|
+
const value = matches.at(-1)?.[1];
|
|
434
|
+
const port = value ? Number(value) : NaN;
|
|
435
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
|
|
436
|
+
}
|
|
437
|
+
function uniquePorts(ports) {
|
|
438
|
+
return [...new Set(ports.filter((port) => port !== null))];
|
|
439
|
+
}
|
|
440
|
+
async function probeAntigravityPorts(ports, csrfToken) {
|
|
441
|
+
for (const port of ports) {
|
|
442
|
+
const server = { port, csrfToken };
|
|
443
|
+
try {
|
|
444
|
+
await readAntigravityQuotaSummary(server);
|
|
445
|
+
await writeAntigravityDebugEvent("port-probe-ok", { port });
|
|
446
|
+
return server;
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
await writeAntigravityDebugEvent("port-probe-failed", {
|
|
450
|
+
port,
|
|
451
|
+
error: error instanceof Error ? error.message : String(error)
|
|
452
|
+
});
|
|
453
|
+
// Try the next loopback listener owned by the same Antigravity process.
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
async function readAntigravityQuotaSummary(server) {
|
|
459
|
+
return requestAntigravityQuotaSummary(server);
|
|
460
|
+
}
|
|
461
|
+
function requestAntigravityQuotaSummary(server) {
|
|
462
|
+
const body = "{}";
|
|
463
|
+
return new Promise((resolve, reject) => {
|
|
464
|
+
const request = https.request({
|
|
465
|
+
hostname: "127.0.0.1",
|
|
466
|
+
port: server.port,
|
|
467
|
+
path: ANTIGRAVITY_QUOTA_SUMMARY_PATH,
|
|
468
|
+
method: "POST",
|
|
469
|
+
timeout: 5000,
|
|
470
|
+
agent: antigravityHttpsAgent,
|
|
471
|
+
headers: {
|
|
472
|
+
"X-Codeium-Csrf-Token": server.csrfToken,
|
|
473
|
+
"Content-Type": "application/json",
|
|
474
|
+
"Connect-Protocol-Version": "1",
|
|
475
|
+
Accept: "application/json",
|
|
476
|
+
"Content-Length": Buffer.byteLength(body)
|
|
477
|
+
}
|
|
478
|
+
}, (response) => {
|
|
479
|
+
const chunks = [];
|
|
480
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
481
|
+
response.on("end", () => {
|
|
482
|
+
const responseBody = Buffer.concat(chunks).toString("utf8");
|
|
483
|
+
if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300) {
|
|
484
|
+
reject(new Error(`Unexpected Antigravity quota summary response: ${response.statusCode ?? "unknown"}`));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
resolve(responseBody ? JSON.parse(responseBody) : {});
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
reject(error);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
request.on("timeout", () => {
|
|
496
|
+
request.destroy(new Error("Timed out reading Antigravity quota summary."));
|
|
497
|
+
});
|
|
498
|
+
request.on("error", reject);
|
|
499
|
+
request.end(body);
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
export function parseAntigravityQuotaEntries(payload) {
|
|
503
|
+
const root = asRecord(payload);
|
|
504
|
+
const response = asRecord(root?.response);
|
|
505
|
+
const groups = asArray(response?.groups);
|
|
506
|
+
const entries = [];
|
|
507
|
+
for (const groupValue of groups) {
|
|
508
|
+
const group = asRecord(groupValue);
|
|
509
|
+
if (!group) {
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const displayName = asString(group.displayName) ?? "";
|
|
513
|
+
const description = asString(group.description) ?? "";
|
|
514
|
+
const modelIds = resolveQuotaGroupModelIds(displayName, description);
|
|
515
|
+
if (modelIds.length === 0) {
|
|
516
|
+
void writeAntigravityDebugEvent("quota-group-skipped", {
|
|
517
|
+
displayName,
|
|
518
|
+
description
|
|
519
|
+
});
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
for (const bucketValue of asArray(group.buckets)) {
|
|
523
|
+
const bucket = asRecord(bucketValue);
|
|
524
|
+
if (!bucket) {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
const bucketId = asString(bucket.bucketId);
|
|
528
|
+
const windowConfig = resolveQuotaWindow(asString(bucket.window));
|
|
529
|
+
const remainingFraction = asFiniteNumber(bucket.remainingFraction);
|
|
530
|
+
const resetTime = asString(bucket.resetTime);
|
|
531
|
+
const resetAt = resetTime === null ? NaN : Date.parse(resetTime);
|
|
532
|
+
if (!bucketId ||
|
|
533
|
+
remainingFraction === null ||
|
|
534
|
+
remainingFraction < 0 ||
|
|
535
|
+
remainingFraction > 1 ||
|
|
536
|
+
!Number.isFinite(resetAt) ||
|
|
537
|
+
!windowConfig) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
entries.push({
|
|
541
|
+
limitId: bucketId,
|
|
542
|
+
modelIds,
|
|
543
|
+
remainingFraction,
|
|
544
|
+
resetAt,
|
|
545
|
+
windowMinutes: windowConfig.windowMinutes,
|
|
546
|
+
scope: windowConfig.scope,
|
|
547
|
+
planType: "unknown"
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return entries;
|
|
552
|
+
}
|
|
553
|
+
function resolveQuotaWindow(window) {
|
|
554
|
+
switch (window) {
|
|
555
|
+
case "5h":
|
|
556
|
+
return {
|
|
557
|
+
scope: "primary",
|
|
558
|
+
windowMinutes: ANTIGRAVITY_PRIMARY_WINDOW_MINUTES
|
|
559
|
+
};
|
|
560
|
+
case "weekly":
|
|
561
|
+
return {
|
|
562
|
+
scope: "secondary",
|
|
563
|
+
windowMinutes: ANTIGRAVITY_WEEKLY_WINDOW_MINUTES
|
|
564
|
+
};
|
|
565
|
+
default:
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function resolveQuotaGroupModelIds(displayName, description) {
|
|
570
|
+
const text = `${displayName} ${description}`.toLowerCase();
|
|
571
|
+
if (text.includes("gemini")) {
|
|
572
|
+
return GEMINI_QUOTA_MODELS;
|
|
573
|
+
}
|
|
574
|
+
if (text.includes("claude") || text.includes("gpt")) {
|
|
575
|
+
return THIRD_PARTY_QUOTA_MODELS;
|
|
576
|
+
}
|
|
577
|
+
return [];
|
|
578
|
+
}
|
|
579
|
+
function asRecord(value) {
|
|
580
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
581
|
+
? value
|
|
582
|
+
: null;
|
|
583
|
+
}
|
|
584
|
+
function asArray(value) {
|
|
585
|
+
return Array.isArray(value) ? value : [];
|
|
586
|
+
}
|
|
587
|
+
function asString(value) {
|
|
588
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
589
|
+
}
|
|
590
|
+
function asFiniteNumber(value) {
|
|
591
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
592
|
+
}
|
|
593
|
+
function isAntigravityDebugEnabled() {
|
|
594
|
+
const value = process.env.LETMECODE_DEBUG_ANTIGRAVITY;
|
|
595
|
+
return value === "1" || value === "true" || value === "yes";
|
|
596
|
+
}
|
|
597
|
+
function isAntigravityRawDebugEnabled() {
|
|
598
|
+
const value = process.env.LETMECODE_DEBUG_ANTIGRAVITY_RAW;
|
|
599
|
+
return value === "1" || value === "true" || value === "yes";
|
|
600
|
+
}
|
|
601
|
+
async function writeAntigravityDebugEvent(event, data) {
|
|
602
|
+
if (!isAntigravityDebugEnabled()) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const line = JSON.stringify({
|
|
606
|
+
timestamp: new Date().toISOString(),
|
|
607
|
+
event,
|
|
608
|
+
data: redactDebugValue(data)
|
|
609
|
+
});
|
|
610
|
+
try {
|
|
611
|
+
await fs.promises.mkdir(path.dirname(ANTIGRAVITY_DEBUG_LOG_PATH), {
|
|
612
|
+
recursive: true
|
|
613
|
+
});
|
|
614
|
+
await fs.promises.appendFile(ANTIGRAVITY_DEBUG_LOG_PATH, `${line}\n`, "utf8");
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
// Debug logging must never break provider stats collection.
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function redactDebugValue(value, key = "") {
|
|
621
|
+
if (isSensitiveDebugKey(key)) {
|
|
622
|
+
return "[redacted]";
|
|
623
|
+
}
|
|
624
|
+
if (Array.isArray(value)) {
|
|
625
|
+
return value.map((item) => redactDebugValue(item));
|
|
626
|
+
}
|
|
627
|
+
if (!value || typeof value !== "object") {
|
|
628
|
+
return value;
|
|
629
|
+
}
|
|
630
|
+
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
|
|
631
|
+
entryKey,
|
|
632
|
+
redactDebugValue(entryValue, entryKey)
|
|
633
|
+
]));
|
|
634
|
+
}
|
|
635
|
+
function isSensitiveDebugKey(key) {
|
|
636
|
+
return /token|csrf|authorization|cookie|email/i.test(key);
|
|
637
|
+
}
|
|
156
638
|
function numberOrZero(value) {
|
|
157
639
|
return typeof value === "number" && Number.isFinite(value)
|
|
158
640
|
? value
|
|
159
641
|
: 0;
|
|
160
642
|
}
|
|
643
|
+
function clampPercent(value) {
|
|
644
|
+
if (!Number.isFinite(value)) {
|
|
645
|
+
return 0;
|
|
646
|
+
}
|
|
647
|
+
return Math.min(100, Math.max(0, value));
|
|
648
|
+
}
|
|
161
649
|
async function readAntigravityUsageCache(cacheRoot) {
|
|
162
650
|
const sessionsRoot = path.join(cacheRoot, "sessions");
|
|
163
651
|
const records = [];
|
|
@@ -58,7 +58,8 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
58
58
|
this.now = options.now ?? (() => new Date());
|
|
59
59
|
}
|
|
60
60
|
async getStats(options = {}) {
|
|
61
|
-
|
|
61
|
+
traceClaude(options.traceLogger, this.usageCommandKind, `Starting stats collection with root=${this.root} entrypoints=[${[...this.entrypoints].join(", ")}].`);
|
|
62
|
+
const resolvedSessionsRoot = await resolveClaudeSessionsRoot(this.root, this.usageCommandKind, options.traceLogger);
|
|
62
63
|
const sessionsRoot = resolvedSessionsRoot.rootPath;
|
|
63
64
|
const agentName = normalizeAnalyticsAgentName(this.label);
|
|
64
65
|
const userIdHash = await readClaudeUserIdHash(this.root, this.usageCommandKind, this.readAuthStatusOutput, agentName, options.traceLogger);
|
|
@@ -74,9 +75,19 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
74
75
|
tokenEvents: 0,
|
|
75
76
|
malformedLines: 0
|
|
76
77
|
};
|
|
77
|
-
const parsedSessionFiles = await loadParsedClaudeSessionFiles(sessionsRoot);
|
|
78
|
+
const parsedSessionFiles = await loadParsedClaudeSessionFiles(sessionsRoot, this.usageCommandKind, options.traceLogger);
|
|
79
|
+
traceClaude(options.traceLogger, this.usageCommandKind, `Loaded ${parsedSessionFiles.length} parsed session file(s) from ${sessionsRoot}.`);
|
|
78
80
|
for (const file of parsedSessionFiles) {
|
|
79
81
|
const matchingEvents = file.events.filter((event) => this.entrypoints.has(event.entrypoint));
|
|
82
|
+
traceClaude(options.traceLogger, this.usageCommandKind, [
|
|
83
|
+
`Session file ${describeSessionFilePath(sessionsRoot, file.filePath)}:`,
|
|
84
|
+
`lines=${file.linesRead}`,
|
|
85
|
+
`malformed=${file.malformedLines}`,
|
|
86
|
+
`assistantUsageEvents=${file.events.length}`,
|
|
87
|
+
`matchingEvents=${matchingEvents.length}`,
|
|
88
|
+
`entrypoints=${summarizeEventCounts(file.events.map((event) => event.entrypoint || "<empty>"))}`,
|
|
89
|
+
`models=${summarizeDistinctValues(file.events.map((event) => event.modelId || "unknown"))}`
|
|
90
|
+
].join(" "));
|
|
80
91
|
if (matchingEvents.length === 0) {
|
|
81
92
|
continue;
|
|
82
93
|
}
|
|
@@ -91,12 +102,22 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
91
102
|
...parsedEvents.keyedEvents.values(),
|
|
92
103
|
...parsedEvents.unkeyedEvents.values()
|
|
93
104
|
];
|
|
105
|
+
traceClaude(options.traceLogger, this.usageCommandKind, [
|
|
106
|
+
`Transcript selection summary: filesWithMatches=${parseTotals.filesScanned}/${parsedSessionFiles.length}`,
|
|
107
|
+
`selectedEvents=${selectedEvents.length}`,
|
|
108
|
+
`duplicateUsageKeys=${parsedEvents.duplicateUsageKeys}`,
|
|
109
|
+
`duplicateUsageKeyCollisions=${parsedEvents.duplicateUsageKeyCollisions}`,
|
|
110
|
+
`duplicateUnkeyedEvents=${parsedEvents.duplicateUnkeyedEvents}`
|
|
111
|
+
].join(" "));
|
|
112
|
+
if (selectedEvents.length === 0 && parsedSessionFiles.length > 0) {
|
|
113
|
+
traceClaude(options.traceLogger, this.usageCommandKind, `No transcript usage matched entrypoints [${[...this.entrypoints].join(", ")}]. Observed entrypoints=${summarizeEventCounts(collectEntryPoints(parsedSessionFiles))}.`);
|
|
114
|
+
}
|
|
94
115
|
for (const event of selectedEvents) {
|
|
95
116
|
addModelUsage(byModel, event.modelId, event.totals);
|
|
96
117
|
const planType = typeof event.rateLimits?.plan_type === "string" ? event.rateLimits.plan_type : undefined;
|
|
97
118
|
const safeEventTimeMs = Number.isFinite(event.timestampMs) ? event.timestampMs : 0;
|
|
98
119
|
addDailyUsage(byDay, event.timestampMs, event.modelId, planType, event.totals);
|
|
99
|
-
applyRateLimits(windows, event.rateLimits, safeEventTimeMs, event.totals, planTypes);
|
|
120
|
+
applyRateLimits(windows, event.rateLimits, safeEventTimeMs, event.modelId, event.totals, planTypes);
|
|
100
121
|
}
|
|
101
122
|
parseTotals.tokenEvents = selectedEvents.length;
|
|
102
123
|
if (parseTotals.malformedLines > 0) {
|
|
@@ -141,6 +162,19 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
141
162
|
const secondaryLimitWindows = liveLimitWindows.secondaryLimitWindows.length > 0
|
|
142
163
|
? liveLimitWindows.secondaryLimitWindows
|
|
143
164
|
: fallbackSecondaryLimitWindows;
|
|
165
|
+
traceClaude(options.traceLogger, this.usageCommandKind, [
|
|
166
|
+
`Finished stats collection:`,
|
|
167
|
+
`filesScanned=${parseTotals.filesScanned}`,
|
|
168
|
+
`linesRead=${parseTotals.linesRead}`,
|
|
169
|
+
`tokenEvents=${parseTotals.tokenEvents}`,
|
|
170
|
+
`models=${modelUsage.length}`,
|
|
171
|
+
`primaryWindows=${primaryLimitWindows.length}`,
|
|
172
|
+
`secondaryWindows=${secondaryLimitWindows.length}`,
|
|
173
|
+
`input=${summaryTotals.inputTokens}`,
|
|
174
|
+
`output=${summaryTotals.outputTokens}`,
|
|
175
|
+
`cacheRead=${summaryTotals.cacheReadInputTokens}`,
|
|
176
|
+
`cacheWrite=${summaryTotals.cacheWriteInputTokens}`
|
|
177
|
+
].join(" "));
|
|
144
178
|
return {
|
|
145
179
|
providerId: this.id,
|
|
146
180
|
providerLabel: this.label,
|
|
@@ -240,17 +274,23 @@ function resolveClaudeCacheWriteBreakdown(usage) {
|
|
|
240
274
|
function isSessionFile(filePath) {
|
|
241
275
|
return filePath.endsWith(".jsonl");
|
|
242
276
|
}
|
|
243
|
-
async function resolveClaudeSessionsRoot(root) {
|
|
277
|
+
async function resolveClaudeSessionsRoot(root, usageCommandKind, traceLogger) {
|
|
244
278
|
const candidates = buildClaudeSessionsRootCandidates(root);
|
|
279
|
+
traceClaude(traceLogger, usageCommandKind, `Checking ${candidates.length} Claude session root candidate(s).`);
|
|
245
280
|
for (const candidate of candidates) {
|
|
246
|
-
|
|
281
|
+
const exists = await isDirectory(candidate.rootPath);
|
|
282
|
+
traceClaude(traceLogger, usageCommandKind, `Session root candidate ${candidate.rootLabel} -> ${candidate.rootPath} (${exists ? "exists" : "missing"}).`);
|
|
283
|
+
if (exists) {
|
|
284
|
+
traceClaude(traceLogger, usageCommandKind, `Selected session root ${candidate.rootLabel} -> ${candidate.rootPath}.`);
|
|
247
285
|
return candidate;
|
|
248
286
|
}
|
|
249
287
|
}
|
|
250
|
-
|
|
288
|
+
const fallbackCandidate = candidates[0] ?? {
|
|
251
289
|
rootLabel: "~/.claude/projects",
|
|
252
290
|
rootPath: path.join(path.resolve(root), ".claude", "projects")
|
|
253
291
|
};
|
|
292
|
+
traceClaude(traceLogger, usageCommandKind, `No session root candidate exists yet; defaulting to ${fallbackCandidate.rootLabel} -> ${fallbackCandidate.rootPath}.`);
|
|
293
|
+
return fallbackCandidate;
|
|
254
294
|
}
|
|
255
295
|
function buildClaudeSessionsRootCandidates(root) {
|
|
256
296
|
const resolvedRoot = path.resolve(root);
|
|
@@ -338,17 +378,21 @@ async function* walkSessionFiles(directory) {
|
|
|
338
378
|
}
|
|
339
379
|
}
|
|
340
380
|
}
|
|
341
|
-
async function loadParsedClaudeSessionFiles(sessionsRoot) {
|
|
381
|
+
async function loadParsedClaudeSessionFiles(sessionsRoot, usageCommandKind, traceLogger) {
|
|
342
382
|
const cacheKey = path.resolve(sessionsRoot);
|
|
343
383
|
const cached = parsedClaudeSessionFilesCache.get(cacheKey);
|
|
344
384
|
if (cached) {
|
|
345
|
-
|
|
385
|
+
const files = await cached;
|
|
386
|
+
traceClaude(traceLogger, usageCommandKind, `Session parse cache hit for ${sessionsRoot} (${files.length} file(s)).`);
|
|
387
|
+
return files;
|
|
346
388
|
}
|
|
347
389
|
const pending = (async () => {
|
|
348
390
|
const files = [];
|
|
391
|
+
traceClaude(traceLogger, usageCommandKind, `Scanning session files under ${sessionsRoot}.`);
|
|
349
392
|
for await (const filePath of walkSessionFiles(sessionsRoot)) {
|
|
350
393
|
files.push(await parseSessionFile(filePath));
|
|
351
394
|
}
|
|
395
|
+
traceClaude(traceLogger, usageCommandKind, `Completed session file scan under ${sessionsRoot}: ${files.length} file(s) parsed.`);
|
|
352
396
|
return files;
|
|
353
397
|
})();
|
|
354
398
|
parsedClaudeSessionFilesCache.set(cacheKey, pending);
|
|
@@ -397,7 +441,7 @@ async function parseSessionFile(filePath) {
|
|
|
397
441
|
rateLimits
|
|
398
442
|
});
|
|
399
443
|
}
|
|
400
|
-
return { linesRead, malformedLines, events };
|
|
444
|
+
return { filePath, linesRead, malformedLines, events };
|
|
401
445
|
}
|
|
402
446
|
function buildUsageEventKey(payloadObject, message) {
|
|
403
447
|
const sessionId = String(payloadObject.sessionId ?? "");
|
|
@@ -493,6 +537,41 @@ function describeUsageOutput(output) {
|
|
|
493
537
|
}
|
|
494
538
|
return output.trim() ? output : "<empty>";
|
|
495
539
|
}
|
|
540
|
+
function describeSessionFilePath(sessionsRoot, filePath) {
|
|
541
|
+
const relativePath = path.relative(sessionsRoot, filePath);
|
|
542
|
+
if (!relativePath || relativePath.startsWith("..")) {
|
|
543
|
+
return filePath;
|
|
544
|
+
}
|
|
545
|
+
return relativePath;
|
|
546
|
+
}
|
|
547
|
+
function summarizeEventCounts(values) {
|
|
548
|
+
const counts = new Map();
|
|
549
|
+
for (const value of values) {
|
|
550
|
+
const normalizedValue = value || "<empty>";
|
|
551
|
+
counts.set(normalizedValue, (counts.get(normalizedValue) ?? 0) + 1);
|
|
552
|
+
}
|
|
553
|
+
if (counts.size === 0) {
|
|
554
|
+
return "<none>";
|
|
555
|
+
}
|
|
556
|
+
return [...counts.entries()]
|
|
557
|
+
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
|
|
558
|
+
.map(([value, count]) => `${value}:${count}`)
|
|
559
|
+
.join(", ");
|
|
560
|
+
}
|
|
561
|
+
function summarizeDistinctValues(values, limit = 5) {
|
|
562
|
+
const distinctValues = [...new Set([...values].filter(Boolean))].sort();
|
|
563
|
+
if (distinctValues.length === 0) {
|
|
564
|
+
return "<none>";
|
|
565
|
+
}
|
|
566
|
+
const visibleValues = distinctValues.slice(0, limit);
|
|
567
|
+
const remainder = distinctValues.length - visibleValues.length;
|
|
568
|
+
return remainder > 0
|
|
569
|
+
? `${visibleValues.join(", ")} (+${remainder} more)`
|
|
570
|
+
: visibleValues.join(", ");
|
|
571
|
+
}
|
|
572
|
+
function collectEntryPoints(files) {
|
|
573
|
+
return files.flatMap((file) => file.events.map((event) => event.entrypoint || "<empty>"));
|
|
574
|
+
}
|
|
496
575
|
function buildClaudeCommandEnvironment() {
|
|
497
576
|
return {
|
|
498
577
|
...process.env,
|
|
@@ -506,19 +585,47 @@ async function buildLiveLimitWindows(options) {
|
|
|
506
585
|
]);
|
|
507
586
|
const snapshots = parseLiveUsageWindowSnapshots(usageOutput, options.now);
|
|
508
587
|
traceClaude(options.traceLogger, options.usageCommandKind, `Parsed ${snapshots.length} live usage snapshot(s) from /usage output.`);
|
|
588
|
+
if (snapshots.length === 0) {
|
|
589
|
+
traceClaude(options.traceLogger, options.usageCommandKind, "No live usage snapshots matched the expected /usage format.");
|
|
590
|
+
}
|
|
509
591
|
const resolvedPlanType = subscriptionType || "live";
|
|
592
|
+
traceClaude(options.traceLogger, options.usageCommandKind, `Resolved live plan type ${resolvedPlanType}.`);
|
|
593
|
+
const primaryLimitWindows = snapshots
|
|
594
|
+
.filter((snapshot) => snapshot.scope === "primary")
|
|
595
|
+
.map((snapshot) => buildLiveLimitWindowRow(snapshot, resolvedPlanType, options.selectedEvents, options.now));
|
|
596
|
+
const secondaryLimitWindows = snapshots
|
|
597
|
+
.filter((snapshot) => snapshot.scope === "secondary")
|
|
598
|
+
.map((snapshot) => buildLiveLimitWindowRow(snapshot, resolvedPlanType, options.selectedEvents, options.now));
|
|
599
|
+
for (let index = 0; index < snapshots.length; index += 1) {
|
|
600
|
+
const snapshot = snapshots[index];
|
|
601
|
+
const row = snapshot.scope === "primary"
|
|
602
|
+
? primaryLimitWindows.filter((window) => window.limitId === `current-${snapshot.label}`)[0]
|
|
603
|
+
: secondaryLimitWindows.filter((window) => window.limitId === `current-${snapshot.label}`)[0];
|
|
604
|
+
if (!row) {
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
traceClaude(options.traceLogger, options.usageCommandKind, [
|
|
608
|
+
`Live window ${snapshot.scope}/${snapshot.label}:`,
|
|
609
|
+
`used=${snapshot.usedPercent}%`,
|
|
610
|
+
`range=${row.startTimeUtcIso}->${row.endTimeUtcIso}`,
|
|
611
|
+
`matchedEvents=${row.eventCount}`,
|
|
612
|
+
`input=${row.totals.inputTokens}`,
|
|
613
|
+
`output=${row.totals.outputTokens}`,
|
|
614
|
+
`cacheRead=${row.totals.cacheReadInputTokens}`,
|
|
615
|
+
`cacheWrite=${row.totals.cacheWriteInputTokens}`
|
|
616
|
+
].join(" "));
|
|
617
|
+
}
|
|
510
618
|
return {
|
|
511
|
-
primaryLimitWindows
|
|
512
|
-
|
|
513
|
-
.map((snapshot) => buildLiveLimitWindowRow(snapshot, resolvedPlanType, options.selectedEvents, options.now)),
|
|
514
|
-
secondaryLimitWindows: snapshots
|
|
515
|
-
.filter((snapshot) => snapshot.scope === "secondary")
|
|
516
|
-
.map((snapshot) => buildLiveLimitWindowRow(snapshot, resolvedPlanType, options.selectedEvents, options.now))
|
|
619
|
+
primaryLimitWindows,
|
|
620
|
+
secondaryLimitWindows
|
|
517
621
|
};
|
|
518
622
|
}
|
|
519
623
|
async function readClaudeSubscriptionType(root, usageCommandKind, override, traceLogger) {
|
|
520
624
|
const output = await readClaudeAuthStatusOutput(root, usageCommandKind, override, traceLogger);
|
|
521
625
|
const subscriptionType = parseClaudeSubscriptionType(output);
|
|
626
|
+
if (output && !subscriptionType) {
|
|
627
|
+
traceClaude(traceLogger, usageCommandKind, "Could not parse subscription type from auth status output.");
|
|
628
|
+
}
|
|
522
629
|
traceClaude(traceLogger, usageCommandKind, `Subscription type result: ${subscriptionType ?? "<none>"}.`);
|
|
523
630
|
return subscriptionType;
|
|
524
631
|
}
|
|
@@ -602,6 +709,7 @@ async function readClaudeUserIdHash(root, usageCommandKind, override, agentName,
|
|
|
602
709
|
const authStatusOutput = await readClaudeAuthStatusOutput(root, usageCommandKind, override, traceLogger);
|
|
603
710
|
const snapshot = parseClaudeAuthStatusSnapshot(authStatusOutput);
|
|
604
711
|
if (!snapshot) {
|
|
712
|
+
traceClaude(traceLogger, usageCommandKind, "Auth status output did not yield an analytics identity snapshot.");
|
|
605
713
|
return null;
|
|
606
714
|
}
|
|
607
715
|
return buildUserIdHash([agentName, snapshot.email, snapshot.orgId, snapshot.orgName]);
|
|
@@ -872,9 +980,19 @@ function buildLiveLimitWindowRow(snapshot, planType, selectedEvents, now) {
|
|
|
872
980
|
minUsedPercent: snapshot.usedPercent,
|
|
873
981
|
maxUsedPercent: snapshot.usedPercent,
|
|
874
982
|
totals,
|
|
983
|
+
modelUsage: buildModelUsageRowsForEvents(inWindowEvents),
|
|
875
984
|
eventCount: totals.eventCount
|
|
876
985
|
};
|
|
877
986
|
}
|
|
987
|
+
function buildModelUsageRowsForEvents(events) {
|
|
988
|
+
const byModel = new Map();
|
|
989
|
+
for (const event of events) {
|
|
990
|
+
addModelUsage(byModel, event.modelId, event.totals);
|
|
991
|
+
}
|
|
992
|
+
return [...byModel.entries()]
|
|
993
|
+
.map(([modelId, totals]) => ({ modelId, totals }))
|
|
994
|
+
.sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
|
|
995
|
+
}
|
|
878
996
|
function toUtcIso(value) {
|
|
879
997
|
return new Date(value).toISOString().replace(".000Z", "Z");
|
|
880
998
|
}
|
|
@@ -349,7 +349,7 @@ async function parseSessionFile(filePath, byModel, byDay, windows, planTypes, kn
|
|
|
349
349
|
const rateLimits = asRecord(payload.rate_limits);
|
|
350
350
|
const planType = typeof rateLimits?.plan_type === "string" ? rateLimits.plan_type : undefined;
|
|
351
351
|
addDailyUsage(byDay, eventTimeMs, resolvedModelId, planType, deltaTotals);
|
|
352
|
-
applyRateLimits(windows, rateLimits, safeEventTimeMs, deltaTotals, planTypes);
|
|
352
|
+
applyRateLimits(windows, rateLimits, safeEventTimeMs, resolvedModelId, deltaTotals, planTypes);
|
|
353
353
|
}
|
|
354
354
|
return { linesRead, tokenEvents, malformedLines };
|
|
355
355
|
}
|
|
@@ -16,7 +16,7 @@ export function createProviders() {
|
|
|
16
16
|
new AntigravityUsageProvider()
|
|
17
17
|
];
|
|
18
18
|
}
|
|
19
|
-
export { AntigravityUsageProvider } from "./antigravity.js";
|
|
19
|
+
export { AntigravityUsageProvider, collectAntigravityQuota, collectAntigravityUsage } from "./antigravity.js";
|
|
20
20
|
export { ClaudeUsageProvider } from "./claude.js";
|
|
21
21
|
export { CodexUsageProvider } from "./codex.js";
|
|
22
22
|
export { CopilotUsageProvider, configureCopilotVsCodeLogging } from "./copilot.js";
|
|
@@ -8,31 +8,35 @@ export function numberOrZero(value) {
|
|
|
8
8
|
export function asRecord(value) {
|
|
9
9
|
return value && typeof value === "object" ? value : null;
|
|
10
10
|
}
|
|
11
|
-
export function applyRateLimits(windows, rateLimits, eventTimeMs, deltaTotals, planTypes) {
|
|
11
|
+
export function applyRateLimits(windows, rateLimits, eventTimeMs, modelId, deltaTotals, planTypes) {
|
|
12
12
|
if (!rateLimits) {
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
15
|
if (typeof rateLimits.plan_type === "string") {
|
|
16
16
|
planTypes.add(rateLimits.plan_type);
|
|
17
17
|
}
|
|
18
|
-
upsertWindow(windows, "primary", rateLimits, asRecord(rateLimits.primary), eventTimeMs, deltaTotals);
|
|
19
|
-
upsertWindow(windows, "secondary", rateLimits, asRecord(rateLimits.secondary), eventTimeMs, deltaTotals);
|
|
18
|
+
upsertWindow(windows, "primary", rateLimits, asRecord(rateLimits.primary), eventTimeMs, modelId, deltaTotals);
|
|
19
|
+
upsertWindow(windows, "secondary", rateLimits, asRecord(rateLimits.secondary), eventTimeMs, modelId, deltaTotals);
|
|
20
20
|
}
|
|
21
21
|
export function buildWindowLists(windows) {
|
|
22
|
-
const rows = collapseNearbyWindows([...windows.values()].map((window) =>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
22
|
+
const rows = collapseNearbyWindows([...windows.values()].map((window) => {
|
|
23
|
+
const usage = computeWindowUsage(window.events);
|
|
24
|
+
return {
|
|
25
|
+
scope: window.scope,
|
|
26
|
+
planType: window.planType,
|
|
27
|
+
limitId: window.limitId,
|
|
28
|
+
windowMinutes: window.windowMinutes,
|
|
29
|
+
startTimeUtcIso: formatIsoFromSeconds(window.minStartsAt),
|
|
30
|
+
endTimeUtcIso: formatIsoFromSeconds(window.maxResetsAt),
|
|
31
|
+
firstSeenUtcIso: formatIsoFromMilliseconds(window.firstSeenMs),
|
|
32
|
+
lastSeenUtcIso: formatIsoFromMilliseconds(window.lastSeenMs),
|
|
33
|
+
minUsedPercent: window.minUsedPercent,
|
|
34
|
+
maxUsedPercent: window.maxUsedPercent,
|
|
35
|
+
totals: usage.totals,
|
|
36
|
+
modelUsage: usage.modelUsage,
|
|
37
|
+
eventCount: 0
|
|
38
|
+
};
|
|
39
|
+
}))
|
|
36
40
|
.map((row) => ({
|
|
37
41
|
...row,
|
|
38
42
|
eventCount: row.totals.eventCount
|
|
@@ -71,7 +75,11 @@ function collapseNearbyWindows(rows) {
|
|
|
71
75
|
if (!existing) {
|
|
72
76
|
collapsed.set(key, {
|
|
73
77
|
...row,
|
|
74
|
-
totals: cloneUsageTotals(row.totals)
|
|
78
|
+
totals: cloneUsageTotals(row.totals),
|
|
79
|
+
modelUsage: row.modelUsage.map((entry) => ({
|
|
80
|
+
modelId: entry.modelId,
|
|
81
|
+
totals: cloneUsageTotals(entry.totals)
|
|
82
|
+
}))
|
|
75
83
|
});
|
|
76
84
|
continue;
|
|
77
85
|
}
|
|
@@ -86,28 +94,34 @@ function collapseNearbyWindows(rows) {
|
|
|
86
94
|
existing.minUsedPercent = Math.min(existing.minUsedPercent, row.minUsedPercent);
|
|
87
95
|
existing.maxUsedPercent = Math.max(existing.maxUsedPercent, row.maxUsedPercent);
|
|
88
96
|
addUsageTotals(existing.totals, row.totals);
|
|
97
|
+
existing.modelUsage = mergeModelUsageRows(existing.modelUsage, row.modelUsage);
|
|
89
98
|
existing.eventCount = existing.totals.eventCount;
|
|
90
99
|
}
|
|
91
100
|
return [...collapsed.values()];
|
|
92
101
|
}
|
|
93
|
-
function
|
|
102
|
+
function computeWindowUsage(events) {
|
|
94
103
|
// Session files are not guaranteed to be parsed in timestamp order, so
|
|
95
104
|
// saturation has to be applied after we sort the captured window events.
|
|
96
105
|
const totals = createEmptyUsageTotals();
|
|
106
|
+
const byModel = new Map();
|
|
97
107
|
let sawBelowCap = false;
|
|
98
108
|
let isExhausted = false;
|
|
99
109
|
for (const event of [...events].sort((left, right) => left.eventTimeMs - right.eventTimeMs)) {
|
|
100
110
|
sawBelowCap || (sawBelowCap = event.usedPercent < 100);
|
|
101
111
|
if (!isExhausted) {
|
|
102
112
|
addUsageTotals(totals, event.totals);
|
|
113
|
+
addWindowModelUsage(byModel, event.modelId, event.totals);
|
|
103
114
|
if (sawBelowCap && event.usedPercent >= 100) {
|
|
104
115
|
isExhausted = true;
|
|
105
116
|
}
|
|
106
117
|
}
|
|
107
118
|
}
|
|
108
|
-
return
|
|
119
|
+
return {
|
|
120
|
+
totals,
|
|
121
|
+
modelUsage: buildModelUsageRows(byModel)
|
|
122
|
+
};
|
|
109
123
|
}
|
|
110
|
-
function upsertWindow(windows, scope, rateLimits, window, eventTimeMs, deltaTotals) {
|
|
124
|
+
function upsertWindow(windows, scope, rateLimits, window, eventTimeMs, modelId, deltaTotals) {
|
|
111
125
|
if (!window) {
|
|
112
126
|
return;
|
|
113
127
|
}
|
|
@@ -132,7 +146,7 @@ function upsertWindow(windows, scope, rateLimits, window, eventTimeMs, deltaTota
|
|
|
132
146
|
lastSeenMs: eventTimeMs,
|
|
133
147
|
minUsedPercent: usedPercent,
|
|
134
148
|
maxUsedPercent: usedPercent,
|
|
135
|
-
events: [{ eventTimeMs, usedPercent, totals: cloneUsageTotals(deltaTotals) }]
|
|
149
|
+
events: [{ eventTimeMs, modelId, usedPercent, totals: cloneUsageTotals(deltaTotals) }]
|
|
136
150
|
});
|
|
137
151
|
return;
|
|
138
152
|
}
|
|
@@ -142,5 +156,25 @@ function upsertWindow(windows, scope, rateLimits, window, eventTimeMs, deltaTota
|
|
|
142
156
|
existing.lastSeenMs = Math.max(existing.lastSeenMs, eventTimeMs);
|
|
143
157
|
existing.minUsedPercent = Math.min(existing.minUsedPercent, usedPercent);
|
|
144
158
|
existing.maxUsedPercent = Math.max(existing.maxUsedPercent, usedPercent);
|
|
145
|
-
existing.events.push({ eventTimeMs, usedPercent, totals: cloneUsageTotals(deltaTotals) });
|
|
159
|
+
existing.events.push({ eventTimeMs, modelId, usedPercent, totals: cloneUsageTotals(deltaTotals) });
|
|
160
|
+
}
|
|
161
|
+
function addWindowModelUsage(byModel, modelId, totals) {
|
|
162
|
+
const existing = byModel.get(modelId);
|
|
163
|
+
if (!existing) {
|
|
164
|
+
byModel.set(modelId, cloneUsageTotals(totals));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
addUsageTotals(existing, totals);
|
|
168
|
+
}
|
|
169
|
+
function buildModelUsageRows(byModel) {
|
|
170
|
+
return [...byModel.entries()]
|
|
171
|
+
.map(([modelId, totals]) => ({ modelId, totals }))
|
|
172
|
+
.sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
|
|
173
|
+
}
|
|
174
|
+
function mergeModelUsageRows(left, right) {
|
|
175
|
+
const byModel = new Map();
|
|
176
|
+
for (const row of [...left, ...right]) {
|
|
177
|
+
addWindowModelUsage(byModel, row.modelId, row.totals);
|
|
178
|
+
}
|
|
179
|
+
return buildModelUsageRows(byModel);
|
|
146
180
|
}
|
|
@@ -37,25 +37,23 @@ function buildAnonymousUsageReport(stats, window, letmecodeVersion) {
|
|
|
37
37
|
used_percents: resolveReportedUsedPercents(window),
|
|
38
38
|
used_exhausted: window.maxUsedPercent >= 100,
|
|
39
39
|
value_dollars: roundDollars(window.totals.estimatedCredits * CREDIT_TO_DOLLARS),
|
|
40
|
-
usage_raw: buildUsageRaw(
|
|
40
|
+
usage_raw: buildUsageRaw(window.modelUsage),
|
|
41
41
|
letmecode_version: letmecodeVersion
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
-
function buildUsageRaw(
|
|
45
|
-
const usageRaw = {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
function buildUsageRaw(modelUsage) {
|
|
45
|
+
const usageRaw = {};
|
|
46
|
+
for (const row of modelUsage) {
|
|
47
|
+
usageRaw[row.modelId] = {
|
|
48
|
+
output: row.totals.outputTokens,
|
|
49
|
+
input_non_cache: row.totals.inputTokens,
|
|
50
|
+
input_cache_w5m: row.totals.cacheWrite5mInputTokens,
|
|
51
|
+
input_cache_w1h: row.totals.cacheWrite1hInputTokens,
|
|
52
|
+
input_cache_read: row.totals.cacheReadInputTokens
|
|
53
|
+
};
|
|
53
54
|
}
|
|
54
55
|
return usageRaw;
|
|
55
56
|
}
|
|
56
|
-
function isAnthropicProvider(providerId) {
|
|
57
|
-
return providerId === "claude" || providerId === "claude-vscode";
|
|
58
|
-
}
|
|
59
57
|
function resolveReportedUsedPercents(window) {
|
|
60
58
|
if (window.minUsedPercent === window.maxUsedPercent) {
|
|
61
59
|
return clampPercent(window.maxUsedPercent);
|