letmecode 0.1.20 → 0.1.22
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 +10 -27
- package/ink-app/dist/index.js +68 -35
- package/ink-app/dist/providers/antigravity/models.js +46 -0
- package/ink-app/dist/providers/antigravity/provider.js +288 -0
- package/ink-app/dist/providers/antigravity/quota-parser.js +49 -0
- package/ink-app/dist/providers/antigravity/rpc/client.js +54 -0
- package/ink-app/dist/providers/antigravity/rpc/discovery.js +84 -0
- package/ink-app/dist/providers/antigravity/rpc/quota.js +25 -0
- package/ink-app/dist/providers/antigravity/rpc/usage.js +80 -0
- package/ink-app/dist/providers/antigravity/types.js +1 -0
- package/ink-app/dist/providers/antigravity/usage-parse.js +23 -0
- package/ink-app/dist/providers/antigravity.js +2 -537
- package/ink-app/dist/providers/claude.js +176 -183
- package/ink-app/dist/providers/contract.js +5 -2
- package/ink-app/dist/providers/copilot/models.js +55 -0
- package/ink-app/dist/providers/copilot/otel/configure.js +134 -0
- package/ink-app/dist/providers/copilot/otel/discover.js +94 -0
- package/ink-app/dist/providers/copilot/otel/parse.js +228 -0
- package/ink-app/dist/providers/copilot/provider.js +259 -0
- package/ink-app/dist/providers/copilot/quota.js +257 -0
- package/ink-app/dist/providers/copilot/usage/aggregate.js +84 -0
- package/ink-app/dist/providers/copilot.js +4 -373
- package/ink-app/dist/providers/index.js +1 -1
- package/ink-app/dist/providers/pricing.js +5 -0
- package/ink-app/dist/reporting.js +11 -1
- package/package.json +11 -13
|
@@ -10,12 +10,26 @@ import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregate
|
|
|
10
10
|
import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "./daily.js";
|
|
11
11
|
import { resolveUsageRate } from "./pricing.js";
|
|
12
12
|
const RATE_CARD = {
|
|
13
|
+
"claude-fable-5": { input: 10, cacheRead: 1, cacheWrite: 12.5, cacheWrite5m: 12.5, cacheWrite1h: 20, output: 50 },
|
|
13
14
|
"claude-opus-4-8": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
|
|
14
15
|
"claude-opus-4-7": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
|
|
15
16
|
"claude-opus-4-6": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
|
|
16
17
|
"claude-opus-4-5": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
|
|
17
18
|
"claude-opus-4-1": { input: 15, cacheRead: 1.5, cacheWrite: 18.75, cacheWrite5m: 18.75, cacheWrite1h: 30, output: 75 },
|
|
18
19
|
"claude-opus-4": { input: 15, cacheRead: 1.5, cacheWrite: 18.75, cacheWrite5m: 18.75, cacheWrite1h: 30, output: 75 },
|
|
20
|
+
"claude-sonnet-5": {
|
|
21
|
+
input: 3,
|
|
22
|
+
cacheRead: 0.3,
|
|
23
|
+
cacheWrite: 3.75,
|
|
24
|
+
cacheWrite5m: 3.75,
|
|
25
|
+
cacheWrite1h: 6,
|
|
26
|
+
output: 15,
|
|
27
|
+
// Intro pricing (~0.66x) through 2026-08-31; reverts to the standard rate above on 2026-09-01.
|
|
28
|
+
introOffer: {
|
|
29
|
+
effectiveUntilMs: Date.UTC(2026, 8, 1),
|
|
30
|
+
rate: { input: 2, cacheRead: 0.2, cacheWrite: 2.5, cacheWrite5m: 2.5, cacheWrite1h: 4, output: 10 }
|
|
31
|
+
}
|
|
32
|
+
},
|
|
19
33
|
"claude-sonnet-4-6": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
|
|
20
34
|
"claude-sonnet-4-5": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
|
|
21
35
|
"claude-sonnet-4": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
|
|
@@ -46,12 +60,14 @@ const parsedClaudeSessionFilesCache = new Map();
|
|
|
46
60
|
const claudeBinaryPathCache = new Map();
|
|
47
61
|
const claudeUsageOutputCache = new Map();
|
|
48
62
|
const claudeAuthStatusOutputCache = new Map();
|
|
63
|
+
const claudeOauthCredentialsOutputCache = new Map();
|
|
49
64
|
export class ClaudeUsageProvider extends UsageProviderBase {
|
|
50
65
|
constructor(options = {}) {
|
|
51
66
|
super(options.id ?? "claude", options.label ?? "Claude");
|
|
52
67
|
this.root = path.resolve(options.root ?? os.homedir());
|
|
53
68
|
this.readUsageCommandOutput = options.readUsageCommandOutput;
|
|
54
69
|
this.readAuthStatusOutput = options.readAuthStatusOutput;
|
|
70
|
+
this.readOauthCredentials = options.readOauthCredentials;
|
|
55
71
|
this.now = options.now ?? (() => new Date());
|
|
56
72
|
}
|
|
57
73
|
async getStats(options = {}) {
|
|
@@ -82,7 +98,6 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
82
98
|
`malformed=${file.malformedLines}`,
|
|
83
99
|
`assistantUsageEvents=${file.events.length}`,
|
|
84
100
|
`matchingEvents=${matchingEvents.length}`,
|
|
85
|
-
`source=${file.sourceKind}`,
|
|
86
101
|
`entrypoints=${summarizeEventCounts(file.events.map((event) => event.entrypoint || "<empty>"))}`,
|
|
87
102
|
`models=${summarizeDistinctValues(file.events.map((event) => event.modelId || "unknown"))}`
|
|
88
103
|
].join(" "));
|
|
@@ -125,7 +140,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
125
140
|
warnings.push(`Collapsed ${parsedEvents.duplicateUsageKeys} duplicate Claude usage event(s) by request/message key.`);
|
|
126
141
|
}
|
|
127
142
|
if (options.verbose && parsedEvents.duplicateUsageKeyCollisions > 0) {
|
|
128
|
-
warnings.push(`Detected ${parsedEvents.duplicateUsageKeyCollisions} Claude usage key collision(s) with different token usage;
|
|
143
|
+
warnings.push(`Detected ${parsedEvents.duplicateUsageKeyCollisions} Claude usage key collision(s) with different token usage; kept the most complete same-key snapshot to avoid double-counting cumulative snapshots.`);
|
|
129
144
|
}
|
|
130
145
|
if (options.verbose && parsedEvents.duplicateUnkeyedEvents > 0) {
|
|
131
146
|
warnings.push(`Collapsed ${parsedEvents.duplicateUnkeyedEvents} adjacent duplicate unkeyed Claude usage event(s) by usage signature.`);
|
|
@@ -148,7 +163,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
148
163
|
const liveLimitWindows = await buildLiveLimitWindows({
|
|
149
164
|
root: this.root,
|
|
150
165
|
readUsageCommandOutput: this.readUsageCommandOutput,
|
|
151
|
-
|
|
166
|
+
readOauthCredentials: this.readOauthCredentials,
|
|
152
167
|
traceLogger: options.traceLogger,
|
|
153
168
|
now: this.now(),
|
|
154
169
|
selectedEvents
|
|
@@ -213,14 +228,14 @@ function normalizeUsage(value) {
|
|
|
213
228
|
inferenceGeo: String(usage.inference_geo ?? "")
|
|
214
229
|
};
|
|
215
230
|
}
|
|
216
|
-
function resolveRate(modelId) {
|
|
217
|
-
return resolveUsageRate(RATE_CARD, modelId, 0, { prefixMatch: true });
|
|
231
|
+
function resolveRate(modelId, timestampMs) {
|
|
232
|
+
return resolveUsageRate(RATE_CARD, modelId, 0, { prefixMatch: true, timestampMs });
|
|
218
233
|
}
|
|
219
234
|
function isInternalClaudeModel(modelId) {
|
|
220
235
|
return modelId === "<synthetic>";
|
|
221
236
|
}
|
|
222
|
-
function creditsFor(modelId, usage) {
|
|
223
|
-
const rate = resolveRate(modelId);
|
|
237
|
+
function creditsFor(modelId, usage, timestampMs) {
|
|
238
|
+
const rate = resolveRate(modelId, timestampMs);
|
|
224
239
|
if (!rate) {
|
|
225
240
|
return 0;
|
|
226
241
|
}
|
|
@@ -234,7 +249,7 @@ function creditsFor(modelId, usage) {
|
|
|
234
249
|
inferenceMultiplier *
|
|
235
250
|
USD_TO_CREDITS);
|
|
236
251
|
}
|
|
237
|
-
function usageToTotals(modelId, usage) {
|
|
252
|
+
function usageToTotals(modelId, usage, timestampMs) {
|
|
238
253
|
const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
|
|
239
254
|
const cacheWriteInputTokens = cacheWriteBreakdown.cacheWrite5mInputTokens +
|
|
240
255
|
cacheWriteBreakdown.cacheWrite1hInputTokens;
|
|
@@ -250,7 +265,7 @@ function usageToTotals(modelId, usage) {
|
|
|
250
265
|
usage.cacheReadInputTokens +
|
|
251
266
|
cacheWriteInputTokens +
|
|
252
267
|
usage.outputTokens,
|
|
253
|
-
estimatedCredits: creditsFor(modelId, usage),
|
|
268
|
+
estimatedCredits: creditsFor(modelId, usage, timestampMs),
|
|
254
269
|
eventCount: 1
|
|
255
270
|
};
|
|
256
271
|
}
|
|
@@ -387,26 +402,20 @@ async function loadParsedClaudeSessionFiles(sessionsRoot, traceLogger) {
|
|
|
387
402
|
const files = [];
|
|
388
403
|
traceClaude(traceLogger, `Scanning session files under ${sessionsRoot}.`);
|
|
389
404
|
for await (const filePath of walkSessionFiles(sessionsRoot)) {
|
|
390
|
-
files.push(await parseSessionFile(filePath
|
|
405
|
+
files.push(await parseSessionFile(filePath));
|
|
391
406
|
}
|
|
392
|
-
inferClaudeSessionFileSources(files);
|
|
393
407
|
traceClaude(traceLogger, `Completed session file scan under ${sessionsRoot}: ${files.length} file(s) parsed.`);
|
|
394
408
|
return files;
|
|
395
409
|
})();
|
|
396
410
|
parsedClaudeSessionFilesCache.set(cacheKey, pending);
|
|
397
411
|
return pending;
|
|
398
412
|
}
|
|
399
|
-
async function parseSessionFile(filePath
|
|
413
|
+
async function parseSessionFile(filePath) {
|
|
400
414
|
const stream = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
401
415
|
const lineReader = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
402
416
|
let linesRead = 0;
|
|
403
417
|
let malformedLines = 0;
|
|
404
418
|
const events = [];
|
|
405
|
-
const assistantEntryPoints = new Set();
|
|
406
|
-
let hasIdeOpenedFileAttachment = false;
|
|
407
|
-
let hasIdeOpenedFileMarker = false;
|
|
408
|
-
let hasIdeTooling = false;
|
|
409
|
-
let hasQueueOperations = false;
|
|
410
419
|
for await (const line of lineReader) {
|
|
411
420
|
linesRead += 1;
|
|
412
421
|
if (!line.trim()) {
|
|
@@ -420,19 +429,6 @@ async function parseSessionFile(filePath, sessionsRoot) {
|
|
|
420
429
|
malformedLines += 1;
|
|
421
430
|
continue;
|
|
422
431
|
}
|
|
423
|
-
if (payloadObject.type === "queue-operation") {
|
|
424
|
-
hasQueueOperations = true;
|
|
425
|
-
}
|
|
426
|
-
if (messageContainsIdeOpenedFileMarker(asRecord(payloadObject.message))) {
|
|
427
|
-
hasIdeOpenedFileMarker = true;
|
|
428
|
-
}
|
|
429
|
-
const attachment = asRecord(payloadObject.attachment);
|
|
430
|
-
if (attachment?.type === "opened_file_in_ide") {
|
|
431
|
-
hasIdeOpenedFileAttachment = true;
|
|
432
|
-
}
|
|
433
|
-
if (attachmentHasIdeTooling(attachment)) {
|
|
434
|
-
hasIdeTooling = true;
|
|
435
|
-
}
|
|
436
432
|
if (payloadObject.type !== "assistant") {
|
|
437
433
|
continue;
|
|
438
434
|
}
|
|
@@ -448,7 +444,6 @@ async function parseSessionFile(filePath, sessionsRoot) {
|
|
|
448
444
|
const normalizedUsage = normalizeUsage(usage);
|
|
449
445
|
const usageKeys = buildUsageEventKeys(payloadObject, message);
|
|
450
446
|
const usageSignature = buildUsageSignature(payloadObject, modelId, normalizedUsage);
|
|
451
|
-
assistantEntryPoints.add(entrypoint);
|
|
452
447
|
events.push({
|
|
453
448
|
entrypoint,
|
|
454
449
|
filePath,
|
|
@@ -458,106 +453,17 @@ async function parseSessionFile(filePath, sessionsRoot) {
|
|
|
458
453
|
timestampMs: eventTimeMs,
|
|
459
454
|
modelId,
|
|
460
455
|
usage: normalizedUsage,
|
|
461
|
-
totals: usageToTotals(modelId, normalizedUsage),
|
|
456
|
+
totals: usageToTotals(modelId, normalizedUsage, eventTimeMs),
|
|
462
457
|
rateLimits
|
|
463
458
|
});
|
|
464
459
|
}
|
|
465
460
|
return {
|
|
466
461
|
filePath,
|
|
467
|
-
sessionGroupKey: buildClaudeSessionGroupKey(sessionsRoot, filePath),
|
|
468
462
|
linesRead,
|
|
469
463
|
malformedLines,
|
|
470
|
-
sourceKind: "unknown",
|
|
471
|
-
sourceReason: "unclassified",
|
|
472
|
-
signals: {
|
|
473
|
-
assistantEntryPoints: [...assistantEntryPoints].sort(),
|
|
474
|
-
hasIdeOpenedFileAttachment,
|
|
475
|
-
hasIdeOpenedFileMarker,
|
|
476
|
-
hasIdeTooling,
|
|
477
|
-
hasQueueOperations
|
|
478
|
-
},
|
|
479
464
|
events
|
|
480
465
|
};
|
|
481
466
|
}
|
|
482
|
-
function buildClaudeSessionGroupKey(sessionsRoot, filePath) {
|
|
483
|
-
const relativePath = path.relative(sessionsRoot, filePath);
|
|
484
|
-
if (!relativePath || relativePath.startsWith("..")) {
|
|
485
|
-
return filePath;
|
|
486
|
-
}
|
|
487
|
-
const normalizedRelativePath = relativePath.split(path.sep).join("/");
|
|
488
|
-
const subagentMatch = normalizedRelativePath.match(/^(.*\/[^/]+)\/subagents\/[^/]+\.jsonl$/);
|
|
489
|
-
if (subagentMatch?.[1]) {
|
|
490
|
-
return subagentMatch[1];
|
|
491
|
-
}
|
|
492
|
-
return normalizedRelativePath.replace(/\.jsonl$/i, "");
|
|
493
|
-
}
|
|
494
|
-
function inferClaudeSessionFileSources(files) {
|
|
495
|
-
const groups = new Map();
|
|
496
|
-
for (const file of files) {
|
|
497
|
-
const group = groups.get(file.sessionGroupKey) ?? {
|
|
498
|
-
assistantEntryPoints: new Set(),
|
|
499
|
-
hasIdeHints: false
|
|
500
|
-
};
|
|
501
|
-
for (const entrypoint of file.signals.assistantEntryPoints) {
|
|
502
|
-
group.assistantEntryPoints.add(entrypoint);
|
|
503
|
-
}
|
|
504
|
-
group.hasIdeHints =
|
|
505
|
-
group.hasIdeHints ||
|
|
506
|
-
file.signals.hasIdeOpenedFileAttachment ||
|
|
507
|
-
file.signals.hasIdeOpenedFileMarker ||
|
|
508
|
-
file.signals.hasIdeTooling ||
|
|
509
|
-
file.signals.hasQueueOperations;
|
|
510
|
-
groups.set(file.sessionGroupKey, group);
|
|
511
|
-
}
|
|
512
|
-
for (const file of files) {
|
|
513
|
-
const group = groups.get(file.sessionGroupKey);
|
|
514
|
-
const { kind, reason } = classifyClaudeSessionGroup(group);
|
|
515
|
-
file.sourceKind = kind;
|
|
516
|
-
file.sourceReason = reason;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
function classifyClaudeSessionGroup(group) {
|
|
520
|
-
if (!group) {
|
|
521
|
-
return { kind: "unknown", reason: "missing session group signals" };
|
|
522
|
-
}
|
|
523
|
-
if (group.assistantEntryPoints.has("claude-vscode")) {
|
|
524
|
-
return { kind: "vscode", reason: "explicit claude-vscode entrypoint" };
|
|
525
|
-
}
|
|
526
|
-
if (group.assistantEntryPoints.has("sdk-cli") || group.assistantEntryPoints.has("claude")) {
|
|
527
|
-
return { kind: "cli", reason: "explicit sdk-cli/claude entrypoint" };
|
|
528
|
-
}
|
|
529
|
-
if (group.assistantEntryPoints.has("cli")) {
|
|
530
|
-
return group.hasIdeHints
|
|
531
|
-
? { kind: "vscode", reason: "generic cli entrypoint with IDE session hints" }
|
|
532
|
-
: { kind: "cli", reason: "generic cli entrypoint without IDE session hints" };
|
|
533
|
-
}
|
|
534
|
-
return { kind: "unknown", reason: "no assistant entrypoints" };
|
|
535
|
-
}
|
|
536
|
-
function attachmentHasIdeTooling(attachment) {
|
|
537
|
-
if (attachment?.type !== "deferred_tools_delta") {
|
|
538
|
-
return false;
|
|
539
|
-
}
|
|
540
|
-
return extractStringArray(attachment.addedNames).some((name) => name.startsWith("mcp__ide__"));
|
|
541
|
-
}
|
|
542
|
-
function messageContainsIdeOpenedFileMarker(message) {
|
|
543
|
-
const content = message?.content;
|
|
544
|
-
if (typeof content === "string") {
|
|
545
|
-
return content.includes("<ide_opened_file>");
|
|
546
|
-
}
|
|
547
|
-
if (!Array.isArray(content)) {
|
|
548
|
-
return false;
|
|
549
|
-
}
|
|
550
|
-
return content.some((item) => {
|
|
551
|
-
const contentItem = asRecord(item);
|
|
552
|
-
return typeof contentItem?.text === "string" && contentItem.text.includes("<ide_opened_file>");
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
function extractStringArray(value) {
|
|
556
|
-
if (!Array.isArray(value)) {
|
|
557
|
-
return [];
|
|
558
|
-
}
|
|
559
|
-
return value.filter((item) => typeof item === "string");
|
|
560
|
-
}
|
|
561
467
|
function buildUsageEventKeys(payloadObject, message) {
|
|
562
468
|
const sessionId = String(payloadObject.sessionId ?? "");
|
|
563
469
|
const requestId = typeof payloadObject.requestId === "string" ? payloadObject.requestId : "";
|
|
@@ -637,8 +543,14 @@ function recordParsedUsageEvent(parsedEvents, event) {
|
|
|
637
543
|
}
|
|
638
544
|
}
|
|
639
545
|
function mergeParsedUsageEvents(previous, next) {
|
|
640
|
-
|
|
641
|
-
|
|
546
|
+
// Same-key events are repeated/streamed snapshots of one logical request. Rather than
|
|
547
|
+
// synthesizing a field-wise maximum (which can fabricate token totals when a snapshot
|
|
548
|
+
// splits cache-write tokens across the 5m/1h buckets differently), keep the single most
|
|
549
|
+
// complete real snapshot and discard the rest.
|
|
550
|
+
const primaryEvent = selectMergedSnapshotEvent(previous, next);
|
|
551
|
+
const otherEvent = primaryEvent === previous ? next : previous;
|
|
552
|
+
const usage = primaryEvent.usage;
|
|
553
|
+
const modelId = selectMergedEventModelId(primaryEvent, otherEvent);
|
|
642
554
|
const latestEvent = normalizeTimestamp(next.timestampMs) >= normalizeTimestamp(previous.timestampMs) ? next : previous;
|
|
643
555
|
const sessionId = extractUsageKeySessionId(previous.usageKeys) || extractUsageKeySessionId(next.usageKeys);
|
|
644
556
|
return {
|
|
@@ -646,38 +558,34 @@ function mergeParsedUsageEvents(previous, next) {
|
|
|
646
558
|
filePath: latestEvent.filePath,
|
|
647
559
|
lineNumber: latestEvent.lineNumber,
|
|
648
560
|
usageKeys: [...new Set([...previous.usageKeys, ...next.usageKeys])],
|
|
649
|
-
usageSignature: buildUsageSignatureFromParts(sessionId, modelId,
|
|
561
|
+
usageSignature: buildUsageSignatureFromParts(sessionId, modelId, usage),
|
|
650
562
|
timestampMs: Math.max(normalizeTimestamp(previous.timestampMs), normalizeTimestamp(next.timestampMs)),
|
|
651
563
|
modelId,
|
|
652
|
-
usage
|
|
653
|
-
totals: usageToTotals(modelId,
|
|
564
|
+
usage,
|
|
565
|
+
totals: usageToTotals(modelId, usage, primaryEvent.timestampMs),
|
|
654
566
|
rateLimits: latestEvent.rateLimits ?? previous.rateLimits ?? next.rateLimits
|
|
655
567
|
};
|
|
656
568
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
outputTokens: Math.max(previous.outputTokens, next.outputTokens),
|
|
665
|
-
inferenceGeo: next.inferenceGeo || previous.inferenceGeo
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
function selectMergedEventModelId(previous, next) {
|
|
669
|
-
if (previous.modelId === next.modelId) {
|
|
670
|
-
return previous.modelId;
|
|
569
|
+
// Pick the snapshot that carries the most usage. Cumulative snapshots are monotonic, so the
|
|
570
|
+
// largest total is the final state; this also keeps a real synthetic-followup row (0 tokens)
|
|
571
|
+
// from clobbering the real usage it follows. Ties fall back to the later, then the earlier-seen
|
|
572
|
+
// event for deterministic output.
|
|
573
|
+
function selectMergedSnapshotEvent(previous, next) {
|
|
574
|
+
if (next.totals.totalTokens !== previous.totals.totalTokens) {
|
|
575
|
+
return next.totals.totalTokens > previous.totals.totalTokens ? next : previous;
|
|
671
576
|
}
|
|
672
|
-
|
|
673
|
-
|
|
577
|
+
return normalizeTimestamp(next.timestampMs) > normalizeTimestamp(previous.timestampMs) ? next : previous;
|
|
578
|
+
}
|
|
579
|
+
function selectMergedEventModelId(primary, other) {
|
|
580
|
+
if (primary.modelId === other.modelId) {
|
|
581
|
+
return primary.modelId;
|
|
674
582
|
}
|
|
675
|
-
|
|
676
|
-
|
|
583
|
+
// The chosen snapshot's own model is authoritative, except when it is the internal
|
|
584
|
+
// <synthetic> placeholder and the other event names a real, priceable model.
|
|
585
|
+
if (isInternalClaudeModel(primary.modelId) && !isInternalClaudeModel(other.modelId)) {
|
|
586
|
+
return other.modelId;
|
|
677
587
|
}
|
|
678
|
-
return
|
|
679
|
-
? next.modelId
|
|
680
|
-
: previous.modelId;
|
|
588
|
+
return primary.modelId;
|
|
681
589
|
}
|
|
682
590
|
function canCollapseAdjacentUnkeyedUsageEvents(previous, next) {
|
|
683
591
|
return previous.filePath === next.filePath && next.lineNumber === previous.lineNumber + 1;
|
|
@@ -756,16 +664,16 @@ function buildClaudeCommandEnvironment() {
|
|
|
756
664
|
};
|
|
757
665
|
}
|
|
758
666
|
async function buildLiveLimitWindows(options) {
|
|
759
|
-
const [usageOutput,
|
|
667
|
+
const [usageOutput, credentials] = await Promise.all([
|
|
760
668
|
readClaudeUsageCommandOutput(options.root, options.readUsageCommandOutput, options.traceLogger),
|
|
761
|
-
|
|
669
|
+
readClaudeOauthCredentials(options.root, options.readOauthCredentials, options.traceLogger)
|
|
762
670
|
]);
|
|
763
671
|
const snapshots = parseLiveUsageWindowSnapshots(usageOutput, options.now);
|
|
764
672
|
traceClaude(options.traceLogger, `Parsed ${snapshots.length} live usage snapshot(s) from /usage output.`);
|
|
765
673
|
if (snapshots.length === 0) {
|
|
766
674
|
traceClaude(options.traceLogger, "No live usage snapshots matched the expected /usage format.");
|
|
767
675
|
}
|
|
768
|
-
const resolvedPlanType = resolveClaudeLivePlanType(
|
|
676
|
+
const resolvedPlanType = resolveClaudeLivePlanType(credentials);
|
|
769
677
|
traceClaude(options.traceLogger, `Resolved live plan type ${resolvedPlanType}.`);
|
|
770
678
|
const primaryLimitWindows = snapshots
|
|
771
679
|
.filter((snapshot) => snapshot.scope === "primary")
|
|
@@ -798,20 +706,82 @@ async function buildLiveLimitWindows(options) {
|
|
|
798
706
|
secondaryLimitWindows
|
|
799
707
|
};
|
|
800
708
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
709
|
+
// Reported as-is: the claude.ai OAuth `rateLimitTier` is not mapped to a friendly tier name
|
|
710
|
+
// anymore, it is combined with `subscriptionType` verbatim (e.g. "team|default_claude_max_5x").
|
|
711
|
+
function resolveClaudeLivePlanType(credentials) {
|
|
712
|
+
const subscriptionType = credentials?.subscriptionType;
|
|
713
|
+
if (!subscriptionType) {
|
|
714
|
+
return "live";
|
|
804
715
|
}
|
|
805
|
-
return subscriptionType
|
|
716
|
+
return credentials?.rateLimitTier ? `${subscriptionType}|${credentials.rateLimitTier}` : subscriptionType;
|
|
806
717
|
}
|
|
807
|
-
async function
|
|
808
|
-
const output = await
|
|
809
|
-
const
|
|
810
|
-
if (output && !
|
|
811
|
-
traceClaude(traceLogger, "Could not parse
|
|
812
|
-
}
|
|
813
|
-
traceClaude(traceLogger, `
|
|
814
|
-
return
|
|
718
|
+
async function readClaudeOauthCredentials(root, override, traceLogger) {
|
|
719
|
+
const output = await readClaudeOauthCredentialsOutput(root, override, traceLogger);
|
|
720
|
+
const credentials = parseClaudeOauthCredentials(output);
|
|
721
|
+
if (output && !credentials) {
|
|
722
|
+
traceClaude(traceLogger, "Could not parse claude.ai OAuth credentials from credentials file.");
|
|
723
|
+
}
|
|
724
|
+
traceClaude(traceLogger, `OAuth credentials result: subscriptionType=${credentials?.subscriptionType ?? "<none>"} rateLimitTier=${credentials?.rateLimitTier ?? "<none>"}.`);
|
|
725
|
+
return credentials;
|
|
726
|
+
}
|
|
727
|
+
async function readClaudeOauthCredentialsOutput(root, override, traceLogger) {
|
|
728
|
+
if (override) {
|
|
729
|
+
try {
|
|
730
|
+
const output = await override();
|
|
731
|
+
traceClaude(traceLogger, "Using injected OAuth credentials override.");
|
|
732
|
+
return output;
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
traceClaude(traceLogger, "Injected OAuth credentials override failed.");
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const credentialsPath = path.join(path.resolve(root), ".claude", ".credentials.json");
|
|
740
|
+
const cacheKey = credentialsPath;
|
|
741
|
+
const cached = claudeOauthCredentialsOutputCache.get(cacheKey);
|
|
742
|
+
if (cached) {
|
|
743
|
+
traceClaude(traceLogger, "OAuth credentials cache hit.");
|
|
744
|
+
return cached;
|
|
745
|
+
}
|
|
746
|
+
const pending = (async () => {
|
|
747
|
+
try {
|
|
748
|
+
const output = await fs.promises.readFile(credentialsPath, { encoding: "utf8" });
|
|
749
|
+
traceClaude(traceLogger, `Read OAuth credentials from ${credentialsPath}.`);
|
|
750
|
+
return output;
|
|
751
|
+
}
|
|
752
|
+
catch {
|
|
753
|
+
traceClaude(traceLogger, `No readable OAuth credentials at ${credentialsPath}.`);
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
})();
|
|
757
|
+
claudeOauthCredentialsOutputCache.set(cacheKey, pending);
|
|
758
|
+
return pending;
|
|
759
|
+
}
|
|
760
|
+
function parseClaudeOauthCredentials(output) {
|
|
761
|
+
if (!output) {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
let payload;
|
|
765
|
+
try {
|
|
766
|
+
payload = JSON.parse(output);
|
|
767
|
+
}
|
|
768
|
+
catch {
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
const oauth = asRecord(asRecord(payload)?.claudeAiOauth);
|
|
772
|
+
if (!oauth) {
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
const subscriptionType = typeof oauth.subscriptionType === "string" && oauth.subscriptionType.trim()
|
|
776
|
+
? oauth.subscriptionType.trim()
|
|
777
|
+
: null;
|
|
778
|
+
const rateLimitTier = typeof oauth.rateLimitTier === "string" && oauth.rateLimitTier.trim()
|
|
779
|
+
? oauth.rateLimitTier.trim()
|
|
780
|
+
: null;
|
|
781
|
+
if (!subscriptionType && !rateLimitTier) {
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
return { subscriptionType, rateLimitTier };
|
|
815
785
|
}
|
|
816
786
|
async function readClaudeAuthStatusOutput(root, override, traceLogger) {
|
|
817
787
|
if (override) {
|
|
@@ -859,10 +829,6 @@ async function readClaudeAuthStatusOutput(root, override, traceLogger) {
|
|
|
859
829
|
claudeAuthStatusOutputCache.set(cacheKey, pending);
|
|
860
830
|
return pending;
|
|
861
831
|
}
|
|
862
|
-
function parseClaudeSubscriptionType(output) {
|
|
863
|
-
const snapshot = parseClaudeAuthStatusSnapshot(output);
|
|
864
|
-
return snapshot?.subscriptionType ?? null;
|
|
865
|
-
}
|
|
866
832
|
function parseClaudeAuthStatusSnapshot(output) {
|
|
867
833
|
if (!output) {
|
|
868
834
|
return null;
|
|
@@ -1063,34 +1029,58 @@ function parseLiveUsageWindowSnapshots(usageOutput, now) {
|
|
|
1063
1029
|
if (!usageOutput) {
|
|
1064
1030
|
return [];
|
|
1065
1031
|
}
|
|
1066
|
-
const snapshots = new Map();
|
|
1067
1032
|
const normalizedOutput = usageOutput.replace(ANSI_ESCAPE_SEQUENCE, "");
|
|
1033
|
+
const parsedLines = [];
|
|
1068
1034
|
for (const line of normalizedOutput.split(/\r?\n/)) {
|
|
1069
1035
|
const match = line
|
|
1070
1036
|
.trim()
|
|
1071
|
-
.match(/^Current\s+(session|week)(?:\s+\(([^)]+)\))?:\s+(\d+)%\s+used\b
|
|
1037
|
+
.match(/^Current\s+(session|week)(?:\s+\(([^)]+)\))?:\s+(\d+)%\s+used\b(?:.*?\bresets\s+(.+))?$/i);
|
|
1072
1038
|
if (!match) {
|
|
1073
1039
|
continue;
|
|
1074
1040
|
}
|
|
1075
|
-
const label = match[1].toLowerCase() === "session" ? "session" : "week";
|
|
1076
|
-
const windowQualifier = (match[2] ?? "").trim().toLowerCase();
|
|
1077
1041
|
const usedPercent = Number(match[3]);
|
|
1078
|
-
|
|
1079
|
-
const resetsAtMs = parseResetTimestampUtc(match[4], now.getTime(), windowMinutes);
|
|
1080
|
-
if (!Number.isFinite(usedPercent) || !resetsAtMs) {
|
|
1042
|
+
if (!Number.isFinite(usedPercent)) {
|
|
1081
1043
|
continue;
|
|
1082
1044
|
}
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
snapshots.set(limitId, {
|
|
1086
|
-
scope: label === "session" ? "primary" : "secondary",
|
|
1045
|
+
const label = match[1].toLowerCase() === "session" ? "session" : "week";
|
|
1046
|
+
parsedLines.push({
|
|
1087
1047
|
label,
|
|
1088
|
-
|
|
1089
|
-
modelScope: isSonnetOnlyWeek ? "sonnet-only" : "all-models",
|
|
1090
|
-
modelType: isSonnetOnlyWeek ? "sonnet only" : undefined,
|
|
1048
|
+
windowQualifier: (match[2] ?? "").trim().toLowerCase(),
|
|
1091
1049
|
usedPercent,
|
|
1050
|
+
windowMinutes: label === "session" ? CLAUDE_SESSION_WINDOW_MINUTES : CLAUDE_WEEK_WINDOW_MINUTES,
|
|
1051
|
+
resetString: match[4]?.trim() || null
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
const resetMsByLabel = new Map();
|
|
1055
|
+
for (const parsed of parsedLines) {
|
|
1056
|
+
if (!parsed.resetString || resetMsByLabel.has(parsed.label)) {
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
const resetsAtMs = parseResetTimestampUtc(parsed.resetString, now.getTime(), parsed.windowMinutes);
|
|
1060
|
+
if (resetsAtMs) {
|
|
1061
|
+
resetMsByLabel.set(parsed.label, resetsAtMs);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
const snapshots = new Map();
|
|
1065
|
+
for (const parsed of parsedLines) {
|
|
1066
|
+
const resetsAtMs = resetMsByLabel.get(parsed.label) ?? null;
|
|
1067
|
+
if (!resetsAtMs) {
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
const isModelScopedWeek = parsed.label === "week" && parsed.windowQualifier !== "" && parsed.windowQualifier !== "all models";
|
|
1071
|
+
const modelFamily = isModelScopedWeek ? parsed.windowQualifier.replace(/\s+only$/, "") : null;
|
|
1072
|
+
const limitId = modelFamily
|
|
1073
|
+
? `current-week-${modelFamily.replace(/\s+/g, "-")}-only`
|
|
1074
|
+
: `current-${parsed.label}`;
|
|
1075
|
+
snapshots.set(limitId, {
|
|
1076
|
+
scope: parsed.label === "session" ? "primary" : "secondary",
|
|
1077
|
+
label: parsed.label,
|
|
1078
|
+
limitId,
|
|
1079
|
+
modelFamily,
|
|
1080
|
+
modelType: modelFamily ? parsed.windowQualifier : undefined,
|
|
1081
|
+
usedPercent: parsed.usedPercent,
|
|
1092
1082
|
resetsAtMs,
|
|
1093
|
-
windowMinutes
|
|
1083
|
+
windowMinutes: parsed.windowMinutes
|
|
1094
1084
|
});
|
|
1095
1085
|
}
|
|
1096
1086
|
return [...snapshots.values()].sort((left, right) => left.windowMinutes - right.windowMinutes || left.limitId.localeCompare(right.limitId));
|
|
@@ -1147,6 +1137,9 @@ function buildLiveLimitWindowRow(snapshot, planType, selectedEvents, now) {
|
|
|
1147
1137
|
event.timestampMs < snapshot.resetsAtMs &&
|
|
1148
1138
|
matchesClaudeLiveSnapshotModelScope(snapshot, event.modelId));
|
|
1149
1139
|
const totals = sumUsageTotals(inWindowEvents.map((event) => event.totals));
|
|
1140
|
+
if (snapshot.usedPercent > 0 && totals.eventCount === 0) {
|
|
1141
|
+
totals.estimatedCreditsStatus = "unavailable";
|
|
1142
|
+
}
|
|
1150
1143
|
const fallbackLastSeenMs = Math.min(now.getTime(), snapshot.resetsAtMs);
|
|
1151
1144
|
const firstSeenMs = inWindowEvents.reduce((minimum, event) => Math.min(minimum, event.timestampMs), Number.POSITIVE_INFINITY);
|
|
1152
1145
|
const lastSeenMs = inWindowEvents.reduce((maximum, event) => Math.max(maximum, event.timestampMs), Number.NEGATIVE_INFINITY);
|
|
@@ -1168,10 +1161,10 @@ function buildLiveLimitWindowRow(snapshot, planType, selectedEvents, now) {
|
|
|
1168
1161
|
};
|
|
1169
1162
|
}
|
|
1170
1163
|
function matchesClaudeLiveSnapshotModelScope(snapshot, modelId) {
|
|
1171
|
-
if (snapshot.
|
|
1164
|
+
if (!snapshot.modelFamily) {
|
|
1172
1165
|
return true;
|
|
1173
1166
|
}
|
|
1174
|
-
return modelId.toLowerCase().includes(
|
|
1167
|
+
return modelId.toLowerCase().includes(snapshot.modelFamily);
|
|
1175
1168
|
}
|
|
1176
1169
|
function buildModelUsageRowsForEvents(events) {
|
|
1177
1170
|
const byModel = new Map();
|
|
@@ -32,8 +32,11 @@ export function addUsageTotals(target, source) {
|
|
|
32
32
|
target.totalTokens += source.totalTokens;
|
|
33
33
|
target.estimatedCredits += source.estimatedCredits;
|
|
34
34
|
target.eventCount += source.eventCount;
|
|
35
|
-
if (source.
|
|
36
|
-
target.
|
|
35
|
+
if (source.cacheReadStatus === "unavailable") {
|
|
36
|
+
target.cacheReadStatus = "unavailable";
|
|
37
|
+
}
|
|
38
|
+
if (source.cacheWriteStatus === "unavailable") {
|
|
39
|
+
target.cacheWriteStatus = "unavailable";
|
|
37
40
|
}
|
|
38
41
|
if (source.estimatedCreditsStatus === "unavailable") {
|
|
39
42
|
target.estimatedCreditsStatus = "unavailable";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { resolveUsageRate } from "../pricing.js";
|
|
2
|
+
/**
|
|
3
|
+
* Copilot-specific estimated API-equivalent rate card (micro-credits per
|
|
4
|
+
* million tokens). This is intentionally separate from the Codex and
|
|
5
|
+
* Antigravity rate cards — Copilot bills the same model families at different
|
|
6
|
+
* effective rates, so there is no single shared source of truth to reuse.
|
|
7
|
+
*/
|
|
8
|
+
export const RATE_CARD = {
|
|
9
|
+
"gpt-5-mini": { input: 25, cacheRead: 2.5, cacheWrite: 25, cacheWrite5m: 25, cacheWrite1h: 25, output: 200 },
|
|
10
|
+
"gpt-5.3-codex": { input: 175, cacheRead: 17.5, cacheWrite: 175, cacheWrite5m: 175, cacheWrite1h: 175, output: 1400 },
|
|
11
|
+
"gpt-5.4": { input: 250, cacheRead: 25, cacheWrite: 250, cacheWrite5m: 250, cacheWrite1h: 250, output: 1500, longContext: { thresholdTokens: 272000, rate: { input: 500, cacheRead: 50, cacheWrite: 500, cacheWrite5m: 500, cacheWrite1h: 500, output: 2250 } } },
|
|
12
|
+
"gpt-5.4-mini": { input: 75, cacheRead: 7.5, cacheWrite: 75, cacheWrite5m: 75, cacheWrite1h: 75, output: 450 },
|
|
13
|
+
"gpt-5.4-nano": { input: 20, cacheRead: 2, cacheWrite: 20, cacheWrite5m: 20, cacheWrite1h: 20, output: 125 },
|
|
14
|
+
"gpt-5.5": { input: 500, cacheRead: 50, cacheWrite: 500, cacheWrite5m: 500, cacheWrite1h: 500, output: 3000, longContext: { thresholdTokens: 272000, rate: { input: 1000, cacheRead: 100, cacheWrite: 1000, cacheWrite5m: 1000, cacheWrite1h: 1000, output: 4500 } } },
|
|
15
|
+
"claude-haiku-4-5": { input: 100, cacheRead: 10, cacheWrite: 125, cacheWrite5m: 125, cacheWrite1h: 200, output: 500 },
|
|
16
|
+
"claude-sonnet-4-5": { input: 300, cacheRead: 30, cacheWrite: 375, cacheWrite5m: 375, cacheWrite1h: 600, output: 1500 },
|
|
17
|
+
"claude-sonnet-4-6": { input: 300, cacheRead: 30, cacheWrite: 375, cacheWrite5m: 375, cacheWrite1h: 600, output: 1500 },
|
|
18
|
+
"claude-opus-4-5": { input: 500, cacheRead: 50, cacheWrite: 625, cacheWrite5m: 625, cacheWrite1h: 1000, output: 2500 },
|
|
19
|
+
"claude-opus-4-6": { input: 500, cacheRead: 50, cacheWrite: 625, cacheWrite5m: 625, cacheWrite1h: 1000, output: 2500 },
|
|
20
|
+
"claude-opus-4-7": { input: 500, cacheRead: 50, cacheWrite: 625, cacheWrite5m: 625, cacheWrite1h: 1000, output: 2500 },
|
|
21
|
+
"claude-opus-4-8": { input: 500, cacheRead: 50, cacheWrite: 625, cacheWrite5m: 625, cacheWrite1h: 1000, output: 2500 },
|
|
22
|
+
"claude-fable-5": { input: 1000, cacheRead: 100, cacheWrite: 1250, cacheWrite5m: 1250, cacheWrite1h: 2000, output: 5000 },
|
|
23
|
+
"gemini-2.5-pro": { input: 125, cacheRead: 12.5, cacheWrite: 125, cacheWrite5m: 125, cacheWrite1h: 125, output: 1000 },
|
|
24
|
+
"gemini-3-flash": { input: 50, cacheRead: 5, cacheWrite: 50, cacheWrite5m: 50, cacheWrite1h: 50, output: 300 },
|
|
25
|
+
"gemini-3.1-pro": { input: 200, cacheRead: 20, cacheWrite: 200, cacheWrite5m: 200, cacheWrite1h: 200, output: 1200, longContext: { thresholdTokens: 200000, rate: { input: 400, cacheRead: 40, cacheWrite: 400, cacheWrite5m: 400, cacheWrite1h: 400, output: 1800 } } },
|
|
26
|
+
"gemini-3.5-flash": { input: 150, cacheRead: 15, cacheWrite: 150, cacheWrite5m: 150, cacheWrite1h: 150, output: 900 },
|
|
27
|
+
"mai-code-1-flash": { input: 75, cacheRead: 7.5, cacheWrite: 75, cacheWrite5m: 75, cacheWrite1h: 75, output: 450 },
|
|
28
|
+
"raptor-mini": { input: 25, cacheRead: 2.5, cacheWrite: 25, cacheWrite5m: 25, cacheWrite1h: 25, output: 200 }
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Model id prefixes that Copilot does not bill (inline completions / next-edit
|
|
32
|
+
* suggestions). These are zero-rated rather than "unknown" so they never turn
|
|
33
|
+
* aggregate credit totals unknown.
|
|
34
|
+
*/
|
|
35
|
+
export const NON_BILLABLE_MODEL_PREFIXES = [
|
|
36
|
+
"copilot-nes",
|
|
37
|
+
"copilot-suggestion",
|
|
38
|
+
"copilot-suggestions"
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* Canonicalize a Copilot model id. The exporter already emits stable,
|
|
42
|
+
* human-readable ids (including dated suffixes like `gpt-5.4-2026-03-01`), and
|
|
43
|
+
* the dashboard surfaces those verbatim, so this only guards the empty case.
|
|
44
|
+
* Prefix-based rate resolution (see {@link rateForCopilotModel}) handles dated
|
|
45
|
+
* suffixes without collapsing the displayed id.
|
|
46
|
+
*/
|
|
47
|
+
export function normalizeCopilotModelId(modelId) {
|
|
48
|
+
return modelId || "unknown";
|
|
49
|
+
}
|
|
50
|
+
export function rateForCopilotModel(modelId, inputTokens) {
|
|
51
|
+
return resolveUsageRate(RATE_CARD, modelId, inputTokens, { prefixMatch: true });
|
|
52
|
+
}
|
|
53
|
+
export function isNonBillableCopilotModel(modelId) {
|
|
54
|
+
return NON_BILLABLE_MODEL_PREFIXES.some((prefix) => modelId === prefix || modelId.startsWith(`${prefix}-`));
|
|
55
|
+
}
|