@diogonzafe/tokenwatch 0.9.0 → 0.10.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
@@ -30,6 +30,7 @@ function rowToEntry(r) {
30
30
  ...r["user_id"] != null && { userId: r["user_id"] },
31
31
  ...r["feature"] != null && { feature: r["feature"] },
32
32
  ...r["app_id"] != null && { appId: r["app_id"] },
33
+ ...r["metadata"] != null && { metadata: r["metadata"] },
33
34
  timestamp: r["timestamp"] instanceof Date ? r["timestamp"].toISOString() : r["timestamp"]
34
35
  };
35
36
  }
@@ -59,6 +60,7 @@ var init_postgres = __esm({
59
60
  user_id TEXT,
60
61
  feature TEXT,
61
62
  app_id TEXT,
63
+ metadata JSONB,
62
64
  timestamp TIMESTAMPTZ NOT NULL
63
65
  )
64
66
  `);
@@ -67,7 +69,8 @@ var init_postgres = __esm({
67
69
  "ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS feature TEXT",
68
70
  "ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS cached_tokens INTEGER NOT NULL DEFAULT 0",
69
71
  "ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS cache_creation_tokens INTEGER NOT NULL DEFAULT 0",
70
- "ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS app_id TEXT"
72
+ "ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS app_id TEXT",
73
+ "ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS metadata JSONB"
71
74
  ]) {
72
75
  await this.client.query(col).catch(() => {
73
76
  });
@@ -77,8 +80,8 @@ var init_postgres = __esm({
77
80
  this.client.query(
78
81
  `INSERT INTO tokenwatch_usage
79
82
  (model, input_tokens, output_tokens, reasoning_tokens, cached_tokens, cache_creation_tokens,
80
- cost_usd, session_id, user_id, feature, app_id, timestamp)
81
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
83
+ cost_usd, session_id, user_id, feature, app_id, metadata, timestamp)
84
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
82
85
  [
83
86
  entry.model,
84
87
  entry.inputTokens,
@@ -91,6 +94,7 @@ var init_postgres = __esm({
91
94
  entry.userId ?? null,
92
95
  entry.feature ?? null,
93
96
  entry.appId ?? null,
97
+ entry.metadata ?? null,
94
98
  entry.timestamp
95
99
  ]
96
100
  ).catch((err) => {
@@ -137,6 +141,7 @@ function rowToEntry2(r) {
137
141
  ...r["user_id"] != null && { userId: r["user_id"] },
138
142
  ...r["feature"] != null && { feature: r["feature"] },
139
143
  ...r["app_id"] != null && { appId: r["app_id"] },
144
+ ...r["metadata"] != null && { metadata: typeof r["metadata"] === "string" ? JSON.parse(r["metadata"]) : r["metadata"] },
140
145
  timestamp: r["timestamp"] instanceof Date ? r["timestamp"].toISOString() : r["timestamp"]
141
146
  };
142
147
  }
@@ -166,6 +171,7 @@ var init_mysql = __esm({
166
171
  user_id VARCHAR(255),
167
172
  feature VARCHAR(255),
168
173
  app_id VARCHAR(255),
174
+ metadata JSON,
169
175
  timestamp DATETIME(3) NOT NULL
170
176
  )
171
177
  `);
@@ -175,7 +181,8 @@ var init_mysql = __esm({
175
181
  ADD COLUMN IF NOT EXISTS feature VARCHAR(255),
176
182
  ADD COLUMN IF NOT EXISTS cached_tokens INT NOT NULL DEFAULT 0,
177
183
  ADD COLUMN IF NOT EXISTS cache_creation_tokens INT NOT NULL DEFAULT 0,
178
- ADD COLUMN IF NOT EXISTS app_id VARCHAR(255)
184
+ ADD COLUMN IF NOT EXISTS app_id VARCHAR(255),
185
+ ADD COLUMN IF NOT EXISTS metadata JSON
179
186
  `).catch(() => {
180
187
  });
181
188
  }
@@ -183,8 +190,8 @@ var init_mysql = __esm({
183
190
  this.client.execute(
184
191
  `INSERT INTO tokenwatch_usage
185
192
  (model, input_tokens, output_tokens, reasoning_tokens, cached_tokens, cache_creation_tokens,
186
- cost_usd, session_id, user_id, feature, app_id, timestamp)
187
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
193
+ cost_usd, session_id, user_id, feature, app_id, metadata, timestamp)
194
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
188
195
  [
189
196
  entry.model,
190
197
  entry.inputTokens,
@@ -197,6 +204,7 @@ var init_mysql = __esm({
197
204
  entry.userId ?? null,
198
205
  entry.feature ?? null,
199
206
  entry.appId ?? null,
207
+ entry.metadata != null ? JSON.stringify(entry.metadata) : null,
200
208
  entry.timestamp
201
209
  ]
202
210
  ).catch((err) => {
@@ -240,6 +248,7 @@ function docToEntry(doc) {
240
248
  ...doc.userId != null && { userId: doc.userId },
241
249
  ...doc.feature != null && { feature: doc.feature },
242
250
  ...doc.appId != null && { appId: doc.appId },
251
+ ...doc.metadata != null && { metadata: doc.metadata },
243
252
  timestamp: doc.timestamp
244
253
  };
245
254
  }
@@ -274,6 +283,7 @@ var init_mongodb = __esm({
274
283
  userId: entry.userId ?? null,
275
284
  ...entry.feature !== void 0 && { feature: entry.feature },
276
285
  ...entry.appId !== void 0 && { appId: entry.appId },
286
+ ...entry.metadata !== void 0 && { metadata: entry.metadata },
277
287
  timestamp: entry.timestamp
278
288
  }).catch((err) => {
279
289
  console.warn("[tokenwatch] MongoStorage.record failed:", err);
@@ -404,6 +414,7 @@ var SqliteStorage = class {
404
414
  user_id TEXT,
405
415
  feature TEXT,
406
416
  app_id TEXT,
417
+ metadata TEXT,
407
418
  timestamp TEXT NOT NULL
408
419
  )
409
420
  `);
@@ -423,13 +434,16 @@ var SqliteStorage = class {
423
434
  if (!cols.includes("app_id")) {
424
435
  this.db.exec(`ALTER TABLE usage ADD COLUMN app_id TEXT`);
425
436
  }
437
+ if (!cols.includes("metadata")) {
438
+ this.db.exec(`ALTER TABLE usage ADD COLUMN metadata TEXT`);
439
+ }
426
440
  }
427
441
  record(entry) {
428
442
  this.db.prepare(
429
443
  `INSERT INTO usage
430
444
  (model, input_tokens, output_tokens, reasoning_tokens, cached_tokens, cache_creation_tokens,
431
- cost_usd, session_id, user_id, feature, app_id, timestamp)
432
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
445
+ cost_usd, session_id, user_id, feature, app_id, metadata, timestamp)
446
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
433
447
  ).run(
434
448
  entry.model,
435
449
  entry.inputTokens,
@@ -442,6 +456,7 @@ var SqliteStorage = class {
442
456
  entry.userId ?? null,
443
457
  entry.feature ?? null,
444
458
  entry.appId ?? null,
459
+ entry.metadata != null ? JSON.stringify(entry.metadata) : null,
445
460
  entry.timestamp
446
461
  );
447
462
  }
@@ -459,6 +474,7 @@ var SqliteStorage = class {
459
474
  ...r.user_id != null && { userId: r.user_id },
460
475
  ...r.feature != null && { feature: r.feature },
461
476
  ...r.app_id != null && { appId: r.app_id },
477
+ ...r.metadata != null && { metadata: JSON.parse(r.metadata) },
462
478
  timestamp: r.timestamp
463
479
  }));
464
480
  }
@@ -536,6 +552,7 @@ var CloudExporter = class {
536
552
  sessionId: entry.sessionId,
537
553
  userId: entry.userId,
538
554
  feature: entry.feature,
555
+ metadata: entry.metadata,
539
556
  timestamp: entry.timestamp
540
557
  })
541
558
  }).catch(() => {
@@ -2200,6 +2217,7 @@ ${issues}`);
2200
2217
  const byUser = {};
2201
2218
  const byFeature = {};
2202
2219
  const byApp = {};
2220
+ const byMetadata = {};
2203
2221
  let totalInput = 0;
2204
2222
  let totalOutput = 0;
2205
2223
  let totalCost = 0;
@@ -2241,6 +2259,14 @@ ${issues}`);
2241
2259
  a.costUSD += e.costUSD;
2242
2260
  a.calls += 1;
2243
2261
  }
2262
+ if (e.metadata) {
2263
+ for (const [key, val] of Object.entries(e.metadata)) {
2264
+ const group = byMetadata[key] ??= {};
2265
+ const slot = group[val] ??= { costUSD: 0, calls: 0 };
2266
+ slot.costUSD += e.costUSD;
2267
+ slot.calls += 1;
2268
+ }
2269
+ }
2244
2270
  }
2245
2271
  if (options && entries.length > 0) {
2246
2272
  periodFrom = entries[0]?.timestamp ?? periodFrom;
@@ -2253,6 +2279,7 @@ ${issues}`);
2253
2279
  byUser,
2254
2280
  byFeature,
2255
2281
  byApp,
2282
+ byMetadata,
2256
2283
  period: { from: periodFrom, to: lastTimestamp },
2257
2284
  ...pricesUpdatedAt ? { pricesUpdatedAt } : {}
2258
2285
  };
@@ -2357,7 +2384,7 @@ ${issues}`);
2357
2384
  }
2358
2385
  async function exportCSV() {
2359
2386
  const entries = await Promise.resolve(storage.getAll());
2360
- const header = "timestamp,model,inputTokens,outputTokens,reasoningTokens,cachedTokens,cacheCreationTokens,costUSD,sessionId,userId,feature,appId";
2387
+ const header = "timestamp,model,inputTokens,outputTokens,reasoningTokens,cachedTokens,cacheCreationTokens,costUSD,sessionId,userId,feature,appId,metadata";
2361
2388
  const rows = entries.map(
2362
2389
  (e) => [
2363
2390
  csvEscape(e.timestamp),
@@ -2371,7 +2398,8 @@ ${issues}`);
2371
2398
  csvEscape(e.sessionId ?? ""),
2372
2399
  csvEscape(e.userId ?? ""),
2373
2400
  csvEscape(e.feature ?? ""),
2374
- csvEscape(e.appId ?? "")
2401
+ csvEscape(e.appId ?? ""),
2402
+ csvEscape(e.metadata ? JSON.stringify(e.metadata) : "")
2375
2403
  ].join(",")
