llm-usage-metrics 0.3.5 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -63,6 +63,8 @@ llm-usage daily
63
63
 
64
64
  OpenCode source support requires Node.js 24+ runtime with built-in `node:sqlite`.
65
65
 
66
+ For `droid`, `Input`, `Output`, `Reasoning`, `Cache Read`, and `Cache Write` come directly from session files, and `totalTokens` is billable raw tokens (`Input + Output + Cache Read + Cache Write`, excluding `Reasoning`). Factory dashboard totals may differ because Factory applies standard-token normalization/multipliers.
67
+
66
68
  ## 🎯 Usage
67
69
 
68
70
  ### Basic Reports
@@ -256,6 +258,8 @@ pnpm run perf:production-benchmark -- \
256
258
  | `LLM_USAGE_PARSE_MAX_PARALLEL` | Max parallel file parses (`1-64`) |
257
259
  | `LLM_USAGE_PARSE_CACHE_ENABLED` | Enable parse cache (`1/0`) |
258
260
 
261
+ Parse cache is source-sharded on disk (`parse-file-cache.<source>.json`) so source-scoped runs avoid loading unrelated cache blobs.
262
+
259
263
  See full environment variable reference in the [documentation](https://ayagmar.github.io/llm-usage-metrics/configuration/).
260
264
 
261
265
  ### Update Checks
package/dist/index.js CHANGED
@@ -1217,7 +1217,6 @@ var DroidSourceAdapter = class {
1217
1217
  toNumberLike(tokenUsage.cacheCreationTokens)
1218
1218
  );
1219
1219
  const billableTokens = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
1220
- const totalTokens = billableTokens + reasoningTokens;
1221
1220
  if (billableTokens === 0) {
1222
1221
  skippedRows++;
1223
1222
  incrementSkippedReason(skippedRowReasons, "no_token_usage");
@@ -1225,6 +1224,7 @@ var DroidSourceAdapter = class {
1225
1224
  }
1226
1225
  const provider = asTrimmedText(settings.providerLock);
1227
1226
  const model = asTrimmedText(settings.model);
1227
+ const totalTokens = billableTokens;
1228
1228
  const primaryTimestamp = normalizeTimestampCandidate(settings.providerLockTimestamp);
1229
1229
  const hasValidPrimaryTimestamp = Boolean(primaryTimestamp);
1230
1230
  const jsonlPath = getSiblingJsonlPath(filePath);
@@ -1243,7 +1243,9 @@ var DroidSourceAdapter = class {
1243
1243
  }
1244
1244
  if (!hasValidPrimaryTimestamp && isMessageRecord(line)) {
1245
1245
  fallbackMessageTimestamp = normalizeTimestampCandidate(line.timestamp);
1246
- break;
1246
+ if (fallbackMessageTimestamp) {
1247
+ break;
1248
+ }
1247
1249
  }
1248
1250
  }
1249
1251
  } catch {
@@ -3508,7 +3510,7 @@ function selectAdaptersForParsing(adapters, sourceFilter) {
3508
3510
  }
3509
3511
 
3510
3512
  // src/cli/build-usage-data-parsing.ts
3511
- import { stat as stat3 } from "fs/promises";
3513
+ import { stat as stat4 } from "fs/promises";
3512
3514
 
3513
3515
  // src/cli/normalize-skipped-row-reasons.ts
3514
3516
  function toPositiveInteger(value) {
@@ -3536,7 +3538,7 @@ function normalizeSkippedRowReasons(value) {
3536
3538
  }
3537
3539
 
3538
3540
  // src/cli/parse-file-cache.ts
3539
- import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
3541
+ import { mkdir as mkdir2, readFile as readFile4, rename, rm, stat as stat3, writeFile as writeFile2 } from "fs/promises";
3540
3542
  import path11 from "path";
3541
3543
  var PARSE_FILE_CACHE_VERSION = 2;
3542
3544
  var CACHE_KEY_SEPARATOR = "\0";
@@ -3627,7 +3629,7 @@ function cloneUsageEvents(events) {
3627
3629
  return events.map((event) => cloneUsageEvent(event));
3628
3630
  }
3629
3631
  function cloneSkippedRowReasons(skippedRowReasons) {
3630
- return (skippedRowReasons ?? []).map((stat4) => ({ reason: stat4.reason, count: stat4.count }));
3632
+ return (skippedRowReasons ?? []).map((stat5) => ({ reason: stat5.reason, count: stat5.count }));
3631
3633
  }
3632
3634
  function normalizeCachedEvents(value) {
3633
3635
  if (!Array.isArray(value)) {
@@ -3678,6 +3680,21 @@ function normalizeCacheEntry(value) {
3678
3680
  function getDefaultParseFileCachePath() {
3679
3681
  return path11.join(getUserCacheRootDir(), "llm-usage-metrics", "parse-file-cache.json");
3680
3682
  }
3683
+ function normalizeCacheShardSource(source) {
3684
+ const normalizedSource = normalizeCacheSource(source);
3685
+ if (!normalizedSource) {
3686
+ return "unknown";
3687
+ }
3688
+ return normalizedSource.replace(/[^a-z0-9._-]/gu, "_");
3689
+ }
3690
+ function getSourceShardedParseFileCachePath(cacheFilePath, source) {
3691
+ const parsedPath = path11.parse(cacheFilePath);
3692
+ const sourceShard = normalizeCacheShardSource(source);
3693
+ if (parsedPath.ext.length > 0) {
3694
+ return path11.join(parsedPath.dir, `${parsedPath.name}.${sourceShard}${parsedPath.ext}`);
3695
+ }
3696
+ return path11.join(parsedPath.dir, `${parsedPath.base}.${sourceShard}`);
3697
+ }
3681
3698
  var ParseFileCache = class _ParseFileCache {
3682
3699
  constructor(cacheFilePath, limits, now) {
3683
3700
  this.cacheFilePath = cacheFilePath;
@@ -3763,8 +3780,15 @@ var ParseFileCache = class _ParseFileCache {
3763
3780
  payloadText = bestPayloadText;
3764
3781
  }
3765
3782
  await mkdir2(path11.dirname(this.cacheFilePath), { recursive: true });
3766
- await writeFile2(this.cacheFilePath, payloadText, "utf8");
3767
- this.dirty = false;
3783
+ const temporaryPath = `${this.cacheFilePath}.${process.pid}.${this.now()}.tmp`;
3784
+ try {
3785
+ await writeFile2(temporaryPath, payloadText, "utf8");
3786
+ await rename(temporaryPath, this.cacheFilePath);
3787
+ this.dirty = false;
3788
+ } catch (error) {
3789
+ await rm(temporaryPath, { force: true }).catch(() => void 0);
3790
+ throw error;
3791
+ }
3768
3792
  }
3769
3793
  toPayload(entries) {
3770
3794
  return {
@@ -3783,6 +3807,17 @@ var ParseFileCache = class _ParseFileCache {
3783
3807
  };
3784
3808
  }
3785
3809
  async loadFromDisk() {
3810
+ let cacheFileSizeBytes;
3811
+ try {
3812
+ const cacheStat = await stat3(this.cacheFilePath);
3813
+ cacheFileSizeBytes = cacheStat.size;
3814
+ } catch {
3815
+ return;
3816
+ }
3817
+ if (cacheFileSizeBytes > this.limits.maxBytes) {
3818
+ this.dirty = true;
3819
+ return;
3820
+ }
3786
3821
  let content;
3787
3822
  try {
3788
3823
  content = await readFile4(this.cacheFilePath, "utf8");
@@ -3808,6 +3843,7 @@ var ParseFileCache = class _ParseFileCache {
3808
3843
  }
3809
3844
  if (Buffer.byteLength(content, "utf8") > this.limits.maxBytes) {
3810
3845
  this.dirty = true;
3846
+ return;
3811
3847
  }
3812
3848
  const entries = Array.isArray(payloadRecord.entries) ? payloadRecord.entries : [];
3813
3849
  for (const rawEntry of entries) {
@@ -3867,7 +3903,7 @@ async function parseAdapterEvents(adapter, maxParallelFileParsing, parseFileCach
3867
3903
  let parseFileDiagnostics;
3868
3904
  if (parseFileCache) {
3869
3905
  try {
3870
- const fileStat = await stat3(filePath);
3906
+ const fileStat = await stat4(filePath);
3871
3907
  fileFingerprint = {
3872
3908
  size: fileStat.size,
3873
3909
  mtimeMs: fileStat.mtimeMs
@@ -3908,25 +3944,44 @@ function getErrorReason(error) {
3908
3944
  return String(error);
3909
3945
  }
3910
3946
  async function parseSelectedAdapters(adaptersToParse, maxParallelFileParsing, options = {}) {
3911
- const parseCache = options.parseCache?.enabled ? await ParseFileCache.load({
3912
- cacheFilePath: options.parseCacheFilePath,
3913
- limits: {
3947
+ const parseCacheBySource = /* @__PURE__ */ new Map();
3948
+ if (options.parseCache?.enabled) {
3949
+ const parseCacheLimits = {
3914
3950
  ttlMs: options.parseCache.ttlMs,
3915
3951
  maxEntries: options.parseCache.maxEntries,
3916
3952
  maxBytes: options.parseCache.maxBytes
3917
- },
3918
- now: options.now
3919
- }) : void 0;
3953
+ };
3954
+ const cacheFilePath = options.parseCacheFilePath ?? getDefaultParseFileCachePath();
3955
+ await Promise.all(
3956
+ adaptersToParse.map(async (adapter) => {
3957
+ const sourceId = adapter.id.toLowerCase();
3958
+ if (parseCacheBySource.has(sourceId)) {
3959
+ return;
3960
+ }
3961
+ parseCacheBySource.set(
3962
+ sourceId,
3963
+ await ParseFileCache.load({
3964
+ cacheFilePath: getSourceShardedParseFileCachePath(cacheFilePath, sourceId),
3965
+ limits: parseCacheLimits,
3966
+ now: options.now
3967
+ })
3968
+ );
3969
+ })
3970
+ );
3971
+ }
3920
3972
  const parseResults = await Promise.allSettled(
3921
3973
  adaptersToParse.map(
3922
- (adapter) => parseAdapterEvents(adapter, maxParallelFileParsing, parseCache)
3974
+ (adapter) => parseAdapterEvents(
3975
+ adapter,
3976
+ maxParallelFileParsing,
3977
+ parseCacheBySource.get(adapter.id.toLowerCase())
3978
+ )
3923
3979
  )
3924
3980
  );
3925
- if (parseCache) {
3926
- try {
3927
- await parseCache.persist();
3928
- } catch {
3929
- }
3981
+ if (parseCacheBySource.size > 0) {
3982
+ await Promise.allSettled(
3983
+ [...parseCacheBySource.values()].map(async (parseCache) => parseCache.persist())
3984
+ );
3930
3985
  }
3931
3986
  const sourceFailures = [];
3932
3987
  const successfulParseResults = [];
@@ -4200,7 +4255,7 @@ function normalizeModelPricing(rawModelPricing) {
4200
4255
  return void 0;
4201
4256
  }
4202
4257
  const cacheReadPerToken = toNonNegativeNumber3(rawModelPricing.cache_read_input_token_cost) ?? toNonNegativeNumber3(rawModelPricing.cache_read_input_token_cost_priority);
4203
- const cacheWritePerToken = toNonNegativeNumber3(rawModelPricing.cache_creation_input_token_cost);
4258
+ const cacheWritePerToken = toNonNegativeNumber3(rawModelPricing.cache_creation_input_token_cost) ?? toNonNegativeNumber3(rawModelPricing.cache_creation_input_token_cost_priority);
4204
4259
  const reasoningPerToken = toNonNegativeNumber3(rawModelPricing.output_cost_per_reasoning_token);
4205
4260
  const modelPricing = {
4206
4261
  inputPer1MUsd: inputPerToken * ONE_MILLION2,
@@ -5786,6 +5841,8 @@ function renderTerminalTable(rows, options = {}) {
5786
5841
  }
5787
5842
 
5788
5843
  // src/render/render-efficiency-report.ts
5844
+ var periodColumnIndex = 0;
5845
+ var minimumEfficiencyColumnWidth = 1;
5789
5846
  function getReportTitle(granularity) {
5790
5847
  switch (granularity) {
5791
5848
  case "daily":
@@ -5830,22 +5887,114 @@ function toTableSortRow(row) {
5830
5887
  costIncomplete: row.costIncomplete
5831
5888
  };
5832
5889
  }
5890
+ function measureRenderedTableWidth(columnWidths) {
5891
+ if (columnWidths.length === 0) {
5892
+ return 0;
5893
+ }
5894
+ return columnWidths.reduce((sum, width) => sum + width, 0) + columnWidths.length * 3 + 1;
5895
+ }
5896
+ function computeColumnWidths2(headerCells, bodyRows) {
5897
+ const columnCount = Math.max(
5898
+ headerCells.length,
5899
+ ...bodyRows.map((row) => row.length),
5900
+ efficiencyTableHeaders.length
5901
+ );
5902
+ const widths = Array.from({ length: columnCount }, () => 0);
5903
+ const measureRow = (row) => {
5904
+ for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
5905
+ for (const line of splitCellLines(row[columnIndex] ?? "")) {
5906
+ widths[columnIndex] = Math.max(widths[columnIndex], visibleWidth(line));
5907
+ }
5908
+ }
5909
+ };
5910
+ measureRow(headerCells);
5911
+ for (const row of bodyRows) {
5912
+ measureRow(row);
5913
+ }
5914
+ return widths;
5915
+ }
5916
+ function resolveWrappedCells(headerCells, bodyRows, widths) {
5917
+ let wrappedHeaderCells = [...headerCells];
5918
+ let wrappedBodyRows = bodyRows.map((row) => [...row]);
5919
+ for (let columnIndex = 0; columnIndex < widths.length; columnIndex += 1) {
5920
+ const columnWidth = widths[columnIndex] ?? 0;
5921
+ if (columnWidth <= 0) {
5922
+ continue;
5923
+ }
5924
+ wrappedHeaderCells = wrapTableColumn([wrappedHeaderCells], {
5925
+ columnIndex,
5926
+ width: columnWidth
5927
+ })[0] ?? [];
5928
+ wrappedBodyRows = wrapTableColumn(wrappedBodyRows, {
5929
+ columnIndex,
5930
+ width: columnWidth
5931
+ });
5932
+ }
5933
+ return {
5934
+ wrappedHeaderCells,
5935
+ wrappedBodyRows
5936
+ };
5937
+ }
5938
+ function fitTableCellsToTerminal(headerCells, bodyRows) {
5939
+ const naturalWidths = computeColumnWidths2(headerCells, bodyRows);
5940
+ const terminalWidth = resolveTtyColumns(process.stdout);
5941
+ if (terminalWidth === void 0 || measureRenderedTableWidth(naturalWidths) <= terminalWidth) {
5942
+ return {
5943
+ headerCells: [...headerCells],
5944
+ bodyRows: bodyRows.map((row) => [...row]),
5945
+ widths: naturalWidths
5946
+ };
5947
+ }
5948
+ const constrainedWidths = [...naturalWidths];
5949
+ let renderedTableWidth = measureRenderedTableWidth(constrainedWidths);
5950
+ while (renderedTableWidth > terminalWidth && constrainedWidths.some((width) => width > minimumEfficiencyColumnWidth)) {
5951
+ let widestIndex = -1;
5952
+ let widestWidth = -1;
5953
+ for (let columnIndex = 0; columnIndex < constrainedWidths.length; columnIndex += 1) {
5954
+ const columnWidth = constrainedWidths[columnIndex];
5955
+ if (columnWidth <= minimumEfficiencyColumnWidth || columnWidth <= widestWidth) {
5956
+ continue;
5957
+ }
5958
+ widestIndex = columnIndex;
5959
+ widestWidth = columnWidth;
5960
+ }
5961
+ if (widestIndex === -1) {
5962
+ break;
5963
+ }
5964
+ const overflowColumns = renderedTableWidth - terminalWidth;
5965
+ const maxReducibleWidth = widestWidth - minimumEfficiencyColumnWidth;
5966
+ const reduction = Math.min(overflowColumns, maxReducibleWidth);
5967
+ if (reduction <= 0) {
5968
+ break;
5969
+ }
5970
+ constrainedWidths[widestIndex] -= reduction;
5971
+ renderedTableWidth -= reduction;
5972
+ }
5973
+ const { wrappedHeaderCells, wrappedBodyRows } = resolveWrappedCells(
5974
+ headerCells,
5975
+ bodyRows,
5976
+ constrainedWidths
5977
+ );
5978
+ return {
5979
+ headerCells: wrappedHeaderCells,
5980
+ bodyRows: wrappedBodyRows,
5981
+ widths: constrainedWidths
5982
+ };
5983
+ }
5833
5984
  function renderTerminalEfficiencyTable(rows) {
5985
+ const headerCells = Array.from(efficiencyTableHeaders);
5834
5986
  const bodyRows = toEfficiencyTableCells(rows);
5835
5987
  const tableSortRows = rows.map((row) => toTableSortRow(row));
5836
- const periodColumnWidth = Math.max(
5837
- efficiencyTableHeaders[0].length,
5838
- ...rows.map((row) => row.periodKey.length)
5839
- );
5988
+ const fittedCells = fitTableCellsToTerminal(headerCells, bodyRows);
5840
5989
  return renderUnicodeTable({
5841
- headerCells: efficiencyTableHeaders,
5842
- bodyRows,
5843
- measureHeaderCells: efficiencyTableHeaders,
5844
- measureBodyRows: bodyRows,
5990
+ headerCells: fittedCells.headerCells,
5991
+ bodyRows: fittedCells.bodyRows,
5992
+ measureHeaderCells: fittedCells.headerCells,
5993
+ measureBodyRows: fittedCells.bodyRows,
5845
5994
  usageRows: tableSortRows,
5846
5995
  tableLayout: "compact",
5847
- modelsColumnIndex: 0,
5848
- modelsColumnWidth: periodColumnWidth
5996
+ modelsColumnIndex: periodColumnIndex,
5997
+ modelsColumnWidth: fittedCells.widths[periodColumnIndex] ?? efficiencyTableHeaders[periodColumnIndex].length
5849
5998
  });
5850
5999
  }
5851
6000
  function toMarkdownSafeCell(value) {