@diogonzafe/tokenwatch 0.1.17 → 0.2.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/index.cjs CHANGED
@@ -1410,7 +1410,11 @@ ${issues}`);
1410
1410
  }
1411
1411
  function track(entry) {
1412
1412
  const price = resolveModelPrice(entry.model);
1413
- const costUSD = calculateCost(entry.inputTokens, entry.outputTokens, price);
1413
+ const costUSD = calculateCost(
1414
+ entry.inputTokens,
1415
+ entry.outputTokens + (entry.reasoningTokens ?? 0),
1416
+ price
1417
+ );
1414
1418
  const full = {
1415
1419
  ...entry,
1416
1420
  costUSD,
@@ -1446,6 +1450,7 @@ ${issues}`);
1446
1450
  const byModel = {};
1447
1451
  const bySession = {};
1448
1452
  const byUser = {};
1453
+ const byFeature = {};
1449
1454
  let totalInput = 0;
1450
1455
  let totalOutput = 0;
1451
1456
  let totalCost = 0;
@@ -1455,11 +1460,12 @@ ${issues}`);
1455
1460
  totalOutput += e.outputTokens;
1456
1461
  totalCost += e.costUSD;
1457
1462
  if (e.timestamp > lastTimestamp) lastTimestamp = e.timestamp;
1458
- const m = byModel[e.model] ??= { costUSD: 0, calls: 0, tokens: { input: 0, output: 0 } };
1463
+ const m = byModel[e.model] ??= { costUSD: 0, calls: 0, tokens: { input: 0, output: 0, reasoning: 0 } };
1459
1464
  m.costUSD += e.costUSD;
1460
1465
  m.calls += 1;
1461
1466
  m.tokens.input += e.inputTokens;
1462
1467
  m.tokens.output += e.outputTokens;
1468
+ m.tokens.reasoning += e.reasoningTokens ?? 0;
1463
1469
  if (e.sessionId) {
1464
1470
  const s = bySession[e.sessionId] ??= { costUSD: 0, calls: 0 };
1465
1471
  s.costUSD += e.costUSD;
@@ -1470,6 +1476,11 @@ ${issues}`);
1470
1476
  u.costUSD += e.costUSD;
1471
1477
  u.calls += 1;
1472
1478
  }
1479
+ if (e.feature) {
1480
+ const f = byFeature[e.feature] ??= { costUSD: 0, calls: 0 };
1481
+ f.costUSD += e.costUSD;
1482
+ f.calls += 1;
1483
+ }
1473
1484
  }
1474
1485
  return {
1475
1486
  totalCostUSD: totalCost,
@@ -1477,6 +1488,7 @@ ${issues}`);
1477
1488
  byModel,
1478
1489
  bySession,
1479
1490
  byUser,
1491
+ byFeature,
1480
1492
  period: { from: startedAt, to: lastTimestamp }
1481
1493
  };
1482
1494
  }
@@ -1492,16 +1504,18 @@ ${issues}`);
1492
1504
  }
