letmecode 0.1.32 → 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.
package/ink-app/dist/index.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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(
|
|
658
|
-
const estimate = limitFullValueUsd(totals,
|
|
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
|
-
|
|
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(
|
|
741
|
+
.map(buildRow);
|
|
677
742
|
const secondaryLimitWindows = snapshots
|
|
678
743
|
.filter((snapshot) => snapshot.scope === "secondary")
|
|
679
|
-
.map(
|
|
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
|
-
|
|
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 >=
|
|
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
|
-
|
|
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,
|
|
@@ -242,6 +242,18 @@ export function resolveMeasuredUsedPercent(window) {
|
|
|
242
242
|
}
|
|
243
243
|
return clampUsedPercent(window.maxUsedPercent - window.minUsedPercent);
|
|
244
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
|
+
}
|
|
245
257
|
function clampUsedPercent(value) {
|
|
246
258
|
if (!Number.isFinite(value)) {
|
|
247
259
|
return 0;
|
|
@@ -2,7 +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
|
+
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
|
|
@@ -156,6 +156,9 @@ function resolveReportedUsedPercents(window) {
|
|
|
156
156
|
}
|
|
157
157
|
function shouldReportUsageWindow(window) {
|
|
158
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) &&
|
|
159
162
|
resolveReportedUsedPercents(window) > SKIP_REPORT_USED_PERCENTS);
|
|
160
163
|
}
|
|
161
164
|
function clampPercent(value) {
|