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.
@@ -17,22 +17,41 @@ const LIMIT_WINDOW_COLUMNS = {
17
17
  date: 17,
18
18
  value: 10
19
19
  };
20
- const MODEL_USAGE_COLUMNS = {
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 DAY_USAGE_COLUMNS = {
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
- const inputPerOutput = formatInputPerOutput(summary.totals);
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
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "gray", children: [pad("model", MODEL_USAGE_COLUMNS.model), " ", pad("input", MODEL_USAGE_COLUMNS.input), " ", pad("cached", MODEL_USAGE_COLUMNS.cached), " ", pad("non-cached", MODEL_USAGE_COLUMNS.nonCached), " ", pad("output", MODEL_USAGE_COLUMNS.output), " ", pad("credits", MODEL_USAGE_COLUMNS.credits), " value"] }), props.stats.modelUsage.map((row) => {
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
- return (_jsxs(Text, { inverse: isSelected, color: isSelected ? "cyan" : undefined, children: [pad(row.modelId, MODEL_USAGE_COLUMNS.model), " ", pad(formatInteger(row.totals.inputTokens), MODEL_USAGE_COLUMNS.input), " ", pad(formatCacheTokens(row.totals, "cached"), MODEL_USAGE_COLUMNS.cached), " ", pad(formatCacheTokens(row.totals, "non-cached"), MODEL_USAGE_COLUMNS.nonCached), " ", pad(formatInteger(row.totals.outputTokens), MODEL_USAGE_COLUMNS.output), " ", pad(formatUsageCredits(row.totals), MODEL_USAGE_COLUMNS.credits), " ", pad(formatUsageUsd(row.totals), MODEL_USAGE_COLUMNS.value)] }, row.modelId));
256
- }), _jsxs(Text, { color: "cyan", children: [pad("TOTAL", MODEL_USAGE_COLUMNS.model), " ", pad(formatInteger(totals.inputTokens), MODEL_USAGE_COLUMNS.input), " ", pad(formatCacheTokens(totals, "cached"), MODEL_USAGE_COLUMNS.cached), " ", pad(formatCacheTokens(totals, "non-cached"), MODEL_USAGE_COLUMNS.nonCached), " ", pad(formatInteger(totals.outputTokens), MODEL_USAGE_COLUMNS.output), " ", pad(formatUsageCredits(totals), MODEL_USAGE_COLUMNS.credits), " ", pad(formatUsageUsd(totals), MODEL_USAGE_COLUMNS.value)] })] }));
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
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "gray", children: [pad("day", DAY_USAGE_COLUMNS.day), " ", pad("events", DAY_USAGE_COLUMNS.events), " ", pad("input", DAY_USAGE_COLUMNS.input), " ", pad("output", DAY_USAGE_COLUMNS.output), " value"] }), props.stats.dayUsage.map((row) => {
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
- return (_jsxs(Text, { inverse: isSelected, color: isSelected ? "cyan" : undefined, children: [pad(formatUtcDay(row.dayKey), DAY_USAGE_COLUMNS.day), " ", pad(formatInteger(row.totals.eventCount), DAY_USAGE_COLUMNS.events), " ", pad(formatInteger(row.totals.inputTokens), DAY_USAGE_COLUMNS.input), " ", pad(formatInteger(row.totals.outputTokens), DAY_USAGE_COLUMNS.output), " ", pad(formatUsageUsd(row.totals), DAY_USAGE_COLUMNS.value)] }, row.dayKey));
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: ["input: ", formatInteger(row.totals.inputTokens), " cached: ", formatCacheTokens(row.totals, "cached")] }), _jsxs(Text, { children: ["non-cached: ", formatCacheTokens(row.totals, "non-cached"), " output: ", formatInteger(row.totals.outputTokens)] }), _jsxs(Text, { children: ["models: ", row.distinctModels.join(", ") || "none"] }), _jsxs(Text, { children: ["plans: ", row.distinctPlanTypes.join(", ") || "none"] }), _jsx(UsageTotalsDetails, { totals: row.totals })] }));
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
- const inputPerOutput = formatInputPerOutput(totals);
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 formatInputPerOutput(totals) {
369
- if (totals.cacheStatus === "unavailable") {
370
- return { cached: "unknown", nonCached: "unknown", output: "1" };
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 { cached: "0", nonCached: "0", output: "0" };
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
- return {
376
- cached: formatInteger(Math.round(totals.cachedInputTokens / totals.outputTokens)),
377
- nonCached: formatInteger(Math.round(totals.nonCachedInputTokens / totals.outputTokens)),
378
- output: "1"
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.inputTokens + totals.cachedInputTokens + totals.outputTokens;
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 cacheWriteKnownTokens = usage.cacheCreation5mInputTokens + usage.cacheCreation1hInputTokens;
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
- (usage.cacheCreation5mInputTokens / 1000000) * rate.cacheWrite5m +
149
- (usage.cacheCreation1hInputTokens / 1000000) * rate.cacheWrite1h +
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 nonCachedInputTokens = usage.inputTokens + usage.cacheCreationInputTokens;
157
- const cachedInputTokens = usage.cacheReadInputTokens;
154
+ const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
155
+ const inputTotalTokens = usage.inputTokens +
156
+ cacheWriteBreakdown.cacheWrite5mInputTokens +
157
+ cacheWriteBreakdown.cacheWrite1hInputTokens +
158
+ usage.cacheReadInputTokens;
158
159
  return {
159
- inputTokens: nonCachedInputTokens + cachedInputTokens,
160
- cachedInputTokens,
161
- nonCachedInputTokens,
160
+ inputTotalTokens,
162
161
  outputTokens: usage.outputTokens,
163
162
  reasoningOutputTokens: 0,
164
- totalTokens: nonCachedInputTokens + cachedInputTokens + usage.outputTokens,
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
- const cachedInputTokens = Math.min(usage.cachedInputTokens, usage.inputTokens);
111
- const nonCachedInputTokens = Math.max(0, usage.inputTokens - cachedInputTokens);
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 cachedInputTokens = Math.min(usage.cachedInputTokens, usage.inputTokens);
115
+ const inputTotalTokens = usage.inputTokens + usage.cachedInputTokens;
118
116
  return {
119
- inputTokens: usage.inputTokens,
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.totalTokens,
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
- inputTokens: 0,
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.inputTokens += source.inputTokens;
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.min(usage.cachedInputTokens ?? 0, usage.inputTokens) : 0;
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
- inputTokens: usage.inputTokens,
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: { ...deltaTotals },
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: { ...row.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: { ...row.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: { ...deltaTotals } }]
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: { ...deltaTotals } });
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.2",
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
+ }