letmecode 0.1.4 → 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 +263 -133
- package/ink-app/dist/providers/antigravity.js +290 -0
- package/ink-app/dist/providers/claude.js +454 -53
- 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 +99 -0
- package/package.json +15 -14
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import readline from "node:readline";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "./contract.js";
|
|
9
|
+
import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "./daily.js";
|
|
10
|
+
import { resolveUsageRate } from "./pricing.js";
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
const RATE_CARD = {
|
|
14
|
+
"gemini-3.5-flash": {
|
|
15
|
+
input: 150,
|
|
16
|
+
cacheRead: 15,
|
|
17
|
+
cacheWrite: 150,
|
|
18
|
+
cacheWrite5m: 150,
|
|
19
|
+
cacheWrite1h: 150,
|
|
20
|
+
output: 900
|
|
21
|
+
},
|
|
22
|
+
"gemini-3.1-pro": {
|
|
23
|
+
input: 200,
|
|
24
|
+
cacheRead: 20,
|
|
25
|
+
cacheWrite: 200,
|
|
26
|
+
cacheWrite5m: 200,
|
|
27
|
+
cacheWrite1h: 200,
|
|
28
|
+
output: 1200,
|
|
29
|
+
longContext: {
|
|
30
|
+
thresholdTokens: 200000,
|
|
31
|
+
rate: {
|
|
32
|
+
input: 400,
|
|
33
|
+
cacheRead: 40,
|
|
34
|
+
cacheWrite: 400,
|
|
35
|
+
cacheWrite5m: 400,
|
|
36
|
+
cacheWrite1h: 400,
|
|
37
|
+
output: 1800
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"gemini-3-flash": {
|
|
42
|
+
input: 50,
|
|
43
|
+
cacheRead: 5,
|
|
44
|
+
cacheWrite: 50,
|
|
45
|
+
cacheWrite5m: 50,
|
|
46
|
+
cacheWrite1h: 50,
|
|
47
|
+
output: 300
|
|
48
|
+
},
|
|
49
|
+
"claude-sonnet-4-6": {
|
|
50
|
+
input: 300,
|
|
51
|
+
cacheRead: 30,
|
|
52
|
+
cacheWrite: 375,
|
|
53
|
+
cacheWrite5m: 375,
|
|
54
|
+
cacheWrite1h: 600,
|
|
55
|
+
output: 1500
|
|
56
|
+
},
|
|
57
|
+
"claude-opus-4-6": {
|
|
58
|
+
input: 500,
|
|
59
|
+
cacheRead: 50,
|
|
60
|
+
cacheWrite: 625,
|
|
61
|
+
cacheWrite5m: 625,
|
|
62
|
+
cacheWrite1h: 1000,
|
|
63
|
+
output: 2500
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const UNPRICED_MODELS = new Set([
|
|
67
|
+
"gpt-oss-120b"
|
|
68
|
+
]);
|
|
69
|
+
const MODEL_ALIASES = {
|
|
70
|
+
"gemini-3-flash-a": "gemini-3-flash",
|
|
71
|
+
"gemini-3-flash-preview": "gemini-3-flash",
|
|
72
|
+
"gemini-3.1-pro-preview": "gemini-3.1-pro",
|
|
73
|
+
"gemini-3.5-flash-preview": "gemini-3.5-flash",
|
|
74
|
+
"claude-sonnet-4-6-20251201": "claude-sonnet-4-6",
|
|
75
|
+
"claude-opus-4-6-20251201": "claude-opus-4-6"
|
|
76
|
+
};
|
|
77
|
+
export class AntigravityUsageProvider extends UsageProviderBase {
|
|
78
|
+
constructor(options = {}) {
|
|
79
|
+
super("antigravity", "Antigravity");
|
|
80
|
+
this.collectUsage =
|
|
81
|
+
options.collectUsage ??
|
|
82
|
+
collectAntigravityUsageFromTokscale;
|
|
83
|
+
}
|
|
84
|
+
async getStats(_options = {}) {
|
|
85
|
+
const warnings = [];
|
|
86
|
+
let records = [];
|
|
87
|
+
try {
|
|
88
|
+
records = await this.collectUsage();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
warnings.push("Open Antigravity IDE before running LetMeCode so token usage can be synchronized.");
|
|
92
|
+
}
|
|
93
|
+
const selectedRecords = deduplicateRecords(records);
|
|
94
|
+
const duplicateEvents = records.length - selectedRecords.length;
|
|
95
|
+
if (duplicateEvents > 0) {
|
|
96
|
+
warnings.push(`Collapsed ${duplicateEvents} duplicate Antigravity usage response(s).`);
|
|
97
|
+
}
|
|
98
|
+
const byModel = new Map();
|
|
99
|
+
const byDay = createDailyUsageAggregates();
|
|
100
|
+
for (const record of selectedRecords) {
|
|
101
|
+
const modelId = resolveModelId(record.modelId);
|
|
102
|
+
const totals = usageRecordToTotals(modelId, record);
|
|
103
|
+
addModelUsage(byModel, modelId, totals);
|
|
104
|
+
addDailyUsage(byDay, record.timestamp, modelId, undefined, totals);
|
|
105
|
+
}
|
|
106
|
+
const modelUsage = [...byModel.entries()]
|
|
107
|
+
.map(([modelId, totals]) => ({
|
|
108
|
+
modelId,
|
|
109
|
+
totals
|
|
110
|
+
}))
|
|
111
|
+
.sort((left, right) => right.totals.estimatedCredits -
|
|
112
|
+
left.totals.estimatedCredits);
|
|
113
|
+
const unknownPricedModels = modelUsage
|
|
114
|
+
.filter((row) => !rateForModel(row.modelId, rowInputTokens(row)) && !UNPRICED_MODELS.has(row.modelId))
|
|
115
|
+
.map((row) => row.modelId);
|
|
116
|
+
if (unknownPricedModels.length > 0) {
|
|
117
|
+
warnings.push(`No Antigravity estimated API-equivalent rate configured for: ${unknownPricedModels.join(", ")}.`);
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
providerId: this.id,
|
|
121
|
+
providerLabel: this.label,
|
|
122
|
+
summary: {
|
|
123
|
+
filesScanned: records.length > 0 ? 1 : 0,
|
|
124
|
+
linesRead: records.length,
|
|
125
|
+
tokenEvents: selectedRecords.length,
|
|
126
|
+
totals: sumUsageTotals(modelUsage.map((row) => row.totals)),
|
|
127
|
+
distinctModels: modelUsage.map((row) => row.modelId),
|
|
128
|
+
distinctPlanTypes: [],
|
|
129
|
+
rootLabel: "Antigravity IDE sync",
|
|
130
|
+
rootPath: "antigravity-ide-rpc"
|
|
131
|
+
},
|
|
132
|
+
modelUsage,
|
|
133
|
+
dayUsage: buildDailyUsageRows(byDay),
|
|
134
|
+
primaryLimitWindows: [],
|
|
135
|
+
secondaryLimitWindows: [],
|
|
136
|
+
warnings
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export async function collectAntigravityUsage() {
|
|
141
|
+
return collectAntigravityUsageFromTokscale();
|
|
142
|
+
}
|
|
143
|
+
async function collectAntigravityUsageFromTokscale() {
|
|
144
|
+
await runTokscale([
|
|
145
|
+
"antigravity",
|
|
146
|
+
"sync"
|
|
147
|
+
]);
|
|
148
|
+
return readAntigravityUsageCache(getAntigravityCacheRoot());
|
|
149
|
+
}
|
|
150
|
+
async function runTokscale(args) {
|
|
151
|
+
return execFileAsync(process.execPath, [require.resolve("@tokscale/cli/dist/index.js"), ...args], {
|
|
152
|
+
encoding: "utf8",
|
|
153
|
+
maxBuffer: 32 * 1024 * 1024
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function numberOrZero(value) {
|
|
157
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
158
|
+
? value
|
|
159
|
+
: 0;
|
|
160
|
+
}
|
|
161
|
+
async function readAntigravityUsageCache(cacheRoot) {
|
|
162
|
+
const sessionsRoot = path.join(cacheRoot, "sessions");
|
|
163
|
+
const records = [];
|
|
164
|
+
for await (const filePath of walkJsonlFiles(sessionsRoot)) {
|
|
165
|
+
const stream = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
166
|
+
const lineReader = readline.createInterface({
|
|
167
|
+
input: stream,
|
|
168
|
+
crlfDelay: Infinity
|
|
169
|
+
});
|
|
170
|
+
for await (const line of lineReader) {
|
|
171
|
+
if (!line.trim()) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
let payload;
|
|
175
|
+
try {
|
|
176
|
+
payload = JSON.parse(line);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const record = usageRecordFromCacheEntry(payload);
|
|
182
|
+
if (record) {
|
|
183
|
+
records.push(record);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return records;
|
|
188
|
+
}
|
|
189
|
+
function usageRecordFromCacheEntry(value) {
|
|
190
|
+
const entry = value && typeof value === "object"
|
|
191
|
+
? value
|
|
192
|
+
: null;
|
|
193
|
+
if (!entry || entry.type !== "usage") {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const sessionId = typeof entry.sessionId === "string" ? entry.sessionId : "";
|
|
197
|
+
const responseId = typeof entry.responseId === "string" ? entry.responseId : "";
|
|
198
|
+
const modelId = typeof entry.modelId === "string" ? entry.modelId : "";
|
|
199
|
+
const timestamp = numberOrZero(entry.timestamp);
|
|
200
|
+
if (!sessionId || !responseId || !modelId || timestamp <= 0) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
type: "usage",
|
|
205
|
+
sessionId,
|
|
206
|
+
responseId,
|
|
207
|
+
timestamp,
|
|
208
|
+
modelId,
|
|
209
|
+
input: numberOrZero(entry.input),
|
|
210
|
+
cacheRead: numberOrZero(entry.cacheRead),
|
|
211
|
+
cacheWrite: numberOrZero(entry.cacheWrite),
|
|
212
|
+
output: numberOrZero(entry.output),
|
|
213
|
+
reasoning: numberOrZero(entry.reasoning)
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function getAntigravityCacheRoot() {
|
|
217
|
+
return path.join(os.homedir(), ".config", "tokscale", "antigravity-cache");
|
|
218
|
+
}
|
|
219
|
+
async function* walkJsonlFiles(directory) {
|
|
220
|
+
let entries;
|
|
221
|
+
try {
|
|
222
|
+
entries = await fs.promises.readdir(directory, {
|
|
223
|
+
withFileTypes: true
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
for (const entry of entries) {
|
|
230
|
+
const fullPath = path.join(directory, entry.name);
|
|
231
|
+
if (entry.isDirectory()) {
|
|
232
|
+
yield* walkJsonlFiles(fullPath);
|
|
233
|
+
}
|
|
234
|
+
else if (entry.isFile() && fullPath.endsWith(".jsonl")) {
|
|
235
|
+
yield fullPath;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function deduplicateRecords(records) {
|
|
240
|
+
const byKey = new Map();
|
|
241
|
+
for (const record of records) {
|
|
242
|
+
byKey.set(`${record.sessionId}:${record.responseId}`, record);
|
|
243
|
+
}
|
|
244
|
+
return [...byKey.values()];
|
|
245
|
+
}
|
|
246
|
+
function usageRecordToTotals(modelId, record) {
|
|
247
|
+
return {
|
|
248
|
+
inputTokens: record.input,
|
|
249
|
+
outputTokens: record.output,
|
|
250
|
+
cacheReadInputTokens: record.cacheRead,
|
|
251
|
+
cacheWriteInputTokens: record.cacheWrite,
|
|
252
|
+
cacheWrite5mInputTokens: 0,
|
|
253
|
+
cacheWrite1hInputTokens: 0,
|
|
254
|
+
reasoningOutputTokens: Math.min(record.reasoning, record.output),
|
|
255
|
+
totalTokens: record.input +
|
|
256
|
+
record.cacheRead +
|
|
257
|
+
record.cacheWrite +
|
|
258
|
+
record.output,
|
|
259
|
+
estimatedCredits: creditsFor(modelId, record),
|
|
260
|
+
eventCount: 1,
|
|
261
|
+
cacheStatus: "known",
|
|
262
|
+
estimatedCreditsStatus: rateForModel(modelId, record.input)
|
|
263
|
+
? "known"
|
|
264
|
+
: "unavailable"
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function creditsFor(modelId, record) {
|
|
268
|
+
const rate = rateForModel(modelId, record.input);
|
|
269
|
+
if (!rate) {
|
|
270
|
+
return 0;
|
|
271
|
+
}
|
|
272
|
+
return ((record.input / 1000000) * rate.input +
|
|
273
|
+
(record.cacheRead / 1000000) * rate.cacheRead +
|
|
274
|
+
(record.cacheWrite / 1000000) * rate.cacheWrite +
|
|
275
|
+
(record.output / 1000000) * rate.output);
|
|
276
|
+
}
|
|
277
|
+
function rateForModel(modelId, inputTokens) {
|
|
278
|
+
return resolveUsageRate(RATE_CARD, modelId, inputTokens);
|
|
279
|
+
}
|
|
280
|
+
function rowInputTokens(row) {
|
|
281
|
+
return row.totals.inputTokens + row.totals.cacheReadInputTokens + row.totals.cacheWriteInputTokens;
|
|
282
|
+
}
|
|
283
|
+
function resolveModelId(modelId) {
|
|
284
|
+
return MODEL_ALIASES[modelId] ?? (modelId || "unknown");
|
|
285
|
+
}
|
|
286
|
+
function addModelUsage(byModel, modelId, deltaTotals) {
|
|
287
|
+
const totals = byModel.get(modelId) ?? createEmptyUsageTotals();
|
|
288
|
+
addUsageTotals(totals, deltaTotals);
|
|
289
|
+
byModel.set(modelId, totals);
|
|
290
|
+
}
|