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.
- package/README.md +4 -2
- package/ink-app/dist/index.js +12 -9
- package/ink-app/dist/providers/antigravity/models.js +13 -1
- package/ink-app/dist/providers/antigravity/provider.js +26 -83
- package/ink-app/dist/providers/claude.js +95 -91
- package/ink-app/dist/providers/codex.js +245 -116
- package/ink-app/dist/providers/copilot/models.js +24 -45
- package/ink-app/dist/providers/copilot/provider.js +25 -4
- package/ink-app/dist/providers/copilot/usage/aggregate.js +14 -11
- package/ink-app/dist/providers/limits.js +42 -5
- package/ink-app/dist/providers/pricing.js +116 -16
- package/ink-app/dist/reporting.js +4 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# LetMeCode
|
|
2
2
|
|
|
3
|
-
Terminal AI
|
|
3
|
+
Terminal AI token-usage and API-equivalent value dashboard for Codex, Claude, Copilot, and Antigravity.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Inspect provider limits, locally recorded token usage, daily activity, and model-level API-equivalent cost estimates in a terminal UI. Subscription quota percentages come from the providers; dollar values are estimates at public API rates, not subscription invoices.
|
|
6
|
+
|
|
7
|
+
Only complete current-format usage events are counted. Incomplete legacy Codex and Claude events are silently ignored instead of being priced with guessed defaults.
|
|
6
8
|
|
|
7
9
|
## Quick start
|
|
8
10
|
|
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, selectLatestActiveLimitWindows } from "./providers/limits.js";
|
|
7
|
+
import { estimateLimitFullValue, 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
|
|
@@ -327,7 +327,7 @@ function SummaryPanel(props) {
|
|
|
327
327
|
return (_jsxs(Box, { borderStyle: "round", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Summary" }), _jsxs(Box, { children: [_jsxs(Box, { flexDirection: "column", width: 45, children: [_jsx(DetailRow, { label: "Plan", value: summary.distinctPlanTypes.join(", ") || "none" }), _jsx(DetailRow, { label: "Models", value: summary.distinctModels.join(", ") || "none" }), _jsx(DetailRow, { label: "Period", value: period }), _jsx(Text, { children: " " }), _jsx(Text, { color: "cyan", children: "Usage" }), _jsx(DetailRow, { label: "Events", value: formatInteger(totals.eventCount) }), _jsx(DetailRow, { label: "Input", value: formatOverviewTokenCount(totals.inputTokens) }), _jsx(DetailRow, { label: "Output", value: formatOverviewTokenCount(totals.outputTokens) }), _jsx(DetailRow, { label: "Cache read", value: formatCacheOverviewTokenCount(totals.cacheReadStatus, totals.cacheReadInputTokens) }), _jsx(DetailRow, { label: "Reasoning", value: formatOverviewTokenCount(totals.reasoningOutputTokens) }), _jsx(DetailRow, { label: "Total", value: formatOverviewTokenCount(totals.totalTokens) }), _jsx(DetailRow, { label: "API equiv.", value: formatUsageUsd(totals) })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Efficiency" }), _jsx(DetailRow, { label: "Cache ratio", value: formatPercent(cacheRatio) }), (() => {
|
|
328
328
|
const { label, value } = formatInputOutputRatio(totals, props.stats.providerId);
|
|
329
329
|
return _jsx(DetailRow, { label: label, value: value, padLength: Math.max(14, label.length + 1), noSlice: true });
|
|
330
|
-
})(), _jsx(DetailRow, { label: "Avg/event", value: `${formatOverviewTokenCount(averageTokensPerEvent)} tokens` }), _jsx(DetailRow, { label: "
|
|
330
|
+
})(), _jsx(DetailRow, { label: "Avg/event", value: `${formatOverviewTokenCount(averageTokensPerEvent)} tokens` }), _jsx(DetailRow, { label: "API eq/event", value: formatUnitUsd(costPerEvent) }), _jsx(Text, { children: " " }), _jsx(Text, { color: "cyan", children: "Data source" }), _jsx(DetailRow, { label: "Files", value: formatInteger(summary.filesScanned) }), _jsx(DetailRow, { label: "Lines", value: formatInteger(summary.linesRead) }), _jsx(DetailRow, { label: "Path", value: summary.rootPath })] })] })] }));
|
|
331
331
|
}
|
|
332
332
|
function ContentPanel(props) {
|
|
333
333
|
if (props.providerState.status === "loading") {
|
|
@@ -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, window
|
|
502
|
+
formatLimitFullValueCompact(window.totals, resolveMeasuredUsedPercent(window))
|
|
503
503
|
]
|
|
504
504
|
};
|
|
505
505
|
}
|
|
@@ -543,7 +543,7 @@ 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: "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, 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)}` }), _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" })] }));
|
|
547
547
|
}
|
|
548
548
|
if (props.viewId === "day-to-day-analyses" && props.selectedDayRow) {
|
|
549
549
|
const row = props.selectedDayRow;
|
|
@@ -621,7 +621,7 @@ function formatUsageUsd(totals, modelId) {
|
|
|
621
621
|
: formatUsd(totals.estimatedCredits * CODEX_CREDIT_COST_USD);
|
|
622
622
|
}
|
|
623
623
|
function isInternalUsageModel(modelId) {
|
|
624
|
-
return modelId === "
|
|
624
|
+
return modelId === "<synthetic>";
|
|
625
625
|
}
|
|
626
626
|
function formatUsd(value) {
|
|
627
627
|
const roundedUpValue = value > 0 ? Math.ceil(value * 100) / 100 : value;
|
|
@@ -641,11 +641,10 @@ function formatUsdWhole(value) {
|
|
|
641
641
|
});
|
|
642
642
|
}
|
|
643
643
|
// Extrapolate the limit's full USD value from the observed API-equivalent cost
|
|
644
|
-
// and how much of the limit it represents.
|
|
645
|
-
//
|
|
646
|
-
// percent is missing.
|
|
644
|
+
// and how much of the limit it represents. The percentage must cover the same
|
|
645
|
+
// paired interval as the token totals; returns "-" when either value is unknown.
|
|
647
646
|
function limitFullValueUsd(totals, usedPercent) {
|
|
648
|
-
if (totals.estimatedCreditsStatus === "unavailable") {
|
|
647
|
+
if (totals.estimatedCreditsStatus === "unavailable" || usedPercent === null) {
|
|
649
648
|
return null;
|
|
650
649
|
}
|
|
651
650
|
const usedUsd = totals.estimatedCredits * CODEX_CREDIT_COST_USD;
|
|
@@ -727,6 +726,10 @@ function formatUsedPercentRange(minUsedPercent, maxUsedPercent) {
|
|
|
727
726
|
? fmt(minUsedPercent)
|
|
728
727
|
: `${fmt(minUsedPercent)}–${fmt(maxUsedPercent)}`;
|
|
729
728
|
}
|
|
729
|
+
function formatMeasuredUsedPercent(window) {
|
|
730
|
+
const measuredUsedPercent = resolveMeasuredUsedPercent(window);
|
|
731
|
+
return measuredUsedPercent === null ? "-" : formatPercent(measuredUsedPercent);
|
|
732
|
+
}
|
|
730
733
|
function formatCompactWindowMinutes(value) {
|
|
731
734
|
const hours = value / 60;
|
|
732
735
|
if (hours >= 24) {
|
|
@@ -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
|
-
|
|
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 {
|
|
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) =>
|
|
74
|
+
.filter((row) => row.totals.estimatedCreditsStatus === "unavailable")
|
|
124
75
|
.map((row) => row.modelId);
|
|
125
76
|
if (unknownPricedModels.length > 0) {
|
|
126
|
-
warnings.push(`No Antigravity
|
|
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:
|
|
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:
|
|
275
|
-
? "known"
|
|
276
|
-
: "unavailable"
|
|
226
|
+
estimatedCreditsStatus: estimatedCredits === undefined ? "unavailable" : "known"
|
|
277
227
|
};
|
|
278
228
|
}
|
|
279
|
-
function creditsFor(modelId, record) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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,37 +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 {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
cacheWrite: 3.75,
|
|
25
|
-
cacheWrite5m: 3.75,
|
|
26
|
-
cacheWrite1h: 6,
|
|
27
|
-
output: 15,
|
|
28
|
-
// Intro pricing (~0.66x) through 2026-08-31; reverts to the standard rate above on 2026-09-01.
|
|
29
|
-
introOffer: {
|
|
30
|
-
effectiveUntilMs: Date.UTC(2026, 8, 1),
|
|
31
|
-
rate: { input: 2, cacheRead: 0.2, cacheWrite: 2.5, cacheWrite5m: 2.5, cacheWrite1h: 4, output: 10 }
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
"claude-sonnet-4-6": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
|
|
35
|
-
"claude-sonnet-4-5": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
|
|
36
|
-
"claude-sonnet-4": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
|
|
37
|
-
"claude-haiku-4-5": { input: 1, cacheRead: 0.1, cacheWrite: 1.25, cacheWrite5m: 1.25, cacheWrite1h: 2, output: 5 },
|
|
38
|
-
"claude-haiku-3-5": { input: 0.8, cacheRead: 0.08, cacheWrite: 1, cacheWrite5m: 1, cacheWrite1h: 1.6, output: 4 }
|
|
39
|
-
};
|
|
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
|
+
*/
|
|
40
24
|
const execFileAsync = promisify(execFile);
|
|
41
|
-
const
|
|
25
|
+
const EMPTY_MODEL_PRICING = new Map();
|
|
42
26
|
const VSCODE_CLAUDE_EXTENSION_PREFIX = "anthropic.claude-code-";
|
|
43
27
|
const CLAUDE_SESSION_WINDOW_MINUTES = 5 * 60;
|
|
44
28
|
const CLAUDE_WEEK_WINDOW_MINUTES = 7 * 24 * 60;
|
|
@@ -113,20 +97,28 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
113
97
|
}
|
|
114
98
|
}
|
|
115
99
|
const selectedEvents = [
|
|
116
|
-
...new Set(parsedEvents.keyedEvents.values())
|
|
117
|
-
...parsedEvents.unkeyedEvents
|
|
100
|
+
...new Set(parsedEvents.keyedEvents.values())
|
|
118
101
|
];
|
|
119
102
|
traceClaude(options.traceLogger, [
|
|
120
103
|
`Transcript selection summary: filesWithMatches=${parseTotals.filesScanned}/${parsedSessionFiles.length}`,
|
|
121
104
|
`selectedEvents=${selectedEvents.length}`,
|
|
122
105
|
`duplicateUsageKeys=${parsedEvents.duplicateUsageKeys}`,
|
|
123
|
-
`duplicateUsageKeyCollisions=${parsedEvents.duplicateUsageKeyCollisions}
|
|
124
|
-
`duplicateUnkeyedEvents=${parsedEvents.duplicateUnkeyedEvents}`
|
|
106
|
+
`duplicateUsageKeyCollisions=${parsedEvents.duplicateUsageKeyCollisions}`
|
|
125
107
|
].join(" "));
|
|
126
108
|
if (selectedEvents.length === 0 && parsedSessionFiles.length > 0) {
|
|
127
109
|
traceClaude(options.traceLogger, "No assistant usage events were found in the parsed Claude session files.");
|
|
128
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
|
+
}
|
|
129
120
|
for (const event of selectedEvents) {
|
|
121
|
+
event.totals = usageToTotals(event.modelId, event.usage, event.timestampMs, pricing);
|
|
130
122
|
addModelUsage(byModel, event.modelId, event.totals);
|
|
131
123
|
const planType = typeof event.rateLimits?.plan_type === "string" ? event.rateLimits.plan_type : undefined;
|
|
132
124
|
const safeEventTimeMs = Number.isFinite(event.timestampMs) ? event.timestampMs : 0;
|
|
@@ -143,17 +135,14 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
143
135
|
if (options.verbose && parsedEvents.duplicateUsageKeyCollisions > 0) {
|
|
144
136
|
warnings.push(`Detected ${parsedEvents.duplicateUsageKeyCollisions} Claude usage key collision(s) with different token usage; kept the most complete same-key snapshot to avoid double-counting cumulative snapshots.`);
|
|
145
137
|
}
|
|
146
|
-
if (options.verbose && parsedEvents.duplicateUnkeyedEvents > 0) {
|
|
147
|
-
warnings.push(`Collapsed ${parsedEvents.duplicateUnkeyedEvents} adjacent duplicate unkeyed Claude usage event(s) by usage signature.`);
|
|
148
|
-
}
|
|
149
138
|
const modelUsage = [...byModel.entries()]
|
|
150
139
|
.map(([modelId, totals]) => ({ modelId, totals }))
|
|
151
140
|
.sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
|
|
152
141
|
const unknownPricedModels = modelUsage
|
|
153
|
-
.
|
|
154
|
-
.
|
|
142
|
+
.filter((row) => row.totals.estimatedCreditsStatus === "unavailable")
|
|
143
|
+
.map((row) => row.modelId);
|
|
155
144
|
if (unknownPricedModels.length > 0) {
|
|
156
|
-
warnings.push(`No
|
|
145
|
+
warnings.push(`No complete API-equivalent pricing returned for: ${unknownPricedModels.join(", ")}.`);
|
|
157
146
|
}
|
|
158
147
|
if (parsedSessionFiles.length === 0) {
|
|
159
148
|
warnings.push(`No Claude session files found under ${sessionsRoot}.`);
|
|
@@ -216,46 +205,53 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
216
205
|
function normalizeUsage(value) {
|
|
217
206
|
const usage = asRecord(value) ?? {};
|
|
218
207
|
const cacheCreation = asRecord(usage.cache_creation);
|
|
208
|
+
const serverToolUse = asRecord(usage.server_tool_use);
|
|
219
209
|
const cacheCreation5mInputTokens = numberOrZero(cacheCreation?.ephemeral_5m_input_tokens);
|
|
220
210
|
const cacheCreation1hInputTokens = numberOrZero(cacheCreation?.ephemeral_1h_input_tokens);
|
|
221
|
-
const cacheCreationInputTokens = Math.max(numberOrZero(usage.cache_creation_input_tokens), cacheCreation5mInputTokens + cacheCreation1hInputTokens);
|
|
222
211
|
return {
|
|
223
212
|
inputTokens: numberOrZero(usage.input_tokens),
|
|
224
213
|
cacheReadInputTokens: numberOrZero(usage.cache_read_input_tokens),
|
|
225
|
-
cacheCreationInputTokens,
|
|
226
214
|
cacheCreation5mInputTokens,
|
|
227
215
|
cacheCreation1hInputTokens,
|
|
228
216
|
outputTokens: numberOrZero(usage.output_tokens),
|
|
229
|
-
inferenceGeo: String(usage.inference_geo ?? "")
|
|
217
|
+
inferenceGeo: String(usage.inference_geo ?? ""),
|
|
218
|
+
speed: String(usage.speed ?? ""),
|
|
219
|
+
webSearchRequests: numberOrZero(serverToolUse?.web_search_requests)
|
|
230
220
|
};
|
|
231
221
|
}
|
|
232
|
-
function resolveRate(modelId, timestampMs) {
|
|
233
|
-
return resolveUsageRate(RATE_CARD, modelId, 0, { prefixMatch: true, timestampMs });
|
|
234
|
-
}
|
|
235
222
|
function isInternalClaudeModel(modelId) {
|
|
236
223
|
return modelId === "<synthetic>";
|
|
237
224
|
}
|
|
238
|
-
function creditsFor(modelId, usage,
|
|
239
|
-
const rate = resolveRate(modelId, timestampMs);
|
|
240
|
-
if (!rate) {
|
|
241
|
-
return 0;
|
|
242
|
-
}
|
|
225
|
+
function creditsFor(modelId, usage, pricing) {
|
|
243
226
|
const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
|
|
244
227
|
// The US inference surcharge must match regardless of the casing the source
|
|
245
228
|
// reports (e.g. "us", "US"), so compare case-insensitively.
|
|
246
229
|
const inferenceMultiplier = usage.inferenceGeo.trim().toLowerCase() === "us" ? 1.1 : 1;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
230
|
+
const speedMultiplier = usage.speed.trim().toLowerCase() === "fast" && isClaudeFastPricedModel(modelId)
|
|
231
|
+
? 2
|
|
232
|
+
: 1;
|
|
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);
|
|
245
|
+
}
|
|
246
|
+
function isClaudeFastPricedModel(modelId) {
|
|
247
|
+
return ["claude-opus-5", "claude-opus-4-8"].some((candidate) => modelId === candidate || modelId.startsWith(`${candidate}-`));
|
|
254
248
|
}
|
|
255
|
-
function usageToTotals(modelId, usage,
|
|
249
|
+
function usageToTotals(modelId, usage, _timestampMs, pricing = EMPTY_MODEL_PRICING) {
|
|
256
250
|
const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
|
|
257
251
|
const cacheWriteInputTokens = cacheWriteBreakdown.cacheWrite5mInputTokens +
|
|
258
252
|
cacheWriteBreakdown.cacheWrite1hInputTokens;
|
|
253
|
+
const estimatedCredits = creditsFor(modelId, usage, pricing);
|
|
254
|
+
const rateKnown = estimatedCredits !== undefined || isInternalClaudeModel(modelId);
|
|
259
255
|
return {
|
|
260
256
|
inputTokens: usage.inputTokens,
|
|
261
257
|
outputTokens: usage.outputTokens,
|
|
@@ -268,8 +264,9 @@ function usageToTotals(modelId, usage, timestampMs) {
|
|
|
268
264
|
usage.cacheReadInputTokens +
|
|
269
265
|
cacheWriteInputTokens +
|
|
270
266
|
usage.outputTokens,
|
|
271
|
-
estimatedCredits:
|
|
272
|
-
eventCount: 1
|
|
267
|
+
estimatedCredits: estimatedCredits ?? 0,
|
|
268
|
+
eventCount: 1,
|
|
269
|
+
estimatedCreditsStatus: rateKnown ? "known" : "unavailable"
|
|
273
270
|
};
|
|
274
271
|
}
|
|
275
272
|
function addModelUsage(byModel, modelId, deltaTotals) {
|
|
@@ -279,10 +276,8 @@ function addModelUsage(byModel, modelId, deltaTotals) {
|
|
|
279
276
|
byModel.set(resolvedModelId, totals);
|
|
280
277
|
}
|
|
281
278
|
function resolveClaudeCacheWriteBreakdown(usage) {
|
|
282
|
-
const cacheWriteKnownTokens = usage.cacheCreation5mInputTokens + usage.cacheCreation1hInputTokens;
|
|
283
|
-
const cacheWriteFallbackTokens = Math.max(0, usage.cacheCreationInputTokens - cacheWriteKnownTokens);
|
|
284
279
|
return {
|
|
285
|
-
cacheWrite5mInputTokens: usage.cacheCreation5mInputTokens
|
|
280
|
+
cacheWrite5mInputTokens: usage.cacheCreation5mInputTokens,
|
|
286
281
|
cacheWrite1hInputTokens: usage.cacheCreation1hInputTokens
|
|
287
282
|
};
|
|
288
283
|
}
|
|
@@ -437,7 +432,7 @@ async function parseSessionFile(filePath) {
|
|
|
437
432
|
}
|
|
438
433
|
const message = asRecord(payloadObject.message);
|
|
439
434
|
const usage = asRecord(message?.usage);
|
|
440
|
-
if (!usage) {
|
|
435
|
+
if (!usage || !isCurrentClaudeUsageEvent(payloadObject, message, usage)) {
|
|
441
436
|
continue;
|
|
442
437
|
}
|
|
443
438
|
const modelId = String(message?.model ?? "unknown");
|
|
@@ -467,6 +462,21 @@ async function parseSessionFile(filePath) {
|
|
|
467
462
|
events
|
|
468
463
|
};
|
|
469
464
|
}
|
|
465
|
+
function isCurrentClaudeUsageEvent(payloadObject, message, usage) {
|
|
466
|
+
return (typeof payloadObject.version === "string" &&
|
|
467
|
+
payloadObject.version.length > 0 &&
|
|
468
|
+
typeof payloadObject.sessionId === "string" &&
|
|
469
|
+
payloadObject.sessionId.length > 0 &&
|
|
470
|
+
typeof payloadObject.requestId === "string" &&
|
|
471
|
+
payloadObject.requestId.length > 0 &&
|
|
472
|
+
typeof message?.id === "string" &&
|
|
473
|
+
message.id.length > 0 &&
|
|
474
|
+
typeof usage.service_tier === "string" &&
|
|
475
|
+
typeof usage.speed === "string" &&
|
|
476
|
+
typeof usage.inference_geo === "string" &&
|
|
477
|
+
asRecord(usage.cache_creation) !== null &&
|
|
478
|
+
asRecord(usage.server_tool_use) !== null);
|
|
479
|
+
}
|
|
470
480
|
function buildUsageEventKeys(payloadObject, message) {
|
|
471
481
|
const sessionId = String(payloadObject.sessionId ?? "");
|
|
472
482
|
const requestId = typeof payloadObject.requestId === "string" ? payloadObject.requestId : "";
|
|
@@ -484,22 +494,20 @@ function buildUsageSignatureFromParts(sessionId, modelId, usage) {
|
|
|
484
494
|
sessionId,
|
|
485
495
|
modelId,
|
|
486
496
|
usage.inputTokens,
|
|
487
|
-
usage.cacheCreationInputTokens,
|
|
488
497
|
usage.cacheCreation5mInputTokens,
|
|
489
498
|
usage.cacheCreation1hInputTokens,
|
|
490
499
|
usage.cacheReadInputTokens,
|
|
491
500
|
usage.outputTokens,
|
|
492
|
-
usage.inferenceGeo
|
|
501
|
+
usage.inferenceGeo,
|
|
502
|
+
usage.speed,
|
|
503
|
+
usage.webSearchRequests
|
|
493
504
|
].join("|");
|
|
494
505
|
}
|
|
495
506
|
function createParsedUsageEventAccumulator() {
|
|
496
507
|
return {
|
|
497
508
|
keyedEvents: new Map(),
|
|
498
|
-
unkeyedEvents: [],
|
|
499
|
-
lastUnkeyedEventsBySignature: new Map(),
|
|
500
509
|
duplicateUsageKeys: 0,
|
|
501
|
-
duplicateUsageKeyCollisions: 0
|
|
502
|
-
duplicateUnkeyedEvents: 0
|
|
510
|
+
duplicateUsageKeyCollisions: 0
|
|
503
511
|
};
|
|
504
512
|
}
|
|
505
513
|
function recordParsedUsageEvent(parsedEvents, event) {
|
|
@@ -527,23 +535,6 @@ function recordParsedUsageEvent(parsedEvents, event) {
|
|
|
527
535
|
}
|
|
528
536
|
return;
|
|
529
537
|
}
|
|
530
|
-
const previousRecord = parsedEvents.lastUnkeyedEventsBySignature.get(event.usageSignature);
|
|
531
|
-
if (!previousRecord || !canCollapseAdjacentUnkeyedUsageEvents(previousRecord.event, event)) {
|
|
532
|
-
parsedEvents.unkeyedEvents.push(event);
|
|
533
|
-
parsedEvents.lastUnkeyedEventsBySignature.set(event.usageSignature, {
|
|
534
|
-
event,
|
|
535
|
-
index: parsedEvents.unkeyedEvents.length - 1
|
|
536
|
-
});
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
parsedEvents.duplicateUnkeyedEvents += 1;
|
|
540
|
-
if (normalizeTimestamp(event.timestampMs) > normalizeTimestamp(previousRecord.event.timestampMs)) {
|
|
541
|
-
parsedEvents.unkeyedEvents[previousRecord.index] = event;
|
|
542
|
-
parsedEvents.lastUnkeyedEventsBySignature.set(event.usageSignature, {
|
|
543
|
-
event,
|
|
544
|
-
index: previousRecord.index
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
538
|
}
|
|
548
539
|
function mergeParsedUsageEvents(previous, next) {
|
|
549
540
|
// Same-key events are repeated/streamed snapshots of one logical request. Rather than
|
|
@@ -595,9 +586,6 @@ function selectMergedEventModelId(primary, other) {
|
|
|
595
586
|
}
|
|
596
587
|
return primary.modelId;
|
|
597
588
|
}
|
|
598
|
-
function canCollapseAdjacentUnkeyedUsageEvents(previous, next) {
|
|
599
|
-
return previous.filePath === next.filePath && next.lineNumber === previous.lineNumber + 1;
|
|
600
|
-
}
|
|
601
589
|
function extractUsageKeySessionId(usageKeys) {
|
|
602
590
|
const usageKey = usageKeys[0];
|
|
603
591
|
if (!usageKey) {
|
|
@@ -1069,6 +1057,19 @@ function parseLiveUsageWindowSnapshots(usageOutput, now) {
|
|
|
1069
1057
|
resetMsByLabel.set(parsed.label, resetsAtMs);
|
|
1070
1058
|
}
|
|
1071
1059
|
}
|
|
1060
|
+
// Claude omits reset timestamps while an entire bucket is displayed as 0%.
|
|
1061
|
+
// Keep that authoritative zero instead of silently dropping the window. The
|
|
1062
|
+
// provisional range starts at the sample time, so no earlier transcript
|
|
1063
|
+
// events are falsely paired with the zero-percent measurement.
|
|
1064
|
+
for (const label of ["session", "week"]) {
|
|
1065
|
+
const sameLabelLines = parsedLines.filter((parsed) => parsed.label === label);
|
|
1066
|
+
if (!resetMsByLabel.has(label) &&
|
|
1067
|
+
sameLabelLines.length > 0 &&
|
|
1068
|
+
sameLabelLines.every((parsed) => parsed.usedPercent === 0)) {
|
|
1069
|
+
const windowMinutes = label === "session" ? CLAUDE_SESSION_WINDOW_MINUTES : CLAUDE_WEEK_WINDOW_MINUTES;
|
|
1070
|
+
resetMsByLabel.set(label, now.getTime() + windowMinutes * 60000);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1072
1073
|
const snapshots = new Map();
|
|
1073
1074
|
for (const parsed of parsedLines) {
|
|
1074
1075
|
const resetsAtMs = resetMsByLabel.get(parsed.label) ?? null;
|
|
@@ -1163,6 +1164,9 @@ function buildLiveLimitWindowRow(snapshot, planType, selectedEvents, now) {
|
|
|
1163
1164
|
lastSeenUtcIso: toUtcIso(Number.isFinite(lastSeenMs) ? lastSeenMs : fallbackLastSeenMs),
|
|
1164
1165
|
minUsedPercent: snapshot.usedPercent,
|
|
1165
1166
|
maxUsedPercent: snapshot.usedPercent,
|
|
1167
|
+
measuredUsedPercent: snapshot.usedPercent > 0 && totals.eventCount === 0
|
|
1168
|
+
? null
|
|
1169
|
+
: snapshot.usedPercent,
|
|
1166
1170
|
totals,
|
|
1167
1171
|
modelUsage: buildModelUsageRowsForEvents(inWindowEvents),
|
|
1168
1172
|
eventCount: totals.eventCount
|