letmecode 0.1.29 → 0.1.30

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.
@@ -20,7 +20,19 @@ const MODEL_IDS = {
20
20
  "claude-opus-4-6-20251201": "claude-opus-4-6"
21
21
  };
22
22
  export function normalizeAntigravityModelId(modelId) {
23
- return MODEL_IDS[modelId] ?? (modelId || "unknown");
23
+ const exactModelId = MODEL_IDS[modelId];
24
+ if (exactModelId) {
25
+ return exactModelId;
26
+ }
27
+ const currentGeminiModel = [
28
+ "gemini-3.8-flash",
29
+ "gemini-3.7-flash",
30
+ "gemini-3.6-flash",
31
+ "gemini-3.1-pro"
32
+ ].find((candidate) => modelId === candidate ||
33
+ modelId === `${candidate}-preview` ||
34
+ ["low", "medium", "high"].some((level) => modelId === `${candidate}-${level}`));
35
+ return currentGeminiModel ?? (modelId || "unknown");
24
36
  }
25
37
  export function antigravityModelScope(rawModelId) {
26
38
  const modelId = normalizeAntigravityModelId(rawModelId);
@@ -1,68 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "../contract.js";
3
3
  import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "../daily.js";
4
- import { resolveUsageRate } from "../pricing.js";
4
+ import { fetchModelPricing, modelCostCredits } from "../pricing.js";
5
5
  import { modelScopeLabel, modelScopeMatches, normalizeAntigravityModelId } from "./models.js";
6
6
  import { parseAntigravityQuotaEntries } from "./quota-parser.js";
7
7
  import { findAntigravityLocalServer } from "./rpc/discovery.js";
8
8
  import { extractQuotaGroups, fetchAntigravityUserStatus } from "./rpc/quota.js";
9
9
  import { collectUsageFromLocalRpc } from "./usage-parse.js";
10
- const RATE_CARD = {
11
- "gemini-3.5-flash": {
12
- input: 150,
13
- cacheRead: 15,
14
- cacheWrite: 150,
15
- cacheWrite5m: 150,
16
- cacheWrite1h: 150,
17
- output: 900
18
- },
19
- "gemini-3.1-pro": {
20
- input: 200,
21
- cacheRead: 20,
22
- cacheWrite: 200,
23
- cacheWrite5m: 200,
24
- cacheWrite1h: 200,
25
- output: 1200,
26
- longContext: {
27
- thresholdTokens: 200000,
28
- rate: {
29
- input: 400,
30
- cacheRead: 40,
31
- cacheWrite: 400,
32
- cacheWrite5m: 400,
33
- cacheWrite1h: 400,
34
- output: 1800
35
- }
36
- }
37
- },
38
- "gemini-3-flash": {
39
- input: 50,
40
- cacheRead: 5,
41
- cacheWrite: 50,
42
- cacheWrite5m: 50,
43
- cacheWrite1h: 50,
44
- output: 300
45
- },
46
- "claude-sonnet-4-6": {
47
- input: 300,
48
- cacheRead: 30,
49
- cacheWrite: 375,
50
- cacheWrite5m: 375,
51
- cacheWrite1h: 600,
52
- output: 1500
53
- },
54
- "claude-opus-4-6": {
55
- input: 500,
56
- cacheRead: 50,
57
- cacheWrite: 625,
58
- cacheWrite5m: 625,
59
- cacheWrite1h: 1000,
60
- output: 2500
61
- }
62
- };
63
- const UNPRICED_MODELS = new Set([
64
- "gpt-oss-120b"
65
- ]);
66
10
  export class AntigravityUsageProvider extends UsageProviderBase {
67
11
  constructor(options = {}) {
68
12
  super("antigravity", "Antigravity");
@@ -104,11 +48,18 @@ export class AntigravityUsageProvider extends UsageProviderBase {
104
48
  if (duplicateEvents > 0) {
105
49
  warnings.push(`Collapsed ${duplicateEvents} duplicate Antigravity usage response(s).`);
106
50
  }
51
+ let pricing = new Map();
52
+ try {
53
+ pricing = await fetchModelPricing(selectedRecords.map((record) => normalizeAntigravityModelId(record.modelId)), "antigravity");
54
+ }
55
+ catch {
56
+ warnings.push("Model pricing API is unavailable.");
57
+ }
107
58
  const byModel = new Map();
108
59
  const byDay = createDailyUsageAggregates();
109
60
  for (const record of selectedRecords) {
110
61
  const modelId = normalizeAntigravityModelId(record.modelId);
111
- const totals = usageRecordToTotals(modelId, record);
62
+ const totals = usageRecordToTotals(modelId, record, pricing);
112
63
  addModelUsage(byModel, modelId, totals);
113
64
  addDailyUsage(byDay, record.timestamp, modelId, undefined, totals);
114
65
  }
@@ -120,12 +71,12 @@ export class AntigravityUsageProvider extends UsageProviderBase {
120
71
  .sort((left, right) => right.totals.estimatedCredits -
121
72
  left.totals.estimatedCredits);
122
73
  const unknownPricedModels = modelUsage
123
- .filter((row) => !rateForModel(row.modelId, rowInputTokens(row)) && !UNPRICED_MODELS.has(row.modelId))
74
+ .filter((row) => row.totals.estimatedCreditsStatus === "unavailable")
124
75
  .map((row) => row.modelId);
125
76
  if (unknownPricedModels.length > 0) {
126
- warnings.push(`No Antigravity estimated API-equivalent rate configured for: ${unknownPricedModels.join(", ")}.`);
77
+ warnings.push(`No complete Antigravity API-equivalent pricing returned for: ${unknownPricedModels.join(", ")}.`);
127
78
  }
128
- const limitWindows = quotaSnapshot?.entries.map((quota) => buildAntigravityLimitWindow(quota, quotaSnapshot.planType, selectedRecords, quotaSnapshot.fetchedAt)) ?? [];
79
+ const limitWindows = quotaSnapshot?.entries.map((quota) => buildAntigravityLimitWindow(quota, quotaSnapshot.planType, selectedRecords, quotaSnapshot.fetchedAt, pricing)) ?? [];
129
80
  return {
130
81
  providerId: this.id,
131
82
  providerLabel: this.label,
@@ -177,7 +128,7 @@ async function collectQuotaFromConnection(connect) {
177
128
  : null
178
129
  };
179
130
  }
180
- function buildAntigravityLimitWindow(quota, planType, records, fetchedAt) {
131
+ function buildAntigravityLimitWindow(quota, planType, records, fetchedAt, pricing) {
181
132
  const startAt = quota.resetAt - quota.windowMinutes * 60000;
182
133
  const byModel = new Map();
183
134
  const matchingTimestamps = [];
@@ -189,7 +140,7 @@ function buildAntigravityLimitWindow(quota, planType, records, fetchedAt) {
189
140
  continue;
190
141
  }
191
142
  matchingTimestamps.push(record.timestamp);
192
- addModelUsage(byModel, modelId, usageRecordToTotals(modelId, record));
143
+ addModelUsage(byModel, modelId, usageRecordToTotals(modelId, record, pricing));
193
144
  }
194
145
  const modelUsage = [...byModel.entries()]
195
146
  .map(([modelId, totals]) => ({
@@ -250,7 +201,8 @@ function deduplicateRecords(records) {
250
201
  function recordTokenTotal(record) {
251
202
  return record.input + record.cacheRead + record.cacheWrite + record.output;
252
203
  }
253
- function usageRecordToTotals(modelId, record) {
204
+ function usageRecordToTotals(modelId, record, pricing) {
205
+ const estimatedCredits = creditsFor(modelId, record, pricing);
254
206
  return {
255
207
  inputTokens: record.input,
256
208
  outputTokens: record.output,
@@ -263,7 +215,7 @@ function usageRecordToTotals(modelId, record) {
263
215
  record.cacheRead +
264
216
  record.cacheWrite +
265
217
  record.output,
266
- estimatedCredits: creditsFor(modelId, record),
218
+ estimatedCredits: estimatedCredits ?? 0,
267
219
  eventCount: 1,
268
220
  // The local RPC reports cache reads but never cache writes, so a zero cache
269
221
  // write is genuinely unknown (not a confirmed zero) and is surfaced as "-".
@@ -271,26 +223,17 @@ function usageRecordToTotals(modelId, record) {
271
223
  // which case it is both billed (see creditsFor) and shown as known.
272
224
  cacheReadStatus: "known",
273
225
  cacheWriteStatus: record.cacheWrite > 0 ? "known" : "unavailable",
274
- estimatedCreditsStatus: rateForModel(modelId, record.input)
275
- ? "known"
276
- : "unavailable"
226
+ estimatedCreditsStatus: estimatedCredits === undefined ? "unavailable" : "known"
277
227
  };
278
228
  }
279
- function creditsFor(modelId, record) {
280
- const rate = rateForModel(modelId, record.input);
281
- if (!rate) {
282
- return 0;
283
- }
284
- return ((record.input / 1000000) * rate.input +
285
- (record.cacheRead / 1000000) * rate.cacheRead +
286
- (record.cacheWrite / 1000000) * rate.cacheWrite +
287
- (record.output / 1000000) * rate.output);
288
- }
289
- function rateForModel(modelId, inputTokens) {
290
- return resolveUsageRate(RATE_CARD, modelId, inputTokens);
291
- }
292
- function rowInputTokens(row) {
293
- return row.totals.inputTokens + row.totals.cacheReadInputTokens + row.totals.cacheWriteInputTokens;
229
+ function creditsFor(modelId, record, pricing) {
230
+ return modelCostCredits(pricing.get(modelId), {
231
+ inputTokens: record.input,
232
+ outputTokens: record.output,
233
+ cacheReadInputTokens: record.cacheRead,
234
+ cacheWrite5mInputTokens: record.cacheWrite,
235
+ cacheWrite1hInputTokens: 0
236
+ });
294
237
  }
295
238
  function addModelUsage(byModel, modelId, deltaTotals) {
296
239
  const totals = byModel.get(modelId) ?? createEmptyUsageTotals();
@@ -8,28 +8,21 @@ import { promisify } from "node:util";
8
8
  import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "./contract.js";
9
9
  import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregates, numberOrZero } from "./limits.js";
10
10
  import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "./daily.js";
11
- import { resolveUsageRate } from "./pricing.js";
12
- // Source: https://platform.claude.com/docs/en/about-claude/pricing
13
- // (checked 2026-09-05).
14
- const RATE_CARD = {
15
- "claude-fable-5-1": { input: 10, cacheRead: 0.25, cacheWrite: 12.5, cacheWrite5m: 12.5, cacheWrite1h: 20, output: 50 },
16
- "claude-fable-5": { input: 10, cacheRead: 1, cacheWrite: 12.5, cacheWrite5m: 12.5, cacheWrite1h: 20, output: 50 },
17
- "claude-opus-5": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
18
- "claude-opus-4-8": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
19
- "claude-opus-4-7": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
20
- "claude-opus-4-6": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
21
- "claude-opus-4-5": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
22
- "claude-opus-4-1": { input: 15, cacheRead: 1.5, cacheWrite: 18.75, cacheWrite5m: 18.75, cacheWrite1h: 30, output: 75 },
23
- "claude-opus-4": { input: 15, cacheRead: 1.5, cacheWrite: 18.75, cacheWrite5m: 18.75, cacheWrite1h: 30, output: 75 },
24
- "claude-sonnet-5": { input: 2, cacheRead: 0.2, cacheWrite: 2.5, cacheWrite5m: 2.5, cacheWrite1h: 4, output: 10 },
25
- "claude-sonnet-4-6": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
26
- "claude-sonnet-4-5": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
27
- "claude-sonnet-4": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
28
- "claude-haiku-4-5": { input: 1, cacheRead: 0.1, cacheWrite: 1.25, cacheWrite5m: 1.25, cacheWrite1h: 2, output: 5 },
29
- "claude-haiku-3-5": { input: 0.8, cacheRead: 0.08, cacheWrite: 1, cacheWrite5m: 1, cacheWrite1h: 1.6, output: 4 }
30
- };
11
+ import { fetchModelPricing, modelCostCredits } from "./pricing.js";
12
+ /*
13
+ Previous local prices in USD per 1M tokens, kept temporarily as requested:
14
+ claude-fable-5-1 and claude-mythos-5-1 10 / 0.25 / 12.5 / 20 / 50
15
+ claude-fable-5 and claude-mythos-5 10 / 1 / 12.5 / 20 / 50
16
+ claude-opus-5, 4-8, 4-7, 4-6, 4-5 5 / 0.5 / 6.25 / 10 / 25
17
+ claude-opus-4-1 and 4 15 / 1.5 / 18.75 / 30 / 75
18
+ claude-sonnet-5 2 / 0.2 / 2.5 / 4 / 10
19
+ claude-sonnet-4-6, 4-5, 4 3 / 0.3 / 3.75 / 6 / 15
20
+ claude-haiku-4-5 1 / 0.1 / 1.25 / 2 / 5
21
+ claude-haiku-3-5 0.8 / 0.08 / 1 / 1.6 / 4
22
+ Columns: input / cache read / cache write 5m / cache write 1h / output.
23
+ */
31
24
  const execFileAsync = promisify(execFile);
32
- const USD_TO_CREDITS = 100;
25
+ const EMPTY_MODEL_PRICING = new Map();
33
26
  const VSCODE_CLAUDE_EXTENSION_PREFIX = "anthropic.claude-code-";
34
27
  const CLAUDE_SESSION_WINDOW_MINUTES = 5 * 60;
35
28
  const CLAUDE_WEEK_WINDOW_MINUTES = 7 * 24 * 60;
@@ -115,7 +108,17 @@ export class ClaudeUsageProvider extends UsageProviderBase {
115
108
  if (selectedEvents.length === 0 && parsedSessionFiles.length > 0) {
116
109
  traceClaude(options.traceLogger, "No assistant usage events were found in the parsed Claude session files.");
117
110
  }
111
+ let pricing = new Map();
112
+ try {
113
+ pricing = await fetchModelPricing(selectedEvents
114
+ .map((event) => event.modelId)
115
+ .filter((modelId) => !isInternalClaudeModel(modelId)), "claude_code");
116
+ }
117
+ catch {
118
+ warnings.push("Model pricing API is unavailable.");
119
+ }
118
120
  for (const event of selectedEvents) {
121
+ event.totals = usageToTotals(event.modelId, event.usage, event.timestampMs, pricing);
119
122
  addModelUsage(byModel, event.modelId, event.totals);
120
123
  const planType = typeof event.rateLimits?.plan_type === "string" ? event.rateLimits.plan_type : undefined;
121
124
  const safeEventTimeMs = Number.isFinite(event.timestampMs) ? event.timestampMs : 0;
@@ -136,10 +139,10 @@ export class ClaudeUsageProvider extends UsageProviderBase {
136
139
  .map(([modelId, totals]) => ({ modelId, totals }))
137
140
  .sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
138
141
  const unknownPricedModels = modelUsage
139
- .map((row) => row.modelId)
140
- .filter((modelId) => !resolveRate(modelId) && !isInternalClaudeModel(modelId));
142
+ .filter((row) => row.totals.estimatedCreditsStatus === "unavailable")
143
+ .map((row) => row.modelId);
141
144
  if (unknownPricedModels.length > 0) {
142
- warnings.push(`No API-equivalent rate configured for: ${unknownPricedModels.join(", ")}.`);
145
+ warnings.push(`No complete API-equivalent pricing returned for: ${unknownPricedModels.join(", ")}.`);
143
146
  }
144
147
  if (parsedSessionFiles.length === 0) {
145
148
  warnings.push(`No Claude session files found under ${sessionsRoot}.`);
@@ -216,17 +219,10 @@ function normalizeUsage(value) {
216
219
  webSearchRequests: numberOrZero(serverToolUse?.web_search_requests)
217
220
  };
218
221
  }
219
- function resolveRate(modelId, timestampMs) {
220
- return resolveUsageRate(RATE_CARD, modelId, 0, { prefixMatch: true, timestampMs });
221
- }
222
222
  function isInternalClaudeModel(modelId) {
223
223
  return modelId === "<synthetic>";
224
224
  }
225
- function creditsFor(modelId, usage, timestampMs) {
226
- const rate = resolveRate(modelId, timestampMs);
227
- if (!rate) {
228
- return 0;
229
- }
225
+ function creditsFor(modelId, usage, pricing) {
230
226
  const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
231
227
  // The US inference surcharge must match regardless of the casing the source
232
228
  // reports (e.g. "us", "US"), so compare case-insensitively.
@@ -234,22 +230,28 @@ function creditsFor(modelId, usage, timestampMs) {
234
230
  const speedMultiplier = usage.speed.trim().toLowerCase() === "fast" && isClaudeFastPricedModel(modelId)
235
231
  ? 2
236
232
  : 1;
237
- const tokenCostUsd = (usage.inputTokens / 1000000) * rate.input +
238
- (usage.cacheReadInputTokens / 1000000) * rate.cacheRead +
239
- (cacheWriteBreakdown.cacheWrite5mInputTokens / 1000000) * rate.cacheWrite5m +
240
- (cacheWriteBreakdown.cacheWrite1hInputTokens / 1000000) * rate.cacheWrite1h +
241
- (usage.outputTokens / 1000000) * rate.output;
242
- const webSearchCostUsd = usage.webSearchRequests * 0.01;
243
- return (tokenCostUsd * speedMultiplier * inferenceMultiplier + webSearchCostUsd) * USD_TO_CREDITS;
233
+ const tokenCostCredits = modelCostCredits(pricing.get(modelId), {
234
+ inputTokens: usage.inputTokens,
235
+ outputTokens: usage.outputTokens,
236
+ cacheReadInputTokens: usage.cacheReadInputTokens,
237
+ cacheWrite5mInputTokens: cacheWriteBreakdown.cacheWrite5mInputTokens,
238
+ cacheWrite1hInputTokens: cacheWriteBreakdown.cacheWrite1hInputTokens
239
+ });
240
+ if (tokenCostCredits === undefined) {
241
+ return undefined;
242
+ }
243
+ return (tokenCostCredits * speedMultiplier * inferenceMultiplier +
244
+ usage.webSearchRequests);
244
245
  }
245
246
  function isClaudeFastPricedModel(modelId) {
246
247
  return ["claude-opus-5", "claude-opus-4-8"].some((candidate) => modelId === candidate || modelId.startsWith(`${candidate}-`));
247
248
  }
248
- function usageToTotals(modelId, usage, timestampMs) {
249
+ function usageToTotals(modelId, usage, _timestampMs, pricing = EMPTY_MODEL_PRICING) {
249
250
  const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
250
251
  const cacheWriteInputTokens = cacheWriteBreakdown.cacheWrite5mInputTokens +
251
252
  cacheWriteBreakdown.cacheWrite1hInputTokens;
252
- const rateKnown = Boolean(resolveRate(modelId, timestampMs)) || isInternalClaudeModel(modelId);
253
+ const estimatedCredits = creditsFor(modelId, usage, pricing);
254
+ const rateKnown = estimatedCredits !== undefined || isInternalClaudeModel(modelId);
253
255
  return {
254
256
  inputTokens: usage.inputTokens,
255
257
  outputTokens: usage.outputTokens,
@@ -262,7 +264,7 @@ function usageToTotals(modelId, usage, timestampMs) {
262
264
  usage.cacheReadInputTokens +
263
265
  cacheWriteInputTokens +
264
266
  usage.outputTokens,
265
- estimatedCredits: creditsFor(modelId, usage, timestampMs),
267
+ estimatedCredits: estimatedCredits ?? 0,
266
268
  eventCount: 1,
267
269
  estimatedCreditsStatus: rateKnown ? "known" : "unavailable"
268
270
  };
@@ -6,95 +6,7 @@ import readline from "node:readline";
6
6
  import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "./contract.js";
7
7
  import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregates, numberOrZero } from "./limits.js";
8
8
  import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "./daily.js";
9
- import { resolveUsageRate } from "./pricing.js";
10
- // One credit equals $0.01 (see CODEX_CREDIT_COST_USD in index.tsx), so credits
11
- // equal USD * 100. Rate cards are expressed in the model's actual API price in
12
- // USD per 1M tokens and scaled to credits in creditsFor, matching the Claude
13
- // provider. These are the real OpenAI API prices, not the (4x cheaper) Codex
14
- // subscription credit prices.
15
- // Source: https://developers.openai.com/api/docs/pricing (checked 2026-09-05).
16
- // GPT-5.6 Sol's current promotional rate is guaranteed only through at least
17
- // 2026-11-21, so it must be rechecked after that date.
18
- const USD_TO_CREDITS = 100;
19
- const GPT_6_ASTRA_RATE = {
20
- input: 10,
21
- cacheRead: 1,
22
- cacheWrite: 12.5,
23
- cacheWrite5m: 12.5,
24
- cacheWrite1h: 12.5,
25
- output: 50,
26
- longContext: {
27
- thresholdTokens: 272000,
28
- rate: { input: 20, cacheRead: 2, cacheWrite: 25, cacheWrite5m: 25, cacheWrite1h: 25, output: 75 }
29
- }
30
- };
31
- const GPT_5_6_SOL_RATE = {
32
- input: 4,
33
- cacheRead: 0.4,
34
- cacheWrite: 5,
35
- cacheWrite5m: 5,
36
- cacheWrite1h: 5,
37
- output: 20,
38
- longContext: {
39
- thresholdTokens: 272000,
40
- rate: { input: 8, cacheRead: 0.8, cacheWrite: 10, cacheWrite5m: 10, cacheWrite1h: 10, output: 30 }
41
- }
42
- };
43
- const RATE_CARD = {
44
- "gpt-6-astra": GPT_6_ASTRA_RATE,
45
- "gpt-5.6-sol": GPT_5_6_SOL_RATE,
46
- "gpt-5.6-terra": {
47
- input: 2,
48
- cacheRead: 0.2,
49
- cacheWrite: 2.5,
50
- cacheWrite5m: 2.5,
51
- cacheWrite1h: 2.5,
52
- output: 12,
53
- longContext: {
54
- thresholdTokens: 272000,
55
- rate: { input: 4, cacheRead: 0.4, cacheWrite: 5, cacheWrite5m: 5, cacheWrite1h: 5, output: 18 }
56
- }
57
- },
58
- "gpt-5.6-luna": {
59
- input: 0.2,
60
- cacheRead: 0.02,
61
- cacheWrite: 0.25,
62
- cacheWrite5m: 0.25,
63
- cacheWrite1h: 0.25,
64
- output: 1.2,
65
- longContext: {
66
- thresholdTokens: 272000,
67
- rate: { input: 0.4, cacheRead: 0.04, cacheWrite: 0.5, cacheWrite5m: 0.5, cacheWrite1h: 0.5, output: 1.8 }
68
- }
69
- },
70
- "gpt-5.5": {
71
- input: 5,
72
- cacheRead: 0.5,
73
- cacheWrite: 0,
74
- cacheWrite5m: 0,
75
- cacheWrite1h: 0,
76
- output: 30,
77
- longContext: {
78
- thresholdTokens: 272000,
79
- rate: { input: 10, cacheRead: 1, cacheWrite: 0, cacheWrite5m: 0, cacheWrite1h: 0, output: 45 }
80
- }
81
- },
82
- "gpt-5.4": {
83
- input: 2.5,
84
- cacheRead: 0.25,
85
- cacheWrite: 0,
86
- cacheWrite5m: 0,
87
- cacheWrite1h: 0,
88
- output: 15,
89
- longContext: {
90
- thresholdTokens: 272000,
91
- rate: { input: 5, cacheRead: 0.5, cacheWrite: 0, cacheWrite5m: 0, cacheWrite1h: 0, output: 22.5 }
92
- }
93
- },
94
- "gpt-5.4-mini": { input: 0.75, cacheRead: 0.075, cacheWrite: 0, cacheWrite5m: 0, cacheWrite1h: 0, output: 4.5 },
95
- "gpt-5.3-codex": { input: 1.75, cacheRead: 0.175, cacheWrite: 0, cacheWrite5m: 0, cacheWrite1h: 0, output: 14 },
96
- "gpt-5.2": { input: 1.75, cacheRead: 0.175, cacheWrite: 0, cacheWrite5m: 0, cacheWrite1h: 0, output: 14 }
97
- };
9
+ import { fetchModelPricing, modelCostCredits } from "./pricing.js";
98
10
  export class CodexUsageProvider extends UsageProviderBase {
99
11
  constructor(options = {}) {
100
12
  super("codex", "Codex");
@@ -112,6 +24,7 @@ export class CodexUsageProvider extends UsageProviderBase {
112
24
  const byDay = createDailyUsageAggregates();
113
25
  const windows = createLimitWindowAggregates();
114
26
  const planTypes = new Set();
27
+ const events = [];
115
28
  const warnings = [];
116
29
  const parseTotals = {
117
30
  filesScanned: 0,
@@ -130,12 +43,35 @@ export class CodexUsageProvider extends UsageProviderBase {
130
43
  }
131
44
  seenSessionFiles.add(sessionFileId);
132
45
  parseTotals.filesScanned += 1;
133
- const fileStats = await parseSessionFile(file, byModel, byDay, windows, planTypes, knownModels);
46
+ const fileStats = await parseSessionFile(file, events);
134
47
  parseTotals.linesRead += fileStats.linesRead;
135
48
  parseTotals.tokenEvents += fileStats.tokenEvents;
136
49
  parseTotals.malformedLines += fileStats.malformedLines;
137
50
  }
138
51
  }
52
+ let pricing = new Map();
53
+ try {
54
+ pricing = await fetchModelPricing(events
55
+ .map((event) => pricingModelId(event.modelId))
56
+ .filter((modelId) => !isAssumedZeroRatedCodexModel(modelId, knownModels)), "codex");
57
+ }
58
+ catch {
59
+ warnings.push("Model pricing API is unavailable.");
60
+ }
61
+ for (const event of events) {
62
+ const deltaTotals = createUsageTotalsForModel(event.modelId, event.usage, knownModels, pricing, event.serviceTier);
63
+ deltaTotals.estimatedCredits += event.webSearchCalls;
64
+ if (!hasCountedRawUsage(event.usage) && event.webSearchCalls > 0) {
65
+ deltaTotals.estimatedCreditsStatus = "known";
66
+ }
67
+ const planType = typeof event.rateLimits?.plan_type === "string"
68
+ ? event.rateLimits.plan_type
69
+ : undefined;
70
+ const safeEventTimeMs = Number.isFinite(event.eventTimeMs) ? event.eventTimeMs : 0;
71
+ addModelUsage(byModel, event.modelId, deltaTotals);
72
+ addDailyUsage(byDay, event.eventTimeMs, event.modelId, planType, deltaTotals);
73
+ applyRateLimits(windows, event.rateLimits, safeEventTimeMs, event.modelId, deltaTotals, planTypes);
74
+ }
139
75
  if (parseTotals.malformedLines > 0) {
140
76
  warnings.push(`Skipped ${parseTotals.malformedLines} malformed JSONL line(s).`);
141
77
  }
@@ -144,10 +80,10 @@ export class CodexUsageProvider extends UsageProviderBase {
144
80
  .sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
145
81
  const unknownPricedModels = modelUsage
146
82
  .filter((row) => row.totals.totalTokens > 0)
147
- .map((row) => row.modelId)
148
- .filter((modelId) => !rateForCodexModel(modelId) && !isAssumedZeroRatedCodexModel(modelId, knownModels));
83
+ .filter((row) => row.totals.estimatedCreditsStatus === "unavailable")
84
+ .map((row) => row.modelId);
149
85
  if (unknownPricedModels.length > 0) {
150
- warnings.push(`No API-equivalent rate configured for: ${unknownPricedModels.join(", ")}.`);
86
+ warnings.push(`No complete API-equivalent pricing returned for: ${unknownPricedModels.join(", ")}.`);
151
87
  }
152
88
  if (parseTotals.filesScanned === 0) {
153
89
  warnings.push(`No Codex session files found under ${codexRoot}.`);
@@ -319,18 +255,18 @@ function normalizeRawUsage(value) {
319
255
  totalTokens: numberOrZero(usage.total_tokens)
320
256
  };
321
257
  }
322
- function creditsFor(modelId, usage, serviceTier) {
323
- const rate = rateForCodexModel(modelId, usage.inputTokens);
324
- if (!rate) {
325
- return 0;
326
- }
258
+ function creditsFor(modelId, usage, pricing, serviceTier) {
327
259
  const { inputTokens, cacheReadInputTokens, cacheWriteInputTokens } = resolveCodexInputBreakdown(usage);
328
- return (((inputTokens / 1000000) * rate.input +
329
- (cacheReadInputTokens / 1000000) * rate.cacheRead +
330
- (cacheWriteInputTokens / 1000000) * rate.cacheWrite +
331
- (usage.outputTokens / 1000000) * rate.output) *
332
- serviceTierPriceMultiplier(serviceTier) *
333
- USD_TO_CREDITS);
260
+ const credits = modelCostCredits(pricing.get(pricingModelId(modelId)), {
261
+ inputTokens,
262
+ outputTokens: usage.outputTokens,
263
+ cacheReadInputTokens,
264
+ cacheWrite5mInputTokens: cacheWriteInputTokens,
265
+ cacheWrite1hInputTokens: 0
266
+ });
267
+ return credits === undefined
268
+ ? undefined
269
+ : credits * serviceTierPriceMultiplier(serviceTier);
334
270
  }
335
271
  function serviceTierPriceMultiplier(serviceTier) {
336
272
  switch (serviceTier?.trim().toLowerCase()) {
@@ -346,15 +282,12 @@ function serviceTierPriceMultiplier(serviceTier) {
346
282
  function isSupportedServiceTier(serviceTier) {
347
283
  return ["default", "priority", "fast", "flex"].includes(serviceTier?.trim().toLowerCase() ?? "");
348
284
  }
349
- function rateForCodexModel(modelId, inputTokens = 0) {
350
- // The unsuffixed API alias routes to Sol. Normalize only the exact alias so
351
- // an unknown future gpt-5.6-* tier is not accidentally charged at Sol rates.
352
- const pricedModelId = modelId === "gpt-5.6"
285
+ function pricingModelId(modelId) {
286
+ return (modelId === "gpt-5.6"
353
287
  ? "gpt-5.6-sol"
354
288
  : modelId === "codex-auto-review"
355
289
  ? "gpt-5.4"
356
- : modelId;
357
- return resolveUsageRate(RATE_CARD, pricedModelId, inputTokens, { prefixMatch: true });
290
+ : modelId);
358
291
  }
359
292
  function rawUsageToTotals(usage) {
360
293
  const { inputTokens, cacheReadInputTokens, cacheWriteInputTokens } = resolveCodexInputBreakdown(usage);
@@ -381,12 +314,13 @@ function resolveCodexInputBreakdown(usage) {
381
314
  cacheWriteInputTokens
382
315
  };
383
316
  }
384
- function createUsageTotalsForModel(modelId, usage, knownModels, serviceTier) {
317
+ function createUsageTotalsForModel(modelId, usage, knownModels, pricing, serviceTier) {
385
318
  const resolvedModelId = modelId || "unknown";
386
319
  const deltaTotals = rawUsageToTotals(usage);
387
- deltaTotals.estimatedCredits = creditsFor(resolvedModelId, usage, serviceTier);
320
+ const estimatedCredits = creditsFor(resolvedModelId, usage, pricing, serviceTier);
321
+ deltaTotals.estimatedCredits = estimatedCredits ?? 0;
388
322
  deltaTotals.eventCount = 1;
389
- if (!rateForCodexModel(resolvedModelId, usage.inputTokens) && !isAssumedZeroRatedCodexModel(resolvedModelId, knownModels)) {
323
+ if (estimatedCredits === undefined && !isAssumedZeroRatedCodexModel(resolvedModelId, knownModels)) {
390
324
  deltaTotals.estimatedCreditsStatus = "unavailable";
391
325
  }
392
326
  return deltaTotals;
@@ -427,7 +361,7 @@ async function* walkSessionFiles(directory) {
427
361
  }
428
362
  }
429
363
  }
430
- async function parseSessionFile(filePath, byModel, byDay, windows, planTypes, knownModels) {
364
+ async function parseSessionFile(filePath, events) {
431
365
  const stream = fs.createReadStream(filePath, { encoding: "utf8" });
432
366
  const lineReader = readline.createInterface({ input: stream, crlfDelay: Infinity });
433
367
  let currentModel = "unknown";
@@ -442,7 +376,7 @@ async function parseSessionFile(filePath, byModel, byDay, windows, planTypes, kn
442
376
  let pendingWebSearchCalls = 0;
443
377
  let lastSeenTimestampMs = 0;
444
378
  const recordUsage = (modelId, usage, eventTimeMs, rateLimits = null, serviceTier = currentServiceTier) => {
445
- const webSearchCostCredits = pendingWebSearchCalls;
379
+ const webSearchCalls = pendingWebSearchCalls;
446
380
  pendingWebSearchCalls = 0;
447
381
  // Current-format-only policy: without the applied tier the exact cost is
448
382
  // unknowable. Silently ignore the event instead of guessing Standard or
@@ -450,24 +384,19 @@ async function parseSessionFile(filePath, byModel, byDay, windows, planTypes, kn
450
384
  if (!isSupportedServiceTier(serviceTier)) {
451
385
  return;
452
386
  }
453
- if (!hasCountedRawUsage(usage) && webSearchCostCredits === 0) {
387
+ if (!hasCountedRawUsage(usage) && webSearchCalls === 0) {
454
388
  return;
455
389
  }
456
390
  const resolvedModelId = modelId || "unknown";
457
- const deltaTotals = createUsageTotalsForModel(resolvedModelId, usage, knownModels, serviceTier);
458
- // OpenAI API web search is $10 / 1k calls: $0.01, or one dashboard
459
- // credit, per completed call. Search-content tokens are already present in
460
- // the model usage record and are priced normally above.
461
- deltaTotals.estimatedCredits += webSearchCostCredits;
462
- if (!hasCountedRawUsage(usage) && webSearchCostCredits > 0) {
463
- deltaTotals.estimatedCreditsStatus = "known";
464
- }
465
- const planType = typeof rateLimits?.plan_type === "string" ? rateLimits.plan_type : undefined;
466
- const safeEventTimeMs = Number.isFinite(eventTimeMs) ? eventTimeMs : 0;
467
391
  tokenEvents += 1;
468
- addModelUsage(byModel, resolvedModelId, deltaTotals);
469
- addDailyUsage(byDay, eventTimeMs, resolvedModelId, planType, deltaTotals);
470
- applyRateLimits(windows, rateLimits, safeEventTimeMs, resolvedModelId, deltaTotals, planTypes);
392
+ events.push({
393
+ modelId: resolvedModelId,
394
+ usage,
395
+ eventTimeMs,
396
+ rateLimits,
397
+ serviceTier,
398
+ webSearchCalls
399
+ });
471
400
  };
472
401
  const flushPendingUsageRecord = () => {
473
402
  if (!pendingUsageRecord) {
@@ -527,6 +456,12 @@ async function parseSessionFile(filePath, byModel, byDay, windows, planTypes, kn
527
456
  if (payloadObject.type === "event_msg") {
528
457
  const payload = asRecord(payloadObject.payload);
529
458
  if (payload?.type === "thread_settings_applied") {
459
+ // Compacted/background sessions can begin with a cumulative usage
460
+ // snapshot before their first model and tier settings. Flush it while
461
+ // the captured settings are still incomplete so the current-contract
462
+ // guard below ignores it instead of attributing it to "unknown" with
463
+ // settings that arrived later.
464
+ flushPendingUsageRecord();
530
465
  const threadSettings = asRecord(payload.thread_settings);
531
466
  if (typeof threadSettings?.model === "string" && threadSettings.model.trim()) {
532
467
  currentModel = threadSettings.model;
@@ -1,56 +1,35 @@
1
- import { resolveUsageRate } from "../pricing.js";
2
- /**
3
- * Copilot-specific estimated API-equivalent rate card (micro-credits per
4
- * million tokens). This is intentionally separate from the Codex and
5
- * Antigravity rate cards Copilot bills the same model families at different
6
- * effective rates, so there is no single shared source of truth to reuse.
7
- */
8
- export const RATE_CARD = {
9
- "gpt-5-mini": { input: 25, cacheRead: 2.5, cacheWrite: 25, cacheWrite5m: 25, cacheWrite1h: 25, output: 200 },
10
- "gpt-5.3-codex": { input: 175, cacheRead: 17.5, cacheWrite: 175, cacheWrite5m: 175, cacheWrite1h: 175, output: 1400 },
11
- "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 } } },
12
- "gpt-5.4-mini": { input: 75, cacheRead: 7.5, cacheWrite: 75, cacheWrite5m: 75, cacheWrite1h: 75, output: 450 },
13
- "gpt-5.4-nano": { input: 20, cacheRead: 2, cacheWrite: 20, cacheWrite5m: 20, cacheWrite1h: 20, output: 125 },
14
- "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 } } },
15
- "claude-haiku-4-5": { input: 100, cacheRead: 10, cacheWrite: 125, cacheWrite5m: 125, cacheWrite1h: 200, output: 500 },
16
- "claude-sonnet-4-5": { input: 300, cacheRead: 30, cacheWrite: 375, cacheWrite5m: 375, cacheWrite1h: 600, output: 1500 },
17
- "claude-sonnet-4-6": { input: 300, cacheRead: 30, cacheWrite: 375, cacheWrite5m: 375, cacheWrite1h: 600, output: 1500 },
18
- "claude-opus-4-5": { input: 500, cacheRead: 50, cacheWrite: 625, cacheWrite5m: 625, cacheWrite1h: 1000, output: 2500 },
19
- "claude-opus-4-6": { input: 500, cacheRead: 50, cacheWrite: 625, cacheWrite5m: 625, cacheWrite1h: 1000, output: 2500 },
20
- "claude-opus-4-7": { input: 500, cacheRead: 50, cacheWrite: 625, cacheWrite5m: 625, cacheWrite1h: 1000, output: 2500 },
21
- "claude-opus-4-8": { input: 500, cacheRead: 50, cacheWrite: 625, cacheWrite5m: 625, cacheWrite1h: 1000, output: 2500 },
22
- "claude-opus-5": { input: 500, cacheRead: 50, cacheWrite: 625, cacheWrite5m: 625, cacheWrite1h: 1000, output: 2500 },
23
- "claude-fable-5": { input: 1000, cacheRead: 100, cacheWrite: 1250, cacheWrite5m: 1250, cacheWrite1h: 2000, output: 5000 },
24
- "gemini-2.5-pro": { input: 125, cacheRead: 12.5, cacheWrite: 125, cacheWrite5m: 125, cacheWrite1h: 125, output: 1000 },
25
- "gemini-3-flash": { input: 50, cacheRead: 5, cacheWrite: 50, cacheWrite5m: 50, cacheWrite1h: 50, output: 300 },
26
- "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 } } },
27
- "gemini-3.5-flash": { input: 150, cacheRead: 15, cacheWrite: 150, cacheWrite5m: 150, cacheWrite1h: 150, output: 900 },
28
- "mai-code-1-flash": { input: 75, cacheRead: 7.5, cacheWrite: 75, cacheWrite5m: 75, cacheWrite1h: 75, output: 450 },
29
- "raptor-mini": { input: 25, cacheRead: 2.5, cacheWrite: 25, cacheWrite5m: 25, cacheWrite1h: 25, output: 200 }
30
- };
31
- /**
32
- * Model id prefixes that Copilot does not bill (inline completions / next-edit
33
- * suggestions). These are zero-rated rather than "unknown" so they never turn
34
- * aggregate credit totals unknown.
35
- */
1
+ /*
2
+ Previous local prices in credits per 1M tokens, kept temporarily as requested:
3
+ gpt-5-mini 25 / 2.5 / 0 / 200; gpt-5.3-codex 175 / 17.5 / 0 / 1400
4
+ gpt-5.4 250 / 25 / 0 / 1500; gpt-5.4-mini 75 / 7.5 / 0 / 450
5
+ gpt-5.4-nano 20 / 2 / 0 / 125; gpt-5.5 500 / 50 / 0 / 3000
6
+ gpt-5.6-luna 20 / 2 / 25 / 120; gpt-5.6-sol 400 / 40 / 500 / 2000
7
+ gpt-5.6-terra 200 / 20 / 250 / 1200; gpt-6-astra 1000 / 100 / 1250 / 5000
8
+ claude-haiku-4-5 100 / 10 / 125 / 200 / 500
9
+ claude-sonnet-4-5 and 4-6 300 / 30 / 375 / 600 / 1500
10
+ claude-opus-4-5, 4-6, 4-7, 4-8, 5 500 / 50 / 625 / 1000 / 2500
11
+ claude-opus-4-8-fast 1000 / 100 / 1250 / 2000 / 5000
12
+ claude-fable-5 1000 / 100 / 1250 / 2000 / 5000
13
+ claude-fable-5-1 1000 / 25 / 1250 / 2000 / 5000
14
+ claude-sonnet-5 200 / 20 / 250 / 400 / 1000
15
+ gemini-2.5-pro 125 / 12.5 / 0 / 1000; gemini-3-flash 50 / 5 / 0 / 300
16
+ gemini-3.1-pro 200 / 20 / 0 / 1200; gemini-3.5-flash 150 / 15 / 0 / 900
17
+ gemini-3.6-flash, 3.7-flash, 3.8-flash 75 / 7.5 / 0 / 375
18
+ mai-code-1-flash 75 / 7.5 / 0 / 450; mai-code-1.1-flash 20 / 2 / 0 / 120
19
+ grok-4.5 and 4.6 200 / 50 / 0 / 600
20
+ kimi-k2.7-code 95 / 19 / 0 / 400; kimi-k3 300 / 30 / 0 / 1500
21
+ raptor-mini 25 / 2.5 / 0 / 200
22
+ Columns with four values: input / cache read / cache write / output.
23
+ Columns with five values: input / cache read / cache write 5m / cache write 1h / output.
24
+ */
36
25
  export const NON_BILLABLE_MODEL_PREFIXES = [
37
26
  "copilot-nes",
38
27
  "copilot-suggestion",
39
28
  "copilot-suggestions"
40
29
  ];
41
- /**
42
- * Canonicalize a Copilot model id. The exporter already emits stable,
43
- * human-readable ids (including dated suffixes like `gpt-5.4-2026-03-01`), and
44
- * the dashboard surfaces those verbatim, so this only guards the empty case.
45
- * Prefix-based rate resolution (see {@link rateForCopilotModel}) handles dated
46
- * suffixes without collapsing the displayed id.
47
- */
48
30
  export function normalizeCopilotModelId(modelId) {
49
31
  return modelId || "unknown";
50
32
  }
51
- export function rateForCopilotModel(modelId, inputTokens) {
52
- return resolveUsageRate(RATE_CARD, modelId, inputTokens, { prefixMatch: true });
53
- }
54
33
  export function isNonBillableCopilotModel(modelId) {
55
34
  return NON_BILLABLE_MODEL_PREFIXES.some((prefix) => modelId === prefix || modelId.startsWith(`${prefix}-`));
56
35
  }
@@ -6,6 +6,8 @@ import { discoverCopilotOtelFiles } from "./otel/discover.js";
6
6
  import { parseCopilotOtelFiles } from "./otel/parse.js";
7
7
  import { getCopilotUserInfo, subtractOneUtcCalendarMonth } from "./quota.js";
8
8
  import { aggregateCopilotUsage, filterCopilotUsageEvents } from "./usage/aggregate.js";
9
+ import { isNonBillableCopilotModel } from "./models.js";
10
+ import { fetchModelPricing } from "../pricing.js";
9
11
  // The token-metered bucket that maps to the "AI Credits" window.
10
12
  const AI_CREDITS_QUOTA_ID = "premium_interactions";
11
13
  export { configureCopilotVsCodeLogging, getCopilotCliOtelEnv };
@@ -48,11 +50,12 @@ export class CopilotUsageProvider extends UsageProviderBase {
48
50
  linesRead: 0,
49
51
  events: [],
50
52
  aggregated: aggregateCopilotUsage([]),
53
+ pricing: new Map(),
51
54
  warnings: ["Copilot OTEL usage is unavailable."]
52
55
  };
53
56
  warnings.push(...usage.warnings);
54
57
  const { windows, unknownLabels, windowWarnings } = quotaInfo
55
- ? buildLimitWindows(quotaInfo, usage.events)
58
+ ? buildLimitWindows(quotaInfo, usage.events, usage.pricing)
56
59
  : { windows: [], unknownLabels: [], windowWarnings: [] };
57
60
  if (unknownLabels.length > 0) {
58
61
  warnings.push(`Copilot quota usage is unknown for: ${unknownLabels.join(", ")}.`);
@@ -83,8 +86,25 @@ export class CopilotUsageProvider extends UsageProviderBase {
83
86
  async loadUsage() {
84
87
  const discovery = await discoverCopilotOtelFiles({ root: this.root, env: this.env });
85
88
  const parsed = await parseCopilotOtelFiles(discovery.files);
86
- const aggregated = aggregateCopilotUsage(parsed.events);
87
89
  const warnings = [...discovery.warnings, ...parsed.warnings];
90
+ let pricing = new Map();
91
+ try {
92
+ pricing = await fetchModelPricing(parsed.events
93
+ .map((event) => event.modelId)
94
+ .filter((modelId) => !isNonBillableCopilotModel(modelId)), "github_copilot");
95
+ }
96
+ catch {
97
+ warnings.push("Model pricing API is unavailable.");
98
+ }
99
+ const aggregated = aggregateCopilotUsage(parsed.events, pricing);
100
+ const unpricedModels = [
101
+ ...new Set(parsed.events
102
+ .map((event) => event.modelId)
103
+ .filter((modelId) => !isNonBillableCopilotModel(modelId) && !pricing.has(modelId)))
104
+ ];
105
+ if (unpricedModels.length > 0) {
106
+ warnings.push(`No Copilot API-equivalent pricing returned for: ${unpricedModels.join(", ")}.`);
107
+ }
88
108
  if (parsed.malformedLines > 0) {
89
109
  warnings.push(`Skipped ${parsed.malformedLines} malformed Copilot JSONL line(s).`);
90
110
  }
@@ -99,6 +119,7 @@ export class CopilotUsageProvider extends UsageProviderBase {
99
119
  linesRead: parsed.linesRead,
100
120
  events: parsed.events,
101
121
  aggregated,
122
+ pricing,
102
123
  warnings
103
124
  };
104
125
  }
@@ -120,7 +141,7 @@ async function describeMissingOtelFile(root) {
120
141
  ? `VS Code Copilot logging is enabled, but ${missing.path} has not been created yet. Reload VS Code and send a Copilot Chat request.`
121
142
  : undefined;
122
143
  }
123
- function buildLimitWindows(quotaInfo, events) {
144
+ function buildLimitWindows(quotaInfo, events, pricing) {
124
145
  const planType = quotaInfo.plan ?? "unknown";
125
146
  const billing = deriveBillingWindow(quotaInfo.resetAt);
126
147
  const windows = [];
@@ -152,7 +173,7 @@ function buildLimitWindows(quotaInfo, events) {
152
173
  // local OTEL token usage that falls inside this billing window.
153
174
  if (isAiCredits && billing) {
154
175
  const windowEvents = filterCopilotUsageEvents(events, billing.startMs, billing.endMs);
155
- const windowUsage = aggregateCopilotUsage(windowEvents);
176
+ const windowUsage = aggregateCopilotUsage(windowEvents, pricing);
156
177
  totals = windowUsage.summaryTotals;
157
178
  modelUsage = windowUsage.modelUsage;
158
179
  eventCount = windowUsage.tokenEvents;
@@ -1,6 +1,7 @@
1
1
  import { addUsageTotals, sumUsageTotals } from "../../contract.js";
2
2
  import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "../../daily.js";
3
- import { isNonBillableCopilotModel, normalizeCopilotModelId, rateForCopilotModel } from "../models.js";
3
+ import { isNonBillableCopilotModel, normalizeCopilotModelId } from "../models.js";
4
+ import { modelCostCredits } from "../../pricing.js";
4
5
  /**
5
6
  * Select events whose timestamp falls in the half-open interval
6
7
  * `[startTimeMs, endTimeMs)`. An event exactly at `endTimeMs` belongs to the
@@ -16,7 +17,7 @@ export function filterCopilotUsageEvents(events, startTimeMs, endTimeMs) {
16
17
  * reported input already INCLUDES cache-read tokens but NOT cache-write tokens.
17
18
  * Pure and deterministic: independent of input ordering.
18
19
  */
19
- export function aggregateCopilotUsage(events) {
20
+ export function aggregateCopilotUsage(events, pricing = new Map()) {
20
21
  const byModel = new Map();
21
22
  const byDay = createDailyUsageAggregates();
22
23
  for (const event of events) {
@@ -35,15 +36,17 @@ export function aggregateCopilotUsage(events) {
35
36
  const output = event.outputTokens;
36
37
  const reasoning = Math.min(event.reasoningOutputTokens, output);
37
38
  const nonBillable = isNonBillableCopilotModel(modelId);
38
- const rate = nonBillable ? undefined : rateForCopilotModel(modelId, event.inputTokens);
39
- const creditsKnown = nonBillable || (hasCacheInfo && rate !== undefined);
39
+ const estimatedCredits = hasCacheInfo
40
+ ? modelCostCredits(pricing.get(modelId), {
41
+ inputTokens: uncachedInput,
42
+ outputTokens: output,
43
+ cacheReadInputTokens: cacheRead,
44
+ cacheWrite5mInputTokens: cacheWrite,
45
+ cacheWrite1hInputTokens: 0
46
+ })
47
+ : undefined;
48
+ const creditsKnown = nonBillable || estimatedCredits !== undefined;
40
49
  const estimatedCreditsStatus = creditsKnown ? "known" : "unavailable";
41
- const estimatedCredits = rate !== undefined && hasCacheInfo
42
- ? (uncachedInput / 1000000) * rate.input +
43
- (cacheRead / 1000000) * rate.cacheRead +
44
- (cacheWrite / 1000000) * rate.cacheWrite +
45
- (output / 1000000) * rate.output
46
- : 0;
47
50
  const totals = {
48
51
  inputTokens: uncachedInput,
49
52
  outputTokens: output,
@@ -53,7 +56,7 @@ export function aggregateCopilotUsage(events) {
53
56
  cacheWrite1hInputTokens: 0,
54
57
  reasoningOutputTokens: reasoning,
55
58
  totalTokens: uncachedInput + cacheRead + cacheWrite + output,
56
- estimatedCredits,
59
+ estimatedCredits: nonBillable ? 0 : (estimatedCredits ?? 0),
57
60
  eventCount: 1,
58
61
  cacheReadStatus: event.cacheReadStatus,
59
62
  cacheWriteStatus: event.cacheWriteStatus,
@@ -1,23 +1,123 @@
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) {
1
+ import { request as httpRequest } from "node:http";
2
+ import { request as httpsRequest } from "node:https";
3
+ const MODEL_PRICING_ENDPOINT = process.env.LETMECODE_MODEL_PRICING_ENDPOINT ??
4
+ "https://devforth.io/admin/adminapi/v1/get_model_pricing";
5
+ const PRICE_CACHE_TTL_MS = 5 * 60000;
6
+ const USD_TO_CREDITS = 100;
7
+ const MODEL_SLUG_ALIASES = {
8
+ "claude-haiku-4-5": "claude-4-5-haiku-reasoning",
9
+ "claude-sonnet-4-5": "claude-4-5-sonnet-thinking",
10
+ "gemini-3-1-pro": "gemini-3-1-pro-preview"
11
+ };
12
+ const responseCache = new Map();
13
+ let pricingTransport = postModelPricingRequest;
14
+ export function configureModelPricingTransport(transport) {
15
+ pricingTransport = transport;
16
+ responseCache.clear();
17
+ }
18
+ export function modelPricingSlug(modelId) {
19
+ const slug = modelId
20
+ .trim()
21
+ .toLowerCase()
22
+ .replace(/[._\s]+/g, "-")
23
+ .replace(/-+/g, "-")
24
+ .replace(/-(?:\d{8}|\d{4}-\d{2}-\d{2})$/, "");
25
+ return MODEL_SLUG_ALIASES[slug] ?? slug;
26
+ }
27
+ export async function fetchModelPricing(modelIds, source) {
28
+ const slugByModelId = new Map([...new Set(modelIds)]
29
+ .filter((modelId) => modelId !== "unknown" && modelId !== "<synthetic>")
30
+ .map((modelId) => [modelId, modelPricingSlug(modelId)]));
31
+ const slugs = [...new Set(slugByModelId.values())].sort();
32
+ if (slugs.length === 0) {
33
+ return new Map();
34
+ }
35
+ const request = {
36
+ slugs,
37
+ available_in: { main: [source], other: [] }
38
+ };
39
+ const response = await fetchCached(request);
40
+ const pricingBySlug = new Map(response.models.map((model) => [
41
+ model.slug,
42
+ {
43
+ input: model.input,
44
+ output: model.output,
45
+ inputCacheRead: model.input_cache_read,
46
+ inputCacheWrite5m: model.input_cache_w5m,
47
+ inputCacheWrite1h: model.input_cache_w1h
48
+ }
49
+ ]));
50
+ return new Map([...slugByModelId.entries()].flatMap(([modelId, slug]) => {
51
+ const pricing = pricingBySlug.get(slug);
52
+ return pricing ? [[modelId, pricing]] : [];
53
+ }));
54
+ }
55
+ export function modelCostCredits(pricing, usage) {
56
+ if (!pricing) {
8
57
  return undefined;
9
58
  }
10
- const rate = rateCard[model];
11
- if (!rate) {
59
+ if (usage.cacheWrite5mInputTokens > 0 && pricing.inputCacheWrite5m === null) {
12
60
  return undefined;
13
61
  }
14
- if (rate.longContext && inputTokens > rate.longContext.thresholdTokens) {
15
- return rate.longContext.rate;
62
+ if (usage.cacheWrite1hInputTokens > 0 && pricing.inputCacheWrite1h === null) {
63
+ return undefined;
16
64
  }
17
- if (rate.introOffer &&
18
- Number.isFinite(options.timestampMs) &&
19
- options.timestampMs < rate.introOffer.effectiveUntilMs) {
20
- return rate.introOffer.rate;
65
+ return ((usage.inputTokens / 1000000) * pricing.input +
66
+ (usage.cacheReadInputTokens / 1000000) * pricing.inputCacheRead +
67
+ (usage.cacheWrite5mInputTokens / 1000000) * (pricing.inputCacheWrite5m ?? 0) +
68
+ (usage.cacheWrite1hInputTokens / 1000000) * (pricing.inputCacheWrite1h ?? 0) +
69
+ (usage.outputTokens / 1000000) * pricing.output) * USD_TO_CREDITS;
70
+ }
71
+ async function fetchCached(request) {
72
+ const key = JSON.stringify(request);
73
+ const cached = responseCache.get(key);
74
+ if (cached && cached.expiresAt > Date.now()) {
75
+ return cached.response;
76
+ }
77
+ const response = pricingTransport(request);
78
+ responseCache.set(key, {
79
+ expiresAt: Date.now() + PRICE_CACHE_TTL_MS,
80
+ response
81
+ });
82
+ try {
83
+ return await response;
21
84
  }
22
- return rate;
85
+ catch (error) {
86
+ responseCache.delete(key);
87
+ throw error;
88
+ }
89
+ }
90
+ async function postModelPricingRequest(body) {
91
+ return new Promise((resolve, reject) => {
92
+ const encodedBody = Buffer.from(JSON.stringify(body), "utf8");
93
+ const target = new URL(MODEL_PRICING_ENDPOINT);
94
+ const request = target.protocol === "http:" ? httpRequest : httpsRequest;
95
+ const req = request({
96
+ method: "POST",
97
+ protocol: target.protocol,
98
+ hostname: target.hostname,
99
+ port: target.port,
100
+ path: `${target.pathname}${target.search}`,
101
+ headers: {
102
+ "content-type": "application/json",
103
+ "content-length": encodedBody.byteLength
104
+ }
105
+ }, (res) => {
106
+ const chunks = [];
107
+ res.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
108
+ res.on("end", () => {
109
+ if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
110
+ reject(new Error(`Model pricing request failed with status ${res.statusCode ?? "unknown"}`));
111
+ return;
112
+ }
113
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
114
+ });
115
+ });
116
+ req.on("error", reject);
117
+ req.setTimeout(5000, () => {
118
+ req.destroy(new Error("Model pricing request timed out"));
119
+ });
120
+ req.write(encodedBody);
121
+ req.end();
122
+ });
23
123
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letmecode",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "description": "Terminal AI usage dashboard for Codex, Claude, Copilot, and Antigravity.",
5
5
  "author": "Devforth (https://devforth.io)",
6
6
  "license": "MIT",