letmecode 0.1.28 → 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.
@@ -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,
@@ -88,6 +88,7 @@ export function buildWindowLists(windows) {
88
88
  lastSeenUtcIso: formatIsoFromMilliseconds(window.lastSeenMs),
89
89
  minUsedPercent: window.minUsedPercent,
90
90
  maxUsedPercent: window.maxUsedPercent,
91
+ measuredUsedPercent: usage.measuredUsedPercent,
91
92
  totals: usage.totals,
92
93
  modelUsage: usage.modelUsage,
93
94
  eventCount: 0
@@ -178,6 +179,10 @@ function collapseNearbyWindows(rows) {
178
179
  existing.lastSeenUtcIso > row.lastSeenUtcIso ? existing.lastSeenUtcIso : row.lastSeenUtcIso;
179
180
  existing.minUsedPercent = Math.min(existing.minUsedPercent, row.minUsedPercent);
180
181
  existing.maxUsedPercent = Math.max(existing.maxUsedPercent, row.maxUsedPercent);
182
+ existing.measuredUsedPercent =
183
+ existing.minUsedPercent === existing.maxUsedPercent
184
+ ? 0
185
+ : clampUsedPercent(existing.maxUsedPercent - existing.minUsedPercent);
181
186
  addUsageTotals(existing.totals, row.totals);
182
187
  existing.modelUsage = mergeModelUsageRows(existing.modelUsage, row.modelUsage);
183
188
  existing.eventCount = existing.totals.eventCount;
@@ -189,13 +194,22 @@ function computeWindowUsage(events) {
189
194
  // saturation has to be applied after we sort the captured window events.
190
195
  const totals = createEmptyUsageTotals();
191
196
  const byModel = new Map();
192
- let sawBelowCap = false;
193
- let isExhausted = false;
194
- for (const event of [...events].sort((left, right) => left.eventTimeMs - right.eventTimeMs)) {
197
+ const sortedEvents = [...events].sort((left, right) => left.eventTimeMs - right.eventTimeMs);
198
+ const baselinePercent = clampUsedPercent(sortedEvents[0]?.usedPercent ?? 0);
199
+ // Each rate-limit snapshot is recorded after the usage event on the same
200
+ // line. The first event therefore has no matching "before" percentage, even
201
+ // when its post-event value rounds to zero. Keep its percentage only as the
202
+ // baseline and aggregate requests from the next snapshot onward.
203
+ const firstIncludedIndex = sortedEvents.length > 0 ? 1 : 0;
204
+ let highestIncludedPercent = baselinePercent;
205
+ let sawBelowCap = baselinePercent < 100;
206
+ let isExhausted = baselinePercent >= 100;
207
+ for (const [index, event] of sortedEvents.entries()) {
195
208
  sawBelowCap || (sawBelowCap = event.usedPercent < 100);
196
- if (!isExhausted) {
209
+ if (index >= firstIncludedIndex && !isExhausted) {
197
210
  addUsageTotals(totals, event.totals);
198
211
  addWindowModelUsage(byModel, event.modelId, event.totals);
212
+ highestIncludedPercent = Math.max(highestIncludedPercent, clampUsedPercent(event.usedPercent));
199
213
  if (sawBelowCap && event.usedPercent >= 100) {
200
214
  isExhausted = true;
201
215
  }
@@ -203,9 +217,32 @@ function computeWindowUsage(events) {
203
217
  }
204
218
  return {
205
219
  totals,
206
- modelUsage: buildModelUsageRows(byModel)
220
+ modelUsage: buildModelUsageRows(byModel),
221
+ measuredUsedPercent: clampUsedPercent(highestIncludedPercent - baselinePercent)
207
222
  };
208
223
  }
224
+ /**
225
+ * Return the provider percentage represented by a window's token totals.
226
+ * Older/injected rows without the explicit field retain the legacy derivation.
227
+ */
228
+ export function resolveMeasuredUsedPercent(window) {
229
+ if (window.measuredUsedPercent === null) {
230
+ return null;
231
+ }
232
+ if (typeof window.measuredUsedPercent === "number" && Number.isFinite(window.measuredUsedPercent)) {
233
+ return clampUsedPercent(window.measuredUsedPercent);
234
+ }
235
+ if (window.minUsedPercent === window.maxUsedPercent) {
236
+ return clampUsedPercent(window.maxUsedPercent);
237
+ }
238
+ return clampUsedPercent(window.maxUsedPercent - window.minUsedPercent);
239
+ }
240
+ function clampUsedPercent(value) {
241
+ if (!Number.isFinite(value)) {
242
+ return 0;
243
+ }
244
+ return Math.max(0, Math.min(100, value));
245
+ }
209
246
  function upsertWindow(windows, scope, rateLimits, window, eventTimeMs, modelId, deltaTotals) {
210
247
  if (!window) {
211
248
  return;
@@ -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
  }
@@ -2,6 +2,7 @@ import { request } from "node:https";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { resolveMeasuredUsedPercent } from "./providers/limits.js";
5
6
  const REPORTING_ENDPOINT = "https://devforth.io/admin/api/report_ussage_anonymous";
6
7
  const CREDIT_TO_DOLLARS = 0.01;
7
8
  // Limit windows at or below this used-percent carry too little signal to be
@@ -99,13 +100,11 @@ function buildUsageRaw(modelUsage) {
99
100
  return usageRaw;
100
101
  }
101
102
  function resolveReportedUsedPercents(window) {
102
- if (window.minUsedPercent === window.maxUsedPercent) {
103
- return clampPercent(window.maxUsedPercent);
104
- }
105
- return clampPercent(window.maxUsedPercent - window.minUsedPercent);
103
+ return clampPercent(resolveMeasuredUsedPercent(window) ?? 0);
106
104
  }
107
105
  function shouldReportUsageWindow(window) {
108
- return resolveReportedUsedPercents(window) > SKIP_REPORT_USED_PERCENTS;
106
+ return (window.totals.estimatedCreditsStatus !== "unavailable" &&
107
+ resolveReportedUsedPercents(window) > SKIP_REPORT_USED_PERCENTS);
109
108
  }
110
109
  function clampPercent(value) {
111
110
  if (!Number.isFinite(value)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letmecode",
3
- "version": "0.1.28",
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",