letmecode 0.1.19 → 0.1.21

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.
@@ -1,373 +1,4 @@
1
- import fs from "node:fs";
2
- import { applyEdits, modify, parse } from "jsonc-parser";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import readline from "node:readline";
6
- import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "./contract.js";
7
- import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "./daily.js";
8
- import { asRecord } from "./limits.js";
9
- import { resolveUsageRate } from "./pricing.js";
10
- const VSCODE_OTEL_SETTINGS = {
11
- "github.copilot.chat.otel.enabled": true,
12
- "github.copilot.chat.otel.exporterType": "file",
13
- "github.copilot.chat.otel.captureContent": false
14
- };
15
- const RATE_CARD = {
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 }
36
- };
37
- const NON_BILLABLE_MODEL_PREFIXES = ["copilot-nes", "copilot-suggestion"];
38
- export class CopilotUsageProvider extends UsageProviderBase {
39
- constructor(options = {}) {
40
- super("copilot", "Copilot");
41
- this.root = path.resolve(options.root ?? os.homedir());
42
- }
43
- async getStats() {
44
- const vscodeOtelFile = getCopilotOtelPath(this.root);
45
- const byModel = new Map();
46
- const byDay = createDailyUsageAggregates();
47
- const warnings = [];
48
- const parseTotals = {
49
- linesRead: 0,
50
- tokenEvents: 0,
51
- malformedLines: 0
52
- };
53
- const vscodeOtelFileExists = await isReadableFile(vscodeOtelFile);
54
- if (vscodeOtelFileExists) {
55
- const fileStats = await parseCopilotJsonlFile(vscodeOtelFile, byModel, byDay);
56
- parseTotals.linesRead += fileStats.linesRead;
57
- parseTotals.tokenEvents += fileStats.tokenEvents;
58
- parseTotals.malformedLines += fileStats.malformedLines;
59
- }
60
- else if (await isCopilotVsCodeLoggingEnabled(this.root, vscodeOtelFile)) {
61
- warnings.push(`VS Code Copilot logging is enabled, but ${vscodeOtelFile} has not been created yet. Reload VS Code and send a Copilot Chat request.`);
62
- }
63
- if (parseTotals.malformedLines > 0) {
64
- warnings.push(`Skipped ${parseTotals.malformedLines} malformed Copilot JSONL line(s).`);
65
- }
66
- const filesScanned = vscodeOtelFileExists ? 1 : 0;
67
- if (filesScanned === 0) {
68
- warnings.push(`No Copilot VS Code OTEL usage file found at ${vscodeOtelFile}.`);
69
- }
70
- else if (parseTotals.tokenEvents === 0) {
71
- warnings.push("No Copilot token usage events found. For VS Code, run Start logging VS Code and reload VS Code.");
72
- }
73
- const modelUsage = [...byModel.entries()]
74
- .map(([modelId, totals]) => ({ modelId, totals }))
75
- .sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
76
- const summaryTotals = sumUsageTotals(modelUsage.map((row) => row.totals));
77
- if (summaryTotals.cacheStatus === "unavailable") {
78
- warnings.push("Copilot cache token attributes are unavailable for some events; cached/non-cached tokens and estimated credits are shown as unknown.");
79
- }
80
- return {
81
- providerId: this.id,
82
- providerLabel: this.label,
83
- summary: {
84
- filesScanned,
85
- linesRead: parseTotals.linesRead,
86
- tokenEvents: parseTotals.tokenEvents,
87
- totals: summaryTotals,
88
- distinctModels: modelUsage.map((row) => row.modelId),
89
- distinctPlanTypes: [],
90
- rootLabel: "~/.copilot/otel/vscode.jsonl",
91
- rootPath: vscodeOtelFile
92
- },
93
- modelUsage,
94
- dayUsage: buildDailyUsageRows(byDay),
95
- primaryLimitWindows: [],
96
- secondaryLimitWindows: [],
97
- warnings
98
- };
99
- }
100
- }
101
- export async function configureCopilotVsCodeLogging(options = {}) {
102
- const root = path.resolve(options.root ?? os.homedir());
103
- const outfile = getCopilotOtelPath(root);
104
- const settingsPath = options.settingsPath ?? (await getVsCodeSettingsPath(root));
105
- const settingsText = await readTextFileOrEmpty(settingsPath);
106
- const { text, changed } = updateJsoncSettings(settingsText, {
107
- ...VSCODE_OTEL_SETTINGS,
108
- "github.copilot.chat.otel.outfile": toVsCodeOutfilePath(outfile)
109
- });
110
- await fs.promises.mkdir(path.dirname(settingsPath), { recursive: true });
111
- await fs.promises.mkdir(path.dirname(outfile), { recursive: true });
112
- if (changed) {
113
- await fs.promises.writeFile(settingsPath, text, "utf8");
114
- }
115
- return { settingsPath, outfile, changed };
116
- }
117
- function getCopilotOtelPath(root) {
118
- return path.join(root, ".copilot", "otel", "vscode.jsonl");
119
- }
120
- function toVsCodeOutfilePath(filePath) {
121
- return process.platform === "win32" ? filePath.replace(/\\/g, "/") : filePath;
122
- }
123
- async function getVsCodeSettingsPath(root) {
124
- const userRoots = getVsCodeUserRoots(root);
125
- for (const userRoot of userRoots) {
126
- if (await isDirectory(userRoot)) {
127
- return path.join(userRoot, "settings.json");
128
- }
129
- }
130
- return path.join(userRoots[0], "settings.json");
131
- }
132
- function getVsCodeUserRoots(root) {
133
- if (process.platform === "darwin") {
134
- const applicationSupport = path.join(root, "Library", "Application Support");
135
- return [
136
- path.join(applicationSupport, "Code", "User"),
137
- path.join(applicationSupport, "Code - Insiders", "User")
138
- ];
139
- }
140
- if (process.platform === "win32") {
141
- const appData = process.env.APPDATA ?? path.join(root, "AppData", "Roaming");
142
- return [path.join(appData, "Code", "User"), path.join(appData, "Code - Insiders", "User")];
143
- }
144
- const configRoot = path.join(root, ".config");
145
- return [path.join(configRoot, "Code", "User"), path.join(configRoot, "Code - Insiders", "User")];
146
- }
147
- async function parseCopilotJsonlFile(filePath, byModel, byDay) {
148
- const stream = fs.createReadStream(filePath, { encoding: "utf8" });
149
- const lineReader = readline.createInterface({ input: stream, crlfDelay: Infinity });
150
- const parseTotals = {
151
- linesRead: 0,
152
- tokenEvents: 0,
153
- malformedLines: 0
154
- };
155
- for await (const line of lineReader) {
156
- parseTotals.linesRead += 1;
157
- if (!line.trim()) {
158
- continue;
159
- }
160
- let payload;
161
- try {
162
- payload = JSON.parse(line);
163
- }
164
- catch {
165
- parseTotals.malformedLines += 1;
166
- continue;
167
- }
168
- const event = extractCopilotUsageEvent(payload);
169
- if (event) {
170
- parseTotals.tokenEvents += 1;
171
- addModelUsage(byModel, event.modelId, event.totals);
172
- addDailyUsage(byDay, event.timestampMs, event.modelId, undefined, event.totals);
173
- }
174
- }
175
- return parseTotals;
176
- }
177
- function extractCopilotUsageEvent(payload) {
178
- const record = asRecord(payload);
179
- if (!record) {
180
- return null;
181
- }
182
- const attributes = asRecord(record.attributes);
183
- if (!attributes || !isCopilotChatSpan(attributes)) {
184
- return null;
185
- }
186
- const usage = usageFromAttributes(attributes);
187
- if (!usage) {
188
- return null;
189
- }
190
- const modelId = stringAttribute(attributes, "gen_ai.response.model") ?? "unknown";
191
- const timestampMs = hrTimeToMs(record.hrTime) ?? Number.NaN;
192
- return {
193
- timestampMs,
194
- modelId,
195
- totals: createUsageTotals(modelId, usage)
196
- };
197
- }
198
- function usageFromAttributes(attributes) {
199
- const inputTokens = numberAttribute(attributes, "gen_ai.usage.input_tokens") ?? 0;
200
- const outputTokens = numberAttribute(attributes, "gen_ai.usage.output_tokens") ?? 0;
201
- const reasoningOutputTokens = numberAttribute(attributes, "gen_ai.usage.reasoning.output_tokens");
202
- const cachedInputTokens = numberAttribute(attributes, "gen_ai.usage.cache_read.input_tokens");
203
- const cacheCreationInputTokens = numberAttribute(attributes, "gen_ai.usage.cache_creation.input_tokens");
204
- if (inputTokens <= 0 && outputTokens <= 0 && (reasoningOutputTokens ?? 0) <= 0) {
205
- return null;
206
- }
207
- return {
208
- inputTokens,
209
- cachedInputTokens,
210
- cacheCreationInputTokens,
211
- outputTokens,
212
- reasoningOutputTokens
213
- };
214
- }
215
- function isCopilotChatSpan(attributes) {
216
- return stringAttribute(attributes, "gen_ai.operation.name") === "chat";
217
- }
218
- function createUsageTotals(modelId, usage) {
219
- const hasCacheInfo = usage.cachedInputTokens !== undefined || usage.cacheCreationInputTokens !== undefined;
220
- const hasKnownCreditPricing = isNonBillableModel(modelId) || (hasCacheInfo && rateForModel(modelId, usage.inputTokens) !== undefined);
221
- const cachedInputTokens = hasCacheInfo ? Math.max(0, usage.cachedInputTokens ?? 0) : 0;
222
- const cacheWriteInputTokens = hasCacheInfo ? Math.max(0, usage.cacheCreationInputTokens ?? 0) : 0;
223
- const uncachedInputTokens = hasCacheInfo
224
- ? Math.max(0, usage.inputTokens - cachedInputTokens - cacheWriteInputTokens)
225
- : usage.inputTokens;
226
- return {
227
- inputTokens: uncachedInputTokens,
228
- outputTokens: usage.outputTokens,
229
- cacheReadInputTokens: cachedInputTokens,
230
- cacheWriteInputTokens,
231
- cacheWrite5mInputTokens: 0,
232
- cacheWrite1hInputTokens: 0,
233
- reasoningOutputTokens: Math.min(usage.reasoningOutputTokens ?? 0, usage.outputTokens),
234
- totalTokens: usage.inputTokens + usage.outputTokens,
235
- estimatedCredits: creditsFor(modelId, usage),
236
- eventCount: 1,
237
- cacheStatus: hasCacheInfo ? "known" : "unavailable",
238
- estimatedCreditsStatus: hasKnownCreditPricing ? "known" : "unavailable"
239
- };
240
- }
241
- function creditsFor(modelId, usage) {
242
- if (isNonBillableModel(modelId)) {
243
- return 0;
244
- }
245
- const rate = rateForModel(modelId, usage.inputTokens);
246
- if (!rate) {
247
- return 0;
248
- }
249
- if (usage.cachedInputTokens === undefined && usage.cacheCreationInputTokens === undefined) {
250
- return 0;
251
- }
252
- const cacheRead = Math.min(usage.cachedInputTokens ?? 0, usage.inputTokens);
253
- const cacheWrite = Math.min(usage.cacheCreationInputTokens ?? 0, Math.max(0, usage.inputTokens - cacheRead));
254
- const regularInput = Math.max(0, usage.inputTokens - cacheRead - cacheWrite);
255
- return ((regularInput / 1000000) * rate.input +
256
- (cacheRead / 1000000) * rate.cacheRead +
257
- (cacheWrite / 1000000) * rate.cacheWrite +
258
- (usage.outputTokens / 1000000) * rate.output);
259
- }
260
- function rateForModel(modelId, inputTokens) {
261
- return resolveUsageRate(RATE_CARD, modelId, inputTokens, { prefixMatch: true });
262
- }
263
- function isNonBillableModel(modelId) {
264
- return NON_BILLABLE_MODEL_PREFIXES.some((prefix) => modelId === prefix || modelId.startsWith(`${prefix}-`));
265
- }
266
- function addModelUsage(byModel, modelId, deltaTotals) {
267
- const resolvedModelId = modelId || "unknown";
268
- const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
269
- addUsageTotals(totals, deltaTotals);
270
- byModel.set(resolvedModelId, totals);
271
- }
272
- function numberAttribute(attributes, key) {
273
- const value = attributes[key];
274
- if (typeof value === "number" && Number.isFinite(value)) {
275
- return value;
276
- }
277
- return undefined;
278
- }
279
- function stringAttribute(attributes, key) {
280
- const value = attributes[key];
281
- if (typeof value === "string" && value) {
282
- return value;
283
- }
284
- return undefined;
285
- }
286
- function hrTimeToMs(value) {
287
- if (!Array.isArray(value)) {
288
- return undefined;
289
- }
290
- const [seconds, nanoseconds] = value;
291
- if (typeof seconds !== "number" ||
292
- !Number.isFinite(seconds) ||
293
- typeof nanoseconds !== "number" ||
294
- !Number.isFinite(nanoseconds)) {
295
- return undefined;
296
- }
297
- return seconds * 1000 + nanoseconds / 1000000;
298
- }
299
- async function isReadableFile(filePath) {
300
- try {
301
- const stat = await fs.promises.stat(filePath);
302
- return stat.isFile();
303
- }
304
- catch {
305
- return false;
306
- }
307
- }
308
- async function isDirectory(filePath) {
309
- try {
310
- const stat = await fs.promises.stat(filePath);
311
- return stat.isDirectory();
312
- }
313
- catch {
314
- return false;
315
- }
316
- }
317
- async function isCopilotVsCodeLoggingEnabled(root, outfile) {
318
- const settings = await readJsonSettings(await getVsCodeSettingsPath(root));
319
- const configuredOutfile = settings["github.copilot.chat.otel.outfile"];
320
- return (settings["github.copilot.chat.otel.enabled"] === true &&
321
- settings["github.copilot.chat.otel.exporterType"] === "file" &&
322
- typeof configuredOutfile === "string" &&
323
- normalizeComparablePath(configuredOutfile) === normalizeComparablePath(toVsCodeOutfilePath(outfile)));
324
- }
325
- function normalizeComparablePath(filePath) {
326
- const normalized = path.resolve(filePath).replace(/\\/g, "/");
327
- return process.platform === "win32" ? normalized.toLowerCase() : normalized;
328
- }
329
- async function readJsonSettings(filePath) {
330
- return parseJsoncSettings(await readTextFileOrEmpty(filePath));
331
- }
332
- async function readTextFileOrEmpty(filePath) {
333
- try {
334
- return await fs.promises.readFile(filePath, "utf8");
335
- }
336
- catch (error) {
337
- if (error.code === "ENOENT") {
338
- return "";
339
- }
340
- throw error;
341
- }
342
- }
343
- function parseJsoncSettings(raw) {
344
- if (!raw.trim()) {
345
- return {};
346
- }
347
- const parsed = parse(raw);
348
- return asRecord(parsed) ?? {};
349
- }
350
- function updateJsoncSettings(raw, values) {
351
- let text = raw.trim() ? raw : "{\n}";
352
- let changed = false;
353
- for (const [key, value] of Object.entries(values)) {
354
- if (parseJsoncSettings(text)[key] === value) {
355
- continue;
356
- }
357
- const edits = modify(text, [key], value, {
358
- formattingOptions: {
359
- eol: "\n",
360
- insertSpaces: true,
361
- tabSize: 4
362
- }
363
- });
364
- if (edits.length > 0) {
365
- text = applyEdits(text, edits);
366
- changed = true;
367
- }
368
- }
369
- if (changed && !text.endsWith("\n")) {
370
- text += "\n";
371
- }
372
- return { text, changed };
373
- }
1
+ // Backward-compatible entry point for the Copilot usage provider. The
2
+ // implementation was split by responsibility under ./copilot/* — this module
3
+ // only re-exports the public surface so existing import paths keep working.
4
+ export { CopilotUsageProvider, configureCopilotVsCodeLogging, getCopilotCliOtelEnv } from "./copilot/provider.js";
@@ -13,5 +13,5 @@ export function createProviders() {
13
13
  export { AntigravityUsageProvider } from "./antigravity.js";
14
14
  export { ClaudeUsageProvider } from "./claude.js";
15
15
  export { CodexUsageProvider } from "./codex.js";
16
- export { CopilotUsageProvider, configureCopilotVsCodeLogging } from "./copilot.js";
16
+ export { CopilotUsageProvider, configureCopilotVsCodeLogging, getCopilotCliOtelEnv } from "./copilot.js";
17
17
  export { UsageProviderBase } from "./contract.js";
@@ -16,7 +16,7 @@ export async function reportAnonymousUsage(statsList) {
16
16
  export async function buildAnonymousUsageReports(statsList) {
17
17
  const letmecodeVersion = await readLetmecodeVersion();
18
18
  return statsList.flatMap((stats) => {
19
- if (!stats.analytics?.userIdHash || stats.providerId === "antigravity") {
19
+ if (!stats.analytics?.userIdHash) {
20
20
  return [];
21
21
  }
22
22
  return [...stats.primaryLimitWindows, ...stats.secondaryLimitWindows]
@@ -49,6 +49,9 @@ function resolveReportModelType(stats, window) {
49
49
  if (stats.providerId === "antigravity") {
50
50
  return resolveAntigravityReportModelType(stats, window);
51
51
  }
52
+ if (stats.providerId === "claude") {
53
+ return resolveClaudeReportModelType(window);
54
+ }
52
55
  if (window.modelType) {
53
56
  return truncateSchemaString(window.modelType, 128);
54
57
  }
@@ -60,6 +63,9 @@ function resolveReportModelType(stats, window) {
60
63
  }
61
64
  return truncateSchemaString(stats.providerId, 128);
62
65
  }
66
+ function resolveClaudeReportModelType(window) {
67
+ return window.modelType?.toLowerCase().includes("sonnet") ? "sonnet-only" : "all";
68
+ }
63
69
  function resolveAntigravityReportModelType(stats, window) {
64
70
  const limitId = window.limitId.toLowerCase();
65
71
  if (limitId.includes("gemini")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letmecode",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "Provider-based terminal usage dashboard for LetMeCode.",
5
5
  "author": "devforth.io",
6
6
  "license": "MIT",