letmecode 0.1.30 → 0.1.33

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.
@@ -4,7 +4,7 @@ import { Box, Text, measureElement, useApp, useInput, useStdin, useStdout, rende
4
4
  import { buildHelpText, buildProviderStatsOptions, parseCliOptions } from "./cli-options.js";
5
5
  import { configureCopilotVsCodeLogging, createProviders } from "./providers/index.js";
6
6
  import { reportAnonymousUsage } from "./reporting.js";
7
- import { estimateLimitFullValue, resolveMeasuredUsedPercent, selectLatestActiveLimitWindows } from "./providers/limits.js";
7
+ import { estimateLimitFullValue, isPartialUsagePairing, resolveMeasuredUsedPercent, selectLatestActiveLimitWindows } from "./providers/limits.js";
8
8
  const ESC = String.fromCharCode(0x1b);
9
9
  // Normal mouse tracking (button press/release only) + SGR extended coordinates.
10
10
  // This makes the controls clickable while leaving Shift+drag native text selection
@@ -499,7 +499,7 @@ function buildLimitWindowTableRow(window, isActive) {
499
499
  formatUsageUsd(window.totals),
500
500
  // Extrapolated full value of the limit, rounded to a single figure here;
501
501
  // the details panel shows the unrounded ±1% range.
502
- formatLimitFullValueCompact(window.totals, resolveMeasuredUsedPercent(window))
502
+ formatLimitFullValueCompact(window)
503
503
  ]
504
504
  };
505
505
  }
@@ -543,7 +543,9 @@ function SelectionDetailsPanel(props) {
543
543
  }
544
544
  if (props.viewId === "limit-windows" && props.selectedLimitRow) {
545
545
  const row = props.selectedLimitRow;
546
- return (_jsxs(DetailsPanelFrame, { children: [_jsxs(Box, { children: [_jsxs(Box, { flexDirection: "column", width: 25, children: [_jsx(DetailRow, { label: "Plan", value: row.planType }), _jsx(DetailRow, { label: "Models", value: formatLimitWindowModels(row) }), _jsx(DetailRow, { label: "Window", value: formatCompactWindowMinutes(row.windowMinutes) }), _jsx(DetailRow, { label: "Usage", value: formatUsedPercentRange(row.minUsedPercent, row.maxUsedPercent) }), _jsx(DetailRow, { label: "Measured", value: formatMeasuredUsedPercent(row) }), _jsx(DetailRow, { label: "Events", value: formatInteger(row.eventCount) })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(DetailRow, { label: "Period", value: `${formatCompactLocalDateTime(row.startTimeUtcIso)} → ${formatCompactLocalDateTime(row.endTimeUtcIso)}` }), _jsx(DetailRow, { label: "Input", value: formatInteger(row.totals.inputTokens) }), _jsx(DetailRow, { label: "Cache read", value: formatCacheTokens(row.totals.cacheReadStatus, row.totals.cacheReadInputTokens) }), _jsx(DetailRow, { label: "Cache write", value: formatCacheTokens(row.totals.cacheWriteStatus, row.totals.cacheWriteInputTokens) }), _jsx(DetailRow, { label: "Output", value: formatInteger(row.totals.outputTokens) }), _jsx(DetailRow, { label: "Total", value: formatInteger(row.totals.totalTokens) })] })] }), _jsx(DetailRow, { label: "API eq.", value: formatUsageUsd(row.totals), note: "API equivalent cost" }), _jsx(DetailRow, { label: "Full value", value: formatLimitFullValueRange(row.totals, resolveMeasuredUsedPercent(row)), note: "API equivalent cost extrapolated from the paired token/usage interval" })] }));
546
+ return (_jsxs(DetailsPanelFrame, { children: [_jsxs(Box, { children: [_jsxs(Box, { flexDirection: "column", width: 25, children: [_jsx(DetailRow, { label: "Plan", value: row.planType }), _jsx(DetailRow, { label: "Models", value: formatLimitWindowModels(row) }), _jsx(DetailRow, { label: "Window", value: formatCompactWindowMinutes(row.windowMinutes) }), _jsx(DetailRow, { label: "Usage", value: formatUsedPercentRange(row.minUsedPercent, row.maxUsedPercent) }), _jsx(DetailRow, { label: "Measured", value: formatMeasuredUsedPercent(row) }), _jsx(DetailRow, { label: "Events", value: formatInteger(row.eventCount) })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(DetailRow, { label: "Period", value: `${formatCompactLocalDateTime(row.startTimeUtcIso)} → ${formatCompactLocalDateTime(row.endTimeUtcIso)}` }), isPartialUsagePairing(row) ? (_jsx(DetailRow, { label: "Paired from", value: formatCompactLocalDateTime(row.pairedUsageStartUtcIso ?? row.startTimeUtcIso), note: "percentage restarted mid-window" })) : null, _jsx(DetailRow, { label: "Input", value: formatInteger(row.totals.inputTokens) }), _jsx(DetailRow, { label: "Cache read", value: formatCacheTokens(row.totals.cacheReadStatus, row.totals.cacheReadInputTokens) }), _jsx(DetailRow, { label: "Cache write", value: formatCacheTokens(row.totals.cacheWriteStatus, row.totals.cacheWriteInputTokens) }), _jsx(DetailRow, { label: "Output", value: formatInteger(row.totals.outputTokens) }), _jsx(DetailRow, { label: "Total", value: formatInteger(row.totals.totalTokens) })] })] }), _jsx(DetailRow, { label: "API eq.", value: formatUsageUsd(row.totals), note: "API equivalent cost" }), _jsx(DetailRow, { label: "Full value", value: formatLimitFullValueRange(row), note: isPartialUsagePairing(row)
547
+ ? `approximate: this percentage restarted mid-window, usage paired from ${formatCompactLocalDateTime(row.pairedUsageStartUtcIso ?? row.startTimeUtcIso)}`
548
+ : "API equivalent cost extrapolated from the paired token/usage interval" })] }));
547
549
  }
