letmecode 0.1.29 → 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/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 +44 -42
- package/ink-app/dist/providers/codex.js +64 -129
- 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/pricing.js +116 -16
- package/package.json +1 -1
|
@@ -20,7 +20,19 @@ const MODEL_IDS = {
|
|
|
20
20
|
"claude-opus-4-6-20251201": "claude-opus-4-6"
|
|
21
21
|
};
|
|
22
22
|
export function normalizeAntigravityModelId(modelId) {
|
|
23
|
-
|
|
23
|
+
const exactModelId = MODEL_IDS[modelId];
|
|
24
|
+
if (exactModelId) {
|
|
25
|
+
return exactModelId;
|
|
26
|
+
}
|
|
27
|
+
const currentGeminiModel = [
|
|
28
|
+
"gemini-3.8-flash",
|
|
29
|
+
"gemini-3.7-flash",
|
|
30
|
+
"gemini-3.6-flash",
|
|
31
|
+
"gemini-3.1-pro"
|
|
32
|
+
].find((candidate) => modelId === candidate ||
|
|
33
|
+
modelId === `${candidate}-preview` ||
|
|
34
|
+
["low", "medium", "high"].some((level) => modelId === `${candidate}-${level}`));
|
|
35
|
+
return currentGeminiModel ?? (modelId || "unknown");
|
|
24
36
|
}
|
|
25
37
|
export function antigravityModelScope(rawModelId) {
|
|
26
38
|
const modelId = normalizeAntigravityModelId(rawModelId);
|
|
@@ -1,68 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "../contract.js";
|
|
3
3
|
import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "../daily.js";
|
|
4
|
-
import {
|
|
4
|
+
import { fetchModelPricing, modelCostCredits } from "../pricing.js";
|
|
5
5
|
import { modelScopeLabel, modelScopeMatches, normalizeAntigravityModelId } from "./models.js";
|
|
6
6
|
import { parseAntigravityQuotaEntries } from "./quota-parser.js";
|
|
7
7
|
import { findAntigravityLocalServer } from "./rpc/discovery.js";
|
|
8
8
|
import { extractQuotaGroups, fetchAntigravityUserStatus } from "./rpc/quota.js";
|
|
9
9
|
import { collectUsageFromLocalRpc } from "./usage-parse.js";
|
|
10
|
-
const RATE_CARD = {
|
|
11
|
-
"gemini-3.5-flash": {
|
|
12
|
-
input: 150,
|
|
13
|
-
cacheRead: 15,
|
|
14
|
-
cacheWrite: 150,
|
|
15
|
-
cacheWrite5m: 150,
|
|
16
|
-
cacheWrite1h: 150,
|
|
17
|
-
output: 900
|
|
18
|
-
},
|
|
19
|
-
"gemini-3.1-pro": {
|
|
20
|
-
input: 200,
|
|
21
|
-
cacheRead: 20,
|
|
22
|
-
cacheWrite: 200,
|
|
23
|
-
cacheWrite5m: 200,
|
|
24
|
-
cacheWrite1h: 200,
|
|
25
|
-
output: 1200,
|
|
26
|
-
longContext: {
|
|
27
|
-
thresholdTokens: 200000,
|
|
28
|
-
rate: {
|
|
29
|
-
input: 400,
|
|
30
|
-
cacheRead: 40,
|
|
31
|
-
cacheWrite: 400,
|
|
32
|
-
cacheWrite5m: 400,
|
|
33
|
-
cacheWrite1h: 400,
|
|
34
|
-
output: 1800
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
"gemini-3-flash": {
|
|
39
|
-
input: 50,
|
|
40
|
-
cacheRead: 5,
|
|
41
|
-
cacheWrite: 50,
|
|
42
|
-
cacheWrite5m: 50,
|
|
43
|
-
cacheWrite1h: 50,
|
|
44
|
-
output: 300
|
|
45
|
-
},
|
|
46
|
-
"claude-sonnet-4-6": {
|
|
47
|
-
input: 300,
|
|
48
|
-
cacheRead: 30,
|
|
49
|
-
cacheWrite: 375,
|
|
50
|
-
cacheWrite5m: 375,
|
|
51
|
-
cacheWrite1h: 600,
|
|
52
|
-
output: 1500
|
|
53
|
-
},
|
|
54
|
-
"claude-opus-4-6": {
|
|
55
|
-
input: 500,
|
|
56
|
-
cacheRead: 50,
|
|
57
|
-
cacheWrite: 625,
|
|
58
|
-
cacheWrite5m: 625,
|
|
59
|
-
cacheWrite1h: 1000,
|
|
60
|
-
output: 2500
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
const UNPRICED_MODELS = new Set([
|
|
64
|
-
"gpt-oss-120b"
|
|
65
|
-
]);
|
|
66
10
|
export class AntigravityUsageProvider extends UsageProviderBase {
|
|
67
11
|
constructor(options = {}) {
|
|
68
12
|
super("antigravity", "Antigravity");
|
|
@@ -104,11 +48,18 @@ export class AntigravityUsageProvider extends UsageProviderBase {
|
|
|
104
48
|
if (duplicateEvents > 0) {
|
|
105
49
|
warnings.push(`Collapsed ${duplicateEvents} duplicate Antigravity usage response(s).`);
|
|
106
50
|
}
|
|
51
|
+
let pricing = new Map();
|
|
52
|
+
try {
|
|
53
|
+
pricing = await fetchModelPricing(selectedRecords.map((record) => normalizeAntigravityModelId(record.modelId)), "antigravity");
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
warnings.push("Model pricing API is unavailable.");
|
|
57
|
+
}
|
|
107
58
|
const byModel = new Map();
|
|
108
59
|
const byDay = createDailyUsageAggregates();
|
|
109
60
|
for (const record of selectedRecords) {
|
|
110
61
|
const modelId = normalizeAntigravityModelId(record.modelId);
|
|
111
|
-
const totals = usageRecordToTotals(modelId, record);
|
|
62
|
+
const totals = usageRecordToTotals(modelId, record, pricing);
|
|
112
63
|
addModelUsage(byModel, modelId, totals);
|
|
113
64
|
addDailyUsage(byDay, record.timestamp, modelId, undefined, totals);
|
|
114
65
|
}
|
|
@@ -120,12 +71,12 @@ export class AntigravityUsageProvider extends UsageProviderBase {
|
|
|
120
71
|
.sort((left, right) => right.totals.estimatedCredits -
|
|
121
72
|
left.totals.estimatedCredits);
|
|
122
73
|
const unknownPricedModels = modelUsage
|
|
123
|
-
.filter((row) =>
|
|
74
|
+
.filter((row) => row.totals.estimatedCreditsStatus === "unavailable")
|
|
124
75
|
.map((row) => row.modelId);
|
|
125
76
|
if (unknownPricedModels.length > 0) {
|
|
126
|
-
warnings.push(`No Antigravity
|
|
77
|
+
warnings.push(`No complete Antigravity API-equivalent pricing returned for: ${unknownPricedModels.join(", ")}.`);
|
|
127
78
|
}
|
|
128
|
-
const limitWindows = quotaSnapshot?.entries.map((quota) => buildAntigravityLimitWindow(quota, quotaSnapshot.planType, selectedRecords, quotaSnapshot.fetchedAt)) ?? [];
|
|
79
|
+
const limitWindows = quotaSnapshot?.entries.map((quota) => buildAntigravityLimitWindow(quota, quotaSnapshot.planType, selectedRecords, quotaSnapshot.fetchedAt, pricing)) ?? [];
|
|
129
80
|
return {
|
|
130
81
|
providerId: this.id,
|
|
131
82
|
providerLabel: this.label,
|
|
@@ -177,7 +128,7 @@ async function collectQuotaFromConnection(connect) {
|
|
|
177
128
|
: null
|
|
178
129
|
};
|
|
179
130
|
}
|
|
180
|
-
function buildAntigravityLimitWindow(quota, planType, records, fetchedAt) {
|
|
131
|
+
function buildAntigravityLimitWindow(quota, planType, records, fetchedAt, pricing) {
|
|
181
132
|
const startAt = quota.resetAt - quota.windowMinutes * 60000;
|
|
182
133
|
const byModel = new Map();
|
|
183
134
|
const matchingTimestamps = [];
|
|
@@ -189,7 +140,7 @@ function buildAntigravityLimitWindow(quota, planType, records, fetchedAt) {
|
|
|
189
140
|
continue;
|
|
190
141
|
}
|
|
191
142
|
matchingTimestamps.push(record.timestamp);
|
|
192
|
-
addModelUsage(byModel, modelId, usageRecordToTotals(modelId, record));
|
|
143
|
+
addModelUsage(byModel, modelId, usageRecordToTotals(modelId, record, pricing));
|
|
193
144
|
}
|
|
194
145
|
const modelUsage = [...byModel.entries()]
|
|
195
146
|
.map(([modelId, totals]) => ({
|
|
@@ -250,7 +201,8 @@ function deduplicateRecords(records) {
|
|
|
250
201
|
function recordTokenTotal(record) {
|
|
251
202
|
return record.input + record.cacheRead + record.cacheWrite + record.output;
|
|
252
203
|
}
|
|
253
|
-
function usageRecordToTotals(modelId, record) {
|
|
204
|
+
function usageRecordToTotals(modelId, record, pricing) {
|
|
205
|
+
const estimatedCredits = creditsFor(modelId, record, pricing);
|
|
254
206
|
return {
|
|
255
207
|
inputTokens: record.input,
|
|
256
208
|
outputTokens: record.output,
|
|
@@ -263,7 +215,7 @@ function usageRecordToTotals(modelId, record) {
|
|
|
263
215
|
record.cacheRead +
|
|
264
216
|
record.cacheWrite +
|
|
265
217
|
record.output,
|
|
266
|
-
estimatedCredits:
|
|
218
|
+
estimatedCredits: estimatedCredits ?? 0,
|
|
267
219
|
eventCount: 1,
|
|
268
220
|
// The local RPC reports cache reads but never cache writes, so a zero cache
|
|
269
221
|
// write is genuinely unknown (not a confirmed zero) and is surfaced as "-".
|
|
@@ -271,26 +223,17 @@ function usageRecordToTotals(modelId, record) {
|
|
|
271
223
|
// which case it is both billed (see creditsFor) and shown as known.
|
|
272
224
|
cacheReadStatus: "known",
|
|
273
225
|
cacheWriteStatus: record.cacheWrite > 0 ? "known" : "unavailable",
|
|
274
|
-
estimatedCreditsStatus:
|
|
275
|
-
? "known"
|
|
276
|
-
: "unavailable"
|
|
226
|
+
estimatedCreditsStatus: estimatedCredits === undefined ? "unavailable" : "known"
|
|
277
227
|
};
|
|
278
228
|
}
|
|
279
|
-
function creditsFor(modelId, record) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
(record.output / 1000000) * rate.output);
|
|
288
|
-
}
|
|
289
|
-
function rateForModel(modelId, inputTokens) {
|
|
290
|
-
return resolveUsageRate(RATE_CARD, modelId, inputTokens);
|
|
291
|
-
}
|
|
292
|
-
function rowInputTokens(row) {
|
|
293
|
-
return row.totals.inputTokens + row.totals.cacheReadInputTokens + row.totals.cacheWriteInputTokens;
|
|
229
|
+
function creditsFor(modelId, record, pricing) {
|
|
230
|
+
return modelCostCredits(pricing.get(modelId), {
|
|
231
|
+
inputTokens: record.input,
|
|
232
|
+
outputTokens: record.output,
|
|
233
|
+
cacheReadInputTokens: record.cacheRead,
|
|
234
|
+
cacheWrite5mInputTokens: record.cacheWrite,
|
|
235
|
+
cacheWrite1hInputTokens: 0
|
|
236
|
+
});
|
|
294
237
|
}
|
|
295
238
|
function addModelUsage(byModel, modelId, deltaTotals) {
|
|
296
239
|
const totals = byModel.get(modelId) ?? createEmptyUsageTotals();
|
|
@@ -8,28 +8,21 @@ import { promisify } from "node:util";
|
|
|
8
8
|
import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "./contract.js";
|
|
9
9
|
import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregates, numberOrZero } from "./limits.js";
|
|
10
10
|
import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "./daily.js";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"claude-sonnet-5": { input: 2, cacheRead: 0.2, cacheWrite: 2.5, cacheWrite5m: 2.5, cacheWrite1h: 4, output: 10 },
|
|
25
|
-
"claude-sonnet-4-6": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
|
|
26
|
-
"claude-sonnet-4-5": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
|
|
27
|
-
"claude-sonnet-4": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
|
|
28
|
-
"claude-haiku-4-5": { input: 1, cacheRead: 0.1, cacheWrite: 1.25, cacheWrite5m: 1.25, cacheWrite1h: 2, output: 5 },
|
|
29
|
-
"claude-haiku-3-5": { input: 0.8, cacheRead: 0.08, cacheWrite: 1, cacheWrite5m: 1, cacheWrite1h: 1.6, output: 4 }
|
|
30
|
-
};
|
|
11
|
+
import { fetchModelPricing, modelCostCredits } from "./pricing.js";
|
|
12
|
+
/*
|
|
13
|
+
Previous local prices in USD per 1M tokens, kept temporarily as requested:
|
|
14
|
+
claude-fable-5-1 and claude-mythos-5-1 10 / 0.25 / 12.5 / 20 / 50
|
|
15
|
+
claude-fable-5 and claude-mythos-5 10 / 1 / 12.5 / 20 / 50
|
|
16
|
+
claude-opus-5, 4-8, 4-7, 4-6, 4-5 5 / 0.5 / 6.25 / 10 / 25
|
|
17
|
+
claude-opus-4-1 and 4 15 / 1.5 / 18.75 / 30 / 75
|
|
18
|
+
claude-sonnet-5 2 / 0.2 / 2.5 / 4 / 10
|
|
19
|
+
claude-sonnet-4-6, 4-5, 4 3 / 0.3 / 3.75 / 6 / 15
|
|
20
|
+
claude-haiku-4-5 1 / 0.1 / 1.25 / 2 / 5
|
|
21
|
+
claude-haiku-3-5 0.8 / 0.08 / 1 / 1.6 / 4
|
|
22
|
+
Columns: input / cache read / cache write 5m / cache write 1h / output.
|
|
23
|
+
*/
|
|
31
24
|
const execFileAsync = promisify(execFile);
|
|
32
|
-
const
|
|
25
|
+
const EMPTY_MODEL_PRICING = new Map();
|
|
33
26
|
const VSCODE_CLAUDE_EXTENSION_PREFIX = "anthropic.claude-code-";
|
|
34
27
|
const CLAUDE_SESSION_WINDOW_MINUTES = 5 * 60;
|
|
35
28
|
const CLAUDE_WEEK_WINDOW_MINUTES = 7 * 24 * 60;
|
|
@@ -115,7 +108,17 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
115
108
|
if (selectedEvents.length === 0 && parsedSessionFiles.length > 0) {
|
|
116
109
|
traceClaude(options.traceLogger, "No assistant usage events were found in the parsed Claude session files.");
|
|
117
110
|
}
|
|
111
|
+
let pricing = new Map();
|
|
112
|
+
try {
|
|
113
|
+
pricing = await fetchModelPricing(selectedEvents
|
|
114
|
+
.map((event) => event.modelId)
|
|
115
|
+
.filter((modelId) => !isInternalClaudeModel(modelId)), "claude_code");
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
warnings.push("Model pricing API is unavailable.");
|
|
119
|
+
}
|
|
118
120
|
for (const event of selectedEvents) {
|
|
121
|
+
event.totals = usageToTotals(event.modelId, event.usage, event.timestampMs, pricing);
|
|
119
122
|
addModelUsage(byModel, event.modelId, event.totals);
|
|
120
123
|
const planType = typeof event.rateLimits?.plan_type === "string" ? event.rateLimits.plan_type : undefined;
|
|
121
124
|
const safeEventTimeMs = Number.isFinite(event.timestampMs) ? event.timestampMs : 0;
|
|
@@ -136,10 +139,10 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
136
139
|
.map(([modelId, totals]) => ({ modelId, totals }))
|
|
137
140
|
.sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
|
|
138
141
|
const unknownPricedModels = modelUsage
|
|
139
|
-
.
|
|
140
|
-
.
|
|
142
|
+
.filter((row) => row.totals.estimatedCreditsStatus === "unavailable")
|
|
143
|
+
.map((row) => row.modelId);
|
|
141
144
|
if (unknownPricedModels.length > 0) {
|
|
142
|
-
warnings.push(`No API-equivalent
|
|
145
|
+
warnings.push(`No complete API-equivalent pricing returned for: ${unknownPricedModels.join(", ")}.`);
|
|
143
146
|
}
|
|
144
147
|
if (parsedSessionFiles.length === 0) {
|
|
145
148
|
warnings.push(`No Claude session files found under ${sessionsRoot}.`);
|
|
@@ -216,17 +219,10 @@ function normalizeUsage(value) {
|
|
|
216
219
|
webSearchRequests: numberOrZero(serverToolUse?.web_search_requests)
|
|
217
220
|
};
|
|
218
221
|
}
|
|
219
|
-
function resolveRate(modelId, timestampMs) {
|
|
220
|
-
return resolveUsageRate(RATE_CARD, modelId, 0, { prefixMatch: true, timestampMs });
|
|
221
|
-
}
|
|
222
222
|
function isInternalClaudeModel(modelId) {
|
|
223
223
|
return modelId === "<synthetic>";
|
|
224
224
|
}
|
|
225
|
-
function creditsFor(modelId, usage,
|
|
226
|
-
const rate = resolveRate(modelId, timestampMs);
|
|
227
|
-
if (!rate) {
|
|
228
|
-
return 0;
|
|
229
|
-
}
|
|
225
|
+
function creditsFor(modelId, usage, pricing) {
|
|
230
226
|
const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
|
|
231
227
|
// The US inference surcharge must match regardless of the casing the source
|
|
232
228
|
// reports (e.g. "us", "US"), so compare case-insensitively.
|
|
@@ -234,22 +230,28 @@ function creditsFor(modelId, usage, timestampMs) {
|
|
|
234
230
|
const speedMultiplier = usage.speed.trim().toLowerCase() === "fast" && isClaudeFastPricedModel(modelId)
|
|
235
231
|
? 2
|
|
236
232
|
: 1;
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
233
|
+
const tokenCostCredits = modelCostCredits(pricing.get(modelId), {
|
|
234
|
+
inputTokens: usage.inputTokens,
|
|
235
|
+
outputTokens: usage.outputTokens,
|
|
236
|
+
cacheReadInputTokens: usage.cacheReadInputTokens,
|
|
237
|
+
cacheWrite5mInputTokens: cacheWriteBreakdown.cacheWrite5mInputTokens,
|
|
238
|
+
cacheWrite1hInputTokens: cacheWriteBreakdown.cacheWrite1hInputTokens
|
|
239
|
+
});
|
|
240
|
+
if (tokenCostCredits === undefined) {
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
return (tokenCostCredits * speedMultiplier * inferenceMultiplier +
|
|
244
|
+
usage.webSearchRequests);
|
|
244
245
|
}
|
|
245
246
|
function isClaudeFastPricedModel(modelId) {
|
|
246
247
|
return ["claude-opus-5", "claude-opus-4-8"].some((candidate) => modelId === candidate || modelId.startsWith(`${candidate}-`));
|
|
247
248
|
}
|
|
248
|
-
function usageToTotals(modelId, usage,
|
|
249
|
+
function usageToTotals(modelId, usage, _timestampMs, pricing = EMPTY_MODEL_PRICING) {
|
|
249
250
|
const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
|
|
250
251
|
const cacheWriteInputTokens = cacheWriteBreakdown.cacheWrite5mInputTokens +
|
|
251
252
|
cacheWriteBreakdown.cacheWrite1hInputTokens;
|
|
252
|
-
const
|
|
253
|
+
const estimatedCredits = creditsFor(modelId, usage, pricing);
|
|
254
|
+
const rateKnown = estimatedCredits !== undefined || isInternalClaudeModel(modelId);
|
|
253
255
|
return {
|
|
254
256
|
inputTokens: usage.inputTokens,
|
|
255
257
|
outputTokens: usage.outputTokens,
|
|
@@ -262,7 +264,7 @@ function usageToTotals(modelId, usage, timestampMs) {
|
|
|
262
264
|
usage.cacheReadInputTokens +
|
|
263
265
|
cacheWriteInputTokens +
|
|
264
266
|
usage.outputTokens,
|
|
265
|
-
estimatedCredits:
|
|
267
|
+
estimatedCredits: estimatedCredits ?? 0,
|
|
266
268
|
eventCount: 1,
|
|
267
269
|
estimatedCreditsStatus: rateKnown ? "known" : "unavailable"
|
|
268
270
|
};
|
|
@@ -6,95 +6,7 @@ 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 OpenAI API prices, not the (4x cheaper) Codex
|
|
14
|
-
// subscription credit prices.
|
|
15
|
-
// Source: https://developers.openai.com/api/docs/pricing (checked 2026-09-05).
|
|
16
|
-
// GPT-5.6 Sol's current promotional rate is guaranteed only through at least
|
|
17
|
-
// 2026-11-21, so it must be rechecked after that date.
|
|
18
|
-
const USD_TO_CREDITS = 100;
|
|
19
|
-
const GPT_6_ASTRA_RATE = {
|
|
20
|
-
input: 10,
|
|
21
|
-
cacheRead: 1,
|
|
22
|
-
cacheWrite: 12.5,
|
|
23
|
-
cacheWrite5m: 12.5,
|
|
24
|
-
cacheWrite1h: 12.5,
|
|
25
|
-
output: 50,
|
|
26
|
-
longContext: {
|
|
27
|
-
thresholdTokens: 272000,
|
|
28
|
-
rate: { input: 20, cacheRead: 2, cacheWrite: 25, cacheWrite5m: 25, cacheWrite1h: 25, output: 75 }
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
const GPT_5_6_SOL_RATE = {
|
|
32
|
-
input: 4,
|
|
33
|
-
cacheRead: 0.4,
|
|
34
|
-
cacheWrite: 5,
|
|
35
|
-
cacheWrite5m: 5,
|
|
36
|
-
cacheWrite1h: 5,
|
|
37
|
-
output: 20,
|
|
38
|
-
longContext: {
|
|
39
|
-
thresholdTokens: 272000,
|
|
40
|
-
rate: { input: 8, cacheRead: 0.8, cacheWrite: 10, cacheWrite5m: 10, cacheWrite1h: 10, output: 30 }
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
const RATE_CARD = {
|
|
44
|
-
"gpt-6-astra": GPT_6_ASTRA_RATE,
|
|
45
|
-
"gpt-5.6-sol": GPT_5_6_SOL_RATE,
|
|
46
|
-
"gpt-5.6-terra": {
|
|
47
|
-
input: 2,
|
|
48
|
-
cacheRead: 0.2,
|
|
49
|
-
cacheWrite: 2.5,
|
|
50
|
-
cacheWrite5m: 2.5,
|
|
51
|
-
cacheWrite1h: 2.5,
|
|
52
|
-
output: 12,
|
|
53
|
-
longContext: {
|
|
54
|
-
thresholdTokens: 272000,
|
|
55
|
-
rate: { input: 4, cacheRead: 0.4, cacheWrite: 5, cacheWrite5m: 5, cacheWrite1h: 5, output: 18 }
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
"gpt-5.6-luna": {
|
|
59
|
-
input: 0.2,
|
|
60
|
-
cacheRead: 0.02,
|
|
61
|
-
cacheWrite: 0.25,
|
|
62
|
-
cacheWrite5m: 0.25,
|
|
63
|
-
cacheWrite1h: 0.25,
|
|
64
|
-
output: 1.2,
|
|
65
|
-
longContext: {
|
|
66
|
-
thresholdTokens: 272000,
|
|
67
|
-
rate: { input: 0.4, cacheRead: 0.04, cacheWrite: 0.5, cacheWrite5m: 0.5, cacheWrite1h: 0.5, output: 1.8 }
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
"gpt-5.5": {
|
|
71
|
-
input: 5,
|
|
72
|
-
cacheRead: 0.5,
|
|
73
|
-
cacheWrite: 0,
|
|
74
|
-
cacheWrite5m: 0,
|
|
75
|
-
cacheWrite1h: 0,
|
|
76
|
-
output: 30,
|
|
77
|
-
longContext: {
|
|
78
|
-
thresholdTokens: 272000,
|
|
79
|
-
rate: { input: 10, cacheRead: 1, cacheWrite: 0, cacheWrite5m: 0, cacheWrite1h: 0, output: 45 }
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
|
-
"gpt-5.4": {
|
|
83
|
-
input: 2.5,
|
|
84
|
-
cacheRead: 0.25,
|
|
85
|
-
cacheWrite: 0,
|
|
86
|
-
cacheWrite5m: 0,
|
|
87
|
-
cacheWrite1h: 0,
|
|
88
|
-
output: 15,
|
|
89
|
-
longContext: {
|
|
90
|
-
thresholdTokens: 272000,
|
|
91
|
-
rate: { input: 5, cacheRead: 0.5, cacheWrite: 0, cacheWrite5m: 0, cacheWrite1h: 0, output: 22.5 }
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
"gpt-5.4-mini": { input: 0.75, cacheRead: 0.075, cacheWrite: 0, cacheWrite5m: 0, cacheWrite1h: 0, output: 4.5 },
|
|
95
|
-
"gpt-5.3-codex": { input: 1.75, cacheRead: 0.175, cacheWrite: 0, cacheWrite5m: 0, cacheWrite1h: 0, output: 14 },
|
|
96
|
-
"gpt-5.2": { input: 1.75, cacheRead: 0.175, cacheWrite: 0, cacheWrite5m: 0, cacheWrite1h: 0, output: 14 }
|
|
97
|
-
};
|
|
9
|
+
import { fetchModelPricing, modelCostCredits } from "./pricing.js";
|
|
98
10
|
export class CodexUsageProvider extends UsageProviderBase {
|
|
99
11
|
constructor(options = {}) {
|
|
100
12
|
super("codex", "Codex");
|
|
@@ -112,6 +24,7 @@ export class CodexUsageProvider extends UsageProviderBase {
|
|
|
112
24
|
const byDay = createDailyUsageAggregates();
|
|
113
25
|
const windows = createLimitWindowAggregates();
|
|
114
26
|
const planTypes = new Set();
|
|
27
|
+
const events = [];
|
|
115
28
|
const warnings = [];
|
|
116
29
|
const parseTotals = {
|
|
117
30
|
filesScanned: 0,
|
|
@@ -130,12 +43,35 @@ export class CodexUsageProvider extends UsageProviderBase {
|
|
|
130
43
|
}
|
|
131
44
|
seenSessionFiles.add(sessionFileId);
|
|
132
45
|
parseTotals.filesScanned += 1;
|
|
133
|
-
const fileStats = await parseSessionFile(file,
|
|
46
|
+
const fileStats = await parseSessionFile(file, events);
|
|
134
47
|
parseTotals.linesRead += fileStats.linesRead;
|
|
135
48
|
parseTotals.tokenEvents += fileStats.tokenEvents;
|
|
136
49
|
parseTotals.malformedLines += fileStats.malformedLines;
|
|
137
50
|
}
|
|
138
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);
|
|
74
|
+
}
|
|
139
75
|
if (parseTotals.malformedLines > 0) {
|
|
140
76
|
warnings.push(`Skipped ${parseTotals.malformedLines} malformed JSONL line(s).`);
|
|
141
77
|
}
|
|
@@ -144,10 +80,10 @@ export class CodexUsageProvider extends UsageProviderBase {
|
|
|
144
80
|
.sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
|
|
145
81
|
const unknownPricedModels = modelUsage
|
|
146
82
|
.filter((row) => row.totals.totalTokens > 0)
|
|
147
|
-
.
|
|
148
|
-
.
|
|
83
|
+
.filter((row) => row.totals.estimatedCreditsStatus === "unavailable")
|
|
84
|
+
.map((row) => row.modelId);
|
|
149
85
|
if (unknownPricedModels.length > 0) {
|
|
150
|
-
warnings.push(`No API-equivalent
|
|
86
|
+
warnings.push(`No complete API-equivalent pricing returned for: ${unknownPricedModels.join(", ")}.`);
|
|
151
87
|
}
|
|
152
88
|
if (parseTotals.filesScanned === 0) {
|
|
153
89
|
warnings.push(`No Codex session files found under ${codexRoot}.`);
|
|
@@ -319,18 +255,18 @@ function normalizeRawUsage(value) {
|
|
|
319
255
|
totalTokens: numberOrZero(usage.total_tokens)
|
|
320
256
|
};
|
|
321
257
|
}
|
|
322
|
-
function creditsFor(modelId, usage, serviceTier) {
|
|
323
|
-
const rate = rateForCodexModel(modelId, usage.inputTokens);
|
|
324
|
-
if (!rate) {
|
|
325
|
-
return 0;
|
|
326
|
-
}
|
|
258
|
+
function creditsFor(modelId, usage, pricing, serviceTier) {
|
|
327
259
|
const { inputTokens, cacheReadInputTokens, cacheWriteInputTokens } = resolveCodexInputBreakdown(usage);
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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);
|
|
334
270
|
}
|
|
335
271
|
function serviceTierPriceMultiplier(serviceTier) {
|
|
336
272
|
switch (serviceTier?.trim().toLowerCase()) {
|
|
@@ -346,15 +282,12 @@ function serviceTierPriceMultiplier(serviceTier) {
|
|
|
346
282
|
function isSupportedServiceTier(serviceTier) {
|
|
347
283
|
return ["default", "priority", "fast", "flex"].includes(serviceTier?.trim().toLowerCase() ?? "");
|
|
348
284
|
}
|
|
349
|
-
function
|
|
350
|
-
|
|
351
|
-
// an unknown future gpt-5.6-* tier is not accidentally charged at Sol rates.
|
|
352
|
-
const pricedModelId = modelId === "gpt-5.6"
|
|
285
|
+
function pricingModelId(modelId) {
|
|
286
|
+
return (modelId === "gpt-5.6"
|
|
353
287
|
? "gpt-5.6-sol"
|
|
354
288
|
: modelId === "codex-auto-review"
|
|
355
289
|
? "gpt-5.4"
|
|
356
|
-
: modelId;
|
|
357
|
-
return resolveUsageRate(RATE_CARD, pricedModelId, inputTokens, { prefixMatch: true });
|
|
290
|
+
: modelId);
|
|
358
291
|
}
|
|
359
292
|
function rawUsageToTotals(usage) {
|
|
360
293
|
const { inputTokens, cacheReadInputTokens, cacheWriteInputTokens } = resolveCodexInputBreakdown(usage);
|
|
@@ -381,12 +314,13 @@ function resolveCodexInputBreakdown(usage) {
|
|
|
381
314
|
cacheWriteInputTokens
|
|
382
315
|
};
|
|
383
316
|
}
|
|
384
|
-
function createUsageTotalsForModel(modelId, usage, knownModels, serviceTier) {
|
|
317
|
+
function createUsageTotalsForModel(modelId, usage, knownModels, pricing, serviceTier) {
|
|
385
318
|
const resolvedModelId = modelId || "unknown";
|
|
386
319
|
const deltaTotals = rawUsageToTotals(usage);
|
|
387
|
-
|
|
320
|
+
const estimatedCredits = creditsFor(resolvedModelId, usage, pricing, serviceTier);
|
|
321
|
+
deltaTotals.estimatedCredits = estimatedCredits ?? 0;
|
|
388
322
|
deltaTotals.eventCount = 1;
|
|
389
|
-
if (
|
|
323
|
+
if (estimatedCredits === undefined && !isAssumedZeroRatedCodexModel(resolvedModelId, knownModels)) {
|
|
390
324
|
deltaTotals.estimatedCreditsStatus = "unavailable";
|
|
391
325
|
}
|
|
392
326
|
return deltaTotals;
|
|
@@ -427,7 +361,7 @@ async function* walkSessionFiles(directory) {
|
|
|
427
361
|
}
|
|
428
362
|
}
|
|
429
363
|
}
|
|
430
|
-
async function parseSessionFile(filePath,
|
|
364
|
+
async function parseSessionFile(filePath, events) {
|
|
431
365
|
const stream = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
432
366
|
const lineReader = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
433
367
|
let currentModel = "unknown";
|
|
@@ -442,7 +376,7 @@ async function parseSessionFile(filePath, byModel, byDay, windows, planTypes, kn
|
|
|
442
376
|
let pendingWebSearchCalls = 0;
|
|
443
377
|
let lastSeenTimestampMs = 0;
|
|
444
378
|
const recordUsage = (modelId, usage, eventTimeMs, rateLimits = null, serviceTier = currentServiceTier) => {
|
|
445
|
-
const
|
|
379
|
+
const webSearchCalls = pendingWebSearchCalls;
|
|
446
380
|
pendingWebSearchCalls = 0;
|
|
447
381
|
// Current-format-only policy: without the applied tier the exact cost is
|
|
448
382
|
// unknowable. Silently ignore the event instead of guessing Standard or
|
|
@@ -450,24 +384,19 @@ async function parseSessionFile(filePath, byModel, byDay, windows, planTypes, kn
|
|
|
450
384
|
if (!isSupportedServiceTier(serviceTier)) {
|
|
451
385
|
return;
|
|
452
386
|
}
|
|
453
|
-
if (!hasCountedRawUsage(usage) &&
|
|
387
|
+
if (!hasCountedRawUsage(usage) && webSearchCalls === 0) {
|
|
454
388
|
return;
|
|
455
389
|
}
|
|
456
390
|
const resolvedModelId = modelId || "unknown";
|
|
457
|
-
const deltaTotals = createUsageTotalsForModel(resolvedModelId, usage, knownModels, serviceTier);
|
|
458
|
-
// OpenAI API web search is $10 / 1k calls: $0.01, or one dashboard
|
|
459
|
-
// credit, per completed call. Search-content tokens are already present in
|
|
460
|
-
// the model usage record and are priced normally above.
|
|
461
|
-
deltaTotals.estimatedCredits += webSearchCostCredits;
|
|
462
|
-
if (!hasCountedRawUsage(usage) && webSearchCostCredits > 0) {
|
|
463
|
-
deltaTotals.estimatedCreditsStatus = "known";
|
|
464
|
-
}
|
|
465
|
-
const planType = typeof rateLimits?.plan_type === "string" ? rateLimits.plan_type : undefined;
|
|
466
|
-
const safeEventTimeMs = Number.isFinite(eventTimeMs) ? eventTimeMs : 0;
|
|
467
391
|
tokenEvents += 1;
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
392
|
+
events.push({
|
|
393
|
+
modelId: resolvedModelId,
|
|
394
|
+
usage,
|
|
395
|
+
eventTimeMs,
|
|
396
|
+
rateLimits,
|
|
397
|
+
serviceTier,
|
|
398
|
+
webSearchCalls
|
|
399
|
+
});
|
|
471
400
|
};
|
|
472
401
|
const flushPendingUsageRecord = () => {
|
|
473
402
|
if (!pendingUsageRecord) {
|
|
@@ -527,6 +456,12 @@ async function parseSessionFile(filePath, byModel, byDay, windows, planTypes, kn
|
|
|
527
456
|
if (payloadObject.type === "event_msg") {
|
|
528
457
|
const payload = asRecord(payloadObject.payload);
|
|
529
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();
|
|
530
465
|
const threadSettings = asRecord(payload.thread_settings);
|
|
531
466
|
if (typeof threadSettings?.model === "string" && threadSettings.model.trim()) {
|
|
532
467
|
currentModel = threadSettings.model;
|
|
@@ -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;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { addUsageTotals, sumUsageTotals } from "../../contract.js";
|
|
2
2
|
import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "../../daily.js";
|
|
3
|
-
import { isNonBillableCopilotModel, normalizeCopilotModelId
|
|
3
|
+
import { isNonBillableCopilotModel, normalizeCopilotModelId } from "../models.js";
|
|
4
|
+
import { modelCostCredits } from "../../pricing.js";
|
|
4
5
|
/**
|
|
5
6
|
* Select events whose timestamp falls in the half-open interval
|
|
6
7
|
* `[startTimeMs, endTimeMs)`. An event exactly at `endTimeMs` belongs to the
|
|
@@ -16,7 +17,7 @@ export function filterCopilotUsageEvents(events, startTimeMs, endTimeMs) {
|
|
|
16
17
|
* reported input already INCLUDES cache-read tokens but NOT cache-write tokens.
|
|
17
18
|
* Pure and deterministic: independent of input ordering.
|
|
18
19
|
*/
|
|
19
|
-
export function aggregateCopilotUsage(events) {
|
|
20
|
+
export function aggregateCopilotUsage(events, pricing = new Map()) {
|
|
20
21
|
const byModel = new Map();
|
|
21
22
|
const byDay = createDailyUsageAggregates();
|
|
22
23
|
for (const event of events) {
|
|
@@ -35,15 +36,17 @@ export function aggregateCopilotUsage(events) {
|
|
|
35
36
|
const output = event.outputTokens;
|
|
36
37
|
const reasoning = Math.min(event.reasoningOutputTokens, output);
|
|
37
38
|
const nonBillable = isNonBillableCopilotModel(modelId);
|
|
38
|
-
const
|
|
39
|
-
|
|
39
|
+
const estimatedCredits = hasCacheInfo
|
|
40
|
+
? modelCostCredits(pricing.get(modelId), {
|
|
41
|
+
inputTokens: uncachedInput,
|
|
42
|
+
outputTokens: output,
|
|
43
|
+
cacheReadInputTokens: cacheRead,
|
|
44
|
+
cacheWrite5mInputTokens: cacheWrite,
|
|
45
|
+
cacheWrite1hInputTokens: 0
|
|
46
|
+
})
|
|
47
|
+
: undefined;
|
|
48
|
+
const creditsKnown = nonBillable || estimatedCredits !== undefined;
|
|
40
49
|
const estimatedCreditsStatus = creditsKnown ? "known" : "unavailable";
|
|
41
|
-
const estimatedCredits = rate !== undefined && hasCacheInfo
|
|
42
|
-
? (uncachedInput / 1000000) * rate.input +
|
|
43
|
-
(cacheRead / 1000000) * rate.cacheRead +
|
|
44
|
-
(cacheWrite / 1000000) * rate.cacheWrite +
|
|
45
|
-
(output / 1000000) * rate.output
|
|
46
|
-
: 0;
|
|
47
50
|
const totals = {
|
|
48
51
|
inputTokens: uncachedInput,
|
|
49
52
|
outputTokens: output,
|
|
@@ -53,7 +56,7 @@ export function aggregateCopilotUsage(events) {
|
|
|
53
56
|
cacheWrite1hInputTokens: 0,
|
|
54
57
|
reasoningOutputTokens: reasoning,
|
|
55
58
|
totalTokens: uncachedInput + cacheRead + cacheWrite + output,
|
|
56
|
-
estimatedCredits,
|
|
59
|
+
estimatedCredits: nonBillable ? 0 : (estimatedCredits ?? 0),
|
|
57
60
|
eventCount: 1,
|
|
58
61
|
cacheReadStatus: event.cacheReadStatus,
|
|
59
62
|
cacheWriteStatus: event.cacheWriteStatus,
|
|
@@ -1,23 +1,123 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { request as httpRequest } from "node:http";
|
|
2
|
+
import { request as httpsRequest } from "node:https";
|
|
3
|
+
const MODEL_PRICING_ENDPOINT = process.env.LETMECODE_MODEL_PRICING_ENDPOINT ??
|
|
4
|
+
"https://devforth.io/admin/adminapi/v1/get_model_pricing";
|
|
5
|
+
const PRICE_CACHE_TTL_MS = 5 * 60000;
|
|
6
|
+
const USD_TO_CREDITS = 100;
|
|
7
|
+
const MODEL_SLUG_ALIASES = {
|
|
8
|
+
"claude-haiku-4-5": "claude-4-5-haiku-reasoning",
|
|
9
|
+
"claude-sonnet-4-5": "claude-4-5-sonnet-thinking",
|
|
10
|
+
"gemini-3-1-pro": "gemini-3-1-pro-preview"
|
|
11
|
+
};
|
|
12
|
+
const responseCache = new Map();
|
|
13
|
+
let pricingTransport = postModelPricingRequest;
|
|
14
|
+
export function configureModelPricingTransport(transport) {
|
|
15
|
+
pricingTransport = transport;
|
|
16
|
+
responseCache.clear();
|
|
17
|
+
}
|
|
18
|
+
export function modelPricingSlug(modelId) {
|
|
19
|
+
const slug = modelId
|
|
20
|
+
.trim()
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.replace(/[._\s]+/g, "-")
|
|
23
|
+
.replace(/-+/g, "-")
|
|
24
|
+
.replace(/-(?:\d{8}|\d{4}-\d{2}-\d{2})$/, "");
|
|
25
|
+
return MODEL_SLUG_ALIASES[slug] ?? slug;
|
|
26
|
+
}
|
|
27
|
+
export async function fetchModelPricing(modelIds, source) {
|
|
28
|
+
const slugByModelId = new Map([...new Set(modelIds)]
|
|
29
|
+
.filter((modelId) => modelId !== "unknown" && modelId !== "<synthetic>")
|
|
30
|
+
.map((modelId) => [modelId, modelPricingSlug(modelId)]));
|
|
31
|
+
const slugs = [...new Set(slugByModelId.values())].sort();
|
|
32
|
+
if (slugs.length === 0) {
|
|
33
|
+
return new Map();
|
|
34
|
+
}
|
|
35
|
+
const request = {
|
|
36
|
+
slugs,
|
|
37
|
+
available_in: { main: [source], other: [] }
|
|
38
|
+
};
|
|
39
|
+
const response = await fetchCached(request);
|
|
40
|
+
const pricingBySlug = new Map(response.models.map((model) => [
|
|
41
|
+
model.slug,
|
|
42
|
+
{
|
|
43
|
+
input: model.input,
|
|
44
|
+
output: model.output,
|
|
45
|
+
inputCacheRead: model.input_cache_read,
|
|
46
|
+
inputCacheWrite5m: model.input_cache_w5m,
|
|
47
|
+
inputCacheWrite1h: model.input_cache_w1h
|
|
48
|
+
}
|
|
49
|
+
]));
|
|
50
|
+
return new Map([...slugByModelId.entries()].flatMap(([modelId, slug]) => {
|
|
51
|
+
const pricing = pricingBySlug.get(slug);
|
|
52
|
+
return pricing ? [[modelId, pricing]] : [];
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
export function modelCostCredits(pricing, usage) {
|
|
56
|
+
if (!pricing) {
|
|
8
57
|
return undefined;
|
|
9
58
|
}
|
|
10
|
-
|
|
11
|
-
if (!rate) {
|
|
59
|
+
if (usage.cacheWrite5mInputTokens > 0 && pricing.inputCacheWrite5m === null) {
|
|
12
60
|
return undefined;
|
|
13
61
|
}
|
|
14
|
-
if (
|
|
15
|
-
return
|
|
62
|
+
if (usage.cacheWrite1hInputTokens > 0 && pricing.inputCacheWrite1h === null) {
|
|
63
|
+
return undefined;
|
|
16
64
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
65
|
+
return ((usage.inputTokens / 1000000) * pricing.input +
|
|
66
|
+
(usage.cacheReadInputTokens / 1000000) * pricing.inputCacheRead +
|
|
67
|
+
(usage.cacheWrite5mInputTokens / 1000000) * (pricing.inputCacheWrite5m ?? 0) +
|
|
68
|
+
(usage.cacheWrite1hInputTokens / 1000000) * (pricing.inputCacheWrite1h ?? 0) +
|
|
69
|
+
(usage.outputTokens / 1000000) * pricing.output) * USD_TO_CREDITS;
|
|
70
|
+
}
|
|
71
|
+
async function fetchCached(request) {
|
|
72
|
+
const key = JSON.stringify(request);
|
|
73
|
+
const cached = responseCache.get(key);
|
|
74
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
75
|
+
return cached.response;
|
|
76
|
+
}
|
|
77
|
+
const response = pricingTransport(request);
|
|
78
|
+
responseCache.set(key, {
|
|
79
|
+
expiresAt: Date.now() + PRICE_CACHE_TTL_MS,
|
|
80
|
+
response
|
|
81
|
+
});
|
|
82
|
+
try {
|
|
83
|
+
return await response;
|
|
21
84
|
}
|
|
22
|
-
|
|
85
|
+
catch (error) {
|
|
86
|
+
responseCache.delete(key);
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function postModelPricingRequest(body) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const encodedBody = Buffer.from(JSON.stringify(body), "utf8");
|
|
93
|
+
const target = new URL(MODEL_PRICING_ENDPOINT);
|
|
94
|
+
const request = target.protocol === "http:" ? httpRequest : httpsRequest;
|
|
95
|
+
const req = request({
|
|
96
|
+
method: "POST",
|
|
97
|
+
protocol: target.protocol,
|
|
98
|
+
hostname: target.hostname,
|
|
99
|
+
port: target.port,
|
|
100
|
+
path: `${target.pathname}${target.search}`,
|
|
101
|
+
headers: {
|
|
102
|
+
"content-type": "application/json",
|
|
103
|
+
"content-length": encodedBody.byteLength
|
|
104
|
+
}
|
|
105
|
+
}, (res) => {
|
|
106
|
+
const chunks = [];
|
|
107
|
+
res.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
108
|
+
res.on("end", () => {
|
|
109
|
+
if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
|
|
110
|
+
reject(new Error(`Model pricing request failed with status ${res.statusCode ?? "unknown"}`));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
req.on("error", reject);
|
|
117
|
+
req.setTimeout(5000, () => {
|
|
118
|
+
req.destroy(new Error("Model pricing request timed out"));
|
|
119
|
+
});
|
|
120
|
+
req.write(encodedBody);
|
|
121
|
+
req.end();
|
|
122
|
+
});
|
|
23
123
|
}
|