letmecode 0.1.8 → 0.1.9

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.
@@ -96,7 +96,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
96
96
  const planType = typeof event.rateLimits?.plan_type === "string" ? event.rateLimits.plan_type : undefined;
97
97
  const safeEventTimeMs = Number.isFinite(event.timestampMs) ? event.timestampMs : 0;
98
98
  addDailyUsage(byDay, event.timestampMs, event.modelId, planType, event.totals);
99
- applyRateLimits(windows, event.rateLimits, safeEventTimeMs, event.totals, planTypes);
99
+ applyRateLimits(windows, event.rateLimits, safeEventTimeMs, event.modelId, event.totals, planTypes);
100
100
  }
101
101
  parseTotals.tokenEvents = selectedEvents.length;
102
102
  if (parseTotals.malformedLines > 0) {
@@ -872,9 +872,19 @@ function buildLiveLimitWindowRow(snapshot, planType, selectedEvents, now) {
872
872
  minUsedPercent: snapshot.usedPercent,
873
873
  maxUsedPercent: snapshot.usedPercent,
874
874
  totals,
875
+ modelUsage: buildModelUsageRowsForEvents(inWindowEvents),
875
876
  eventCount: totals.eventCount
876
877
  };
877
878
  }
879
+ function buildModelUsageRowsForEvents(events) {
880
+ const byModel = new Map();
881
+ for (const event of events) {
882
+ addModelUsage(byModel, event.modelId, event.totals);
883
+ }
884
+ return [...byModel.entries()]
885
+ .map(([modelId, totals]) => ({ modelId, totals }))
886
+ .sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
887
+ }
878
888
  function toUtcIso(value) {
879
889
  return new Date(value).toISOString().replace(".000Z", "Z");
880
890
  }
@@ -349,7 +349,7 @@ async function parseSessionFile(filePath, byModel, byDay, windows, planTypes, kn
349
349
  const rateLimits = asRecord(payload.rate_limits);
350
350
  const planType = typeof rateLimits?.plan_type === "string" ? rateLimits.plan_type : undefined;
351
351
  addDailyUsage(byDay, eventTimeMs, resolvedModelId, planType, deltaTotals);
352
- applyRateLimits(windows, rateLimits, safeEventTimeMs, deltaTotals, planTypes);
352
+ applyRateLimits(windows, rateLimits, safeEventTimeMs, resolvedModelId, deltaTotals, planTypes);
353
353
  }
354
354
  return { linesRead, tokenEvents, malformedLines };
355
355
  }
