letmecode 0.1.0 → 0.1.1
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 +138 -23
- package/ink-app/dist/providers/claude.js +225 -0
- package/ink-app/dist/providers/codex.js +13 -106
- package/ink-app/dist/providers/contract.js +17 -0
- package/ink-app/dist/providers/index.js +3 -1
- package/ink-app/dist/providers/limits.js +146 -0
- package/package.json +1 -1
package/ink-app/dist/index.js
CHANGED
|
@@ -1,17 +1,45 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React, { useEffect, useState } from "react";
|
|
3
3
|
import { Box, Text, useApp, useInput, render } from "ink";
|
|
4
4
|
import { createProviders } from "./providers/index.js";
|
|
5
5
|
const VERTICAL_TABS = [
|
|
6
|
-
{ id: "limit-windows", label: "
|
|
7
|
-
{ id: "
|
|
6
|
+
{ id: "limit-windows", label: "Limits" },
|
|
7
|
+
{ id: "summary", label: "Summary" },
|
|
8
|
+
{ id: "usage-by-model", label: "by model" }
|
|
8
9
|
];
|
|
10
|
+
const CODEX_CREDIT_COST_USD = 0.01;
|
|
11
|
+
const LIMIT_WINDOW_COLUMNS = {
|
|
12
|
+
plan: 8,
|
|
13
|
+
window: 8,
|
|
14
|
+
used: 10,
|
|
15
|
+
date: 17,
|
|
16
|
+
value: 10
|
|
17
|
+
};
|
|
18
|
+
const MODEL_USAGE_COLUMNS = {
|
|
19
|
+
model: 17,
|
|
20
|
+
input: 12,
|
|
21
|
+
cached: 12,
|
|
22
|
+
nonCached: 12,
|
|
23
|
+
output: 11,
|
|
24
|
+
credits: 12,
|
|
25
|
+
value: 12
|
|
26
|
+
};
|
|
9
27
|
function App() {
|
|
10
28
|
const { exit } = useApp();
|
|
11
29
|
const providers = React.useState(() => createProviders())[0];
|
|
12
30
|
const [providerStates, setProviderStates] = useState(providers.map((provider) => ({ provider, status: "loading" })));
|
|
13
31
|
const [selectedProviderIndex, setSelectedProviderIndex] = useState(0);
|
|
14
32
|
const [selectedVerticalTabIndex, setSelectedVerticalTabIndex] = useState(0);
|
|
33
|
+
const [selectedLimitRowIndex, setSelectedLimitRowIndex] = useState(0);
|
|
34
|
+
const [selectedModelRowIndex, setSelectedModelRowIndex] = useState(0);
|
|
35
|
+
const selectedProvider = providerStates[selectedProviderIndex];
|
|
36
|
+
const selectedVerticalTab = VERTICAL_TABS[selectedVerticalTabIndex];
|
|
37
|
+
const limitRows = getLimitRows(selectedProvider);
|
|
38
|
+
const modelRows = getModelRows(selectedProvider);
|
|
39
|
+
const activeLimitRowIndex = clampSelectionIndex(selectedLimitRowIndex, limitRows.length);
|
|
40
|
+
const activeModelRowIndex = clampSelectionIndex(selectedModelRowIndex, modelRows.length);
|
|
41
|
+
const selectedLimitRow = activeLimitRowIndex >= 0 ? limitRows[activeLimitRowIndex] : undefined;
|
|
42
|
+
const selectedModelRow = activeModelRowIndex >= 0 ? modelRows[activeModelRowIndex] : undefined;
|
|
15
43
|
useEffect(() => {
|
|
16
44
|
let cancelled = false;
|
|
17
45
|
for (const provider of providers) {
|
|
@@ -44,6 +72,26 @@ function App() {
|
|
|
44
72
|
exit();
|
|
45
73
|
return;
|
|
46
74
|
}
|
|
75
|
+
if ((key.ctrl || key.shift) && key.downArrow) {
|
|
76
|
+
if (selectedVerticalTab.id === "limit-windows") {
|
|
77
|
+
setSelectedLimitRowIndex(clampSelectionIndex(activeLimitRowIndex + 1, limitRows.length));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (selectedVerticalTab.id === "usage-by-model") {
|
|
81
|
+
setSelectedModelRowIndex(clampSelectionIndex(activeModelRowIndex + 1, modelRows.length));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if ((key.ctrl || key.shift) && key.upArrow) {
|
|
86
|
+
if (selectedVerticalTab.id === "limit-windows") {
|
|
87
|
+
setSelectedLimitRowIndex(clampSelectionIndex(activeLimitRowIndex - 1, limitRows.length));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (selectedVerticalTab.id === "usage-by-model") {
|
|
91
|
+
setSelectedModelRowIndex(clampSelectionIndex(activeModelRowIndex - 1, modelRows.length));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
47
95
|
if ((input === "\t" && !key.shift) || key.rightArrow) {
|
|
48
96
|
setSelectedProviderIndex((current) => (current + 1) % providerStates.length);
|
|
49
97
|
return;
|
|
@@ -60,9 +108,7 @@ function App() {
|
|
|
60
108
|
setSelectedVerticalTabIndex((current) => (current - 1 + VERTICAL_TABS.length) % VERTICAL_TABS.length);
|
|
61
109
|
}
|
|
62
110
|
});
|
|
63
|
-
|
|
64
|
-
const selectedVerticalTab = VERTICAL_TABS[selectedVerticalTabIndex];
|
|
65
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "letmecode usage dashboard" }), _jsx(Text, { color: "gray", children: "tab/shift+tab or left/right to switch providers, j/k or up/down for details, q to quit" }), _jsx(Box, { marginTop: 1, children: providerStates.map((state, index) => (_jsx(ProviderTab, { label: state.provider.label, active: index === selectedProviderIndex, status: state.status }, state.provider.id))) }), _jsx(Box, { marginTop: 1, borderStyle: "round", paddingX: 1, paddingY: 0, flexDirection: "column", children: _jsx(SummarySection, { providerState: selectedProvider }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Box, { flexDirection: "column", width: 22, marginRight: 2, children: VERTICAL_TABS.map((tab, index) => (_jsx(VerticalTab, { label: tab.label, active: index === selectedVerticalTabIndex }, tab.id))) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsx(ContentPanel, { providerState: selectedProvider, tabId: selectedVerticalTab.id }) })] }), selectedProvider.status === "ready" && selectedProvider.stats.warnings.length > 0 ? (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "Warnings" }), selectedProvider.stats.warnings.map((warning) => (_jsx(Text, { children: warning }, warning)))] })) : null] }));
|
|
111
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "letmecode usage dashboard" }), _jsx(Text, { color: "gray", children: "tab/shift+tab or left/right to switch providers, j/k or up/down for details, ctrl+up/down or shift+up/down to select a row, q to quit" }), _jsx(Box, { marginTop: 1, children: providerStates.map((state, index) => (_jsx(ProviderTab, { label: state.provider.label, active: index === selectedProviderIndex, status: state.status }, state.provider.id))) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Box, { flexDirection: "column", width: 12, marginRight: 2, children: VERTICAL_TABS.map((tab, index) => (_jsx(VerticalTab, { label: tab.label, active: index === selectedVerticalTabIndex }, tab.id))) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsx(ContentPanel, { providerState: selectedProvider, tabId: selectedVerticalTab.id, selectedLimitRowKey: selectedLimitRow ? getLimitRowKey(selectedLimitRow) : undefined, selectedModelId: selectedModelRow?.modelId }) })] }), _jsx(SelectionDetailsPanel, { providerState: selectedProvider, tabId: selectedVerticalTab.id, selectedLimitRow: selectedLimitRow, selectedModelRow: selectedModelRow }), selectedProvider.status === "ready" && selectedProvider.stats.warnings.length > 0 ? (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "Warnings" }), selectedProvider.stats.warnings.map((warning) => (_jsx(Text, { children: warning }, warning)))] })) : null] }));
|
|
66
112
|
}
|
|
67
113
|
function ProviderTab(props) {
|
|
68
114
|
const statusColor = props.status === "error" ? "red" : props.status === "loading" ? "yellow" : "green";
|
|
@@ -72,15 +118,10 @@ function ProviderTab(props) {
|
|
|
72
118
|
function VerticalTab(props) {
|
|
73
119
|
return (_jsx(Text, { inverse: props.active, children: props.active ? ` ${props.label} ` : ` ${props.label}` }));
|
|
74
120
|
}
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
if (props.providerState.status === "error") {
|
|
80
|
-
return (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: props.providerState.provider.label }), _jsxs(Text, { color: "red", children: ["Failed to load provider stats: ", props.providerState.errorMessage] })] }));
|
|
81
|
-
}
|
|
82
|
-
const { summary } = props.providerState.stats;
|
|
83
|
-
return (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: props.providerState.stats.providerLabel }), _jsxs(Text, { children: ["root: ", summary.rootLabel, " (", summary.rootPath, ")"] }), _jsxs(Text, { children: ["files: ", formatInteger(summary.filesScanned), " lines: ", formatInteger(summary.linesRead), " token events: ", formatInteger(summary.tokenEvents)] }), _jsxs(Text, { children: ["input: ", formatInteger(summary.totals.inputTokens), " cached: ", formatInteger(summary.totals.cachedInputTokens), " non-cached: ", formatInteger(summary.totals.nonCachedInputTokens)] }), _jsxs(Text, { children: ["output: ", formatInteger(summary.totals.outputTokens), " reasoning: ", formatInteger(summary.totals.reasoningOutputTokens), " total: ", formatInteger(summary.totals.totalTokens)] }), _jsxs(Text, { children: ["estimated credits: ", formatCredits(summary.totals.estimatedCredits), " models: ", summary.distinctModels.join(", ") || "none", " plans: ", summary.distinctPlanTypes.join(", ") || "none"] })] }));
|
|
121
|
+
function SummaryPanel(props) {
|
|
122
|
+
const { summary } = props.stats;
|
|
123
|
+
const inputPerOutput = formatInputPerOutput(summary.totals);
|
|
124
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: props.stats.providerLabel }), _jsxs(Text, { children: ["root: ", summary.rootLabel, " (", summary.rootPath, ")"] }), _jsxs(Text, { children: ["files: ", formatInteger(summary.filesScanned), " lines: ", formatInteger(summary.linesRead), " token events: ", formatInteger(summary.tokenEvents)] }), _jsxs(Text, { children: ["input: ", formatInteger(summary.totals.inputTokens), " cached: ", formatInteger(summary.totals.cachedInputTokens), " non-cached: ", formatInteger(summary.totals.nonCachedInputTokens)] }), _jsxs(Text, { children: ["output: ", formatInteger(summary.totals.outputTokens), " reasoning: ", formatInteger(summary.totals.reasoningOutputTokens), " total: ", formatInteger(summary.totals.totalTokens)] }), _jsxs(Text, { children: ["estimated credits: ", formatCredits(summary.totals.estimatedCredits)] }), _jsxs(Text, { children: ["IpO: ", inputPerOutput.cached, ":", inputPerOutput.nonCached, ":", inputPerOutput.output] }), _jsxs(Text, { children: ["models: ", summary.distinctModels.join(", ") || "none"] }), _jsxs(Text, { children: ["plans: ", summary.distinctPlanTypes.join(", ") || "none"] })] }));
|
|
84
125
|
}
|
|
85
126
|
function ContentPanel(props) {
|
|
86
127
|
if (props.providerState.status === "loading") {
|
|
@@ -90,21 +131,25 @@ function ContentPanel(props) {
|
|
|
90
131
|
return _jsxs(Text, { color: "red", children: ["Provider error: ", props.providerState.errorMessage] });
|
|
91
132
|
}
|
|
92
133
|
if (props.tabId === "limit-windows") {
|
|
93
|
-
return _jsx(LimitWindowsPanel, { stats: props.providerState.stats });
|
|
134
|
+
return _jsx(LimitWindowsPanel, { stats: props.providerState.stats, selectedRowKey: props.selectedLimitRowKey });
|
|
94
135
|
}
|
|
95
|
-
|
|
136
|
+
if (props.tabId === "summary") {
|
|
137
|
+
return _jsx(SummaryPanel, { stats: props.providerState.stats });
|
|
138
|
+
}
|
|
139
|
+
return _jsx(UsageByModelPanel, { stats: props.providerState.stats, selectedModelId: props.selectedModelId });
|
|
96
140
|
}
|
|
97
141
|
function LimitWindowsPanel(props) {
|
|
98
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Primary" }), _jsx(LimitWindowSection, { windows: props.stats.primaryLimitWindows }), _jsx(Box, { marginTop: 1 }), _jsx(Text, { bold: true, children: "Secondary" }), _jsx(LimitWindowSection, { windows: props.stats.secondaryLimitWindows })] }));
|
|
142
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Primary" }), _jsx(LimitWindowSection, { windows: props.stats.primaryLimitWindows, selectedRowKey: props.selectedRowKey }), _jsx(Box, { marginTop: 1 }), _jsx(Text, { bold: true, children: "Secondary" }), _jsx(LimitWindowSection, { windows: props.stats.secondaryLimitWindows, selectedRowKey: props.selectedRowKey })] }));
|
|
99
143
|
}
|
|
100
144
|
function LimitWindowSection(props) {
|
|
101
145
|
if (props.windows.length === 0) {
|
|
102
146
|
return _jsx(Text, { color: "gray", children: "No windows found." });
|
|
103
147
|
}
|
|
104
|
-
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
148
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "gray", children: [pad("plan", LIMIT_WINDOW_COLUMNS.plan), " ", pad("window", LIMIT_WINDOW_COLUMNS.window), " ", pad("used", LIMIT_WINDOW_COLUMNS.used), " ", pad("start", LIMIT_WINDOW_COLUMNS.date), " ", pad("end", LIMIT_WINDOW_COLUMNS.date), " value"] }), props.windows.map((window) => {
|
|
105
149
|
const windowLabel = formatWindowMinutes(window.windowMinutes);
|
|
106
150
|
const usedLabel = `${window.minUsedPercent}%->${window.maxUsedPercent}%`;
|
|
107
|
-
|
|
151
|
+
const isSelected = props.selectedRowKey === getLimitRowKey(window);
|
|
152
|
+
return (_jsxs(Text, { inverse: isSelected, color: isSelected ? "cyan" : undefined, children: [pad(window.planType, LIMIT_WINDOW_COLUMNS.plan), " ", pad(windowLabel, LIMIT_WINDOW_COLUMNS.window), " ", pad(usedLabel, LIMIT_WINDOW_COLUMNS.used), " ", pad(formatLocalDateTime(window.startTimeUtcIso), LIMIT_WINDOW_COLUMNS.date), " ", pad(formatLocalDateTime(window.endTimeUtcIso), LIMIT_WINDOW_COLUMNS.date), " ", pad(formatUsd(window.totals.estimatedCredits * CODEX_CREDIT_COST_USD), LIMIT_WINDOW_COLUMNS.value)] }, getLimitRowKey(window)));
|
|
108
153
|
})] }));
|
|
109
154
|
}
|
|
110
155
|
function UsageByModelPanel(props) {
|
|
@@ -112,7 +157,28 @@ function UsageByModelPanel(props) {
|
|
|
112
157
|
return _jsx(Text, { color: "gray", children: "No model usage found." });
|
|
113
158
|
}
|
|
114
159
|
const totals = props.stats.summary.totals;
|
|
115
|
-
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
160
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "gray", children: [pad("model", MODEL_USAGE_COLUMNS.model), " ", pad("input", MODEL_USAGE_COLUMNS.input), " ", pad("cached", MODEL_USAGE_COLUMNS.cached), " ", pad("non-cached", MODEL_USAGE_COLUMNS.nonCached), " ", pad("output", MODEL_USAGE_COLUMNS.output), " ", pad("credits", MODEL_USAGE_COLUMNS.credits), " value"] }), props.stats.modelUsage.map((row) => {
|
|
161
|
+
const isSelected = props.selectedModelId === row.modelId;
|
|
162
|
+
return (_jsxs(Text, { inverse: isSelected, color: isSelected ? "cyan" : undefined, children: [pad(row.modelId, MODEL_USAGE_COLUMNS.model), " ", pad(formatInteger(row.totals.inputTokens), MODEL_USAGE_COLUMNS.input), " ", pad(formatInteger(row.totals.cachedInputTokens), MODEL_USAGE_COLUMNS.cached), " ", pad(formatInteger(row.totals.nonCachedInputTokens), MODEL_USAGE_COLUMNS.nonCached), " ", pad(formatInteger(row.totals.outputTokens), MODEL_USAGE_COLUMNS.output), " ", pad(formatCredits(row.totals.estimatedCredits), MODEL_USAGE_COLUMNS.credits), " ", pad(formatUsd(row.totals.estimatedCredits * CODEX_CREDIT_COST_USD), MODEL_USAGE_COLUMNS.value)] }, row.modelId));
|
|
163
|
+
}), _jsxs(Text, { color: "cyan", children: [pad("TOTAL", MODEL_USAGE_COLUMNS.model), " ", pad(formatInteger(totals.inputTokens), MODEL_USAGE_COLUMNS.input), " ", pad(formatInteger(totals.cachedInputTokens), MODEL_USAGE_COLUMNS.cached), " ", pad(formatInteger(totals.nonCachedInputTokens), MODEL_USAGE_COLUMNS.nonCached), " ", pad(formatInteger(totals.outputTokens), MODEL_USAGE_COLUMNS.output), " ", pad(formatCredits(totals.estimatedCredits), MODEL_USAGE_COLUMNS.credits), " ", pad(formatUsd(totals.estimatedCredits * CODEX_CREDIT_COST_USD), MODEL_USAGE_COLUMNS.value)] })] }));
|
|
164
|
+
}
|
|
165
|
+
function SelectionDetailsPanel(props) {
|
|
166
|
+
if (props.providerState.status !== "ready") {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
if (props.tabId === "limit-windows" && props.selectedLimitRow) {
|
|
170
|
+
const row = props.selectedLimitRow;
|
|
171
|
+
return (_jsxs(Box, { marginTop: 1, borderStyle: "round", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Limit details" }), _jsxs(Text, { children: [row.scope, " plan: ", row.planType, " window: ", formatWindowMinutes(row.windowMinutes), " used: ", row.minUsedPercent, "%", "->", row.maxUsedPercent, "% limit: ", row.limitId] }), _jsxs(Text, { children: ["range: ", formatLocalDateTime(row.startTimeUtcIso), " ", "->", " ", formatLocalDateTime(row.endTimeUtcIso), " events: ", formatInteger(row.eventCount)] }), _jsx(UsageTotalsDetails, { totals: row.totals })] }));
|
|
172
|
+
}
|
|
173
|
+
if (props.tabId === "usage-by-model" && props.selectedModelRow) {
|
|
174
|
+
return (_jsxs(Box, { marginTop: 1, borderStyle: "round", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Model details" }), _jsxs(Text, { children: ["model: ", props.selectedModelRow.modelId, " events: ", formatInteger(props.selectedModelRow.totals.eventCount)] }), _jsx(UsageTotalsDetails, { totals: props.selectedModelRow.totals })] }));
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
function UsageTotalsDetails(props) {
|
|
179
|
+
const { totals } = props;
|
|
180
|
+
const inputPerOutput = formatInputPerOutput(totals);
|
|
181
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Total credits burned: ", formatCredits(totals.estimatedCredits)] }), _jsxs(Text, { children: ["Credits Value (@ $0.01/credit): ", formatUsd(totals.estimatedCredits * CODEX_CREDIT_COST_USD)] }), _jsxs(Text, { children: ["IpO: ", inputPerOutput.cached, ":", inputPerOutput.nonCached, ":", inputPerOutput.output] })] }));
|
|
116
182
|
}
|
|
117
183
|
function formatInteger(value) {
|
|
118
184
|
return Math.round(value).toLocaleString("en-US");
|
|
@@ -123,6 +189,14 @@ function formatCredits(value) {
|
|
|
123
189
|
maximumFractionDigits: 2
|
|
124
190
|
});
|
|
125
191
|
}
|
|
192
|
+
function formatUsd(value) {
|
|
193
|
+
return value.toLocaleString("en-US", {
|
|
194
|
+
currency: "USD",
|
|
195
|
+
style: "currency",
|
|
196
|
+
minimumFractionDigits: 2,
|
|
197
|
+
maximumFractionDigits: 4
|
|
198
|
+
});
|
|
199
|
+
}
|
|
126
200
|
function formatWindowMinutes(value) {
|
|
127
201
|
const hours = value / 60;
|
|
128
202
|
if (hours >= 24) {
|
|
@@ -130,12 +204,53 @@ function formatWindowMinutes(value) {
|
|
|
130
204
|
}
|
|
131
205
|
return `${hours.toFixed(2)}h`;
|
|
132
206
|
}
|
|
133
|
-
function
|
|
134
|
-
|
|
207
|
+
function formatLocalDateTime(value) {
|
|
208
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
209
|
+
month: "short",
|
|
210
|
+
day: "2-digit",
|
|
211
|
+
year: "2-digit",
|
|
212
|
+
hour: "2-digit",
|
|
213
|
+
minute: "2-digit",
|
|
214
|
+
hour12: false,
|
|
215
|
+
hourCycle: "h23"
|
|
216
|
+
}).formatToParts(new Date(value));
|
|
217
|
+
const lookup = Object.fromEntries(parts.map((part) => [part.type, part.value]));
|
|
218
|
+
return `${lookup.day} ${lookup.month} ${lookup.year} ${lookup.hour}:${lookup.minute}`;
|
|
135
219
|
}
|
|
136
220
|
function pad(value, length) {
|
|
137
221
|
return value.length >= length ? value.slice(0, length) : value.padEnd(length);
|
|
138
222
|
}
|
|
223
|
+
function formatInputPerOutput(totals) {
|
|
224
|
+
if (totals.outputTokens <= 0) {
|
|
225
|
+
return { cached: "0", nonCached: "0", output: "0" };
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
cached: formatInteger(Math.round(totals.cachedInputTokens / totals.outputTokens)),
|
|
229
|
+
nonCached: formatInteger(Math.round(totals.nonCachedInputTokens / totals.outputTokens)),
|
|
230
|
+
output: "1"
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function clampSelectionIndex(value, rowCount) {
|
|
234
|
+
if (rowCount === 0) {
|
|
235
|
+
return -1;
|
|
236
|
+
}
|
|
237
|
+
return Math.max(0, Math.min(value, rowCount - 1));
|
|
238
|
+
}
|
|
239
|
+
function getLimitRows(providerState) {
|
|
240
|
+
if (providerState.status !== "ready") {
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
return [...providerState.stats.primaryLimitWindows, ...providerState.stats.secondaryLimitWindows];
|
|
244
|
+
}
|
|
245
|
+
function getModelRows(providerState) {
|
|
246
|
+
if (providerState.status !== "ready") {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
return providerState.stats.modelUsage;
|
|
250
|
+
}
|
|
251
|
+
function getLimitRowKey(row) {
|
|
252
|
+
return `${row.scope}-${row.planType}-${row.limitId}-${row.startTimeUtcIso}-${row.endTimeUtcIso}`;
|
|
253
|
+
}
|
|
139
254
|
export function main() {
|
|
140
255
|
render(_jsx(App, {}));
|
|
141
256
|
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import readline from "node:readline";
|
|
5
|
+
import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "./contract.js";
|
|
6
|
+
import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregates, numberOrZero } from "./limits.js";
|
|
7
|
+
const RATE_CARD = {
|
|
8
|
+
"claude-opus-4-8": { input: 500, cacheWrite5m: 625, cacheWrite1h: 1000, cacheRead: 50, output: 2500 },
|
|
9
|
+
"claude-opus-4-7": { input: 500, cacheWrite5m: 625, cacheWrite1h: 1000, cacheRead: 50, output: 2500 },
|
|
10
|
+
"claude-opus-4-6": { input: 500, cacheWrite5m: 625, cacheWrite1h: 1000, cacheRead: 50, output: 2500 },
|
|
11
|
+
"claude-opus-4-5": { input: 500, cacheWrite5m: 625, cacheWrite1h: 1000, cacheRead: 50, output: 2500 },
|
|
12
|
+
"claude-opus-4-1": { input: 1500, cacheWrite5m: 1875, cacheWrite1h: 3000, cacheRead: 150, output: 7500 },
|
|
13
|
+
"claude-opus-4": { input: 1500, cacheWrite5m: 1875, cacheWrite1h: 3000, cacheRead: 150, output: 7500 },
|
|
14
|
+
"claude-sonnet-4-6": { input: 300, cacheWrite5m: 375, cacheWrite1h: 600, cacheRead: 30, output: 1500 },
|
|
15
|
+
"claude-sonnet-4-5": { input: 300, cacheWrite5m: 375, cacheWrite1h: 600, cacheRead: 30, output: 1500 },
|
|
16
|
+
"claude-sonnet-4": { input: 300, cacheWrite5m: 375, cacheWrite1h: 600, cacheRead: 30, output: 1500 },
|
|
17
|
+
"claude-haiku-4-5": { input: 100, cacheWrite5m: 125, cacheWrite1h: 200, cacheRead: 10, output: 500 },
|
|
18
|
+
"claude-haiku-3-5": { input: 80, cacheWrite5m: 100, cacheWrite1h: 160, cacheRead: 8, output: 400 }
|
|
19
|
+
};
|
|
20
|
+
export class ClaudeUsageProvider extends UsageProviderBase {
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
super("claude", "Claude");
|
|
23
|
+
this.root = path.resolve(options.root ?? os.homedir());
|
|
24
|
+
}
|
|
25
|
+
async getStats() {
|
|
26
|
+
const sessionsRoot = path.join(this.root, ".claude", "projects");
|
|
27
|
+
const byModel = new Map();
|
|
28
|
+
const windows = createLimitWindowAggregates();
|
|
29
|
+
const planTypes = new Set();
|
|
30
|
+
const warnings = [];
|
|
31
|
+
const seenUsageEvents = new Set();
|
|
32
|
+
const parseTotals = {
|
|
33
|
+
filesScanned: 0,
|
|
34
|
+
linesRead: 0,
|
|
35
|
+
tokenEvents: 0,
|
|
36
|
+
malformedLines: 0
|
|
37
|
+
};
|
|
38
|
+
for await (const file of walkSessionFiles(sessionsRoot)) {
|
|
39
|
+
parseTotals.filesScanned += 1;
|
|
40
|
+
const fileStats = await parseSessionFile(file, byModel, windows, planTypes, seenUsageEvents);
|
|
41
|
+
parseTotals.linesRead += fileStats.linesRead;
|
|
42
|
+
parseTotals.tokenEvents += fileStats.tokenEvents;
|
|
43
|
+
parseTotals.malformedLines += fileStats.malformedLines;
|
|
44
|
+
}
|
|
45
|
+
if (parseTotals.malformedLines > 0) {
|
|
46
|
+
warnings.push(`Skipped ${parseTotals.malformedLines} malformed JSONL line(s).`);
|
|
47
|
+
}
|
|
48
|
+
const modelUsage = [...byModel.entries()]
|
|
49
|
+
.map(([modelId, totals]) => ({ modelId, totals }))
|
|
50
|
+
.sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
|
|
51
|
+
const unknownPricedModels = modelUsage
|
|
52
|
+
.map((row) => row.modelId)
|
|
53
|
+
.filter((modelId) => !resolveRate(modelId));
|
|
54
|
+
if (unknownPricedModels.length > 0) {
|
|
55
|
+
warnings.push(`No credit rate configured for: ${unknownPricedModels.join(", ")}.`);
|
|
56
|
+
}
|
|
57
|
+
if (parseTotals.filesScanned === 0) {
|
|
58
|
+
warnings.push(`No Claude session files found under ${sessionsRoot}.`);
|
|
59
|
+
}
|
|
60
|
+
const summaryTotals = sumUsageTotals(modelUsage.map((row) => row.totals));
|
|
61
|
+
const [primaryLimitWindows, secondaryLimitWindows] = buildWindowLists(windows);
|
|
62
|
+
if (parseTotals.filesScanned > 0 &&
|
|
63
|
+
parseTotals.tokenEvents > 0 &&
|
|
64
|
+
primaryLimitWindows.length === 0 &&
|
|
65
|
+
secondaryLimitWindows.length === 0) {
|
|
66
|
+
warnings.push("Claude transcripts did not expose rate-limit windows in the local logs.");
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
providerId: this.id,
|
|
70
|
+
providerLabel: this.label,
|
|
71
|
+
summary: {
|
|
72
|
+
filesScanned: parseTotals.filesScanned,
|
|
73
|
+
linesRead: parseTotals.linesRead,
|
|
74
|
+
tokenEvents: parseTotals.tokenEvents,
|
|
75
|
+
totals: summaryTotals,
|
|
76
|
+
distinctModels: modelUsage.map((row) => row.modelId),
|
|
77
|
+
distinctPlanTypes: [...planTypes].sort(),
|
|
78
|
+
rootLabel: "~/.claude/projects",
|
|
79
|
+
rootPath: sessionsRoot
|
|
80
|
+
},
|
|
81
|
+
modelUsage,
|
|
82
|
+
primaryLimitWindows,
|
|
83
|
+
secondaryLimitWindows,
|
|
84
|
+
warnings
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function normalizeUsage(value) {
|
|
89
|
+
const usage = asRecord(value) ?? {};
|
|
90
|
+
const cacheCreation = asRecord(usage.cache_creation);
|
|
91
|
+
const cacheCreation5mInputTokens = numberOrZero(cacheCreation?.ephemeral_5m_input_tokens);
|
|
92
|
+
const cacheCreation1hInputTokens = numberOrZero(cacheCreation?.ephemeral_1h_input_tokens);
|
|
93
|
+
const cacheCreationInputTokens = Math.max(numberOrZero(usage.cache_creation_input_tokens), cacheCreation5mInputTokens + cacheCreation1hInputTokens);
|
|
94
|
+
return {
|
|
95
|
+
inputTokens: numberOrZero(usage.input_tokens),
|
|
96
|
+
cacheReadInputTokens: numberOrZero(usage.cache_read_input_tokens),
|
|
97
|
+
cacheCreationInputTokens,
|
|
98
|
+
cacheCreation5mInputTokens,
|
|
99
|
+
cacheCreation1hInputTokens,
|
|
100
|
+
outputTokens: numberOrZero(usage.output_tokens),
|
|
101
|
+
inferenceGeo: String(usage.inference_geo ?? "")
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function resolveRate(modelId) {
|
|
105
|
+
const candidates = Object.keys(RATE_CARD).sort((left, right) => right.length - left.length);
|
|
106
|
+
for (const candidate of candidates) {
|
|
107
|
+
if (modelId === candidate || modelId.startsWith(`${candidate}-`)) {
|
|
108
|
+
return RATE_CARD[candidate];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
function creditsFor(modelId, usage) {
|
|
114
|
+
const rate = resolveRate(modelId);
|
|
115
|
+
if (!rate) {
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
const cacheWriteKnownTokens = usage.cacheCreation5mInputTokens + usage.cacheCreation1hInputTokens;
|
|
119
|
+
const cacheWriteFallbackTokens = Math.max(0, usage.cacheCreationInputTokens - cacheWriteKnownTokens);
|
|
120
|
+
const inferenceMultiplier = usage.inferenceGeo === "us" ? 1.1 : 1;
|
|
121
|
+
return (((usage.inputTokens / 1000000) * rate.input +
|
|
122
|
+
(usage.cacheReadInputTokens / 1000000) * rate.cacheRead +
|
|
123
|
+
(usage.cacheCreation5mInputTokens / 1000000) * rate.cacheWrite5m +
|
|
124
|
+
(usage.cacheCreation1hInputTokens / 1000000) * rate.cacheWrite1h +
|
|
125
|
+
(cacheWriteFallbackTokens / 1000000) * rate.cacheWrite5m +
|
|
126
|
+
(usage.outputTokens / 1000000) * rate.output) *
|
|
127
|
+
inferenceMultiplier);
|
|
128
|
+
}
|
|
129
|
+
function usageToTotals(modelId, usage) {
|
|
130
|
+
const nonCachedInputTokens = usage.inputTokens + usage.cacheCreationInputTokens;
|
|
131
|
+
const cachedInputTokens = usage.cacheReadInputTokens;
|
|
132
|
+
return {
|
|
133
|
+
inputTokens: nonCachedInputTokens + cachedInputTokens,
|
|
134
|
+
cachedInputTokens,
|
|
135
|
+
nonCachedInputTokens,
|
|
136
|
+
outputTokens: usage.outputTokens,
|
|
137
|
+
reasoningOutputTokens: 0,
|
|
138
|
+
totalTokens: nonCachedInputTokens + cachedInputTokens + usage.outputTokens,
|
|
139
|
+
estimatedCredits: creditsFor(modelId, usage),
|
|
140
|
+
eventCount: 1
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function addModelUsage(byModel, modelId, deltaTotals) {
|
|
144
|
+
const resolvedModelId = modelId || "unknown";
|
|
145
|
+
const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
|
|
146
|
+
addUsageTotals(totals, deltaTotals);
|
|
147
|
+
byModel.set(resolvedModelId, totals);
|
|
148
|
+
}
|
|
149
|
+
function isSessionFile(filePath) {
|
|
150
|
+
return filePath.endsWith(".jsonl") && filePath.includes(`${path.sep}.claude${path.sep}projects${path.sep}`);
|
|
151
|
+
}
|
|
152
|
+
async function* walkSessionFiles(directory) {
|
|
153
|
+
let entries;
|
|
154
|
+
try {
|
|
155
|
+
entries = await fs.promises.readdir(directory, { withFileTypes: true });
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
const fullPath = path.join(directory, entry.name);
|
|
162
|
+
if (entry.isDirectory()) {
|
|
163
|
+
yield* walkSessionFiles(fullPath);
|
|
164
|
+
}
|
|
165
|
+
else if (entry.isFile() && isSessionFile(fullPath)) {
|
|
166
|
+
yield fullPath;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function parseSessionFile(filePath, byModel, windows, planTypes, seenUsageEvents) {
|
|
171
|
+
const stream = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
172
|
+
const lineReader = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
173
|
+
let linesRead = 0;
|
|
174
|
+
let tokenEvents = 0;
|
|
175
|
+
let malformedLines = 0;
|
|
176
|
+
for await (const line of lineReader) {
|
|
177
|
+
linesRead += 1;
|
|
178
|
+
if (!line.trim()) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
let payloadObject;
|
|
182
|
+
try {
|
|
183
|
+
payloadObject = JSON.parse(line);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
malformedLines += 1;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (payloadObject.type !== "assistant") {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const message = asRecord(payloadObject.message);
|
|
193
|
+
const usage = asRecord(message?.usage);
|
|
194
|
+
if (!usage) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const usageKey = buildUsageEventKey(payloadObject, message);
|
|
198
|
+
if (usageKey && seenUsageEvents.has(usageKey)) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (usageKey) {
|
|
202
|
+
seenUsageEvents.add(usageKey);
|
|
203
|
+
}
|
|
204
|
+
const modelId = String(message?.model ?? "unknown");
|
|
205
|
+
const deltaTotals = usageToTotals(modelId, normalizeUsage(usage));
|
|
206
|
+
addModelUsage(byModel, modelId, deltaTotals);
|
|
207
|
+
tokenEvents += 1;
|
|
208
|
+
const eventTimeMs = Date.parse(String(payloadObject.timestamp ?? ""));
|
|
209
|
+
const safeEventTimeMs = Number.isFinite(eventTimeMs) ? eventTimeMs : 0;
|
|
210
|
+
applyRateLimits(windows, extractRateLimits(payloadObject, message), safeEventTimeMs, deltaTotals, planTypes);
|
|
211
|
+
}
|
|
212
|
+
return { linesRead, tokenEvents, malformedLines };
|
|
213
|
+
}
|
|
214
|
+
function buildUsageEventKey(payloadObject, message) {
|
|
215
|
+
const sessionId = String(payloadObject.sessionId ?? "");
|
|
216
|
+
const requestId = typeof payloadObject.requestId === "string" ? payloadObject.requestId : "";
|
|
217
|
+
const messageId = typeof message?.id === "string" ? message.id : "";
|
|
218
|
+
if (!requestId && !messageId) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
return `${sessionId}|${requestId || messageId}`;
|
|
222
|
+
}
|
|
223
|
+
function extractRateLimits(payloadObject, message) {
|
|
224
|
+
return asRecord(payloadObject.rate_limits) ?? asRecord(message?.rate_limits);
|
|
225
|
+
}
|
|
@@ -2,7 +2,8 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import readline from "node:readline";
|
|
5
|
-
import { UsageProviderBase, createEmptyUsageTotals } from "./contract.js";
|
|
5
|
+
import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "./contract.js";
|
|
6
|
+
import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregates, numberOrZero } from "./limits.js";
|
|
6
7
|
const RATE_CARD = {
|
|
7
8
|
"gpt-5.5": { input: 125, cachedInput: 12.5, output: 750 },
|
|
8
9
|
"gpt-5.4": { input: 62.5, cachedInput: 6.25, output: 375 },
|
|
@@ -16,7 +17,7 @@ export class CodexUsageProvider extends UsageProviderBase {
|
|
|
16
17
|
async getStats() {
|
|
17
18
|
const sessionsRoot = path.join(this.root, ".codex", "sessions");
|
|
18
19
|
const byModel = new Map();
|
|
19
|
-
const windows =
|
|
20
|
+
const windows = createLimitWindowAggregates();
|
|
20
21
|
const planTypes = new Set();
|
|
21
22
|
const warnings = [];
|
|
22
23
|
const parseTotals = {
|
|
@@ -78,9 +79,6 @@ function createEmptyRawUsage() {
|
|
|
78
79
|
totalTokens: 0
|
|
79
80
|
};
|
|
80
81
|
}
|
|
81
|
-
function numberOrZero(value) {
|
|
82
|
-
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
83
|
-
}
|
|
84
82
|
function normalizeRawUsage(value) {
|
|
85
83
|
const usage = value && typeof value === "object" ? value : {};
|
|
86
84
|
return {
|
|
@@ -124,104 +122,19 @@ function rawUsageToTotals(usage) {
|
|
|
124
122
|
eventCount: 0
|
|
125
123
|
};
|
|
126
124
|
}
|
|
127
|
-
function
|
|
128
|
-
target.inputTokens += source.inputTokens;
|
|
129
|
-
target.cachedInputTokens += source.cachedInputTokens;
|
|
130
|
-
target.nonCachedInputTokens += source.nonCachedInputTokens;
|
|
131
|
-
target.outputTokens += source.outputTokens;
|
|
132
|
-
target.reasoningOutputTokens += source.reasoningOutputTokens;
|
|
133
|
-
target.totalTokens += source.totalTokens;
|
|
134
|
-
target.estimatedCredits += source.estimatedCredits;
|
|
135
|
-
target.eventCount += source.eventCount;
|
|
136
|
-
}
|
|
137
|
-
function addModelUsage(byModel, modelId, usage) {
|
|
125
|
+
function createUsageTotalsForModel(modelId, usage) {
|
|
138
126
|
const resolvedModelId = modelId || "unknown";
|
|
139
|
-
const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
|
|
140
127
|
const deltaTotals = rawUsageToTotals(usage);
|
|
141
128
|
deltaTotals.estimatedCredits = creditsFor(resolvedModelId, usage);
|
|
142
129
|
deltaTotals.eventCount = 1;
|
|
130
|
+
return deltaTotals;
|
|
131
|
+
}
|
|
132
|
+
function addModelUsage(byModel, modelId, deltaTotals) {
|
|
133
|
+
const resolvedModelId = modelId || "unknown";
|
|
134
|
+
const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
|
|
143
135
|
addUsageTotals(totals, deltaTotals);
|
|
144
136
|
byModel.set(resolvedModelId, totals);
|
|
145
137
|
}
|
|
146
|
-
function sumUsageTotals(rows) {
|
|
147
|
-
const totals = createEmptyUsageTotals();
|
|
148
|
-
for (const row of rows) {
|
|
149
|
-
addUsageTotals(totals, row);
|
|
150
|
-
}
|
|
151
|
-
return totals;
|
|
152
|
-
}
|
|
153
|
-
function formatIsoFromSeconds(seconds) {
|
|
154
|
-
return new Date(seconds * 1000).toISOString().replace(".000Z", "Z");
|
|
155
|
-
}
|
|
156
|
-
function formatIsoFromMilliseconds(milliseconds) {
|
|
157
|
-
return new Date(milliseconds).toISOString().replace(".000Z", "Z");
|
|
158
|
-
}
|
|
159
|
-
function makeWindowKey(scope, rateLimits, window) {
|
|
160
|
-
return [
|
|
161
|
-
scope,
|
|
162
|
-
String(rateLimits.limit_id ?? "unknown"),
|
|
163
|
-
String(rateLimits.plan_type ?? "unknown"),
|
|
164
|
-
numberOrZero(window.window_minutes),
|
|
165
|
-
Math.round(numberOrZero(window.resets_at) / 60)
|
|
166
|
-
].join("|");
|
|
167
|
-
}
|
|
168
|
-
function upsertWindow(windows, scope, rateLimits, window, eventTimeMs) {
|
|
169
|
-
if (!window) {
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
const windowMinutes = numberOrZero(window.window_minutes);
|
|
173
|
-
const resetsAt = numberOrZero(window.resets_at);
|
|
174
|
-
if (!windowMinutes || !resetsAt) {
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
const startsAt = resetsAt - windowMinutes * 60;
|
|
178
|
-
const usedPercent = numberOrZero(window.used_percent);
|
|
179
|
-
const key = makeWindowKey(scope, rateLimits, window);
|
|
180
|
-
const existing = windows.get(key);
|
|
181
|
-
if (!existing) {
|
|
182
|
-
windows.set(key, {
|
|
183
|
-
scope,
|
|
184
|
-
limitId: String(rateLimits.limit_id ?? "unknown"),
|
|
185
|
-
planType: String(rateLimits.plan_type ?? "unknown"),
|
|
186
|
-
windowMinutes,
|
|
187
|
-
minStartsAt: startsAt,
|
|
188
|
-
maxResetsAt: resetsAt,
|
|
189
|
-
firstSeenMs: eventTimeMs,
|
|
190
|
-
lastSeenMs: eventTimeMs,
|
|
191
|
-
minUsedPercent: usedPercent,
|
|
192
|
-
maxUsedPercent: usedPercent,
|
|
193
|
-
eventCount: 1
|
|
194
|
-
});
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
existing.minStartsAt = Math.min(existing.minStartsAt, startsAt);
|
|
198
|
-
existing.maxResetsAt = Math.max(existing.maxResetsAt, resetsAt);
|
|
199
|
-
existing.firstSeenMs = Math.min(existing.firstSeenMs, eventTimeMs);
|
|
200
|
-
existing.lastSeenMs = Math.max(existing.lastSeenMs, eventTimeMs);
|
|
201
|
-
existing.minUsedPercent = Math.min(existing.minUsedPercent, usedPercent);
|
|
202
|
-
existing.maxUsedPercent = Math.max(existing.maxUsedPercent, usedPercent);
|
|
203
|
-
existing.eventCount += 1;
|
|
204
|
-
}
|
|
205
|
-
function buildWindowLists(windows) {
|
|
206
|
-
const rows = [...windows.values()]
|
|
207
|
-
.map((window) => ({
|
|
208
|
-
scope: window.scope,
|
|
209
|
-
planType: window.planType,
|
|
210
|
-
limitId: window.limitId,
|
|
211
|
-
windowMinutes: window.windowMinutes,
|
|
212
|
-
startTimeIso: formatIsoFromSeconds(window.minStartsAt),
|
|
213
|
-
endTimeIso: formatIsoFromSeconds(window.maxResetsAt),
|
|
214
|
-
firstSeenIso: formatIsoFromMilliseconds(window.firstSeenMs),
|
|
215
|
-
lastSeenIso: formatIsoFromMilliseconds(window.lastSeenMs),
|
|
216
|
-
minUsedPercent: window.minUsedPercent,
|
|
217
|
-
maxUsedPercent: window.maxUsedPercent,
|
|
218
|
-
eventCount: window.eventCount
|
|
219
|
-
}))
|
|
220
|
-
.sort((left, right) => right.endTimeIso.localeCompare(left.endTimeIso));
|
|
221
|
-
const primary = rows.filter((row) => row.scope === "primary").slice(0, 5);
|
|
222
|
-
const secondary = rows.filter((row) => row.scope === "secondary").slice(0, 5);
|
|
223
|
-
return [primary, secondary];
|
|
224
|
-
}
|
|
225
138
|
function isSessionFile(filePath) {
|
|
226
139
|
return filePath.endsWith(".jsonl") && filePath.includes(`${path.sep}.codex${path.sep}sessions${path.sep}`);
|
|
227
140
|
}
|
|
@@ -279,23 +192,17 @@ async function parseSessionFile(filePath, byModel, windows, planTypes) {
|
|
|
279
192
|
continue;
|
|
280
193
|
}
|
|
281
194
|
const info = payload.info;
|
|
282
|
-
const rateLimits = payload.rate_limits;
|
|
283
195
|
const totalUsage = normalizeRawUsage(info?.total_token_usage);
|
|
284
196
|
const lastUsage = info?.last_token_usage;
|
|
285
197
|
const usage = lastUsage ? normalizeRawUsage(lastUsage) : previousTotal ? subtractRawUsage(totalUsage, previousTotal) : totalUsage;
|
|
286
198
|
previousTotal = totalUsage;
|
|
199
|
+
const resolvedModelId = currentModel || "unknown";
|
|
200
|
+
const deltaTotals = createUsageTotalsForModel(resolvedModelId, usage);
|
|
287
201
|
tokenEvents += 1;
|
|
288
|
-
addModelUsage(byModel,
|
|
289
|
-
if (typeof rateLimits?.plan_type === "string") {
|
|
290
|
-
planTypes.add(rateLimits.plan_type);
|
|
291
|
-
}
|
|
202
|
+
addModelUsage(byModel, resolvedModelId, deltaTotals);
|
|
292
203
|
const eventTimeMs = Date.parse(String(payloadObject.timestamp ?? ""));
|
|
293
204
|
const safeEventTimeMs = Number.isFinite(eventTimeMs) ? eventTimeMs : 0;
|
|
294
|
-
|
|
295
|
-
upsertWindow(windows, "secondary", rateLimits ?? {}, asRecord(rateLimits?.secondary), safeEventTimeMs);
|
|
205
|
+
applyRateLimits(windows, asRecord(payload.rate_limits), safeEventTimeMs, deltaTotals, planTypes);
|
|
296
206
|
}
|
|
297
207
|
return { linesRead, tokenEvents, malformedLines };
|
|
298
208
|
}
|
|
299
|
-
function asRecord(value) {
|
|
300
|
-
return value && typeof value === "object" ? value : null;
|
|
301
|
-
}
|
|
@@ -16,3 +16,20 @@ export function createEmptyUsageTotals() {
|
|
|
16
16
|
eventCount: 0
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
+
export function addUsageTotals(target, source) {
|
|
20
|
+
target.inputTokens += source.inputTokens;
|
|
21
|
+
target.cachedInputTokens += source.cachedInputTokens;
|
|
22
|
+
target.nonCachedInputTokens += source.nonCachedInputTokens;
|
|
23
|
+
target.outputTokens += source.outputTokens;
|
|
24
|
+
target.reasoningOutputTokens += source.reasoningOutputTokens;
|
|
25
|
+
target.totalTokens += source.totalTokens;
|
|
26
|
+
target.estimatedCredits += source.estimatedCredits;
|
|
27
|
+
target.eventCount += source.eventCount;
|
|
28
|
+
}
|
|
29
|
+
export function sumUsageTotals(rows) {
|
|
30
|
+
const totals = createEmptyUsageTotals();
|
|
31
|
+
for (const row of rows) {
|
|
32
|
+
addUsageTotals(totals, row);
|
|
33
|
+
}
|
|
34
|
+
return totals;
|
|
35
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { ClaudeUsageProvider } from "./claude.js";
|
|
1
2
|
import { CodexUsageProvider } from "./codex.js";
|
|
2
3
|
export function createProviders() {
|
|
3
|
-
return [new CodexUsageProvider()];
|
|
4
|
+
return [new CodexUsageProvider(), new ClaudeUsageProvider()];
|
|
4
5
|
}
|
|
6
|
+
export { ClaudeUsageProvider } from "./claude.js";
|
|
5
7
|
export { CodexUsageProvider } from "./codex.js";
|
|
6
8
|
export { UsageProviderBase } from "./contract.js";
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { addUsageTotals, createEmptyUsageTotals } from "./contract.js";
|
|
2
|
+
export function createLimitWindowAggregates() {
|
|
3
|
+
return new Map();
|
|
4
|
+
}
|
|
5
|
+
export function numberOrZero(value) {
|
|
6
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
7
|
+
}
|
|
8
|
+
export function asRecord(value) {
|
|
9
|
+
return value && typeof value === "object" ? value : null;
|
|
10
|
+
}
|
|
11
|
+
export function applyRateLimits(windows, rateLimits, eventTimeMs, deltaTotals, planTypes) {
|
|
12
|
+
if (!rateLimits) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (typeof rateLimits.plan_type === "string") {
|
|
16
|
+
planTypes.add(rateLimits.plan_type);
|
|
17
|
+
}
|
|
18
|
+
upsertWindow(windows, "primary", rateLimits, asRecord(rateLimits.primary), eventTimeMs, deltaTotals);
|
|
19
|
+
upsertWindow(windows, "secondary", rateLimits, asRecord(rateLimits.secondary), eventTimeMs, deltaTotals);
|
|
20
|
+
}
|
|
21
|
+
export function buildWindowLists(windows) {
|
|
22
|
+
const rows = collapseNearbyWindows([...windows.values()].map((window) => ({
|
|
23
|
+
scope: window.scope,
|
|
24
|
+
planType: window.planType,
|
|
25
|
+
limitId: window.limitId,
|
|
26
|
+
windowMinutes: window.windowMinutes,
|
|
27
|
+
startTimeUtcIso: formatIsoFromSeconds(window.minStartsAt),
|
|
28
|
+
endTimeUtcIso: formatIsoFromSeconds(window.maxResetsAt),
|
|
29
|
+
firstSeenUtcIso: formatIsoFromMilliseconds(window.firstSeenMs),
|
|
30
|
+
lastSeenUtcIso: formatIsoFromMilliseconds(window.lastSeenMs),
|
|
31
|
+
minUsedPercent: window.minUsedPercent,
|
|
32
|
+
maxUsedPercent: window.maxUsedPercent,
|
|
33
|
+
totals: computeWindowTotals(window.events),
|
|
34
|
+
eventCount: 0
|
|
35
|
+
})))
|
|
36
|
+
.map((row) => ({
|
|
37
|
+
...row,
|
|
38
|
+
eventCount: row.totals.eventCount
|
|
39
|
+
}))
|
|
40
|
+
.sort((left, right) => right.endTimeUtcIso.localeCompare(left.endTimeUtcIso));
|
|
41
|
+
const primary = rows.filter((row) => row.scope === "primary").slice(0, 5);
|
|
42
|
+
const secondary = rows.filter((row) => row.scope === "secondary").slice(0, 5);
|
|
43
|
+
return [primary, secondary];
|
|
44
|
+
}
|
|
45
|
+
function formatIsoFromSeconds(seconds) {
|
|
46
|
+
return new Date(seconds * 1000).toISOString().replace(".000Z", "Z");
|
|
47
|
+
}
|
|
48
|
+
function formatIsoFromMilliseconds(milliseconds) {
|
|
49
|
+
return new Date(milliseconds).toISOString().replace(".000Z", "Z");
|
|
50
|
+
}
|
|
51
|
+
function makeWindowKey(scope, rateLimits, window) {
|
|
52
|
+
return [
|
|
53
|
+
scope,
|
|
54
|
+
String(rateLimits.limit_id ?? "unknown"),
|
|
55
|
+
String(rateLimits.plan_type ?? "unknown"),
|
|
56
|
+
numberOrZero(window.window_minutes),
|
|
57
|
+
numberOrZero(window.resets_at)
|
|
58
|
+
].join("|");
|
|
59
|
+
}
|
|
60
|
+
function collapseNearbyWindows(rows) {
|
|
61
|
+
const collapsed = new Map();
|
|
62
|
+
for (const row of rows) {
|
|
63
|
+
const key = [
|
|
64
|
+
row.scope,
|
|
65
|
+
row.limitId,
|
|
66
|
+
row.planType,
|
|
67
|
+
row.windowMinutes,
|
|
68
|
+
Math.round(Date.parse(row.endTimeUtcIso) / 60000)
|
|
69
|
+
].join("|");
|
|
70
|
+
const existing = collapsed.get(key);
|
|
71
|
+
if (!existing) {
|
|
72
|
+
collapsed.set(key, {
|
|
73
|
+
...row,
|
|
74
|
+
totals: { ...row.totals }
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
existing.startTimeUtcIso =
|
|
79
|
+
existing.startTimeUtcIso < row.startTimeUtcIso ? existing.startTimeUtcIso : row.startTimeUtcIso;
|
|
80
|
+
existing.endTimeUtcIso =
|
|
81
|
+
existing.endTimeUtcIso > row.endTimeUtcIso ? existing.endTimeUtcIso : row.endTimeUtcIso;
|
|
82
|
+
existing.firstSeenUtcIso =
|
|
83
|
+
existing.firstSeenUtcIso < row.firstSeenUtcIso ? existing.firstSeenUtcIso : row.firstSeenUtcIso;
|
|
84
|
+
existing.lastSeenUtcIso =
|
|
85
|
+
existing.lastSeenUtcIso > row.lastSeenUtcIso ? existing.lastSeenUtcIso : row.lastSeenUtcIso;
|
|
86
|
+
existing.minUsedPercent = Math.min(existing.minUsedPercent, row.minUsedPercent);
|
|
87
|
+
existing.maxUsedPercent = Math.max(existing.maxUsedPercent, row.maxUsedPercent);
|
|
88
|
+
addUsageTotals(existing.totals, row.totals);
|
|
89
|
+
existing.eventCount = existing.totals.eventCount;
|
|
90
|
+
}
|
|
91
|
+
return [...collapsed.values()];
|
|
92
|
+
}
|
|
93
|
+
function computeWindowTotals(events) {
|
|
94
|
+
// Session files are not guaranteed to be parsed in timestamp order, so
|
|
95
|
+
// saturation has to be applied after we sort the captured window events.
|
|
96
|
+
const totals = createEmptyUsageTotals();
|
|
97
|
+
let sawBelowCap = false;
|
|
98
|
+
let isExhausted = false;
|
|
99
|
+
for (const event of [...events].sort((left, right) => left.eventTimeMs - right.eventTimeMs)) {
|
|
100
|
+
sawBelowCap || (sawBelowCap = event.usedPercent < 100);
|
|
101
|
+
if (!isExhausted) {
|
|
102
|
+
addUsageTotals(totals, event.totals);
|
|
103
|
+
if (sawBelowCap && event.usedPercent >= 100) {
|
|
104
|
+
isExhausted = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return totals;
|
|
109
|
+
}
|
|
110
|
+
function upsertWindow(windows, scope, rateLimits, window, eventTimeMs, deltaTotals) {
|
|
111
|
+
if (!window) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const windowMinutes = numberOrZero(window.window_minutes);
|
|
115
|
+
const resetsAt = numberOrZero(window.resets_at);
|
|
116
|
+
if (!windowMinutes || !resetsAt) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const startsAt = resetsAt - windowMinutes * 60;
|
|
120
|
+
const usedPercent = numberOrZero(window.used_percent);
|
|
121
|
+
const key = makeWindowKey(scope, rateLimits, window);
|
|
122
|
+
const existing = windows.get(key);
|
|
123
|
+
if (!existing) {
|
|
124
|
+
windows.set(key, {
|
|
125
|
+
scope,
|
|
126
|
+
limitId: String(rateLimits.limit_id ?? "unknown"),
|
|
127
|
+
planType: String(rateLimits.plan_type ?? "unknown"),
|
|
128
|
+
windowMinutes,
|
|
129
|
+
minStartsAt: startsAt,
|
|
130
|
+
maxResetsAt: resetsAt,
|
|
131
|
+
firstSeenMs: eventTimeMs,
|
|
132
|
+
lastSeenMs: eventTimeMs,
|
|
133
|
+
minUsedPercent: usedPercent,
|
|
134
|
+
maxUsedPercent: usedPercent,
|
|
135
|
+
events: [{ eventTimeMs, usedPercent, totals: { ...deltaTotals } }]
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
existing.minStartsAt = Math.min(existing.minStartsAt, startsAt);
|
|
140
|
+
existing.maxResetsAt = Math.max(existing.maxResetsAt, resetsAt);
|
|
141
|
+
existing.firstSeenMs = Math.min(existing.firstSeenMs, eventTimeMs);
|
|
142
|
+
existing.lastSeenMs = Math.max(existing.lastSeenMs, eventTimeMs);
|
|
143
|
+
existing.minUsedPercent = Math.min(existing.minUsedPercent, usedPercent);
|
|
144
|
+
existing.maxUsedPercent = Math.max(existing.maxUsedPercent, usedPercent);
|
|
145
|
+
existing.events.push({ eventTimeMs, usedPercent, totals: { ...deltaTotals } });
|
|
146
|
+
}
|