@diogonzafe/tokenwatch 0.1.17 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -105,29 +105,40 @@ var SqliteStorage = class {
105
105
  migrate() {
106
106
  this.db.exec(`
107
107
  CREATE TABLE IF NOT EXISTS usage (
108
- id INTEGER PRIMARY KEY AUTOINCREMENT,
109
- model TEXT NOT NULL,
110
- input_tokens INTEGER NOT NULL,
111
- output_tokens INTEGER NOT NULL,
112
- cost_usd REAL NOT NULL,
113
- session_id TEXT,
114
- user_id TEXT,
115
- timestamp TEXT NOT NULL
108
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
109
+ model TEXT NOT NULL,
110
+ input_tokens INTEGER NOT NULL,
111
+ output_tokens INTEGER NOT NULL,
112
+ reasoning_tokens INTEGER NOT NULL DEFAULT 0,
113
+ cost_usd REAL NOT NULL,
114
+ session_id TEXT,
115
+ user_id TEXT,
116
+ feature TEXT,
117
+ timestamp TEXT NOT NULL
116
118
  )
117
119
  `);
120
+ const cols = this.db.prepare(`PRAGMA table_info(usage)`).all().map((c) => c.name);
121
+ if (!cols.includes("reasoning_tokens")) {
122
+ this.db.exec(`ALTER TABLE usage ADD COLUMN reasoning_tokens INTEGER NOT NULL DEFAULT 0`);
123
+ }
124
+ if (!cols.includes("feature")) {
125
+ this.db.exec(`ALTER TABLE usage ADD COLUMN feature TEXT`);
126
+ }
118
127
  }
119
128
  record(entry) {
120
129
  this.db.prepare(
121
130
  `INSERT INTO usage
122
- (model, input_tokens, output_tokens, cost_usd, session_id, user_id, timestamp)
123
- VALUES (?, ?, ?, ?, ?, ?, ?)`
131
+ (model, input_tokens, output_tokens, reasoning_tokens, cost_usd, session_id, user_id, feature, timestamp)
132
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
124
133
  ).run(
125
134
  entry.model,
126
135
  entry.inputTokens,
127
136
  entry.outputTokens,
137
+ entry.reasoningTokens ?? 0,
128
138
  entry.costUSD,
129
139
  entry.sessionId ?? null,
130
140
  entry.userId ?? null,
141
+ entry.feature ?? null,
131
142
  entry.timestamp
132
143
  );
133
144
  }
@@ -137,9 +148,11 @@ var SqliteStorage = class {
137
148
  model: r.model,
138
149
  inputTokens: r.input_tokens,
139
150
  outputTokens: r.output_tokens,
151
+ ...r.reasoning_tokens > 0 && { reasoningTokens: r.reasoning_tokens },
140
152
  costUSD: r.cost_usd,
141
153
  ...r.session_id != null && { sessionId: r.session_id },
142
154
  ...r.user_id != null && { userId: r.user_id },
155
+ ...r.feature != null && { feature: r.feature },
143
156
  timestamp: r.timestamp
144
157
  }));
145
158
  }
@@ -1446,6 +1459,7 @@ ${issues}`);
1446
1459
  const byModel = {};
1447
1460
  const bySession = {};
1448
1461
  const byUser = {};
1462
+ const byFeature = {};
1449
1463
  let totalInput = 0;
1450
1464
  let totalOutput = 0;
1451
1465
  let totalCost = 0;
@@ -1455,11 +1469,12 @@ ${issues}`);
1455
1469
  totalOutput += e.outputTokens;
1456
1470
  totalCost += e.costUSD;
1457
1471
  if (e.timestamp > lastTimestamp) lastTimestamp = e.timestamp;
1458
- const m = byModel[e.model] ??= { costUSD: 0, calls: 0, tokens: { input: 0, output: 0 } };
1472
+ const m = byModel[e.model] ??= { costUSD: 0, calls: 0, tokens: { input: 0, output: 0, reasoning: 0 } };
1459
1473
  m.costUSD += e.costUSD;
