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 +4 -0
- package/dist/index.js +180 -31
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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((
|
|
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
|
-
|
|
3767
|
-
|
|
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
|
|
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
|
|
3912
|
-
|
|
3913
|
-
|
|
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
|
-
|
|
3919
|
-
|
|
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(
|
|
3974
|
+
(adapter) => parseAdapterEvents(
|
|
3975
|
+
adapter,
|
|
3976
|
+
maxParallelFileParsing,
|
|
3977
|
+
parseCacheBySource.get(adapter.id.toLowerCase())
|
|
3978
|
+
)
|
|
3923
3979
|
)
|
|
3924
3980
|
);
|
|
3925
|
-
if (
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
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
|
|
5837
|
-
efficiencyTableHeaders[0].length,
|
|
5838
|
-
...rows.map((row) => row.periodKey.length)
|
|
5839
|
-
);
|
|
5988
|
+
const fittedCells = fitTableCellsToTerminal(headerCells, bodyRows);
|
|
5840
5989
|
return renderUnicodeTable({
|
|
5841
|
-
headerCells:
|
|
5842
|
-
bodyRows,
|
|
5843
|
-
measureHeaderCells:
|
|
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:
|
|
5848
|
-
modelsColumnWidth:
|
|
5996
|
+
modelsColumnIndex: periodColumnIndex,
|
|
5997
|
+
modelsColumnWidth: fittedCells.widths[periodColumnIndex] ?? efficiencyTableHeaders[periodColumnIndex].length
|
|
5849
5998
|
});
|
|
5850
5999
|
}
|
|
5851
6000
|
function toMarkdownSafeCell(value) {
|