2376
2404
  );
2377
2405
  return [header, ...rows].join("\n");
@@ -2490,6 +2518,7 @@ async function getDashboardData(storage, filter) {
2490
2518
  const byUser = {};
2491
2519
  const byFeature = {};
2492
2520
  const byApp = {};
2521
+ const byMetadata = {};
2493
2522
  let totalInput = 0;
2494
2523
  let totalOutput = 0;
2495
2524
  let totalCost = 0;
@@ -2528,6 +2557,14 @@ async function getDashboardData(storage, filter) {
2528
2557
  a.costUSD += e.costUSD;
2529
2558
  a.calls += 1;
2530
2559
  }
2560
+ if (e.metadata) {
2561
+ for (const [key, val] of Object.entries(e.metadata)) {
2562
+ const group = byMetadata[key] ??= {};
2563
+ const slot = group[val] ??= { costUSD: 0, calls: 0 };
2564
+ slot.costUSD += e.costUSD;
2565
+ slot.calls += 1;
2566
+ }
2567
+ }
2531
2568
  }
2532
2569
  const now = (/* @__PURE__ */ new Date()).toISOString();
2533
2570
  const periodFrom = entries[0]?.timestamp ?? now;
@@ -2540,6 +2577,7 @@ async function getDashboardData(storage, filter) {
2540
2577
  byUser,
2541
2578
  byFeature,
2542
2579
  byApp,
2580
+ byMetadata,
2543
2581
  period: { from: periodFrom, to: periodTo }
2544
2582
  };
