letmecode 0.1.28 → 0.1.30
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 +4 -2
- package/ink-app/dist/index.js +12 -9
- package/ink-app/dist/providers/antigravity/models.js +13 -1
- package/ink-app/dist/providers/antigravity/provider.js +26 -83
- package/ink-app/dist/providers/claude.js +95 -91
- package/ink-app/dist/providers/codex.js +245 -116
- package/ink-app/dist/providers/copilot/models.js +24 -45
- package/ink-app/dist/providers/copilot/provider.js +25 -4
- package/ink-app/dist/providers/copilot/usage/aggregate.js +14 -11
- package/ink-app/dist/providers/limits.js +42 -5
- package/ink-app/dist/providers/pricing.js +116 -16
- package/ink-app/dist/reporting.js +4 -5
- package/package.json +1 -1
|
@@ -6,68 +6,25 @@ import readline from "node:readline";
|
|
|
6
6
|
import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "./contract.js";
|
|
7
7
|
import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregates, numberOrZero } from "./limits.js";
|
|
8
8
|
import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "./daily.js";
|
|
9
|
-
import {
|
|
10
|
-
// One credit equals $0.01 (see CODEX_CREDIT_COST_USD in index.tsx), so credits
|
|
11
|
-
// equal USD * 100. Rate cards are expressed in the model's actual API price in
|
|
12
|
-
// USD per 1M tokens and scaled to credits in creditsFor, matching the Claude
|
|
13
|
-
// provider. These are the real gpt-5.* API prices, not the (4x cheaper) Codex
|
|
14
|
-
// subscription credit prices.
|
|
15
|
-
const USD_TO_CREDITS = 100;
|
|
16
|
-
const GPT_5_6_SOL_RATE = {
|
|
17
|
-
input: 5,
|
|
18
|
-
cacheRead: 0.5,
|
|
19
|
-
cacheWrite: 6.25,
|
|
20
|
-
cacheWrite5m: 6.25,
|
|
21
|
-
cacheWrite1h: 6.25,
|
|
22
|
-
output: 30,
|
|
23
|
-
longContext: {
|
|
24
|
-
thresholdTokens: 272000,
|
|
25
|
-
rate: { input: 10, cacheRead: 1, cacheWrite: 12.5, cacheWrite5m: 12.5, cacheWrite1h: 12.5, output: 45 }
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
const RATE_CARD = {
|
|
29
|
-
"gpt-5.6-sol": GPT_5_6_SOL_RATE,
|
|
30
|
-
"gpt-5.6-terra": {
|
|
31
|
-
input: 2.5,
|
|
32
|
-
cacheRead: 0.25,
|
|
33
|
-
cacheWrite: 3.125,
|
|
34
|
-
cacheWrite5m: 3.125,
|
|
35
|
-
cacheWrite1h: 3.125,
|
|
36
|
-
output: 15,
|
|
37
|
-
longContext: {
|
|
38
|
-
thresholdTokens: 272000,
|
|
39
|
-
rate: { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 6.25, output: 22.5 }
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
"gpt-5.6-luna": {
|
|
43
|
-
input: 1,
|
|
44
|
-
cacheRead: 0.1,
|
|
45
|
-
cacheWrite: 1.25,
|
|
46
|
-
cacheWrite5m: 1.25,
|
|
47
|
-
cacheWrite1h: 1.25,
|
|
48
|
-
output: 6,
|
|
49
|
-
longContext: {
|
|
50
|
-
thresholdTokens: 272000,
|
|
51
|
-
rate: { input: 2, cacheRead: 0.2, cacheWrite: 2.5, cacheWrite5m: 2.5, cacheWrite1h: 2.5, output: 9 }
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
"gpt-5.5": { input: 5, cacheRead: 0.5, cacheWrite: 5, cacheWrite5m: 5, cacheWrite1h: 5, output: 30 },
|
|
55
|
-
"gpt-5.4": { input: 2.5, cacheRead: 0.25, cacheWrite: 2.5, cacheWrite5m: 2.5, cacheWrite1h: 2.5, output: 15 },
|
|
56
|
-
"gpt-5.4-mini": { input: 0.75, cacheRead: 0.075, cacheWrite: 0.75, cacheWrite5m: 0.75, cacheWrite1h: 0.75, output: 4.5 }
|
|
57
|
-
};
|
|
9
|
+
import { fetchModelPricing, modelCostCredits } from "./pricing.js";
|
|
58
10
|
export class CodexUsageProvider extends UsageProviderBase {
|
|
59
11
|
constructor(options = {}) {
|
|
60
12
|
super("codex", "Codex");
|
|
61
13
|
this.root = path.resolve(options.root ?? os.homedir());
|
|
62
14
|
}
|
|
63
15
|
async getStats(_options = {}) {
|
|
64
|
-
const
|
|
16
|
+
const codexRoot = path.join(this.root, ".codex");
|
|
17
|
+
const sessionRoots = [
|
|
18
|
+
path.join(codexRoot, "sessions"),
|
|
19
|
+
path.join(codexRoot, "archived_sessions")
|
|
20
|
+
];
|
|
65
21
|
const knownModels = await readCodexModelMetadata(this.root);
|
|
66
22
|
const userIdHash = await readCodexUserIdHash(this.root, this.label);
|
|
67
23
|
const byModel = new Map();
|
|
68
24
|
const byDay = createDailyUsageAggregates();
|
|
69
25
|
const windows = createLimitWindowAggregates();
|
|
70
26
|
const planTypes = new Set();
|
|
27
|
+
const events = [];
|
|
71
28
|
const warnings = [];
|
|
72
29
|
const parseTotals = {
|
|
73
30
|
filesScanned: 0,
|
|
@@ -75,12 +32,45 @@ export class CodexUsageProvider extends UsageProviderBase {
|
|
|
75
32
|
tokenEvents: 0,
|
|
76
33
|
malformedLines: 0
|
|
77
34
|
};
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
35
|
+
const seenSessionFiles = new Set();
|
|
36
|
+
for (const sessionRoot of sessionRoots) {
|
|
37
|
+
for await (const file of walkSessionFiles(sessionRoot)) {
|
|
38
|
+
// Archiving normally moves a rollout, but guard against a transient copy
|
|
39
|
+
// existing in both locations while Codex updates its session index.
|
|
40
|
+
const sessionFileId = path.basename(file);
|
|
41
|
+
if (seenSessionFiles.has(sessionFileId)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
seenSessionFiles.add(sessionFileId);
|
|
45
|
+
parseTotals.filesScanned += 1;
|
|
46
|
+
const fileStats = await parseSessionFile(file, events);
|
|
47
|
+
parseTotals.linesRead += fileStats.linesRead;
|
|
48
|
+
parseTotals.tokenEvents += fileStats.tokenEvents;
|
|
49
|
+
parseTotals.malformedLines += fileStats.malformedLines;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
let pricing = new Map();
|
|
53
|
+
try {
|
|
54
|
+
pricing = await fetchModelPricing(events
|
|
55
|
+
.map((event) => pricingModelId(event.modelId))
|
|
56
|
+
.filter((modelId) => !isAssumedZeroRatedCodexModel(modelId, knownModels)), "codex");
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
warnings.push("Model pricing API is unavailable.");
|
|
60
|
+
}
|
|
61
|
+
for (const event of events) {
|
|
62
|
+
const deltaTotals = createUsageTotalsForModel(event.modelId, event.usage, knownModels, pricing, event.serviceTier);
|
|
63
|
+
deltaTotals.estimatedCredits += event.webSearchCalls;
|
|
64
|
+
if (!hasCountedRawUsage(event.usage) && event.webSearchCalls > 0) {
|
|
65
|
+
deltaTotals.estimatedCreditsStatus = "known";
|
|
66
|
+
}
|
|
67
|
+
const planType = typeof event.rateLimits?.plan_type === "string"
|
|
68
|
+
? event.rateLimits.plan_type
|
|
69
|
+
: undefined;
|
|
70
|
+
const safeEventTimeMs = Number.isFinite(event.eventTimeMs) ? event.eventTimeMs : 0;
|
|
71
|
+
addModelUsage(byModel, event.modelId, deltaTotals);
|
|
72
|
+
addDailyUsage(byDay, event.eventTimeMs, event.modelId, planType, deltaTotals);
|
|
73
|
+
applyRateLimits(windows, event.rateLimits, safeEventTimeMs, event.modelId, deltaTotals, planTypes);
|
|
84
74
|
}
|
|
85
75
|
if (parseTotals.malformedLines > 0) {
|
|
86
76
|
warnings.push(`Skipped ${parseTotals.malformedLines} malformed JSONL line(s).`);
|
|
@@ -89,13 +79,14 @@ export class CodexUsageProvider extends UsageProviderBase {
|
|
|
89
79
|
.map(([modelId, totals]) => ({ modelId, totals }))
|
|
90
80
|
.sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
|
|
91
81
|
const unknownPricedModels = modelUsage
|
|
92
|
-
.
|
|
93
|
-
.filter((
|
|
82
|
+
.filter((row) => row.totals.totalTokens > 0)
|
|
83
|
+
.filter((row) => row.totals.estimatedCreditsStatus === "unavailable")
|
|
84
|
+
.map((row) => row.modelId);
|
|
94
85
|
if (unknownPricedModels.length > 0) {
|
|
95
|
-
warnings.push(`No
|
|
86
|
+
warnings.push(`No complete API-equivalent pricing returned for: ${unknownPricedModels.join(", ")}.`);
|
|
96
87
|
}
|
|
97
88
|
if (parseTotals.filesScanned === 0) {
|
|
98
|
-
warnings.push(`No Codex session files found under ${
|
|
89
|
+
warnings.push(`No Codex session files found under ${codexRoot}.`);
|
|
99
90
|
}
|
|
100
91
|
const summaryTotals = sumUsageTotals(modelUsage.map((row) => row.totals));
|
|
101
92
|
const dayUsage = buildDailyUsageRows(byDay);
|
|
@@ -110,8 +101,8 @@ export class CodexUsageProvider extends UsageProviderBase {
|
|
|
110
101
|
totals: summaryTotals,
|
|
111
102
|
distinctModels: modelUsage.map((row) => row.modelId),
|
|
112
103
|
distinctPlanTypes: [...planTypes].sort(),
|
|
113
|
-
rootLabel: "~/.codex
|
|
114
|
-
rootPath:
|
|
104
|
+
rootLabel: "~/.codex",
|
|
105
|
+
rootPath: codexRoot
|
|
115
106
|
},
|
|
116
107
|
modelUsage,
|
|
117
108
|
dayUsage,
|
|
@@ -247,6 +238,7 @@ function createEmptyRawUsage() {
|
|
|
247
238
|
return {
|
|
248
239
|
inputTokens: 0,
|
|
249
240
|
cachedInputTokens: 0,
|
|
241
|
+
cacheWriteInputTokens: 0,
|
|
250
242
|
outputTokens: 0,
|
|
251
243
|
reasoningOutputTokens: 0,
|
|
252
244
|
totalTokens: 0
|
|
@@ -257,60 +249,78 @@ function normalizeRawUsage(value) {
|
|
|
257
249
|
return {
|
|
258
250
|
inputTokens: numberOrZero(usage.input_tokens),
|
|
259
251
|
cachedInputTokens: numberOrZero(usage.cached_input_tokens),
|
|
252
|
+
cacheWriteInputTokens: numberOrZero(usage.cache_write_input_tokens),
|
|
260
253
|
outputTokens: numberOrZero(usage.output_tokens),
|
|
261
254
|
reasoningOutputTokens: numberOrZero(usage.reasoning_output_tokens),
|
|
262
255
|
totalTokens: numberOrZero(usage.total_tokens)
|
|
263
256
|
};
|
|
264
257
|
}
|
|
265
|
-
function
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
outputTokens:
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
258
|
+
function creditsFor(modelId, usage, pricing, serviceTier) {
|
|
259
|
+
const { inputTokens, cacheReadInputTokens, cacheWriteInputTokens } = resolveCodexInputBreakdown(usage);
|
|
260
|
+
const credits = modelCostCredits(pricing.get(pricingModelId(modelId)), {
|
|
261
|
+
inputTokens,
|
|
262
|
+
outputTokens: usage.outputTokens,
|
|
263
|
+
cacheReadInputTokens,
|
|
264
|
+
cacheWrite5mInputTokens: cacheWriteInputTokens,
|
|
265
|
+
cacheWrite1hInputTokens: 0
|
|
266
|
+
});
|
|
267
|
+
return credits === undefined
|
|
268
|
+
? undefined
|
|
269
|
+
: credits * serviceTierPriceMultiplier(serviceTier);
|
|
273
270
|
}
|
|
274
|
-
function
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
271
|
+
function serviceTierPriceMultiplier(serviceTier) {
|
|
272
|
+
switch (serviceTier?.trim().toLowerCase()) {
|
|
273
|
+
case "fast":
|
|
274
|
+
case "priority":
|
|
275
|
+
return 2;
|
|
276
|
+
case "flex":
|
|
277
|
+
return 0.5;
|
|
278
|
+
default:
|
|
279
|
+
return 1;
|
|
278
280
|
}
|
|
279
|
-
const cachedInputTokens = Math.min(usage.cachedInputTokens, usage.inputTokens);
|
|
280
|
-
const nonCachedInputTokens = Math.max(0, usage.inputTokens - cachedInputTokens);
|
|
281
|
-
return (((nonCachedInputTokens / 1000000) * rate.input +
|
|
282
|
-
(cachedInputTokens / 1000000) * rate.cacheRead +
|
|
283
|
-
(usage.outputTokens / 1000000) * rate.output) *
|
|
284
|
-
USD_TO_CREDITS);
|
|
285
281
|
}
|
|
286
|
-
function
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
return
|
|
282
|
+
function isSupportedServiceTier(serviceTier) {
|
|
283
|
+
return ["default", "priority", "fast", "flex"].includes(serviceTier?.trim().toLowerCase() ?? "");
|
|
284
|
+
}
|
|
285
|
+
function pricingModelId(modelId) {
|
|
286
|
+
return (modelId === "gpt-5.6"
|
|
287
|
+
? "gpt-5.6-sol"
|
|
288
|
+
: modelId === "codex-auto-review"
|
|
289
|
+
? "gpt-5.4"
|
|
290
|
+
: modelId);
|
|
291
291
|
}
|
|
292
292
|
function rawUsageToTotals(usage) {
|
|
293
|
-
const cacheReadInputTokens =
|
|
294
|
-
const inputTokens = Math.max(0, usage.inputTokens - cacheReadInputTokens);
|
|
293
|
+
const { inputTokens, cacheReadInputTokens, cacheWriteInputTokens } = resolveCodexInputBreakdown(usage);
|
|
295
294
|
return {
|
|
296
295
|
inputTokens,
|
|
297
296
|
outputTokens: usage.outputTokens,
|
|
298
297
|
cacheReadInputTokens,
|
|
299
|
-
cacheWriteInputTokens
|
|
300
|
-
cacheWrite5mInputTokens:
|
|
298
|
+
cacheWriteInputTokens,
|
|
299
|
+
cacheWrite5mInputTokens: cacheWriteInputTokens,
|
|
301
300
|
cacheWrite1hInputTokens: 0,
|
|
302
301
|
reasoningOutputTokens: usage.reasoningOutputTokens,
|
|
303
|
-
totalTokens: inputTokens + cacheReadInputTokens + usage.outputTokens,
|
|
302
|
+
totalTokens: inputTokens + cacheReadInputTokens + cacheWriteInputTokens + usage.outputTokens,
|
|
304
303
|
estimatedCredits: 0,
|
|
305
304
|
eventCount: 0
|
|
306
305
|
};
|
|
307
306
|
}
|
|
308
|
-
function
|
|
307
|
+
function resolveCodexInputBreakdown(usage) {
|
|
308
|
+
const cacheReadInputTokens = Math.min(Math.max(0, usage.cachedInputTokens), usage.inputTokens);
|
|
309
|
+
const inputAfterCacheRead = Math.max(0, usage.inputTokens - cacheReadInputTokens);
|
|
310
|
+
const cacheWriteInputTokens = Math.min(Math.max(0, usage.cacheWriteInputTokens), inputAfterCacheRead);
|
|
311
|
+
return {
|
|
312
|
+
inputTokens: Math.max(0, inputAfterCacheRead - cacheWriteInputTokens),
|
|
313
|
+
cacheReadInputTokens,
|
|
314
|
+
cacheWriteInputTokens
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function createUsageTotalsForModel(modelId, usage, knownModels, pricing, serviceTier) {
|
|
309
318
|
const resolvedModelId = modelId || "unknown";
|
|
310
319
|
const deltaTotals = rawUsageToTotals(usage);
|
|
311
|
-
|
|
320
|
+
const estimatedCredits = creditsFor(resolvedModelId, usage, pricing, serviceTier);
|
|
321
|
+
deltaTotals.estimatedCredits = estimatedCredits ?? 0;
|
|
312
322
|
deltaTotals.eventCount = 1;
|
|
313
|
-
if (
|
|
323
|
+
if (estimatedCredits === undefined && !isAssumedZeroRatedCodexModel(resolvedModelId, knownModels)) {
|
|
314
324
|
deltaTotals.estimatedCreditsStatus = "unavailable";
|
|
315
325
|
}
|
|
316
326
|
return deltaTotals;
|
|
@@ -325,12 +335,13 @@ function isHiddenCodexModel(modelId, knownModels) {
|
|
|
325
335
|
return knownModels.get(modelId)?.visibility === "hide";
|
|
326
336
|
}
|
|
327
337
|
function isAssumedZeroRatedCodexModel(modelId, knownModels) {
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
|
|
338
|
+
// Synthetic bookkeeping rows carry no billable model call. Other hidden
|
|
339
|
+
// models are not assumed free: Codex auto-review is mapped to GPT-5.4 above,
|
|
340
|
+
// and an unknown hidden model must make the estimate explicitly unavailable.
|
|
341
|
+
return modelId === "<synthetic>" && isHiddenCodexModel(modelId, knownModels);
|
|
331
342
|
}
|
|
332
343
|
function isSessionFile(filePath) {
|
|
333
|
-
return filePath.endsWith(".jsonl")
|
|
344
|
+
return filePath.endsWith(".jsonl");
|
|
334
345
|
}
|
|
335
346
|
async function* walkSessionFiles(directory) {
|
|
336
347
|
let entries;
|
|
@@ -350,14 +361,50 @@ async function* walkSessionFiles(directory) {
|
|
|
350
361
|
}
|
|
351
362
|
}
|
|
352
363
|
}
|
|
353
|
-
async function parseSessionFile(filePath,
|
|
364
|
+
async function parseSessionFile(filePath, events) {
|
|
354
365
|
const stream = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
355
366
|
const lineReader = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
356
367
|
let currentModel = "unknown";
|
|
357
|
-
let
|
|
368
|
+
let currentServiceTier;
|
|
369
|
+
let pendingUsageRecord;
|
|
370
|
+
let firstSessionMetadataSeen = false;
|
|
371
|
+
let forkStartedAtMs = null;
|
|
372
|
+
let isOwnForkHistory = true;
|
|
358
373
|
let linesRead = 0;
|
|
359
374
|
let tokenEvents = 0;
|
|
360
375
|
let malformedLines = 0;
|
|
376
|
+
let pendingWebSearchCalls = 0;
|
|
377
|
+
let lastSeenTimestampMs = 0;
|
|
378
|
+
const recordUsage = (modelId, usage, eventTimeMs, rateLimits = null, serviceTier = currentServiceTier) => {
|
|
379
|
+
const webSearchCalls = pendingWebSearchCalls;
|
|
380
|
+
pendingWebSearchCalls = 0;
|
|
381
|
+
// Current-format-only policy: without the applied tier the exact cost is
|
|
382
|
+
// unknowable. Silently ignore the event instead of guessing Standard or
|
|
383
|
+
// carrying compatibility warnings for legacy rollout formats.
|
|
384
|
+
if (!isSupportedServiceTier(serviceTier)) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (!hasCountedRawUsage(usage) && webSearchCalls === 0) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const resolvedModelId = modelId || "unknown";
|
|
391
|
+
tokenEvents += 1;
|
|
392
|
+
events.push({
|
|
393
|
+
modelId: resolvedModelId,
|
|
394
|
+
usage,
|
|
395
|
+
eventTimeMs,
|
|
396
|
+
rateLimits,
|
|
397
|
+
serviceTier,
|
|
398
|
+
webSearchCalls
|
|
399
|
+
});
|
|
400
|
+
};
|
|
401
|
+
const flushPendingUsageRecord = () => {
|
|
402
|
+
if (!pendingUsageRecord) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
recordUsage(pendingUsageRecord.modelId, pendingUsageRecord.usage, pendingUsageRecord.eventTimeMs, null, pendingUsageRecord.serviceTier);
|
|
406
|
+
pendingUsageRecord = undefined;
|
|
407
|
+
};
|
|
361
408
|
for await (const line of lineReader) {
|
|
362
409
|
linesRead += 1;
|
|
363
410
|
if (!line.trim()) {
|
|
@@ -371,35 +418,117 @@ async function parseSessionFile(filePath, byModel, byDay, windows, planTypes, kn
|
|
|
371
418
|
malformedLines += 1;
|
|
372
419
|
continue;
|
|
373
420
|
}
|
|
421
|
+
const parsedTimestampMs = Date.parse(String(payloadObject.timestamp ?? ""));
|
|
422
|
+
if (Number.isFinite(parsedTimestampMs)) {
|
|
423
|
+
lastSeenTimestampMs = parsedTimestampMs;
|
|
424
|
+
}
|
|
425
|
+
if (payloadObject.type === "session_meta" && !firstSessionMetadataSeen) {
|
|
426
|
+
firstSessionMetadataSeen = true;
|
|
427
|
+
const payload = asRecord(payloadObject.payload);
|
|
428
|
+
const sessionStartedAtMs = parseSortableCodexIdTimestamp(payload?.id);
|
|
429
|
+
if (typeof payload?.forked_from_id === "string" && sessionStartedAtMs !== null) {
|
|
430
|
+
forkStartedAtMs = sessionStartedAtMs;
|
|
431
|
+
isOwnForkHistory = false;
|
|
432
|
+
}
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
374
435
|
if (payloadObject.type === "turn_context") {
|
|
375
|
-
const payload = payloadObject.payload;
|
|
376
|
-
const
|
|
377
|
-
|
|
436
|
+
const payload = asRecord(payloadObject.payload);
|
|
437
|
+
const turnStartedAtMs = parseSortableCodexIdTimestamp(payload?.turn_id);
|
|
438
|
+
if (forkStartedAtMs !== null && turnStartedAtMs !== null && turnStartedAtMs < forkStartedAtMs) {
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (!isOwnForkHistory) {
|
|
442
|
+
if (forkStartedAtMs !== null && turnStartedAtMs === null) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
isOwnForkHistory = true;
|
|
446
|
+
}
|
|
447
|
+
flushPendingUsageRecord();
|
|
448
|
+
const collaborationMode = asRecord(payload?.collaboration_mode);
|
|
449
|
+
const settings = asRecord(collaborationMode?.settings);
|
|
378
450
|
currentModel = String(payload?.model ?? settings?.model ?? currentModel);
|
|
379
451
|
continue;
|
|
380
452
|
}
|
|
453
|
+
// A fork inherits the latest thread settings from its parent history. Keep
|
|
454
|
+
// tracking those settings while skipping the inherited usage itself, so a
|
|
455
|
+
// child request is priced with the tier that was actually in force.
|
|
456
|
+
if (payloadObject.type === "event_msg") {
|
|
457
|
+
const payload = asRecord(payloadObject.payload);
|
|
458
|
+
if (payload?.type === "thread_settings_applied") {
|
|
459
|
+
// Compacted/background sessions can begin with a cumulative usage
|
|
460
|
+
// snapshot before their first model and tier settings. Flush it while
|
|
461
|
+
// the captured settings are still incomplete so the current-contract
|
|
462
|
+
// guard below ignores it instead of attributing it to "unknown" with
|
|
463
|
+
// settings that arrived later.
|
|
464
|
+
flushPendingUsageRecord();
|
|
465
|
+
const threadSettings = asRecord(payload.thread_settings);
|
|
466
|
+
if (typeof threadSettings?.model === "string" && threadSettings.model.trim()) {
|
|
467
|
+
currentModel = threadSettings.model;
|
|
468
|
+
}
|
|
469
|
+
if (typeof threadSettings?.service_tier === "string" && threadSettings.service_tier.trim()) {
|
|
470
|
+
currentServiceTier = threadSettings.service_tier;
|
|
471
|
+
}
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (!isOwnForkHistory) {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (payloadObject.type === "token_usage_record") {
|
|
479
|
+
flushPendingUsageRecord();
|
|
480
|
+
const payload = asRecord(payloadObject.payload);
|
|
481
|
+
const usage = normalizeRawUsage(payload?.usage);
|
|
482
|
+
if (hasCountedRawUsage(usage)) {
|
|
483
|
+
pendingUsageRecord = {
|
|
484
|
+
modelId: currentModel || "unknown",
|
|
485
|
+
usage,
|
|
486
|
+
eventTimeMs: Date.parse(String(payloadObject.timestamp ?? "")),
|
|
487
|
+
serviceTier: currentServiceTier
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
381
492
|
if (payloadObject.type !== "event_msg") {
|
|
382
493
|
continue;
|
|
383
494
|
}
|
|
384
|
-
const payload = payloadObject.payload;
|
|
495
|
+
const payload = asRecord(payloadObject.payload);
|
|
496
|
+
if (payload?.type === "web_search_end") {
|
|
497
|
+
pendingWebSearchCalls += 1;
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
385
500
|
if (payload?.type !== "token_count") {
|
|
386
501
|
continue;
|
|
387
502
|
}
|
|
388
|
-
const info = payload.info;
|
|
389
|
-
const totalUsage = normalizeRawUsage(info?.total_token_usage);
|
|
390
|
-
const lastUsage = info?.last_token_usage;
|
|
391
|
-
const usage = lastUsage ? normalizeRawUsage(lastUsage) : previousTotal ? subtractRawUsage(totalUsage, previousTotal) : totalUsage;
|
|
392
|
-
previousTotal = totalUsage;
|
|
393
|
-
const resolvedModelId = currentModel || "unknown";
|
|
394
|
-
const deltaTotals = createUsageTotalsForModel(resolvedModelId, usage, knownModels);
|
|
395
|
-
tokenEvents += 1;
|
|
396
|
-
addModelUsage(byModel, resolvedModelId, deltaTotals);
|
|
397
503
|
const eventTimeMs = Date.parse(String(payloadObject.timestamp ?? ""));
|
|
398
|
-
const safeEventTimeMs = Number.isFinite(eventTimeMs) ? eventTimeMs : 0;
|
|
399
504
|
const rateLimits = asRecord(payload.rate_limits);
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
505
|
+
if (pendingUsageRecord) {
|
|
506
|
+
const pending = pendingUsageRecord;
|
|
507
|
+
pendingUsageRecord = undefined;
|
|
508
|
+
recordUsage(pending.modelId, pending.usage, pending.eventTimeMs, rateLimits, pending.serviceTier);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
flushPendingUsageRecord();
|
|
512
|
+
if (pendingWebSearchCalls > 0) {
|
|
513
|
+
recordUsage(currentModel, createEmptyRawUsage(), lastSeenTimestampMs);
|
|
403
514
|
}
|
|
404
515
|
return { linesRead, tokenEvents, malformedLines };
|
|
405
516
|
}
|
|
517
|
+
function hasCountedRawUsage(usage) {
|
|
518
|
+
return (usage.inputTokens > 0 ||
|
|
519
|
+
usage.cachedInputTokens > 0 ||
|
|
520
|
+
usage.cacheWriteInputTokens > 0 ||
|
|
521
|
+
usage.outputTokens > 0 ||
|
|
522
|
+
usage.reasoningOutputTokens > 0);
|
|
523
|
+
}
|
|
524
|
+
function parseSortableCodexIdTimestamp(value) {
|
|
525
|
+
if (typeof value !== "string") {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
const timestampHex = value.replace(/-/g, "").slice(0, 12);
|
|
529
|
+
if (!/^[0-9a-f]{12}$/i.test(timestampHex)) {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
const timestamp = Number.parseInt(timestampHex, 16);
|
|
533
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
534
|
+
}
|
|
@@ -1,56 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"gemini-3-flash": { input: 50, cacheRead: 5, cacheWrite: 50, cacheWrite5m: 50, cacheWrite1h: 50, output: 300 },
|
|
26
|
-
"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 } } },
|
|
27
|
-
"gemini-3.5-flash": { input: 150, cacheRead: 15, cacheWrite: 150, cacheWrite5m: 150, cacheWrite1h: 150, output: 900 },
|
|
28
|
-
"mai-code-1-flash": { input: 75, cacheRead: 7.5, cacheWrite: 75, cacheWrite5m: 75, cacheWrite1h: 75, output: 450 },
|
|
29
|
-
"raptor-mini": { input: 25, cacheRead: 2.5, cacheWrite: 25, cacheWrite5m: 25, cacheWrite1h: 25, output: 200 }
|
|
30
|
-
};
|
|
31
|
-
/**
|
|
32
|
-
* Model id prefixes that Copilot does not bill (inline completions / next-edit
|
|
33
|
-
* suggestions). These are zero-rated rather than "unknown" so they never turn
|
|
34
|
-
* aggregate credit totals unknown.
|
|
35
|
-
*/
|
|
1
|
+
/*
|
|
2
|
+
Previous local prices in credits per 1M tokens, kept temporarily as requested:
|
|
3
|
+
gpt-5-mini 25 / 2.5 / 0 / 200; gpt-5.3-codex 175 / 17.5 / 0 / 1400
|
|
4
|
+
gpt-5.4 250 / 25 / 0 / 1500; gpt-5.4-mini 75 / 7.5 / 0 / 450
|
|
5
|
+
gpt-5.4-nano 20 / 2 / 0 / 125; gpt-5.5 500 / 50 / 0 / 3000
|
|
6
|
+
gpt-5.6-luna 20 / 2 / 25 / 120; gpt-5.6-sol 400 / 40 / 500 / 2000
|
|
7
|
+
gpt-5.6-terra 200 / 20 / 250 / 1200; gpt-6-astra 1000 / 100 / 1250 / 5000
|
|
8
|
+
claude-haiku-4-5 100 / 10 / 125 / 200 / 500
|
|
9
|
+
claude-sonnet-4-5 and 4-6 300 / 30 / 375 / 600 / 1500
|
|
10
|
+
claude-opus-4-5, 4-6, 4-7, 4-8, 5 500 / 50 / 625 / 1000 / 2500
|
|
11
|
+
claude-opus-4-8-fast 1000 / 100 / 1250 / 2000 / 5000
|
|
12
|
+
claude-fable-5 1000 / 100 / 1250 / 2000 / 5000
|
|
13
|
+
claude-fable-5-1 1000 / 25 / 1250 / 2000 / 5000
|
|
14
|
+
claude-sonnet-5 200 / 20 / 250 / 400 / 1000
|
|
15
|
+
gemini-2.5-pro 125 / 12.5 / 0 / 1000; gemini-3-flash 50 / 5 / 0 / 300
|
|
16
|
+
gemini-3.1-pro 200 / 20 / 0 / 1200; gemini-3.5-flash 150 / 15 / 0 / 900
|
|
17
|
+
gemini-3.6-flash, 3.7-flash, 3.8-flash 75 / 7.5 / 0 / 375
|
|
18
|
+
mai-code-1-flash 75 / 7.5 / 0 / 450; mai-code-1.1-flash 20 / 2 / 0 / 120
|
|
19
|
+
grok-4.5 and 4.6 200 / 50 / 0 / 600
|
|
20
|
+
kimi-k2.7-code 95 / 19 / 0 / 400; kimi-k3 300 / 30 / 0 / 1500
|
|
21
|
+
raptor-mini 25 / 2.5 / 0 / 200
|
|
22
|
+
Columns with four values: input / cache read / cache write / output.
|
|
23
|
+
Columns with five values: input / cache read / cache write 5m / cache write 1h / output.
|
|
24
|
+
*/
|
|
36
25
|
export const NON_BILLABLE_MODEL_PREFIXES = [
|
|
37
26
|
"copilot-nes",
|
|
38
27
|
"copilot-suggestion",
|
|
39
28
|
"copilot-suggestions"
|
|
40
29
|
];
|
|
41
|
-
/**
|
|
42
|
-
* Canonicalize a Copilot model id. The exporter already emits stable,
|
|
43
|
-
* human-readable ids (including dated suffixes like `gpt-5.4-2026-03-01`), and
|
|
44
|
-
* the dashboard surfaces those verbatim, so this only guards the empty case.
|
|
45
|
-
* Prefix-based rate resolution (see {@link rateForCopilotModel}) handles dated
|
|
46
|
-
* suffixes without collapsing the displayed id.
|
|
47
|
-
*/
|
|
48
30
|
export function normalizeCopilotModelId(modelId) {
|
|
49
31
|
return modelId || "unknown";
|
|
50
32
|
}
|
|
51
|
-
export function rateForCopilotModel(modelId, inputTokens) {
|
|
52
|
-
return resolveUsageRate(RATE_CARD, modelId, inputTokens, { prefixMatch: true });
|
|
53
|
-
}
|
|
54
33
|
export function isNonBillableCopilotModel(modelId) {
|
|
55
34
|
return NON_BILLABLE_MODEL_PREFIXES.some((prefix) => modelId === prefix || modelId.startsWith(`${prefix}-`));
|
|
56
35
|
}
|
|
@@ -6,6 +6,8 @@ import { discoverCopilotOtelFiles } from "./otel/discover.js";
|
|
|
6
6
|
import { parseCopilotOtelFiles } from "./otel/parse.js";
|
|
7
7
|
import { getCopilotUserInfo, subtractOneUtcCalendarMonth } from "./quota.js";
|
|
8
8
|
import { aggregateCopilotUsage, filterCopilotUsageEvents } from "./usage/aggregate.js";
|
|
9
|
+
import { isNonBillableCopilotModel } from "./models.js";
|
|
10
|
+
import { fetchModelPricing } from "../pricing.js";
|
|
9
11
|
// The token-metered bucket that maps to the "AI Credits" window.
|
|
10
12
|
const AI_CREDITS_QUOTA_ID = "premium_interactions";
|
|
11
13
|
export { configureCopilotVsCodeLogging, getCopilotCliOtelEnv };
|
|
@@ -48,11 +50,12 @@ export class CopilotUsageProvider extends UsageProviderBase {
|
|
|
48
50
|
linesRead: 0,
|
|
49
51
|
events: [],
|
|
50
52
|
aggregated: aggregateCopilotUsage([]),
|
|
53
|
+
pricing: new Map(),
|
|
51
54
|
warnings: ["Copilot OTEL usage is unavailable."]
|
|
52
55
|
};
|
|
53
56
|
warnings.push(...usage.warnings);
|
|
54
57
|
const { windows, unknownLabels, windowWarnings } = quotaInfo
|
|
55
|
-
? buildLimitWindows(quotaInfo, usage.events)
|
|
58
|
+
? buildLimitWindows(quotaInfo, usage.events, usage.pricing)
|
|
56
59
|
: { windows: [], unknownLabels: [], windowWarnings: [] };
|
|
57
60
|
if (unknownLabels.length > 0) {
|
|
58
61
|
warnings.push(`Copilot quota usage is unknown for: ${unknownLabels.join(", ")}.`);
|
|
@@ -83,8 +86,25 @@ export class CopilotUsageProvider extends UsageProviderBase {
|
|
|
83
86
|
async loadUsage() {
|
|
84
87
|
const discovery = await discoverCopilotOtelFiles({ root: this.root, env: this.env });
|
|
85
88
|
const parsed = await parseCopilotOtelFiles(discovery.files);
|
|
86
|
-
const aggregated = aggregateCopilotUsage(parsed.events);
|
|
87
89
|
const warnings = [...discovery.warnings, ...parsed.warnings];
|
|
90
|
+
let pricing = new Map();
|
|
91
|
+
try {
|
|
92
|
+
pricing = await fetchModelPricing(parsed.events
|
|
93
|
+
.map((event) => event.modelId)
|
|
94
|
+
.filter((modelId) => !isNonBillableCopilotModel(modelId)), "github_copilot");
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
warnings.push("Model pricing API is unavailable.");
|
|
98
|
+
}
|
|
99
|
+
const aggregated = aggregateCopilotUsage(parsed.events, pricing);
|
|
100
|
+
const unpricedModels = [
|
|
101
|
+
...new Set(parsed.events
|
|
102
|
+
.map((event) => event.modelId)
|
|
103
|
+
.filter((modelId) => !isNonBillableCopilotModel(modelId) && !pricing.has(modelId)))
|
|
104
|
+
];
|
|
105
|
+
if (unpricedModels.length > 0) {
|
|
106
|
+
warnings.push(`No Copilot API-equivalent pricing returned for: ${unpricedModels.join(", ")}.`);
|
|
107
|
+
}
|
|
88
108
|
if (parsed.malformedLines > 0) {
|
|
89
109
|
warnings.push(`Skipped ${parsed.malformedLines} malformed Copilot JSONL line(s).`);
|
|
90
110
|
}
|
|
@@ -99,6 +119,7 @@ export class CopilotUsageProvider extends UsageProviderBase {
|
|
|
99
119
|
linesRead: parsed.linesRead,
|
|
100
120
|
events: parsed.events,
|
|
101
121
|
aggregated,
|
|
122
|
+
pricing,
|
|
102
123
|
warnings
|
|
103
124
|
};
|
|
104
125
|
}
|
|
@@ -120,7 +141,7 @@ async function describeMissingOtelFile(root) {
|
|
|
120
141
|
? `VS Code Copilot logging is enabled, but ${missing.path} has not been created yet. Reload VS Code and send a Copilot Chat request.`
|
|
121
142
|
: undefined;
|
|
122
143
|
}
|
|
123
|
-
function buildLimitWindows(quotaInfo, events) {
|
|
144
|
+
function buildLimitWindows(quotaInfo, events, pricing) {
|
|
124
145
|
const planType = quotaInfo.plan ?? "unknown";
|
|
125
146
|
const billing = deriveBillingWindow(quotaInfo.resetAt);
|
|
126
147
|
const windows = [];
|
|
@@ -152,7 +173,7 @@ function buildLimitWindows(quotaInfo, events) {
|
|
|
152
173
|
// local OTEL token usage that falls inside this billing window.
|
|
153
174
|
if (isAiCredits && billing) {
|
|
154
175
|
const windowEvents = filterCopilotUsageEvents(events, billing.startMs, billing.endMs);
|
|
155
|
-
const windowUsage = aggregateCopilotUsage(windowEvents);
|
|
176
|
+
const windowUsage = aggregateCopilotUsage(windowEvents, pricing);
|
|
156
177
|
totals = windowUsage.summaryTotals;
|
|
157
178
|
modelUsage = windowUsage.modelUsage;
|
|
158
179
|
eventCount = windowUsage.tokenEvents;
|