llm-usage-metrics 0.5.1 → 0.5.2

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/dist/index.js CHANGED
@@ -2302,8 +2302,9 @@ function addOutcomeTotals(left, right) {
2302
2302
  };
2303
2303
  }
2304
2304
  function addUsageTotals(left, right) {
2305
+ const hasAnyBucketUsage = (value) => value.inputTokens > 0 || value.outputTokens > 0 || value.reasoningTokens > 0 || value.cacheReadTokens > 0 || value.cacheWriteTokens > 0;
2305
2306
  const hasUnknownCost = left.costIncomplete === true && left.costUsd === void 0 || right.costIncomplete === true && right.costUsd === void 0;
2306
- const isNeutralZeroCost = (value) => value.totalTokens === 0 && value.costUsd === 0 && value.costIncomplete !== true;
2307
+ const isNeutralZeroCost = (value) => !hasAnyBucketUsage(value) && value.totalTokens === 0 && value.costUsd === 0 && value.costIncomplete !== true;
2307
2308
  const leftKnownCost = left.costUsd !== void 0 && !isNeutralZeroCost(left) ? left.costUsd : void 0;
2308
2309
  const rightKnownCost = right.costUsd !== void 0 && !isNeutralZeroCost(right) ? right.costUsd : void 0;
2309
2310
  let costUsd = leftKnownCost !== void 0 && rightKnownCost !== void 0 ? addUsd2(leftKnownCost, rightKnownCost) : leftKnownCost ?? rightKnownCost;
@@ -2321,6 +2322,9 @@ function addUsageTotals(left, right) {
2321
2322
  costIncomplete: left.costIncomplete || right.costIncomplete ? true : void 0
2322
2323
  };
2323
2324
  }
