letmecode 0.1.2 → 0.1.3
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 +77 -29
- package/ink-app/dist/providers/claude.js +28 -13
- package/ink-app/dist/providers/codex.js +13 -11
- 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
|
@@ -17,22 +17,41 @@ 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
|
];
|
|
@@ -210,8 +229,7 @@ function VerticalTab(props) {
|
|
|
210
229
|
}
|
|
211
230
|
function SummaryPanel(props) {
|
|
212
231
|
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"] })] }));
|
|
232
|
+
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
233
|
}
|
|
216
234
|
function ContentPanel(props) {
|
|
217
235
|
if (props.providerState.status === "loading") {
|
|
@@ -250,18 +268,43 @@ function UsageByModelPanel(props) {
|
|
|
250
268
|
return _jsx(Text, { color: "gray", children: "No model usage found." });
|
|
251
269
|
}
|
|
252
270
|
const totals = props.stats.summary.totals;
|
|
253
|
-
|
|
271
|
+
if (totals.tokenBreakdown.schema === "anthropic") {
|
|
272
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "gray", children: [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"] }), props.stats.modelUsage.map((row) => {
|
|
273
|
+
const isSelected = props.selectedModelId === row.modelId;
|
|
274
|
+
if (row.totals.tokenBreakdown.schema !== "anthropic") {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
return (_jsxs(Text, { inverse: isSelected, color: isSelected ? "cyan" : undefined, children: [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), ANTHROPIC_MODEL_USAGE_COLUMNS.credits), " ", pad(formatUsageUsd(row.totals), ANTHROPIC_MODEL_USAGE_COLUMNS.value)] }, row.modelId));
|
|
278
|
+
}), _jsxs(Text, { color: "cyan", children: [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)] })] }));
|
|
279
|
+
}
|
|
280
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "gray", children: [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"] }), props.stats.modelUsage.map((row) => {
|
|
254
281
|
const isSelected = props.selectedModelId === row.modelId;
|
|
255
|
-
|
|
256
|
-
|
|
282
|
+
if (row.totals.tokenBreakdown.schema !== "openai") {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
return (_jsxs(Text, { inverse: isSelected, color: isSelected ? "cyan" : undefined, children: [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), OPENAI_MODEL_USAGE_COLUMNS.credits), " ", pad(formatUsageUsd(row.totals), OPENAI_MODEL_USAGE_COLUMNS.value)] }, row.modelId));
|
|
286
|
+
}), _jsxs(Text, { color: "cyan", children: [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)] })] }));
|
|
257
287
|
}
|
|
258
288
|
function DayToDayPanel(props) {
|
|
259
289
|
if (props.stats.dayUsage.length === 0) {
|
|
260
290
|
return _jsx(Text, { color: "gray", children: "No day-by-day usage found." });
|
|
261
291
|
}
|
|
262
|
-
|
|
292
|
+
const totals = props.stats.summary.totals;
|
|
293
|
+
if (totals.tokenBreakdown.schema === "anthropic") {
|
|
294
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "gray", children: [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"] }), props.stats.dayUsage.map((row) => {
|
|
295
|
+
const isSelected = props.selectedDayKey === row.dayKey;
|
|
296
|
+
if (row.totals.tokenBreakdown.schema !== "anthropic") {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
return (_jsxs(Text, { inverse: isSelected, color: isSelected ? "cyan" : undefined, children: [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)] }, row.dayKey));
|
|
300
|
+
})] }));
|
|
301
|
+
}
|
|
302
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "gray", children: [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"] }), props.stats.dayUsage.map((row) => {
|
|
263
303
|
const isSelected = props.selectedDayKey === row.dayKey;
|
|
264
|
-
|
|
304
|
+
if (row.totals.tokenBreakdown.schema !== "openai") {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
return (_jsxs(Text, { inverse: isSelected, color: isSelected ? "cyan" : undefined, children: [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)] }, row.dayKey));
|
|
265
308
|
})] }));
|
|
266
309
|
}
|
|
267
310
|
function SelectionDetailsPanel(props) {
|
|
@@ -274,7 +317,7 @@ function SelectionDetailsPanel(props) {
|
|
|
274
317
|
}
|
|
275
318
|
if (props.tabId === "day-to-day-analyses" && props.selectedDayRow) {
|
|
276
319
|
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: ["
|
|
320
|
+
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
321
|
}
|
|
279
322
|
if (props.tabId === "usage-by-model" && props.selectedModelRow) {
|
|
280
323
|
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 })] }));
|
|
@@ -283,8 +326,7 @@ function SelectionDetailsPanel(props) {
|
|
|
283
326
|
}
|
|
284
327
|
function UsageTotalsDetails(props) {
|
|
285
328
|
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] })] }));
|
|
329
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(UsageBreakdownLines, { totals: totals }), _jsxs(Text, { children: ["Total credits burned: ", formatUsageCredits(totals)] }), _jsxs(Text, { children: ["Credits Value (@ $0.01/credit): ", formatUsageUsd(totals)] }), _jsxs(Text, { children: ["IpO: ", formatInputPerOutput(totals)] })] }));
|
|
288
330
|
}
|
|
289
331
|
function formatInteger(value) {
|
|
290
332
|
return Math.round(value).toLocaleString("en-US");
|
|
@@ -306,12 +348,6 @@ function formatUsageUsd(totals) {
|
|
|
306
348
|
? "unknown"
|
|
307
349
|
: formatUsd(totals.estimatedCredits * CODEX_CREDIT_COST_USD);
|
|
308
350
|
}
|
|
309
|
-
function formatCacheTokens(totals, kind) {
|
|
310
|
-
if (totals.cacheStatus === "unavailable") {
|
|
311
|
-
return "unknown";
|
|
312
|
-
}
|
|
313
|
-
return formatInteger(kind === "cached" ? totals.cachedInputTokens : totals.nonCachedInputTokens);
|
|
314
|
-
}
|
|
315
351
|
function formatUsd(value) {
|
|
316
352
|
if (value > 0 && value < 0.0001) {
|
|
317
353
|
return "<$0.0001";
|
|
@@ -365,18 +401,30 @@ function formatEventRange(firstEventUtcIso, lastEventUtcIso) {
|
|
|
365
401
|
function pad(value, length) {
|
|
366
402
|
return value.length >= length ? value.slice(0, length) : value.padEnd(length);
|
|
367
403
|
}
|
|
368
|
-
function
|
|
369
|
-
|
|
370
|
-
|
|
404
|
+
function UsageBreakdownLines(props) {
|
|
405
|
+
const { totals } = props;
|
|
406
|
+
if (totals.tokenBreakdown.schema === "anthropic") {
|
|
407
|
+
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)] })] }));
|
|
408
|
+
}
|
|
409
|
+
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)] })] }));
|
|
410
|
+
}
|
|
411
|
+
function formatOpenAiTokens(totals, kind) {
|
|
412
|
+
if (totals.tokenBreakdown.schema !== "openai" || totals.cacheStatus === "unavailable") {
|
|
413
|
+
return "unknown";
|
|
371
414
|
}
|
|
415
|
+
return formatInteger(kind === "non-cached" ? totals.tokenBreakdown.nonCachedInputTokens : totals.tokenBreakdown.cachedInputTokens);
|
|
416
|
+
}
|
|
417
|
+
function formatInputPerOutput(totals) {
|
|
372
418
|
if (totals.outputTokens <= 0) {
|
|
373
|
-
return
|
|
419
|
+
return totals.tokenBreakdown.schema === "anthropic" ? "input:cacheW5m:cacheW1h:cacheRead:output = 0:0:0:0:0" : "uncached:cached:output = 0:0:0";
|
|
374
420
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
421
|
+
if (totals.tokenBreakdown.schema === "anthropic") {
|
|
422
|
+
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`;
|
|
423
|
+
}
|
|
424
|
+
if (totals.cacheStatus === "unavailable") {
|
|
425
|
+
return "uncached:cached:output = unknown:unknown:1";
|
|
426
|
+
}
|
|
427
|
+
return `uncached:cached:output = ${formatInteger(Math.round(totals.tokenBreakdown.nonCachedInputTokens / totals.outputTokens))}:${formatInteger(Math.round(totals.tokenBreakdown.cachedInputTokens / totals.outputTokens))}:1`;
|
|
380
428
|
}
|
|
381
429
|
function clampSelectionIndex(value, rowCount) {
|
|
382
430
|
if (rowCount === 0) {
|
|
@@ -396,7 +444,7 @@ function providerUsageScore(state) {
|
|
|
396
444
|
return 0;
|
|
397
445
|
}
|
|
398
446
|
const totals = state.stats.summary.totals;
|
|
399
|
-
return totals.
|
|
447
|
+
return totals.inputTotalTokens + totals.outputTokens;
|
|
400
448
|
}
|
|
401
449
|
function getLimitRows(providerState) {
|
|
402
450
|
if (providerState.status !== "ready") {
|
|
@@ -140,38 +140,53 @@ function creditsFor(modelId, usage) {
|
|
|
140
140
|
if (!rate) {
|
|
141
141
|
return 0;
|
|
142
142
|
}
|
|
143
|
-
const
|
|
144
|
-
const cacheWriteFallbackTokens = Math.max(0, usage.cacheCreationInputTokens - cacheWriteKnownTokens);
|
|
143
|
+
const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
|
|
145
144
|
const inferenceMultiplier = usage.inferenceGeo === "us" ? 1.1 : 1;
|
|
146
145
|
return (((usage.inputTokens / 1000000) * rate.input +
|
|
147
146
|
(usage.cacheReadInputTokens / 1000000) * rate.cacheRead +
|
|
148
|
-
(
|
|
149
|
-
(
|
|
150
|
-
(cacheWriteFallbackTokens / 1000000) * rate.cacheWrite5m +
|
|
147
|
+
(cacheWriteBreakdown.cacheWrite5mInputTokens / 1000000) * rate.cacheWrite5m +
|
|
148
|
+
(cacheWriteBreakdown.cacheWrite1hInputTokens / 1000000) * rate.cacheWrite1h +
|
|
151
149
|
(usage.outputTokens / 1000000) * rate.output) *
|
|
152
150
|
inferenceMultiplier *
|
|
153
151
|
USD_TO_CREDITS);
|
|
154
152
|
}
|
|
155
153
|
function usageToTotals(modelId, usage) {
|
|
156
|
-
const
|
|
157
|
-
const
|
|
154
|
+
const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
|
|
155
|
+
const inputTotalTokens = usage.inputTokens +
|
|
156
|
+
cacheWriteBreakdown.cacheWrite5mInputTokens +
|
|
157
|
+
cacheWriteBreakdown.cacheWrite1hInputTokens +
|
|
158
|
+
usage.cacheReadInputTokens;
|
|
158
159
|
return {
|
|
159
|
-
|
|
160
|
-
cachedInputTokens,
|
|
161
|
-
nonCachedInputTokens,
|
|
160
|
+
inputTotalTokens,
|
|
162
161
|
outputTokens: usage.outputTokens,
|
|
163
162
|
reasoningOutputTokens: 0,
|
|
164
|
-
totalTokens:
|
|
163
|
+
totalTokens: inputTotalTokens + usage.outputTokens,
|
|
165
164
|
estimatedCredits: creditsFor(modelId, usage),
|
|
166
|
-
eventCount: 1
|
|
165
|
+
eventCount: 1,
|
|
166
|
+
tokenBreakdown: {
|
|
167
|
+
schema: "anthropic",
|
|
168
|
+
inputTokens: usage.inputTokens,
|
|
169
|
+
cacheWrite5mInputTokens: cacheWriteBreakdown.cacheWrite5mInputTokens,
|
|
170
|
+
cacheWrite1hInputTokens: cacheWriteBreakdown.cacheWrite1hInputTokens,
|
|
171
|
+
cacheReadInputTokens: usage.cacheReadInputTokens,
|
|
172
|
+
outputTokens: usage.outputTokens
|
|
173
|
+
}
|
|
167
174
|
};
|
|
168
175
|
}
|
|
169
176
|
function addModelUsage(byModel, modelId, deltaTotals) {
|
|
170
177
|
const resolvedModelId = modelId || "unknown";
|
|
171
|
-
const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
|
|
178
|
+
const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals("anthropic");
|
|
172
179
|
addUsageTotals(totals, deltaTotals);
|
|
173
180
|
byModel.set(resolvedModelId, totals);
|
|
174
181
|
}
|
|
182
|
+
function resolveClaudeCacheWriteBreakdown(usage) {
|
|
183
|
+
const cacheWriteKnownTokens = usage.cacheCreation5mInputTokens + usage.cacheCreation1hInputTokens;
|
|
184
|
+
const cacheWriteFallbackTokens = Math.max(0, usage.cacheCreationInputTokens - cacheWriteKnownTokens);
|
|
185
|
+
return {
|
|
186
|
+
cacheWrite5mInputTokens: usage.cacheCreation5mInputTokens + cacheWriteFallbackTokens,
|
|
187
|
+
cacheWrite1hInputTokens: usage.cacheCreation1hInputTokens
|
|
188
|
+
};
|
|
189
|
+
}
|
|
175
190
|
function isSessionFile(filePath) {
|
|
176
191
|
return filePath.endsWith(".jsonl") && filePath.includes(`${path.sep}.claude${path.sep}projects${path.sep}`);
|
|
177
192
|
}
|
|
@@ -107,23 +107,25 @@ function creditsFor(modelId, usage) {
|
|
|
107
107
|
if (!rate) {
|
|
108
108
|
return 0;
|
|
109
109
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return ((nonCachedInputTokens / 1000000) * rate.input +
|
|
113
|
-
(cachedInputTokens / 1000000) * rate.cachedInput +
|
|
110
|
+
return ((usage.inputTokens / 1000000) * rate.input +
|
|
111
|
+
(usage.cachedInputTokens / 1000000) * rate.cachedInput +
|
|
114
112
|
(usage.outputTokens / 1000000) * rate.output);
|
|
115
113
|
}
|
|
116
114
|
function rawUsageToTotals(usage) {
|
|
117
|
-
const
|
|
115
|
+
const inputTotalTokens = usage.inputTokens + usage.cachedInputTokens;
|
|
118
116
|
return {
|
|
119
|
-
|
|
120
|
-
cachedInputTokens,
|
|
121
|
-
nonCachedInputTokens: Math.max(0, usage.inputTokens - cachedInputTokens),
|
|
117
|
+
inputTotalTokens,
|
|
122
118
|
outputTokens: usage.outputTokens,
|
|
123
119
|
reasoningOutputTokens: usage.reasoningOutputTokens,
|
|
124
|
-
totalTokens: usage.
|
|
120
|
+
totalTokens: inputTotalTokens + usage.outputTokens,
|
|
125
121
|
estimatedCredits: 0,
|
|
126
|
-
eventCount: 0
|
|
122
|
+
eventCount: 0,
|
|
123
|
+
tokenBreakdown: {
|
|
124
|
+
schema: "openai",
|
|
125
|
+
nonCachedInputTokens: usage.inputTokens,
|
|
126
|
+
cachedInputTokens: usage.cachedInputTokens,
|
|
127
|
+
outputTokens: usage.outputTokens
|
|
128
|
+
}
|
|
127
129
|
};
|
|
128
130
|
}
|
|
129
131
|
function createUsageTotalsForModel(modelId, usage) {
|
|
@@ -135,7 +137,7 @@ function createUsageTotalsForModel(modelId, usage) {
|
|
|
135
137
|
}
|
|
136
138
|
function addModelUsage(byModel, modelId, deltaTotals) {
|
|
137
139
|
const resolvedModelId = modelId || "unknown";
|
|
138
|
-
const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
|
|
140
|
+
const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals("openai");
|
|
139
141
|
addUsageTotals(totals, deltaTotals);
|
|
140
142
|
byModel.set(resolvedModelId, totals);
|
|
141
143
|
}
|
|
@@ -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.3",
|
|
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
|
+
}
|