548
550
  if (props.viewId === "day-to-day-analyses" && props.selectedDayRow) {
549
551
  const row = props.selectedDayRow;
@@ -650,17 +652,24 @@ function limitFullValueUsd(totals, usedPercent) {
650
652
  const usedUsd = totals.estimatedCredits * CODEX_CREDIT_COST_USD;
651
653
  return estimateLimitFullValue(usedUsd, usedPercent);
652
654
  }
653
- function formatLimitFullValueCompact(totals, usedPercent) {
654
- const estimate = limitFullValueUsd(totals, usedPercent);
655
- return estimate ? formatUsdWhole(estimate.point) : "-";
655
+ // A window whose paired usage covers only part of its span is paired across an
656
+ // only approximately known interval, so the figures it yields carry a "~".
657
+ function formatLimitFullValueCompact(window) {
658
+ const estimate = limitFullValueUsd(window.totals, resolveMeasuredUsedPercent(window));
659
+ if (!estimate) {
660
+ return "-";
661
+ }
662
+ const value = formatUsdWhole(estimate.point);
663
+ return isPartialUsagePairing(window) ? `~${value}` : value;
656
664
  }
657
- function formatLimitFullValueRange(totals, usedPercent) {
658
- const estimate = limitFullValueUsd(totals, usedPercent);
665
+ function formatLimitFullValueRange(window) {
666
+ const estimate = limitFullValueUsd(window.totals, resolveMeasuredUsedPercent(window));
659
667
  if (!estimate) {
660
668
  return "-";
661
669
  }
662
670
  const low = formatUsd(estimate.low);
663
- return Number.isFinite(estimate.high) ? `${low} – ${formatUsd(estimate.high)}` : `≥ ${low}`;
671
+ const range = Number.isFinite(estimate.high) ? `${low} – ${formatUsd(estimate.high)}` : `≥ ${low}`;
672
+ return isPartialUsagePairing(window) ? `~ ${range}` : range;
664
673
  }
665
674
  function formatUnitUsd(value) {
666
675
  if (!Number.isFinite(value)) {
@@ -0,0 +1,140 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { asRecord } from "./limits.js";
4
+ const CLAUDE_PLAN_STATE_VERSION = 1;
5
+ const MAX_TRACKED_PLANS = 20;
6
+ // Refresh ticks re-resolve the same plan every few seconds; only persist the
7
+ // heartbeat once it is stale enough to matter for a switch boundary.
8
+ const LAST_SEEN_REFRESH_MS = 5 * 60000;
9
+ export function getClaudePlanStatePath(root) {
10
+ return path.join(path.resolve(root), ".letmecode", "claude-plan-state.json");
11
+ }
12
+ /**
13
+ * Record that `planType` is the plan in effect now and return the moment the
14
+ * current plan first became visible, but only when letmecode had already seen a
15
+ * different plan before it. A first-ever observation returns null: without a
16
+ * previous plan on record there is no evidence that the provider's percentages
17
+ * restarted, so the caller must keep pairing the full window.
18
+ */
19
+ export async function recordClaudePlanObservation(options) {
20
+ const statePath = getClaudePlanStatePath(options.root);
21
+ const nowMs = options.now.getTime();
22
+ const nowIso = new Date(nowMs).toISOString();
23
+ const state = await readClaudePlanState(statePath);
24
+ const observations = state.observations;
25
+ const previous = resolveLatestObservation(observations, options.planType);
26
+ const existing = observations.find((observation) => observation.planType === options.planType);
27
+ let entry;
28
+ let changed = false;
29
+ if (!existing) {
30
+ entry = {
31
+ planType: options.planType,
32
+ firstSeenUtcIso: nowIso,
33
+ lastSeenUtcIso: nowIso,
34
+ ...(previous ? { switchedFromPlanType: previous.planType } : {})
35
+ };
36
+ observations.push(entry);
37
+ changed = true;
38
+ }
39
+ else if (previous) {
40
+ // The account came back to a plan letmecode saw earlier: its percentages
41
+ // restarted again, so the stale first-seen no longer marks the boundary.
42
+ existing.firstSeenUtcIso = nowIso;
43
+ existing.lastSeenUtcIso = nowIso;
44
+ existing.switchedFromPlanType = previous.planType;
45
+ entry = existing;
46
+ changed = true;
47
+ }
48
+ else {
49
+ entry = existing;
50
+ changed = nowMs - Date.parse(existing.lastSeenUtcIso) >= LAST_SEEN_REFRESH_MS;
51
+ existing.lastSeenUtcIso = nowIso;
52
+ }
53
+ if (changed) {
54
+ await writeClaudePlanState(statePath, {
55
+ version: CLAUDE_PLAN_STATE_VERSION,
56
+ observations: observations.slice(-MAX_TRACKED_PLANS)
57
+ }, options.traceLogger);
58
+ }
59
+ if (!entry.switchedFromPlanType) {
60
+ return null;
61
+ }
62
+ const planStartMs = Date.parse(entry.firstSeenUtcIso);
63
+ return Number.isFinite(planStartMs) ? planStartMs : null;
64
+ }
65
+ /** Latest plan on record that is not `planType`, by the time it was last seen. */
66
+ function resolveLatestObservation(observations, planType) {
67
+ let latest = null;
68
+ let latestMs = Number.NEGATIVE_INFINITY;
69
+ for (const observation of observations) {
70
+ if (observation.planType === planType) {
71
+ continue;
72
+ }
73
+ const lastSeenMs = Date.parse(observation.lastSeenUtcIso);
74
+ if (Number.isFinite(lastSeenMs) && lastSeenMs > latestMs) {
75
+ latest = observation;
76
+ latestMs = lastSeenMs;
77
+ }
78
+ }
79
+ return latest;
80
+ }
81
+ async function readClaudePlanState(statePath) {
82
+ let text;
83
+ try {
84
+ text = await fs.promises.readFile(statePath, { encoding: "utf8" });
85
+ }
86
+ catch {
87
+ return { version: CLAUDE_PLAN_STATE_VERSION, observations: [] };
88
+ }
89
+ let payload;
90
+ try {
91
+ payload = JSON.parse(text);
92
+ }
93
+ catch {
94
+ return { version: CLAUDE_PLAN_STATE_VERSION, observations: [] };
95
+ }
96
+ const record = asRecord(payload);
97
+ if (!record || record.version !== CLAUDE_PLAN_STATE_VERSION || !Array.isArray(record.observations)) {
98
+ return { version: CLAUDE_PLAN_STATE_VERSION, observations: [] };
99
+ }
100
+ return {
101
+ version: CLAUDE_PLAN_STATE_VERSION,
102
+ observations: record.observations
103
+ .map((value) => parseObservation(value))
104
+ .filter((observation) => observation !== null)
105
+ };
106
+ }
107
+ function parseObservation(value) {
108
+ const record = asRecord(value);
109
+ if (!record) {
110
+ return null;
111
+ }
112
+ const planType = typeof record.planType === "string" ? record.planType.trim() : "";
113
+ const firstSeenUtcIso = typeof record.firstSeenUtcIso === "string" ? record.firstSeenUtcIso : "";
114
+ const lastSeenUtcIso = typeof record.lastSeenUtcIso === "string" ? record.lastSeenUtcIso : "";
115
+ if (!planType || !Number.isFinite(Date.parse(firstSeenUtcIso)) || !Number.isFinite(Date.parse(lastSeenUtcIso))) {
116
+ return null;
117
+ }
118
+ const switchedFromPlanType = typeof record.switchedFromPlanType === "string" && record.switchedFromPlanType.trim()
119
+ ? record.switchedFromPlanType.trim()
120
+ : undefined;
121
+ return {
122
+ planType,
123
+ firstSeenUtcIso,
124
+ lastSeenUtcIso,
125
+ ...(switchedFromPlanType ? { switchedFromPlanType } : {})
126
+ };
127
+ }
128
+ /** Never let a read-only or full disk break the dashboard. */
129
+ async function writeClaudePlanState(statePath, state, traceLogger) {
130
+ const temporaryPath = `${statePath}.${process.pid}.tmp`;
131
+ try {
132
+ await fs.promises.mkdir(path.dirname(statePath), { recursive: true });
133
+ await fs.promises.writeFile(temporaryPath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
134
+ await fs.promises.rename(temporaryPath, statePath);
135
+ }
136
+ catch (error) {
137
+ traceLogger?.log(`[Claude] Could not persist plan state to ${statePath}: ${String(error)}`);
138
+ await fs.promises.rm(temporaryPath, { force: true }).catch(() => undefined);
139
+ }
140
+ }
@@ -6,7 +6,8 @@ import path from "node:path";
6
6
  import readline from "node:readline";
7
7
  import { promisify } from "node:util";
8
8
  import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "./contract.js";
9
- import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregates, numberOrZero } from "./limits.js";
9
+ import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregates, isPartialUsagePairing, numberOrZero } from "./limits.js";
10
+ import { recordClaudePlanObservation } from "./claude-plan-state.js";
10
11
  import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "./daily.js";
11
12
  import { fetchModelPricing, modelCostCredits } from "./pricing.js";
12
13
  /*
@@ -27,6 +28,14 @@ const VSCODE_CLAUDE_EXTENSION_PREFIX = "anthropic.claude-code-";
27
28
  const CLAUDE_SESSION_WINDOW_MINUTES = 5 * 60;
28
29
  const CLAUDE_WEEK_WINDOW_MINUTES = 7 * 24 * 60;
29
30
  const ANSI_ESCAPE_SEQUENCE = /\u001B\[[0-9;]*[A-Za-z]/g;
31
+ // Transcript `quotaLimits` entries name the quota cycle that rejected a request.
32
+ // Their reset instant identifies the cycle, so a cycle that differs from the one
33
+ // /usage reports now proves the account's quota accounting changed mid-window.
34
+ const CLAUDE_QUOTA_WINDOW_MINUTES_BY_RATE_LIMIT_TYPE = {
35
+ five_hour: CLAUDE_SESSION_WINDOW_MINUTES,
36
+ seven_day: CLAUDE_WEEK_WINDOW_MINUTES
37
+ };
38
+ const CLAUDE_QUOTA_RESET_MATCH_TOLERANCE_MS = 60000;
30
39
  const MONTH_INDEX_BY_LABEL = {
31
40
  jan: 0,
32
41
  feb: 1,
@@ -96,6 +105,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
96
105
  recordParsedUsageEvent(parsedEvents, event);
97
106
  }
98
107
  }
108
+ const quotaObservations = parsedSessionFiles.flatMap((file) => file.quotaObservations);
99
109
  const selectedEvents = [
100
110
  ...new Set(parsedEvents.keyedEvents.values())
101
111
  ];
@@ -156,7 +166,8 @@ export class ClaudeUsageProvider extends UsageProviderBase {
156
166
  readOauthCredentials: this.readOauthCredentials,
157
167
  traceLogger: options.traceLogger,
158
168
  now: this.now(),
159
- selectedEvents
169
+ selectedEvents,
170
+ quotaObservations
160
171
  });
161
172
  const primaryLimitWindows = liveLimitWindows.primaryLimitWindows.length > 0
162
173
  ? liveLimitWindows.primaryLimitWindows
@@ -164,6 +175,11 @@ export class ClaudeUsageProvider extends UsageProviderBase {
164
175
  const secondaryLimitWindows = liveLimitWindows.secondaryLimitWindows.length > 0
165
176
  ? liveLimitWindows.secondaryLimitWindows
166
177
  : fallbackSecondaryLimitWindows;
178
+ for (const window of [...primaryLimitWindows, ...secondaryLimitWindows]) {
179
+ if (isPartialUsagePairing(window) && window.pairedUsageStartUtcIso) {
180
+ warnings.push(`The ${formatWindowMinutesLabel(window.windowMinutes)} ${window.planType} limit restarted its percentage mid-window, so only usage from ${window.pairedUsageStartUtcIso} is paired with it and its full value is approximate.`);
181
+ }
182
+ }
167
183
  traceClaude(options.traceLogger, [
168
184
  `Finished stats collection:`,
169
185
  `filesScanned=${parseTotals.filesScanned}`,
@@ -414,6 +430,7 @@ async function parseSessionFile(filePath) {
414
430
  let linesRead = 0;
415
431
  let malformedLines = 0;
416
432
  const events = [];
433
+ const quotaObservations = [];
417
434
  for await (const line of lineReader) {
418
435
  linesRead += 1;
419
436
  if (!line.trim()) {
@@ -430,6 +447,12 @@ async function parseSessionFile(filePath) {
430
447
  if (payloadObject.type !== "assistant") {
431
448
  continue;
432
449
  }
450
+ // Rejected requests carry the quota cycle that turned them down but no usage,
451
+ // so this has to be read before the usage gate below drops the line.
452
+ const quotaObservation = extractQuotaObservation(payloadObject);
453
+ if (quotaObservation) {
454
+ quotaObservations.push(quotaObservation);
455
+ }
433
456
  const message = asRecord(payloadObject.message);
434
457
  const usage = asRecord(message?.usage);
435
458
  if (!usage || !isCurrentClaudeUsageEvent(payloadObject, message, usage)) {
@@ -459,7 +482,25 @@ async function parseSessionFile(filePath) {
459
482
  filePath,
460
483
  linesRead,
461
484
  malformedLines,
462
- events
485
+ events,
486
+ quotaObservations
487
+ };
488
+ }
489
+ function extractQuotaObservation(payloadObject) {
490
+ const quotaLimits = asRecord(payloadObject.quotaLimits);
491
+ if (!quotaLimits) {
492
+ return null;
493
+ }
494
+ const rateLimitType = typeof quotaLimits.rateLimitType === "string" ? quotaLimits.rateLimitType : "";
495
+ const resetsAtSeconds = numberOrZero(quotaLimits.resetsAt);
496
+ const timestampMs = Date.parse(String(payloadObject.timestamp ?? ""));
497
+ if (!rateLimitType || !resetsAtSeconds || !Number.isFinite(timestampMs)) {
498
+ return null;
499
+ }
500
+ return {
501
+ timestampMs,
502
+ rateLimitType,
503
+ resetsAtMs: resetsAtSeconds * 1000
463
504
  };
464
505
  }
465
506
  function isCurrentClaudeUsageEvent(payloadObject, message, usage) {
@@ -600,6 +641,15 @@ function normalizeTimestamp(value) {
600
641
  function extractRateLimits(payloadObject, message) {
601
642
  return asRecord(payloadObject.rate_limits) ?? asRecord(message?.rate_limits);
602
643
  }
644
+ function formatWindowMinutesLabel(windowMinutes) {
645
+ if (windowMinutes % (24 * 60) === 0) {
646
+ return `${windowMinutes / (24 * 60)}d`;
647
+ }
648
+ if (windowMinutes % 60 === 0) {
649
+ return `${windowMinutes / 60}h`;
650
+ }
651
+ return `${windowMinutes}m`;
652
+ }
603
653
  function traceClaude(traceLogger, message) {
604
654
  if (!traceLogger) {
605
655
  return;
@@ -671,12 +721,27 @@ async function buildLiveLimitWindows(options) {
671
721
  }
672
722
  const resolvedPlanType = resolveClaudeLivePlanType(credentials);
673
723
  traceClaude(options.traceLogger, `Resolved live plan type ${resolvedPlanType}.`);
724
+ // A percentage reported for the current plan cannot describe usage produced
725
+ // under the plan the account had before it, so remember when the switch became
726
+ // visible and stop pairing anything older with that percentage.
727
+ const planStartMs = resolvedPlanType === "live"
728
+ ? null
729
+ : await recordClaudePlanObservation({
730
+ root: options.root,
731
+ planType: resolvedPlanType,
732
+ now: options.now,
733
+ traceLogger: options.traceLogger
734
+ });
735
+ if (planStartMs !== null) {
736
+ traceClaude(options.traceLogger, `Plan ${resolvedPlanType} first seen at ${toUtcIso(planStartMs)}; older usage is not paired with its percentages.`);
737
+ }
738
+ const buildRow = (snapshot) => buildLiveLimitWindowRow(snapshot, resolvedPlanType, options.selectedEvents, options.now, resolvePairedUsageStartMs(snapshot, options.quotaObservations, planStartMs, options.traceLogger));
674
739
  const primaryLimitWindows = snapshots
675
740
  .filter((snapshot) => snapshot.scope === "primary")
676
- .map((snapshot) => buildLiveLimitWindowRow(snapshot, resolvedPlanType, options.selectedEvents, options.now));
741
+ .map(buildRow);
677
742
  const secondaryLimitWindows = snapshots
678
743
  .filter((snapshot) => snapshot.scope === "secondary")
679
- .map((snapshot) => buildLiveLimitWindowRow(snapshot, resolvedPlanType, options.selectedEvents, options.now));
744
+ .map(buildRow);
680
745
  for (let index = 0; index < snapshots.length; index += 1) {
681
746
  const snapshot = snapshots[index];
682
747
  const row = snapshot.scope === "primary"
@@ -690,6 +755,7 @@ async function buildLiveLimitWindows(options) {
690
755
  `used=${snapshot.usedPercent}%`,
691
756
  `limit=${snapshot.limitId}`,
692
757
  `range=${row.startTimeUtcIso}->${row.endTimeUtcIso}`,
758
+ `pairedFrom=${row.pairedUsageStartUtcIso ?? row.startTimeUtcIso}`,
693
759
  `matchedEvents=${row.eventCount}`,
694
760
  `input=${row.totals.inputTokens}`,
695
761
  `output=${row.totals.outputTokens}`,
@@ -1139,10 +1205,56 @@ function resolveHour24(hour12, meridiem) {
1139
1205
  }
1140
1206
  return meridiem === "pm" ? hour12 + 12 : hour12;
1141
1207
  }
1142
- function buildLiveLimitWindowRow(snapshot, planType, selectedEvents, now) {
1208
+ /**
1209
+ * Start of the interval a snapshot's percentage can describe. Usage recorded
1210
+ * before the account's current quota accounting began - a plan switch, or a
1211
+ * rejection from a different quota cycle inside the same window - is no longer
1212
+ * represented by the percentage /usage reports now, so pairing it would inflate
1213
+ * everything extrapolated from the pair. Returns null when the whole window
1214
+ * belongs to the current accounting.
1215
+ *
1216
+ * Both signals only bracket the switch: a foreign-cycle rejection happened at or
1217
+ * before it, the first sighting of the current plan at or after it. The earliest
1218
+ * candidate wins, so no usage that the current percentage does cover is dropped,
1219
+ * and callers treat what they extrapolate as approximate rather than exact.
1220
+ */
1221
+ function resolvePairedUsageStartMs(snapshot, quotaObservations, planStartMs, traceLogger) {
1222
+ const windowStartMs = snapshot.resetsAtMs - snapshot.windowMinutes * 60000;
1223
+ const candidates = [];
1224
+ if (planStartMs !== null && planStartMs > windowStartMs) {
1225
+ candidates.push(planStartMs);
1226
+ }
1227
+ const foreignCycleEndMs = resolveForeignQuotaCycleEndMs(snapshot, quotaObservations, windowStartMs);
1228
+ if (foreignCycleEndMs !== null) {
1229
+ traceClaude(traceLogger, `Window ${snapshot.limitId} contains a ${snapshot.windowMinutes}m quota rejection from another cycle at ${toUtcIso(foreignCycleEndMs)}.`);
1230
+ // The rejection itself belongs to the previous cycle, so pairing resumes
1231
+ // right after it.
1232
+ candidates.push(foreignCycleEndMs + 1);
1233
+ }
1234
+ return candidates.length > 0 ? Math.min(...candidates) : null;
1235
+ }
1236
+ /**
1237
+ * Latest moment inside the window at which a quota cycle other than the one the
1238
+ * snapshot reports was still in force.
1239
+ */
1240
+ function resolveForeignQuotaCycleEndMs(snapshot, quotaObservations, windowStartMs) {
1241
+ let latestMs = null;
1242
+ for (const observation of quotaObservations) {
1243
+ if (CLAUDE_QUOTA_WINDOW_MINUTES_BY_RATE_LIMIT_TYPE[observation.rateLimitType] !== snapshot.windowMinutes ||
1244
+ observation.timestampMs < windowStartMs ||
1245
+ observation.timestampMs >= snapshot.resetsAtMs ||
1246
+ Math.abs(observation.resetsAtMs - snapshot.resetsAtMs) <= CLAUDE_QUOTA_RESET_MATCH_TOLERANCE_MS) {
1247
+ continue;
1248
+ }
1249
+ latestMs = latestMs === null ? observation.timestampMs : Math.max(latestMs, observation.timestampMs);
1250
+ }
1251
+ return latestMs;
1252
+ }
1253
+ function buildLiveLimitWindowRow(snapshot, planType, selectedEvents, now, pairedUsageStartMs = null) {
1143
1254
  const startTimeMs = snapshot.resetsAtMs - snapshot.windowMinutes * 60000;
1255
+ const usageStartMs = Math.max(startTimeMs, pairedUsageStartMs ?? startTimeMs);
1144
1256
  const inWindowEvents = selectedEvents.filter((event) => Number.isFinite(event.timestampMs) &&
1145
- event.timestampMs >= startTimeMs &&
1257
+ event.timestampMs >= usageStartMs &&
1146
1258
  event.timestampMs < snapshot.resetsAtMs &&
1147
1259
  matchesClaudeLiveSnapshotModelScope(snapshot, event.modelId));
1148
1260
  const totals = sumUsageTotals(inWindowEvents.map((event) => event.totals));
@@ -1150,6 +1262,7 @@ function buildLiveLimitWindowRow(snapshot, planType, selectedEvents, now) {
1150
1262
  totals.estimatedCreditsStatus = "unavailable";
1151
1263
  }
1152
1264
  const fallbackLastSeenMs = Math.min(now.getTime(), snapshot.resetsAtMs);
1265
+ const hasPartialPairing = usageStartMs > startTimeMs;
1153
1266
  const firstSeenMs = inWindowEvents.reduce((minimum, event) => Math.min(minimum, event.timestampMs), Number.POSITIVE_INFINITY);
1154
1267
  const lastSeenMs = inWindowEvents.reduce((maximum, event) => Math.max(maximum, event.timestampMs), Number.NEGATIVE_INFINITY);
1155
1268
  return {
@@ -1160,7 +1273,8 @@ function buildLiveLimitWindowRow(snapshot, planType, selectedEvents, now) {
1160
1273
  windowMinutes: snapshot.windowMinutes,
1161
1274
  startTimeUtcIso: toUtcIso(startTimeMs),
1162
1275
  endTimeUtcIso: toUtcIso(snapshot.resetsAtMs),
1163
- firstSeenUtcIso: toUtcIso(Number.isFinite(firstSeenMs) ? firstSeenMs : startTimeMs),
1276
+ ...(hasPartialPairing ? { pairedUsageStartUtcIso: toUtcIso(usageStartMs) } : {}),
1277
+ firstSeenUtcIso: toUtcIso(Number.isFinite(firstSeenMs) ? firstSeenMs : usageStartMs),
1164
1278
  lastSeenUtcIso: toUtcIso(Number.isFinite(lastSeenMs) ? lastSeenMs : fallbackLastSeenMs),
1165
1279
  minUsedPercent: snapshot.usedPercent,
1166
1280
  maxUsedPercent: snapshot.usedPercent,
@@ -19,7 +19,12 @@ export function selectLatestActiveLimitWindows(windows, nowMs = Date.now()) {
19
19
  continue;
20
20
  }
21
21
  const startMs = Date.parse(window.startTimeUtcIso);
22
- const groupKey = JSON.stringify([window.planType, window.windowMinutes]);
22
+ // `limitId` is part of the key because a plan can run several limits of the
23
+ // same duration side by side: Claude meters an all-models week and a
24
+ // model-family week that share a plan, a length and a reset instant. Keyed
25
+ // on plan and duration alone they collapse into one group and the family
26
+ // window loses the tie, so it never counts as active.
27
+ const groupKey = JSON.stringify([window.planType, window.limitId, window.windowMinutes]);
23
28
  const current = latestByPlanAndWindow.get(groupKey);
24
29
  if (!current || startMs > current.startMs) {
25
30
  latestByPlanAndWindow.set(groupKey, { window, startMs });
@@ -237,6 +242,18 @@ export function resolveMeasuredUsedPercent(window) {
237
242
  }
238
243
  return clampUsedPercent(window.maxUsedPercent - window.minUsedPercent);
239
244
  }
245
+ /**
246
+ * True when a window's token totals cover only part of the window, so anything
247
+ * extrapolated from them (e.g. the limit's full value) is approximate.
248
+ */
249
+ export function isPartialUsagePairing(window) {
250
+ if (!window.pairedUsageStartUtcIso) {
251
+ return false;
252
+ }
253
+ const pairedStartMs = Date.parse(window.pairedUsageStartUtcIso);
254
+ const startMs = Date.parse(window.startTimeUtcIso);
255
+ return Number.isFinite(pairedStartMs) && Number.isFinite(startMs) && pairedStartMs > startMs;
256
+ }
240
257
  function clampUsedPercent(value) {
241
258
  if (!Number.isFinite(value)) {
242
259
  return 0;
@@ -44,7 +44,17 @@ export async function fetchModelPricing(modelIds, source) {
44
44
  output: model.output,
45
45
  inputCacheRead: model.input_cache_read,
46
46
  inputCacheWrite5m: model.input_cache_w5m,
47
- inputCacheWrite1h: model.input_cache_w1h
47
+ inputCacheWrite1h: model.input_cache_w1h,
48
+ longContext: model.long_context
49
+ ? {
50
+ inputTokensThreshold: model.long_context.input_tokens_threshold,
51
+ input: model.long_context.input,
52
+ output: model.long_context.output,
53
+ inputCacheRead: model.long_context.input_cache_read,
54
+ inputCacheWrite5m: model.long_context.input_cache_w5m,
55
+ inputCacheWrite1h: model.long_context.input_cache_w1h
56
+ }
57
+ : null
48
58
  }
49
59
  ]));
50
60
  return new Map([...slugByModelId.entries()].flatMap(([modelId, slug]) => {
@@ -56,17 +66,24 @@ export function modelCostCredits(pricing, usage) {
56
66
  if (!pricing) {
57
67
  return undefined;
58
68
  }
59
- if (usage.cacheWrite5mInputTokens > 0 && pricing.inputCacheWrite5m === null) {
69
+ const totalInputTokens = usage.inputTokens +
70
+ usage.cacheReadInputTokens +
71
+ usage.cacheWrite5mInputTokens +
72
+ usage.cacheWrite1hInputTokens;
73
+ const rates = pricing.longContext && totalInputTokens > pricing.longContext.inputTokensThreshold
74
+ ? pricing.longContext
75
+ : pricing;
76
+ if (usage.cacheWrite5mInputTokens > 0 && rates.inputCacheWrite5m === null) {
60
77
  return undefined;
61
78
  }
62
- if (usage.cacheWrite1hInputTokens > 0 && pricing.inputCacheWrite1h === null) {
79
+ if (usage.cacheWrite1hInputTokens > 0 && rates.inputCacheWrite1h === null) {
63
80
  return undefined;
64
81
  }
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;
82
+ return ((usage.inputTokens / 1000000) * rates.input +
83
+ (usage.cacheReadInputTokens / 1000000) * rates.inputCacheRead +
84
+ (usage.cacheWrite5mInputTokens / 1000000) * (rates.inputCacheWrite5m ?? 0) +
85
+ (usage.cacheWrite1hInputTokens / 1000000) * (rates.inputCacheWrite1h ?? 0) +
86
+ (usage.outputTokens / 1000000) * rates.output) * USD_TO_CREDITS;
70
87
  }
71
88
  async function fetchCached(request) {
72
89
  const key = JSON.stringify(request);
@@ -2,12 +2,17 @@ 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
+ import { isPartialUsagePairing, resolveMeasuredUsedPercent } from "./providers/limits.js";
6
6
  const REPORTING_ENDPOINT = "https://devforth.io/admin/api/report_ussage_anonymous";
7
7
  const CREDIT_TO_DOLLARS = 0.01;
8
8
  // Limit windows at or below this used-percent carry too little signal to be
9
9
  // worth reporting, so they are dropped from the anonymous usage payload.
10
10
  const SKIP_REPORT_USED_PERCENTS = 3;
11
+ // Name of the catch-all limit class: the window meters every model the plan
12
+ // offers, rather than one carved-out family.
13
+ const ALL_LIMIT_CLASS = "All";
14
+ // Mirrors the reporting endpoint's schema, which caps `limit_class` at 64.
15
+ const LIMIT_CLASS_MAX_LENGTH = 64;
11
16
  let versionCache = null;
12
17
  export async function reportAnonymousUsage(statsList) {
13
18
  const payload = await buildAnonymousUsagePayload(statsList);
@@ -36,6 +41,7 @@ function buildAnonymousUsageReport(stats, window, letmecodeVersion) {
36
41
  return {
37
42
  agent: stats.analytics?.agentName ?? stats.providerLabel.replace(/\s+/g, ""),
38
43
  model_type: resolveReportModelType(stats, window),
44
+ limit_class: resolveReportLimitClass(stats, window),
39
45
  userid_hash: stats.analytics?.userIdHash ?? "",
40
46
  plan_id: window.planType,
41
47
  window_duration_seconds: window.windowMinutes * 60,
@@ -48,6 +54,52 @@ function buildAnonymousUsageReport(stats, window, letmecodeVersion) {
48
54
  letmecode_version: letmecodeVersion
49
55
  };
50
56
  }
57
+ /**
58
+ * Canonical, display-ready name of the limit class a window belongs to.
59
+ * Every provider words the same idea differently - Antigravity separates Gemini
60
+ * from third-party models, Claude carves a model family out of the weekly
61
+ * window - so letmecode folds them onto one vocabulary and consumers never have
62
+ * to learn the provider-specific spellings in `model_type`.
63
+ *
64
+ * The vocabulary is open: a class nobody has seen before is reported under its
65
+ * own name rather than forced into an existing bucket.
66
+ */
67
+ function resolveReportLimitClass(stats, window) {
68
+ if (stats.providerId === "claude") {
69
+ const modelFamily = window.modelType?.toLowerCase().replace(/\s+only$/, "").trim();
70
+ return modelFamily ? toLimitClassName(modelFamily) : ALL_LIMIT_CLASS;
71
+ }
72
+ if (stats.providerId === "antigravity") {
73
+ const limitId = window.limitId.toLowerCase();
74
+ if (limitId.includes("gemini")) {
75
+ return "Main";
76
+ }
77
+ if (limitId.startsWith("3p") || limitId.includes("third-party")) {
78
+ return "Other";
79
+ }
80
+ return toLimitClassName(window.limitId);
81
+ }
82
+ if (stats.providerId === "copilot") {
83
+ // Copilot quota labels ("AI Credits", "Premium requests") are already the
84
+ // name of the bucket they meter.
85
+ return window.modelType ? toLimitClassName(window.modelType) : ALL_LIMIT_CLASS;
86
+ }
87
+ // Codex, and anything else, meters every model against one shared allowance.
88
+ return ALL_LIMIT_CLASS;
89
+ }
90
+ /**
91
+ * Title-case a provider string into a class name, leaving words that already
92
+ * carry capitals alone so acronyms such as "AI Credits" survive intact.
93
+ */
94
+ function toLimitClassName(value) {
95
+ const name = value
96
+ .trim()
97
+ .split(/[\s_-]+/)
98
+ .filter(Boolean)
99
+ .map((word) => (/[A-Z]/.test(word) ? word : word.charAt(0).toUpperCase() + word.slice(1)))
100
+ .join(" ");
101
+ return name ? truncateSchemaString(name, LIMIT_CLASS_MAX_LENGTH) : ALL_LIMIT_CLASS;
102
+ }
51
103
  function resolveReportModelType(stats, window) {
52
104
  if (stats.providerId === "antigravity") {
53
105
  return resolveAntigravityReportModelType(stats, window);
@@ -104,6 +156,9 @@ function resolveReportedUsedPercents(window) {
104
156
  }
105
157
  function shouldReportUsageWindow(window) {
106
158
  return (window.totals.estimatedCreditsStatus !== "unavailable" &&
159
+ // A window whose percentage restarted mid-window pairs a full percentage
160
+ // with partial dollars, which would skew every aggregate built from it.
161
+ !isPartialUsagePairing(window) &&
107
162
  resolveReportedUsedPercents(window) > SKIP_REPORT_USED_PERCENTS);
108
163
  }
109
164
  function clampPercent(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letmecode",
3
- "version": "0.1.30",
3
+ "version": "0.1.33",
4
4
  "description": "Terminal AI usage dashboard for Codex, Claude, Copilot, and Antigravity.",
5
5
  "author": "Devforth (https://devforth.io)",
6
6
  "license": "MIT",