@diogonzafe/tokenwatch 0.2.1 → 0.4.0

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/cli.js CHANGED
@@ -22,7 +22,7 @@ async function fetchRemotePrices(url = REMOTE_URL) {
22
22
  const data = await res.json();
23
23
  if (!data?.models) return null;
24
24
  await persistCache(data);
25
- return data.models;
25
+ return { models: data.models, updated_at: data.updated_at ?? "" };
26
26
  } catch {
27
27
  return null;
28
28
  }
@@ -34,7 +34,8 @@ async function loadCachedPrices() {
34
34
  const data = JSON.parse(raw);
35
35
  const age = Date.now() - (data._cachedAt ?? 0);
36
36
  if (age > CACHE_TTL_MS) return null;
37
- return data.models ?? null;
37
+ if (!data.models) return null;
38
+ return { models: data.models, updated_at: data.updated_at ?? "" };
38
39
  } catch {
39
40
  return null;
40
41
  }
@@ -98,16 +99,18 @@ var SqliteStorage = class {
98
99
  migrate() {
99
100
  this.db.exec(`
100
101
  CREATE TABLE IF NOT EXISTS usage (
101
- id INTEGER PRIMARY KEY AUTOINCREMENT,
102
- model TEXT NOT NULL,
103
- input_tokens INTEGER NOT NULL,
104
- output_tokens INTEGER NOT NULL,
105
- reasoning_tokens INTEGER NOT NULL DEFAULT 0,
106
- cost_usd REAL NOT NULL,
107
- session_id TEXT,
108
- user_id TEXT,
109
- feature TEXT,
110
- timestamp TEXT NOT NULL
102
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
103
+ model TEXT NOT NULL,
104
+ input_tokens INTEGER NOT NULL,
105
+ output_tokens INTEGER NOT NULL,
106
+ reasoning_tokens INTEGER NOT NULL DEFAULT 0,
107
+ cached_tokens INTEGER NOT NULL DEFAULT 0,
108
+ cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
109
+ cost_usd REAL NOT NULL,
110
+ session_id TEXT,
111
+ user_id TEXT,
112
+ feature TEXT,
113
+ timestamp TEXT NOT NULL
111
114
  )
112
115
  `);
113
116
  const cols = this.db.prepare(`PRAGMA table_info(usage)`).all().map((c) => c.name);
@@ -117,17 +120,26 @@ var SqliteStorage = class {
117
120
  if (!cols.includes("feature")) {
118
121
  this.db.exec(`ALTER TABLE usage ADD COLUMN feature TEXT`);
119
122
  }
123
+ if (!cols.includes("cached_tokens")) {
124
+ this.db.exec(`ALTER TABLE usage ADD COLUMN cached_tokens INTEGER NOT NULL DEFAULT 0`);
125
+ }
126
+ if (!cols.includes("cache_creation_tokens")) {
127
+ this.db.exec(`ALTER TABLE usage ADD COLUMN cache_creation_tokens INTEGER NOT NULL DEFAULT 0`);
128
+ }
120
129
  }
121
130
  record(entry) {
122
131
  this.db.prepare(
123
132
  `INSERT INTO usage
124
- (model, input_tokens, output_tokens, reasoning_tokens, cost_usd, session_id, user_id, feature, timestamp)
125
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
133
+ (model, input_tokens, output_tokens, reasoning_tokens, cached_tokens, cache_creation_tokens,
134
+ cost_usd, session_id, user_id, feature, timestamp)
135
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
126
136
  ).run(
127
137
  entry.model,
128
138
  entry.inputTokens,
129
139
  entry.outputTokens,
130
140
  entry.reasoningTokens ?? 0,
141
+ entry.cachedTokens ?? 0,
142
+ entry.cacheCreationTokens ?? 0,
131
143
  entry.costUSD,
132
144
  entry.sessionId ?? null,
133
145
  entry.userId ?? null,
@@ -142,6 +154,8 @@ var SqliteStorage = class {
142
154
  inputTokens: r.input_tokens,
143
155
  outputTokens: r.output_tokens,
144
156
  ...r.reasoning_tokens > 0 && { reasoningTokens: r.reasoning_tokens },
157
+ ...r.cached_tokens > 0 && { cachedTokens: r.cached_tokens },
158
+ ...r.cache_creation_tokens > 0 && { cacheCreationTokens: r.cache_creation_tokens },
145
159
  costUSD: r.cost_usd,
146
160
  ...r.session_id != null && { sessionId: r.session_id },
147
161
  ...r.user_id != null && { userId: r.user_id },
@@ -188,93 +202,153 @@ function lookupInMap(model, map) {
188
202
  }
189
203
  return void 0;
190
204
  }
191
- function calculateCost(inputTokens, outputTokens, price) {
192
- return inputTokens / 1e6 * price.input + outputTokens / 1e6 * price.output;
205
+ function calculateCost(inputTokens, outputTokens, price, cachedTokens = 0, cacheCreationTokens = 0) {
206
+ const regularInputCost = inputTokens / 1e6 * price.input;
207
+ const cachedReadCost = cachedTokens / 1e6 * (price.cachedInput ?? price.input);
208
+ const cacheCreationCost = cacheCreationTokens / 1e6 * (price.cacheCreationInput ?? price.input * 1.25);
209
+ const outputCost = outputTokens / 1e6 * price.output;
210
+ return regularInputCost + cachedReadCost + cacheCreationCost + outputCost;
211
+ }
212
+
213
+ // src/core/suggestions.ts
214
+ var PROVIDER_PREFIXES = ["gpt-", "claude-", "gemini-", "deepseek-"];
215
+ function getProviderPrefix(model) {
216
+ return PROVIDER_PREFIXES.find((p) => model.startsWith(p));
217
+ }
218
+ function maybeSuggestCheaperModel(model, costUSD, inputTokens, outputTokens, layers) {
219
+ if (costUSD <= 0) return;
220
+ const prefix = getProviderPrefix(model);
221
+ if (!prefix) return;
222
+ const mergedMap = {
223
+ ...layers.bundledPrices,
224
+ ...layers.remotePrices ?? {},
225
+ ...layers.customPrices ?? {}
226
+ };
227
+ let cheapestModel;
228
+ let cheapestCost = Infinity;
229
+ for (const key of Object.keys(mergedMap)) {
230
+ if (key === model || !key.startsWith(prefix)) continue;
231
+ const price = mergedMap[key];
232
+ if (!price) continue;
233
+ const candidateCost = calculateCost(inputTokens, outputTokens, price);
234
+ if (candidateCost < cheapestCost) {
235
+ cheapestCost = candidateCost;
236
+ cheapestModel = key;
237
+ }
238
+ }
239
+ if (cheapestModel === void 0 || cheapestCost >= costUSD * 0.5) return;
240
+ const savingsPct = Math.round((1 - cheapestCost / costUSD) * 100);
241
+ console.log(
242
+ `[tokenwatch] Suggestion: ${cheapestModel} could handle this for ~$${cheapestCost.toFixed(4)} (${savingsPct}% cheaper than ${model})`
243
+ );
193
244
  }
194
245
 
195
246
  // prices.json
196
247
  var prices_default = {
197
- updated_at: "2026-04-21",
248
+ updated_at: "2026-04-22",
198
249
  source: "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json",
199
250
  models: {
200
251
  "gpt-4o": {
201
252
  input: 2.5,
202
253
  output: 10,
254
+ cachedInput: 1.25,
203
255
  maxInputTokens: 128e3
204
256
  },
205
257
  "gpt-4o-mini": {
206
258
  input: 0.15,
207
259
  output: 0.6,
260
+ cachedInput: 0.075,
208
261
  maxInputTokens: 128e3
209
262
  },
210
263
  "gpt-5": {
211
264
  input: 1.25,
212
265
  output: 10,
266
+ cachedInput: 0.125,
213
267
  maxInputTokens: 272e3
214
268
  },
215
269
  "gpt-5-mini": {
216
270
  input: 0.25,
217
271
  output: 2,
272
+ cachedInput: 0.025,
218
273
  maxInputTokens: 272e3
219
274
  },
220
275
  "gpt-5-nano": {
221
276
  input: 0.05,
222
277
  output: 0.4,
278
+ cachedInput: 5e-3,
223
279
  maxInputTokens: 272e3
224
280
  },
225
281
  "claude-opus-4-6": {
226
282
  input: 5,
227
283
  output: 25,
284
+ cachedInput: 0.5,
285
+ cacheCreationInput: 6.25,
228
286
  maxInputTokens: 1e6
229
287
  },
230
288
  "claude-sonnet-4-6": {
231
289
  input: 3,
232
290
  output: 15,
291
+ cachedInput: 0.3,
292
+ cacheCreationInput: 3.75,
233
293
  maxInputTokens: 1e6
234
294
  },
235
295
  "claude-haiku-4-5": {
236
296
  input: 1,
237
297
  output: 5,
298
+ cachedInput: 0.1,
299
+ cacheCreationInput: 1.25,
238
300
  maxInputTokens: 2e5
239
301
  },
240
302
  "gemini-2.5-pro": {
241
303
  input: 1.25,
242
304
  output: 10,
305
+ cachedInput: 0.125,
243
306
  maxInputTokens: 1048576
244
307
  },
245
308
  "gemini-2.5-flash": {
246
309
  input: 0.3,
247
310
  output: 2.5,
311
+ cachedInput: 0.03,
248
312
  maxInputTokens: 1048576
249
313
  },
250
314
  "deepseek-chat": {
251
315
  input: 0.28,
252
316
  output: 0.42,
317
+ cachedInput: 0.028,
253
318
  maxInputTokens: 131072
254
319
  },
255
320
  "deepseek-reasoner": {
256
321
  input: 0.28,
257
322
  output: 0.42,
323
+ cachedInput: 0.028,
258
324
  maxInputTokens: 131072
259
325
  },
260
326
  "claude-opus-4-5": {
261
327
  input: 5,
262
328
  output: 25,
329
+ cachedInput: 0.5,
330
+ cacheCreationInput: 6.25,
263
331
  maxInputTokens: 2e5
264
332
  },
265
333
  "claude-opus-4-7": {
266
334
  input: 5,
267
335
  output: 25,
336
+ cachedInput: 0.5,
337
+ cacheCreationInput: 6.25,
268
338
  maxInputTokens: 1e6
269
339
  },
270
340
  "claude-opus-4-1": {
271
341
  input: 15,
272
342
  output: 75,
343
+ cachedInput: 1.5,
344
+ cacheCreationInput: 18.75,
273
345
  maxInputTokens: 2e5
274
346
  },
275
347
  "claude-sonnet-4-5": {
276
348
  input: 3,
277
349
  output: 15,
350
+ cachedInput: 0.3,
351
+ cacheCreationInput: 3.75,
278
352
  maxInputTokens: 2e5
279
353
  },
280
354
  "gpt-oss-120b": {
@@ -365,36 +439,43 @@ var prices_default = {
365
439
  "gpt-4.1": {
366
440
  input: 2,
367
441
  output: 8,
442
+ cachedInput: 0.5,
368
443
  maxInputTokens: 1047576
369
444
  },
370
445
  "gpt-4.1-2025-04-14": {
371
446
  input: 2,
372
447
  output: 8,
448
+ cachedInput: 0.5,
373
449
  maxInputTokens: 1047576
374
450
  },
375
451
  "gpt-4.1-mini": {
376
452
  input: 0.4,
377
453
  output: 1.6,
454
+ cachedInput: 0.1,
378
455
  maxInputTokens: 1047576
379
456
  },
380
457
  "gpt-4.1-mini-2025-04-14": {
381
458
  input: 0.4,
382
459
  output: 1.6,
460
+ cachedInput: 0.1,
383
461
  maxInputTokens: 1047576
384
462
  },
385
463
  "gpt-4.1-nano": {
386
464
  input: 0.1,
387
465
  output: 0.4,
466
+ cachedInput: 0.025,
388
467
  maxInputTokens: 1047576
389
468
  },
390
469
  "gpt-4.1-nano-2025-04-14": {
391
470
  input: 0.1,
392
471
  output: 0.4,
472
+ cachedInput: 0.025,
393
473
  maxInputTokens: 1047576
394
474
  },
395
475
  "gpt-4.5-preview": {
396
476
  input: 75,
397
477
  output: 150,
478
+ cachedInput: 37.5,
398
479
  maxInputTokens: 128e3
399
480
  },
400
481
  "gpt-4o-2024-05-13": {
@@ -405,11 +486,13 @@ var prices_default = {
405
486
  "gpt-4o-2024-08-06": {
406
487
  input: 2.5,
407
488
  output: 10,
489
+ cachedInput: 1.25,
408
490
  maxInputTokens: 128e3
409
491
  },
410
492
  "gpt-4o-2024-11-20": {
411
493
  input: 2.5,
412
494
  output: 10,
495
+ cachedInput: 1.25,
413
496
  maxInputTokens: 128e3
414
497
  },
415
498
  "gpt-audio-2025-08-28": {
@@ -435,6 +518,7 @@ var prices_default = {
435
518
  "gpt-4o-mini-2024-07-18": {
436
519
  input: 0.15,
437
520
  output: 0.6,
521
+ cachedInput: 0.075,
438
522
  maxInputTokens: 128e3
439
523
  },
440
524
  "gpt-4o-mini-audio-preview-2024-12-17": {
@@ -445,21 +529,25 @@ var prices_default = {
445
529
  "gpt-4o-mini-realtime-preview-2024-12-17": {
446
530
  input: 0.6,
447
531
  output: 2.4,
532
+ cachedInput: 0.3,
448
533
  maxInputTokens: 128e3
449
534
  },
450
535
  "gpt-realtime-2025-08-28": {
451
536
  input: 4,
452
537
  output: 16,
538
+ cachedInput: 0.4,
453
539
  maxInputTokens: 32e3
454
540
  },
455
541
  "gpt-realtime-1.5-2026-02-23": {
456
542
  input: 4,
457
543
  output: 16,
544
+ cachedInput: 4,
458
545
  maxInputTokens: 32e3
459
546
  },
460
547
  "gpt-realtime-mini-2025-10-06": {
461
548
  input: 0.6,
462
549
  output: 2.4,
550
+ cachedInput: 0.06,
463
551
  maxInputTokens: 128e3
464
552
  },
465
553
  "gpt-4o-mini-transcribe": {
@@ -470,11 +558,13 @@ var prices_default = {
470
558
  "gpt-4o-realtime-preview-2024-10-01": {
471
559
  input: 5,
472
560
  output: 20,
561
+ cachedInput: 2.5,
473
562
  maxInputTokens: 128e3
474
563
  },
475
564
  "gpt-4o-realtime-preview-2024-12-17": {
476
565
  input: 5,
477
566
  output: 20,
567
+ cachedInput: 2.5,
478
568
  maxInputTokens: 128e3
479
569
  },
480
570
  "gpt-4o-transcribe": {
@@ -490,51 +580,61 @@ var prices_default = {
490
580
  "gpt-5.1-2025-11-13": {
491
581
  input: 1.25,
492
582
  output: 10,
583
+ cachedInput: 0.125,
493
584
  maxInputTokens: 272e3
494
585
  },
495
586
  "gpt-5.1-chat-2025-11-13": {
496
587
  input: 1.25,
497
588
  output: 10,
589
+ cachedInput: 0.125,
498
590
  maxInputTokens: 128e3
499
591
  },
500
592
  "gpt-5.1-codex-2025-11-13": {
501
593
  input: 1.25,
502
594
  output: 10,
595
+ cachedInput: 0.125,
503
596
  maxInputTokens: 272e3
504
597
  },
505
598
  "gpt-5.1-codex-mini-2025-11-13": {
506
599
  input: 0.25,
507
600
  output: 2,
601
+ cachedInput: 0.025,
508
602
  maxInputTokens: 272e3
509
603
  },
510
604
  "gpt-5-2025-08-07": {
511
605
  input: 1.25,
512
606
  output: 10,
607
+ cachedInput: 0.125,
513
608
  maxInputTokens: 272e3
514
609
  },
515
610
  "gpt-5-chat": {
516
611
  input: 1.25,
517
612
  output: 10,
613
+ cachedInput: 0.125,
518
614
  maxInputTokens: 128e3
519
615
  },
520
616
  "gpt-5-chat-latest": {
521
617
  input: 1.25,
522
618
  output: 10,
619
+ cachedInput: 0.125,
523
620
  maxInputTokens: 128e3
524
621
  },
525
622
  "gpt-5-codex": {
526
623
  input: 1.25,
527
624
  output: 10,
625
+ cachedInput: 0.125,
528
626
  maxInputTokens: 272e3
529
627
  },
530
628
  "gpt-5-mini-2025-08-07": {
531
629
  input: 0.25,
532
630
  output: 2,
631
+ cachedInput: 0.025,
533
632
  maxInputTokens: 272e3
534
633
  },
535
634
  "gpt-5-nano-2025-08-07": {
536
635
  input: 0.05,
537
636
  output: 0.4,
637
+ cachedInput: 5e-3,
538
638
  maxInputTokens: 272e3
539
639
  },
540
640
  "gpt-5-pro": {
@@ -545,61 +645,73 @@ var prices_default = {
545
645
  "gpt-5.1": {
546
646
  input: 1.25,
547
647
  output: 10,
648
+ cachedInput: 0.125,
548
649
  maxInputTokens: 272e3
549
650
  },
550
651
  "gpt-5.1-chat": {
551
652
  input: 1.25,
552
653
  output: 10,
654
+ cachedInput: 0.125,
553
655
  maxInputTokens: 128e3
554
656
  },
555
657
  "gpt-5.1-codex": {
556
658
  input: 1.25,
557
659
  output: 10,
660
+ cachedInput: 0.125,
558
661
  maxInputTokens: 272e3
559
662
  },
560
663
  "gpt-5.1-codex-max": {
561
664
  input: 1.25,
562
665
  output: 10,
666
+ cachedInput: 0.125,
563
667
  maxInputTokens: 272e3
564
668
  },
565
669
  "gpt-5.1-codex-mini": {
566
670
  input: 0.25,
567
671
  output: 2,
672
+ cachedInput: 0.025,
568
673
  maxInputTokens: 272e3
569
674
  },
570
675
  "gpt-5.2": {
571
676
  input: 1.75,
572
677
  output: 14,
678
+ cachedInput: 0.175,
573
679
  maxInputTokens: 272e3
574
680
  },
575
681
  "gpt-5.2-2025-12-11": {
576
682
  input: 1.75,
577
683
  output: 14,
684
+ cachedInput: 0.175,
578
685
  maxInputTokens: 272e3
579
686
  },
580
687
  "gpt-5.2-chat": {
581
688
  input: 1.75,
582
689
  output: 14,
690
+ cachedInput: 0.175,
583
691
  maxInputTokens: 128e3
584
692
  },
585
693
  "gpt-5.2-chat-2025-12-11": {
586
694
  input: 1.75,
587
695
  output: 14,
696
+ cachedInput: 0.175,
588
697
  maxInputTokens: 128e3
589
698
  },
590
699
  "gpt-5.2-codex": {
591
700
  input: 1.75,
592
701
  output: 14,
702
+ cachedInput: 0.175,
593
703
  maxInputTokens: 272e3
594
704
  },
595
705
  "gpt-5.3-chat": {
596
706
  input: 1.75,
597
707
  output: 14,
708
+ cachedInput: 0.175,
598
709
  maxInputTokens: 128e3
599
710
  },
600
711
  "gpt-5.3-codex": {
601
712
  input: 1.75,
602
713
  output: 14,
714
+ cachedInput: 0.175,
603
715
  maxInputTokens: 272e3
604
716
  },
605
717
  "gpt-5.2-pro": {
@@ -615,71 +727,85 @@ var prices_default = {
615
727
  "gpt-5.4": {
616
728
  input: 2.5,
617
729
  output: 15,
730
+ cachedInput: 0.25,
618
731
  maxInputTokens: 105e4
619
732
  },
620
733
  "gpt-5.4-2026-03-05": {
621
734
  input: 2.5,
622
735
  output: 15,
736
+ cachedInput: 0.25,
623
737
  maxInputTokens: 105e4
624
738
  },
625
739
  "gpt-5.4-pro": {
626
740
  input: 30,
627
741
  output: 180,
742
+ cachedInput: 3,
628
743
  maxInputTokens: 105e4
629
744
  },
630
745
  "gpt-5.4-pro-2026-03-05": {
631
746
  input: 30,
632
747
  output: 180,
748
+ cachedInput: 3,
633
749
  maxInputTokens: 105e4
634
750
  },
635
751
  "gpt-5.4-mini": {
636
752
  input: 0.75,
637
753
  output: 4.5,
754
+ cachedInput: 0.075,
638
755
  maxInputTokens: 272e3
639
756
  },
640
757
  "gpt-5.4-nano": {
641
758
  input: 0.2,
642
759
  output: 1.25,
760
+ cachedInput: 0.02,
643
761
  maxInputTokens: 272e3
644
762
  },
645
763
  "o1-2024-12-17": {
646
764
  input: 15,
647
765
  output: 60,
766
+ cachedInput: 7.5,
648
767
  maxInputTokens: 2e5
649
768
  },
650
769
  "o1-mini": {
651
770
  input: 1.21,
652
771
  output: 4.84,
772
+ cachedInput: 0.605,
653
773
  maxInputTokens: 128e3
654
774
  },
655
775
  "o1-mini-2024-09-12": {
656
776
  input: 1.1,
657
777
  output: 4.4,
778
+ cachedInput: 0.55,
658
779
  maxInputTokens: 128e3
659
780
  },
660
781
  "o1-preview": {
661
782
  input: 15,
662
783
  output: 60,
784
+ cachedInput: 7.5,
663
785
  maxInputTokens: 128e3
664
786
  },
665
787
  "o1-preview-2024-09-12": {
666
788
  input: 15,
667
789
  output: 60,
790
+ cachedInput: 7.5,
668
791
  maxInputTokens: 128e3
669
792
  },
670
793
  "o3-2025-04-16": {
671
794
  input: 2,
672
795
  output: 8,
796
+ cachedInput: 0.5,
673
797
  maxInputTokens: 2e5
674
798
  },
675
799
  "o3-mini": {
676
800
  input: 1.1,
677
801
  output: 4.4,
802
+ cachedInput: 0.55,
678
803
  maxInputTokens: 2e5
679
804
  },
680
805
  "o3-mini-2025-01-31": {
681
806
  input: 1.1,
682
807
  output: 4.4,
808
+ cachedInput: 0.55,
683
809
  maxInputTokens: 2e5
684
810
  },
685
811
  "o3-pro": {
@@ -695,11 +821,13 @@ var prices_default = {
695
821
  "o4-mini": {
696
822
  input: 1.1,
697
823
  output: 4.4,
824
+ cachedInput: 0.275,
698
825
  maxInputTokens: 2e5
699
826
  },
700
827
  "o4-mini-2025-04-16": {
701
828
  input: 1.1,
702
829
  output: 4.4,
830
+ cachedInput: 0.275,
703
831
  maxInputTokens: 2e5
704
832
  },
705
833
  "deepseek-v3.2": {
@@ -720,6 +848,7 @@ var prices_default = {
720
848
  "deepseek-v3": {
721
849
  input: 0.27,
722
850
  output: 1.1,
851
+ cachedInput: 0.07,
723
852
  maxInputTokens: 65536
724
853
  },
725
854
  "deepseek-v3-0324": {
@@ -735,76 +864,105 @@ var prices_default = {
735
864
  "claude-haiku-4-5-20251001": {
736
865
  input: 1,
737
866
  output: 5,
867
+ cachedInput: 0.1,
868
+ cacheCreationInput: 1.25,
738
869
  maxInputTokens: 2e5
739
870
  },
740
871
  "claude-3-7-sonnet-20250219": {
741
872
  input: 3,
742
873
  output: 15,
874
+ cachedInput: 0.3,
875
+ cacheCreationInput: 3.75,
743
876
  maxInputTokens: 2e5
744
877
  },
745
878
  "claude-3-haiku-20240307": {
746
879
  input: 0.25,
747
880
  output: 1.25,
881
+ cachedInput: 0.03,
882
+ cacheCreationInput: 0.3,
748
883
  maxInputTokens: 2e5
749
884
  },
750
885
  "claude-3-opus-20240229": {
751
886
  input: 15,
752
887
  output: 75,
888
+ cachedInput: 1.5,
889
+ cacheCreationInput: 18.75,
753
890
  maxInputTokens: 2e5
754
891
  },
755
892
  "claude-4-opus-20250514": {
756
893
  input: 15,
757
894
  output: 75,
895
+ cachedInput: 1.5,
896
+ cacheCreationInput: 18.75,
758
897
  maxInputTokens: 2e5
759
898
  },
760
899
  "claude-4-sonnet-20250514": {
761
900
  input: 3,
762
901
  output: 15,
902
+ cachedInput: 0.3,
903
+ cacheCreationInput: 3.75,
763
904
  maxInputTokens: 1e6
764
905
  },
765
906
  "claude-sonnet-4-5-20250929": {
766
907
  input: 3,
767
908
  output: 15,
909
+ cachedInput: 0.3,
910
+ cacheCreationInput: 3.75,
768
911
  maxInputTokens: 2e5
769
912
  },
770
913
  "claude-sonnet-4-5-20250929-v1:0": {
771
914
  input: 3,
772
915
  output: 15,
916
+ cachedInput: 0.3,
917
+ cacheCreationInput: 3.75,
773
918
  maxInputTokens: 2e5
774
919
  },
775
920
  "claude-opus-4-1-20250805": {
776
921
  input: 15,
777
922
  output: 75,
923
+ cachedInput: 1.5,
924
+ cacheCreationInput: 18.75,
778
925
  maxInputTokens: 2e5
779
926
  },
780
927
  "claude-opus-4-20250514": {
781
928
  input: 15,
782
929
  output: 75,
930
+ cachedInput: 1.5,
931
+ cacheCreationInput: 18.75,
783
932
  maxInputTokens: 2e5
784
933
  },
785
934
  "claude-opus-4-5-20251101": {
786
935
  input: 5,
787
936
  output: 25,
937
+ cachedInput: 0.5,
938
+ cacheCreationInput: 6.25,
788
939
  maxInputTokens: 2e5
789
940
  },
790
941
  "claude-opus-4-6-20260205": {
791
942
  input: 5,
792
943
  output: 25,
944
+ cachedInput: 0.5,
945
+ cacheCreationInput: 6.25,
793
946
  maxInputTokens: 1e6
794
947
  },
795
948
  "claude-opus-4-7-20260416": {
796
949
  input: 5,
797
950
  output: 25,
951
+ cachedInput: 0.5,
952
+ cacheCreationInput: 6.25,
798
953
  maxInputTokens: 1e6
799
954
  },
800
955
  "claude-sonnet-4-20250514": {
801
956
  input: 3,
802
957
  output: 15,
958
+ cachedInput: 0.3,
959
+ cacheCreationInput: 3.75,
803
960
  maxInputTokens: 1e6
804
961
  },
805
962
  "codex-mini-latest": {
806
963
  input: 1.5,
807
964
  output: 6,
965
+ cachedInput: 0.375,
808
966
  maxInputTokens: 2e5
809
967
  },
810
968
  "deepseek-ai/deepseek-r1": {
@@ -854,6 +1012,7 @@ var prices_default = {
854
1012
  "deepseek-ai/deepseek-v3.1-terminus": {
855
1013
  input: 0.27,
856
1014
  output: 1,
1015
+ cachedInput: 0.216,
857
1016
  maxInputTokens: 163840
858
1017
  },
859
1018
  "deepseek-coder": {
@@ -864,26 +1023,31 @@ var prices_default = {
864
1023
  "gemini-2.0-flash": {
865
1024
  input: 0.1,
866
1025
  output: 0.4,
1026
+ cachedInput: 0.025,
867
1027
  maxInputTokens: 1048576
868
1028
  },
869
1029
  "gemini-2.0-flash-001": {
870
1030
  input: 0.1,
871
1031
  output: 0.4,
1032
+ cachedInput: 0.025,
872
1033
  maxInputTokens: 1048576
873
1034
  },
874
1035
  "gemini-2.0-flash-lite": {
875
1036
  input: 0.075,
876
1037
  output: 0.3,
1038
+ cachedInput: 0.01875,
877
1039
  maxInputTokens: 1048576
878
1040
  },
879
1041
  "gemini-2.0-flash-lite-001": {
880
1042
  input: 0.075,
881
1043
  output: 0.3,
1044
+ cachedInput: 0.01875,
882
1045
  maxInputTokens: 1048576
883
1046
  },
884
1047
  "gemini-2.5-flash-image": {
885
1048
  input: 0.3,
886
1049
  output: 2.5,
1050
+ cachedInput: 0.03,
887
1051
  maxInputTokens: 32768
888
1052
  },
889
1053
  "gemini-3-pro-image-preview": {
@@ -899,51 +1063,61 @@ var prices_default = {
899
1063
  "gemini-3.1-flash-lite-preview": {
900
1064
  input: 0.25,
901
1065
  output: 1.5,
1066
+ cachedInput: 0.025,
902
1067
  maxInputTokens: 1048576
903
1068
  },
904
1069
  "gemini-2.5-flash-lite": {
905
1070
  input: 0.1,
906
1071
  output: 0.4,
1072
+ cachedInput: 0.01,
907
1073
  maxInputTokens: 1048576
908
1074
  },
909
1075
  "gemini-2.5-flash-lite-preview-09-2025": {
910
1076
  input: 0.1,
911
1077
  output: 0.4,
1078
+ cachedInput: 0.01,
912
1079
  maxInputTokens: 1048576
913
1080
  },
914
1081
  "gemini-2.5-flash-preview-09-2025": {
915
1082
  input: 0.3,
916
1083
  output: 2.5,
1084
+ cachedInput: 0.075,
917
1085
  maxInputTokens: 1048576
918
1086
  },
919
1087
  "gemini-live-2.5-flash-preview-native-audio-09-2025": {
920
1088
  input: 0.3,
921
1089
  output: 2,
1090
+ cachedInput: 0.075,
922
1091
  maxInputTokens: 1048576
923
1092
  },
924
1093
  "gemini-2.5-flash-lite-preview-06-17": {
925
1094
  input: 0.1,
926
1095
  output: 0.4,
1096
+ cachedInput: 0.025,
927
1097
  maxInputTokens: 1048576
928
1098
  },
929
1099
  "gemini-3-pro-preview": {
930
1100
  input: 2,
931
1101
  output: 12,
1102
+ cachedInput: 0.2,
932
1103
  maxInputTokens: 1048576
933
1104
  },
934
1105
  "gemini-3.1-pro-preview": {
935
1106
  input: 2,
936
1107
  output: 12,
1108
+ cachedInput: 0.2,
937
1109
  maxInputTokens: 1048576
938
1110
  },
939
1111
  "gemini-3.1-pro-preview-customtools": {
940
1112
  input: 2,
941
1113
  output: 12,
1114
+ cachedInput: 0.2,
942
1115
  maxInputTokens: 1048576
943
1116
  },
944
1117
  "gemini-3-flash-preview": {
945
1118
  input: 0.5,
946
1119
  output: 3,
1120
+ cachedInput: 0.05,
947
1121
  maxInputTokens: 1048576
948
1122
  },
949
1123
  "gemini-robotics-er-1.5-preview": {
@@ -959,11 +1133,13 @@ var prices_default = {
959
1133
  "gemini-flash-latest": {
960
1134
  input: 0.3,
961
1135
  output: 2.5,
1136
+ cachedInput: 0.03,
962
1137
  maxInputTokens: 1048576
963
1138
  },
964
1139
  "gemini-flash-lite-latest": {
965
1140
  input: 0.1,
966
1141
  output: 0.4,
1142
+ cachedInput: 0.01,
967
1143
  maxInputTokens: 1048576
968
1144
  },
969
1145
  "gemini-gemma-2-27b-it": {
@@ -1039,39 +1215,47 @@ var prices_default = {
1039
1215
  "gpt-4o-mini-realtime-preview": {
1040
1216
  input: 0.6,
1041
1217
  output: 2.4,
1218
+ cachedInput: 0.3,
1042
1219
  maxInputTokens: 128e3
1043
1220
  },
1044
1221
  "gpt-4o-realtime-preview": {
1045
1222
  input: 5,
1046
1223
  output: 20,
1224
+ cachedInput: 2.5,
1047
1225
  maxInputTokens: 128e3
1048
1226
  },
1049
1227
  "gpt-4o-realtime-preview-2025-06-03": {
1050
1228
  input: 5,
1051
1229
  output: 20,
1230
+ cachedInput: 2.5,
1052
1231
  maxInputTokens: 128e3
1053
1232
  },
1054
1233
  "gpt-image-1.5": {
1055
1234
  input: 5,
1056
- output: 10
1235
+ output: 10,
1236
+ cachedInput: 1.25
1057
1237
  },
1058
1238
  "gpt-image-1.5-2025-12-16": {
1059
1239
  input: 5,
1060
- output: 10
1240
+ output: 10,
1241
+ cachedInput: 1.25
1061
1242
  },
1062
1243
  "gpt-5.1-chat-latest": {
1063
1244
  input: 1.25,
1064
1245
  output: 10,
1246
+ cachedInput: 0.125,
1065
1247
  maxInputTokens: 128e3
1066
1248
  },
1067
1249
  "gpt-5.2-chat-latest": {
1068
1250
  input: 1.75,
1069
1251
  output: 14,
1252
+ cachedInput: 0.175,
1070
1253
  maxInputTokens: 128e3
1071
1254
  },
1072
1255
  "gpt-5.3-chat-latest": {
1073
1256
  input: 1.75,
1074
1257
  output: 14,
1258
+ cachedInput: 0.175,
1075
1259
  maxInputTokens: 128e3
1076
1260
  },
1077
1261
  "gpt-5-pro-2025-10-06": {
@@ -1082,11 +1266,13 @@ var prices_default = {
1082
1266
  "gpt-realtime": {
1083
1267
  input: 4,
1084
1268
  output: 16,
1269
+ cachedInput: 0.4,
1085
1270
  maxInputTokens: 32e3
1086
1271
  },
1087
1272
  "gpt-realtime-1.5": {
1088
1273
  input: 4,
1089
1274
  output: 16,
1275
+ cachedInput: 0.4,
1090
1276
  maxInputTokens: 32e3
1091
1277
  },
1092
1278
  "gpt-realtime-mini": {
@@ -1133,6 +1319,7 @@ var prices_default = {
1133
1319
  o1: {
1134
1320
  input: 15,
1135
1321
  output: 60,
1322
+ cachedInput: 7.5,
1136
1323
  maxInputTokens: 2e5
1137
1324
  },
1138
1325
  "o1-pro": {
@@ -1148,6 +1335,7 @@ var prices_default = {
1148
1335
  o3: {
1149
1336
  input: 2,
1150
1337
  output: 8,
1338
+ cachedInput: 0.5,
1151
1339
  maxInputTokens: 2e5
1152
1340
  },
1153
1341
  "gpt-oss-20b": {
@@ -1172,6 +1360,8 @@ var prices_default = {
1172
1360
  "claude-haiku-4-5@20251001": {
1173
1361
  input: 1,
1174
1362
  output: 5,
1363
+ cachedInput: 0.1,
1364
+ cacheCreationInput: 1.25,
1175
1365
  maxInputTokens: 2e5
1176
1366
  },
1177
1367
  "claude-3-5-sonnet": {
@@ -1187,6 +1377,8 @@ var prices_default = {
1187
1377
  "claude-3-7-sonnet@20250219": {
1188
1378
  input: 3,
1189
1379
  output: 15,
1380
+ cachedInput: 0.3,
1381
+ cacheCreationInput: 3.75,
1190
1382
  maxInputTokens: 2e5
1191
1383
  },
1192
1384
  "claude-3-haiku": {
@@ -1222,46 +1414,64 @@ var prices_default = {
1222
1414
  "claude-opus-4": {
1223
1415
  input: 15,
1224
1416
  output: 75,
1417
+ cachedInput: 1.5,
1418
+ cacheCreationInput: 18.75,
1225
1419
  maxInputTokens: 2e5
1226
1420
  },
1227
1421
  "claude-opus-4-1@20250805": {
1228
1422
  input: 15,
1229
1423
  output: 75,
1424
+ cachedInput: 1.5,
1425
+ cacheCreationInput: 18.75,
1230
1426
  maxInputTokens: 2e5
1231
1427
  },
1232
1428
  "claude-opus-4-5@20251101": {
1233
1429
  input: 5,
1234
1430
  output: 25,
1431
+ cachedInput: 0.5,
1432
+ cacheCreationInput: 6.25,
1235
1433
  maxInputTokens: 2e5
1236
1434
  },
1237
1435
  "claude-opus-4-6@default": {
1238
1436
  input: 5,
1239
1437
  output: 25,
1438
+ cachedInput: 0.5,
1439
+ cacheCreationInput: 6.25,
1240
1440
  maxInputTokens: 1e6
1241
1441
  },
1242
1442
  "claude-opus-4-7@default": {
1243
1443
  input: 5,
1244
1444
  output: 25,
1445
+ cachedInput: 0.5,
1446
+ cacheCreationInput: 6.25,
1245
1447
  maxInputTokens: 1e6
1246
1448
  },
1247
1449
  "claude-sonnet-4-5@20250929": {
1248
1450
  input: 3,
1249
1451
  output: 15,
1452
+ cachedInput: 0.3,
1453
+ cacheCreationInput: 3.75,
1250
1454
  maxInputTokens: 2e5
1251
1455
  },
1252
1456
  "claude-opus-4@20250514": {
1253
1457
  input: 15,
1254
1458
  output: 75,
1459
+ cachedInput: 1.5,
1460
+ cacheCreationInput: 18.75,
1255
1461
  maxInputTokens: 2e5
1256
1462
  },
1257
1463
  "claude-sonnet-4": {
1258
1464
  input: 3,
1259
1465
  output: 15,
1466
+ cachedInput: 0.3,
1467
+ cacheCreationInput: 3.75,
1260
1468
  maxInputTokens: 1e6
1261
1469
  },
1262
1470
  "claude-sonnet-4@20250514": {
1263
1471
  input: 3,
1264
1472
  output: 15,
1473
+ cachedInput: 0.3,
1474
+ cacheCreationInput: 3.75,
1265
1475
  maxInputTokens: 1e6
1266
1476
  },
1267
1477
  "deepseek-ai/deepseek-v3.1-maas": {
@@ -1311,6 +1521,7 @@ var prices_default = {
1311
1521
  "gpt-realtime-mini-2025-12-15": {
1312
1522
  input: 0.6,
1313
1523
  output: 2.4,
1524
+ cachedInput: 0.06,
1314
1525
  maxInputTokens: 128e3
1315
1526
  },
1316
1527
  "gemini-2.5-flash-native-audio-latest": {
@@ -1336,16 +1547,20 @@ var prices_default = {
1336
1547
  "gemini-pro-latest": {
1337
1548
  input: 1.25,
1338
1549
  output: 10,
1550
+ cachedInput: 0.125,
1339
1551
  maxInputTokens: 1048576
1340
1552
  },
1341
1553
  "gemini-exp-1206": {
1342
1554
  input: 0.3,
1343
1555
  output: 2.5,
1556
+ cachedInput: 0.03,
1344
1557
  maxInputTokens: 1048576
1345
1558
  },
1346
1559
  "claude-sonnet-4-6@default": {
1347
1560
  input: 3,
1348
1561
  output: 15,
1562
+ cachedInput: 0.3,
1563
+ cacheCreationInput: 3.75,
1349
1564
  maxInputTokens: 1e6
1350
1565
  }
1351
1566
  }
@@ -1353,11 +1568,19 @@ var prices_default = {
1353
1568
 
1354
1569
  // src/core/tracker.ts
1355
1570
  var bundledPrices = prices_default.models;
1571
+ var bundledUpdatedAt = prices_default.updated_at ?? "";
1356
1572
  var ModelPriceSchema = z.object({
1357
1573
  input: z.number().nonnegative(),
1358
1574
  output: z.number().nonnegative(),
1575
+ cachedInput: z.number().nonnegative().optional(),
1576
+ cacheCreationInput: z.number().nonnegative().optional(),
1359
1577
  maxInputTokens: z.number().positive().optional()
1360
1578
  });
1579
+ var BudgetConfigSchema = z.object({
1580
+ threshold: z.number().positive(),
1581
+ webhookUrl: z.string().url(),
1582
+ mode: z.enum(["once", "always"]).optional().default("once")
1583
+ });
1361
1584
  var TrackerConfigSchema = z.object({
1362
1585
  storage: z.union([z.enum(["memory", "sqlite"]), z.custom((v) => {
1363
1586
  return v !== null && typeof v === "object" && typeof v.record === "function" && typeof v.getAll === "function" && typeof v.clearAll === "function" && typeof v.clearSession === "function";
@@ -1365,7 +1588,13 @@ var TrackerConfigSchema = z.object({
1365
1588
  alertThreshold: z.number().positive().optional(),
1366
1589
  webhookUrl: z.string().url().optional(),
1367
1590
  syncPrices: z.boolean().optional().default(true),
1368
- customPrices: z.record(z.string(), ModelPriceSchema).optional()
1591
+ customPrices: z.record(z.string(), ModelPriceSchema).optional(),
1592
+ warnIfStaleAfterHours: z.number().nonnegative().optional().default(72),
1593
+ budgets: z.object({
1594
+ perUser: BudgetConfigSchema.optional(),
1595
+ perSession: BudgetConfigSchema.optional()
1596
+ }).optional(),
1597
+ suggestions: z.boolean().optional().default(false)
1369
1598
  });
1370
1599
  function createTracker(config = {}) {
1371
1600
  const parsed = TrackerConfigSchema.safeParse(config);
@@ -1379,19 +1608,45 @@ ${issues}`);
1379
1608
  alertThreshold,
1380
1609
  webhookUrl,
1381
1610
  syncPrices,
1382
- customPrices
1611
+ customPrices,
1612
+ warnIfStaleAfterHours,
1613
+ budgets,
1614
+ suggestions
1383
1615
  } = parsed.data;
1384
1616
  const storage = typeof storageOption === "object" ? storageOption : createStorage(storageOption);
1385
1617
  let remotePrices;
1618
+ let pricesUpdatedAt = bundledUpdatedAt;
1386
1619
  if (syncPrices) {
1387
1620
  getRemotePrices().then((result) => {
1388
- if (result) remotePrices = result;
1621
+ if (result) {
1622
+ remotePrices = result.models;
1623
+ pricesUpdatedAt = result.updated_at;
1624
+ }
1389
1625
  }).catch(() => {
1390
1626
  });
1391
1627
  }
1628
+ let stalenessChecked = false;
1629
+ function maybeWarnStaleness() {
1630
+ if (stalenessChecked || !warnIfStaleAfterHours) return;
1631
+ stalenessChecked = true;
1632
+ if (!pricesUpdatedAt) return;
1633
+ try {
1634
+ const updatedMs = new Date(pricesUpdatedAt).getTime();
1635
+ const ageHours = (Date.now() - updatedMs) / (1e3 * 60 * 60);
1636
+ if (ageHours > warnIfStaleAfterHours) {
1637
+ console.warn(
1638
+ `[tokenwatch] Price data is ${Math.round(ageHours)}h old (updated_at: ${pricesUpdatedAt}). Run "tokenwatch sync" to refresh, or set warnIfStaleAfterHours: 0 to suppress.`
1639
+ );
1640
+ }
1641
+ } catch {
1642
+ }
1643
+ }
1392
1644
  let alertFired = false;
1645
+ const firedUserAlerts = /* @__PURE__ */ new Set();
1646
+ const firedSessionAlerts = /* @__PURE__ */ new Set();
1393
1647
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1394
1648
  function resolveModelPrice(model) {
1649
+ maybeWarnStaleness();
1395
1650
  return resolvePrice(model, {
1396
1651
  bundledPrices,
1397
1652
  ...customPrices !== void 0 && { customPrices },
@@ -1400,39 +1655,94 @@ ${issues}`);
1400
1655
  }
1401
1656
  function track(entry) {
1402
1657
  const price = resolveModelPrice(entry.model);
1403
- const costUSD = calculateCost(entry.inputTokens, entry.outputTokens, price);
1658
+ const costUSD = calculateCost(
1659
+ entry.inputTokens,
1660
+ entry.outputTokens,
1661
+ price,
1662
+ entry.cachedTokens,
1663
+ entry.cacheCreationTokens
1664
+ );
1404
1665
  const full = {
1405
1666
  ...entry,
1406
1667
  costUSD,
1407
1668
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1408
1669
  };
1409
1670
  storage.record(full);
1410
- maybeFireAlert();
1671
+ maybeFireAlerts(full);
1672
+ if (suggestions) {
1673
+ maybeSuggestCheaperModel(entry.model, costUSD, entry.inputTokens, entry.outputTokens, {
1674
+ bundledPrices,
1675
+ ...customPrices !== void 0 && { customPrices },
1676
+ ...remotePrices !== void 0 && { remotePrices }
1677
+ });
1678
+ }
1411
1679
  }
1412
- function maybeFireAlert() {
1413
- if (!alertThreshold || !webhookUrl || alertFired) return;
1414
- alertFired = true;
1415
- Promise.resolve(storage.getAll()).then((entries) => {
1416
- const total = computeTotal(entries);
1417
- if (total < alertThreshold) {
1418
- alertFired = false;
1419
- return;
1420
- }
1421
- const payload = {
1422
- text: `[tokenwatch] Alert: total cost reached $${total.toFixed(4)} USD (threshold: $${alertThreshold})`
1423
- };
1424
- fetch(webhookUrl, {
1425
- method: "POST",
1426
- headers: { "Content-Type": "application/json" },
1427
- body: JSON.stringify(payload)
1680
+ function maybeFireAlerts(entry) {
1681
+ if (alertThreshold && webhookUrl && !alertFired) {
1682
+ alertFired = true;
1683
+ Promise.resolve(storage.getAll()).then((entries) => {
1684
+ const total = computeTotal(entries);
1685
+ if (total < alertThreshold) {
1686
+ alertFired = false;
1687
+ return;
1688
+ }
1689
+ fireWebhook(webhookUrl, {
1690
+ text: `[tokenwatch] Alert: total cost reached $${total.toFixed(4)} USD (threshold: $${alertThreshold})`
1691
+ });
1428
1692
  }).catch(() => {
1693
+ alertFired = false;
1429
1694
  });
1695
+ }
1696
+ if (budgets?.perUser && entry.userId) {
1697
+ const cfg = budgets.perUser;
1698
+ const uid = entry.userId;
1699
+ if (cfg.mode === "always" || !firedUserAlerts.has(uid)) {
1700
+ if (cfg.mode !== "always") firedUserAlerts.add(uid);
1701
+ Promise.resolve(storage.getAll()).then((entries) => {
1702
+ const userCost = entries.filter((e) => e.userId === uid).reduce((s, e) => s + e.costUSD, 0);
1703
+ if (userCost >= cfg.threshold) {
1704
+ fireWebhook(cfg.webhookUrl, {
1705
+ text: `[tokenwatch] Budget alert: user "${uid}" reached $${userCost.toFixed(4)} USD (threshold: $${cfg.threshold})`
1706
+ });
1707
+ } else {
1708
+ if (cfg.mode !== "always") firedUserAlerts.delete(uid);
1709
+ }
1710
+ }).catch(() => {
1711
+ if (cfg.mode !== "always") firedUserAlerts.delete(uid);
1712
+ });
1713
+ }
1714
+ }
1715
+ if (budgets?.perSession && entry.sessionId) {
1716
+ const cfg = budgets.perSession;
1717
+ const sid = entry.sessionId;
1718
+ if (cfg.mode === "always" || !firedSessionAlerts.has(sid)) {
1719
+ if (cfg.mode !== "always") firedSessionAlerts.add(sid);
1720
+ Promise.resolve(storage.getAll()).then((entries) => {
1721
+ const sessionCost = entries.filter((e) => e.sessionId === sid).reduce((s, e) => s + e.costUSD, 0);
1722
+ if (sessionCost >= cfg.threshold) {
1723
+ fireWebhook(cfg.webhookUrl, {
1724
+ text: `[tokenwatch] Budget alert: session "${sid}" reached $${sessionCost.toFixed(4)} USD (threshold: $${cfg.threshold})`
1725
+ });
1726
+ } else {
1727
+ if (cfg.mode !== "always") firedSessionAlerts.delete(sid);
1728
+ }
1729
+ }).catch(() => {
1730
+ if (cfg.mode !== "always") firedSessionAlerts.delete(sid);
1731
+ });
1732
+ }
1733
+ }
1734
+ }
1735
+ function fireWebhook(url, payload) {
1736
+ fetch(url, {
1737
+ method: "POST",
1738
+ headers: { "Content-Type": "application/json" },
1739
+ body: JSON.stringify(payload)
1430
1740
  }).catch(() => {
1431
- alertFired = false;
1432
1741
  });
1433
1742
  }
1434
- async function getReport() {
1435
- const entries = await Promise.resolve(storage.getAll());
1743
+ async function getReport(options) {
1744
+ const allEntries = await Promise.resolve(storage.getAll());
1745
+ const entries = filterEntries(allEntries, options);
1436
1746
  const byModel = {};
1437
1747
  const bySession = {};
1438
1748
  const byUser = {};
@@ -1440,18 +1750,24 @@ ${issues}`);
1440
1750
  let totalInput = 0;
1441
1751
  let totalOutput = 0;
1442
1752
  let totalCost = 0;
1443
- let lastTimestamp = startedAt;
1753
+ let periodFrom = options ? entries[0]?.timestamp ?? startedAt : startedAt;
1754
+ let lastTimestamp = periodFrom;
1444
1755
  for (const e of entries) {
1445
- totalInput += e.inputTokens;
1756
+ totalInput += e.inputTokens + (e.cachedTokens ?? 0) + (e.cacheCreationTokens ?? 0);
1446
1757
  totalOutput += e.outputTokens;
1447
1758
  totalCost += e.costUSD;
1448
1759
  if (e.timestamp > lastTimestamp) lastTimestamp = e.timestamp;
1449
- const m = byModel[e.model] ??= { costUSD: 0, calls: 0, tokens: { input: 0, output: 0, reasoning: 0 } };
1760
+ const m = byModel[e.model] ??= {
1761
+ costUSD: 0,
1762
+ calls: 0,
1763
+ tokens: { input: 0, output: 0, reasoning: 0, cached: 0 }
1764
+ };
1450
1765
  m.costUSD += e.costUSD;
1451
1766
  m.calls += 1;
1452
- m.tokens.input += e.inputTokens;
1767
+ m.tokens.input += e.inputTokens + (e.cachedTokens ?? 0) + (e.cacheCreationTokens ?? 0);
1453
1768
  m.tokens.output += e.outputTokens;
1454
1769
  m.tokens.reasoning += e.reasoningTokens ?? 0;
1770
+ m.tokens.cached += e.cachedTokens ?? 0;
1455
1771
  if (e.sessionId) {
1456
1772
  const s = bySession[e.sessionId] ??= { costUSD: 0, calls: 0 };
1457
1773
  s.costUSD += e.costUSD;
@@ -1468,6 +1784,9 @@ ${issues}`);
1468
1784
  f.calls += 1;
1469
1785
  }
1470
1786
  }
1787
+ if (options && entries.length > 0) {
1788
+ periodFrom = entries[0]?.timestamp ?? periodFrom;
1789
+ }
1471
1790
  return {
1472
1791
  totalCostUSD: totalCost,
1473
1792
  totalTokens: { input: totalInput, output: totalOutput },
@@ -1475,22 +1794,66 @@ ${issues}`);
1475
1794
  bySession,
1476
1795
  byUser,
1477
1796
  byFeature,
1478
- period: { from: startedAt, to: lastTimestamp }
1797
+ period: { from: periodFrom, to: lastTimestamp },
1798
+ ...pricesUpdatedAt ? { pricesUpdatedAt } : {}
1799
+ };
1800
+ }
1801
+ async function getCostForecast(options = {}) {
1802
+ const windowHours = options.windowHours ?? 24;
1803
+ const allEntries = await Promise.resolve(storage.getAll());
1804
+ const now = Date.now();
1805
+ const windowStart = now - windowHours * 60 * 60 * 1e3;
1806
+ const windowEntries = allEntries.filter(
1807
+ (e) => new Date(e.timestamp).getTime() >= windowStart
1808
+ );
1809
+ if (windowEntries.length < 2) {
1810
+ return {
1811
+ burnRatePerHour: 0,
1812
+ projectedDailyCostUSD: 0,
1813
+ projectedMonthlyCostUSD: 0,
1814
+ basedOnHours: 0,
1815
+ basedOnPeriod: null
1816
+ };
1817
+ }
1818
+ const first = windowEntries[0]?.timestamp ?? "";
1819
+ const last = windowEntries[windowEntries.length - 1]?.timestamp ?? "";
1820
+ const actualMs = new Date(last).getTime() - new Date(first).getTime();
1821
+ const actualHours = actualMs / (1e3 * 60 * 60);
1822
+ if (actualHours < 1e-3) {
1823
+ return {
1824
+ burnRatePerHour: 0,
1825
+ projectedDailyCostUSD: 0,
1826
+ projectedMonthlyCostUSD: 0,
1827
+ basedOnHours: 0,
1828
+ basedOnPeriod: { from: first, to: last }
1829
+ };
1830
+ }
1831
+ const totalCost = windowEntries.reduce((s, e) => s + e.costUSD, 0);
1832
+ const burnRatePerHour = totalCost / actualHours;
1833
+ return {
1834
+ burnRatePerHour,
1835
+ projectedDailyCostUSD: burnRatePerHour * 24,
1836
+ projectedMonthlyCostUSD: burnRatePerHour * 24 * 30,
1837
+ basedOnHours: Math.round(actualHours * 100) / 100,
1838
+ basedOnPeriod: { from: first, to: last }
1479
1839
  };
1480
1840
  }
1481
1841
  async function reset() {
1482
1842
  await Promise.resolve(storage.clearAll());
1483
1843
  alertFired = false;
1844
+ firedUserAlerts.clear();
1845
+ firedSessionAlerts.clear();
1484
1846
  }
1485
1847
  async function resetSession(sessionId) {
1486
1848
  await Promise.resolve(storage.clearSession(sessionId));
1849
+ firedSessionAlerts.delete(sessionId);
1487
1850
  }
1488
1851
  async function exportJSON() {
1489
1852
  return JSON.stringify(await getReport(), null, 2);
1490
1853
  }
1491
1854
  async function exportCSV() {
1492
1855
  const entries = await Promise.resolve(storage.getAll());
1493
- const header = "timestamp,model,inputTokens,outputTokens,reasoningTokens,costUSD,sessionId,userId,feature";
1856
+ const header = "timestamp,model,inputTokens,outputTokens,reasoningTokens,cachedTokens,cacheCreationTokens,costUSD,sessionId,userId,feature";
1494
1857
  const rows = entries.map(
1495
1858
  (e) => [
1496
1859
  csvEscape(e.timestamp),
@@ -1498,6 +1861,8 @@ ${issues}`);
1498
1861
  e.inputTokens,
1499
1862
  e.outputTokens,
1500
1863
  e.reasoningTokens ?? 0,
1864
+ e.cachedTokens ?? 0,
1865
+ e.cacheCreationTokens ?? 0,
1501
1866
  e.costUSD.toFixed(8),
1502
1867
  csvEscape(e.sessionId ?? ""),
1503
1868
  csvEscape(e.userId ?? ""),
@@ -1513,11 +1878,47 @@ ${issues}`);
1513
1878
  ...remotePrices !== void 0 && { remotePrices }
1514
1879
  }) ?? null;
1515
1880
  }
1516
- return { track, getReport, reset, resetSession, exportJSON, exportCSV, getModelInfo };
1881
+ return {
1882
+ track,
1883
+ getReport,
1884
+ getCostForecast,
1885
+ reset,
1886
+ resetSession,
1887
+ exportJSON,
1888
+ exportCSV,
1889
+ getModelInfo
1890
+ };
1517
1891
  }
1518
1892
  function computeTotal(entries) {
1519
1893
  return entries.reduce((sum, e) => sum + e.costUSD, 0);
1520
1894
  }
1895
+ function parseLastMs(last) {
1896
+ const match = /^(\d+(?:\.\d+)?)(h|d)$/.exec(last.trim());
1897
+ if (!match) throw new Error(`[tokenwatch] Invalid "last" value: "${last}". Use e.g. "24h", "7d".`);
1898
+ const value = parseFloat(match[1] ?? "0");
1899
+ const unit = match[2] ?? "h";
1900
+ return unit === "h" ? value * 60 * 60 * 1e3 : value * 24 * 60 * 60 * 1e3;
1901
+ }
1902
+ function filterEntries(entries, options) {
1903
+ if (!options) return entries;
1904
+ let sinceMs;
1905
+ let untilMs;
1906
+ if (options.last) {
1907
+ sinceMs = Date.now() - parseLastMs(options.last);
1908
+ } else if (options.since) {
1909
+ sinceMs = new Date(options.since).getTime();
1910
+ }
1911
+ if (options.until) {
1912
+ untilMs = new Date(options.until).getTime();
1913
+ }
1914
+ if (sinceMs === void 0 && untilMs === void 0) return entries;
1915
+ return entries.filter((e) => {
1916
+ const ts = new Date(e.timestamp).getTime();
1917
+ if (sinceMs !== void 0 && ts < sinceMs) return false;
1918
+ if (untilMs !== void 0 && ts > untilMs) return false;
1919
+ return true;
1920
+ });
1921
+ }
1521
1922
  function csvEscape(value) {
1522
1923
  if (value.includes(",") || value.includes('"') || value.includes("\n")) {
1523
1924
  return `"${value.replace(/"/g, '""')}"`;
@@ -1538,7 +1939,7 @@ async function cmdSync() {
1538
1939
  console.log("Fetching latest prices from remote...");
1539
1940
  const result = await fetchRemotePrices();
1540
1941
  if (result) {
1541
- console.log(`\u2713 Prices updated. ${Object.keys(result).length} models cached.`);
1942
+ console.log(`\u2713 Prices updated. ${Object.keys(result.models).length} models cached (updated_at: ${result.updated_at}).`);
1542
1943
  } else {
1543
1944
  console.error("\u2717 Failed to fetch remote prices. Check your internet connection.");
1544
1945
  process.exit(1);
@@ -1584,6 +1985,9 @@ async function cmdReport() {
1584
1985
  console.log(` Total cost: $${report.totalCostUSD.toFixed(6)} USD`);
1585
1986
  console.log(` Total tokens: ${report.totalTokens.input.toLocaleString()} in / ${report.totalTokens.output.toLocaleString()} out`);
1586
1987
  console.log(` Period: ${report.period.from} \u2192 ${report.period.to}`);
1988
+ if (report.pricesUpdatedAt) {
1989
+ console.log(` Prices as of: ${report.pricesUpdatedAt}`);
1990
+ }
1587
1991
  if (Object.keys(report.byModel).length > 0) {
1588
1992
  console.log("\n By model:");
1589
1993
  for (const [model, stats] of Object.entries(report.byModel)) {