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.
@@ -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, cachedInput: 12.5, output: 750 },
10
- "gpt-5.4": { input: 62.5, cachedInput: 6.25, output: 375 },
11
- "gpt-5.4-mini": { input: 18.75, cachedInput: 1.875, output: 113 }
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[modelId];
232
+ const rate = resolveUsageRate(RATE_CARD, modelId);
141
233
  if (!rate) {
142
234
  return 0;
143
235
  }
144
- return ((usage.inputTokens / 1000000) * rate.input +
145
- (usage.cachedInputTokens / 1000000) * rate.cachedInput +
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 inputTotalTokens = usage.inputTokens + usage.cachedInputTokens;
243
+ const cacheReadInputTokens = Math.min(usage.cachedInputTokens, usage.inputTokens);
244
+ const inputTokens = Math.max(0, usage.inputTokens - cacheReadInputTokens);
150
245
  return {
151
- inputTotalTokens,
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: inputTotalTokens + usage.outputTokens,
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("openai");
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(schema = "openai") {
7
+ export function createEmptyUsageTotals() {
8
8
  return {
9
- inputTotalTokens: 0,
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.inputTotalTokens += source.inputTotalTokens;
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(rows[0]?.tokenBreakdown.schema ?? "openai");
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, cachedInput: 2.5, output: 200 },
16
- "gpt-5.3-codex": { input: 175, cachedInput: 17.5, output: 1400 },
17
- "gpt-5.4": { input: 250, cachedInput: 25, output: 1500 },
18
- "gpt-5.4-mini": { input: 75, cachedInput: 7.5, output: 450 },
19
- "gpt-5.4-nano": { input: 20, cachedInput: 2, output: 125 },
20
- "gpt-5.5": { input: 500, cachedInput: 50, output: 3000 },
21
- "claude-haiku-4-5": { input: 100, cachedInput: 10, cacheWrite: 125, output: 500 },
22
- "claude-sonnet-4-5": { input: 300, cachedInput: 30, cacheWrite: 375, output: 1500 },
23
- "claude-sonnet-4-6": { input: 300, cachedInput: 30, cacheWrite: 375, output: 1500 },
24
- "claude-opus-4-5": { input: 500, cachedInput: 50, cacheWrite: 625, output: 2500 },
25
- "claude-opus-4-6": { input: 500, cachedInput: 50, cacheWrite: 625, output: 2500 },
26
- "claude-opus-4-7": { input: 500, cachedInput: 50, cacheWrite: 625, output: 2500 },
27
- "claude-opus-4-8": { input: 500, cachedInput: 50, cacheWrite: 625, output: 2500 },
28
- "claude-fable-5": { input: 1000, cachedInput: 100, cacheWrite: 1250, output: 5000 },
29
- "gemini-2.5-pro": { input: 125, cachedInput: 12.5, output: 1000 },
30
- "gemini-3-flash": { input: 50, cachedInput: 5, output: 300 },
31
- "gemini-3.1-pro": { input: 200, cachedInput: 20, output: 1200 },
32
- "gemini-3.5-flash": { input: 150, cachedInput: 15, output: 900 },
33
- "mai-code-1-flash": { input: 75, cachedInput: 7.5, output: 450 },
34
- "raptor-mini": { input: 25, cachedInput: 2.5, output: 200 }
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
- : 0;
225
+ : usage.inputTokens;
230
226
  return {
231
- inputTotalTokens: usage.inputTokens,
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.cachedInput +
263
- (cacheWrite / 1000000) * (rate.cacheWrite ?? rate.input) +
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
- const candidates = Object.keys(RATE_CARD).sort((left, right) => right.length - left.length);
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("openai");
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 [new CodexUsageProvider(), new ClaudeUsageProvider(), new CopilotUsageProvider()];
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(events[0]?.totals.tokenBreakdown.schema ?? "openai");
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
+ }
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "letmecode",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Provider-based terminal usage dashboard for LetMeCode.",
5
5
  "author": "devforth.io",
6
6
  "license": "MIT",
7
- "packageManager": "pnpm@10.28.2",
8
7
  "type": "commonjs",
9
- "bin": "./bin/letmecode.js",
8
+ "bin": {
9
+ "letmecode": "./bin/letmecode.js"
10
+ },
10
11
  "files": [
11
12
  "bin",
12
13
  "dist",
@@ -19,16 +20,6 @@
19
20
  "publishConfig": {
20
21
  "access": "public"
21
22
  },
22
- "scripts": {
23
- "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
24
- "build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
25
- "prepack": "npm run build",
26
- "prestart": "npm run build",
27
- "start": "node ./bin/letmecode.js",
28
- "pretest": "npm run build",
29
- "smoke": "node ./bin/letmecode.js",
30
- "test": "node --test ink-app/test/*.test.mjs"
31
- },
32
23
  "keywords": [
33
24
  "cli",
34
25
  "ink",
@@ -36,6 +27,7 @@
36
27
  "typescript"
37
28
  ],
38
29
  "dependencies": {
30
+ "@tokscale/cli": "4.0.2",
39
31
  "ink": "4.4.1",
40
32
  "jsonc-parser": "^3.3.1",
41
33
  "react": "18.3.1"
@@ -44,5 +36,14 @@
44
36
  "@types/node": "^24.0.7",
45
37
  "@types/react": "^18.3.24",
46
38
  "typescript": "^5.8.3"
39
+ },
40
+ "scripts": {
41
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
42
+ "build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
43
+ "prestart": "npm run build",
44
+ "start": "node ./bin/letmecode.js",
45
+ "pretest": "npm run build",
46
+ "smoke": "node ./bin/letmecode.js",
47
+ "test": "node --test ink-app/test/*.test.mjs"
47
48
  }
48
- }
49
+ }