letmecode 0.1.2 → 0.1.4
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 +340 -51
- package/ink-app/dist/providers/claude.js +32 -14
- package/ink-app/dist/providers/codex.js +63 -16
- package/ink-app/dist/providers/contract.js +48 -9
- package/ink-app/dist/providers/copilot.js +13 -5
- package/ink-app/dist/providers/daily.js +3 -3
- package/ink-app/dist/providers/limits.js +5 -5
- package/package.json +13 -11
package/ink-app/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { useEffect, useState } from "react";
|
|
3
|
-
import { Box, Text, useApp, useInput, render } from "ink";
|
|
2
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Box, Text, measureElement, useApp, useInput, useStdout, render } from "ink";
|
|
4
4
|
import { configureCopilotVsCodeLogging, createProviders } from "./providers/index.js";
|
|
5
5
|
const VERTICAL_TABS = [
|
|
6
6
|
{ id: "limit-windows", label: "Limits" },
|
|
@@ -17,27 +17,52 @@ const LIMIT_WINDOW_COLUMNS = {
|
|
|
17
17
|
date: 17,
|
|
18
18
|
value: 10
|
|
19
19
|
};
|
|
20
|
-
const
|
|
20
|
+
const OPENAI_MODEL_USAGE_COLUMNS = {
|
|
21
21
|
model: 17,
|
|
22
22
|
input: 12,
|
|
23
23
|
cached: 12,
|
|
24
|
-
nonCached: 12,
|
|
25
24
|
output: 11,
|
|
26
25
|
credits: 12,
|
|
27
26
|
value: 12
|
|
28
27
|
};
|
|
29
|
-
const
|
|
28
|
+
const ANTHROPIC_MODEL_USAGE_COLUMNS = {
|
|
29
|
+
model: 17,
|
|
30
|
+
input: 10,
|
|
31
|
+
cacheWrite5m: 10,
|
|
32
|
+
cacheWrite1h: 10,
|
|
33
|
+
cacheRead: 10,
|
|
34
|
+
output: 10,
|
|
35
|
+
credits: 12,
|
|
36
|
+
value: 12
|
|
37
|
+
};
|
|
38
|
+
const OPENAI_DAY_USAGE_COLUMNS = {
|
|
30
39
|
day: 11,
|
|
31
40
|
events: 6,
|
|
32
41
|
input: 11,
|
|
33
42
|
output: 10,
|
|
34
43
|
value: 10
|
|
35
44
|
};
|
|
45
|
+
const ANTHROPIC_DAY_USAGE_COLUMNS = {
|
|
46
|
+
day: 11,
|
|
47
|
+
events: 6,
|
|
48
|
+
input: 10,
|
|
49
|
+
cacheWrite5m: 10,
|
|
50
|
+
cacheWrite1h: 10,
|
|
51
|
+
cacheRead: 10,
|
|
52
|
+
output: 10,
|
|
53
|
+
value: 10
|
|
54
|
+
};
|
|
36
55
|
const COPILOT_ACTIONS = [
|
|
37
56
|
{ id: "vscode", label: "Start logging VS Code", enabled: true }
|
|
38
57
|
];
|
|
58
|
+
const ENTER_FULLSCREEN_MODE = "\u001B[?1049h\u001B[2J\u001B[H";
|
|
59
|
+
const EXIT_FULLSCREEN_MODE = "\u001B[?1049l";
|
|
60
|
+
const SCROLLBAR_TRACK_GLYPH = "│";
|
|
61
|
+
const SCROLLBAR_THUMB_GLYPH = "█";
|
|
39
62
|
function App(props) {
|
|
40
63
|
const { exit } = useApp();
|
|
64
|
+
const viewportHeight = useViewportHeight();
|
|
65
|
+
const { ref: contentPanelRef, height: contentPanelHeight } = useMeasuredElementSize();
|
|
41
66
|
const providers = React.useState(() => createProviders())[0];
|
|
42
67
|
const [providerStates, setProviderStates] = useState(providers.map((provider) => ({ provider, status: "loading" })));
|
|
43
68
|
const [selectedProviderId, setSelectedProviderId] = useState(providers[0]?.id ?? "");
|
|
@@ -166,7 +191,7 @@ function App(props) {
|
|
|
166
191
|
setSelectedVerticalTabIndex((current) => (current - 1 + VERTICAL_TABS.length) % VERTICAL_TABS.length);
|
|
167
192
|
}
|
|
168
193
|
});
|
|
169
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "letmecode usage dashboard" }), _jsx(Text, { color: "gray", children: "[/]/tab to switch providers, j/k or up/down for details, left/right to select a row, enter for actions, q to quit" }), _jsx(Box, { marginTop: 1, children: sortedProviderStates.map((state) => (_jsx(ProviderTab, { label: state.provider.label, active: state.provider.id === selectedProvider.provider.id, status: state.status }, state.provider.id))) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Box, { flexDirection: "column", width: VERTICAL_TAB_WIDTH, 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, selectedDayKey: selectedDayRow?.dayKey, selectedModelId: selectedModelRow?.modelId }) })] }), _jsx(SelectionDetailsPanel, { providerState: selectedProvider, tabId: selectedVerticalTab.id, selectedLimitRow: selectedLimitRow, selectedDayRow: selectedDayRow, selectedModelRow: selectedModelRow }), _jsx(CopilotActionsPanel, { providerState: selectedProvider, actionMessage: copilotActionMessage, selectedActionIndex: selectedCopilotActionIndex }), 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] }));
|
|
194
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, height: viewportHeight, overflow: "hidden", children: [_jsx(Text, { bold: true, color: "cyan", children: "letmecode usage dashboard" }), _jsx(Text, { color: "gray", children: "[/]/tab to switch providers, j/k or up/down for details, left/right to select a row, enter for actions, q to quit" }), _jsx(Box, { marginTop: 1, children: sortedProviderStates.map((state) => (_jsx(ProviderTab, { label: state.provider.label, active: state.provider.id === selectedProvider.provider.id, status: state.status }, state.provider.id))) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [_jsxs(Box, { flexGrow: 1, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", width: VERTICAL_TAB_WIDTH, marginRight: 2, overflow: "hidden", children: VERTICAL_TABS.map((tab, index) => (_jsx(VerticalTab, { label: tab.label, active: index === selectedVerticalTabIndex }, tab.id))) }), _jsx(Box, { ref: contentPanelRef, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: _jsx(ContentPanel, { providerState: selectedProvider, tabId: selectedVerticalTab.id, selectedLimitRowKey: selectedLimitRow ? getLimitRowKey(selectedLimitRow) : undefined, selectedDayKey: selectedDayRow?.dayKey, selectedModelId: selectedModelRow?.modelId, availableHeight: contentPanelHeight }) })] }), _jsx(SelectionDetailsPanel, { providerState: selectedProvider, tabId: selectedVerticalTab.id, selectedLimitRow: selectedLimitRow, selectedDayRow: selectedDayRow, selectedModelRow: selectedModelRow }), _jsx(CopilotActionsPanel, { providerState: selectedProvider, actionMessage: copilotActionMessage, selectedActionIndex: selectedCopilotActionIndex }), selectedProvider.status === "ready" && selectedProvider.stats.warnings.length > 0 ? (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", overflow: "hidden", children: [_jsx(Text, { color: "yellow", children: "Warnings" }), selectedProvider.stats.warnings.map((warning) => (_jsx(Text, { children: warning }, warning)))] })) : null] })] }));
|
|
170
195
|
}
|
|
171
196
|
function CopilotActionsPanel(props) {
|
|
172
197
|
if (props.providerState.provider.id !== "copilot") {
|
|
@@ -210,8 +235,7 @@ function VerticalTab(props) {
|
|
|
210
235
|
}
|
|
211
236
|
function SummaryPanel(props) {
|
|
212
237
|
const { summary } = props.stats;
|
|
213
|
-
|
|
214
|
-
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: ", formatCacheTokens(summary.totals, "cached"), " non-cached: ", formatCacheTokens(summary.totals, "non-cached")] }), _jsxs(Text, { children: ["output: ", formatInteger(summary.totals.outputTokens), " reasoning: ", formatInteger(summary.totals.reasoningOutputTokens), " total: ", formatInteger(summary.totals.totalTokens)] }), _jsxs(Text, { children: ["estimated credits: ", formatUsageCredits(summary.totals)] }), _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"] })] }));
|
|
238
|
+
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)] }), _jsx(UsageBreakdownLines, { totals: summary.totals }), _jsxs(Text, { children: ["estimated credits: ", formatUsageCredits(summary.totals)] }), _jsxs(Text, { children: ["IpO: ", formatInputPerOutput(summary.totals)] }), _jsxs(Text, { children: ["models: ", summary.distinctModels.join(", ") || "none"] }), _jsxs(Text, { children: ["plans: ", summary.distinctPlanTypes.join(", ") || "none"] })] }));
|
|
215
239
|
}
|
|
216
240
|
function ContentPanel(props) {
|
|
217
241
|
if (props.providerState.status === "loading") {
|
|
@@ -221,48 +245,171 @@ function ContentPanel(props) {
|
|
|
221
245
|
return _jsxs(Text, { color: "red", children: ["Provider error: ", props.providerState.errorMessage] });
|
|
222
246
|
}
|
|
223
247
|
if (props.tabId === "limit-windows") {
|
|
224
|
-
return _jsx(LimitWindowsPanel, { stats: props.providerState.stats, selectedRowKey: props.selectedLimitRowKey });
|
|
248
|
+
return (_jsx(LimitWindowsPanel, { stats: props.providerState.stats, selectedRowKey: props.selectedLimitRowKey, availableHeight: props.availableHeight }));
|
|
225
249
|
}
|
|
226
250
|
if (props.tabId === "summary") {
|
|
227
251
|
return _jsx(SummaryPanel, { stats: props.providerState.stats });
|
|
228
252
|
}
|
|
229
253
|
if (props.tabId === "day-to-day-analyses") {
|
|
230
|
-
return _jsx(DayToDayPanel, { stats: props.providerState.stats, selectedDayKey: props.selectedDayKey });
|
|
254
|
+
return (_jsx(DayToDayPanel, { stats: props.providerState.stats, selectedDayKey: props.selectedDayKey, availableHeight: props.availableHeight }));
|
|
231
255
|
}
|
|
232
|
-
return _jsx(UsageByModelPanel, { stats: props.providerState.stats, selectedModelId: props.selectedModelId });
|
|
256
|
+
return (_jsx(UsageByModelPanel, { stats: props.providerState.stats, selectedModelId: props.selectedModelId, availableHeight: props.availableHeight }));
|
|
233
257
|
}
|
|
234
258
|
function LimitWindowsPanel(props) {
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const usedLabel = `${window.minUsedPercent}%->${window.maxUsedPercent}%`;
|
|
244
|
-
const isSelected = props.selectedRowKey === getLimitRowKey(window);
|
|
245
|
-
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)));
|
|
246
|
-
})] }));
|
|
259
|
+
const bodyLines = [
|
|
260
|
+
{ key: "primary-title", text: "Primary Limit Windows", bold: true },
|
|
261
|
+
...buildLimitWindowSectionLines("primary", props.stats.primaryLimitWindows, props.selectedRowKey),
|
|
262
|
+
{ key: "section-gap", text: "" },
|
|
263
|
+
{ key: "secondary-title", text: "Secondary Limit Windows", bold: true },
|
|
264
|
+
...buildLimitWindowSectionLines("secondary", props.stats.secondaryLimitWindows, props.selectedRowKey)
|
|
265
|
+
];
|
|
266
|
+
return (_jsx(ScrollableLineViewport, { bodyLines: bodyLines, selectedBodyLineKey: props.selectedRowKey ? `limit-row:${props.selectedRowKey}` : undefined, availableHeight: props.availableHeight }));
|
|
247
267
|
}
|
|
248
268
|
function UsageByModelPanel(props) {
|
|
249
269
|
if (props.stats.modelUsage.length === 0) {
|
|
250
270
|
return _jsx(Text, { color: "gray", children: "No model usage found." });
|
|
251
271
|
}
|
|
252
272
|
const totals = props.stats.summary.totals;
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
273
|
+
if (totals.tokenBreakdown.schema === "anthropic") {
|
|
274
|
+
return (_jsx(ScrollableLineViewport, { headerLines: [
|
|
275
|
+
{
|
|
276
|
+
key: "anthropic-model-header",
|
|
277
|
+
text: `${pad("model", ANTHROPIC_MODEL_USAGE_COLUMNS.model)} ${pad("input", ANTHROPIC_MODEL_USAGE_COLUMNS.input)} ${pad("cacheW5m", ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite5m)} ${pad("cacheW1h", ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite1h)} ${pad("cacheRead", ANTHROPIC_MODEL_USAGE_COLUMNS.cacheRead)} ${pad("output", ANTHROPIC_MODEL_USAGE_COLUMNS.output)} ${pad("credits", ANTHROPIC_MODEL_USAGE_COLUMNS.credits)} value`,
|
|
278
|
+
color: "gray"
|
|
279
|
+
}
|
|
280
|
+
], bodyLines: props.stats.modelUsage.flatMap((row) => {
|
|
281
|
+
if (row.totals.tokenBreakdown.schema !== "anthropic") {
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
return [
|
|
285
|
+
{
|
|
286
|
+
key: `model-row:${row.modelId}`,
|
|
287
|
+
text: `${pad(row.modelId, ANTHROPIC_MODEL_USAGE_COLUMNS.model)} ${pad(formatInteger(row.totals.tokenBreakdown.inputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.input)} ${pad(formatInteger(row.totals.tokenBreakdown.cacheWrite5mInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite5m)} ${pad(formatInteger(row.totals.tokenBreakdown.cacheWrite1hInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite1h)} ${pad(formatInteger(row.totals.tokenBreakdown.cacheReadInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheRead)} ${pad(formatInteger(row.totals.outputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.output)} ${pad(formatUsageCredits(row.totals, row.modelId), ANTHROPIC_MODEL_USAGE_COLUMNS.credits)} ${pad(formatUsageUsd(row.totals, row.modelId), ANTHROPIC_MODEL_USAGE_COLUMNS.value)}`,
|
|
288
|
+
inverse: props.selectedModelId === row.modelId,
|
|
289
|
+
color: props.selectedModelId === row.modelId ? "cyan" : undefined
|
|
290
|
+
}
|
|
291
|
+
];
|
|
292
|
+
}), footerLines: [
|
|
293
|
+
{
|
|
294
|
+
key: "anthropic-model-total",
|
|
295
|
+
text: `${pad("TOTAL", ANTHROPIC_MODEL_USAGE_COLUMNS.model)} ${pad(formatInteger(totals.tokenBreakdown.inputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.input)} ${pad(formatInteger(totals.tokenBreakdown.cacheWrite5mInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite5m)} ${pad(formatInteger(totals.tokenBreakdown.cacheWrite1hInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite1h)} ${pad(formatInteger(totals.tokenBreakdown.cacheReadInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheRead)} ${pad(formatInteger(totals.outputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.output)} ${pad(formatUsageCredits(totals), ANTHROPIC_MODEL_USAGE_COLUMNS.credits)} ${pad(formatUsageUsd(totals), ANTHROPIC_MODEL_USAGE_COLUMNS.value)}`,
|
|
296
|
+
color: "cyan"
|
|
297
|
+
}
|
|
298
|
+
], selectedBodyLineKey: props.selectedModelId ? `model-row:${props.selectedModelId}` : undefined, availableHeight: props.availableHeight }));
|
|
299
|
+
}
|
|
300
|
+
return (_jsx(ScrollableLineViewport, { headerLines: [
|
|
301
|
+
{
|
|
302
|
+
key: "openai-model-header",
|
|
303
|
+
text: `${pad("model", OPENAI_MODEL_USAGE_COLUMNS.model)} ${pad("uncached", OPENAI_MODEL_USAGE_COLUMNS.input)} ${pad("cached", OPENAI_MODEL_USAGE_COLUMNS.cached)} ${pad("output", OPENAI_MODEL_USAGE_COLUMNS.output)} ${pad("credits", OPENAI_MODEL_USAGE_COLUMNS.credits)} value`,
|
|
304
|
+
color: "gray"
|
|
305
|
+
}
|
|
306
|
+
], bodyLines: props.stats.modelUsage.flatMap((row) => {
|
|
307
|
+
if (row.totals.tokenBreakdown.schema !== "openai") {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
return [
|
|
311
|
+
{
|
|
312
|
+
key: `model-row:${row.modelId}`,
|
|
313
|
+
text: `${pad(row.modelId, OPENAI_MODEL_USAGE_COLUMNS.model)} ${pad(formatOpenAiTokens(row.totals, "non-cached"), OPENAI_MODEL_USAGE_COLUMNS.input)} ${pad(formatOpenAiTokens(row.totals, "cached"), OPENAI_MODEL_USAGE_COLUMNS.cached)} ${pad(formatInteger(row.totals.outputTokens), OPENAI_MODEL_USAGE_COLUMNS.output)} ${pad(formatUsageCredits(row.totals, row.modelId), OPENAI_MODEL_USAGE_COLUMNS.credits)} ${pad(formatUsageUsd(row.totals, row.modelId), OPENAI_MODEL_USAGE_COLUMNS.value)}`,
|
|
314
|
+
inverse: props.selectedModelId === row.modelId,
|
|
315
|
+
color: props.selectedModelId === row.modelId ? "cyan" : undefined
|
|
316
|
+
}
|
|
317
|
+
];
|
|
318
|
+
}), footerLines: [
|
|
319
|
+
{
|
|
320
|
+
key: "openai-model-total",
|
|
321
|
+
text: `${pad("TOTAL", OPENAI_MODEL_USAGE_COLUMNS.model)} ${pad(formatOpenAiTokens(totals, "non-cached"), OPENAI_MODEL_USAGE_COLUMNS.input)} ${pad(formatOpenAiTokens(totals, "cached"), OPENAI_MODEL_USAGE_COLUMNS.cached)} ${pad(formatInteger(totals.outputTokens), OPENAI_MODEL_USAGE_COLUMNS.output)} ${pad(formatUsageCredits(totals), OPENAI_MODEL_USAGE_COLUMNS.credits)} ${pad(formatUsageUsd(totals), OPENAI_MODEL_USAGE_COLUMNS.value)}`,
|
|
322
|
+
color: "cyan"
|
|
323
|
+
}
|
|
324
|
+
], selectedBodyLineKey: props.selectedModelId ? `model-row:${props.selectedModelId}` : undefined, availableHeight: props.availableHeight }));
|
|
257
325
|
}
|
|
258
326
|
function DayToDayPanel(props) {
|
|
259
327
|
if (props.stats.dayUsage.length === 0) {
|
|
260
328
|
return _jsx(Text, { color: "gray", children: "No day-by-day usage found." });
|
|
261
329
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
330
|
+
const totals = props.stats.summary.totals;
|
|
331
|
+
if (totals.tokenBreakdown.schema === "anthropic") {
|
|
332
|
+
return (_jsx(ScrollableLineViewport, { headerLines: [
|
|
333
|
+
{
|
|
334
|
+
key: "anthropic-day-header",
|
|
335
|
+
text: `${pad("day", ANTHROPIC_DAY_USAGE_COLUMNS.day)} ${pad("events", ANTHROPIC_DAY_USAGE_COLUMNS.events)} ${pad("input", ANTHROPIC_DAY_USAGE_COLUMNS.input)} ${pad("cacheW5m", ANTHROPIC_DAY_USAGE_COLUMNS.cacheWrite5m)} ${pad("cacheW1h", ANTHROPIC_DAY_USAGE_COLUMNS.cacheWrite1h)} ${pad("cacheRead", ANTHROPIC_DAY_USAGE_COLUMNS.cacheRead)} ${pad("output", ANTHROPIC_DAY_USAGE_COLUMNS.output)} value`,
|
|
336
|
+
color: "gray"
|
|
337
|
+
}
|
|
338
|
+
], bodyLines: props.stats.dayUsage.flatMap((row) => {
|
|
339
|
+
if (row.totals.tokenBreakdown.schema !== "anthropic") {
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
return [
|
|
343
|
+
{
|
|
344
|
+
key: `day-row:${row.dayKey}`,
|
|
345
|
+
text: `${pad(formatUtcDay(row.dayKey), ANTHROPIC_DAY_USAGE_COLUMNS.day)} ${pad(formatInteger(row.totals.eventCount), ANTHROPIC_DAY_USAGE_COLUMNS.events)} ${pad(formatInteger(row.totals.tokenBreakdown.inputTokens), ANTHROPIC_DAY_USAGE_COLUMNS.input)} ${pad(formatInteger(row.totals.tokenBreakdown.cacheWrite5mInputTokens), ANTHROPIC_DAY_USAGE_COLUMNS.cacheWrite5m)} ${pad(formatInteger(row.totals.tokenBreakdown.cacheWrite1hInputTokens), ANTHROPIC_DAY_USAGE_COLUMNS.cacheWrite1h)} ${pad(formatInteger(row.totals.tokenBreakdown.cacheReadInputTokens), ANTHROPIC_DAY_USAGE_COLUMNS.cacheRead)} ${pad(formatInteger(row.totals.outputTokens), ANTHROPIC_DAY_USAGE_COLUMNS.output)} ${pad(formatUsageUsd(row.totals), ANTHROPIC_DAY_USAGE_COLUMNS.value)}`,
|
|
346
|
+
inverse: props.selectedDayKey === row.dayKey,
|
|
347
|
+
color: props.selectedDayKey === row.dayKey ? "cyan" : undefined
|
|
348
|
+
}
|
|
349
|
+
];
|
|
350
|
+
}), selectedBodyLineKey: props.selectedDayKey ? `day-row:${props.selectedDayKey}` : undefined, availableHeight: props.availableHeight }));
|
|
351
|
+
}
|
|
352
|
+
return (_jsx(ScrollableLineViewport, { headerLines: [
|
|
353
|
+
{
|
|
354
|
+
key: "openai-day-header",
|
|
355
|
+
text: `${pad("day", OPENAI_DAY_USAGE_COLUMNS.day)} ${pad("events", OPENAI_DAY_USAGE_COLUMNS.events)} ${pad("input", OPENAI_DAY_USAGE_COLUMNS.input)} ${pad("output", OPENAI_DAY_USAGE_COLUMNS.output)} value`,
|
|
356
|
+
color: "gray"
|
|
357
|
+
}
|
|
358
|
+
], bodyLines: props.stats.dayUsage.flatMap((row) => {
|
|
359
|
+
if (row.totals.tokenBreakdown.schema !== "openai") {
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
return [
|
|
363
|
+
{
|
|
364
|
+
key: `day-row:${row.dayKey}`,
|
|
365
|
+
text: `${pad(formatUtcDay(row.dayKey), OPENAI_DAY_USAGE_COLUMNS.day)} ${pad(formatInteger(row.totals.eventCount), OPENAI_DAY_USAGE_COLUMNS.events)} ${pad(formatInteger(row.totals.inputTotalTokens), OPENAI_DAY_USAGE_COLUMNS.input)} ${pad(formatInteger(row.totals.outputTokens), OPENAI_DAY_USAGE_COLUMNS.output)} ${pad(formatUsageUsd(row.totals), OPENAI_DAY_USAGE_COLUMNS.value)}`,
|
|
366
|
+
inverse: props.selectedDayKey === row.dayKey,
|
|
367
|
+
color: props.selectedDayKey === row.dayKey ? "cyan" : undefined
|
|
368
|
+
}
|
|
369
|
+
];
|
|
370
|
+
}), selectedBodyLineKey: props.selectedDayKey ? `day-row:${props.selectedDayKey}` : undefined, availableHeight: props.availableHeight }));
|
|
371
|
+
}
|
|
372
|
+
function ScrollableLineViewport(props) {
|
|
373
|
+
const headerLines = props.headerLines ?? [];
|
|
374
|
+
const footerLines = props.footerLines ?? [];
|
|
375
|
+
const layout = resolveScrollableViewportLayout(props.availableHeight, headerLines.length, props.bodyLines.length, footerLines.length);
|
|
376
|
+
const selectedBodyLineIndex = props.selectedBodyLineKey
|
|
377
|
+
? props.bodyLines.findIndex((line) => line.key === props.selectedBodyLineKey)
|
|
378
|
+
: -1;
|
|
379
|
+
const scrollOffset = useAutoScrollOffset(selectedBodyLineIndex, props.bodyLines.length, layout.bodyVisibleCount);
|
|
380
|
+
const visibleHeaderLines = headerLines.slice(0, layout.headerVisibleCount);
|
|
381
|
+
const visibleBodyLines = props.bodyLines.slice(scrollOffset, scrollOffset + layout.bodyVisibleCount);
|
|
382
|
+
const visibleFooterLines = footerLines.slice(Math.max(0, footerLines.length - layout.footerVisibleCount));
|
|
383
|
+
const bodyScrollbarLines = buildScrollbarLines(layout.bodyVisibleCount, props.bodyLines.length, scrollOffset);
|
|
384
|
+
const showScrollbar = bodyScrollbarLines.length > 0;
|
|
385
|
+
return (_jsxs(Box, { flexDirection: "row", overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [visibleHeaderLines.map((line) => (_jsx(ScrollableViewportLine, { line: line }, line.key))), visibleBodyLines.map((line) => (_jsx(ScrollableViewportLine, { line: line }, line.key))), visibleFooterLines.map((line) => (_jsx(ScrollableViewportLine, { line: line }, line.key)))] }), showScrollbar ? (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [visibleHeaderLines.map((line) => (_jsx(Text, { color: "gray", children: " " }, `${line.key}-scrollbar`))), bodyScrollbarLines.map((line, index) => (_jsx(Text, { color: line === "#" ? "cyan" : "gray", children: line }, `scrollbar-${index}`))), visibleFooterLines.map((line) => (_jsx(Text, { color: "gray", children: " " }, `${line.key}-scrollbar`)))] })) : null] }));
|
|
386
|
+
}
|
|
387
|
+
function ScrollableViewportLine(props) {
|
|
388
|
+
return (_jsx(Text, { bold: props.line.bold, color: props.line.color, inverse: props.line.inverse, wrap: "truncate-end", children: props.line.text }));
|
|
389
|
+
}
|
|
390
|
+
function buildLimitWindowSectionLines(scope, windows, selectedRowKey) {
|
|
391
|
+
if (windows.length === 0) {
|
|
392
|
+
return [{ key: `${scope}-empty`, text: "No windows found.", color: "gray" }];
|
|
393
|
+
}
|
|
394
|
+
return [
|
|
395
|
+
{
|
|
396
|
+
key: `${scope}-header`,
|
|
397
|
+
text: `${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`,
|
|
398
|
+
color: "gray"
|
|
399
|
+
},
|
|
400
|
+
...windows.map((window) => {
|
|
401
|
+
const lineKey = getLimitRowKey(window);
|
|
402
|
+
const windowLabel = formatWindowMinutes(window.windowMinutes);
|
|
403
|
+
const usedLabel = `${window.minUsedPercent}%->${window.maxUsedPercent}%`;
|
|
404
|
+
const isSelected = selectedRowKey === lineKey;
|
|
405
|
+
return {
|
|
406
|
+
key: `limit-row:${lineKey}`,
|
|
407
|
+
text: `${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)}`,
|
|
408
|
+
inverse: isSelected,
|
|
409
|
+
color: isSelected ? "cyan" : undefined
|
|
410
|
+
};
|
|
411
|
+
})
|
|
412
|
+
];
|
|
266
413
|
}
|
|
267
414
|
function SelectionDetailsPanel(props) {
|
|
268
415
|
if (props.providerState.status !== "ready") {
|
|
@@ -274,17 +421,16 @@ function SelectionDetailsPanel(props) {
|
|
|
274
421
|
}
|
|
275
422
|
if (props.tabId === "day-to-day-analyses" && props.selectedDayRow) {
|
|
276
423
|
const row = props.selectedDayRow;
|
|
277
|
-
return (_jsxs(Box, { marginTop: 1, borderStyle: "round", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Day details" }), _jsxs(Text, { children: ["day: ", formatUtcDay(row.dayKey), " events: ", formatInteger(row.totals.eventCount), " models: ", formatInteger(row.distinctModels.length), " plans: ", formatInteger(row.distinctPlanTypes.length)] }), _jsxs(Text, { children: ["range: ", formatEventRange(row.firstEventUtcIso, row.lastEventUtcIso)] }), _jsxs(Text, { children: ["
|
|
424
|
+
return (_jsxs(Box, { marginTop: 1, borderStyle: "round", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Day details" }), _jsxs(Text, { children: ["day: ", formatUtcDay(row.dayKey), " events: ", formatInteger(row.totals.eventCount), " models: ", formatInteger(row.distinctModels.length), " plans: ", formatInteger(row.distinctPlanTypes.length)] }), _jsxs(Text, { children: ["range: ", formatEventRange(row.firstEventUtcIso, row.lastEventUtcIso)] }), _jsxs(Text, { children: ["models: ", row.distinctModels.join(", ") || "none"] }), _jsxs(Text, { children: ["plans: ", row.distinctPlanTypes.join(", ") || "none"] }), _jsx(UsageTotalsDetails, { totals: row.totals })] }));
|
|
278
425
|
}
|
|
279
426
|
if (props.tabId === "usage-by-model" && props.selectedModelRow) {
|
|
280
|
-
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 })] }));
|
|
427
|
+
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, modelId: props.selectedModelRow.modelId })] }));
|
|
281
428
|
}
|
|
282
429
|
return null;
|
|
283
430
|
}
|
|
284
431
|
function UsageTotalsDetails(props) {
|
|
285
432
|
const { totals } = props;
|
|
286
|
-
|
|
287
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Total credits burned: ", formatUsageCredits(totals)] }), _jsxs(Text, { children: ["Credits Value (@ $0.01/credit): ", formatUsageUsd(totals)] }), _jsxs(Text, { children: ["IpO: ", inputPerOutput.cached, ":", inputPerOutput.nonCached, ":", inputPerOutput.output] })] }));
|
|
433
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(UsageBreakdownLines, { totals: totals }), _jsxs(Text, { children: ["Burned tokens for: ", formatUsageUsd(totals, props.modelId)] }), _jsxs(Text, { children: ["IpO: ", formatInputPerOutput(totals)] })] }));
|
|
288
434
|
}
|
|
289
435
|
function formatInteger(value) {
|
|
290
436
|
return Math.round(value).toLocaleString("en-US");
|
|
@@ -298,19 +444,22 @@ function formatCredits(value) {
|
|
|
298
444
|
maximumFractionDigits: 2
|
|
299
445
|
});
|
|
300
446
|
}
|
|
301
|
-
function formatUsageCredits(totals) {
|
|
447
|
+
function formatUsageCredits(totals, modelId) {
|
|
448
|
+
if (isInternalUsageModel(modelId)) {
|
|
449
|
+
return "N/A";
|
|
450
|
+
}
|
|
302
451
|
return totals.estimatedCreditsStatus === "unavailable" ? "unknown" : formatCredits(totals.estimatedCredits);
|
|
303
452
|
}
|
|
304
|
-
function formatUsageUsd(totals) {
|
|
453
|
+
function formatUsageUsd(totals, modelId) {
|
|
454
|
+
if (isInternalUsageModel(modelId)) {
|
|
455
|
+
return "N/A";
|
|
456
|
+
}
|
|
305
457
|
return totals.estimatedCreditsStatus === "unavailable"
|
|
306
458
|
? "unknown"
|
|
307
459
|
: formatUsd(totals.estimatedCredits * CODEX_CREDIT_COST_USD);
|
|
308
460
|
}
|
|
309
|
-
function
|
|
310
|
-
|
|
311
|
-
return "unknown";
|
|
312
|
-
}
|
|
313
|
-
return formatInteger(kind === "cached" ? totals.cachedInputTokens : totals.nonCachedInputTokens);
|
|
461
|
+
function isInternalUsageModel(modelId) {
|
|
462
|
+
return modelId === "codex-auto-review" || modelId === "<synthetic>";
|
|
314
463
|
}
|
|
315
464
|
function formatUsd(value) {
|
|
316
465
|
if (value > 0 && value < 0.0001) {
|
|
@@ -365,19 +514,107 @@ function formatEventRange(firstEventUtcIso, lastEventUtcIso) {
|
|
|
365
514
|
function pad(value, length) {
|
|
366
515
|
return value.length >= length ? value.slice(0, length) : value.padEnd(length);
|
|
367
516
|
}
|
|
368
|
-
function
|
|
369
|
-
|
|
370
|
-
|
|
517
|
+
function UsageBreakdownLines(props) {
|
|
518
|
+
const { totals } = props;
|
|
519
|
+
if (totals.tokenBreakdown.schema === "anthropic") {
|
|
520
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["input total: ", formatInteger(totals.inputTotalTokens), " input: ", formatInteger(totals.tokenBreakdown.inputTokens), " cacheW5m: ", formatInteger(totals.tokenBreakdown.cacheWrite5mInputTokens)] }), _jsxs(Text, { children: ["cacheW1h: ", formatInteger(totals.tokenBreakdown.cacheWrite1hInputTokens), " cacheRead: ", formatInteger(totals.tokenBreakdown.cacheReadInputTokens), " output: ", formatInteger(totals.outputTokens), " reasoning: ", formatInteger(totals.reasoningOutputTokens), " total: ", formatInteger(totals.totalTokens)] })] }));
|
|
521
|
+
}
|
|
522
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["input total: ", formatInteger(totals.inputTotalTokens), " uncached: ", formatOpenAiTokens(totals, "non-cached"), " cached: ", formatOpenAiTokens(totals, "cached")] }), _jsxs(Text, { children: ["output: ", formatInteger(totals.outputTokens), " reasoning: ", formatInteger(totals.reasoningOutputTokens), " total: ", formatInteger(totals.totalTokens)] })] }));
|
|
523
|
+
}
|
|
524
|
+
function formatOpenAiTokens(totals, kind) {
|
|
525
|
+
if (totals.tokenBreakdown.schema !== "openai" || totals.cacheStatus === "unavailable") {
|
|
526
|
+
return "unknown";
|
|
371
527
|
}
|
|
528
|
+
return formatInteger(kind === "non-cached" ? totals.tokenBreakdown.nonCachedInputTokens : totals.tokenBreakdown.cachedInputTokens);
|
|
529
|
+
}
|
|
530
|
+
function formatInputPerOutput(totals) {
|
|
372
531
|
if (totals.outputTokens <= 0) {
|
|
373
|
-
return
|
|
532
|
+
return totals.tokenBreakdown.schema === "anthropic" ? "input:cacheW5m:cacheW1h:cacheRead:output = 0:0:0:0:0" : "uncached:cached:output = 0:0:0";
|
|
533
|
+
}
|
|
534
|
+
if (totals.tokenBreakdown.schema === "anthropic") {
|
|
535
|
+
return `input:cacheW5m:cacheW1h:cacheRead:output = ${formatInteger(Math.round(totals.tokenBreakdown.inputTokens / totals.outputTokens))}:${formatInteger(Math.round(totals.tokenBreakdown.cacheWrite5mInputTokens / totals.outputTokens))}:${formatInteger(Math.round(totals.tokenBreakdown.cacheWrite1hInputTokens / totals.outputTokens))}:${formatInteger(Math.round(totals.tokenBreakdown.cacheReadInputTokens / totals.outputTokens))}:1`;
|
|
536
|
+
}
|
|
537
|
+
if (totals.cacheStatus === "unavailable") {
|
|
538
|
+
return "uncached:cached:output = unknown:unknown:1";
|
|
374
539
|
}
|
|
540
|
+
return `uncached:cached:output = ${formatInteger(Math.round(totals.tokenBreakdown.nonCachedInputTokens / totals.outputTokens))}:${formatInteger(Math.round(totals.tokenBreakdown.cachedInputTokens / totals.outputTokens))}:1`;
|
|
541
|
+
}
|
|
542
|
+
function useMeasuredElementSize() {
|
|
543
|
+
const ref = useRef(null);
|
|
544
|
+
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
545
|
+
useEffect(() => {
|
|
546
|
+
if (!ref.current) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const nextSize = measureElement(ref.current);
|
|
550
|
+
setSize((current) => current.width === nextSize.width && current.height === nextSize.height ? current : nextSize);
|
|
551
|
+
});
|
|
375
552
|
return {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
553
|
+
ref,
|
|
554
|
+
width: size.width,
|
|
555
|
+
height: size.height
|
|
379
556
|
};
|
|
380
557
|
}
|
|
558
|
+
function useAutoScrollOffset(selectedIndex, rowCount, viewportSize) {
|
|
559
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
560
|
+
useEffect(() => {
|
|
561
|
+
const maxOffset = Math.max(0, rowCount - Math.max(0, viewportSize));
|
|
562
|
+
setScrollOffset((current) => {
|
|
563
|
+
let next = Math.max(0, Math.min(current, maxOffset));
|
|
564
|
+
if (selectedIndex < 0 || viewportSize <= 0) {
|
|
565
|
+
return next;
|
|
566
|
+
}
|
|
567
|
+
if (selectedIndex < next) {
|
|
568
|
+
next = selectedIndex;
|
|
569
|
+
}
|
|
570
|
+
else if (selectedIndex >= next + viewportSize) {
|
|
571
|
+
next = selectedIndex - viewportSize + 1;
|
|
572
|
+
}
|
|
573
|
+
return Math.max(0, Math.min(next, maxOffset));
|
|
574
|
+
});
|
|
575
|
+
}, [rowCount, selectedIndex, viewportSize]);
|
|
576
|
+
return scrollOffset;
|
|
577
|
+
}
|
|
578
|
+
function resolveScrollableViewportLayout(availableHeight, headerCount, bodyCount, footerCount) {
|
|
579
|
+
const totalHeight = Math.max(1, availableHeight || 1);
|
|
580
|
+
if (bodyCount === 0) {
|
|
581
|
+
const headerVisibleCount = Math.min(headerCount, totalHeight);
|
|
582
|
+
return {
|
|
583
|
+
headerVisibleCount,
|
|
584
|
+
bodyVisibleCount: 0,
|
|
585
|
+
footerVisibleCount: Math.min(footerCount, Math.max(0, totalHeight - headerVisibleCount))
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
let headerVisibleCount = Math.min(headerCount, totalHeight);
|
|
589
|
+
let footerVisibleCount = Math.min(footerCount, Math.max(0, totalHeight - headerVisibleCount - 1));
|
|
590
|
+
let bodyVisibleCount = Math.min(bodyCount, Math.max(0, totalHeight - headerVisibleCount - footerVisibleCount));
|
|
591
|
+
if (bodyVisibleCount === 0 && footerVisibleCount > 0) {
|
|
592
|
+
footerVisibleCount -= 1;
|
|
593
|
+
bodyVisibleCount = Math.min(bodyCount, Math.max(0, totalHeight - headerVisibleCount - footerVisibleCount));
|
|
594
|
+
}
|
|
595
|
+
if (bodyVisibleCount === 0 && headerVisibleCount > 0) {
|
|
596
|
+
headerVisibleCount -= 1;
|
|
597
|
+
bodyVisibleCount = Math.min(bodyCount, Math.max(0, totalHeight - headerVisibleCount - footerVisibleCount));
|
|
598
|
+
}
|
|
599
|
+
if (bodyVisibleCount === 0) {
|
|
600
|
+
return {
|
|
601
|
+
headerVisibleCount: 0,
|
|
602
|
+
bodyVisibleCount: Math.min(bodyCount, totalHeight),
|
|
603
|
+
footerVisibleCount: 0
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
return { headerVisibleCount, bodyVisibleCount, footerVisibleCount };
|
|
607
|
+
}
|
|
608
|
+
function buildScrollbarLines(viewportSize, rowCount, scrollOffset) {
|
|
609
|
+
if (viewportSize <= 0 || rowCount <= viewportSize) {
|
|
610
|
+
return [];
|
|
611
|
+
}
|
|
612
|
+
const thumbSize = Math.max(1, Math.round((viewportSize * viewportSize) / rowCount));
|
|
613
|
+
const maxThumbOffset = Math.max(0, viewportSize - thumbSize);
|
|
614
|
+
const maxScrollOffset = Math.max(1, rowCount - viewportSize);
|
|
615
|
+
const thumbOffset = Math.round((Math.max(0, Math.min(scrollOffset, maxScrollOffset)) / maxScrollOffset) * maxThumbOffset);
|
|
616
|
+
return Array.from({ length: viewportSize }, (_, index) => index >= thumbOffset && index < thumbOffset + thumbSize ? SCROLLBAR_THUMB_GLYPH : SCROLLBAR_TRACK_GLYPH);
|
|
617
|
+
}
|
|
381
618
|
function clampSelectionIndex(value, rowCount) {
|
|
382
619
|
if (rowCount === 0) {
|
|
383
620
|
return -1;
|
|
@@ -396,7 +633,7 @@ function providerUsageScore(state) {
|
|
|
396
633
|
return 0;
|
|
397
634
|
}
|
|
398
635
|
const totals = state.stats.summary.totals;
|
|
399
|
-
return totals.
|
|
636
|
+
return totals.inputTotalTokens + totals.outputTokens;
|
|
400
637
|
}
|
|
401
638
|
function getLimitRows(providerState) {
|
|
402
639
|
if (providerState.status !== "ready") {
|
|
@@ -424,7 +661,59 @@ function parseStatsOptions(argv) {
|
|
|
424
661
|
verbose: argv.includes("-v") || argv.includes("--verbose")
|
|
425
662
|
};
|
|
426
663
|
}
|
|
664
|
+
function useViewportHeight() {
|
|
665
|
+
const { stdout } = useStdout();
|
|
666
|
+
const [viewportHeight, setViewportHeight] = useState(() => resolveViewportHeight(stdout.rows));
|
|
667
|
+
useEffect(() => {
|
|
668
|
+
const updateViewportHeight = () => {
|
|
669
|
+
setViewportHeight(resolveViewportHeight(stdout.rows));
|
|
670
|
+
};
|
|
671
|
+
updateViewportHeight();
|
|
672
|
+
if (!stdout.isTTY) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
stdout.on("resize", updateViewportHeight);
|
|
676
|
+
return () => {
|
|
677
|
+
stdout.off("resize", updateViewportHeight);
|
|
678
|
+
};
|
|
679
|
+
}, [stdout]);
|
|
680
|
+
return viewportHeight;
|
|
681
|
+
}
|
|
682
|
+
function resolveViewportHeight(rows) {
|
|
683
|
+
const terminalRows = typeof rows === "number" && rows > 0 ? rows : 24;
|
|
684
|
+
// Keep Ink below the terminal height so it stays on its incremental redraw path
|
|
685
|
+
// instead of the full-screen print path that can leave the viewport scrolled.
|
|
686
|
+
return Math.max(1, terminalRows - 1);
|
|
687
|
+
}
|
|
427
688
|
export function main(argv = process.argv.slice(2)) {
|
|
428
|
-
|
|
689
|
+
const restoreFullscreen = enterFullscreenMode(process.stdout);
|
|
690
|
+
const exitHandler = () => {
|
|
691
|
+
restoreFullscreen();
|
|
692
|
+
};
|
|
693
|
+
process.once("exit", exitHandler);
|
|
694
|
+
const instance = render(_jsx(App, { statsOptions: parseStatsOptions(argv) }), {
|
|
695
|
+
stdout: process.stdout,
|
|
696
|
+
stdin: process.stdin,
|
|
697
|
+
stderr: process.stderr
|
|
698
|
+
});
|
|
699
|
+
void instance.waitUntilExit().finally(() => {
|
|
700
|
+
process.off("exit", exitHandler);
|
|
701
|
+
instance.cleanup();
|
|
702
|
+
restoreFullscreen();
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
function enterFullscreenMode(stdout) {
|
|
706
|
+
if (!stdout.isTTY) {
|
|
707
|
+
return () => { };
|
|
708
|
+
}
|
|
709
|
+
let restored = false;
|
|
710
|
+
stdout.write(ENTER_FULLSCREEN_MODE);
|
|
711
|
+
return () => {
|
|
712
|
+
if (restored) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
restored = true;
|
|
716
|
+
stdout.write(EXIT_FULLSCREEN_MODE);
|
|
717
|
+
};
|
|
429
718
|
}
|
|
430
719
|
main();
|
|
@@ -73,7 +73,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
73
73
|
.sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
|
|
74
74
|
const unknownPricedModels = modelUsage
|
|
75
75
|
.map((row) => row.modelId)
|
|
76
|
-
.filter((modelId) => !resolveRate(modelId));
|
|
76
|
+
.filter((modelId) => !resolveRate(modelId) && !isInternalClaudeModel(modelId));
|
|
77
77
|
if (unknownPricedModels.length > 0) {
|
|
78
78
|
warnings.push(`No credit rate configured for: ${unknownPricedModels.join(", ")}.`);
|
|
79
79
|
}
|
|
@@ -135,43 +135,61 @@ function resolveRate(modelId) {
|
|
|
135
135
|
}
|
|
136
136
|
return undefined;
|
|
137
137
|
}
|
|
138
|
+
function isInternalClaudeModel(modelId) {
|
|
139
|
+
return modelId === "<synthetic>";
|
|
140
|
+
}
|
|
138
141
|
function creditsFor(modelId, usage) {
|
|
139
142
|
const rate = resolveRate(modelId);
|
|
140
143
|
if (!rate) {
|
|
141
144
|
return 0;
|
|
142
145
|
}
|
|
143
|
-
const
|
|
144
|
-
const cacheWriteFallbackTokens = Math.max(0, usage.cacheCreationInputTokens - cacheWriteKnownTokens);
|
|
146
|
+
const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
|
|
145
147
|
const inferenceMultiplier = usage.inferenceGeo === "us" ? 1.1 : 1;
|
|
146
148
|
return (((usage.inputTokens / 1000000) * rate.input +
|
|
147
149
|
(usage.cacheReadInputTokens / 1000000) * rate.cacheRead +
|
|
148
|
-
(
|
|
149
|
-
(
|
|
150
|
-
(cacheWriteFallbackTokens / 1000000) * rate.cacheWrite5m +
|
|
150
|
+
(cacheWriteBreakdown.cacheWrite5mInputTokens / 1000000) * rate.cacheWrite5m +
|
|
151
|
+
(cacheWriteBreakdown.cacheWrite1hInputTokens / 1000000) * rate.cacheWrite1h +
|
|
151
152
|
(usage.outputTokens / 1000000) * rate.output) *
|
|
152
153
|
inferenceMultiplier *
|
|
153
154
|
USD_TO_CREDITS);
|
|
154
155
|
}
|
|
155
156
|
function usageToTotals(modelId, usage) {
|
|
156
|
-
const
|
|
157
|
-
const
|
|
157
|
+
const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
|
|
158
|
+
const inputTotalTokens = usage.inputTokens +
|
|
159
|
+
cacheWriteBreakdown.cacheWrite5mInputTokens +
|
|
160
|
+
cacheWriteBreakdown.cacheWrite1hInputTokens +
|
|
161
|
+
usage.cacheReadInputTokens;
|
|
158
162
|
return {
|
|
159
|
-
|
|
160
|
-
cachedInputTokens,
|
|
161
|
-
nonCachedInputTokens,
|
|
163
|
+
inputTotalTokens,
|
|
162
164
|
outputTokens: usage.outputTokens,
|
|
163
165
|
reasoningOutputTokens: 0,
|
|
164
|
-
totalTokens:
|
|
166
|
+
totalTokens: inputTotalTokens + usage.outputTokens,
|
|
165
167
|
estimatedCredits: creditsFor(modelId, usage),
|
|
166
|
-
eventCount: 1
|
|
168
|
+
eventCount: 1,
|
|
169
|
+
tokenBreakdown: {
|
|
170
|
+
schema: "anthropic",
|
|
171
|
+
inputTokens: usage.inputTokens,
|
|
172
|
+
cacheWrite5mInputTokens: cacheWriteBreakdown.cacheWrite5mInputTokens,
|
|
173
|
+
cacheWrite1hInputTokens: cacheWriteBreakdown.cacheWrite1hInputTokens,
|
|
174
|
+
cacheReadInputTokens: usage.cacheReadInputTokens,
|
|
175
|
+
outputTokens: usage.outputTokens
|
|
176
|
+
}
|
|
167
177
|
};
|
|
168
178
|
}
|
|
169
179
|
function addModelUsage(byModel, modelId, deltaTotals) {
|
|
170
180
|
const resolvedModelId = modelId || "unknown";
|
|
171
|
-
const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
|
|
181
|
+
const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals("anthropic");
|
|
172
182
|
addUsageTotals(totals, deltaTotals);
|
|
173
183
|
byModel.set(resolvedModelId, totals);
|
|
174
184
|
}
|
|
185
|
+
function resolveClaudeCacheWriteBreakdown(usage) {
|
|
186
|
+
const cacheWriteKnownTokens = usage.cacheCreation5mInputTokens + usage.cacheCreation1hInputTokens;
|
|
187
|
+
const cacheWriteFallbackTokens = Math.max(0, usage.cacheCreationInputTokens - cacheWriteKnownTokens);
|
|
188
|
+
return {
|
|
189
|
+
cacheWrite5mInputTokens: usage.cacheCreation5mInputTokens + cacheWriteFallbackTokens,
|
|
190
|
+
cacheWrite1hInputTokens: usage.cacheCreation1hInputTokens
|
|
191
|
+
};
|
|
192
|
+
}
|
|
175
193
|
function isSessionFile(filePath) {
|
|
176
194
|
return filePath.endsWith(".jsonl") && filePath.includes(`${path.sep}.claude${path.sep}projects${path.sep}`);
|
|
177
195
|
}
|
|
@@ -17,6 +17,7 @@ export class CodexUsageProvider extends UsageProviderBase {
|
|
|
17
17
|
}
|
|
18
18
|
async getStats(_options = {}) {
|
|
19
19
|
const sessionsRoot = path.join(this.root, ".codex", "sessions");
|
|
20
|
+
const knownModels = await readCodexModelMetadata(this.root);
|
|
20
21
|
const byModel = new Map();
|
|
21
22
|
const byDay = createDailyUsageAggregates();
|
|
22
23
|
const windows = createLimitWindowAggregates();
|
|
@@ -30,7 +31,7 @@ export class CodexUsageProvider extends UsageProviderBase {
|
|
|
30
31
|
};
|
|
31
32
|
for await (const file of walkSessionFiles(sessionsRoot)) {
|
|
32
33
|
parseTotals.filesScanned += 1;
|
|
33
|
-
const fileStats = await parseSessionFile(file, byModel, byDay, windows, planTypes);
|
|
34
|
+
const fileStats = await parseSessionFile(file, byModel, byDay, windows, planTypes, knownModels);
|
|
34
35
|
parseTotals.linesRead += fileStats.linesRead;
|
|
35
36
|
parseTotals.tokenEvents += fileStats.tokenEvents;
|
|
36
37
|
parseTotals.malformedLines += fileStats.malformedLines;
|
|
@@ -43,7 +44,7 @@ export class CodexUsageProvider extends UsageProviderBase {
|
|
|
43
44
|
.sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
|
|
44
45
|
const unknownPricedModels = modelUsage
|
|
45
46
|
.map((row) => row.modelId)
|
|
46
|
-
.filter((modelId) => !RATE_CARD[modelId]);
|
|
47
|
+
.filter((modelId) => !RATE_CARD[modelId] && !isAssumedZeroRatedCodexModel(modelId, knownModels));
|
|
47
48
|
if (unknownPricedModels.length > 0) {
|
|
48
49
|
warnings.push(`No credit rate configured for: ${unknownPricedModels.join(", ")}.`);
|
|
49
50
|
}
|
|
@@ -74,6 +75,39 @@ export class CodexUsageProvider extends UsageProviderBase {
|
|
|
74
75
|
};
|
|
75
76
|
}
|
|
76
77
|
}
|
|
78
|
+
async function readCodexModelMetadata(root) {
|
|
79
|
+
const modelsCachePath = path.join(root, ".codex", "models_cache.json");
|
|
80
|
+
let fileText;
|
|
81
|
+
try {
|
|
82
|
+
fileText = await fs.promises.readFile(modelsCachePath, "utf8");
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return new Map();
|
|
86
|
+
}
|
|
87
|
+
let payload;
|
|
88
|
+
try {
|
|
89
|
+
payload = JSON.parse(fileText);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return new Map();
|
|
93
|
+
}
|
|
94
|
+
const models = asRecord(payload)?.models;
|
|
95
|
+
if (!Array.isArray(models)) {
|
|
96
|
+
return new Map();
|
|
97
|
+
}
|
|
98
|
+
const metadata = new Map();
|
|
99
|
+
for (const model of models) {
|
|
100
|
+
const record = asRecord(model);
|
|
101
|
+
const slug = typeof record?.slug === "string" ? record.slug : "";
|
|
102
|
+
if (!slug) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
metadata.set(slug, {
|
|
106
|
+
visibility: typeof record?.visibility === "string" ? record.visibility : ""
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return metadata;
|
|
110
|
+
}
|
|
77
111
|
function createEmptyRawUsage() {
|
|
78
112
|
return {
|
|
79
113
|
inputTokens: 0,
|
|
@@ -107,38 +141,51 @@ function creditsFor(modelId, usage) {
|
|
|
107
141
|
if (!rate) {
|
|
108
142
|
return 0;
|
|
109
143
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return ((nonCachedInputTokens / 1000000) * rate.input +
|
|
113
|
-
(cachedInputTokens / 1000000) * rate.cachedInput +
|
|
144
|
+
return ((usage.inputTokens / 1000000) * rate.input +
|
|
145
|
+
(usage.cachedInputTokens / 1000000) * rate.cachedInput +
|
|
114
146
|
(usage.outputTokens / 1000000) * rate.output);
|
|
115
147
|
}
|
|
116
148
|
function rawUsageToTotals(usage) {
|
|
117
|
-
const
|
|
149
|
+
const inputTotalTokens = usage.inputTokens + usage.cachedInputTokens;
|
|
118
150
|
return {
|
|
119
|
-
|
|
120
|
-
cachedInputTokens,
|
|
121
|
-
nonCachedInputTokens: Math.max(0, usage.inputTokens - cachedInputTokens),
|
|
151
|
+
inputTotalTokens,
|
|
122
152
|
outputTokens: usage.outputTokens,
|
|
123
153
|
reasoningOutputTokens: usage.reasoningOutputTokens,
|
|
124
|
-
totalTokens: usage.
|
|
154
|
+
totalTokens: inputTotalTokens + usage.outputTokens,
|
|
125
155
|
estimatedCredits: 0,
|
|
126
|
-
eventCount: 0
|
|
156
|
+
eventCount: 0,
|
|
157
|
+
tokenBreakdown: {
|
|
158
|
+
schema: "openai",
|
|
159
|
+
nonCachedInputTokens: usage.inputTokens,
|
|
160
|
+
cachedInputTokens: usage.cachedInputTokens,
|
|
161
|
+
outputTokens: usage.outputTokens
|
|
162
|
+
}
|
|
127
163
|
};
|
|
128
164
|
}
|
|
129
|
-
function createUsageTotalsForModel(modelId, usage) {
|
|
165
|
+
function createUsageTotalsForModel(modelId, usage, knownModels) {
|
|
130
166
|
const resolvedModelId = modelId || "unknown";
|
|
131
167
|
const deltaTotals = rawUsageToTotals(usage);
|
|
132
168
|
deltaTotals.estimatedCredits = creditsFor(resolvedModelId, usage);
|
|
133
169
|
deltaTotals.eventCount = 1;
|
|
170
|
+
if (!RATE_CARD[resolvedModelId] && !isAssumedZeroRatedCodexModel(resolvedModelId, knownModels)) {
|
|
171
|
+
deltaTotals.estimatedCreditsStatus = "unavailable";
|
|
172
|
+
}
|
|
134
173
|
return deltaTotals;
|
|
135
174
|
}
|
|
136
175
|
function addModelUsage(byModel, modelId, deltaTotals) {
|
|
137
176
|
const resolvedModelId = modelId || "unknown";
|
|
138
|
-
const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
|
|
177
|
+
const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals("openai");
|
|
139
178
|
addUsageTotals(totals, deltaTotals);
|
|
140
179
|
byModel.set(resolvedModelId, totals);
|
|
141
180
|
}
|
|
181
|
+
function isHiddenCodexModel(modelId, knownModels) {
|
|
182
|
+
return knownModels.get(modelId)?.visibility === "hide";
|
|
183
|
+
}
|
|
184
|
+
function isAssumedZeroRatedCodexModel(modelId, knownModels) {
|
|
185
|
+
// Hidden internal Codex models do not have a public rate card entry. For dashboard
|
|
186
|
+
// rollups we treat them as zero-rated so they do not turn aggregate totals unknown.
|
|
187
|
+
return isHiddenCodexModel(modelId, knownModels);
|
|
188
|
+
}
|
|
142
189
|
function isSessionFile(filePath) {
|
|
143
190
|
return filePath.endsWith(".jsonl") && filePath.includes(`${path.sep}.codex${path.sep}sessions${path.sep}`);
|
|
144
191
|
}
|
|
@@ -160,7 +207,7 @@ async function* walkSessionFiles(directory) {
|
|
|
160
207
|
}
|
|
161
208
|
}
|
|
162
209
|
}
|
|
163
|
-
async function parseSessionFile(filePath, byModel, byDay, windows, planTypes) {
|
|
210
|
+
async function parseSessionFile(filePath, byModel, byDay, windows, planTypes, knownModels) {
|
|
164
211
|
const stream = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
165
212
|
const lineReader = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
166
213
|
let currentModel = "unknown";
|
|
@@ -201,7 +248,7 @@ async function parseSessionFile(filePath, byModel, byDay, windows, planTypes) {
|
|
|
201
248
|
const usage = lastUsage ? normalizeRawUsage(lastUsage) : previousTotal ? subtractRawUsage(totalUsage, previousTotal) : totalUsage;
|
|
202
249
|
previousTotal = totalUsage;
|
|
203
250
|
const resolvedModelId = currentModel || "unknown";
|
|
204
|
-
const deltaTotals = createUsageTotalsForModel(resolvedModelId, usage);
|
|
251
|
+
const deltaTotals = createUsageTotalsForModel(resolvedModelId, usage, knownModels);
|
|
205
252
|
tokenEvents += 1;
|
|
206
253
|
addModelUsage(byModel, resolvedModelId, deltaTotals);
|
|
207
254
|
const eventTimeMs = Date.parse(String(payloadObject.timestamp ?? ""));
|
|
@@ -4,27 +4,31 @@ export class UsageProviderBase {
|
|
|
4
4
|
this.label = label;
|
|
5
5
|
}
|
|
6
6
|
}
|
|
7
|
-
export function createEmptyUsageTotals() {
|
|
7
|
+
export function createEmptyUsageTotals(schema = "openai") {
|
|
8
8
|
return {
|
|
9
|
-
|
|
10
|
-
cachedInputTokens: 0,
|
|
11
|
-
nonCachedInputTokens: 0,
|
|
9
|
+
inputTotalTokens: 0,
|
|
12
10
|
outputTokens: 0,
|
|
13
11
|
reasoningOutputTokens: 0,
|
|
14
12
|
totalTokens: 0,
|
|
15
13
|
estimatedCredits: 0,
|
|
16
|
-
eventCount: 0
|
|
14
|
+
eventCount: 0,
|
|
15
|
+
tokenBreakdown: createEmptyUsageTokenBreakdown(schema)
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function cloneUsageTotals(totals) {
|
|
19
|
+
return {
|
|
20
|
+
...totals,
|
|
21
|
+
tokenBreakdown: { ...totals.tokenBreakdown }
|
|
17
22
|
};
|
|
18
23
|
}
|
|
19
24
|
export function addUsageTotals(target, source) {
|
|
20
|
-
target.
|
|
21
|
-
target.cachedInputTokens += source.cachedInputTokens;
|
|
22
|
-
target.nonCachedInputTokens += source.nonCachedInputTokens;
|
|
25
|
+
target.inputTotalTokens += source.inputTotalTokens;
|
|
23
26
|
target.outputTokens += source.outputTokens;
|
|
24
27
|
target.reasoningOutputTokens += source.reasoningOutputTokens;
|
|
25
28
|
target.totalTokens += source.totalTokens;
|
|
26
29
|
target.estimatedCredits += source.estimatedCredits;
|
|
27
30
|
target.eventCount += source.eventCount;
|
|
31
|
+
addUsageTokenBreakdown(target.tokenBreakdown, source.tokenBreakdown);
|
|
28
32
|
if (source.cacheStatus === "unavailable") {
|
|
29
33
|
target.cacheStatus = "unavailable";
|
|
30
34
|
}
|
|
@@ -33,9 +37,44 @@ export function addUsageTotals(target, source) {
|
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
export function sumUsageTotals(rows) {
|
|
36
|
-
const totals = createEmptyUsageTotals();
|
|
40
|
+
const totals = createEmptyUsageTotals(rows[0]?.tokenBreakdown.schema ?? "openai");
|
|
37
41
|
for (const row of rows) {
|
|
38
42
|
addUsageTotals(totals, row);
|
|
39
43
|
}
|
|
40
44
|
return totals;
|
|
41
45
|
}
|
|
46
|
+
function createEmptyUsageTokenBreakdown(schema) {
|
|
47
|
+
if (schema === "anthropic") {
|
|
48
|
+
return {
|
|
49
|
+
schema,
|
|
50
|
+
inputTokens: 0,
|
|
51
|
+
cacheWrite5mInputTokens: 0,
|
|
52
|
+
cacheWrite1hInputTokens: 0,
|
|
53
|
+
cacheReadInputTokens: 0,
|
|
54
|
+
outputTokens: 0
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
schema,
|
|
59
|
+
nonCachedInputTokens: 0,
|
|
60
|
+
cachedInputTokens: 0,
|
|
61
|
+
outputTokens: 0
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function addUsageTokenBreakdown(target, source) {
|
|
65
|
+
if (target.schema !== source.schema) {
|
|
66
|
+
throw new Error(`Cannot merge ${source.schema} usage totals into ${target.schema} totals.`);
|
|
67
|
+
}
|
|
68
|
+
target.outputTokens += source.outputTokens;
|
|
69
|
+
if (target.schema === "anthropic" && source.schema === "anthropic") {
|
|
70
|
+
target.inputTokens += source.inputTokens;
|
|
71
|
+
target.cacheWrite5mInputTokens += source.cacheWrite5mInputTokens;
|
|
72
|
+
target.cacheWrite1hInputTokens += source.cacheWrite1hInputTokens;
|
|
73
|
+
target.cacheReadInputTokens += source.cacheReadInputTokens;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (target.schema === "openai" && source.schema === "openai") {
|
|
77
|
+
target.nonCachedInputTokens += source.nonCachedInputTokens;
|
|
78
|
+
target.cachedInputTokens += source.cachedInputTokens;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -222,16 +222,24 @@ function isCopilotChatSpan(attributes) {
|
|
|
222
222
|
function createUsageTotals(modelId, usage) {
|
|
223
223
|
const hasCacheInfo = usage.cachedInputTokens !== undefined || usage.cacheCreationInputTokens !== undefined;
|
|
224
224
|
const hasKnownCreditPricing = isNonBillableModel(modelId) || (hasCacheInfo && rateForModel(modelId, usage.inputTokens) !== undefined);
|
|
225
|
-
const cachedInputTokens = hasCacheInfo ? Math.
|
|
225
|
+
const cachedInputTokens = hasCacheInfo ? Math.max(0, usage.cachedInputTokens ?? 0) : 0;
|
|
226
|
+
const cacheWriteInputTokens = hasCacheInfo ? Math.max(0, usage.cacheCreationInputTokens ?? 0) : 0;
|
|
227
|
+
const uncachedInputTokens = hasCacheInfo
|
|
228
|
+
? Math.max(0, usage.inputTokens - cachedInputTokens - cacheWriteInputTokens)
|
|
229
|
+
: 0;
|
|
226
230
|
return {
|
|
227
|
-
|
|
228
|
-
cachedInputTokens,
|
|
229
|
-
nonCachedInputTokens: hasCacheInfo ? Math.max(0, usage.inputTokens - cachedInputTokens) : 0,
|
|
231
|
+
inputTotalTokens: usage.inputTokens,
|
|
230
232
|
outputTokens: usage.outputTokens,
|
|
231
233
|
reasoningOutputTokens: Math.min(usage.reasoningOutputTokens ?? 0, usage.outputTokens),
|
|
232
234
|
totalTokens: usage.inputTokens + usage.outputTokens,
|
|
233
235
|
estimatedCredits: creditsFor(modelId, usage),
|
|
234
236
|
eventCount: 1,
|
|
237
|
+
tokenBreakdown: {
|
|
238
|
+
schema: "openai",
|
|
239
|
+
nonCachedInputTokens: uncachedInputTokens,
|
|
240
|
+
cachedInputTokens,
|
|
241
|
+
outputTokens: usage.outputTokens
|
|
242
|
+
},
|
|
235
243
|
cacheStatus: hasCacheInfo ? "known" : "unavailable",
|
|
236
244
|
estimatedCreditsStatus: hasKnownCreditPricing ? "known" : "unavailable"
|
|
237
245
|
};
|
|
@@ -272,7 +280,7 @@ function isNonBillableModel(modelId) {
|
|
|
272
280
|
}
|
|
273
281
|
function addModelUsage(byModel, modelId, deltaTotals) {
|
|
274
282
|
const resolvedModelId = modelId || "unknown";
|
|
275
|
-
const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
|
|
283
|
+
const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals("openai");
|
|
276
284
|
addUsageTotals(totals, deltaTotals);
|
|
277
285
|
byModel.set(resolvedModelId, totals);
|
|
278
286
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { addUsageTotals } from "./contract.js";
|
|
1
|
+
import { addUsageTotals, cloneUsageTotals } from "./contract.js";
|
|
2
2
|
export function createDailyUsageAggregates() {
|
|
3
3
|
return new Map();
|
|
4
4
|
}
|
|
@@ -18,7 +18,7 @@ export function addDailyUsage(rows, eventTimeMs, modelId, planType, deltaTotals)
|
|
|
18
18
|
sortTimeMs,
|
|
19
19
|
firstEventMs: Number.isFinite(eventTimeMs) ? eventTimeMs : null,
|
|
20
20
|
lastEventMs: Number.isFinite(eventTimeMs) ? eventTimeMs : null,
|
|
21
|
-
totals:
|
|
21
|
+
totals: cloneUsageTotals(deltaTotals),
|
|
22
22
|
models,
|
|
23
23
|
planTypes
|
|
24
24
|
});
|
|
@@ -46,7 +46,7 @@ export function buildDailyUsageRows(rows) {
|
|
|
46
46
|
lastEventUtcIso: row.lastEventMs === null ? null : formatIsoFromMilliseconds(row.lastEventMs),
|
|
47
47
|
distinctModels: [...row.models].sort(),
|
|
48
48
|
distinctPlanTypes: [...row.planTypes].sort(),
|
|
49
|
-
totals:
|
|
49
|
+
totals: cloneUsageTotals(row.totals)
|
|
50
50
|
}));
|
|
51
51
|
}
|
|
52
52
|
function resolveDayBucket(eventTimeMs) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { addUsageTotals, createEmptyUsageTotals } from "./contract.js";
|
|
1
|
+
import { addUsageTotals, cloneUsageTotals, createEmptyUsageTotals } from "./contract.js";
|
|
2
2
|
export function createLimitWindowAggregates() {
|
|
3
3
|
return new Map();
|
|
4
4
|
}
|
|
@@ -71,7 +71,7 @@ function collapseNearbyWindows(rows) {
|
|
|
71
71
|
if (!existing) {
|
|
72
72
|
collapsed.set(key, {
|
|
73
73
|
...row,
|
|
74
|
-
totals:
|
|
74
|
+
totals: cloneUsageTotals(row.totals)
|
|
75
75
|
});
|
|
76
76
|
continue;
|
|
77
77
|
}
|
|
@@ -93,7 +93,7 @@ function collapseNearbyWindows(rows) {
|
|
|
93
93
|
function computeWindowTotals(events) {
|
|
94
94
|
// Session files are not guaranteed to be parsed in timestamp order, so
|
|
95
95
|
// saturation has to be applied after we sort the captured window events.
|
|
96
|
-
const totals = createEmptyUsageTotals();
|
|
96
|
+
const totals = createEmptyUsageTotals(events[0]?.totals.tokenBreakdown.schema ?? "openai");
|
|
97
97
|
let sawBelowCap = false;
|
|
98
98
|
let isExhausted = false;
|
|
99
99
|
for (const event of [...events].sort((left, right) => left.eventTimeMs - right.eventTimeMs)) {
|
|
@@ -132,7 +132,7 @@ function upsertWindow(windows, scope, rateLimits, window, eventTimeMs, deltaTota
|
|
|
132
132
|
lastSeenMs: eventTimeMs,
|
|
133
133
|
minUsedPercent: usedPercent,
|
|
134
134
|
maxUsedPercent: usedPercent,
|
|
135
|
-
events: [{ eventTimeMs, usedPercent, totals:
|
|
135
|
+
events: [{ eventTimeMs, usedPercent, totals: cloneUsageTotals(deltaTotals) }]
|
|
136
136
|
});
|
|
137
137
|
return;
|
|
138
138
|
}
|
|
@@ -142,5 +142,5 @@ function upsertWindow(windows, scope, rateLimits, window, eventTimeMs, deltaTota
|
|
|
142
142
|
existing.lastSeenMs = Math.max(existing.lastSeenMs, eventTimeMs);
|
|
143
143
|
existing.minUsedPercent = Math.min(existing.minUsedPercent, usedPercent);
|
|
144
144
|
existing.maxUsedPercent = Math.max(existing.maxUsedPercent, usedPercent);
|
|
145
|
-
existing.events.push({ eventTimeMs, usedPercent, totals:
|
|
145
|
+
existing.events.push({ eventTimeMs, usedPercent, totals: cloneUsageTotals(deltaTotals) });
|
|
146
146
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letmecode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Provider-based terminal usage dashboard for LetMeCode.",
|
|
5
5
|
"author": "devforth.io",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"packageManager": "pnpm@10.28.2",
|
|
7
8
|
"type": "commonjs",
|
|
8
9
|
"bin": "./bin/letmecode.js",
|
|
9
10
|
"files": [
|
|
@@ -18,6 +19,16 @@
|
|
|
18
19
|
"publishConfig": {
|
|
19
20
|
"access": "public"
|
|
20
21
|
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
|
|
24
|
+
"build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
|
|
25
|
+
"prepack": "npm run build",
|
|
26
|
+
"prestart": "npm run build",
|
|
27
|
+
"start": "node ./bin/letmecode.js",
|
|
28
|
+
"pretest": "npm run build",
|
|
29
|
+
"smoke": "node ./bin/letmecode.js",
|
|
30
|
+
"test": "node --test ink-app/test/*.test.mjs"
|
|
31
|
+
},
|
|
21
32
|
"keywords": [
|
|
22
33
|
"cli",
|
|
23
34
|
"ink",
|
|
@@ -33,14 +44,5 @@
|
|
|
33
44
|
"@types/node": "^24.0.7",
|
|
34
45
|
"@types/react": "^18.3.24",
|
|
35
46
|
"typescript": "^5.8.3"
|
|
36
|
-
},
|
|
37
|
-
"scripts": {
|
|
38
|
-
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
|
|
39
|
-
"build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
|
|
40
|
-
"prestart": "npm run build",
|
|
41
|
-
"start": "node ./bin/letmecode.js",
|
|
42
|
-
"pretest": "npm run build",
|
|
43
|
-
"smoke": "node ./bin/letmecode.js",
|
|
44
|
-
"test": "node --test ink-app/test/*.test.mjs"
|
|
45
47
|
}
|
|
46
|
-
}
|
|
48
|
+
}
|