llm-usage-metrics 0.7.0 → 0.7.1
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 +5 -2
- package/dist/index.js +123 -69
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -214,6 +214,9 @@ llm-usage monthly --pricing-offline
|
|
|
214
214
|
|
|
215
215
|
# Continue even if pricing fetch fails
|
|
216
216
|
llm-usage monthly --ignore-pricing-failures
|
|
217
|
+
|
|
218
|
+
# Override per-model pricing from a local JSON file
|
|
219
|
+
llm-usage monthly --pricing-overrides ./pricing-overrides.json
|
|
217
220
|
```
|
|
218
221
|
|
|
219
222
|
## 🧪 Production Benchmarks
|
|
@@ -313,8 +316,8 @@ See full environment variable reference in the [documentation](https://ayagmar.g
|
|
|
313
316
|
The CLI performs lightweight update checks with smart defaults:
|
|
314
317
|
|
|
315
318
|
- 1-hour cache TTL
|
|
316
|
-
- Fresh cached update results are used immediately
|
|
317
|
-
- Stale or missing cache
|
|
319
|
+
- Fresh cached update results are used immediately without any network call
|
|
320
|
+
- Stale or missing cache triggers a bounded fetch (default 1s timeout) so the update prompt stays consistent across commands, instead of silently skipping the run that refreshes the cache
|
|
318
321
|
- Skipped for `--help`, `--version`, `npx`, and direct source/development runs
|
|
319
322
|
- Prompts only in interactive TTY sessions
|
|
320
323
|
|
package/dist/index.js
CHANGED
|
@@ -106,9 +106,6 @@ function getParsingRuntimeConfig(env = process.env) {
|
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
// src/update/update-notifier.ts
|
|
110
|
-
import { spawn as spawn2 } from "child_process";
|
|
111
|
-
|
|
112
109
|
// src/update/update-cache-repository.ts
|
|
113
110
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
114
111
|
import path2 from "path";
|
|
@@ -529,7 +526,6 @@ async function runInteractiveInstallAndRestart(options) {
|
|
|
529
526
|
|
|
530
527
|
// src/update/update-notifier.ts
|
|
531
528
|
var UPDATE_CHECK_SKIP_ENV_VAR = "LLM_USAGE_SKIP_UPDATE_CHECK";
|
|
532
|
-
var UPDATE_CHECK_REFRESH_ENV_VAR = "LLM_USAGE_REFRESH_UPDATE_CHECK";
|
|
533
529
|
function isTruthyEnvFlag(value) {
|
|
534
530
|
if (value === void 0) {
|
|
535
531
|
return false;
|
|
@@ -588,32 +584,6 @@ function toResolveLatestVersionOptions(options, env) {
|
|
|
588
584
|
now: options.now
|
|
589
585
|
};
|
|
590
586
|
}
|
|
591
|
-
function runDetachedCommandWithSpawn(command, args, options = {}) {
|
|
592
|
-
const child = spawn2(command, args, {
|
|
593
|
-
env: options.env,
|
|
594
|
-
stdio: options.stdio ?? "ignore",
|
|
595
|
-
detached: true
|
|
596
|
-
});
|
|
597
|
-
child.on("error", () => void 0);
|
|
598
|
-
child.unref();
|
|
599
|
-
}
|
|
600
|
-
function scheduleBackgroundUpdateRefresh(options, env, argv) {
|
|
601
|
-
const spawnDetachedCommand = options.spawnDetachedCommand ?? runDetachedCommandWithSpawn;
|
|
602
|
-
spawnDetachedCommand(options.execPath ?? process.execPath, argv.slice(1), {
|
|
603
|
-
env: {
|
|
604
|
-
...env,
|
|
605
|
-
[UPDATE_CHECK_REFRESH_ENV_VAR]: "1"
|
|
606
|
-
},
|
|
607
|
-
stdio: "ignore"
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
async function refreshUpdateCheckCache(options) {
|
|
611
|
-
try {
|
|
612
|
-
const env = options.env ?? process.env;
|
|
613
|
-
await resolveLatestVersion(toResolveLatestVersionOptions(options, env));
|
|
614
|
-
} catch {
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
587
|
async function checkForUpdatesAndMaybeRestart(options) {
|
|
618
588
|
const env = options.env ?? process.env;
|
|
619
589
|
const argv = options.argv ?? process.argv;
|
|
@@ -630,19 +600,7 @@ async function checkForUpdatesAndMaybeRestart(options) {
|
|
|
630
600
|
return { continueExecution: true };
|
|
631
601
|
}
|
|
632
602
|
try {
|
|
633
|
-
const
|
|
634
|
-
const cacheFilePath = resolveOptions.cacheFilePath ?? getDefaultUpdateCheckCachePath();
|
|
635
|
-
const cachePayload = await readUpdateCheckCachePayload(cacheFilePath);
|
|
636
|
-
const cacheTtlMs = resolveOptions.cacheTtlMs ?? DEFAULT_UPDATE_CHECK_CACHE_TTL_MS;
|
|
637
|
-
const now = resolveOptions.now ?? Date.now;
|
|
638
|
-
if (!cachePayload || !isCacheFresh(cachePayload, cacheTtlMs, now)) {
|
|
639
|
-
try {
|
|
640
|
-
scheduleBackgroundUpdateRefresh(options, env, argv);
|
|
641
|
-
} catch {
|
|
642
|
-
}
|
|
643
|
-
return { continueExecution: true };
|
|
644
|
-
}
|
|
645
|
-
const latestVersion = cachePayload.latestVersion;
|
|
603
|
+
const latestVersion = await resolveLatestVersion(toResolveLatestVersionOptions(options, env));
|
|
646
604
|
if (!latestVersion || !shouldOfferUpdate(options.currentVersion, latestVersion)) {
|
|
647
605
|
return { continueExecution: true };
|
|
648
606
|
}
|
|
@@ -2659,7 +2617,7 @@ function aggregateEfficiency(options) {
|
|
|
2659
2617
|
}
|
|
2660
2618
|
|
|
2661
2619
|
// src/efficiency/git-outcome-collector.ts
|
|
2662
|
-
import { spawn as
|
|
2620
|
+
import { spawn as spawn2 } from "child_process";
|
|
2663
2621
|
import { createInterface as createInterface2 } from "readline";
|
|
2664
2622
|
import path3 from "path";
|
|
2665
2623
|
import { stat } from "fs/promises";
|
|
@@ -2858,7 +2816,7 @@ function parseGitLogShortstatLines(lines, authorEmail) {
|
|
|
2858
2816
|
}
|
|
2859
2817
|
async function runGitCommand(repoDir, args) {
|
|
2860
2818
|
return await new Promise((resolve, reject) => {
|
|
2861
|
-
const child =
|
|
2819
|
+
const child = spawn2("git", args, {
|
|
2862
2820
|
cwd: repoDir,
|
|
2863
2821
|
env: {
|
|
2864
2822
|
...process.env,
|
|
@@ -3763,13 +3721,16 @@ function parseUsage(usage) {
|
|
|
3763
3721
|
totalTokens
|
|
3764
3722
|
};
|
|
3765
3723
|
}
|
|
3766
|
-
function createDedupKey(filePath, line, message) {
|
|
3724
|
+
function createDedupKey(filePath, line, message, timestamp, model) {
|
|
3767
3725
|
const messageId = asTrimmedText(message.id);
|
|
3768
3726
|
if (messageId) {
|
|
3769
3727
|
return `${filePath}\0${messageId}`;
|
|
3770
3728
|
}
|
|
3771
3729
|
const uuid = asTrimmedText(line.uuid);
|
|
3772
|
-
|
|
3730
|
+
if (uuid) {
|
|
3731
|
+
return `${filePath}\0${uuid}`;
|
|
3732
|
+
}
|
|
3733
|
+
return `${filePath}\0${timestamp}\0${model ?? ""}`;
|
|
3773
3734
|
}
|
|
3774
3735
|
function comparePendingEvents(left, right) {
|
|
3775
3736
|
if (left.timestamp !== right.timestamp) {
|
|
@@ -3842,12 +3803,7 @@ var ClaudeSourceAdapter = class {
|
|
|
3842
3803
|
incrementSkippedReason(skippedRowReasons, "invalid_timestamp");
|
|
3843
3804
|
continue;
|
|
3844
3805
|
}
|
|
3845
|
-
const dedupKey = createDedupKey(filePath, line, message);
|
|
3846
|
-
if (!dedupKey) {
|
|
3847
|
-
skippedRows++;
|
|
3848
|
-
incrementSkippedReason(skippedRowReasons, "missing_message_id");
|
|
3849
|
-
continue;
|
|
3850
|
-
}
|
|
3806
|
+
const dedupKey = createDedupKey(filePath, line, message, timestamp, model);
|
|
3851
3807
|
const sessionId = asTrimmedText(line.sessionId) ?? getFallbackSessionId(filePath);
|
|
3852
3808
|
const repoRoot = asTrimmedText(line.cwd);
|
|
3853
3809
|
const provider = resolveProvider(message, model);
|
|
@@ -6462,8 +6418,91 @@ function applyPricingToEvents(events, pricingSource) {
|
|
|
6462
6418
|
});
|
|
6463
6419
|
}
|
|
6464
6420
|
|
|
6421
|
+
// src/pricing/pricing-override-source.ts
|
|
6422
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
6423
|
+
function toFiniteUsdRate(value) {
|
|
6424
|
+
if (value === null || value === void 0) {
|
|
6425
|
+
return void 0;
|
|
6426
|
+
}
|
|
6427
|
+
if (typeof value === "string" && value.trim() === "") {
|
|
6428
|
+
return void 0;
|
|
6429
|
+
}
|
|
6430
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
6431
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
6432
|
+
}
|
|
6433
|
+
function normalizeReasoningBilling(value) {
|
|
6434
|
+
if (value === "included-in-output" || value === "separate") {
|
|
6435
|
+
return value;
|
|
6436
|
+
}
|
|
6437
|
+
return void 0;
|
|
6438
|
+
}
|
|
6439
|
+
function normalizePricingOverride(raw) {
|
|
6440
|
+
const inputPer1MUsd = toFiniteUsdRate(toNumberLike(raw.inputPer1MUsd));
|
|
6441
|
+
const outputPer1MUsd = toFiniteUsdRate(toNumberLike(raw.outputPer1MUsd));
|
|
6442
|
+
if (inputPer1MUsd === void 0 || outputPer1MUsd === void 0) {
|
|
6443
|
+
return void 0;
|
|
6444
|
+
}
|
|
6445
|
+
const cacheReadPer1MUsd = toFiniteUsdRate(toNumberLike(raw.cacheReadPer1MUsd));
|
|
6446
|
+
const cacheWritePer1MUsd = toFiniteUsdRate(toNumberLike(raw.cacheWritePer1MUsd));
|
|
6447
|
+
const reasoningPer1MUsd = toFiniteUsdRate(toNumberLike(raw.reasoningPer1MUsd));
|
|
6448
|
+
const reasoningBilling = normalizeReasoningBilling(raw.reasoningBilling);
|
|
6449
|
+
return {
|
|
6450
|
+
inputPer1MUsd,
|
|
6451
|
+
outputPer1MUsd,
|
|
6452
|
+
...cacheReadPer1MUsd !== void 0 ? { cacheReadPer1MUsd } : {},
|
|
6453
|
+
...cacheWritePer1MUsd !== void 0 ? { cacheWritePer1MUsd } : {},
|
|
6454
|
+
...reasoningPer1MUsd !== void 0 ? { reasoningPer1MUsd } : {},
|
|
6455
|
+
...reasoningBilling !== void 0 ? { reasoningBilling } : {}
|
|
6456
|
+
};
|
|
6457
|
+
}
|
|
6458
|
+
function normalizeOverrideFile(payload) {
|
|
6459
|
+
const root = asRecord(payload);
|
|
6460
|
+
const overrides = /* @__PURE__ */ new Map();
|
|
6461
|
+
const modelsRecord = asRecord(root?.models);
|
|
6462
|
+
if (!modelsRecord) {
|
|
6463
|
+
return overrides;
|
|
6464
|
+
}
|
|
6465
|
+
for (const [modelName, rawPricing] of Object.entries(modelsRecord)) {
|
|
6466
|
+
const normalizedModelName = asTrimmedText(modelName)?.toLowerCase();
|
|
6467
|
+
if (!normalizedModelName) {
|
|
6468
|
+
continue;
|
|
6469
|
+
}
|
|
6470
|
+
const pricing = normalizePricingOverride(asRecord(rawPricing) ?? {});
|
|
6471
|
+
if (pricing) {
|
|
6472
|
+
overrides.set(normalizedModelName, pricing);
|
|
6473
|
+
}
|
|
6474
|
+
}
|
|
6475
|
+
return overrides;
|
|
6476
|
+
}
|
|
6477
|
+
async function loadPricingOverrides(filePath) {
|
|
6478
|
+
const fileContents = await readFile5(filePath, "utf8");
|
|
6479
|
+
const parsed = JSON.parse(fileContents);
|
|
6480
|
+
return normalizeOverrideFile(parsed);
|
|
6481
|
+
}
|
|
6482
|
+
var PricingOverrideSource = class {
|
|
6483
|
+
overrides;
|
|
6484
|
+
delegate;
|
|
6485
|
+
constructor(overrides, delegate) {
|
|
6486
|
+
this.overrides = overrides;
|
|
6487
|
+
this.delegate = delegate;
|
|
6488
|
+
}
|
|
6489
|
+
resolveModelAlias(model) {
|
|
6490
|
+
if (this.overrides.has(model.toLowerCase())) {
|
|
6491
|
+
return model;
|
|
6492
|
+
}
|
|
6493
|
+
return this.delegate.resolveModelAlias(model);
|
|
6494
|
+
}
|
|
6495
|
+
getPricing(model) {
|
|
6496
|
+
const override = this.overrides.get(model.toLowerCase());
|
|
6497
|
+
if (override) {
|
|
6498
|
+
return override;
|
|
6499
|
+
}
|
|
6500
|
+
return this.delegate.getPricing(model);
|
|
6501
|
+
}
|
|
6502
|
+
};
|
|
6503
|
+
|
|
6465
6504
|
// src/pricing/litellm-pricing-fetcher.ts
|
|
6466
|
-
import { mkdir as mkdir3, readFile as
|
|
6505
|
+
import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
|
|
6467
6506
|
import path13 from "path";
|
|
6468
6507
|
|
|
6469
6508
|
// src/pricing/litellm-model-map.json
|
|
@@ -7007,7 +7046,7 @@ var LiteLLMPricingFetcher = class {
|
|
|
7007
7046
|
async readCachePayload() {
|
|
7008
7047
|
let content;
|
|
7009
7048
|
try {
|
|
7010
|
-
content = await
|
|
7049
|
+
content = await readFile6(this.cacheFilePath, "utf8");
|
|
7011
7050
|
} catch {
|
|
7012
7051
|
return void 0;
|
|
7013
7052
|
}
|
|
@@ -7054,7 +7093,27 @@ var LiteLLMPricingFetcher = class {
|
|
|
7054
7093
|
};
|
|
7055
7094
|
|
|
7056
7095
|
// src/cli/build-usage-data-pricing.ts
|
|
7096
|
+
function wrapWithPricingOverrides(overrides, delegate) {
|
|
7097
|
+
if (!overrides || overrides.size === 0) {
|
|
7098
|
+
return delegate;
|
|
7099
|
+
}
|
|
7100
|
+
return new PricingOverrideSource(overrides, delegate);
|
|
7101
|
+
}
|
|
7057
7102
|
async function resolvePricingSource(options, runtimeConfig) {
|
|
7103
|
+
let pricingOverrides;
|
|
7104
|
+
if (options.pricingOverrides) {
|
|
7105
|
+
try {
|
|
7106
|
+
pricingOverrides = await loadPricingOverrides(options.pricingOverrides);
|
|
7107
|
+
} catch (error) {
|
|
7108
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
7109
|
+
throw new Error(
|
|
7110
|
+
`Could not load --pricing-overrides from ${options.pricingOverrides}: ${reason}`,
|
|
7111
|
+
{
|
|
7112
|
+
cause: error
|
|
7113
|
+
}
|
|
7114
|
+
);
|
|
7115
|
+
}
|
|
7116
|
+
}
|
|
7058
7117
|
const litellmPricingFetcher = new LiteLLMPricingFetcher({
|
|
7059
7118
|
sourceUrl: options.pricingUrl,
|
|
7060
7119
|
offline: options.pricingOffline,
|
|
@@ -7063,10 +7122,11 @@ async function resolvePricingSource(options, runtimeConfig) {
|
|
|
7063
7122
|
});
|
|
7064
7123
|
try {
|
|
7065
7124
|
const fromCache = await litellmPricingFetcher.load();
|
|
7125
|
+
const source = wrapWithPricingOverrides(pricingOverrides, litellmPricingFetcher);
|
|
7066
7126
|
if (options.pricingOffline) {
|
|
7067
|
-
return { source
|
|
7127
|
+
return { source, origin: "offline-cache" };
|
|
7068
7128
|
}
|
|
7069
|
-
return { source
|
|
7129
|
+
return { source, origin: fromCache ? "cache" : "network" };
|
|
7070
7130
|
} catch (error) {
|
|
7071
7131
|
if (options.pricingOffline) {
|
|
7072
7132
|
throw new Error("Offline pricing mode enabled but cached pricing is unavailable", {
|
|
@@ -7634,7 +7694,7 @@ function emitEnvVarOverrides(activeEnvOverrides, diagnosticsLogger) {
|
|
|
7634
7694
|
}
|
|
7635
7695
|
|
|
7636
7696
|
// src/cli/share-artifact.ts
|
|
7637
|
-
import { spawn as
|
|
7697
|
+
import { spawn as spawn3 } from "child_process";
|
|
7638
7698
|
import { constants as constants3 } from "fs";
|
|
7639
7699
|
import { access as access3, writeFile as writeFile4 } from "fs/promises";
|
|
7640
7700
|
import path14 from "path";
|
|
@@ -7716,7 +7776,7 @@ async function resolveOpenCommand(filePath, platform) {
|
|
|
7716
7776
|
}
|
|
7717
7777
|
async function spawnDetached(command, args) {
|
|
7718
7778
|
await new Promise((resolve, reject) => {
|
|
7719
|
-
const child =
|
|
7779
|
+
const child = spawn3(command, args, {
|
|
7720
7780
|
detached: true,
|
|
7721
7781
|
stdio: "ignore",
|
|
7722
7782
|
windowsHide: true
|
|
@@ -9916,7 +9976,10 @@ function registerSharedReportOptions(command, profile) {
|
|
|
9916
9976
|
"--model <name>",
|
|
9917
9977
|
"Filter by model (repeatable/comma-separated; exact when exact match exists after source/provider/date filters, otherwise substring)",
|
|
9918
9978
|
collectRepeatedOption
|
|
9919
|
-
).option("--pricing-url <url>", "Override LiteLLM pricing source URL").option(
|
|
9979
|
+
).option("--pricing-url <url>", "Override LiteLLM pricing source URL").option(
|
|
9980
|
+
"--pricing-overrides <path>",
|
|
9981
|
+
"Path to a JSON file of per-model pricing overrides (takes precedence over LiteLLM)"
|
|
9982
|
+
).option("--pricing-offline", "Use cached LiteLLM pricing only (no network fetch)").option(
|
|
9920
9983
|
"--ignore-pricing-failures",
|
|
9921
9984
|
"Continue without estimated costs when pricing cannot be loaded"
|
|
9922
9985
|
).option("--json", "Render output as JSON");
|
|
@@ -10230,15 +10293,6 @@ var { packageName, packageVersion } = loadPackageMetadataFromRuntime();
|
|
|
10230
10293
|
var updateRuntimeConfig = getUpdateNotifierRuntimeConfig();
|
|
10231
10294
|
var cli = createCli({ version: packageVersion });
|
|
10232
10295
|
async function main() {
|
|
10233
|
-
if (process.env[UPDATE_CHECK_REFRESH_ENV_VAR] === "1") {
|
|
10234
|
-
await refreshUpdateCheckCache({
|
|
10235
|
-
packageName,
|
|
10236
|
-
currentVersion: packageVersion,
|
|
10237
|
-
cacheTtlMs: updateRuntimeConfig.cacheTtlMs,
|
|
10238
|
-
fetchTimeoutMs: updateRuntimeConfig.fetchTimeoutMs
|
|
10239
|
-
});
|
|
10240
|
-
return;
|
|
10241
|
-
}
|
|
10242
10296
|
const updateResult = await checkForUpdatesAndMaybeRestart({
|
|
10243
10297
|
packageName,
|
|
10244
10298
|
currentVersion: packageVersion,
|