@@ -8,31 +8,35 @@ export function numberOrZero(value) {
8
8
  export function asRecord(value) {
9
9
  return value && typeof value === "object" ? value : null;
10
10
  }
11
- export function applyRateLimits(windows, rateLimits, eventTimeMs, deltaTotals, planTypes) {
11
+ export function applyRateLimits(windows, rateLimits, eventTimeMs, modelId, deltaTotals, planTypes) {
12
12
  if (!rateLimits) {
13
13
  return;
14
14
  }
15
15
  if (typeof rateLimits.plan_type === "string") {
16
16
  planTypes.add(rateLimits.plan_type);
17
17
  }
18
- upsertWindow(windows, "primary", rateLimits, asRecord(rateLimits.primary), eventTimeMs, deltaTotals);
19
- upsertWindow(windows, "secondary", rateLimits, asRecord(rateLimits.secondary), eventTimeMs, deltaTotals);
18
+ upsertWindow(windows, "primary", rateLimits, asRecord(rateLimits.primary), eventTimeMs, modelId, deltaTotals);
19
+ upsertWindow(windows, "secondary", rateLimits, asRecord(rateLimits.secondary), eventTimeMs, modelId, deltaTotals);
20
20
  }
21
21
  export function buildWindowLists(windows) {
22
- const rows = collapseNearbyWindows([...windows.values()].map((window) => ({
23
- scope: window.scope,
24
- planType: window.planType,
25
- limitId: window.limitId,
26
- windowMinutes: window.windowMinutes,
27
- startTimeUtcIso: formatIsoFromSeconds(window.minStartsAt),
28
- endTimeUtcIso: formatIsoFromSeconds(window.maxResetsAt),
29
- firstSeenUtcIso: formatIsoFromMilliseconds(window.firstSeenMs),
30
- lastSeenUtcIso: formatIsoFromMilliseconds(window.lastSeenMs),
31
- minUsedPercent: window.minUsedPercent,
32
- maxUsedPercent: window.maxUsedPercent,
33
- totals: computeWindowTotals(window.events),
34
- eventCount: 0
35
- })))
22
+ const rows = collapseNearbyWindows([...windows.values()].map((window) => {
23
+ const usage = computeWindowUsage(window.events);
24
+ return {
25
+ scope: window.scope,
26
+ planType: window.planType,
27
+ limitId: window.limitId,
28
+ windowMinutes: window.windowMinutes,
29
+ startTimeUtcIso: formatIsoFromSeconds(window.minStartsAt),
30
+ endTimeUtcIso: formatIsoFromSeconds(window.maxResetsAt),
31
+ firstSeenUtcIso: formatIsoFromMilliseconds(window.firstSeenMs),
32
+ lastSeenUtcIso: formatIsoFromMilliseconds(window.lastSeenMs),
33
+ minUsedPercent: window.minUsedPercent,
34
+ maxUsedPercent: window.maxUsedPercent,
35
+ totals: usage.totals,
36
+ modelUsage: usage.modelUsage,
37
+ eventCount: 0
38
+ };
39
+ }))
36
40
  .map((row) => ({
37
41
  ...row,
38
42
  eventCount: row.totals.eventCount
@@ -71,7 +75,11 @@ function collapseNearbyWindows(rows) {
71
75
  if (!existing) {
72
76
  collapsed.set(key, {
73
77
  ...row,
74
- totals: cloneUsageTotals(row.totals)
78
+ totals: cloneUsageTotals(row.totals),
79
+ modelUsage: row.modelUsage.map((entry) => ({
80
+ modelId: entry.modelId,
81
+ totals: cloneUsageTotals(entry.totals)
82
+ }))
75
83
  });
76
84
  continue;
77
85
  }
@@ -86,28 +94,34 @@ function collapseNearbyWindows(rows) {
86
94
  existing.minUsedPercent = Math.min(existing.minUsedPercent, row.minUsedPercent);
87
95
  existing.maxUsedPercent = Math.max(existing.maxUsedPercent, row.maxUsedPercent);
88
96
  addUsageTotals(existing.totals, row.totals);
97
+ existing.modelUsage = mergeModelUsageRows(existing.modelUsage, row.modelUsage);
89
98
  existing.eventCount = existing.totals.eventCount;
90
99
  }
91
100
  return [...collapsed.values()];
92
101
  }
93
- function computeWindowTotals(events) {
102
+ function computeWindowUsage(events) {
94
103
  // Session files are not guaranteed to be parsed in timestamp order, so
95
104
  // saturation has to be applied after we sort the captured window events.
96
105
  const totals = createEmptyUsageTotals();
106
+ const byModel = new Map();
97
107
  let sawBelowCap = false;
98
108
  let isExhausted = false;
99
109
  for (const event of [...events].sort((left, right) => left.eventTimeMs - right.eventTimeMs)) {
100
110
  sawBelowCap || (sawBelowCap = event.usedPercent < 100);
101
111
  if (!isExhausted) {
102
112
  addUsageTotals(totals, event.totals);
113
+ addWindowModelUsage(byModel, event.modelId, event.totals);
103
114
  if (sawBelowCap && event.usedPercent >= 100) {
104
115
  isExhausted = true;
105
116
  }
106
117
  }
107
118
  }
108
- return totals;
119
+ return {
120
+ totals,
121
+ modelUsage: buildModelUsageRows(byModel)
122
+ };
109
123
  }
110
- function upsertWindow(windows, scope, rateLimits, window, eventTimeMs, deltaTotals) {
124
+ function upsertWindow(windows, scope, rateLimits, window, eventTimeMs, modelId, deltaTotals) {
111
125
  if (!window) {
112
126
  return;
113
127
  }
@@ -132,7 +146,7 @@ function upsertWindow(windows, scope, rateLimits, window, eventTimeMs, deltaTota
132
146
  lastSeenMs: eventTimeMs,
133
147
  minUsedPercent: usedPercent,
134
148
  maxUsedPercent: usedPercent,
135
- events: [{ eventTimeMs, usedPercent, totals: cloneUsageTotals(deltaTotals) }]
149
+ events: [{ eventTimeMs, modelId, usedPercent, totals: cloneUsageTotals(deltaTotals) }]
136
150
  });
137
151
  return;
138
152
  }
@@ -142,5 +156,25 @@ function upsertWindow(windows, scope, rateLimits, window, eventTimeMs, deltaTota
142
156
  existing.lastSeenMs = Math.max(existing.lastSeenMs, eventTimeMs);
143
157
  existing.minUsedPercent = Math.min(existing.minUsedPercent, usedPercent);
144
158
  existing.maxUsedPercent = Math.max(existing.maxUsedPercent, usedPercent);
145
- existing.events.push({ eventTimeMs, usedPercent, totals: cloneUsageTotals(deltaTotals) });
159
+ existing.events.push({ eventTimeMs, modelId, usedPercent, totals: cloneUsageTotals(deltaTotals) });
160
+ }
161
+ function addWindowModelUsage(byModel, modelId, totals) {
162
+ const existing = byModel.get(modelId);
163
+ if (!existing) {
164
+ byModel.set(modelId, cloneUsageTotals(totals));
165
+ return;
166
+ }
167
+ addUsageTotals(existing, totals);
168
+ }
169
+ function buildModelUsageRows(byModel) {
170
+ return [...byModel.entries()]
171
+ .map(([modelId, totals]) => ({ modelId, totals }))
172
+ .sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
173
+ }
174
+ function mergeModelUsageRows(left, right) {
175
+ const byModel = new Map();
176
+ for (const row of [...left, ...right]) {
177
+ addWindowModelUsage(byModel, row.modelId, row.totals);
178
+ }
179
+ return buildModelUsageRows(byModel);
146
180
  }
@@ -37,25 +37,23 @@ function buildAnonymousUsageReport(stats, window, letmecodeVersion) {
37
37
  used_percents: resolveReportedUsedPercents(window),
38
38
  used_exhausted: window.maxUsedPercent >= 100,
39
39
  value_dollars: roundDollars(window.totals.estimatedCredits * CREDIT_TO_DOLLARS),
40
- usage_raw: buildUsageRaw(stats.providerId, window),
40
+ usage_raw: buildUsageRaw(window.modelUsage),
41
41
  letmecode_version: letmecodeVersion
42
42
  };
43
43
  }
44
- function buildUsageRaw(providerId, window) {
45
- const usageRaw = {
46
- output: window.totals.outputTokens,
47
- input_non_cache: window.totals.inputTokens,
48
- input_cache_read: window.totals.cacheReadInputTokens
49
- };
50
- if (isAnthropicProvider(providerId)) {
51
- usageRaw.input_cache_w5m = window.totals.cacheWrite5mInputTokens;
52
- usageRaw.input_cache_w1h = window.totals.cacheWrite1hInputTokens;
44
+ function buildUsageRaw(modelUsage) {
45
+ const usageRaw = {};
46
+ for (const row of modelUsage) {
47
+ usageRaw[row.modelId] = {
48
+ output: row.totals.outputTokens,
49
+ input_non_cache: row.totals.inputTokens,
50
+ input_cache_w5m: row.totals.cacheWrite5mInputTokens,
51
+ input_cache_w1h: row.totals.cacheWrite1hInputTokens,
52
+ input_cache_read: row.totals.cacheReadInputTokens
53
+ };
53
54
  }
54
55
  return usageRaw;
55
56
  }
56
- function isAnthropicProvider(providerId) {
57
- return providerId === "claude" || providerId === "claude-vscode";
58
- }
59
57
  function resolveReportedUsedPercents(window) {
60
58
  if (window.minUsedPercent === window.maxUsedPercent) {
61
59
  return clampPercent(window.maxUsedPercent);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letmecode",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Provider-based terminal usage dashboard for LetMeCode.",
5
5
  "author": "devforth.io",
6
6
  "license": "MIT",