ai-zero-token 2.0.7 → 2.0.8

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.
@@ -10,15 +10,62 @@ import {
10
10
  getUsageLifetimePath
11
11
  } from "../store/state-paths.js";
12
12
  const durationBucketLimits = [100, 300, 500, 1e3, 2e3, 5e3, 1e4, 3e4, 6e4, 12e4, Number.POSITIVE_INFINITY];
13
+ const openAIGpt54LongContextInputThreshold = 272e3;
14
+ const gpt54Pricing = {
15
+ inputUsdPerToken: 25e-7,
16
+ outputUsdPerToken: 15e-6,
17
+ cacheCreationUsdPerToken: 25e-7,
18
+ cacheReadUsdPerToken: 25e-8,
19
+ longContextInputThreshold: openAIGpt54LongContextInputThreshold,
20
+ longContextInputMultiplier: 2,
21
+ longContextOutputMultiplier: 1.5
22
+ };
23
+ const tokenPricingByModel = {
24
+ "gpt-5.5": gpt54Pricing,
25
+ "gpt-5.4": gpt54Pricing,
26
+ "gpt-5.4-mini": {
27
+ inputUsdPerToken: 75e-8,
28
+ outputUsdPerToken: 45e-7,
29
+ cacheCreationUsdPerToken: 0,
30
+ cacheReadUsdPerToken: 75e-9
31
+ },
32
+ "gpt-5.4-nano": {
33
+ inputUsdPerToken: 2e-7,
34
+ outputUsdPerToken: 125e-8,
35
+ cacheCreationUsdPerToken: 0,
36
+ cacheReadUsdPerToken: 2e-8
37
+ },
38
+ "gpt-5.2": {
39
+ inputUsdPerToken: 175e-8,
40
+ outputUsdPerToken: 14e-6,
41
+ cacheCreationUsdPerToken: 175e-8,
42
+ cacheReadUsdPerToken: 175e-9
43
+ },
44
+ "gpt-5.3-codex": {
45
+ inputUsdPerToken: 15e-7,
46
+ outputUsdPerToken: 12e-6,
47
+ cacheCreationUsdPerToken: 15e-7,
48
+ cacheReadUsdPerToken: 15e-8
49
+ }
50
+ };
13
51
  function createAggregate() {
14
52
  return {
15
53
  requestCount: 0,
16
54
  successCount: 0,
17
55
  failureCount: 0,
18
56
  inputTokens: 0,
57
+ uncachedInputTokens: 0,
19
58
  outputTokens: 0,
20
59
  totalTokens: 0,
60
+ cacheCreationTokens: 0,
61
+ cacheReadTokens: 0,
62
+ inputCostUsd: 0,
63
+ outputCostUsd: 0,
64
+ cacheCreationCostUsd: 0,
65
+ cacheReadCostUsd: 0,
66
+ estimatedCostUsd: 0,
21
67
  unknownTokenCount: 0,
68
+ unknownTokenStatusCounts: {},
22
69
  imageCount: 0,
23
70
  totalDurationMs: 0,
24
71
  averageDurationMs: 0,
@@ -29,6 +76,7 @@ function createAggregate() {
29
76
  function cloneAggregate(value) {
30
77
  return {
31
78
  ...value,
79
+ unknownTokenStatusCounts: { ...value.unknownTokenStatusCounts },
32
80
  durationBuckets: { ...value.durationBuckets }
33
81
  };
34
82
  }
@@ -40,14 +88,25 @@ function normalizeAggregate(value) {
40
88
  return createAggregate();
41
89
  }
42
90
  const record = value;
91
+ const inputTokens = Math.max(0, Math.trunc(normalizeNumber(record.inputTokens)));
92
+ const cacheReadTokens = Math.max(0, Math.trunc(normalizeNumber(record.cacheReadTokens)));
43
93
  const aggregate = {
44
94
  requestCount: Math.max(0, Math.trunc(normalizeNumber(record.requestCount))),
45
95
  successCount: Math.max(0, Math.trunc(normalizeNumber(record.successCount))),
46
96
  failureCount: Math.max(0, Math.trunc(normalizeNumber(record.failureCount))),
47
- inputTokens: Math.max(0, Math.trunc(normalizeNumber(record.inputTokens))),
97
+ inputTokens,
98
+ uncachedInputTokens: Math.max(0, Math.trunc(normalizeNumber(record.uncachedInputTokens, Math.max(0, inputTokens - cacheReadTokens)))),
48
99
  outputTokens: Math.max(0, Math.trunc(normalizeNumber(record.outputTokens))),
49
100
  totalTokens: Math.max(0, Math.trunc(normalizeNumber(record.totalTokens))),
101
+ cacheCreationTokens: Math.max(0, Math.trunc(normalizeNumber(record.cacheCreationTokens))),
102
+ cacheReadTokens,
103
+ inputCostUsd: Math.max(0, normalizeNumber(record.inputCostUsd)),
104
+ outputCostUsd: Math.max(0, normalizeNumber(record.outputCostUsd)),
105
+ cacheCreationCostUsd: Math.max(0, normalizeNumber(record.cacheCreationCostUsd)),
106
+ cacheReadCostUsd: Math.max(0, normalizeNumber(record.cacheReadCostUsd)),
107
+ estimatedCostUsd: Math.max(0, normalizeNumber(record.estimatedCostUsd)),
50
108
  unknownTokenCount: Math.max(0, Math.trunc(normalizeNumber(record.unknownTokenCount))),
109
+ unknownTokenStatusCounts: {},
51
110
  imageCount: Math.max(0, Math.trunc(normalizeNumber(record.imageCount))),
52
111
  totalDurationMs: Math.max(0, normalizeNumber(record.totalDurationMs)),
53
112
  averageDurationMs: 0,
@@ -59,6 +118,11 @@ function normalizeAggregate(value) {
59
118
  aggregate.durationBuckets[key] = Math.max(0, Math.trunc(normalizeNumber(item)));
60
119
  }
61
120
  }
121
+ if (record.unknownTokenStatusCounts && typeof record.unknownTokenStatusCounts === "object") {
122
+ for (const [key, item] of Object.entries(record.unknownTokenStatusCounts)) {
123
+ aggregate.unknownTokenStatusCounts[key] = Math.max(0, Math.trunc(normalizeNumber(item)));
124
+ }
125
+ }
62
126
  refreshDerivedMetrics(aggregate);
63
127
  return aggregate;
64
128
  }
@@ -96,6 +160,7 @@ function createLifetimeStore() {
96
160
  byModel: {},
97
161
  byEndpoint: {},
98
162
  byError: {},
163
+ byTokenUsageStatus: {},
99
164
  byImageRoute: {},
100
165
  bySource: {}
101
166
  };
@@ -126,6 +191,7 @@ function normalizeLifetimeStore(value) {
126
191
  byModel: normalizeDimensionStore(record.byModel),
127
192
  byEndpoint: normalizeDimensionStore(record.byEndpoint),
128
193
  byError: normalizeDimensionStore(record.byError),
194
+ byTokenUsageStatus: normalizeDimensionStore(record.byTokenUsageStatus),
129
195
  byImageRoute: normalizeDimensionStore(record.byImageRoute),
130
196
  bySource: normalizeDimensionStore(record.bySource)
131
197
  };
@@ -175,18 +241,183 @@ function refreshDerivedMetrics(aggregate) {
175
241
  function tokenNumber(value) {
176
242
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.trunc(value) : null;
177
243
  }
244
+ function optionalString(value) {
245
+ return typeof value === "string" && value.trim() ? value : void 0;
246
+ }
247
+ function normalizeTokenUsageForEvent(value) {
248
+ if (!value || typeof value !== "object") {
249
+ return null;
250
+ }
251
+ const record = value;
252
+ const usage = {
253
+ inputTokens: tokenNumber(record.inputTokens),
254
+ uncachedInputTokens: tokenNumber(record.uncachedInputTokens),
255
+ outputTokens: tokenNumber(record.outputTokens),
256
+ totalTokens: tokenNumber(record.totalTokens),
257
+ cacheCreationTokens: tokenNumber(record.cacheCreationTokens),
258
+ cacheReadTokens: tokenNumber(record.cacheReadTokens)
259
+ };
260
+ return Object.values(usage).some((item) => item !== null) ? usage : null;
261
+ }
262
+ function normalizeImageRoute(value) {
263
+ return value === "codex-tool" || value === "chatgpt-web" ? value : "none";
264
+ }
265
+ function normalizeTokenUsageStatus(value, tokenUsage, success) {
266
+ if (value === "captured" || value === "missing_terminal" || value === "terminal_without_usage" || value === "parse_failed" || value === "upstream_error" || value === "not_returned") {
267
+ return value;
268
+ }
269
+ if (tokenNumber(tokenUsage?.totalTokens) !== null) {
270
+ return "captured";
271
+ }
272
+ return success ? "not_returned" : "upstream_error";
273
+ }
274
+ function normalizeUsageRecordEvent(event) {
275
+ const statusCode = Math.trunc(normalizeNumber(event.statusCode));
276
+ const timestamp = normalizeNumber(event.timestamp, Date.now());
277
+ const success = event.success ?? (statusCode >= 200 && statusCode < 400);
278
+ const tokenUsage = normalizeTokenUsageForEvent(event.tokenUsage);
279
+ return {
280
+ ...event,
281
+ id: optionalString(event.id) ?? randomUUID(),
282
+ timestamp,
283
+ statusCode,
284
+ durationMs: Math.max(0, normalizeNumber(event.durationMs)),
285
+ endpoint: optionalString(event.endpoint) ?? "-",
286
+ method: optionalString(event.method) ?? "-",
287
+ model: optionalString(event.model) ?? "-",
288
+ source: optionalString(event.source) ?? "-",
289
+ profileId: optionalString(event.profileId),
290
+ accountId: optionalString(event.accountId),
291
+ accountLabel: optionalString(event.accountLabel),
292
+ planType: optionalString(event.planType),
293
+ errorType: optionalString(event.errorType),
294
+ tokenUsage,
295
+ tokenUsageStatus: normalizeTokenUsageStatus(event.tokenUsageStatus, tokenUsage, success),
296
+ imageRoute: normalizeImageRoute(event.imageRoute),
297
+ imageCount: Math.max(0, Math.trunc(normalizeNumber(event.imageCount))),
298
+ success
299
+ };
300
+ }
301
+ function lastModelSegment(model) {
302
+ const trimmed = model.trim();
303
+ if (!trimmed) {
304
+ return "";
305
+ }
306
+ const parts = trimmed.split("/");
307
+ return parts[parts.length - 1]?.trim() ?? "";
308
+ }
309
+ function canonicalizeModelForPricing(model) {
310
+ let normalized = lastModelSegment(model).toLowerCase();
311
+ if (!normalized) {
312
+ return "";
313
+ }
314
+ normalized = normalized.replaceAll("_", "-").replace(/\s+/g, "-");
315
+ while (normalized.includes("--")) {
316
+ normalized = normalized.replaceAll("--", "-");
317
+ }
318
+ if (normalized.startsWith("gpt5")) {
319
+ normalized = `gpt-5${normalized.slice("gpt5".length)}`;
320
+ }
321
+ normalized = normalized.replaceAll("gpt-5.4mini", "gpt-5.4-mini").replaceAll("gpt-5.4nano", "gpt-5.4-nano").replaceAll("gpt-5.3-codexspark", "gpt-5.3-codex-spark").replaceAll("gpt-5.3codexspark", "gpt-5.3-codex-spark").replaceAll("gpt-5.3codex", "gpt-5.3-codex");
322
+ const compactSuffix = "-openai-compact";
323
+ if (normalized.endsWith(compactSuffix)) {
324
+ normalized = normalized.slice(0, -compactSuffix.length);
325
+ }
326
+ return normalized;
327
+ }
328
+ function pricingKeyForModel(model) {
329
+ const normalized = canonicalizeModelForPricing(model);
330
+ if (!normalized) {
331
+ return null;
332
+ }
333
+ if (normalized.includes("gpt-5.5")) {
334
+ return "gpt-5.5";
335
+ }
336
+ if (normalized.includes("gpt-5.4-mini")) {
337
+ return "gpt-5.4-mini";
338
+ }
339
+ if (normalized.includes("gpt-5.4-nano")) {
340
+ return "gpt-5.4-nano";
341
+ }
342
+ if (normalized.includes("gpt-5.4")) {
343
+ return "gpt-5.4";
344
+ }
345
+ if (normalized.includes("gpt-5.2")) {
346
+ return "gpt-5.2";
347
+ }
348
+ if (normalized.includes("gpt-5.3-codex") || normalized.includes("gpt-5.3") || normalized.includes("codex")) {
349
+ return "gpt-5.3-codex";
350
+ }
351
+ if (normalized.includes("gpt-5")) {
352
+ return "gpt-5.4";
353
+ }
354
+ return null;
355
+ }
356
+ function estimateUsageCost(model, tokenUsage) {
357
+ const pricingKey = pricingKeyForModel(model);
358
+ const pricing = pricingKey ? tokenPricingByModel[pricingKey] : void 0;
359
+ if (!pricing) {
360
+ return {
361
+ inputCostUsd: 0,
362
+ outputCostUsd: 0,
363
+ cacheCreationCostUsd: 0,
364
+ cacheReadCostUsd: 0,
365
+ estimatedCostUsd: 0
366
+ };
367
+ }
368
+ const inputTokens = tokenNumber(tokenUsage?.inputTokens) ?? 0;
369
+ const cacheReadTokens = tokenNumber(tokenUsage?.cacheReadTokens) ?? 0;
370
+ const uncachedInputTokens = tokenNumber(tokenUsage?.uncachedInputTokens) ?? Math.max(0, inputTokens - cacheReadTokens);
371
+ const outputTokens = tokenNumber(tokenUsage?.outputTokens) ?? 0;
372
+ const cacheCreationTokens = tokenNumber(tokenUsage?.cacheCreationTokens) ?? 0;
373
+ const totalInputTokens = uncachedInputTokens + cacheReadTokens;
374
+ const longContext = pricing.longContextInputThreshold && totalInputTokens > pricing.longContextInputThreshold;
375
+ const inputMultiplier = longContext ? pricing.longContextInputMultiplier ?? 1 : 1;
376
+ const outputMultiplier = longContext ? pricing.longContextOutputMultiplier ?? 1 : 1;
377
+ const inputCostUsd = uncachedInputTokens * pricing.inputUsdPerToken * inputMultiplier;
378
+ const outputCostUsd = outputTokens * pricing.outputUsdPerToken * outputMultiplier;
379
+ const cacheCreationCostUsd = cacheCreationTokens * pricing.cacheCreationUsdPerToken;
380
+ const cacheReadCostUsd = cacheReadTokens * pricing.cacheReadUsdPerToken;
381
+ return {
382
+ inputCostUsd,
383
+ outputCostUsd,
384
+ cacheCreationCostUsd,
385
+ cacheReadCostUsd,
386
+ estimatedCostUsd: inputCostUsd + outputCostUsd + cacheCreationCostUsd + cacheReadCostUsd
387
+ };
388
+ }
389
+ function tokenUsageStatusForEvent(event) {
390
+ return normalizeTokenUsageStatus(event.tokenUsageStatus, event.tokenUsage ?? null, event.success ?? (event.statusCode >= 200 && event.statusCode < 400));
391
+ }
178
392
  function addToAggregate(aggregate, event) {
179
393
  const success = event.success ?? (event.statusCode >= 200 && event.statusCode < 400);
180
394
  const inputTokens = tokenNumber(event.tokenUsage?.inputTokens);
395
+ const cacheReadTokens = tokenNumber(event.tokenUsage?.cacheReadTokens);
396
+ const cacheCreationTokens = tokenNumber(event.tokenUsage?.cacheCreationTokens);
397
+ const inferredUncachedInputTokens = inputTokens !== null ? Math.max(0, inputTokens - (cacheReadTokens ?? 0)) : null;
398
+ const uncachedInputTokens = tokenNumber(event.tokenUsage?.uncachedInputTokens) ?? inferredUncachedInputTokens;
181
399
  const outputTokens = tokenNumber(event.tokenUsage?.outputTokens);
182
400
  const totalTokens = tokenNumber(event.tokenUsage?.totalTokens) ?? (inputTokens !== null || outputTokens !== null ? (inputTokens ?? 0) + (outputTokens ?? 0) : null);
401
+ const cost = estimateUsageCost(event.model, event.tokenUsage);
183
402
  aggregate.requestCount += 1;
184
403
  aggregate.successCount += success ? 1 : 0;
185
404
  aggregate.failureCount += success ? 0 : 1;
186
405
  aggregate.inputTokens += inputTokens ?? 0;
406
+ aggregate.uncachedInputTokens += uncachedInputTokens ?? 0;
187
407
  aggregate.outputTokens += outputTokens ?? 0;
188
408
  aggregate.totalTokens += totalTokens ?? 0;
189
- aggregate.unknownTokenCount += totalTokens === null ? 1 : 0;
409
+ aggregate.cacheCreationTokens += cacheCreationTokens ?? 0;
410
+ aggregate.cacheReadTokens += cacheReadTokens ?? 0;
411
+ aggregate.inputCostUsd += cost.inputCostUsd;
412
+ aggregate.outputCostUsd += cost.outputCostUsd;
413
+ aggregate.cacheCreationCostUsd += cost.cacheCreationCostUsd;
414
+ aggregate.cacheReadCostUsd += cost.cacheReadCostUsd;
415
+ aggregate.estimatedCostUsd += cost.estimatedCostUsd;
416
+ if (totalTokens === null) {
417
+ const tokenUsageStatus = tokenUsageStatusForEvent(event);
418
+ aggregate.unknownTokenCount += 1;
419
+ aggregate.unknownTokenStatusCounts[tokenUsageStatus] = (aggregate.unknownTokenStatusCounts[tokenUsageStatus] ?? 0) + 1;
420
+ }
190
421
  aggregate.imageCount += Math.max(0, Math.trunc(event.imageCount ?? 0));
191
422
  aggregate.totalDurationMs += Math.max(0, event.durationMs);
192
423
  const bucket = durationBucketKey(event.durationMs);
@@ -202,6 +433,42 @@ function imageRouteLabel(route) {
202
433
  }
203
434
  return "\u975E\u751F\u56FE";
204
435
  }
436
+ function tokenUsageStatusLabel(status) {
437
+ if (status === "captured") {
438
+ return "\u5DF2\u6355\u83B7\u7528\u91CF";
439
+ }
440
+ if (status === "missing_terminal") {
441
+ return "\u7F3A\u5C11\u7EC8\u6001\u4E8B\u4EF6";
442
+ }
443
+ if (status === "terminal_without_usage") {
444
+ return "\u7EC8\u6001\u65E0 usage";
445
+ }
446
+ if (status === "parse_failed") {
447
+ return "SSE \u89E3\u6790\u5931\u8D25";
448
+ }
449
+ if (status === "upstream_error") {
450
+ return "\u4E0A\u6E38\u9519\u8BEF";
451
+ }
452
+ return "\u672A\u8FD4\u56DE usage";
453
+ }
454
+ function addEventToStores(daily, lifetime, normalized) {
455
+ const date = formatLocalDate(normalized.timestamp);
456
+ daily.days[date] = daily.days[date] ? normalizeAggregate(daily.days[date]) : createAggregate();
457
+ addToAggregate(daily.days[date], normalized);
458
+ addToAggregate(lifetime.aggregate, normalized);
459
+ bumpDimension(lifetime.byAccount, normalized.profileId || normalized.accountId || normalized.accountLabel || "-", normalized.accountLabel || normalized.accountId || normalized.profileId || "-", normalized);
460
+ bumpDimension(lifetime.byModel, normalized.model || "-", normalized.model || "-", normalized);
461
+ bumpDimension(lifetime.byEndpoint, `${normalized.method} ${normalized.endpoint}`, `${normalized.method} ${normalized.endpoint}`, normalized);
462
+ bumpDimension(lifetime.bySource, normalized.source || "-", normalized.source || "-", normalized);
463
+ bumpDimension(lifetime.byTokenUsageStatus, normalized.tokenUsageStatus ?? "not_returned", tokenUsageStatusLabel(normalized.tokenUsageStatus ?? "not_returned"), normalized);
464
+ bumpDimension(lifetime.byImageRoute, normalized.imageRoute, imageRouteLabel(normalized.imageRoute), normalized);
465
+ if (!normalized.success) {
466
+ const errorType = normalized.errorType?.trim() || `HTTP ${normalized.statusCode}`;
467
+ bumpDimension(lifetime.byError, errorType, errorType, normalized);
468
+ }
469
+ daily.updatedAt = Math.max(daily.updatedAt, normalized.timestamp);
470
+ lifetime.updatedAt = Math.max(lifetime.updatedAt, normalized.timestamp);
471
+ }
205
472
  function bumpDimension(store, key, label, event) {
206
473
  const normalizedKey = key.trim() || "-";
207
474
  const existing = store[normalizedKey] ?? {
@@ -227,6 +494,14 @@ async function readJsonFile(filePath) {
227
494
  return null;
228
495
  }
229
496
  }
497
+ async function fileExists(filePath) {
498
+ try {
499
+ await fs.access(filePath);
500
+ return true;
501
+ } catch {
502
+ return false;
503
+ }
504
+ }
230
505
  async function writeJsonAtomic(filePath, value) {
231
506
  await fs.mkdir(path.dirname(filePath), { recursive: true });
232
507
  const tempPath = `${filePath}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
@@ -239,6 +514,86 @@ async function writeJsonAtomic(filePath, value) {
239
514
  throw error;
240
515
  }
241
516
  }
517
+ function formatBackupTimestamp(date = /* @__PURE__ */ new Date()) {
518
+ const year = date.getFullYear();
519
+ const month = String(date.getMonth() + 1).padStart(2, "0");
520
+ const day = String(date.getDate()).padStart(2, "0");
521
+ const hours = String(date.getHours()).padStart(2, "0");
522
+ const minutes = String(date.getMinutes()).padStart(2, "0");
523
+ const seconds = String(date.getSeconds()).padStart(2, "0");
524
+ return `${year}${month}${day}-${hours}${minutes}${seconds}`;
525
+ }
526
+ async function uniqueBackupDir(usageDir) {
527
+ const parentDir = path.dirname(usageDir);
528
+ const baseName = `${path.basename(usageDir)}.backup.${formatBackupTimestamp()}`;
529
+ let candidate = path.join(parentDir, baseName);
530
+ for (let index = 2; await fileExists(candidate); index += 1) {
531
+ candidate = path.join(parentDir, `${baseName}-${index}`);
532
+ }
533
+ return candidate;
534
+ }
535
+ function aggregateNeedsCostBackfill(aggregate) {
536
+ return aggregate.totalTokens > 0 && aggregate.estimatedCostUsd <= 0;
537
+ }
538
+ function shouldBackfillUsageCosts(daily, lifetime) {
539
+ const knownPricedModelMissingCost = Object.values(lifetime.byModel).some(
540
+ (row) => aggregateNeedsCostBackfill(row.aggregate) && pricingKeyForModel(row.key) !== null
541
+ );
542
+ if (!knownPricedModelMissingCost) {
543
+ return false;
544
+ }
545
+ return aggregateNeedsCostBackfill(lifetime.aggregate) || Object.values(daily.days).some(aggregateNeedsCostBackfill);
546
+ }
547
+ function shouldBackfillUsageDiagnostics(daily, lifetime) {
548
+ if (lifetime.aggregate.requestCount <= 0) {
549
+ return false;
550
+ }
551
+ if (Object.keys(lifetime.byTokenUsageStatus).length === 0) {
552
+ return true;
553
+ }
554
+ if (lifetime.aggregate.unknownTokenCount > 0 && Object.keys(lifetime.aggregate.unknownTokenStatusCounts).length === 0) {
555
+ return true;
556
+ }
557
+ return Object.values(daily.days).some(
558
+ (aggregate) => aggregate.unknownTokenCount > 0 && Object.keys(aggregate.unknownTokenStatusCounts).length === 0
559
+ );
560
+ }
561
+ async function rebuildStoresFromEventLogs() {
562
+ let entries;
563
+ try {
564
+ entries = await fs.readdir(getUsageEventsDir());
565
+ } catch {
566
+ return null;
567
+ }
568
+ const eventFiles = entries.filter((entry) => entry.endsWith(".jsonl")).sort();
569
+ if (eventFiles.length === 0) {
570
+ return null;
571
+ }
572
+ const daily = createDailyStore();
573
+ const lifetime = createLifetimeStore();
574
+ let seen = false;
575
+ for (const fileName of eventFiles) {
576
+ let content;
577
+ try {
578
+ content = await fs.readFile(path.join(getUsageEventsDir(), fileName), "utf8");
579
+ } catch {
580
+ continue;
581
+ }
582
+ for (const line of content.split(/\r?\n/)) {
583
+ const trimmed = line.trim();
584
+ if (!trimmed) {
585
+ continue;
586
+ }
587
+ try {
588
+ const parsed = JSON.parse(trimmed);
589
+ addEventToStores(daily, lifetime, normalizeUsageRecordEvent(parsed));
590
+ seen = true;
591
+ } catch {
592
+ }
593
+ }
594
+ }
595
+ return seen ? { daily, lifetime } : null;
596
+ }
242
597
  class UsageService {
243
598
  startedAt = Date.now();
244
599
  startupAggregate = createAggregate();
@@ -248,39 +603,13 @@ class UsageService {
248
603
  saveQueue = Promise.resolve();
249
604
  async record(event) {
250
605
  await this.ensureLoaded();
251
- const timestamp = event.timestamp ?? Date.now();
606
+ const normalized = normalizeUsageRecordEvent(event);
607
+ const timestamp = normalized.timestamp;
252
608
  const date = formatLocalDate(timestamp);
253
- const normalized = {
254
- ...event,
255
- id: event.id ?? randomUUID(),
256
- timestamp,
257
- statusCode: event.statusCode,
258
- durationMs: Math.max(0, event.durationMs),
259
- endpoint: event.endpoint || "-",
260
- method: event.method || "-",
261
- model: event.model || "-",
262
- source: event.source || "-",
263
- imageRoute: event.imageRoute ?? "none",
264
- imageCount: Math.max(0, Math.trunc(event.imageCount ?? 0)),
265
- success: event.success ?? (event.statusCode >= 200 && event.statusCode < 400)
266
- };
267
609
  const daily = this.dailyStore ?? createDailyStore();
268
610
  const lifetime = this.lifetimeStore ?? createLifetimeStore();
269
- daily.days[date] = daily.days[date] ? normalizeAggregate(daily.days[date]) : createAggregate();
270
611
  addToAggregate(this.startupAggregate, normalized);
271
- addToAggregate(daily.days[date], normalized);
272
- addToAggregate(lifetime.aggregate, normalized);
273
- bumpDimension(lifetime.byAccount, normalized.profileId || normalized.accountId || normalized.accountLabel || "-", normalized.accountLabel || normalized.accountId || normalized.profileId || "-", normalized);
274
- bumpDimension(lifetime.byModel, normalized.model || "-", normalized.model || "-", normalized);
275
- bumpDimension(lifetime.byEndpoint, `${normalized.method} ${normalized.endpoint}`, `${normalized.method} ${normalized.endpoint}`, normalized);
276
- bumpDimension(lifetime.bySource, normalized.source || "-", normalized.source || "-", normalized);
277
- bumpDimension(lifetime.byImageRoute, normalized.imageRoute ?? "none", imageRouteLabel(normalized.imageRoute ?? "none"), normalized);
278
- if (!normalized.success) {
279
- const errorType = normalized.errorType?.trim() || `HTTP ${normalized.statusCode}`;
280
- bumpDimension(lifetime.byError, errorType, errorType, normalized);
281
- }
282
- daily.updatedAt = timestamp;
283
- lifetime.updatedAt = timestamp;
612
+ addEventToStores(daily, lifetime, normalized);
284
613
  this.dailyStore = daily;
285
614
  this.lifetimeStore = lifetime;
286
615
  const eventPath = path.join(getUsageEventsDir(), `${date}.jsonl`);
@@ -323,10 +652,41 @@ class UsageService {
323
652
  byModel: topRows(lifetime.byModel, 16),
324
653
  byEndpoint: topRows(lifetime.byEndpoint, 16),
325
654
  byError: topRows(lifetime.byError, 16),
655
+ byTokenUsageStatus: topRows(lifetime.byTokenUsageStatus, 8),
326
656
  byImageRoute: topRows(lifetime.byImageRoute, 8),
327
657
  bySource: topRows(lifetime.bySource, 8)
328
658
  };
329
659
  }
660
+ async backupAndReset() {
661
+ await this.ensureLoaded();
662
+ let backupDir = "";
663
+ const reset = async () => {
664
+ const usageDir = getUsageDir();
665
+ await fs.mkdir(path.dirname(usageDir), { recursive: true });
666
+ backupDir = await uniqueBackupDir(usageDir);
667
+ if (await fileExists(usageDir)) {
668
+ await fs.rename(usageDir, backupDir);
669
+ } else {
670
+ await fs.mkdir(backupDir, { recursive: true });
671
+ }
672
+ const daily = createDailyStore();
673
+ const lifetime = createLifetimeStore();
674
+ Object.assign(this.startupAggregate, createAggregate());
675
+ this.dailyStore = daily;
676
+ this.lifetimeStore = lifetime;
677
+ await fs.mkdir(getUsageEventsDir(), { recursive: true });
678
+ await Promise.all([
679
+ writeJsonAtomic(getUsageDailyPath(), daily),
680
+ writeJsonAtomic(getUsageLifetimePath(), lifetime)
681
+ ]);
682
+ };
683
+ this.saveQueue = this.saveQueue.then(reset, reset);
684
+ await this.saveQueue;
685
+ return {
686
+ backupDir,
687
+ usage: await this.getSummary()
688
+ };
689
+ }
330
690
  async ensureLoaded() {
331
691
  if (!this.loadPromise) {
332
692
  this.loadPromise = (async () => {
@@ -339,6 +699,17 @@ class UsageService {
339
699
  ]);
340
700
  this.dailyStore = normalizeDailyStore(dailyRaw);
341
701
  this.lifetimeStore = normalizeLifetimeStore(lifetimeRaw);
702
+ if (shouldBackfillUsageCosts(this.dailyStore, this.lifetimeStore) || shouldBackfillUsageDiagnostics(this.dailyStore, this.lifetimeStore)) {
703
+ const rebuilt = await rebuildStoresFromEventLogs();
704
+ if (rebuilt) {
705
+ this.dailyStore = rebuilt.daily;
706
+ this.lifetimeStore = rebuilt.lifetime;
707
+ await Promise.all([
708
+ writeJsonAtomic(getUsageDailyPath(), rebuilt.daily),
709
+ writeJsonAtomic(getUsageLifetimePath(), rebuilt.lifetime)
710
+ ]);
711
+ }
712
+ }
342
713
  })();
343
714
  }
344
715
  await this.loadPromise;
@@ -48,10 +48,71 @@ function sqliteQuote(value) {
48
48
  async function runSqlite(dbPath, sql) {
49
49
  const { stdout } = await execFileAsync("sqlite3", [dbPath, sql], {
50
50
  timeout: 15e3,
51
- maxBuffer: 1024 * 1024
51
+ maxBuffer: 8 * 1024 * 1024
52
52
  });
53
53
  return stdout.trim();
54
54
  }
55
+ function resolveCodexSessionPath(value) {
56
+ return path.isAbsolute(value) ? value : path.join(getCodexHomeDir(), value);
57
+ }
58
+ function parseLegacyHistoryThreadRows(raw) {
59
+ if (!raw.trim()) {
60
+ return [];
61
+ }
62
+ return raw.split(/\r?\n/).map((line) => {
63
+ const separator = line.indexOf(" ");
64
+ if (separator === -1) {
65
+ return null;
66
+ }
67
+ const id = line.slice(0, separator).trim();
68
+ const rolloutPath = line.slice(separator + 1).trim();
69
+ return id && rolloutPath ? { id, rolloutPath } : null;
70
+ }).filter((item) => Boolean(item));
71
+ }
72
+ async function patchSessionRolloutProvider(rolloutPath, backupSuffix, fromProvider, toProvider) {
73
+ const targetPath = resolveCodexSessionPath(rolloutPath);
74
+ let raw = "";
75
+ try {
76
+ raw = await fs.readFile(targetPath, "utf8");
77
+ } catch (error) {
78
+ if (error && typeof error === "object" && error.code === "ENOENT") {
79
+ return false;
80
+ }
81
+ throw error;
82
+ }
83
+ const newline = raw.includes("\r\n") ? "\r\n" : "\n";
84
+ const trailingNewline = raw.endsWith("\n");
85
+ const lines = raw.replace(/\r?\n$/u, "").split(/\r?\n/u);
86
+ let changed = false;
87
+ for (let index = 0; index < Math.min(lines.length, 20); index += 1) {
88
+ try {
89
+ const parsed = JSON.parse(lines[index] ?? "");
90
+ if (!isRecord(parsed) || parsed.type !== "session_meta" || !isRecord(parsed.payload)) {
91
+ continue;
92
+ }
93
+ if (parsed.payload.model_provider !== fromProvider) {
94
+ return false;
95
+ }
96
+ parsed.payload.model_provider = toProvider;
97
+ lines[index] = JSON.stringify(parsed);
98
+ changed = true;
99
+ break;
100
+ } catch {
101
+ continue;
102
+ }
103
+ }
104
+ if (!changed) {
105
+ return false;
106
+ }
107
+ await fs.copyFile(targetPath, `${targetPath}.azt-backup-${backupSuffix}`);
108
+ const tmpPath = `${targetPath}.tmp-${process.pid}`;
109
+ await fs.writeFile(tmpPath, `${lines.join(newline)}${trailingNewline ? newline : ""}`, {
110
+ encoding: "utf8",
111
+ mode: 384
112
+ });
113
+ await fs.rename(tmpPath, targetPath);
114
+ return true;
115
+ }
55
116
  async function migrateLegacyCodexHistoryProvider() {
56
117
  const dbPath = getCodexStateDbPath();
57
118
  if (!await fileExists(dbPath)) {
@@ -62,20 +123,32 @@ async function migrateLegacyCodexHistoryProvider() {
62
123
  };
63
124
  }
64
125
  try {
65
- const countRaw = await runSqlite(
126
+ const rowsRaw = await runSqlite(
66
127
  dbPath,
67
- `select count(*) from threads where model_provider=${sqliteQuote(LEGACY_CODEX_PROVIDER_ID)};`
128
+ `select id || char(9) || rollout_path from threads where model_provider=${sqliteQuote(LEGACY_CODEX_PROVIDER_ID)};`
68
129
  );
69
- const migratedCount = Number.parseInt(countRaw, 10);
70
- if (!Number.isFinite(migratedCount) || migratedCount <= 0) {
130
+ const legacyThreads = parseLegacyHistoryThreadRows(rowsRaw);
131
+ if (legacyThreads.length <= 0) {
71
132
  return {
72
133
  path: dbPath,
73
134
  migratedCount: 0,
74
135
  skipped: true
75
136
  };
76
137
  }
77
- const backupPath = `${dbPath}.azt-backup-${createBackupSuffix()}`;
138
+ const backupSuffix = createBackupSuffix();
139
+ const backupPath = `${dbPath}.azt-backup-${backupSuffix}`;
78
140
  await runSqlite(dbPath, `.backup ${sqliteQuote(backupPath)}`);
141
+ let rolloutPatchedCount = 0;
142
+ const rolloutPatchErrors = [];
143
+ for (const thread of legacyThreads) {
144
+ try {
145
+ if (await patchSessionRolloutProvider(thread.rolloutPath, backupSuffix, LEGACY_CODEX_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID)) {
146
+ rolloutPatchedCount += 1;
147
+ }
148
+ } catch (error) {
149
+ rolloutPatchErrors.push(`${thread.id}: ${error instanceof Error ? error.message : String(error)}`);
150
+ }
151
+ }
79
152
  await runSqlite(
80
153
  dbPath,
81
154
  `update threads set model_provider=${sqliteQuote(OPENAI_CODEX_PROVIDER_ID)} where model_provider=${sqliteQuote(LEGACY_CODEX_PROVIDER_ID)};`
@@ -83,7 +156,9 @@ async function migrateLegacyCodexHistoryProvider() {
83
156
  return {
84
157
  path: dbPath,
85
158
  backupPath,
86
- migratedCount
159
+ migratedCount: legacyThreads.length,
160
+ rolloutPatchedCount,
161
+ rolloutPatchErrors: rolloutPatchErrors.length ? rolloutPatchErrors.slice(0, 20) : void 0
87
162
  };
88
163
  } catch (error) {
89
164
  return {