2545
2583
  const forecastWindowMs = 24 * 60 * 60 * 1e3;
@@ -3515,7 +3553,65 @@ function ModelTable({ models, total, t, onRowClick, exampleHover }) {
3515
3553
  </section>
3516
3554
  );
3517
3555
  }
3518
- Object.assign(window, { ModelTable });
3556
+ function MetaGroup({ label, rows, t }) {
3557
+ const [open, setOpen] = useStateT(true);
3558
+ const { fmtUSD, fmtInt } = window.TW;
3559
+ const total = rows.reduce(function(s, r) { return s + r[1].costUSD; }, 0);
3560
+ return (
3561
+ <section className="tw-section">
3562
+ <div className="tw-sec-head" style={{ cursor: 'pointer' }} onClick={() => setOpen(function(v) { return !v; })}>
3563
+ <h3 style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
3564
+ <button className="tw-collapse" style={{ transform: open ? 'none' : 'rotate(-90deg)' }}><Ico.chevron /></button>
3565
+ {label}
3566
+ </h3>
3567
+ <span className="tw-sec-sub">{rows.length + ' values \xB7 ' + fmtUSD(total, 4)}</span>
3568
+ </div>
3569
+ {open && (
3570
+ <div className="tw-table-wrap">
3571
+ <table className={'tw-table' + (t.density === 'compact' ? ' compact' : '')}>
3572
+ <thead>
3573
+ <tr>
3574
+ <th style={{ textAlign: 'left' }}>Value</th>
3575
+ <th style={{ textAlign: 'right' }}>Cost</th>
3576
+ <th style={{ textAlign: 'right' }}>Calls</th>
3577
+ <th style={{ textAlign: 'right' }}>Avg / call</th>
3578
+ </tr>
3579
+ </thead>
3580
+ <tbody>
3581
+ {rows.map(function(r) {
3582
+ var val = r[0], stats = r[1];
3583
+ return (
3584
+ <tr key={val} className="tw-row">
3585
+ <td>{val}</td>
3586
+ <td style={{ textAlign: 'right' }}>{fmtUSD(stats.costUSD, 4)}</td>
3587
+ <td style={{ textAlign: 'right' }}>{fmtInt(stats.calls)}</td>
3588
+ <td style={{ textAlign: 'right' }}>{fmtUSD(stats.costUSD / Math.max(stats.calls, 1), 4)}</td>
3589
+ </tr>
3590
+ );
3591
+ })}
3592
+ </tbody>
3593
+ </table>
3594
+ </div>
3595
+ )}
3596
+ </section>
3597
+ );
3598
+ }
3599
+
3600
+ function MetadataSection({ byMetadata, t }) {
3601
+ var keys = Object.keys(byMetadata || {});
3602
+ if (keys.length === 0) return null;
3603
+ return (
3604
+ <div>
3605
+ {keys.map(function(key) {
3606
+ var group = byMetadata[key];
3607
+ var rows = Object.entries(group).sort(function(a, b) { return b[1].costUSD - a[1].costUSD; });
3608
+ return <MetaGroup key={key} label={key} rows={rows} t={t} />;
3609
+ })}
3610
+ </div>
3611
+ );
3612
+ }
3613
+
3614
+ Object.assign(window, { ModelTable, MetaGroup, MetadataSection });
3519
3615
  </script>