2325
+ function hasMeaningfulUsageSignal(usageTotals) {
2326
+ return usageTotals.totalTokens > 0 || usageTotals.inputTokens > 0 || usageTotals.outputTokens > 0 || usageTotals.reasoningTokens > 0 || usageTotals.cacheReadTokens > 0 || usageTotals.cacheWriteTokens > 0 || usageTotals.costUsd !== void 0 || usageTotals.costIncomplete === true;
2327
+ }
2324
2328
  function computeDerivedMetrics(usage, outcomes) {
2325
2329
  const costUsd = usage.costUsd;
2326
2330
  const nonCacheTotalTokens = usage.inputTokens + usage.outputTokens + usage.reasoningTokens;
@@ -2343,7 +2347,7 @@ function aggregateEfficiency(options) {
2343
2347
  const usageTotals = usageTotalsByPeriod.get(periodKey) ?? createEmptyEfficiencyUsageTotals();
2344
2348
  const outcomeTotals = options.periodOutcomes.get(periodKey) ?? createEmptyEfficiencyOutcomeTotals();
2345
2349
  const hasUsageRow = usageTotalsByPeriod.has(periodKey);
2346
- const hasUsageSignal3 = hasUsageRow && (usageTotals.totalTokens > 0 || usageTotals.costUsd !== void 0 || usageTotals.costIncomplete === true);
2350
+ const hasUsageSignal3 = hasUsageRow && hasMeaningfulUsageSignal(usageTotals);
2347
2351
  if (outcomeTotals.commitCount === 0 || !hasUsageSignal3) {
2348
2352
  continue;
2349
2353
  }
@@ -2854,88 +2858,6 @@ async function attributeUsageEventsToRepo(events, repoDir, resolveRepoRoot3 = re
2854
2858
  };
2855
2859
  }
2856
2860
 
2857
- // src/cli/build-usage-data-diagnostics.ts
2858
- function buildUsageDiagnostics(params) {
2859
- const parseResultBySource = new Map(
2860
- params.successfulParseResults.map((result) => [result.source.toLowerCase(), result])
2861
- );
2862
- const sessionStats = params.adaptersToParse.map((adapter) => {
2863
- const parseResult = parseResultBySource.get(adapter.id.toLowerCase());
2864
- return {
2865
- source: adapter.id,
2866
- filesFound: parseResult?.filesFound ?? 0,
2867
- eventsParsed: parseResult?.events.length ?? 0
2868
- };
2869
- });
2870
- const skippedRows = params.successfulParseResults.filter((result) => result.skippedRows > 0).map((result) => ({
2871
- source: result.source,
2872
- skippedRows: result.skippedRows,
2873
- reasons: result.skippedRowReasons
2874
- }));
2875
- return {
2876
- sessionStats,
2877
- sourceFailures: params.sourceFailures,
2878
- skippedRows,
2879
- pricingOrigin: params.pricingOrigin,
2880
- pricingWarning: params.pricingWarning,
2881
- activeEnvOverrides: params.activeEnvOverrides,
2882
- timezone: params.timezone,
2883
- runtimeProfile: params.runtimeProfile
2884
- };
2885
- }
2886
- function assembleUsageDataResult(events, rows, diagnostics) {
2887
- return {
2888
- events,
2889
- rows,
2890
- diagnostics
2891
- };
2892
- }
2893
-
2894
- // src/config/env-var-display.ts
2895
- var ENV_VARS_TO_DISPLAY = [
2896
- { name: "LLM_USAGE_SKIP_UPDATE_CHECK", description: "skip startup update check" },
2897
- {
2898
- name: "LLM_USAGE_UPDATE_CACHE_SCOPE",
2899
- description: "update-check cache scope (global/session)"
2900
- },
2901
- { name: "LLM_USAGE_UPDATE_CACHE_SESSION_KEY", description: "update-check session cache key" },
2902
- { name: "LLM_USAGE_UPDATE_CACHE_TTL_MS", description: "update-check cache TTL" },
2903
- { name: "LLM_USAGE_UPDATE_FETCH_TIMEOUT_MS", description: "update-check fetch timeout" },
2904
- { name: "LLM_USAGE_PRICING_CACHE_TTL_MS", description: "pricing cache TTL" },
2905
- { name: "LLM_USAGE_PRICING_FETCH_TIMEOUT_MS", description: "pricing fetch timeout" },
2906
- { name: "LLM_USAGE_PARSE_MAX_PARALLEL", description: "max parallel file parsing" },
2907
- { name: "LLM_USAGE_PARSE_CACHE_ENABLED", description: "enable file parse cache" },
2908
- { name: "LLM_USAGE_PARSE_CACHE_TTL_MS", description: "file parse cache TTL" },
2909
- { name: "LLM_USAGE_PARSE_CACHE_MAX_ENTRIES", description: "file parse cache max entries" },
2910
- { name: "LLM_USAGE_PARSE_CACHE_MAX_BYTES", description: "file parse cache max bytes" },
2911
- { name: "LLM_USAGE_PROFILE_RUNTIME", description: "emit runtime profiling diagnostics" }
2912
- ];
2913
- function getActiveEnvVarOverrides() {
2914
- const overrides = [];
2915
- for (const { name, description } of ENV_VARS_TO_DISPLAY) {
2916
- const value = process.env[name];
2917
- if (value !== void 0 && value !== "") {
2918
- overrides.push({ name, value, description });
2919
- }
2920
- }
2921
- return overrides;
2922
- }
2923
- function formatEnvVarOverrides(overrides) {
2924
- if (overrides.length === 0) {
2925
- return [];
2926
- }
2927
- const lines = [];
2928
- lines.push("Active environment overrides:");
2929
- for (const { name, value, description } of overrides) {
2930
- lines.push(` ${name}=${value} (${description})`);
2931
- }
2932
- return lines;
2933
- }
2934
-
2935
- // src/sources/codex/codex-source-adapter.ts
2936
- import os2 from "os";
2937
- import path6 from "path";
2938
-
2939
2861
  // src/domain/provider-normalization.ts
2940
2862
  var billingProviderAliases = /* @__PURE__ */ new Map([
2941
2863
  ["openai-codex", "openai"],
@@ -3095,7 +3017,7 @@ function createUsageEvent(input) {
3095
3017
  const cacheWriteTokens = normalizeNonNegativeInteger(input.cacheWriteTokens);
3096
3018
  const declaredTotalTokens = normalizeNonNegativeInteger(input.totalTokens);
3097
3019
  const componentTotalTokens = inputTokens + outputTokens + reasoningTokens + cacheReadTokens + cacheWriteTokens;
3098
- const totalTokens = declaredTotalTokens > 0 ? declaredTotalTokens : componentTotalTokens;
3020
+ const totalTokens = input.totalTokens === void 0 ? componentTotalTokens : declaredTotalTokens;
3099
3021
  const costUsd = normalizeUsdCost(input.costUsd);
3100
3022
  const costMode = resolveCostMode(input.costMode, costUsd);
3101
3023
  return {
@@ -3122,8 +3044,90 @@ function isPriceableEvent(event) {
3122
3044
  return hasBillableTokenBuckets(event);
3123
3045
  }
3124
3046
 
3047
+ // src/cli/build-usage-data-diagnostics.ts
3048
+ function buildUsageDiagnostics(params) {
3049
+ const parseResultBySource = new Map(
3050
+ params.successfulParseResults.map((result) => [result.source.toLowerCase(), result])
3051
+ );
3052
+ const sessionStats = params.adaptersToParse.map((adapter) => {
3053
+ const parseResult = parseResultBySource.get(adapter.id.toLowerCase());
3054
+ return {
3055
+ source: adapter.id,
3056
+ filesFound: parseResult?.filesFound ?? 0,
3057
+ eventsParsed: parseResult?.events.length ?? 0
3058
+ };
3059
+ });
3060
+ const skippedRows = params.successfulParseResults.filter((result) => result.skippedRows > 0).map((result) => ({
3061
+ source: result.source,
3062
+ skippedRows: result.skippedRows,
3063
+ reasons: result.skippedRowReasons
3064
+ }));
3065
+ return {
3066
+ sessionStats,
3067
+ sourceFailures: params.sourceFailures,
3068
+ skippedRows,
3069
+ pricingOrigin: params.pricingOrigin,
3070
+ pricingWarning: params.pricingWarning,
3071
+ activeEnvOverrides: params.activeEnvOverrides,
3072
+ timezone: params.timezone,
3073
+ runtimeProfile: params.runtimeProfile
3074
+ };
3075
+ }
3076
+ function assembleUsageDataResult(events, rows, diagnostics) {
3077
+ return {
3078
+ events,
3079
+ rows,
3080
+ diagnostics
3081
+ };
3082
+ }
3083
+
3084
+ // src/config/env-var-display.ts
3085
+ var ENV_VARS_TO_DISPLAY = [
3086
+ { name: "LLM_USAGE_SKIP_UPDATE_CHECK", description: "skip startup update check" },
3087
+ {
3088
+ name: "LLM_USAGE_UPDATE_CACHE_SCOPE",
3089
+ description: "update-check cache scope (global/session)"
3090
+ },
3091
+ { name: "LLM_USAGE_UPDATE_CACHE_SESSION_KEY", description: "update-check session cache key" },
3092
+ { name: "LLM_USAGE_UPDATE_CACHE_TTL_MS", description: "update-check cache TTL" },
3093
+ { name: "LLM_USAGE_UPDATE_FETCH_TIMEOUT_MS", description: "update-check fetch timeout" },
3094
+ { name: "LLM_USAGE_PRICING_CACHE_TTL_MS", description: "pricing cache TTL" },
3095
+ { name: "LLM_USAGE_PRICING_FETCH_TIMEOUT_MS", description: "pricing fetch timeout" },
3096
+ { name: "LLM_USAGE_PARSE_MAX_PARALLEL", description: "max parallel file parsing" },
3097
+ { name: "LLM_USAGE_PARSE_CACHE_ENABLED", description: "enable file parse cache" },
3098
+ { name: "LLM_USAGE_PARSE_CACHE_TTL_MS", description: "file parse cache TTL" },
3099
+ { name: "LLM_USAGE_PARSE_CACHE_MAX_ENTRIES", description: "file parse cache max entries" },
3100
+ { name: "LLM_USAGE_PARSE_CACHE_MAX_BYTES", description: "file parse cache max bytes" },
3101
+ { name: "LLM_USAGE_PROFILE_RUNTIME", description: "emit runtime profiling diagnostics" }
3102
+ ];
3103
+ function getActiveEnvVarOverrides() {
3104
+ const overrides = [];
3105
+ for (const { name, description } of ENV_VARS_TO_DISPLAY) {
3106
+ const value = process.env[name];
3107
+ if (value !== void 0 && value !== "") {
3108
+ overrides.push({ name, value, description });
3109
+ }
3110
+ }
3111
+ return overrides;
3112
+ }
3113
+ function formatEnvVarOverrides(overrides) {
3114
+ if (overrides.length === 0) {
3115
+ return [];
3116
+ }
3117
+ const lines = [];
3118
+ lines.push("Active environment overrides:");
3119
+ for (const { name, value, description } of overrides) {
3120
+ lines.push(` ${name}=${value} (${description})`);
3121
+ }
3122
+ return lines;
3123
+ }
3124
+
3125
+ // src/sources/codex/codex-source-adapter.ts
3126
+ import os2 from "os";
3127
+ import path6 from "path";
3128
+
3125
3129
  // src/utils/discover-files.ts
3126
- import { readdir } from "fs/promises";
3130
+ import { readdir, realpath as realpath2, stat as stat2 } from "fs/promises";
3127
3131
  import path5 from "path";
3128
3132
  function getNodeErrorCode2(error) {
3129
3133
  const record = asRecord(error);
@@ -3148,7 +3152,22 @@ function normalizeExtension(extension) {
3148
3152
  }
3149
3153
  return normalized;
3150
3154
  }
3151
- async function walkDirectory(rootDir, acc, options) {
3155
+ async function walkDirectory(rootDir, acc, options, ancestryRealPaths) {
3156
+ let resolvedRootDir;
3157
+ try {
3158
+ resolvedRootDir = await realpath2(rootDir);
3159
+ } catch (error) {
3160
+ if (getNodeErrorCode2(error) === "ENOENT") {
3161
+ return;
3162
+ }
3163
+ if (options.allowPermissionSkip && isSkippableDirectoryReadError(error)) {
3164
+ return;
3165
+ }
3166
+ throw error;
3167
+ }
3168
+ if (ancestryRealPaths.has(resolvedRootDir)) {
3169
+ return;
3170
+ }
3152
3171
  let entries;
3153
3172
  try {
3154
3173
  entries = await readdir(rootDir, { withFileTypes: true, encoding: "utf8" });
@@ -3161,13 +3180,37 @@ async function walkDirectory(rootDir, acc, options) {
3161
3180
  }
3162
3181
  throw error;
3163
3182
  }
3183
+ const nextAncestryRealPaths = new Set(ancestryRealPaths);
3184
+ nextAncestryRealPaths.add(resolvedRootDir);
3164
3185
  if (options.sort) {
3165
3186
  entries.sort((left, right) => compareByCodePoint(left.name, right.name));
3166
3187
  }
3167
3188
  for (const entry of entries) {
3168
3189
  const entryPath = path5.join(rootDir, entry.name);
3190
+ if (entry.isSymbolicLink()) {
3191
+ try {
3192
+ const entryStats = await stat2(entryPath);
3193
+ const resolvedEntryPath = await realpath2(entryPath);
3194
+ if (entryStats.isDirectory() && options.recursive) {
3195
+ await walkDirectory(entryPath, acc, options, nextAncestryRealPaths);
3196
+ continue;
3197
+ }
3198
+ if (entryStats.isFile() && (matchesExtension(entry.name, options.extension) || matchesExtension(path5.basename(resolvedEntryPath), options.extension))) {
3199
+ acc.push(entryPath);
3200
+ }
3201
+ } catch (error) {
3202
+ if (getNodeErrorCode2(error) === "ENOENT") {
3203
+ continue;
3204
+ }
3205
+ if (options.allowPermissionSkip && isSkippableDirectoryReadError(error)) {
3206
+ continue;
3207
+ }
3208
+ throw error;
3209
+ }
3210
+ continue;
3211
+ }
3169
3212
  if (entry.isDirectory() && options.recursive) {
3170
- await walkDirectory(entryPath, acc, options);
3213
+ await walkDirectory(entryPath, acc, options, nextAncestryRealPaths);
3171
3214
  continue;
3172
3215
  }
3173
3216
  if (entry.isFile() && matchesExtension(entry.name, options.extension)) {
@@ -3175,6 +3218,33 @@ async function walkDirectory(rootDir, acc, options) {
3175
3218
  }
3176
3219
  }
3177
3220
  }
3221
+ async function toCanonicalFiles(files, options) {
3222
+ const canonicalFiles = [];
3223
+ const seenRealPaths = /* @__PURE__ */ new Set();
3224
+ for (const filePath of files) {
3225
+ let resolvedFilePath;
3226
+ try {
3227
+ resolvedFilePath = await realpath2(filePath);
3228
+ } catch (error) {
3229
+ if (getNodeErrorCode2(error) === "ENOENT") {
3230
+ continue;
3231
+ }
3232
+ if (options.allowPermissionSkip && isSkippableDirectoryReadError(error)) {
3233
+ continue;
3234
+ }
3235
+ throw error;
3236
+ }
3237
+ if (seenRealPaths.has(resolvedFilePath)) {
3238
+ continue;
3239
+ }
3240
+ seenRealPaths.add(resolvedFilePath);
3241
+ canonicalFiles.push(resolvedFilePath);
3242
+ }
3243
+ if (options.sort) {
3244
+ canonicalFiles.sort(compareByCodePoint);
3245
+ }
3246
+ return canonicalFiles;
3247
+ }
3178
3248
  async function discoverFiles(rootDir, options) {
3179
3249
  const files = [];
3180
3250
  const resolvedOptions = {
@@ -3183,8 +3253,8 @@ async function discoverFiles(rootDir, options) {
3183
3253
  allowPermissionSkip: options.allowPermissionSkip ?? true,
3184
3254
  sort: options.sort ?? true
3185
3255
  };
3186
- await walkDirectory(rootDir, files, resolvedOptions);
3187
- return files;
3256
+ await walkDirectory(rootDir, files, resolvedOptions, /* @__PURE__ */ new Set());
3257
+ return toCanonicalFiles(files, resolvedOptions);
3188
3258
  }
3189
3259
 
3190
3260
  // src/utils/discover-jsonl-files.ts
@@ -3193,7 +3263,7 @@ async function discoverJsonlFiles(rootDir) {
3193
3263
  }
3194
3264
 
3195
3265
  // src/utils/fs-helpers.ts
3196
- import { access as access2, constants as constants2, stat as stat2 } from "fs/promises";
3266
+ import { access as access2, constants as constants2, stat as stat3 } from "fs/promises";
3197
3267
  async function pathExists(filePath) {
3198
3268
  try {
3199
3269
  await access2(filePath, constants2.F_OK);
@@ -3212,21 +3282,21 @@ async function pathReadable(filePath) {
3212
3282
  }
3213
3283
  async function pathIsDirectory(filePath) {
3214
3284
  try {
3215
- return (await stat2(filePath)).isDirectory();
3285
+ return (await stat3(filePath)).isDirectory();
3216
3286
  } catch {
3217
3287
  return false;
3218
3288
  }
3219
3289
  }
3220
3290
  async function pathIsFile(filePath) {
3221
3291
  try {
3222
- return (await stat2(filePath)).isFile();
3292
+ return (await stat3(filePath)).isFile();
3223
3293
  } catch {
3224
3294
  return false;
3225
3295
  }
3226
3296
  }
3227
3297
  async function pathStat(filePath) {
3228
3298
  try {
3229
- return await stat2(filePath);
3299
+ return await stat3(filePath);
3230
3300
  } catch {
3231
3301
  return void 0;
3232
3302
  }
@@ -3274,6 +3344,8 @@ async function* readJsonlObjects(filePath, options = {}) {
3274
3344
  }
3275
3345
 
3276
3346
  // src/sources/parsing-utils.ts
3347
+ var MIN_PLAUSIBLE_UNIX_SECONDS_ABS = 1e8;
3348
+ var UNIX_SECONDS_ABS_CUTOFF = 1e10;
3277
3349
  function asTrimmedText(value) {
3278
3350
  if (typeof value !== "string") {
3279
3351
  return void 0;
@@ -3290,6 +3362,38 @@ function toNumberLike(value) {
3290
3362
  }
3291
3363
  return void 0;
3292
3364
  }
3365
+ function normalizeTimestampCandidate(candidate) {
3366
+ if (candidate instanceof Date) {
3367
+ return Number.isNaN(candidate.getTime()) ? void 0 : candidate.toISOString();
3368
+ }
3369
+ if (typeof candidate === "number" && Number.isFinite(candidate)) {
3370
+ if (Math.abs(candidate) < MIN_PLAUSIBLE_UNIX_SECONDS_ABS) {
3371
+ return void 0;
3372
+ }
3373
+ const timestampMs = Math.abs(candidate) <= UNIX_SECONDS_ABS_CUTOFF ? candidate * 1e3 : candidate;
3374
+ const date2 = new Date(timestampMs);
3375
+ if (Number.isNaN(date2.getTime())) {
3376
+ return void 0;
3377
+ }
3378
+ return date2.toISOString();
3379
+ }
3380
+ const normalizedText = asTrimmedText(candidate);
3381
+ if (!normalizedText) {
3382
+ return void 0;
3383
+ }
3384
+ const numericTimestamp = /^-?\d+$/u.test(normalizedText) && normalizedText.length >= 9 ? Number(normalizedText) : NaN;
3385
+ if (Number.isFinite(numericTimestamp)) {
3386
+ return normalizeTimestampCandidate(numericTimestamp);
3387
+ }
3388
+ if (/^-?\d+$/u.test(normalizedText)) {
3389
+ return void 0;
3390
+ }
3391
+ const date = new Date(normalizedText);
3392
+ if (Number.isNaN(date.getTime())) {
3393
+ return void 0;
3394
+ }
3395
+ return date.toISOString();
3396
+ }
3293
3397
 
3294
3398
  // src/sources/codex/codex-source-adapter.ts
3295
3399
  var defaultSessionsDir = path6.join(os2.homedir(), ".codex", "sessions");
@@ -3345,10 +3449,22 @@ function addUsage(left, right) {
3345
3449
  function hasUsageSignal(usage) {
3346
3450
  return usage.inputTokens > 0 || usage.cacheReadTokens > 0 || usage.outputTokens > 0 || usage.reasoningTokens > 0 || usage.totalTokens > 0;
3347
3451
  }
3452
+ function hasUsageRollback(current, previous) {
3453
+ return current.inputTokens < previous.inputTokens || current.cacheReadTokens < previous.cacheReadTokens || current.outputTokens < previous.outputTokens || current.reasoningTokens < previous.reasoningTokens || current.totalTokens < previous.totalTokens;
3454
+ }
3348
3455
  function deriveDeltaUsage(info, previousTotalUsage) {
3349
3456
  const totalUsage = toUsage(info.total_token_usage);
3350
3457
  const lastUsage = toUsage(info.last_token_usage);
3351
3458
  if (totalUsage && previousTotalUsage) {
3459
+ if (hasUsageRollback(totalUsage, previousTotalUsage)) {
3460
+ if (lastUsage && hasUsageSignal(lastUsage)) {
3461
+ return { deltaUsage: lastUsage, latestTotalUsage: totalUsage };
3462
+ }
3463
+ return {
3464
+ deltaUsage: hasUsageSignal(totalUsage) ? totalUsage : void 0,
3465
+ latestTotalUsage: totalUsage
3466
+ };
3467
+ }
3352
3468
  const deltaFromTotals = subtractUsage(totalUsage, previousTotalUsage);
3353
3469
  if (hasUsageSignal(deltaFromTotals)) {
3354
3470
  return { deltaUsage: deltaFromTotals, latestTotalUsage: totalUsage };
@@ -3356,7 +3472,7 @@ function deriveDeltaUsage(info, previousTotalUsage) {
3356
3472
  return { latestTotalUsage: totalUsage };
3357
3473
  }
3358
3474
  if (lastUsage) {
3359
- return { deltaUsage: lastUsage, latestTotalUsage: totalUsage };
3475
+ return { deltaUsage: lastUsage, latestTotalUsage: totalUsage, fromLastUsageOnly: true };
3360
3476
  }
3361
3477
  if (!totalUsage) {
3362
3478
  return {};
@@ -3364,6 +3480,16 @@ function deriveDeltaUsage(info, previousTotalUsage) {
3364
3480
  const deltaUsage = previousTotalUsage ? subtractUsage(totalUsage, previousTotalUsage) : totalUsage;
3365
3481
  return { deltaUsage, latestTotalUsage: totalUsage };
3366
3482
  }
3483
+ function createLastUsageOnlyKey(timestamp, usage) {
3484
+ return [
3485
+ timestamp,
3486
+ usage.inputTokens,
3487
+ usage.cacheReadTokens,
3488
+ usage.outputTokens,
3489
+ usage.reasoningTokens,
3490
+ usage.totalTokens
3491
+ ].join(":");
3492
+ }
3367
3493
  function getFallbackSessionId(filePath) {
3368
3494
  return path6.basename(filePath, ".jsonl");
3369
3495
  }
@@ -3435,16 +3561,33 @@ var CodexSourceAdapter = class {
3435
3561
  if (!info) {
3436
3562
  continue;
3437
3563
  }
3438
- const { deltaUsage, latestTotalUsage } = deriveDeltaUsage(info, state.previousTotalUsage);
3564
+ const { deltaUsage, latestTotalUsage, fromLastUsageOnly } = deriveDeltaUsage(
3565
+ info,
3566
+ state.previousTotalUsage
3567
+ );
3439
3568
  if (!deltaUsage || !hasUsageSignal(deltaUsage)) {
3569
+ if (!fromLastUsageOnly) {
3570
+ state.previousLastUsageOnlyKey = void 0;
3571
+ }
3440
3572
  state.previousTotalUsage = latestTotalUsage ?? state.previousTotalUsage;
3441
3573
  continue;
3442
3574
  }
3443
- const timestamp = asTrimmedText(line.timestamp);
3575
+ const timestamp = normalizeTimestampCandidate(line.timestamp);
3444
3576
  if (!timestamp) {
3445
- state.previousTotalUsage = latestTotalUsage ?? state.previousTotalUsage;
3577
+ if (!fromLastUsageOnly) {
3578
+ state.previousLastUsageOnlyKey = void 0;
3579
+ }
3446
3580
  continue;
3447
3581
  }
3582
+ if (fromLastUsageOnly) {
3583
+ const currentLastUsageOnlyKey = createLastUsageOnlyKey(timestamp, deltaUsage);
3584
+ if (state.previousLastUsageOnlyKey === currentLastUsageOnlyKey) {
3585
+ continue;
3586
+ }
3587
+ state.previousLastUsageOnlyKey = currentLastUsageOnlyKey;
3588
+ } else {
3589
+ state.previousLastUsageOnlyKey = void 0;
3590
+ }
3448
3591
  const model = state.model ?? LEGACY_CODEX_MODEL_FALLBACK;
3449
3592
  try {
3450
3593
  events.push(
@@ -3506,24 +3649,6 @@ var DROID_MESSAGE_LINE_PATTERN = /"type"\s*:\s*"message"/u;
3506
3649
  function shouldParseDroidJsonlLine(lineText) {
3507
3650
  return DROID_SESSION_START_LINE_PATTERN.test(lineText) || DROID_MESSAGE_LINE_PATTERN.test(lineText);
3508
3651
  }
3509
- var UNIX_SECONDS_ABS_CUTOFF = 1e10;
3510
- function normalizeTimestampCandidate(candidate) {
3511
- let date;
3512
- if (typeof candidate === "number" && Number.isFinite(candidate)) {
3513
- const timestampMs = Math.abs(candidate) <= UNIX_SECONDS_ABS_CUTOFF ? candidate * 1e3 : candidate;
3514
- date = new Date(timestampMs);
3515
- } else {
3516
- const normalizedText = asTrimmedText(candidate);
3517
- if (!normalizedText) {
3518
- return void 0;
3519
- }
3520
- date = new Date(normalizedText);
3521
- }
3522
- if (Number.isNaN(date.getTime())) {
3523
- return void 0;
3524
- }
3525
- return date.toISOString();
3526
- }
3527
3652
  function getSettingsSessionId(filePath) {
3528
3653
  return path7.basename(filePath, ".settings.json");
3529
3654
  }
@@ -3604,7 +3729,8 @@ var DroidSourceAdapter = class {
3604
3729
  toNumberLike(tokenUsage.cacheCreationTokens)
3605
3730
  );
3606
3731
  const billableTokens = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
3607
- if (billableTokens === 0) {
3732
+ const hasUsageSignal3 = billableTokens > 0 || reasoningTokens > 0;
3733
+ if (!hasUsageSignal3) {
3608
3734
  skippedRows++;
3609
3735
  incrementSkippedReason(skippedRowReasons, "no_token_usage");
3610
3736
  return toParseDiagnostics(events, skippedRows, skippedRowReasons);
@@ -3724,7 +3850,7 @@ async function discoverSessionFiles(geminiDir) {
3724
3850
  for (const projectEntry of projectEntries.sort(
3725
3851
  (left, right) => compareByCodePoint(left.name, right.name)
3726
3852
  )) {
3727
- if (!projectEntry.isDirectory()) {
3853
+ if (!projectEntry.isDirectory() && !projectEntry.isSymbolicLink()) {
3728
3854
  continue;
3729
3855
  }
3730
3856
  const chatsDir = path8.join(tmpDir, projectEntry.name, "chats");
@@ -3790,16 +3916,6 @@ function extractTokenUsage(tokens) {
3790
3916
  totalTokens
3791
3917
  };
3792
3918
  }
3793
- function normalizeTimestamp2(candidate) {
3794
- if (typeof candidate !== "string" || isBlankText(candidate)) {
3795
- return void 0;
3796
- }
3797
- const date = new Date(candidate.trim());
3798
- if (Number.isNaN(date.getTime())) {
3799
- return void 0;
3800
- }
3801
- return date.toISOString();
3802
- }
3803
3919
  var GeminiSourceAdapter = class {
3804
3920
  id = "gemini";
3805
3921
  capabilities = {
@@ -3889,7 +4005,7 @@ var GeminiSourceAdapter = class {
3889
4005
  incrementSkippedReason(skippedRowReasons, "no_token_usage");
3890
4006
  continue;
3891
4007
  }
3892
- const timestamp = normalizeTimestamp2(message.timestamp);
4008
+ const timestamp = normalizeTimestampCandidate(message.timestamp);
3893
4009
  if (!timestamp) {
3894
4010
  skippedRows++;
3895
4011
  incrementSkippedReason(skippedRowReasons, "invalid_timestamp");
@@ -3924,8 +4040,15 @@ import path9 from "path";
3924
4040
  function deduplicate(paths) {
3925
4041
  return [...new Set(paths)];
3926
4042
  }
4043
+ function normalizeEnvPath(value) {
4044
+ if (value === void 0) {
4045
+ return void 0;
4046
+ }
4047
+ const normalized = value.trim();
4048
+ return normalized || void 0;
4049
+ }
3927
4050
  function getLinuxLikeCandidates(homeDir, env) {
3928
- const xdgDataHome = env.XDG_DATA_HOME ?? path9.join(homeDir, ".local", "share");
4051
+ const xdgDataHome = normalizeEnvPath(env.XDG_DATA_HOME) ?? path9.join(homeDir, ".local", "share");
3929
4052
  return [
3930
4053
  path9.join(xdgDataHome, "opencode", "opencode.db"),
3931
4054
  path9.join(xdgDataHome, "opencode", "db.sqlite"),
@@ -3943,7 +4066,8 @@ function getMacOsCandidates(homeDir) {
3943
4066
  ];
3944
4067
  }
3945
4068
  function getWindowsCandidates(homeDir, env) {
3946
- const roamingBase = env.APPDATA ?? env.LOCALAPPDATA ?? (env.USERPROFILE ? path9.join(env.USERPROFILE, "AppData", "Roaming") : void 0);
4069
+ const userProfile = normalizeEnvPath(env.USERPROFILE);
4070
+ const roamingBase = normalizeEnvPath(env.APPDATA) ?? normalizeEnvPath(env.LOCALAPPDATA) ?? (userProfile ? path9.join(userProfile, "AppData", "Roaming") : void 0);
3947
4071
  const roamingCandidates = roamingBase ? [
3948
4072
  path9.join(roamingBase, "opencode", "opencode.db"),
3949
4073
  path9.join(roamingBase, "opencode", "db.sqlite")
@@ -4022,33 +4146,6 @@ async function loadNodeSqliteModule() {
4022
4146
  }
4023
4147
 
4024
4148
  // src/sources/opencode/opencode-row-parser.ts
4025
- var UNIX_SECONDS_ABS_CUTOFF2 = 1e10;
4026
- function normalizeTimestampCandidate2(candidate) {
4027
- if (typeof candidate === "number" && Number.isFinite(candidate)) {
4028
- const timestampMs = Math.abs(candidate) <= UNIX_SECONDS_ABS_CUTOFF2 ? candidate * 1e3 : candidate;
4029
- const date = new Date(timestampMs);
4030
- if (Number.isNaN(date.getTime())) {
4031
- return void 0;
4032
- }
4033
- return date.toISOString();
4034
- }
4035
- if (typeof candidate === "string") {
4036
- const trimmed = candidate.trim();
4037
- if (!trimmed) {
4038
- return void 0;
4039
- }
4040
- const numericTimestamp = Number(trimmed);
4041
- if (Number.isFinite(numericTimestamp)) {
4042
- return normalizeTimestampCandidate2(numericTimestamp);
4043
- }
4044
- const date = new Date(trimmed);
4045
- if (Number.isNaN(date.getTime())) {
4046
- return void 0;
4047
- }
4048
- return date.toISOString();
4049
- }
4050
- return void 0;
4051
- }
4052
4149
  function resolveTimestamp(rowTimestamp, messagePayload) {
4053
4150
  const timestampCandidates = [
4054
4151
  rowTimestamp,
@@ -4057,7 +4154,7 @@ function resolveTimestamp(rowTimestamp, messagePayload) {
4057
4154
  messagePayload.time_created
4058
4155
  ];
4059
4156
  for (const candidate of timestampCandidates) {
4060
- const resolved = normalizeTimestampCandidate2(candidate);
4157
+ const resolved = normalizeTimestampCandidate(candidate);
4061
4158
  if (resolved) {
4062
4159
  return resolved;
4063
4160
  }
@@ -4481,28 +4578,10 @@ var PI_MODEL_CHANGE_LINE_PATTERN = /"type"\s*:\s*"model_change"/u;
4481
4578
  function shouldParsePiJsonlLine(lineText) {
4482
4579
  return PI_MESSAGE_LINE_PATTERN.test(lineText) || PI_SESSION_LINE_PATTERN.test(lineText) || PI_MODEL_CHANGE_LINE_PATTERN.test(lineText);
4483
4580
  }
4484
- var UNIX_SECONDS_ABS_CUTOFF3 = 1e10;
4485
- function normalizeTimestampCandidate3(candidate) {
4486
- let date;
4487
- if (typeof candidate === "number" && Number.isFinite(candidate)) {
4488
- const timestampMs = Math.abs(candidate) <= UNIX_SECONDS_ABS_CUTOFF3 ? candidate * 1e3 : candidate;
4489
- date = new Date(timestampMs);
4490
- } else {
4491
- const normalizedText = asTrimmedText(candidate);
4492
- if (!normalizedText) {
4493
- return void 0;
4494
- }
4495
- date = new Date(normalizedText);
4496
- }
4497
- if (Number.isNaN(date.getTime())) {
4498
- return void 0;
4499
- }
4500
- return date.toISOString();
4501
- }
4502
4581
  function resolveTimestamp2(line, message, state) {
4503
4582
  const candidates = [line.timestamp, message?.timestamp, state.sessionTimestamp];
4504
4583
  for (const candidate of candidates) {
4505
- const normalizedTimestamp = normalizeTimestampCandidate3(candidate);
4584
+ const normalizedTimestamp = normalizeTimestampCandidate(candidate);
4506
4585
  if (normalizedTimestamp) {
4507
4586
  return normalizedTimestamp;
4508
4587
  }
@@ -5027,7 +5106,7 @@ function throwOnExplicitSourceScopeConflicts(adapters, selectedAdapters, options
5027
5106
  }
5028
5107
 
5029
5108
  // src/cli/build-usage-data-parsing.ts
5030
- import { stat as stat4 } from "fs/promises";
5109
+ import { stat as stat5 } from "fs/promises";
5031
5110
 
5032
5111
  // src/cli/normalize-skipped-row-reasons.ts
5033
5112
  function toPositiveInteger(value) {
@@ -5055,7 +5134,7 @@ function normalizeSkippedRowReasons(value) {
5055
5134
  }
5056
5135
 
5057
5136
  // src/cli/parse-file-cache.ts
5058
- import { mkdir as mkdir2, readFile as readFile4, rename, rm, stat as stat3, writeFile as writeFile2 } from "fs/promises";
5137
+ import { mkdir as mkdir2, readFile as readFile4, rename, rm, stat as stat4, writeFile as writeFile2 } from "fs/promises";
5059
5138
  import path11 from "path";
5060
5139
  var PARSE_FILE_CACHE_VERSION = 5;
5061
5140
  var CACHE_KEY_SEPARATOR = "\0";
@@ -5148,7 +5227,7 @@ function cloneUsageEvents(events) {
5148
5227
  return events.map((event) => cloneUsageEvent(event));
5149
5228
  }
5150
5229
  function cloneSkippedRowReasons(skippedRowReasons) {
5151
- return (skippedRowReasons ?? []).map((stat5) => ({ reason: stat5.reason, count: stat5.count }));
5230
+ return (skippedRowReasons ?? []).map((stat6) => ({ reason: stat6.reason, count: stat6.count }));
5152
5231
  }
5153
5232
  function normalizeCachedEvents(value) {
5154
5233
  if (!Array.isArray(value)) {
@@ -5429,7 +5508,7 @@ var ParseFileCache = class _ParseFileCache {
5429
5508
  async loadFromDisk() {
5430
5509
  let cacheFileSizeBytes;
5431
5510
  try {
5432
- const cacheStat = await stat3(this.cacheFilePath);
5511
+ const cacheStat = await stat4(this.cacheFilePath);
5433
5512
  cacheFileSizeBytes = cacheStat.size;
5434
5513
  } catch {
5435
5514
  return;
@@ -5504,7 +5583,7 @@ function isMissingPathError(error) {
5504
5583
  }
5505
5584
  async function createParseDependencyFingerprint(filePath, options) {
5506
5585
  try {
5507
- const fileStat = await stat4(filePath);
5586
+ const fileStat = await stat5(filePath);
5508
5587
  return {
5509
5588
  path: filePath,
5510
5589
  exists: true,
@@ -6884,7 +6963,7 @@ function resolveScopeNote(options) {
6884
6963
  return `Usage filters (${activeFilters.join(", ")}) affect commit attribution too: only commit days with matching repo-attributed usage events are counted.`;
6885
6964
  }
6886
6965
  function hasMeaningfulEfficiencyUsageSignal(event) {
6887
- return event.totalTokens > 0 || event.costUsd !== void 0 && event.costUsd > 0;
6966
+ return event.totalTokens > 0 || hasBillableTokenBuckets(event) || event.costUsd !== void 0;
6888
6967
  }
6889
6968
  async function buildEfficiencyData(granularity, options, deps = {}) {
6890
6969
  const buildDataset = deps.buildUsageEventDataset ?? buildUsageEventDataset;
@@ -7654,6 +7733,9 @@ var USD_PRECISION_SCALE3 = 1e12;
7654
7733
  function roundUsd(value) {
7655
7734
  return Math.round(value * USD_PRECISION_SCALE3) / USD_PRECISION_SCALE3;
7656
7735
  }
7736
+ function addUsd3(left, right) {
7737
+ return roundUsd(left + right);
7738
+ }
7657
7739
  function hasAnyUsageSignal(period) {
7658
7740
  return period.totalTokens > 0 || period.baselineCostIncomplete || (period.baselineCostUsd ?? 0) > 0;
7659
7741
  }
@@ -7811,9 +7893,22 @@ function resolveBaselinePeriods(usageRows) {
7811
7893
  periodRows.set(row.periodKey, row);
7812
7894
  continue;
7813
7895
  }
7814
- if (!periodRows.has(row.periodKey)) {
7896
+ const existingRow = periodRows.get(row.periodKey);
7897
+ if (!existingRow) {
7815
7898
  periodRows.set(row.periodKey, row);
7899
+ continue;
7816
7900
  }
7901
+ periodRows.set(row.periodKey, {
7902
+ ...existingRow,
7903
+ inputTokens: existingRow.inputTokens + row.inputTokens,
7904
+ outputTokens: existingRow.outputTokens + row.outputTokens,
7905
+ reasoningTokens: existingRow.reasoningTokens + row.reasoningTokens,
7906
+ cacheReadTokens: existingRow.cacheReadTokens + row.cacheReadTokens,
7907
+ cacheWriteTokens: existingRow.cacheWriteTokens + row.cacheWriteTokens,
7908
+ totalTokens: existingRow.totalTokens + row.totalTokens,
7909
+ costUsd: row.costUsd !== void 0 ? addUsd3(existingRow.costUsd ?? 0, row.costUsd) : existingRow.costUsd,
7910
+ costIncomplete: existingRow.costIncomplete === true || row.costIncomplete === true ? true : void 0
7911
+ });
7817
7912
  }
7818
7913
  const sortedPeriodKeys = [...periodRows.keys()].sort(compareByCodePoint);
7819
7914
  const periods = sortedPeriodKeys.map((periodKey) => {
@@ -8403,6 +8498,13 @@ function renderTrendsReport(trendsData, format, options = {}) {
8403
8498
  }
8404
8499
 
8405
8500
  // src/trends/aggregate-trends.ts
8501
+ var VALUE_PRECISION_SCALE = 1e12;
8502
+ function addValue(left, right) {
8503
+ return Math.round((left + right) * VALUE_PRECISION_SCALE) / VALUE_PRECISION_SCALE;
8504
+ }
8505
+ function divideValue(value, divisor) {
8506
+ return Math.round(value / divisor * VALUE_PRECISION_SCALE) / VALUE_PRECISION_SCALE;
8507
+ }
8406
8508
  function toTrendBucket(row, metric) {
8407
8509
  return {
8408
8510
  date: row.periodKey,
@@ -8431,12 +8533,12 @@ function buildTrendSummary(buckets) {
8431
8533
  observedDayCount: 0
8432
8534
  };
8433
8535
  }
8434
- const total = buckets.reduce((sum, bucket) => sum + bucket.value, 0);
8536
+ const total = buckets.reduce((sum, bucket) => addValue(sum, bucket.value), 0);
8435
8537
  const observedBuckets = buckets.filter((bucket) => bucket.observed);
8436
8538
  if (observedBuckets.length === 0) {
8437
8539
  return {
8438
8540
  total,
8439
- average: buckets.length > 0 ? total / buckets.length : 0,
8541
+ average: buckets.length > 0 ? divideValue(total, buckets.length) : 0,
8440
8542
  peak: {
8441
8543
  date: "",
8442
8544
  value: 0
@@ -8452,7 +8554,7 @@ function buildTrendSummary(buckets) {
8452
8554
  );
8453
8555
  return {
8454
8556
  total,
8455
- average: buckets.length > 0 ? total / buckets.length : 0,
8557
+ average: buckets.length > 0 ? divideValue(total, buckets.length) : 0,
8456
8558
  peak: {
8457
8559
  date: peak.date,
8458
8560
  value: peak.value
@@ -8472,6 +8574,46 @@ function buildSeries(source, rowsByDate, dateKeys, metric) {
8472
8574
  summary: buildTrendSummary(buckets)
8473
8575
  };
8474
8576
  }
8577
+ function createEmptyUsageRow(periodKey, rowType, source) {
8578
+ return rowType === "period_combined" ? {
8579
+ rowType,
8580
+ periodKey,
8581
+ source: "combined",
8582
+ models: [],
8583
+ modelBreakdown: [],
8584
+ inputTokens: 0,
8585
+ outputTokens: 0,
8586
+ reasoningTokens: 0,
8587
+ cacheReadTokens: 0,
8588
+ cacheWriteTokens: 0,
8589
+ totalTokens: 0
8590
+ } : {
8591
+ rowType,
8592
+ periodKey,
8593
+ source,
8594
+ models: [],
8595
+ modelBreakdown: [],
8596
+ inputTokens: 0,
8597
+ outputTokens: 0,
8598
+ reasoningTokens: 0,
8599
+ cacheReadTokens: 0,
8600
+ cacheWriteTokens: 0,
8601
+ totalTokens: 0
8602
+ };
8603
+ }
8604
+ function addRowTotals(target, row) {
8605
+ return {
8606
+ ...target,
8607
+ inputTokens: target.inputTokens + row.inputTokens,
8608
+ outputTokens: target.outputTokens + row.outputTokens,
8609
+ reasoningTokens: target.reasoningTokens + row.reasoningTokens,
8610
+ cacheReadTokens: target.cacheReadTokens + row.cacheReadTokens,
8611
+ cacheWriteTokens: target.cacheWriteTokens + row.cacheWriteTokens,
8612
+ totalTokens: target.totalTokens + row.totalTokens,
8613
+ costUsd: row.costUsd !== void 0 ? addValue(target.costUsd ?? 0, row.costUsd) : target.costUsd,
8614
+ costIncomplete: target.costIncomplete === true || row.costIncomplete === true ? true : void 0
8615
+ };
8616
+ }
8475
8617
  function toCombinedRowsByDate(rows) {
8476
8618
  const combinedByDate = /* @__PURE__ */ new Map();
8477
8619
  const sourceOnlyByDate = /* @__PURE__ */ new Map();
@@ -8483,9 +8625,8 @@ function toCombinedRowsByDate(rows) {
8483
8625
  combinedByDate.set(row.periodKey, row);
8484
8626
  continue;
8485
8627
  }
8486
- if (!sourceOnlyByDate.has(row.periodKey)) {
8487
- sourceOnlyByDate.set(row.periodKey, row);
8488
- }
8628
+ const existingSourceOnlyRow = sourceOnlyByDate.get(row.periodKey) ?? createEmptyUsageRow(row.periodKey, "period_combined", "combined");
8629
+ sourceOnlyByDate.set(row.periodKey, addRowTotals(existingSourceOnlyRow, row));
8489
8630
  }
8490
8631
  const resolved = /* @__PURE__ */ new Map();
8491
8632
  for (const [date, row] of sourceOnlyByDate) {
@@ -8506,7 +8647,8 @@ function toSourceSeries(rows, dateKeys, options) {
8506
8647
  continue;
8507
8648
  }
8508
8649
  const sourceRows = rowsBySource.get(row.source) ?? /* @__PURE__ */ new Map();
8509
- sourceRows.set(row.periodKey, row);
8650
+ const existingSourceRow = sourceRows.get(row.periodKey) ?? createEmptyUsageRow(row.periodKey, "period_source", row.source);
8651
+ sourceRows.set(row.periodKey, addRowTotals(existingSourceRow, row));
8510
8652
  rowsBySource.set(row.source, sourceRows);
8511
8653
  }
8512
8654
  const observedSources = [...rowsBySource.keys()].sort((left, right) => {