@diogonzafe/tokenwatch 0.2.0 → 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,29 +99,51 @@ 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
- cost_usd REAL NOT NULL,
106
- session_id TEXT,
107
- user_id TEXT,
108
- 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
109
114
  )
110
115
  `);
116
+ const cols = this.db.prepare(`PRAGMA table_info(usage)`).all().map((c) => c.name);
117
+ if (!cols.includes("reasoning_tokens")) {
118
+ this.db.exec(`ALTER TABLE usage ADD COLUMN reasoning_tokens INTEGER NOT NULL DEFAULT 0`);
119
+ }
120
+ if (!cols.includes("feature")) {
121
+ this.db.exec(`ALTER TABLE usage ADD COLUMN feature TEXT`);
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
+ }
111
129
  }
112
130
  record(entry) {
113
131
  this.db.prepare(
114
132
  `INSERT INTO usage
115
- (model, input_tokens, output_tokens, cost_usd, session_id, user_id, timestamp)
116
- 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
117
136
  ).run(
118
137
  entry.model,
119
138
  entry.inputTokens,
120
139
  entry.outputTokens,
140
+ entry.reasoningTokens ?? 0,
141
+ entry.cachedTokens ?? 0,
142
+ entry.cacheCreationTokens ?? 0,
121
143
  entry.costUSD,
122
144
  entry.sessionId ?? null,
123
145
  entry.userId ?? null,
146
+ entry.feature ?? null,
124
147
  entry.timestamp
125
148
  );
126
149
  }
@@ -130,9 +153,13 @@ var SqliteStorage = class {
130
153
  model: r.model,
131
154
  inputTokens: r.input_tokens,
132
155
  outputTokens: r.output_tokens,
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 },
133
159
  costUSD: r.cost_usd,
134
160
  ...r.session_id != null && { sessionId: r.session_id },
135
161
  ...r.user_id != null && { userId: r.user_id },
162
+ ...r.feature != null && { feature: r.feature },
136
163
  timestamp: r.timestamp
137
164
  }));
138
165
  }
@@ -175,93 +202,153 @@ function lookupInMap(model, map) {
175
202
  }
176
203
  return void 0;
177
204
  }
178
- function calculateCost(inputTokens, outputTokens, price) {
179
- 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
+ );
180
244
  }
181
245
 
182
246
  // prices.json