3520
3616
 
3521
3617
  <script type="text/babel">
@@ -3824,6 +3920,7 @@ Object.assign(window, {
3824
3920
  BASE_MODELS, RANGES, modelsForRange, kpisForRange,
3825
3921
  seriesForRange, seedFeed, makeCall, callsForModel,
3826
3922
  FEATURES, FEATURE_COLOR, BUDGET, _buildSeries: buildSeries,
3923
+ byMetadata: {},
3827
3924
  },
3828
3925
  });
3829
3926
 
@@ -3868,6 +3965,7 @@ Object.assign(window, {
3868
3965
  var elapsed = window.TW.BUDGET.cycleDays - window.TW.BUDGET.daysLeft;
3869
3966
  window.TW.BUDGET.used = fc.burnRatePerHour * 24 * Math.max(elapsed, 1);
3870
3967
  }
3968
+ if (r.byMetadata) window.TW.byMetadata = r.byMetadata;
3871
3969
  window.dispatchEvent(new CustomEvent('tw-data-update'));
3872
3970
  }
3873
3971
  var evtSource = null;
@@ -4010,6 +4108,10 @@ function App() {
4010
4108
 
4011
4109
  const models = kpis.models;
4012
4110
 
4111
+ const byMetadata = useMemoApp(() => {
4112
+ return window.TW.byMetadata || {};
4113
+ }, [_sseV]);
4114
+
4013
4115
  useEffectApp(() => {
4014
4116
  const onKey = (e) => {
4015
4117
  if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
@@ -4045,6 +4147,7 @@ function App() {
4045
4147
  <ChartsRow range={range} models={models} kpis={kpis} series={series} t={t} />
4046
4148
  <ModelTable models={models} total={kpis.cost} t={t} exampleHover={t.smartHighlight}
4047
4149
  onRowClick={(m) => setSelModel({ ...m, share: m.cost / Math.max(kpis.cost, 0.0001) })} />
4150
+ <MetadataSection byMetadata={byMetadata} t={t} />
4048
4151
  <ForecastSection t={t} />
4049
4152
  <LiveActivity t={t} />
4050
4153
  </main>