1493
1505
  async function exportCSV() {
1494
1506
  const entries = await Promise.resolve(storage.getAll());
1495
- const header = "timestamp,model,inputTokens,outputTokens,costUSD,sessionId,userId";
1507
+ const header = "timestamp,model,inputTokens,outputTokens,reasoningTokens,costUSD,sessionId,userId,feature";
1496
1508
  const rows = entries.map(
1497
1509
  (e) => [
1498
1510
  csvEscape(e.timestamp),
1499
1511
  csvEscape(e.model),
1500
1512
  e.inputTokens,
1501
1513
  e.outputTokens,
1514
+ e.reasoningTokens ?? 0,
1502
1515
  e.costUSD.toFixed(8),
1503
1516
  csvEscape(e.sessionId ?? ""),
1504
- csvEscape(e.userId ?? "")
1517
+ csvEscape(e.userId ?? ""),
1518
+ csvEscape(e.feature ?? "")
1505
1519
  ].join(",")
1506
1520
  );
1507
1521
  return [header, ...rows].join("\n");
@@ -1527,42 +1541,46 @@ function csvEscape(value) {
1527
1541
 
1528
1542
  // src/providers/openai.ts
1529
1543
  function extractMeta(params) {
1530
- const { __sessionId, __userId, ...cleaned } = params;
1544
+ const { __sessionId, __userId, __feature, ...cleaned } = params;
1531
1545
  return {
1532
1546
  cleaned,
1533
1547
  sessionId: typeof __sessionId === "string" ? __sessionId : void 0,
1534
- userId: typeof __userId === "string" ? __userId : void 0
1548
+ userId: typeof __userId === "string" ? __userId : void 0,
1549
+ feature: typeof __feature === "string" ? __feature : void 0
1535
1550
  };
1536
1551
  }
1537
1552
  function extractUsage(usage) {
1538
- if (!usage) return { inputTokens: 0, outputTokens: 0 };
1553
+ if (!usage) return { inputTokens: 0, outputTokens: 0, reasoningTokens: 0 };
1539
1554
  return {
1540
1555
  inputTokens: usage.prompt_tokens ?? usage.input_tokens ?? 0,
1541
- outputTokens: usage.completion_tokens ?? usage.output_tokens ?? 0
1556
+ outputTokens: usage.completion_tokens ?? usage.output_tokens ?? 0,
1557
+ reasoningTokens: usage.completion_tokens_details?.reasoning_tokens ?? 0
1542
1558
  };
1543
1559
  }
1544
- function trackWithMeta(tracker, model, inputTokens, outputTokens, sessionId, userId) {
1560
+ function trackWithMeta(tracker, model, inputTokens, outputTokens, reasoningTokens, sessionId, userId, feature) {
1545
1561
  tracker.track({
1546
1562
  model,
1547
1563
  inputTokens,
1548
1564
  outputTokens,
1565
+ ...reasoningTokens > 0 && { reasoningTokens },
1549
1566
  ...sessionId !== void 0 && { sessionId },
1550
- ...userId !== void 0 && { userId }
1567
+ ...userId !== void 0 && { userId },
1568
+ ...feature !== void 0 && { feature }
1551
1569
  });
1552
1570
  }
1553
- async function* wrapStream(stream, model, sessionId, userId, tracker) {
1571
+ async function* wrapStream(stream, model, sessionId, userId, feature, tracker) {
1554
1572
  let lastChunk;
1555
1573
  for await (const chunk of stream) {
1556
1574
  lastChunk = chunk;
1557
1575
  yield chunk;
1558
1576
  }
1559
- const { inputTokens, outputTokens } = extractUsage(lastChunk?.usage);
1577
+ const { inputTokens, outputTokens, reasoningTokens } = extractUsage(lastChunk?.usage);
1560
1578
  if (!lastChunk?.usage) {
1561
1579
  console.warn(
1562
1580
  `[tokenwatch] No usage data in stream for model "${model}". Cost recorded as $0. Pass stream_options: { include_usage: true } to get accurate costs.`
1563
1581
  );
1564
1582
  }
1565
- trackWithMeta(tracker, model, inputTokens, outputTokens, sessionId, userId);
1583
+ trackWithMeta(tracker, model, inputTokens, outputTokens, reasoningTokens, sessionId, userId, feature);
1566
1584
  }
1567
1585
  function wrapOpenAI(client, tracker) {
1568
1586
  const proxiedCompletions = new Proxy(client.chat.completions, {
@@ -1570,7 +1588,7 @@ function wrapOpenAI(client, tracker) {
1570
1588
  if (prop !== "create")
1571
1589
  return target[prop];
1572
1590
  return async function(params) {
1573
- const { cleaned, sessionId, userId } = extractMeta(params);
1591
+ const { cleaned, sessionId, userId, feature } = extractMeta(params);
1574
1592
  const model = typeof cleaned["model"] === "string" ? cleaned["model"] : "unknown";
1575
1593
  const result = await target.create(cleaned);
1576
1594
  if (result && typeof result === "object" && Symbol.asyncIterator in result) {
@@ -1579,18 +1597,21 @@ function wrapOpenAI(client, tracker) {
1579
1597
  model,
1580
1598
  sessionId,
1581
1599
  userId,
1600
+ feature,
1582
1601
  tracker
1583
1602
  );
1584
1603
  }
1585
1604
  const completion = result;
1586
- const { inputTokens, outputTokens } = extractUsage(completion.usage);
1605
+ const { inputTokens, outputTokens, reasoningTokens } = extractUsage(completion.usage);
1587
1606
  trackWithMeta(
1588
1607
  tracker,
1589
1608
  completion.model ?? model,
1590
1609
  inputTokens,
1591
1610
  outputTokens,
1611
+ reasoningTokens,
1592
1612
  sessionId,
1593
- userId
1613
+ userId,
1614
+ feature
1594
1615
  );
1595
1616
  return result;
1596
1617
  };
@@ -1602,9 +1623,25 @@ function wrapOpenAI(client, tracker) {
1602
1623
  return target[prop];
1603
1624
  }
1604
1625
  });
1626
+ const proxiedEmbeddings = client.embeddings ? new Proxy(client.embeddings, {
1627
+ get(target, prop) {
1628
+ if (prop !== "create")
1629
+ return target[prop];
1630
+ return async function(params) {
1631
+ const { cleaned, sessionId, userId, feature } = extractMeta(params);
1632
+ const model = typeof cleaned["model"] === "string" ? cleaned["model"] : "unknown";
1633
+ const result = await target.create(cleaned);
1634
+ const embedding = result;
1635
+ const inputTokens = embedding.usage?.total_tokens ?? 0;
1636
+ trackWithMeta(tracker, embedding.model ?? model, inputTokens, 0, 0, sessionId, userId, feature);
1637
+ return result;
1638
+ };
1639
+ }
1640
+ }) : void 0;
1605
1641
  return new Proxy(client, {
1606
1642
  get(target, prop) {
1607
1643
  if (prop === "chat") return proxiedChat;
1644
+ if (prop === "embeddings") return proxiedEmbeddings;
1608
1645
  return target[prop];
1609
1646
  }
1610
1647
  });
@@ -1612,11 +1649,12 @@ function wrapOpenAI(client, tracker) {
1612
1649
 
1613
1650
  // src/providers/anthropic.ts
1614
1651
  function extractMeta2(params) {
1615
- const { __sessionId, __userId, ...cleaned } = params;
1652
+ const { __sessionId, __userId, __feature, ...cleaned } = params;
1616
1653
  return {
1617
1654
  cleaned,
1618
1655
  sessionId: typeof __sessionId === "string" ? __sessionId : void 0,
1619
- userId: typeof __userId === "string" ? __userId : void 0
1656
+ userId: typeof __userId === "string" ? __userId : void 0,
1657
+ feature: typeof __feature === "string" ? __feature : void 0
1620
1658
  };
1621
1659
  }
1622
1660
  function extractUsage2(usage) {
@@ -1626,18 +1664,31 @@ function extractUsage2(usage) {
1626
1664
  outputTokens: usage.output_tokens ?? 0
1627
1665
  };
1628
1666
  }
1629
- function trackWithMeta2(tracker, model, inputTokens, outputTokens, sessionId, userId) {
1667
+ function extractThinkingTokenApprox(content) {
1668
+ if (!content) return 0;
1669
+ const chars = content.filter((b) => b.type === "thinking").reduce((sum, b) => sum + (b.thinking?.length ?? 0), 0);
1670
+ return chars > 0 ? Math.round(chars / 4) : 0;
1671
+ }
1672
+ function trackWithMeta2(tracker, model, inputTokens, outputTokens, reasoningTokens, sessionId, userId, feature) {
1630
1673
  tracker.track({
1631
1674
  model,
1632
1675
  inputTokens,
1633
1676
  outputTokens,
1677
+ // For Anthropic, reasoningTokens is informational (thinking already in outputTokens).
1678
+ // Pass 0 so tracker does not add it to cost (tracker only adds when > 0 AND separate).
1679
+ // We store it as a field but the tracker cost formula adds reasoningTokens to outputTokens,
1680
+ // so we must NOT pass it here to avoid double-counting.
1634
1681
  ...sessionId !== void 0 && { sessionId },
1635
- ...userId !== void 0 && { userId }
1682
+ ...userId !== void 0 && { userId },
1683
+ ...feature !== void 0 && { feature },
1684
+ ...reasoningTokens > 0 && { reasoningTokens }
1636
1685
  });
1637
1686
  }
1638
- async function* wrapStream2(stream, model, sessionId, userId, tracker) {
1687
+ async function* wrapStream2(stream, model, sessionId, userId, feature, tracker) {
1639
1688
  let inputTokens = 0;
1640
1689
  let outputTokens = 0;
1690
+ let currentBlockIsThinking = false;
1691
+ let thinkingCharCount = 0;
1641
1692
  for await (const event of stream) {
1642
1693
  yield event;
1643
1694
  if (event.type === "message_start" && event.message?.usage) {
@@ -1646,8 +1697,18 @@ async function* wrapStream2(stream, model, sessionId, userId, tracker) {
1646
1697
  if (event.type === "message_delta" && event.usage) {
1647
1698
  outputTokens = event.usage.output_tokens ?? 0;
1648
1699
  }
1700
+ if (event.type === "content_block_start") {
1701
+ currentBlockIsThinking = event.content_block?.type === "thinking";
1702
+ }
1703
+ if (event.type === "content_block_stop") {
1704
+ currentBlockIsThinking = false;
1705
+ }
1706
+ if (event.type === "content_block_delta" && currentBlockIsThinking && event.delta?.thinking) {
1707
+ thinkingCharCount += event.delta.thinking.length;
1708
+ }
1649
1709
  }
1650
- trackWithMeta2(tracker, model, inputTokens, outputTokens, sessionId, userId);
1710
+ const reasoningTokens = thinkingCharCount > 0 ? Math.round(thinkingCharCount / 4) : 0;
1711
+ trackWithMeta2(tracker, model, inputTokens, outputTokens, reasoningTokens, sessionId, userId, feature);
1651
1712
  }
1652
1713
  function wrapAnthropic(client, tracker) {
1653
1714
  const proxiedMessages = new Proxy(client.messages, {
@@ -1655,7 +1716,7 @@ function wrapAnthropic(client, tracker) {
1655
1716
  if (prop !== "create")
1656
1717
  return target[prop];
1657
1718
  return async function(params) {
1658
- const { cleaned, sessionId, userId } = extractMeta2(params);
1719
+ const { cleaned, sessionId, userId, feature } = extractMeta2(params);
1659
1720
  const model = typeof cleaned["model"] === "string" ? cleaned["model"] : "unknown";
1660
1721
  const result = await target.create(cleaned);
1661
1722
  if (result && typeof result === "object" && Symbol.asyncIterator in result) {
@@ -1664,18 +1725,22 @@ function wrapAnthropic(client, tracker) {
1664
1725
  model,
1665
1726
  sessionId,
1666
1727
  userId,
1728
+ feature,
1667
1729
  tracker
1668
1730
  );
1669
1731
  }
1670
1732
  const message = result;
1671
1733
  const { inputTokens, outputTokens } = extractUsage2(message.usage);
1734
+ const reasoningTokens = extractThinkingTokenApprox(message.content);
1672
1735
  trackWithMeta2(
1673
1736
  tracker,
1674
1737
  message.model ?? model,
1675
1738
  inputTokens,
1676
1739
  outputTokens,
1740
+ reasoningTokens,
1677
1741
  sessionId,
1678
- userId
1742
+ userId,
1743
+ feature
1679
1744
  );
1680
1745
  return result;
1681
1746
  };
@@ -1696,7 +1761,11 @@ function wrapGemini(client, tracker) {
1696
1761
  if (prop !== "getGenerativeModel")
1697
1762
  return target[prop];
1698
1763
  return function(modelParams) {
1699
- const modelInstance = target.getGenerativeModel(modelParams);
1764
+ const { __sessionId, __userId, __feature, ...cleanedParams } = modelParams;
1765
+ const feature = typeof __feature === "string" ? __feature : void 0;
1766
+ const sessionId = typeof __sessionId === "string" ? __sessionId : void 0;
1767
+ const userId = typeof __userId === "string" ? __userId : void 0;
1768
+ const modelInstance = target.getGenerativeModel(cleanedParams);
1700
1769
  const modelId = modelParams.model;
1701
1770
  return new Proxy(modelInstance, {
1702
1771
  get(mTarget, mProp) {
@@ -1707,7 +1776,10 @@ function wrapGemini(client, tracker) {
1707
1776
  tracker.track({
1708
1777
  model: modelId,
1709
1778
  inputTokens: meta?.promptTokenCount ?? 0,
1710
- outputTokens: meta?.candidatesTokenCount ?? 0
1779
+ outputTokens: meta?.candidatesTokenCount ?? 0,
1780
+ ...sessionId !== void 0 && { sessionId },
1781
+ ...userId !== void 0 && { userId },
1782
+ ...feature !== void 0 && { feature }
1711
1783
  });
1712
1784
  return result;
1713
1785
  };
@@ -1720,7 +1792,10 @@ function wrapGemini(client, tracker) {
1720
1792
  tracker.track({
1721
1793
  model: modelId,
1722
1794
  inputTokens: meta?.promptTokenCount ?? 0,
1723
- outputTokens: meta?.candidatesTokenCount ?? 0
1795
+ outputTokens: meta?.candidatesTokenCount ?? 0,
1796
+ ...sessionId !== void 0 && { sessionId },
1797
+ ...userId !== void 0 && { userId },
1798
+ ...feature !== void 0 && { feature }
1724
1799
  });
1725
1800
  }).catch(() => {
1726
1801
  });