183
247
  var prices_default = {
184
- updated_at: "2026-04-21",
248
+ updated_at: "2026-04-22",
185
249
  source: "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json",
186
250
  models: {
187
251
  "gpt-4o": {
188
252
  input: 2.5,
189
253
  output: 10,
254
+ cachedInput: 1.25,
190
255
  maxInputTokens: 128e3
191
256
  },
192
257
  "gpt-4o-mini": {
193
258
  input: 0.15,
194
259
  output: 0.6,
260
+ cachedInput: 0.075,
195
261
  maxInputTokens: 128e3
196
262
  },
197
263
  "gpt-5": {
198
264
  input: 1.25,
199
265
  output: 10,
266
+ cachedInput: 0.125,
200
267
  maxInputTokens: 272e3
201
268
  },
202
269
  "gpt-5-mini": {
203
270
  input: 0.25,
204
271
  output: 2,
272
+ cachedInput: 0.025,
205
273
  maxInputTokens: 272e3
206
274
  },
207
275
  "gpt-5-nano": {
208
276
  input: 0.05,
209
277
  output: 0.4,
278
+ cachedInput: 5e-3,
210
279
  maxInputTokens: 272e3
211
280
  },
212
281
  "claude-opus-4-6": {
213
282
  input: 5,
214
283
  output: 25,
284
+ cachedInput: 0.5,
285
+ cacheCreationInput: 6.25,
215
286
  maxInputTokens: 1e6
216
287
  },
217
288
  "claude-sonnet-4-6": {
218
289
  input: 3,
219
290
  output: 15,
291
+ cachedInput: 0.3,
292
+ cacheCreationInput: 3.75,
220
293
  maxInputTokens: 1e6
221
294
  },
222
295
  "claude-haiku-4-5": {
223
296
  input: 1,
224
297
  output: 5,
298
+ cachedInput: 0.1,
299
+ cacheCreationInput: 1.25,
225
300
  maxInputTokens: 2e5
226
301
  },
227
302
  "gemini-2.5-pro": {
228
303
  input: 1.25,
229
304
  output: 10,
305
+ cachedInput: 0.125,
230
306
  maxInputTokens: 1048576
231
307
  },
232
308
  "gemini-2.5-flash": {
233
309
  input: 0.3,
234
310
  output: 2.5,
311
+ cachedInput: 0.03,
235
312
  maxInputTokens: 1048576
236
313
  },
237
314
  "deepseek-chat": {
238
315
  input: 0.28,
239
316
  output: 0.42,
317
+ cachedInput: 0.028,
240
318
  maxInputTokens: 131072
241
319
  },
242
320
  "deepseek-reasoner": {
243
321
  input: 0.28,
244
322
  output: 0.42,
323
+ cachedInput: 0.028,
245
324
  maxInputTokens: 131072
246
325
  },
247
326
  "claude-opus-4-5": {
248
327
  input: 5,
249
328
  output: 25,
329
+ cachedInput: 0.5,
330
+ cacheCreationInput: 6.25,
250
331
  maxInputTokens: 2e5
251
332
  },
252
333
  "claude-opus-4-7": {
253
334
  input: 5,
254
335
  output: 25,
336
+ cachedInput: 0.5,
337
+ cacheCreationInput: 6.25,
255
338
  maxInputTokens: 1e6
256
339
  },
257
340
  "claude-opus-4-1": {
258
341
  input: 15,
259
342
  output: 75,
343
+ cachedInput: 1.5,
344
+ cacheCreationInput: 18.75,
260
345
  maxInputTokens: 2e5
261
346
  },
262
347
  "claude-sonnet-4-5": {
263
348
  input: 3,
264
349
  output: 15,
350
+ cachedInput: 0.3,
351
+ cacheCreationInput: 3.75,
265
352
  maxInputTokens: 2e5
266
353
  },
267
354
  "gpt-oss-120b": {
@@ -352,36 +439,43 @@ var prices_default = {
352
439
  "gpt-4.1": {
353
440
  input: 2,
354
441
  output: 8,
442
+ cachedInput: 0.5,
355
443
  maxInputTokens: 1047576
356
444
  },
357
445
  "gpt-4.1-2025-04-14": {
358
446
  input: 2,
359
447
  output: 8,
448
+ cachedInput: 0.5,
360
449
  maxInputTokens: 1047576
361
450
  },
362
451
  "gpt-4.1-mini": {
363
452
  input: 0.4,
364
453
  output: 1.6,
454
+ cachedInput: 0.1,
365
455
  maxInputTokens: 1047576
366
456
  },
367
457
  "gpt-4.1-mini-2025-04-14": {
368
458
  input: 0.4,
369
459
  output: 1.6,
460
+ cachedInput: 0.1,
370
461
  maxInputTokens: 1047576
371
462
  },
372
463
  "gpt-4.1-nano": {
373
464
  input: 0.1,
374
465
  output: 0.4,
466
+ cachedInput: 0.025,
375
467
  maxInputTokens: 1047576
376
468
  },
377
469
  "gpt-4.1-nano-2025-04-14": {
378
470
  input: 0.1,
379
471
  output: 0.4,
472
+ cachedInput: 0.025,
380
473
  maxInputTokens: 1047576
381
474
  },
382
475
  "gpt-4.5-preview": {
383
476
  input: 75,
384
477
  output: 150,
478
+ cachedInput: 37.5,
385
479
  maxInputTokens: 128e3
386
480
  },
387
481
  "gpt-4o-2024-05-13": {
@@ -392,11 +486,13 @@ var prices_default = {
392
486
  "gpt-4o-2024-08-06": {
393
487
  input: 2.5,
394
488
  output: 10,
489
+ cachedInput: 1.25,
395
490
  maxInputTokens: 128e3
396
491
  },
397
492
  "gpt-4o-2024-11-20": {
398
493
  input: 2.5,
399
494
  output: 10,
495
+ cachedInput: 1.25,
400
496
  maxInputTokens: 128e3
401
497
  },
402
498
  "gpt-audio-2025-08-28": {
@@ -422,6 +518,7 @@ var prices_default = {
422
518
  "gpt-4o-mini-2024-07-18": {
423
519
  input: 0.15,
424
520
  output: 0.6,
521
+ cachedInput: 0.075,
425
522
  maxInputTokens: 128e3
426
523
  },
427
524
  "gpt-4o-mini-audio-preview-2024-12-17": {
@@ -432,21 +529,25 @@ var prices_default = {
432
529
  "gpt-4o-mini-realtime-preview-2024-12-17": {
433
530
  input: 0.6,
434
531
  output: 2.4,
532
+ cachedInput: 0.3,
435
533
  maxInputTokens: 128e3
436
534
  },
437
535
  "gpt-realtime-2025-08-28": {
438
536
  input: 4,
439
537
  output: 16,
538
+ cachedInput: 0.4,
440
539
  maxInputTokens: 32e3
441
540
  },
442
541
  "gpt-realtime-1.5-2026-02-23": {
443
542
  input: 4,
444
543
  output: 16,
544
+ cachedInput: 4,
445
545
  maxInputTokens: 32e3
446
546
  },
447
547
  "gpt-realtime-mini-2025-10-06": {
448
548
  input: 0.6,
449
549
  output: 2.4,
550
+ cachedInput: 0.06,
450
551
  maxInputTokens: 128e3
451
552
  },
452
553
  "gpt-4o-mini-transcribe": {
@@ -457,11 +558,13 @@ var prices_default = {
457
558
  "gpt-4o-realtime-preview-2024-10-01": {
458
559
  input: 5,
459
560
  output: 20,
561
+ cachedInput: 2.5,
460
562
  maxInputTokens: 128e3
461
563
  },
462
564
  "gpt-4o-realtime-preview-2024-12-17": {
463
565
  input: 5,
464
566
  output: 20,
567
+ cachedInput: 2.5,
465
568
  maxInputTokens: 128e3
466
569
  },
467
570
  "gpt-4o-transcribe": {
@@ -477,51 +580,61 @@ var prices_default = {
477
580
  "gpt-5.1-2025-11-13": {
478
581
  input: 1.25,
479
582
  output: 10,
583
+ cachedInput: 0.125,
480
584
  maxInputTokens: 272e3
481
585
  },
482
586
  "gpt-5.1-chat-2025-11-13": {
483
587
  input: 1.25,
484
588
  output: 10,
589
+ cachedInput: 0.125,
485
590
  maxInputTokens: 128e3
486
591
  },
487
592
  "gpt-5.1-codex-2025-11-13": {
488
593
  input: 1.25,
489
594
  output: 10,
595
+ cachedInput: 0.125,
490
596
  maxInputTokens: 272e3
491
597
  },
492
598
  "gpt-5.1-codex-mini-2025-11-13": {
493
599
  input: 0.25,
494
600
  output: 2,
601
+ cachedInput: 0.025,
495
602
  maxInputTokens: 272e3
496
603
  },
497
604
  "gpt-5-2025-08-07": {
498
605
  input: 1.25,
499
606
  output: 10,
607
+ cachedInput: 0.125,
500
608
  maxInputTokens: 272e3
501
609
  },
502
610
  "gpt-5-chat": {
503
611
  input: 1.25,
504
612
  output: 10,
613
+ cachedInput: 0.125,
505
614
  maxInputTokens: 128e3
506
615
  },
507
616
  "gpt-5-chat-latest": {
508
617
  input: 1.25,
509
618
  output: 10,
619
+ cachedInput: 0.125,
510
620
  maxInputTokens: 128e3
511
621
  },
512
622
  "gpt-5-codex": {
513
623
  input: 1.25,
514
624
  output: 10,
625
+ cachedInput: 0.125,
515
626
  maxInputTokens: 272e3
516
627
  },
517
628
  "gpt-5-mini-2025-08-07": {
518
629
  input: 0.25,
519
630
  output: 2,
631
+ cachedInput: 0.025,
520
632
  maxInputTokens: 272e3
521
633
  },
522
634
  "gpt-5-nano-2025-08-07": {
523
635
  input: 0.05,
524
636
  output: 0.4,
637
+ cachedInput: 5e-3,
525
638
  maxInputTokens: 272e3
526
639
  },
527
640
  "gpt-5-pro": {
@@ -532,61 +645,73 @@ var prices_default = {
532
645
  "gpt-5.1": {
533
646
  input: 1.25,
534
647
  output: 10,
648
+ cachedInput: 0.125,
535
649
  maxInputTokens: 272e3
536
650
  },
537
651
  "gpt-5.1-chat": {
538
652
  input: 1.25,
539
653
  output: 10,
654
+ cachedInput: 0.125,
540
655
  maxInputTokens: 128e3
541
656
  },
542
657
  "gpt-5.1-codex": {
543
658
  input: 1.25,
544
659
  output: 10,
660
+ cachedInput: 0.125,
545
661
  maxInputTokens: 272e3
546
662
  },
547
663
  "gpt-5.1-codex-max": {
548
664
  input: 1.25,
549
665
  output: 10,
666
+ cachedInput: 0.125,
550
667
  maxInputTokens: 272e3
551
668
  },
552
669
  "gpt-5.1-codex-mini": {
553
670
  input: 0.25,
554
671
  output: 2,
672
+ cachedInput: 0.025,
555
673
  maxInputTokens: 272e3
556
674
  },
557
675
  "gpt-5.2": {
558
676
  input: 1.75,
559
677
  output: 14,
678
+ cachedInput: 0.175,
560
679
  maxInputTokens: 272e3
561
680
  },
562
681
  "gpt-5.2-2025-12-11": {
563
682
  input: 1.75,
564
683
  output: 14,
684
+ cachedInput: 0.175,
565
685
  maxInputTokens: 272e3
566
686
  },
567
687
  "gpt-5.2-chat": {
568
688
  input: 1.75,
569
689
  output: 14,
690
+ cachedInput: 0.175,
570
691
  maxInputTokens: 128e3
571
692
  },
572
693
  "gpt-5.2-chat-2025-12-11": {
573
694
  input: 1.75,
574
695
  output: 14,
696
+ cachedInput: 0.175,
575
697
  maxInputTokens: 128e3
576
698
  },
577
699
  "gpt-5.2-codex": {
578
700
  input: 1.75,
579
701
  output: 14,
702
+ cachedInput: 0.175,
580
703
  maxInputTokens: 272e3
581
704
  },
582
705
  "gpt-5.3-chat": {
583
706
  input: 1.75,
584
707
  output: 14,
708
+ cachedInput: 0.175,
585
709
  maxInputTokens: 128e3
586
710
  },
587
711
  "gpt-5.3-codex": {
588
712
  input: 1.75,
589
713
  output: 14,
714
+ cachedInput: 0.175,
590
715
  maxInputTokens: 272e3
591
716
  },
592
717
  "gpt-5.2-pro": {
@@ -602,71 +727,85 @@ var prices_default = {
602
727
  "gpt-5.4": {
603
728
  input: 2.5,
604
729
  output: 15,
730
+ cachedInput: 0.25,
605
731
  maxInputTokens: 105e4
606
732
  },
607
733
  "gpt-5.4-2026-03-05": {
608
734
  input: 2.5,
609
735
  output: 15,
736
+ cachedInput: 0.25,
610
737
  maxInputTokens: 105e4
611
738
  },
612
739
  "gpt-5.4-pro": {
613
740
  input: 30,
614
741
  output: 180,
742
+ cachedInput: 3,
615
743
  maxInputTokens: 105e4
616
744
  },
617
745
  "gpt-5.4-pro-2026-03-05": {
618
746
  input: 30,
619
747
  output: 180,
748
+ cachedInput: 3,
620
749
  maxInputTokens: 105e4
621
750
  },
622
751
  "gpt-5.4-mini": {
623
752
  input: 0.75,
624
753
  output: 4.5,
754
+ cachedInput: 0.075,
625
755
  maxInputTokens: 272e3
626
756
  },
627
757
  "gpt-5.4-nano": {
628
758
  input: 0.2,
629
759
  output: 1.25,
760
+ cachedInput: 0.02,
630
761
  maxInputTokens: 272e3
631
762
  },
632
763
  "o1-2024-12-17": {
633
764
  input: 15,
634
765
  output: 60,
766
+ cachedInput: 7.5,
635
767
  maxInputTokens: 2e5
636
768
  },
637
769
  "o1-mini": {
638
770
  input: 1.21,
639
771
  output: 4.84,
772
+ cachedInput: 0.605,
640
773
  maxInputTokens: 128e3
641
774
  },
642
775
  "o1-mini-2024-09-12": {
643
776
  input: 1.1,
644
777
  output: 4.4,
778
+ cachedInput: 0.55,
645
779
  maxInputTokens: 128e3
646
780
  },
647
781
  "o1-preview": {
648
782
  input: 15,
649
783
  output: 60,
784
+ cachedInput: 7.5,
650
785
  maxInputTokens: 128e3
651
786
  },
652
787
  "o1-preview-2024-09-12": {
653
788
  input: 15,
654
789
  output: 60,
790
+ cachedInput: 7.5,
655
791
  maxInputTokens: 128e3
656
792
  },
657
793
  "o3-2025-04-16": {
658
794
  input: 2,
659
795
  output: 8,
796
+ cachedInput: 0.5,
660
797
  maxInputTokens: 2e5
661
798
  },
662
799
  "o3-mini": {
663
800
  input: 1.1,
664
801
  output: 4.4,
802
+ cachedInput: 0.55,
665
803
  maxInputTokens: 2e5
666
804
  },
667
805
  "o3-mini-2025-01-31": {
668
806
  input: 1.1,
669
807
  output: 4.4,
808
+ cachedInput: 0.55,
670
809
  maxInputTokens: 2e5
671
810
  },
672
811
  "o3-pro": {
@@ -682,11 +821,13 @@ var prices_default = {
682
821
  "o4-mini": {
683
822
  input: 1.1,
684
823
  output: 4.4,
824
+ cachedInput: 0.275,
685
825
  maxInputTokens: 2e5
686
826
  },
687
827
  "o4-mini-2025-04-16": {
688
828
  input: 1.1,
689
829
  output: 4.4,
830
+ cachedInput: 0.275,
690
831
  maxInputTokens: 2e5
691
832
  },
692
833
  "deepseek-v3.2": {
@@ -707,6 +848,7 @@ var prices_default = {
707
848
  "deepseek-v3": {
708
849
  input: 0.27,
709
850
  output: 1.1,
851
+ cachedInput: 0.07,
710
852
  maxInputTokens: 65536
711
853
  },
712
854
  "deepseek-v3-0324": {
@@ -722,76 +864,105 @@ var prices_default = {
722
864
  "claude-haiku-4-5-20251001": {
723
865
  input: 1,
724
866
  output: 5,
867
+ cachedInput: 0.1,
868
+ cacheCreationInput: 1.25,
725
869
  maxInputTokens: 2e5
726
870
  },
727
871
  "claude-3-7-sonnet-20250219": {
728
872
  input: 3,
729
873
  output: 15,
874
+ cachedInput: 0.3,
875
+ cacheCreationInput: 3.75,
730
876
  maxInputTokens: 2e5
731
877
  },
732
878
  "claude-3-haiku-20240307": {
733
879
  input: 0.25,
734
880
  output: 1.25,
881
+ cachedInput: 0.03,
882
+ cacheCreationInput: 0.3,
735
883
  maxInputTokens: 2e5
736
884
  },
737
885
  "claude-3-opus-20240229": {
738
886
  input: 15,
739
887
  output: 75,
888
+ cachedInput: 1.5,
889
+ cacheCreationInput: 18.75,
740
890
  maxInputTokens: 2e5
741
891
  },
742
892
  "claude-4-opus-20250514": {
743
893
  input: 15,
744
894
  output: 75,
895
+ cachedInput: 1.5,
896
+ cacheCreationInput: 18.75,
745
897
  maxInputTokens: 2e5
746
898
  },
747
899
  "claude-4-sonnet-20250514": {
748
900
  input: 3,
749
901
  output: 15,
902
+ cachedInput: 0.3,
903
+ cacheCreationInput: 3.75,
750
904
  maxInputTokens: 1e6
751
905
  },
752
906
  "claude-sonnet-4-5-20250929": {
753
907
  input: 3,
754
908
  output: 15,
909
+ cachedInput: 0.3,
910
+ cacheCreationInput: 3.75,
755
911
  maxInputTokens: 2e5
756
912
  },
757
913
  "claude-sonnet-4-5-20250929-v1:0": {
758
914
  input: 3,
759
915
  output: 15,
916
+ cachedInput: 0.3,
917
+ cacheCreationInput: 3.75,
760
918
  maxInputTokens: 2e5
761
919
  },
762
920
  "claude-opus-4-1-20250805": {
763
921
  input: 15,
764
922
  output: 75,
923
+ cachedInput: 1.5,
924
+ cacheCreationInput: 18.75,
765
925
  maxInputTokens: 2e5
766
926
  },
767
927
  "claude-opus-4-20250514": {
768
928
  input: 15,
769
929
  output: 75,
930
+ cachedInput: 1.5,
931
+ cacheCreationInput: 18.75,
770
932
  maxInputTokens: 2e5
771
933
  },
772
934
  "claude-opus-4-5-20251101": {
773
935
  input: 5,
774
936
  output: 25,
937
+ cachedInput: 0.5,
938
+ cacheCreationInput: 6.25,
775
939
  maxInputTokens: 2e5
776
940
  },
777
941
  "claude-opus-4-6-20260205": {
778
942
  input: 5,
779
943
  output: 25,
944
+ cachedInput: 0.5,
945
+ cacheCreationInput: 6.25,
780
946
  maxInputTokens: 1e6
781
947
  },
782
948
  "claude-opus-4-7-20260416": {
783
949
  input: 5,
784
950
  output: 25,
951
+ cachedInput: 0.5,
952
+ cacheCreationInput: 6.25,
785
953
  maxInputTokens: 1e6
786
954
  },
787
955
  "claude-sonnet-4-20250514": {
788
956
  input: 3,
789
957
  output: 15,
958
+ cachedInput: 0.3,
959
+ cacheCreationInput: 3.75,
790
960
  maxInputTokens: 1e6
791
961
  },
792
962
  "codex-mini-latest": {
793
963
  input: 1.5,
794
964
  output: 6,
965
+ cachedInput: 0.375,
795
966
  maxInputTokens: 2e5
796
967
  },
797
968
  "deepseek-ai/deepseek-r1": {
@@ -841,6 +1012,7 @@ var prices_default = {
841
1012
  "deepseek-ai/deepseek-v3.1-terminus": {
842
1013
  input: 0.27,
843
1014
  output: 1,
1015
+ cachedInput: 0.216,
844
1016
  maxInputTokens: 163840
845
1017
  },
846
1018
  "deepseek-coder": {
@@ -851,26 +1023,31 @@ var prices_default = {
851
1023
  "gemini-2.0-flash": {
852
1024
  input: 0.1,
853
1025
  output: 0.4,
1026
+ cachedInput: 0.025,
854
1027
  maxInputTokens: 1048576
855
1028
  },
856
1029
  "gemini-2.0-flash-001": {
857
1030
  input: 0.1,
858
1031
  output: 0.4,
1032
+ cachedInput: 0.025,
859
1033
  maxInputTokens: 1048576
860
1034
  },
861
1035
  "gemini-2.0-flash-lite": {
862
1036
  input: 0.075,
863
1037
  output: 0.3,
1038
+ cachedInput: 0.01875,
864
1039
  maxInputTokens: 1048576
865
1040
  },
866
1041
  "gemini-2.0-flash-lite-001": {
867
1042
  input: 0.075,
868
1043
  output: 0.3,
1044
+ cachedInput: 0.01875,
869
1045
  maxInputTokens: 1048576
870
1046
  },
871
1047
  "gemini-2.5-flash-image": {
872
1048
  input: 0.3,
873
1049
  output: 2.5,
1050
+ cachedInput: 0.03,
874
1051
  maxInputTokens: 32768
875
1052
  },
876
1053
  "gemini-3-pro-image-preview": {
@@ -886,51 +1063,61 @@ var prices_default = {
886
1063
  "gemini-3.1-flash-lite-preview": {
887
1064
  input: 0.25,
888
1065
  output: 1.5,
1066
+ cachedInput: 0.025,
889
1067
  maxInputTokens: 1048576
890
1068
  },
891
1069
  "gemini-2.5-flash-lite": {
892
1070
  input: 0.1,
893
1071
  output: 0.4,
1072
+ cachedInput: 0.01,
894
1073
  maxInputTokens: 1048576
895
1074
  },
896
1075
  "gemini-2.5-flash-lite-preview-09-2025": {
897
1076
  input: 0.1,
898
1077
  output: 0.4,
1078
+ cachedInput: 0.01,
899
1079
  maxInputTokens: 1048576
900
1080
  },
901
1081
  "gemini-2.5-flash-preview-09-2025": {
902
1082
  input: 0.3,
903
1083
  output: 2.5,
1084
+ cachedInput: 0.075,
904
1085
  maxInputTokens: 1048576
905
1086
  },
906
1087
  "gemini-live-2.5-flash-preview-native-audio-09-2025": {
907
1088
  input: 0.3,
908
1089
  output: 2,
1090
+ cachedInput: 0.075,
909
1091
  maxInputTokens: 1048576
910
1092
  },
911
1093
  "gemini-2.5-flash-lite-preview-06-17": {
912
1094
  input: 0.1,
913
1095
  output: 0.4,
1096
+ cachedInput: 0.025,
914
1097
  maxInputTokens: 1048576
915
1098
  },
916
1099
  "gemini-3-pro-preview": {
917
1100
  input: 2,
918
1101
  output: 12,
1102
+ cachedInput: 0.2,
919
1103
  maxInputTokens: 1048576
920
1104
  },
921
1105
  "gemini-3.1-pro-preview": {
922
1106
  input: 2,
923
1107
  output: 12,
1108
+ cachedInput: 0.2,
924
1109
  maxInputTokens: 1048576
925
1110
  },
926
1111
  "gemini-3.1-pro-preview-customtools": {
927
1112
  input: 2,
928
1113
  output: 12,
1114
+ cachedInput: 0.2,
929
1115
  maxInputTokens: 1048576
930
1116
  },
931
1117
  "gemini-3-flash-preview": {
932
1118
  input: 0.5,
933
1119
  output: 3,
1120
+ cachedInput: 0.05,
934
1121
  maxInputTokens: 1048576
935
1122
  },
936
1123
  "gemini-robotics-er-1.5-preview": {
@@ -946,11 +1133,13 @@ var prices_default = {
946
1133
  "gemini-flash-latest": {
947
1134
  input: 0.3,
948
1135
  output: 2.5,
1136
+ cachedInput: 0.03,
949
1137
  maxInputTokens: 1048576
950
1138
  },
951
1139
  "gemini-flash-lite-latest": {
952
1140
  input: 0.1,
953
1141
  output: 0.4,
1142
+ cachedInput: 0.01,
954
1143
  maxInputTokens: 1048576
955
1144
  },
956
1145
  "gemini-gemma-2-27b-it": {
@@ -1026,39 +1215,47 @@ var prices_default = {
1026
1215
  "gpt-4o-mini-realtime-preview": {
1027
1216
  input: 0.6,
1028
1217
  output: 2.4,
1218
+ cachedInput: 0.3,
1029
1219
  maxInputTokens: 128e3
1030
1220
  },
1031
1221
  "gpt-4o-realtime-preview": {
1032
1222
  input: 5,
1033
1223
  output: 20,
1224
+ cachedInput: 2.5,
1034
1225
  maxInputTokens: 128e3
1035
1226
  },
1036
1227
  "gpt-4o-realtime-preview-2025-06-03": {
1037
1228
  input: 5,
1038
1229
  output: 20,
1230
+ cachedInput: 2.5,
1039
1231
  maxInputTokens: 128e3
1040
1232
  },
1041
1233
  "gpt-image-1.5": {
1042
1234
  input: 5,
1043
- output: 10
1235
+ output: 10,
1236
+ cachedInput: 1.25
1044
1237
  },
1045
1238
  "gpt-image-1.5-2025-12-16": {
1046
1239
  input: 5,
1047
- output: 10
1240
+ output: 10,
1241
+ cachedInput: 1.25
1048
1242
  },
1049
1243
  "gpt-5.1-chat-latest": {
1050
1244
  input: 1.25,
1051
1245
  output: 10,
1246
+ cachedInput: 0.125,
1052
1247
  maxInputTokens: 128e3
1053
1248
  },
1054
1249
  "gpt-5.2-chat-latest": {
1055
1250
  input: 1.75,
1056
1251
  output: 14,
1252
+ cachedInput: 0.175,
1057
1253
  maxInputTokens: 128e3
1058
1254
  },
1059
1255
  "gpt-5.3-chat-latest": {
1060
1256
  input: 1.75,
1061
1257
  output: 14,
1258
+ cachedInput: 0.175,
1062
1259
  maxInputTokens: 128e3
1063
1260
  },
1064
1261
  "gpt-5-pro-2025-10-06": {
@@ -1069,11 +1266,13 @@ var prices_default = {
1069
1266
  "gpt-realtime": {
1070
1267
  input: 4,
1071
1268
  output: 16,
1269
+ cachedInput: 0.4,
1072
1270
  maxInputTokens: 32e3
1073
1271
  },
1074
1272
  "gpt-realtime-1.5": {
1075
1273
  input: 4,
1076
1274
  output: 16,
1275
+ cachedInput: 0.4,
1077
1276
  maxInputTokens: 32e3
1078
1277
  },
1079
1278
  "gpt-realtime-mini": {
@@ -1120,6 +1319,7 @@ var prices_default = {
1120
1319
  o1: {
1121
1320
  input: 15,
1122
1321
  output: 60,
1322
+ cachedInput: 7.5,
1123
1323
  maxInputTokens: 2e5
1124
1324
  },
1125
1325
  "o1-pro": {
@@ -1135,6 +1335,7 @@ var prices_default = {
1135
1335
  o3: {
1136
1336
  input: 2,
1137
1337
  output: 8,
1338
+ cachedInput: 0.5,
1138
1339
  maxInputTokens: 2e5
1139
1340
  },
1140
1341
  "gpt-oss-20b": {
@@ -1159,6 +1360,8 @@ var prices_default = {
1159
1360
  "claude-haiku-4-5@20251001": {
1160
1361
  input: 1,
1161
1362
  output: 5,
1363
+ cachedInput: 0.1,
1364
+ cacheCreationInput: 1.25,
1162
1365
  maxInputTokens: 2e5
1163
1366
  },
1164
1367
  "claude-3-5-sonnet": {
@@ -1174,6 +1377,8 @@ var prices_default = {
1174
1377
  "claude-3-7-sonnet@20250219": {
1175
1378
  input: 3,
1176
1379
  output: 15,
1380
+ cachedInput: 0.3,
1381
+ cacheCreationInput: 3.75,
1177
1382
  maxInputTokens: 2e5
1178
1383
  },
1179
1384
  "claude-3-haiku": {
@@ -1209,46 +1414,64 @@ var prices_default = {
1209
1414
  "claude-opus-4": {
1210
1415
  input: 15,
1211
1416
  output: 75,
1417
+ cachedInput: 1.5,
1418
+ cacheCreationInput: 18.75,
1212
1419
  maxInputTokens: 2e5
1213
1420
  },
1214
1421
  "claude-opus-4-1@20250805": {
1215
1422
  input: 15,
1216
1423
  output: 75,
1424
+ cachedInput: 1.5,
1425
+ cacheCreationInput: 18.75,
1217
1426
  maxInputTokens: 2e5
1218
1427
  },
1219
1428
  "claude-opus-4-5@20251101": {
1220
1429
  input: 5,
1221
1430
  output: 25,
1431
+ cachedInput: 0.5,
1432
+ cacheCreationInput: 6.25,
1222
1433
  maxInputTokens: 2e5
1223
1434
  },
1224
1435
  "claude-opus-4-6@default": {
1225
1436
  input: 5,
1226
1437
  output: 25,
1438
+ cachedInput: 0.5,
1439
+ cacheCreationInput: 6.25,
1227
1440
  maxInputTokens: 1e6
1228
1441
  },
1229
1442
  "claude-opus-4-7@default": {
1230
1443
  input: 5,
1231
1444
  output: 25,
1445
+ cachedInput: 0.5,
1446
+ cacheCreationInput: 6.25,
1232
1447
  maxInputTokens: 1e6
1233
1448
  },
1234
1449
  "claude-sonnet-4-5@20250929": {
1235
1450
  input: 3,
1236
1451
  output: 15,
1452
+ cachedInput: 0.3,
1453
+ cacheCreationInput: 3.75,
1237
1454
  maxInputTokens: 2e5
1238
1455
  },
1239
1456
  "claude-opus-4@20250514": {
1240
1457
  input: 15,
1241
1458
  output: 75,
1459
+ cachedInput: 1.5,
1460
+ cacheCreationInput: 18.75,
1242
1461
  maxInputTokens: 2e5
1243
1462
  },
1244
1463
  "claude-sonnet-4": {
1245
1464
  input: 3,
1246
1465
  output: 15,
1466
+ cachedInput: 0.3,
1467
+ cacheCreationInput: 3.75,
1247
1468
  maxInputTokens: 1e6
1248
1469
  },
1249
1470
  "claude-sonnet-4@20250514": {
1250
1471
  input: 3,
1251
1472
  output: 15,
1473
+ cachedInput: 0.3,
1474
+ cacheCreationInput: 3.75,
1252
1475
  maxInputTokens: 1e6
1253
1476
  },
1254
1477
  "deepseek-ai/deepseek-v3.1-maas": {
@@ -1298,6 +1521,7 @@ var prices_default = {
1298
1521
  "gpt-realtime-mini-2025-12-15": {
1299
1522
  input: 0.6,
1300
1523
  output: 2.4,
1524
+ cachedInput: 0.06,
1301
1525
  maxInputTokens: 128e3
1302
1526
  },
1303
1527
  "gemini-2.5-flash-native-audio-latest": {
@@ -1323,16 +1547,20 @@ var prices_default = {
1323
1547
  "gemini-pro-latest": {
1324
1548
  input: 1.25,
1325
1549
  output: 10,
1550
+ cachedInput: 0.125,
1326
1551
  maxInputTokens: 1048576
1327
1552
  },
1328
1553
  "gemini-exp-1206": {
1329
1554
  input: 0.3,
1330
1555
  output: 2.5,
1556
+ cachedInput: 0.03,
1331
1557
  maxInputTokens: 1048576
1332
1558
  },
1333
1559
  "claude-sonnet-4-6@default": {
1334
1560
  input: 3,
1335
1561
  output: 15,
1562
+ cachedInput: 0.3,
1563
+ cacheCreationInput: 3.75,
1336
1564
  maxInputTokens: 1e6
1337
1565
  }
1338
1566
  }
@@ -1340,11 +1568,19 @@ var prices_default = {
1340
1568
 
1341
1569
  // src/core/tracker.ts
1342
1570
  var bundledPrices = prices_default.models;
1571
+ var bundledUpdatedAt = prices_default.updated_at ?? "";
1343
1572
  var ModelPriceSchema = z.object({
1344
1573
  input: z.number().nonnegative(),
1345
1574
  output: z.number().nonnegative(),
1575
+ cachedInput: z.number().nonnegative().optional(),
1576
+ cacheCreationInput: z.number().nonnegative().optional(),
1346
1577
  maxInputTokens: z.number().positive().optional()
1347
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
+ });
1348
1584
  var TrackerConfigSchema = z.object({
1349
1585
  storage: z.union([z.enum(["memory", "sqlite"]), z.custom((v) => {
1350
1586
  return v !== null && typeof v === "object" && typeof v.record === "function" && typeof v.getAll === "function" && typeof v.clearAll === "function" && typeof v.clearSession === "function";
@@ -1352,7 +1588,13 @@ var TrackerConfigSchema = z.object({
1352
1588
  alertThreshold: z.number().positive().optional(),
1353
1589
  webhookUrl: z.string().url().optional(),
1354
1590
  syncPrices: z.boolean().optional().default(true),
1355
- 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)
1356
1598
  });
1357
1599
  function createTracker(config = {}) {
1358
1600
  const parsed = TrackerConfigSchema.safeParse(config);
@@ -1366,19 +1608,45 @@ ${issues}`);
1366
1608
  alertThreshold,
1367
1609
  webhookUrl,
1368
1610
  syncPrices,
1369
- customPrices
1611
+ customPrices,
1612
+ warnIfStaleAfterHours,
1613
+ budgets,
1614
+ suggestions
1370
1615
  } = parsed.data;
1371
1616
  const storage = typeof storageOption === "object" ? storageOption : createStorage(storageOption);
1372
1617
  let remotePrices;
1618
+ let pricesUpdatedAt = bundledUpdatedAt;
1373
1619
  if (syncPrices) {
1374
1620
  getRemotePrices().then((result) => {
1375
- if (result) remotePrices = result;
1621
+ if (result) {
1622
+ remotePrices = result.models;
1623
+ pricesUpdatedAt = result.updated_at;
1624
+ }
1376
1625
  }).catch(() => {
1377
1626
  });
1378
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
+ }
1379
1644
  let alertFired = false;
1645
+ const firedUserAlerts = /* @__PURE__ */ new Set();
1646
+ const firedSessionAlerts = /* @__PURE__ */ new Set();
1380
1647
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1381
1648
  function resolveModelPrice(model) {
1649
+ maybeWarnStaleness();
1382
1650
  return resolvePrice(model, {
1383
1651
  bundledPrices,
1384
1652
  ...customPrices !== void 0 && { customPrices },
@@ -1389,8 +1657,10 @@ ${issues}`);
1389
1657
  const price = resolveModelPrice(entry.model);
1390
1658
  const costUSD = calculateCost(
1391
1659
  entry.inputTokens,
1392
- entry.outputTokens + (entry.reasoningTokens ?? 0),
1393
- price
1660
+ entry.outputTokens,
1661
+ price,
1662
+ entry.cachedTokens,
1663
+ entry.cacheCreationTokens
1394
1664
  );
1395
1665
  const full = {
1396
1666
  ...entry,
@@ -1398,32 +1668,81 @@ ${issues}`);
1398
1668
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1399
1669
  };
1400
1670
  storage.record(full);
1401
- 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
+ }
1402
1679
  }
1403
- function maybeFireAlert() {
1404
- if (!alertThreshold || !webhookUrl || alertFired) return;
1405
- alertFired = true;
1406
- Promise.resolve(storage.getAll()).then((entries) => {
1407
- const total = computeTotal(entries);
1408
- if (total < alertThreshold) {
1409
- alertFired = false;
1410
- return;
1411
- }
1412
- const payload = {
1413
- text: `[tokenwatch] Alert: total cost reached $${total.toFixed(4)} USD (threshold: $${alertThreshold})`
1414
- };
1415
- fetch(webhookUrl, {
1416
- method: "POST",
1417
- headers: { "Content-Type": "application/json" },
1418
- 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
+ });
1419
1692
  }).catch(() => {
1693
+ alertFired = false;
1420
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)
1421
1740
  }).catch(() => {
1422
- alertFired = false;
1423
1741
  });
1424
1742
  }
1425
- async function getReport() {
1426
- 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);
1427
1746
  const byModel = {};
1428
1747
  const bySession = {};
1429
1748
  const byUser = {};
@@ -1431,18 +1750,24 @@ ${issues}`);
1431
1750
  let totalInput = 0;
1432
1751
  let totalOutput = 0;
1433
1752
  let totalCost = 0;
1434
- let lastTimestamp = startedAt;
1753
+ let periodFrom = options ? entries[0]?.timestamp ?? startedAt : startedAt;
1754
+ let lastTimestamp = periodFrom;
1435
1755
  for (const e of entries) {
1436
- totalInput += e.inputTokens;
1756
+ totalInput += e.inputTokens + (e.cachedTokens ?? 0) + (e.cacheCreationTokens ?? 0);
1437
1757
  totalOutput += e.outputTokens;
1438
1758
  totalCost += e.costUSD;
1439
1759
  if (e.timestamp > lastTimestamp) lastTimestamp = e.timestamp;
1440
- 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
+ };
1441
1765
  m.costUSD += e.costUSD;
1442
1766
  m.calls += 1;
1443
- m.tokens.input += e.inputTokens;
1767
+ m.tokens.input += e.inputTokens + (e.cachedTokens ?? 0) + (e.cacheCreationTokens ?? 0);
1444
1768
  m.tokens.output += e.outputTokens;
1445
1769
  m.tokens.reasoning += e.reasoningTokens ?? 0;
1770
+ m.tokens.cached += e.cachedTokens ?? 0;
1446
1771
  if (e.sessionId) {
1447
1772
  const s = bySession[e.sessionId] ??= { costUSD: 0, calls: 0 };
1448
1773
  s.costUSD += e.costUSD;
@@ -1459,6 +1784,9 @@ ${issues}`);
1459
1784
  f.calls += 1;
1460
1785
  }
1461
1786
  }
1787
+ if (options && entries.length > 0) {
1788
+ periodFrom = entries[0]?.timestamp ?? periodFrom;
1789
+ }
1462
1790
  return {
1463
1791
  totalCostUSD: totalCost,
1464
1792
  totalTokens: { input: totalInput, output: totalOutput },
@@ -1466,22 +1794,66 @@ ${issues}`);
1466
1794
  bySession,
1467
1795
  byUser,
1468
1796
  byFeature,
1469
- 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 }
1470
1839
  };
1471
1840
  }
1472
1841
  async function reset() {
1473
1842
  await Promise.resolve(storage.clearAll());
1474
1843
  alertFired = false;
1844
+ firedUserAlerts.clear();
1845
+ firedSessionAlerts.clear();
1475
1846
  }
1476
1847
  async function resetSession(sessionId) {
1477
1848
  await Promise.resolve(storage.clearSession(sessionId));
1849
+ firedSessionAlerts.delete(sessionId);
1478
1850
  }
1479
1851
  async function exportJSON() {
1480
1852
  return JSON.stringify(await getReport(), null, 2);
1481
1853
  }
1482
1854
  async function exportCSV() {
1483
1855
  const entries = await Promise.resolve(storage.getAll());
1484
- 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";
1485
1857
  const rows = entries.map(
1486
1858
  (e) => [
1487
1859
  csvEscape(e.timestamp),
@@ -1489,6 +1861,8 @@ ${issues}`);
1489
1861
  e.inputTokens,
1490
1862
  e.outputTokens,
1491
1863
  e.reasoningTokens ?? 0,
1864
+ e.cachedTokens ?? 0,
1865
+ e.cacheCreationTokens ?? 0,
1492
1866
  e.costUSD.toFixed(8),
1493
1867
  csvEscape(e.sessionId ?? ""),
1494
1868
  csvEscape(e.userId ?? ""),
@@ -1504,11 +1878,47 @@ ${issues}`);
1504
1878
  ...remotePrices !== void 0 && { remotePrices }
1505
1879
  }) ?? null;
1506
1880
  }
1507
- 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
+ };
1508
1891
  }
1509
1892
  function computeTotal(entries) {
1510
1893
  return entries.reduce((sum, e) => sum + e.costUSD, 0);
1511
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
+ }
1512
1922
  function csvEscape(value) {
1513
1923
  if (value.includes(",") || value.includes('"') || value.includes("\n")) {
1514
1924
  return `"${value.replace(/"/g, '""')}"`;
@@ -1529,7 +1939,7 @@ async function cmdSync() {
1529
1939
  console.log("Fetching latest prices from remote...");
1530
1940
  const result = await fetchRemotePrices();
1531
1941
  if (result) {
1532
- 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}).`);
1533
1943
  } else {
1534
1944
  console.error("\u2717 Failed to fetch remote prices. Check your internet connection.");
1535
1945
  process.exit(1);
@@ -1575,6 +1985,9 @@ async function cmdReport() {
1575
1985
  console.log(` Total cost: $${report.totalCostUSD.toFixed(6)} USD`);
1576
1986
  console.log(` Total tokens: ${report.totalTokens.input.toLocaleString()} in / ${report.totalTokens.output.toLocaleString()} out`);
1577
1987
  console.log(` Period: ${report.period.from} \u2192 ${report.period.to}`);
1988
+ if (report.pricesUpdatedAt) {
1989
+ console.log(` Prices as of: ${report.pricesUpdatedAt}`);
1990
+ }
1578
1991
  if (Object.keys(report.byModel).length > 0) {
1579
1992
  console.log("\n By model:");
1580
1993
  for (const [model, stats] of Object.entries(report.byModel)) {
@@ -1593,6 +2006,12 @@ async function cmdReport() {
1593
2006
  console.log(` ${session.padEnd(30)} $${stats.costUSD.toFixed(6)} (${stats.calls} calls)`);
1594
2007
  }
1595
2008
  }
2009
+ if (Object.keys(report.byFeature).length > 0) {
2010
+ console.log("\n By feature:");
2011
+ for (const [feature, stats] of Object.entries(report.byFeature)) {
2012
+ console.log(` ${feature.padEnd(30)} $${stats.costUSD.toFixed(6)} (${stats.calls} calls)`);
2013
+ }
2014
+ }
1596
2015
  console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
1597
2016
  }
1598
2017
  function cmdHelp() {