letmecode 0.1.3 → 0.1.5
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 +1 -0
- package/ink-app/dist/index.js +474 -103
- package/ink-app/dist/providers/antigravity.js +290 -0
- package/ink-app/dist/providers/claude.js +458 -54
- package/ink-app/dist/providers/codex.js +161 -23
- package/ink-app/dist/providers/contract.js +14 -46
- package/ink-app/dist/providers/copilot.js +31 -46
- package/ink-app/dist/providers/index.js +14 -1
- package/ink-app/dist/providers/limits.js +1 -1
- package/ink-app/dist/providers/pricing.js +18 -0
- package/ink-app/dist/reporting.js +99 -0
- package/package.json +15 -14
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
@@ -5,10 +6,11 @@ import readline from "node:readline";
|
|
|
5
6
|
import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "./contract.js";
|
|
6
7
|
import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregates, numberOrZero } from "./limits.js";
|
|
7
8
|
import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "./daily.js";
|
|
9
|
+
import { resolveUsageRate } from "./pricing.js";
|
|
8
10
|
const RATE_CARD = {
|
|
9
|
-
"gpt-5.5": { input: 125,
|
|
10
|
-
"gpt-5.4": { input: 62.5,
|
|
11
|
-
"gpt-5.4-mini": { input: 18.75,
|
|
11
|
+
"gpt-5.5": { input: 125, cacheRead: 12.5, cacheWrite: 125, cacheWrite5m: 125, cacheWrite1h: 125, output: 750 },
|
|
12
|
+
"gpt-5.4": { input: 62.5, cacheRead: 6.25, cacheWrite: 62.5, cacheWrite5m: 62.5, cacheWrite1h: 62.5, output: 375 },
|
|
13
|
+
"gpt-5.4-mini": { input: 18.75, cacheRead: 1.875, cacheWrite: 18.75, cacheWrite5m: 18.75, cacheWrite1h: 18.75, output: 113 }
|
|
12
14
|
};
|
|
13
15
|
export class CodexUsageProvider extends UsageProviderBase {
|
|
14
16
|
constructor(options = {}) {
|
|
@@ -17,6 +19,8 @@ export class CodexUsageProvider extends UsageProviderBase {
|
|
|
17
19
|
}
|
|
18
20
|
async getStats(_options = {}) {
|
|
19
21
|
const sessionsRoot = path.join(this.root, ".codex", "sessions");
|
|
22
|
+
const knownModels = await readCodexModelMetadata(this.root);
|
|
23
|
+
const userIdHash = await readCodexUserIdHash(this.root, this.label);
|
|
20
24
|
const byModel = new Map();
|
|
21
25
|
const byDay = createDailyUsageAggregates();
|
|
22
26
|
const windows = createLimitWindowAggregates();
|
|
@@ -30,7 +34,7 @@ export class CodexUsageProvider extends UsageProviderBase {
|
|
|
30
34
|
};
|
|
31
35
|
for await (const file of walkSessionFiles(sessionsRoot)) {
|
|
32
36
|
parseTotals.filesScanned += 1;
|
|
33
|
-
const fileStats = await parseSessionFile(file, byModel, byDay, windows, planTypes);
|
|
37
|
+
const fileStats = await parseSessionFile(file, byModel, byDay, windows, planTypes, knownModels);
|
|
34
38
|
parseTotals.linesRead += fileStats.linesRead;
|
|
35
39
|
parseTotals.tokenEvents += fileStats.tokenEvents;
|
|
36
40
|
parseTotals.malformedLines += fileStats.malformedLines;
|
|
@@ -43,7 +47,7 @@ export class CodexUsageProvider extends UsageProviderBase {
|
|
|
43
47
|
.sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
|
|
44
48
|
const unknownPricedModels = modelUsage
|
|
45
49
|
.map((row) => row.modelId)
|
|
46
|
-
.filter((modelId) => !RATE_CARD[modelId]);
|
|
50
|
+
.filter((modelId) => !RATE_CARD[modelId] && !isAssumedZeroRatedCodexModel(modelId, knownModels));
|
|
47
51
|
if (unknownPricedModels.length > 0) {
|
|
48
52
|
warnings.push(`No credit rate configured for: ${unknownPricedModels.join(", ")}.`);
|
|
49
53
|
}
|
|
@@ -70,10 +74,132 @@ export class CodexUsageProvider extends UsageProviderBase {
|
|
|
70
74
|
dayUsage,
|
|
71
75
|
primaryLimitWindows,
|
|
72
76
|
secondaryLimitWindows,
|
|
73
|
-
warnings
|
|
77
|
+
warnings,
|
|
78
|
+
analytics: {
|
|
79
|
+
agentName: normalizeAnalyticsAgentName(this.label),
|
|
80
|
+
userIdHash
|
|
81
|
+
}
|
|
74
82
|
};
|
|
75
83
|
}
|
|
76
84
|
}
|
|
85
|
+
async function readCodexModelMetadata(root) {
|
|
86
|
+
const modelsCachePath = path.join(root, ".codex", "models_cache.json");
|
|
87
|
+
let fileText;
|
|
88
|
+
try {
|
|
89
|
+
fileText = await fs.promises.readFile(modelsCachePath, "utf8");
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return new Map();
|
|
93
|
+
}
|
|
94
|
+
let payload;
|
|
95
|
+
try {
|
|
96
|
+
payload = JSON.parse(fileText);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return new Map();
|
|
100
|
+
}
|
|
101
|
+
const models = asRecord(payload)?.models;
|
|
102
|
+
if (!Array.isArray(models)) {
|
|
103
|
+
return new Map();
|
|
104
|
+
}
|
|
105
|
+
const metadata = new Map();
|
|
106
|
+
for (const model of models) {
|
|
107
|
+
const record = asRecord(model);
|
|
108
|
+
const slug = typeof record?.slug === "string" ? record.slug : "";
|
|
109
|
+
if (!slug) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
metadata.set(slug, {
|
|
113
|
+
visibility: typeof record?.visibility === "string" ? record.visibility : ""
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return metadata;
|
|
117
|
+
}
|
|
118
|
+
async function readCodexUserIdHash(root, agentName) {
|
|
119
|
+
const authPath = path.join(root, ".codex", "auth.json");
|
|
120
|
+
let fileText;
|
|
121
|
+
try {
|
|
122
|
+
fileText = await fs.promises.readFile(authPath, "utf8");
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
let payload;
|
|
128
|
+
try {
|
|
129
|
+
payload = JSON.parse(fileText);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const tokens = asRecord(asRecord(payload)?.tokens);
|
|
135
|
+
const idToken = typeof tokens?.id_token === "string" ? tokens.id_token : "";
|
|
136
|
+
const accessToken = typeof tokens?.access_token === "string" ? tokens.access_token : "";
|
|
137
|
+
const identity = extractCodexIdentity(idToken) ?? extractCodexIdentity(accessToken);
|
|
138
|
+
if (!identity) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return buildUserIdHash([
|
|
142
|
+
normalizeAnalyticsAgentName(agentName),
|
|
143
|
+
identity.email,
|
|
144
|
+
identity.orgId,
|
|
145
|
+
identity.orgName
|
|
146
|
+
]);
|
|
147
|
+
}
|
|
148
|
+
function extractCodexIdentity(token) {
|
|
149
|
+
const payload = decodeJwtPayload(token);
|
|
150
|
+
if (!payload) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const authRecord = asRecord(payload["https://api.openai.com/auth"]);
|
|
154
|
+
const profileRecord = asRecord(payload["https://api.openai.com/profile"]);
|
|
155
|
+
const organizations = Array.isArray(authRecord?.organizations) ? authRecord.organizations : [];
|
|
156
|
+
const defaultOrganizationRecord = organizations
|
|
157
|
+
.map((organization) => asRecord(organization))
|
|
158
|
+
.find((organization) => organization?.is_default === true) ??
|
|
159
|
+
organizations
|
|
160
|
+
.map((organization) => asRecord(organization))
|
|
161
|
+
.find(Boolean) ??
|
|
162
|
+
null;
|
|
163
|
+
const emailCandidates = [
|
|
164
|
+
typeof payload.email === "string" ? payload.email : "",
|
|
165
|
+
typeof profileRecord?.email === "string" ? profileRecord.email : ""
|
|
166
|
+
];
|
|
167
|
+
const email = emailCandidates.find((candidate) => candidate) ?? "";
|
|
168
|
+
const orgId = typeof defaultOrganizationRecord?.id === "string" ? defaultOrganizationRecord.id : "";
|
|
169
|
+
const orgName = typeof defaultOrganizationRecord?.title === "string" ? defaultOrganizationRecord.title : "";
|
|
170
|
+
if (!email || !orgId || !orgName) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
return { email, orgId, orgName };
|
|
174
|
+
}
|
|
175
|
+
function decodeJwtPayload(token) {
|
|
176
|
+
const parts = token.split(".");
|
|
177
|
+
if (parts.length < 2 || !parts[1]) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const payloadText = Buffer.from(normalizeBase64Url(parts[1]), "base64").toString("utf8");
|
|
182
|
+
const payload = JSON.parse(payloadText);
|
|
183
|
+
return asRecord(payload);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function normalizeBase64Url(value) {
|
|
190
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
191
|
+
const paddingLength = (4 - (normalized.length % 4)) % 4;
|
|
192
|
+
return normalized + "=".repeat(paddingLength);
|
|
193
|
+
}
|
|
194
|
+
function buildUserIdHash(parts) {
|
|
195
|
+
if (parts.some((part) => !part)) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
return createHash("md5").update(parts.join("-")).digest("hex");
|
|
199
|
+
}
|
|
200
|
+
function normalizeAnalyticsAgentName(label) {
|
|
201
|
+
return label.replace(/\s+/g, "");
|
|
202
|
+
}
|
|
77
203
|
function createEmptyRawUsage() {
|
|
78
204
|
return {
|
|
79
205
|
inputTokens: 0,
|
|
@@ -103,44 +229,56 @@ function subtractRawUsage(current, previous) {
|
|
|
103
229
|
};
|
|
104
230
|
}
|
|
105
231
|
function creditsFor(modelId, usage) {
|
|
106
|
-
const rate = RATE_CARD
|
|
232
|
+
const rate = resolveUsageRate(RATE_CARD, modelId);
|
|
107
233
|
if (!rate) {
|
|
108
234
|
return 0;
|
|
109
235
|
}
|
|
110
|
-
|
|
111
|
-
|
|
236
|
+
const cachedInputTokens = Math.min(usage.cachedInputTokens, usage.inputTokens);
|
|
237
|
+
const nonCachedInputTokens = Math.max(0, usage.inputTokens - cachedInputTokens);
|
|
238
|
+
return ((nonCachedInputTokens / 1000000) * rate.input +
|
|
239
|
+
(cachedInputTokens / 1000000) * rate.cacheRead +
|
|
112
240
|
(usage.outputTokens / 1000000) * rate.output);
|
|
113
241
|
}
|
|
114
242
|
function rawUsageToTotals(usage) {
|
|
115
|
-
const
|
|
243
|
+
const cacheReadInputTokens = Math.min(usage.cachedInputTokens, usage.inputTokens);
|
|
244
|
+
const inputTokens = Math.max(0, usage.inputTokens - cacheReadInputTokens);
|
|
116
245
|
return {
|
|
117
|
-
|
|
246
|
+
inputTokens,
|
|
118
247
|
outputTokens: usage.outputTokens,
|
|
248
|
+
cacheReadInputTokens,
|
|
249
|
+
cacheWriteInputTokens: 0,
|
|
250
|
+
cacheWrite5mInputTokens: 0,
|
|
251
|
+
cacheWrite1hInputTokens: 0,
|
|
119
252
|
reasoningOutputTokens: usage.reasoningOutputTokens,
|
|
120
|
-
totalTokens:
|
|
253
|
+
totalTokens: inputTokens + cacheReadInputTokens + usage.outputTokens,
|
|
121
254
|
estimatedCredits: 0,
|
|
122
|
-
eventCount: 0
|
|
123
|
-
tokenBreakdown: {
|
|
124
|
-
schema: "openai",
|
|
125
|
-
nonCachedInputTokens: usage.inputTokens,
|
|
126
|
-
cachedInputTokens: usage.cachedInputTokens,
|
|
127
|
-
outputTokens: usage.outputTokens
|
|
128
|
-
}
|
|
255
|
+
eventCount: 0
|
|
129
256
|
};
|
|
130
257
|
}
|
|
131
|
-
function createUsageTotalsForModel(modelId, usage) {
|
|
258
|
+
function createUsageTotalsForModel(modelId, usage, knownModels) {
|
|
132
259
|
const resolvedModelId = modelId || "unknown";
|
|
133
260
|
const deltaTotals = rawUsageToTotals(usage);
|
|
134
261
|
deltaTotals.estimatedCredits = creditsFor(resolvedModelId, usage);
|
|
135
262
|
deltaTotals.eventCount = 1;
|
|
263
|
+
if (!RATE_CARD[resolvedModelId] && !isAssumedZeroRatedCodexModel(resolvedModelId, knownModels)) {
|
|
264
|
+
deltaTotals.estimatedCreditsStatus = "unavailable";
|
|
265
|
+
}
|
|
136
266
|
return deltaTotals;
|
|
137
267
|
}
|
|
138
268
|
function addModelUsage(byModel, modelId, deltaTotals) {
|
|
139
269
|
const resolvedModelId = modelId || "unknown";
|
|
140
|
-
const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals(
|
|
270
|
+
const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
|
|
141
271
|
addUsageTotals(totals, deltaTotals);
|
|
142
272
|
byModel.set(resolvedModelId, totals);
|
|
143
273
|
}
|
|
274
|
+
function isHiddenCodexModel(modelId, knownModels) {
|
|
275
|
+
return knownModels.get(modelId)?.visibility === "hide";
|
|
276
|
+
}
|
|
277
|
+
function isAssumedZeroRatedCodexModel(modelId, knownModels) {
|
|
278
|
+
// Hidden internal Codex models do not have a public rate card entry. For dashboard
|
|
279
|
+
// rollups we treat them as zero-rated so they do not turn aggregate totals unknown.
|
|
280
|
+
return isHiddenCodexModel(modelId, knownModels);
|
|
281
|
+
}
|
|
144
282
|
function isSessionFile(filePath) {
|
|
145
283
|
return filePath.endsWith(".jsonl") && filePath.includes(`${path.sep}.codex${path.sep}sessions${path.sep}`);
|
|
146
284
|
}
|
|
@@ -162,7 +300,7 @@ async function* walkSessionFiles(directory) {
|
|
|
162
300
|
}
|
|
163
301
|
}
|
|
164
302
|
}
|
|
165
|
-
async function parseSessionFile(filePath, byModel, byDay, windows, planTypes) {
|
|
303
|
+
async function parseSessionFile(filePath, byModel, byDay, windows, planTypes, knownModels) {
|
|
166
304
|
const stream = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
167
305
|
const lineReader = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
168
306
|
let currentModel = "unknown";
|
|
@@ -203,7 +341,7 @@ async function parseSessionFile(filePath, byModel, byDay, windows, planTypes) {
|
|
|
203
341
|
const usage = lastUsage ? normalizeRawUsage(lastUsage) : previousTotal ? subtractRawUsage(totalUsage, previousTotal) : totalUsage;
|
|
204
342
|
previousTotal = totalUsage;
|
|
205
343
|
const resolvedModelId = currentModel || "unknown";
|
|
206
|
-
const deltaTotals = createUsageTotalsForModel(resolvedModelId, usage);
|
|
344
|
+
const deltaTotals = createUsageTotalsForModel(resolvedModelId, usage, knownModels);
|
|
207
345
|
tokenEvents += 1;
|
|
208
346
|
addModelUsage(byModel, resolvedModelId, deltaTotals);
|
|
209
347
|
const eventTimeMs = Date.parse(String(payloadObject.timestamp ?? ""));
|
|
@@ -4,31 +4,34 @@ export class UsageProviderBase {
|
|
|
4
4
|
this.label = label;
|
|
5
5
|
}
|
|
6
6
|
}
|
|
7
|
-
export function createEmptyUsageTotals(
|
|
7
|
+
export function createEmptyUsageTotals() {
|
|
8
8
|
return {
|
|
9
|
-
|
|
9
|
+
inputTokens: 0,
|
|
10
10
|
outputTokens: 0,
|
|
11
|
+
cacheReadInputTokens: 0,
|
|
12
|
+
cacheWriteInputTokens: 0,
|
|
13
|
+
cacheWrite5mInputTokens: 0,
|
|
14
|
+
cacheWrite1hInputTokens: 0,
|
|
11
15
|
reasoningOutputTokens: 0,
|
|
12
16
|
totalTokens: 0,
|
|
13
17
|
estimatedCredits: 0,
|
|
14
|
-
eventCount: 0
|
|
15
|
-
tokenBreakdown: createEmptyUsageTokenBreakdown(schema)
|
|
18
|
+
eventCount: 0
|
|
16
19
|
};
|
|
17
20
|
}
|
|
18
21
|
export function cloneUsageTotals(totals) {
|
|
19
|
-
return {
|
|
20
|
-
...totals,
|
|
21
|
-
tokenBreakdown: { ...totals.tokenBreakdown }
|
|
22
|
-
};
|
|
22
|
+
return { ...totals };
|
|
23
23
|
}
|
|
24
24
|
export function addUsageTotals(target, source) {
|
|
25
|
-
target.
|
|
25
|
+
target.inputTokens += source.inputTokens;
|
|
26
26
|
target.outputTokens += source.outputTokens;
|
|
27
|
+
target.cacheReadInputTokens += source.cacheReadInputTokens;
|
|
28
|
+
target.cacheWriteInputTokens += source.cacheWriteInputTokens;
|
|
29
|
+
target.cacheWrite5mInputTokens += source.cacheWrite5mInputTokens;
|
|
30
|
+
target.cacheWrite1hInputTokens += source.cacheWrite1hInputTokens;
|
|
27
31
|
target.reasoningOutputTokens += source.reasoningOutputTokens;
|
|
28
32
|
target.totalTokens += source.totalTokens;
|
|
29
33
|
target.estimatedCredits += source.estimatedCredits;
|
|
30
34
|
target.eventCount += source.eventCount;
|
|
31
|
-
addUsageTokenBreakdown(target.tokenBreakdown, source.tokenBreakdown);
|
|
32
35
|
if (source.cacheStatus === "unavailable") {
|
|
33
36
|
target.cacheStatus = "unavailable";
|
|
34
37
|
}
|
|
@@ -37,44 +40,9 @@ export function addUsageTotals(target, source) {
|
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
42
|
export function sumUsageTotals(rows) {
|
|
40
|
-
const totals = createEmptyUsageTotals(
|
|
43
|
+
const totals = createEmptyUsageTotals();
|
|
41
44
|
for (const row of rows) {
|
|
42
45
|
addUsageTotals(totals, row);
|
|
43
46
|
}
|
|
44
47
|
return totals;
|
|
45
48
|
}
|
|
46
|
-
function createEmptyUsageTokenBreakdown(schema) {
|
|
47
|
-
if (schema === "anthropic") {
|
|
48
|
-
return {
|
|
49
|
-
schema,
|
|
50
|
-
inputTokens: 0,
|
|
51
|
-
cacheWrite5mInputTokens: 0,
|
|
52
|
-
cacheWrite1hInputTokens: 0,
|
|
53
|
-
cacheReadInputTokens: 0,
|
|
54
|
-
outputTokens: 0
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
return {
|
|
58
|
-
schema,
|
|
59
|
-
nonCachedInputTokens: 0,
|
|
60
|
-
cachedInputTokens: 0,
|
|
61
|
-
outputTokens: 0
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
function addUsageTokenBreakdown(target, source) {
|
|
65
|
-
if (target.schema !== source.schema) {
|
|
66
|
-
throw new Error(`Cannot merge ${source.schema} usage totals into ${target.schema} totals.`);
|
|
67
|
-
}
|
|
68
|
-
target.outputTokens += source.outputTokens;
|
|
69
|
-
if (target.schema === "anthropic" && source.schema === "anthropic") {
|
|
70
|
-
target.inputTokens += source.inputTokens;
|
|
71
|
-
target.cacheWrite5mInputTokens += source.cacheWrite5mInputTokens;
|
|
72
|
-
target.cacheWrite1hInputTokens += source.cacheWrite1hInputTokens;
|
|
73
|
-
target.cacheReadInputTokens += source.cacheReadInputTokens;
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
if (target.schema === "openai" && source.schema === "openai") {
|
|
77
|
-
target.nonCachedInputTokens += source.nonCachedInputTokens;
|
|
78
|
-
target.cachedInputTokens += source.cachedInputTokens;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
@@ -6,37 +6,33 @@ import readline from "node:readline";
|
|
|
6
6
|
import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "./contract.js";
|
|
7
7
|
import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "./daily.js";
|
|
8
8
|
import { asRecord } from "./limits.js";
|
|
9
|
+
import { resolveUsageRate } from "./pricing.js";
|
|
9
10
|
const VSCODE_OTEL_SETTINGS = {
|
|
10
11
|
"github.copilot.chat.otel.enabled": true,
|
|
11
12
|
"github.copilot.chat.otel.exporterType": "file",
|
|
12
13
|
"github.copilot.chat.otel.captureContent": false
|
|
13
14
|
};
|
|
14
15
|
const RATE_CARD = {
|
|
15
|
-
"gpt-5-mini": { input: 25,
|
|
16
|
-
"gpt-5.3-codex": { input: 175,
|
|
17
|
-
"gpt-5.4": { input: 250,
|
|
18
|
-
"gpt-5.4-mini": { input: 75,
|
|
19
|
-
"gpt-5.4-nano": { input: 20,
|
|
20
|
-
"gpt-5.5": { input: 500,
|
|
21
|
-
"claude-haiku-4-5": { input: 100,
|
|
22
|
-
"claude-sonnet-4-5": { input: 300,
|
|
23
|
-
"claude-sonnet-4-6": { input: 300,
|
|
24
|
-
"claude-opus-4-5": { input: 500,
|
|
25
|
-
"claude-opus-4-6": { input: 500,
|
|
26
|
-
"claude-opus-4-7": { input: 500,
|
|
27
|
-
"claude-opus-4-8": { input: 500,
|
|
28
|
-
"claude-fable-5": { input: 1000,
|
|
29
|
-
"gemini-2.5-pro": { input: 125,
|
|
30
|
-
"gemini-3-flash": { input: 50,
|
|
31
|
-
"gemini-3.1-pro": { input: 200,
|
|
32
|
-
"gemini-3.5-flash": { input: 150,
|
|
33
|
-
"mai-code-1-flash": { input: 75,
|
|
34
|
-
"raptor-mini": { input: 25,
|
|
35
|
-
};
|
|
36
|
-
const LONG_CONTEXT_RATE_CARD = {
|
|
37
|
-
"gpt-5.4": { thresholdInputTokens: 272000, input: 500, cachedInput: 50, output: 2250 },
|
|
38
|
-
"gpt-5.5": { thresholdInputTokens: 272000, input: 1000, cachedInput: 100, output: 4500 },
|
|
39
|
-
"gemini-3.1-pro": { thresholdInputTokens: 200000, input: 400, cachedInput: 40, output: 1800 }
|
|
16
|
+
"gpt-5-mini": { input: 25, cacheRead: 2.5, cacheWrite: 25, cacheWrite5m: 25, cacheWrite1h: 25, output: 200 },
|
|
17
|
+
"gpt-5.3-codex": { input: 175, cacheRead: 17.5, cacheWrite: 175, cacheWrite5m: 175, cacheWrite1h: 175, output: 1400 },
|
|
18
|
+
"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 } } },
|
|
19
|
+
"gpt-5.4-mini": { input: 75, cacheRead: 7.5, cacheWrite: 75, cacheWrite5m: 75, cacheWrite1h: 75, output: 450 },
|
|
20
|
+
"gpt-5.4-nano": { input: 20, cacheRead: 2, cacheWrite: 20, cacheWrite5m: 20, cacheWrite1h: 20, output: 125 },
|
|
21
|
+
"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 } } },
|
|
22
|
+
"claude-haiku-4-5": { input: 100, cacheRead: 10, cacheWrite: 125, cacheWrite5m: 125, cacheWrite1h: 200, output: 500 },
|
|
23
|
+
"claude-sonnet-4-5": { input: 300, cacheRead: 30, cacheWrite: 375, cacheWrite5m: 375, cacheWrite1h: 600, output: 1500 },
|
|
24
|
+
"claude-sonnet-4-6": { input: 300, cacheRead: 30, cacheWrite: 375, cacheWrite5m: 375, cacheWrite1h: 600, output: 1500 },
|
|
25
|
+
"claude-opus-4-5": { input: 500, cacheRead: 50, cacheWrite: 625, cacheWrite5m: 625, cacheWrite1h: 1000, output: 2500 },
|
|
26
|
+
"claude-opus-4-6": { input: 500, cacheRead: 50, cacheWrite: 625, cacheWrite5m: 625, cacheWrite1h: 1000, output: 2500 },
|
|
27
|
+
"claude-opus-4-7": { input: 500, cacheRead: 50, cacheWrite: 625, cacheWrite5m: 625, cacheWrite1h: 1000, output: 2500 },
|
|
28
|
+
"claude-opus-4-8": { input: 500, cacheRead: 50, cacheWrite: 625, cacheWrite5m: 625, cacheWrite1h: 1000, output: 2500 },
|
|
29
|
+
"claude-fable-5": { input: 1000, cacheRead: 100, cacheWrite: 1250, cacheWrite5m: 1250, cacheWrite1h: 2000, output: 5000 },
|
|
30
|
+
"gemini-2.5-pro": { input: 125, cacheRead: 12.5, cacheWrite: 125, cacheWrite5m: 125, cacheWrite1h: 125, output: 1000 },
|
|
31
|
+
"gemini-3-flash": { input: 50, cacheRead: 5, cacheWrite: 50, cacheWrite5m: 50, cacheWrite1h: 50, output: 300 },
|
|
32
|
+
"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 } } },
|
|
33
|
+
"gemini-3.5-flash": { input: 150, cacheRead: 15, cacheWrite: 150, cacheWrite5m: 150, cacheWrite1h: 150, output: 900 },
|
|
34
|
+
"mai-code-1-flash": { input: 75, cacheRead: 7.5, cacheWrite: 75, cacheWrite5m: 75, cacheWrite1h: 75, output: 450 },
|
|
35
|
+
"raptor-mini": { input: 25, cacheRead: 2.5, cacheWrite: 25, cacheWrite5m: 25, cacheWrite1h: 25, output: 200 }
|
|
40
36
|
};
|
|
41
37
|
const NON_BILLABLE_MODEL_PREFIXES = ["copilot-nes", "copilot-suggestion"];
|
|
42
38
|
export class CopilotUsageProvider extends UsageProviderBase {
|
|
@@ -226,20 +222,18 @@ function createUsageTotals(modelId, usage) {
|
|
|
226
222
|
const cacheWriteInputTokens = hasCacheInfo ? Math.max(0, usage.cacheCreationInputTokens ?? 0) : 0;
|
|
227
223
|
const uncachedInputTokens = hasCacheInfo
|
|
228
224
|
? Math.max(0, usage.inputTokens - cachedInputTokens - cacheWriteInputTokens)
|
|
229
|
-
:
|
|
225
|
+
: usage.inputTokens;
|
|
230
226
|
return {
|
|
231
|
-
|
|
227
|
+
inputTokens: uncachedInputTokens,
|
|
232
228
|
outputTokens: usage.outputTokens,
|
|
229
|
+
cacheReadInputTokens: cachedInputTokens,
|
|
230
|
+
cacheWriteInputTokens,
|
|
231
|
+
cacheWrite5mInputTokens: 0,
|
|
232
|
+
cacheWrite1hInputTokens: 0,
|
|
233
233
|
reasoningOutputTokens: Math.min(usage.reasoningOutputTokens ?? 0, usage.outputTokens),
|
|
234
234
|
totalTokens: usage.inputTokens + usage.outputTokens,
|
|
235
235
|
estimatedCredits: creditsFor(modelId, usage),
|
|
236
236
|
eventCount: 1,
|
|
237
|
-
tokenBreakdown: {
|
|
238
|
-
schema: "openai",
|
|
239
|
-
nonCachedInputTokens: uncachedInputTokens,
|
|
240
|
-
cachedInputTokens,
|
|
241
|
-
outputTokens: usage.outputTokens
|
|
242
|
-
},
|
|
243
237
|
cacheStatus: hasCacheInfo ? "known" : "unavailable",
|
|
244
238
|
estimatedCreditsStatus: hasKnownCreditPricing ? "known" : "unavailable"
|
|
245
239
|
};
|
|
@@ -259,28 +253,19 @@ function creditsFor(modelId, usage) {
|
|
|
259
253
|
const cacheWrite = Math.min(usage.cacheCreationInputTokens ?? 0, Math.max(0, usage.inputTokens - cacheRead));
|
|
260
254
|
const regularInput = Math.max(0, usage.inputTokens - cacheRead - cacheWrite);
|
|
261
255
|
return ((regularInput / 1000000) * rate.input +
|
|
262
|
-
(cacheRead / 1000000) * rate.
|
|
263
|
-
(cacheWrite / 1000000) *
|
|
256
|
+
(cacheRead / 1000000) * rate.cacheRead +
|
|
257
|
+
(cacheWrite / 1000000) * rate.cacheWrite +
|
|
264
258
|
(usage.outputTokens / 1000000) * rate.output);
|
|
265
259
|
}
|
|
266
260
|
function rateForModel(modelId, inputTokens) {
|
|
267
|
-
|
|
268
|
-
const model = candidates.find((candidate) => modelId === candidate || modelId.startsWith(`${candidate}-`));
|
|
269
|
-
if (!model) {
|
|
270
|
-
return undefined;
|
|
271
|
-
}
|
|
272
|
-
const longContextRate = LONG_CONTEXT_RATE_CARD[model];
|
|
273
|
-
if (longContextRate && inputTokens > longContextRate.thresholdInputTokens) {
|
|
274
|
-
return longContextRate;
|
|
275
|
-
}
|
|
276
|
-
return RATE_CARD[model];
|
|
261
|
+
return resolveUsageRate(RATE_CARD, modelId, inputTokens, { prefixMatch: true });
|
|
277
262
|
}
|
|
278
263
|
function isNonBillableModel(modelId) {
|
|
279
264
|
return NON_BILLABLE_MODEL_PREFIXES.some((prefix) => modelId === prefix || modelId.startsWith(`${prefix}-`));
|
|
280
265
|
}
|
|
281
266
|
function addModelUsage(byModel, modelId, deltaTotals) {
|
|
282
267
|
const resolvedModelId = modelId || "unknown";
|
|
283
|
-
const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals(
|
|
268
|
+
const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
|
|
284
269
|
addUsageTotals(totals, deltaTotals);
|
|
285
270
|
byModel.set(resolvedModelId, totals);
|
|
286
271
|
}
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { ClaudeUsageProvider } from "./claude.js";
|
|
2
2
|
import { CodexUsageProvider } from "./codex.js";
|
|
3
3
|
import { CopilotUsageProvider } from "./copilot.js";
|
|
4
|
+
import { AntigravityUsageProvider } from "./antigravity.js";
|
|
4
5
|
export function createProviders() {
|
|
5
|
-
return [
|
|
6
|
+
return [
|
|
7
|
+
new CodexUsageProvider(),
|
|
8
|
+
new ClaudeUsageProvider(),
|
|
9
|
+
new ClaudeUsageProvider({
|
|
10
|
+
id: "claude-vscode",
|
|
11
|
+
label: "Claude VSCode",
|
|
12
|
+
entrypoints: ["claude-vscode"],
|
|
13
|
+
usageCommandKind: "vscode"
|
|
14
|
+
}),
|
|
15
|
+
new CopilotUsageProvider(),
|
|
16
|
+
new AntigravityUsageProvider()
|
|
17
|
+
];
|
|
6
18
|
}
|
|
19
|
+
export { AntigravityUsageProvider } from "./antigravity.js";
|
|
7
20
|
export { ClaudeUsageProvider } from "./claude.js";
|
|
8
21
|
export { CodexUsageProvider } from "./codex.js";
|
|
9
22
|
export { CopilotUsageProvider, configureCopilotVsCodeLogging } from "./copilot.js";
|
|
@@ -93,7 +93,7 @@ function collapseNearbyWindows(rows) {
|
|
|
93
93
|
function computeWindowTotals(events) {
|
|
94
94
|
// Session files are not guaranteed to be parsed in timestamp order, so
|
|
95
95
|
// saturation has to be applied after we sort the captured window events.
|
|
96
|
-
const totals = createEmptyUsageTotals(
|
|
96
|
+
const totals = createEmptyUsageTotals();
|
|
97
97
|
let sawBelowCap = false;
|
|
98
98
|
let isExhausted = false;
|
|
99
99
|
for (const event of [...events].sort((left, right) => left.eventTimeMs - right.eventTimeMs)) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function resolveUsageRate(rateCard, modelId, inputTokens = 0, options = {}) {
|
|
2
|
+
const model = options.prefixMatch
|
|
3
|
+
? Object.keys(rateCard)
|
|
4
|
+
.sort((left, right) => right.length - left.length)
|
|
5
|
+
.find((candidate) => modelId === candidate || modelId.startsWith(`${candidate}-`))
|
|
6
|
+
: modelId;
|
|
7
|
+
if (!model) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
const rate = rateCard[model];
|
|
11
|
+
if (!rate) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
if (rate.longContext && inputTokens > rate.longContext.thresholdTokens) {
|
|
15
|
+
return rate.longContext.rate;
|
|
16
|
+
}
|
|
17
|
+
return rate;
|
|
18
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { request } from "node:https";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const REPORTING_ENDPOINT = "https://devforth.io/admin/api/report_ussage_anonymous";
|
|
6
|
+
const CREDIT_TO_DOLLARS = 0.01;
|
|
7
|
+
let versionCache = null;
|
|
8
|
+
export async function reportAnonymousUsage(statsList) {
|
|
9
|
+
const payload = await buildAnonymousUsagePayload(statsList);
|
|
10
|
+
if (payload.data.length === 0) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
await postJson(REPORTING_ENDPOINT, payload);
|
|
14
|
+
}
|
|
15
|
+
export async function buildAnonymousUsageReports(statsList) {
|
|
16
|
+
const letmecodeVersion = await readLetmecodeVersion();
|
|
17
|
+
return statsList.flatMap((stats) => {
|
|
18
|
+
if (!stats.analytics?.userIdHash) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
return [...stats.primaryLimitWindows, ...stats.secondaryLimitWindows].map((window) => buildAnonymousUsageReport(stats, window, letmecodeVersion));
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export async function buildAnonymousUsagePayload(statsList) {
|
|
25
|
+
return {
|
|
26
|
+
data: await buildAnonymousUsageReports(statsList)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function buildAnonymousUsageReport(stats, window, letmecodeVersion) {
|
|
30
|
+
return {
|
|
31
|
+
agent: stats.analytics?.agentName ?? stats.providerLabel.replace(/\s+/g, ""),
|
|
32
|
+
userid_hash: stats.analytics?.userIdHash ?? "",
|
|
33
|
+
plan_id: window.planType,
|
|
34
|
+
window_duration_seconds: window.windowMinutes * 60,
|
|
35
|
+
window_start_utc_iso: window.startTimeUtcIso,
|
|
36
|
+
window_end_utc_iso: window.endTimeUtcIso,
|
|
37
|
+
used_percents: resolveReportedUsedPercents(window),
|
|
38
|
+
used_exhausted: window.maxUsedPercent >= 100,
|
|
39
|
+
value_dollars: roundDollars(window.totals.estimatedCredits * CREDIT_TO_DOLLARS),
|
|
40
|
+
letmecode_version: letmecodeVersion
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function resolveReportedUsedPercents(window) {
|
|
44
|
+
if (window.minUsedPercent === window.maxUsedPercent) {
|
|
45
|
+
return clampPercent(window.maxUsedPercent);
|
|
46
|
+
}
|
|
47
|
+
return clampPercent(window.maxUsedPercent - window.minUsedPercent);
|
|
48
|
+
}
|
|
49
|
+
function clampPercent(value) {
|
|
50
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
51
|
+
}
|
|
52
|
+
function roundDollars(value) {
|
|
53
|
+
return Number(value.toFixed(6));
|
|
54
|
+
}
|
|
55
|
+
async function readLetmecodeVersion() {
|
|
56
|
+
if (versionCache) {
|
|
57
|
+
return versionCache;
|
|
58
|
+
}
|
|
59
|
+
versionCache = (async () => {
|
|
60
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
61
|
+
const packageJsonPath = path.resolve(path.dirname(currentFilePath), "..", "..", "package.json");
|
|
62
|
+
const fileText = await fs.readFile(packageJsonPath, "utf8");
|
|
63
|
+
const payload = JSON.parse(fileText);
|
|
64
|
+
return typeof payload.version === "string" && payload.version ? payload.version : "unknown";
|
|
65
|
+
})();
|
|
66
|
+
return versionCache;
|
|
67
|
+
}
|
|
68
|
+
async function postJson(url, body) {
|
|
69
|
+
await new Promise((resolve, reject) => {
|
|
70
|
+
const encodedBody = Buffer.from(JSON.stringify(body), "utf8");
|
|
71
|
+
const target = new URL(url);
|
|
72
|
+
const req = request({
|
|
73
|
+
method: "POST",
|
|
74
|
+
protocol: target.protocol,
|
|
75
|
+
hostname: target.hostname,
|
|
76
|
+
port: target.port,
|
|
77
|
+
path: `${target.pathname}${target.search}`,
|
|
78
|
+
headers: {
|
|
79
|
+
"content-type": "application/json",
|
|
80
|
+
"content-length": encodedBody.byteLength
|
|
81
|
+
}
|
|
82
|
+
}, (res) => {
|
|
83
|
+
res.resume();
|
|
84
|
+
res.on("end", () => {
|
|
85
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
86
|
+
resolve();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
reject(new Error(`Unexpected response status: ${res.statusCode ?? "unknown"}`));
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
req.on("error", reject);
|
|
93
|
+
req.setTimeout(5000, () => {
|
|
94
|
+
req.destroy(new Error("Anonymous usage reporting timed out"));
|
|
95
|
+
});
|
|
96
|
+
req.write(encodedBody);
|
|
97
|
+
req.end();
|
|
98
|
+
});
|
|
99
|
+
}
|