1460
1474
  m.calls += 1;
1461
1475
  m.tokens.input += e.inputTokens;
1462
1476
  m.tokens.output += e.outputTokens;
1477
+ m.tokens.reasoning += e.reasoningTokens ?? 0;
1463
1478
  if (e.sessionId) {
1464
1479
  const s = bySession[e.sessionId] ??= { costUSD: 0, calls: 0 };
1465
1480
  s.costUSD += e.costUSD;
@@ -1470,6 +1485,11 @@ ${issues}`);
1470
1485
  u.costUSD += e.costUSD;
1471
1486
  u.calls += 1;
1472
1487
  }
1488
+ if (e.feature) {
1489
+ const f = byFeature[e.feature] ??= { costUSD: 0, calls: 0 };
1490
+ f.costUSD += e.costUSD;
1491
+ f.calls += 1;
1492
+ }
1473
1493
  }
1474
1494
  return {
1475
1495
  totalCostUSD: totalCost,
@@ -1477,6 +1497,7 @@ ${issues}`);
1477
1497
  byModel,
1478
1498
  bySession,
1479
1499
  byUser,
1500
+ byFeature,
1480
1501
  period: { from: startedAt, to: lastTimestamp }
1481
1502
  };
1482
1503
  }
@@ -1492,16 +1513,18 @@ ${issues}`);
1492
1513
  }
1493
1514
  async function exportCSV() {
1494
1515
  const entries = await Promise.resolve(storage.getAll());
1495
- const header = "timestamp,model,inputTokens,outputTokens,costUSD,sessionId,userId";
1516
+ const header = "timestamp,model,inputTokens,outputTokens,reasoningTokens,costUSD,sessionId,userId,feature";
1496
1517
  const rows = entries.map(
1497
1518
  (e) => [
1498
1519
  csvEscape(e.timestamp),
1499
1520
  csvEscape(e.model),
1500
1521
  e.inputTokens,
1501
1522
  e.outputTokens,
1523
+ e.reasoningTokens ?? 0,
1502
1524
  e.costUSD.toFixed(8),
1503
1525
  csvEscape(e.sessionId ?? ""),
1504
- csvEscape(e.userId ?? "")
1526
+ csvEscape(e.userId ?? ""),
1527
+ csvEscape(e.feature ?? "")
1505
1528
  ].join(",")
1506
1529
  );
1507
1530
  return [header, ...rows].join("\n");
@@ -1527,42 +1550,46 @@ function csvEscape(value) {
1527
1550
 
1528
1551
  // src/providers/openai.ts
1529
1552
  function extractMeta(params) {
1530
- const { __sessionId, __userId, ...cleaned } = params;
1553
+ const { __sessionId, __userId, __feature, ...cleaned } = params;
1531
1554
  return {
1532
1555
  cleaned,
1533
1556
  sessionId: typeof __sessionId === "string" ? __sessionId : void 0,
1534
- userId: typeof __userId === "string" ? __userId : void 0
1557
+ userId: typeof __userId === "string" ? __userId : void 0,
1558
+ feature: typeof __feature === "string" ? __feature : void 0
1535
1559
  };
1536
1560
  }
1537
1561
  function extractUsage(usage) {
1538
- if (!usage) return { inputTokens: 0, outputTokens: 0 };
1562
+ if (!usage) return { inputTokens: 0, outputTokens: 0, reasoningTokens: 0 };
1539
1563
  return {
1540
1564
  inputTokens: usage.prompt_tokens ?? usage.input_tokens ?? 0,
1541
- outputTokens: usage.completion_tokens ?? usage.output_tokens ?? 0
1565
+ outputTokens: usage.completion_tokens ?? usage.output_tokens ?? 0,
1566
+ reasoningTokens: usage.completion_tokens_details?.reasoning_tokens ?? 0
1542
1567
  };
1543
1568
  }
1544
- function trackWithMeta(tracker, model, inputTokens, outputTokens, sessionId, userId) {
1569
+ function trackWithMeta(tracker, model, inputTokens, outputTokens, reasoningTokens, sessionId, userId, feature) {
1545
1570
  tracker.track({
1546
1571
  model,
1547
1572
  inputTokens,
1548
- outputTokens,
1573
+ outputTokens: outputTokens + reasoningTokens,
1574
+ ...reasoningTokens > 0 && { reasoningTokens },
1549
1575
  ...sessionId !== void 0 && { sessionId },
1550
- ...userId !== void 0 && { userId }
1576
+ ...userId !== void 0 && { userId },
1577
+ ...feature !== void 0 && { feature }
1551
1578
  });
1552
1579
  }
1553
- async function* wrapStream(stream, model, sessionId, userId, tracker) {
1580
+ async function* wrapStream(stream, model, sessionId, userId, feature, tracker) {
1554
1581
  let lastChunk;
1555
1582
  for await (const chunk of stream) {
1556
1583
  lastChunk = chunk;
1557
1584
  yield chunk;
1558
1585
  }
1559
- const { inputTokens, outputTokens } = extractUsage(lastChunk?.usage);
1586
+ const { inputTokens, outputTokens, reasoningTokens } = extractUsage(lastChunk?.usage);
1560
1587
  if (!lastChunk?.usage) {
1561
1588
  console.warn(
1562
1589
  `[tokenwatch] No usage data in stream for model "${model}". Cost recorded as $0. Pass stream_options: { include_usage: true } to get accurate costs.`
1563
1590
  );
1564
1591
  }
1565
- trackWithMeta(tracker, model, inputTokens, outputTokens, sessionId, userId);
1592
+ trackWithMeta(tracker, model, inputTokens, outputTokens, reasoningTokens, sessionId, userId, feature);
1566
1593
  }
1567
1594
  function wrapOpenAI(client, tracker) {
1568
1595
  const proxiedCompletions = new Proxy(client.chat.completions, {
@@ -1570,7 +1597,7 @@ function wrapOpenAI(client, tracker) {
1570
1597
  if (prop !== "create")
1571
1598
  return target[prop];
1572
1599
  return async function(params) {
1573
- const { cleaned, sessionId, userId } = extractMeta(params);
1600
+ const { cleaned, sessionId, userId, feature } = extractMeta(params);
1574
1601
  const model = typeof cleaned["model"] === "string" ? cleaned["model"] : "unknown";
1575
1602
  const result = await target.create(cleaned);
1576
1603
  if (result && typeof result === "object" && Symbol.asyncIterator in result) {
@@ -1579,18 +1606,21 @@ function wrapOpenAI(client, tracker) {
1579
1606
  model,
1580
1607
  sessionId,
1581
1608
  userId,
1609
+ feature,
1582
1610
  tracker
1583
1611
  );
1584
1612
  }
1585
1613
  const completion = result;
1586
- const { inputTokens, outputTokens } = extractUsage(completion.usage);
1614
+ const { inputTokens, outputTokens, reasoningTokens } = extractUsage(completion.usage);
1587
1615
  trackWithMeta(
1588
1616
  tracker,
1589
1617
  completion.model ?? model,
1590
1618
  inputTokens,
1591
1619
  outputTokens,
1620
+ reasoningTokens,
1592
1621
  sessionId,
1593
- userId
1622
+ userId,
1623
+ feature
1594
1624
  );
1595
1625
  return result;
1596
1626
  };
@@ -1602,9 +1632,25 @@ function wrapOpenAI(client, tracker) {
1602
1632
  return target[prop];
1603
1633
  }
1604
1634
  });
1635
+ const proxiedEmbeddings = client.embeddings ? new Proxy(client.embeddings, {
1636
+ get(target, prop) {
1637
+ if (prop !== "create")
1638
+ return target[prop];
1639
+ return async function(params) {
1640
+ const { cleaned, sessionId, userId, feature } = extractMeta(params);
1641
+ const model = typeof cleaned["model"] === "string" ? cleaned["model"] : "unknown";
1642
+ const result = await target.create(cleaned);
1643
+ const embedding = result;
1644
+ const inputTokens = embedding.usage?.total_tokens ?? 0;
1645
+ trackWithMeta(tracker, embedding.model ?? model, inputTokens, 0, 0, sessionId, userId, feature);
1646
+ return result;
1647
+ };
1648
+ }
1649
+ }) : void 0;
1605
1650
  return new Proxy(client, {
1606
1651
  get(target, prop) {
1607
1652
  if (prop === "chat") return proxiedChat;
1653
+ if (prop === "embeddings") return proxiedEmbeddings;
1608
1654
  return target[prop];
1609
1655
  }
1610
1656
  });
@@ -1612,11 +1658,12 @@ function wrapOpenAI(client, tracker) {
1612
1658
 
1613
1659
  // src/providers/anthropic.ts
1614
1660
  function extractMeta2(params) {
1615
- const { __sessionId, __userId, ...cleaned } = params;
1661
+ const { __sessionId, __userId, __feature, ...cleaned } = params;
1616
1662
  return {
1617
1663
  cleaned,
1618
1664
  sessionId: typeof __sessionId === "string" ? __sessionId : void 0,
1619
- userId: typeof __userId === "string" ? __userId : void 0
1665
+ userId: typeof __userId === "string" ? __userId : void 0,
1666
+ feature: typeof __feature === "string" ? __feature : void 0
1620
1667
  };
1621
1668
  }
1622
1669
  function extractUsage2(usage) {
@@ -1626,18 +1673,27 @@ function extractUsage2(usage) {
1626
1673
  outputTokens: usage.output_tokens ?? 0
1627
1674
  };
1628
1675
  }
1629
- function trackWithMeta2(tracker, model, inputTokens, outputTokens, sessionId, userId) {
1676
+ function extractThinkingTokenApprox(content) {
1677
+ if (!content) return 0;
1678
+ const chars = content.filter((b) => b.type === "thinking").reduce((sum, b) => sum + (b.thinking?.length ?? 0), 0);
1679
+ return chars > 0 ? Math.round(chars / 4) : 0;
1680
+ }
1681
+ function trackWithMeta2(tracker, model, inputTokens, outputTokens, reasoningTokens, sessionId, userId, feature) {
1630
1682
  tracker.track({
1631
1683
  model,
1632
1684
  inputTokens,
1633
1685
  outputTokens,
1686
+ ...reasoningTokens > 0 && { reasoningTokens },
1634
1687
  ...sessionId !== void 0 && { sessionId },
1635
- ...userId !== void 0 && { userId }
1688
+ ...userId !== void 0 && { userId },
1689
+ ...feature !== void 0 && { feature }
1636
1690
  });
1637
1691
  }
1638
- async function* wrapStream2(stream, model, sessionId, userId, tracker) {
1692
+ async function* wrapStream2(stream, model, sessionId, userId, feature, tracker) {
1639
1693
  let inputTokens = 0;
1640
1694
  let outputTokens = 0;
1695
+ let currentBlockIsThinking = false;
1696
+ let thinkingCharCount = 0;
1641
1697
  for await (const event of stream) {
1642
1698
  yield event;
1643
1699
  if (event.type === "message_start" && event.message?.usage) {
@@ -1646,8 +1702,18 @@ async function* wrapStream2(stream, model, sessionId, userId, tracker) {
1646
1702
  if (event.type === "message_delta" && event.usage) {
1647
1703
  outputTokens = event.usage.output_tokens ?? 0;
1648
1704
  }
1705
+ if (event.type === "content_block_start") {
1706
+ currentBlockIsThinking = event.content_block?.type === "thinking";
1707
+ }
1708
+ if (event.type === "content_block_stop") {
1709
+ currentBlockIsThinking = false;
1710
+ }
1711
+ if (event.type === "content_block_delta" && currentBlockIsThinking && event.delta?.thinking) {
1712
+ thinkingCharCount += event.delta.thinking.length;
1713
+ }
1649
1714
  }
1650
- trackWithMeta2(tracker, model, inputTokens, outputTokens, sessionId, userId);
1715
+ const reasoningTokens = thinkingCharCount > 0 ? Math.round(thinkingCharCount / 4) : 0;
1716
+ trackWithMeta2(tracker, model, inputTokens, outputTokens, reasoningTokens, sessionId, userId, feature);
1651
1717
  }
1652
1718
  function wrapAnthropic(client, tracker) {
1653
1719
  const proxiedMessages = new Proxy(client.messages, {
@@ -1655,7 +1721,7 @@ function wrapAnthropic(client, tracker) {
1655
1721
  if (prop !== "create")
1656
1722
  return target[prop];
1657
1723
  return async function(params) {
1658
- const { cleaned, sessionId, userId } = extractMeta2(params);
1724
+ const { cleaned, sessionId, userId, feature } = extractMeta2(params);
1659
1725
  const model = typeof cleaned["model"] === "string" ? cleaned["model"] : "unknown";
1660
1726
  const result = await target.create(cleaned);
1661
1727
  if (result && typeof result === "object" && Symbol.asyncIterator in result) {
@@ -1664,18 +1730,22 @@ function wrapAnthropic(client, tracker) {
1664
1730
  model,
1665
1731
  sessionId,
1666
1732
  userId,
1733
+ feature,
1667
1734
  tracker
1668
1735
  );
1669
1736
  }
1670
1737
  const message = result;
1671
1738
  const { inputTokens, outputTokens } = extractUsage2(message.usage);
1739
+ const reasoningTokens = extractThinkingTokenApprox(message.content);
1672
1740
  trackWithMeta2(
1673
1741
  tracker,
1674
1742
  message.model ?? model,
1675
1743
  inputTokens,
1676
1744
  outputTokens,
1745
+ reasoningTokens,
1677
1746
  sessionId,
1678
- userId
1747
+ userId,
1748
+ feature
1679
1749
  );
1680
1750
  return result;
1681
1751
  };
@@ -1696,7 +1766,11 @@ function wrapGemini(client, tracker) {
1696
1766
  if (prop !== "getGenerativeModel")
1697
1767
  return target[prop];
1698
1768
  return function(modelParams) {
1699
- const modelInstance = target.getGenerativeModel(modelParams);
1769
+ const { __sessionId, __userId, __feature, ...cleanedParams } = modelParams;
1770
+ const feature = typeof __feature === "string" ? __feature : void 0;
1771
+ const sessionId = typeof __sessionId === "string" ? __sessionId : void 0;
1772
+ const userId = typeof __userId === "string" ? __userId : void 0;
1773
+ const modelInstance = target.getGenerativeModel(cleanedParams);
1700
1774
  const modelId = modelParams.model;
1701
1775
  return new Proxy(modelInstance, {
1702
1776
  get(mTarget, mProp) {
@@ -1707,7 +1781,10 @@ function wrapGemini(client, tracker) {
1707
1781
  tracker.track({
1708
1782
  model: modelId,
1709
1783
  inputTokens: meta?.promptTokenCount ?? 0,
1710
- outputTokens: meta?.candidatesTokenCount ?? 0
1784
+ outputTokens: meta?.candidatesTokenCount ?? 0,
1785
+ ...sessionId !== void 0 && { sessionId },
1786
+ ...userId !== void 0 && { userId },
1787
+ ...feature !== void 0 && { feature }
1711
1788
  });
1712
1789
  return result;
1713
1790
  };
@@ -1720,7 +1797,10 @@ function wrapGemini(client, tracker) {
1720
1797
  tracker.track({
1721
1798
  model: modelId,
1722
1799
  inputTokens: meta?.promptTokenCount ?? 0,
1723
- outputTokens: meta?.candidatesTokenCount ?? 0
1800
+ outputTokens: meta?.candidatesTokenCount ?? 0,
1801
+ ...sessionId !== void 0 && { sessionId },
1802
+ ...userId !== void 0 && { userId },
1803
+ ...feature !== void 0 && { feature }
1724
1804
  });
1725
1805
  }).catch(() => {
1726
1806
  });