letmecode 0.1.4 → 0.1.6
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 +263 -133
- package/ink-app/dist/providers/antigravity.js +290 -0
- package/ink-app/dist/providers/claude.js +539 -56
- package/ink-app/dist/providers/codex.js +111 -18
- 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 +115 -0
- package/package.json +5 -2
|
@@ -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 = {}) {
|
|
@@ -18,6 +20,7 @@ export class CodexUsageProvider extends UsageProviderBase {
|
|
|
18
20
|
async getStats(_options = {}) {
|
|
19
21
|
const sessionsRoot = path.join(this.root, ".codex", "sessions");
|
|
20
22
|
const knownModels = await readCodexModelMetadata(this.root);
|
|
23
|
+
const userIdHash = await readCodexUserIdHash(this.root, this.label);
|
|
21
24
|
const byModel = new Map();
|
|
22
25
|
const byDay = createDailyUsageAggregates();
|
|
23
26
|
const windows = createLimitWindowAggregates();
|
|
@@ -71,7 +74,11 @@ export class CodexUsageProvider extends UsageProviderBase {
|
|
|
71
74
|
dayUsage,
|
|
72
75
|
primaryLimitWindows,
|
|
73
76
|
secondaryLimitWindows,
|
|
74
|
-
warnings
|
|
77
|
+
warnings,
|
|
78
|
+
analytics: {
|
|
79
|
+
agentName: normalizeAnalyticsAgentName(this.label),
|
|
80
|
+
userIdHash
|
|
81
|
+
}
|
|
75
82
|
};
|
|
76
83
|
}
|
|
77
84
|
}
|
|
@@ -108,6 +115,91 @@ async function readCodexModelMetadata(root) {
|
|
|
108
115
|
}
|
|
109
116
|
return metadata;
|
|
110
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
|
+
}
|
|
111
203
|
function createEmptyRawUsage() {
|
|
112
204
|
return {
|
|
113
205
|
inputTokens: 0,
|
|
@@ -137,29 +229,30 @@ function subtractRawUsage(current, previous) {
|
|
|
137
229
|
};
|
|
138
230
|
}
|
|
139
231
|
function creditsFor(modelId, usage) {
|
|
140
|
-
const rate = RATE_CARD
|
|
232
|
+
const rate = resolveUsageRate(RATE_CARD, modelId);
|
|
141
233
|
if (!rate) {
|
|
142
234
|
return 0;
|
|
143
235
|
}
|
|
144
|
-
|
|
145
|
-
|
|
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 +
|
|
146
240
|
(usage.outputTokens / 1000000) * rate.output);
|
|
147
241
|
}
|
|
148
242
|
function rawUsageToTotals(usage) {
|
|
149
|
-
const
|
|
243
|
+
const cacheReadInputTokens = Math.min(usage.cachedInputTokens, usage.inputTokens);
|
|
244
|
+
const inputTokens = Math.max(0, usage.inputTokens - cacheReadInputTokens);
|
|
150
245
|
return {
|
|
151
|
-
|
|
246
|
+
inputTokens,
|
|
152
247
|
outputTokens: usage.outputTokens,
|
|
248
|
+
cacheReadInputTokens,
|
|
249
|
+
cacheWriteInputTokens: 0,
|
|
250
|
+
cacheWrite5mInputTokens: 0,
|
|
251
|
+
cacheWrite1hInputTokens: 0,
|
|
153
252
|
reasoningOutputTokens: usage.reasoningOutputTokens,
|
|
154
|
-
totalTokens:
|
|
253
|
+
totalTokens: inputTokens + cacheReadInputTokens + usage.outputTokens,
|
|
155
254
|
estimatedCredits: 0,
|
|
156
|
-
eventCount: 0
|
|
157
|
-
tokenBreakdown: {
|
|
158
|
-
schema: "openai",
|
|
159
|
-
nonCachedInputTokens: usage.inputTokens,
|
|
160
|
-
cachedInputTokens: usage.cachedInputTokens,
|
|
161
|
-
outputTokens: usage.outputTokens
|
|
162
|
-
}
|
|
255
|
+
eventCount: 0
|
|
163
256
|
};
|
|
164
257
|
}
|
|
165
258
|
function createUsageTotalsForModel(modelId, usage, knownModels) {
|
|
@@ -174,7 +267,7 @@ function createUsageTotalsForModel(modelId, usage, knownModels) {
|
|
|
174
267
|
}
|
|
175
268
|
function addModelUsage(byModel, modelId, deltaTotals) {
|
|
176
269
|
const resolvedModelId = modelId || "unknown";
|
|
177
|
-
const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals(
|
|
270
|
+
const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
|
|
178
271
|
addUsageTotals(totals, deltaTotals);
|
|
179
272
|
byModel.set(resolvedModelId, totals);
|
|
180
273
|
}
|
|
@@ -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,115 @@
|
|
|
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
|
+
usage_raw: buildUsageRaw(stats.providerId, window),
|
|
41
|
+
letmecode_version: letmecodeVersion
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function buildUsageRaw(providerId, window) {
|
|
45
|
+
const usageRaw = {
|
|
46
|
+
output: window.totals.outputTokens,
|
|
47
|
+
input_non_cache: window.totals.inputTokens,
|
|
48
|
+
input_cache_read: window.totals.cacheReadInputTokens
|
|
49
|
+
};
|
|
50
|
+
if (isAnthropicProvider(providerId)) {
|
|
51
|
+
usageRaw.input_cache_w5m = window.totals.cacheWrite5mInputTokens;
|
|
52
|
+
usageRaw.input_cache_w1h = window.totals.cacheWrite1hInputTokens;
|
|
53
|
+
}
|
|
54
|
+
return usageRaw;
|
|
55
|
+
}
|
|
56
|
+
function isAnthropicProvider(providerId) {
|
|
57
|
+
return providerId === "claude" || providerId === "claude-vscode";
|
|
58
|
+
}
|
|
59
|
+
function resolveReportedUsedPercents(window) {
|
|
60
|
+
if (window.minUsedPercent === window.maxUsedPercent) {
|
|
61
|
+
return clampPercent(window.maxUsedPercent);
|
|
62
|
+
}
|
|
63
|
+
return clampPercent(window.maxUsedPercent - window.minUsedPercent);
|
|
64
|
+
}
|
|
65
|
+
function clampPercent(value) {
|
|
66
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
67
|
+
}
|
|
68
|
+
function roundDollars(value) {
|
|
69
|
+
return Number(value.toFixed(6));
|
|
70
|
+
}
|
|
71
|
+
async function readLetmecodeVersion() {
|
|
72
|
+
if (versionCache) {
|
|
73
|
+
return versionCache;
|
|
74
|
+
}
|
|
75
|
+
versionCache = (async () => {
|
|
76
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
77
|
+
const packageJsonPath = path.resolve(path.dirname(currentFilePath), "..", "..", "package.json");
|
|
78
|
+
const fileText = await fs.readFile(packageJsonPath, "utf8");
|
|
79
|
+
const payload = JSON.parse(fileText);
|
|
80
|
+
return typeof payload.version === "string" && payload.version ? payload.version : "unknown";
|
|
81
|
+
})();
|
|
82
|
+
return versionCache;
|
|
83
|
+
}
|
|
84
|
+
async function postJson(url, body) {
|
|
85
|
+
await new Promise((resolve, reject) => {
|
|
86
|
+
const encodedBody = Buffer.from(JSON.stringify(body), "utf8");
|
|
87
|
+
const target = new URL(url);
|
|
88
|
+
const req = request({
|
|
89
|
+
method: "POST",
|
|
90
|
+
protocol: target.protocol,
|
|
91
|
+
hostname: target.hostname,
|
|
92
|
+
port: target.port,
|
|
93
|
+
path: `${target.pathname}${target.search}`,
|
|
94
|
+
headers: {
|
|
95
|
+
"content-type": "application/json",
|
|
96
|
+
"content-length": encodedBody.byteLength
|
|
97
|
+
}
|
|
98
|
+
}, (res) => {
|
|
99
|
+
res.resume();
|
|
100
|
+
res.on("end", () => {
|
|
101
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
102
|
+
resolve();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
reject(new Error(`Unexpected response status: ${res.statusCode ?? "unknown"}`));
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
req.on("error", reject);
|
|
109
|
+
req.setTimeout(5000, () => {
|
|
110
|
+
req.destroy(new Error("Anonymous usage reporting timed out"));
|
|
111
|
+
});
|
|
112
|
+
req.write(encodedBody);
|
|
113
|
+
req.end();
|
|
114
|
+
});
|
|
115
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letmecode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Provider-based terminal usage dashboard for LetMeCode.",
|
|
5
5
|
"author": "devforth.io",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"packageManager": "pnpm@10.28.2",
|
|
8
8
|
"type": "commonjs",
|
|
9
|
-
"bin":
|
|
9
|
+
"bin": {
|
|
10
|
+
"letmecode": "./bin/letmecode.js"
|
|
11
|
+
},
|
|
10
12
|
"files": [
|
|
11
13
|
"bin",
|
|
12
14
|
"dist",
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
"typescript"
|
|
37
39
|
],
|
|
38
40
|
"dependencies": {
|
|
41
|
+
"@tokscale/cli": "4.0.2",
|
|
39
42
|
"ink": "4.4.1",
|
|
40
43
|
"jsonc-parser": "^3.3.1",
|
|
41
44
|
"react": "18.3.1"
|