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 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 refreshes in the background instead of blocking report execution
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 resolveOptions = toResolveLatestVersionOptions(options, env);
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 spawn3 } from "child_process";
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 = spawn3("git", args, {
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
- return uuid ? `${filePath}\0${uuid}` : void 0;
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 readFile5, writeFile as writeFile3 } from "fs/promises";
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 readFile5(this.cacheFilePath, "utf8");
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: litellmPricingFetcher, origin: "offline-cache" };
7127
+ return { source, origin: "offline-cache" };
7068
7128
  }
7069
- return { source: litellmPricingFetcher, origin: fromCache ? "cache" : "network" };
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 spawn4 } from "child_process";
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 = spawn4(command, args, {
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("--pricing-offline", "Use cached LiteLLM pricing only (no network fetch)").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,