@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.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { T as TrackerConfig, a as Tracker, b as TrackingMeta } from './index-Cy_sl3FI.js';
2
- export { I as IStorage, M as ModelPrice, c as ModelStats, P as PriceMap, d as PricesFile, R as Report, S as SessionStats, U as UsageEntry, e as UserStats } from './index-Cy_sl3FI.js';
1
+ import { T as TrackerConfig, a as Tracker, b as TrackingMeta } from './index-BQZaFcHQ.js';
2
+ export { I as IStorage, M as ModelPrice, c as ModelStats, P as PriceMap, d as PricesFile, R as Report, S as SessionStats, U as UsageEntry, e as UserStats } from './index-BQZaFcHQ.js';
3
3
 
4
4
  declare function createTracker(config?: TrackerConfig): Tracker;
5
5
 
@@ -9,23 +9,31 @@ interface CompletionsLike {
9
9
  interface ChatLike {
10
10
  completions: CompletionsLike;
11
11
  }
12
+ interface EmbeddingsLike {
13
+ create(params: Record<string, unknown>): Promise<unknown>;
14
+ }
12
15
  type OpenAILike = {
13
16
  chat: ChatLike;
17
+ embeddings?: EmbeddingsLike;
14
18
  } & Record<string, unknown>;
15
19
  type AugmentedCreate$1<TCreate extends (...args: any[]) => any> = (params: Parameters<TCreate>[0] & TrackingMeta) => ReturnType<TCreate>;
16
- type WrappedOpenAI<T extends OpenAILike> = Omit<T, 'chat'> & {
20
+ type WrappedOpenAI<T extends OpenAILike> = Omit<T, 'chat' | 'embeddings'> & {
17
21
  chat: Omit<T['chat'], 'completions'> & {
18
22
  completions: Omit<T['chat']['completions'], 'create'> & {
19
23
  create: AugmentedCreate$1<T['chat']['completions']['create']>;
20
24
  };
21
25
  };
26
+ embeddings: T['embeddings'] extends EmbeddingsLike ? Omit<T['embeddings'], 'create'> & {
27
+ create: AugmentedCreate$1<T['embeddings']['create']>;
28
+ } : T['embeddings'];
22
29
  };
23
30
  /**
24
31
  * Wraps an OpenAI client (or any OpenAI-compatible client) to transparently
25
- * intercept chat.completions.create calls and report token usage to the tracker.
32
+ * intercept chat.completions.create and embeddings.create calls and report
33
+ * token usage to the tracker.
26
34
  *
27
- * The returned client is typed to accept __sessionId and __userId alongside the
28
- * normal params — no type cast required at the call site.
35
+ * The returned client is typed to accept __sessionId, __userId, and __feature
36
+ * alongside the normal params — no type cast required at the call site.
29
37
  */
30
38
  declare function wrapOpenAI<T extends OpenAILike>(client: T, tracker: Tracker): WrappedOpenAI<T>;
31
39
 
@@ -45,8 +53,12 @@ type WrappedAnthropic<T extends AnthropicLike> = Omit<T, 'messages'> & {
45
53
  * Wraps an Anthropic client to transparently intercept messages.create calls
46
54
  * and report token usage to the tracker.
47
55
  *
48
- * The returned client is typed to accept __sessionId and __userId alongside the
49
- * normal params — no type cast required at the call site.
56
+ * The returned client is typed to accept __sessionId, __userId, and __feature
57
+ * alongside the normal params — no type cast required at the call site.
58
+ *
59
+ * For extended thinking models, reasoningTokens is stored as an approximation
60
+ * (thinking block characters ÷ 4). It is informational only — thinking output
61
+ * is already included in outputTokens and is not double-counted in cost.
50
62
  */
51
63
  declare function wrapAnthropic<T extends AnthropicLike>(client: T, tracker: Tracker): WrappedAnthropic<T>;
52
64
 
@@ -79,6 +91,9 @@ interface GenAILike {
79
91
  * Wraps a GoogleGenerativeAI client to transparently intercept
80
92
  * generateContent / generateContentStream calls and report token usage.
81
93
  *
94
+ * Pass __feature in getGenerativeModel params to tag all calls from that model
95
+ * instance with a product feature name (appears in report.byFeature).
96
+ *
82
97
  * Returns the same type T that was passed in.
83
98
  */
84
99
  declare function wrapGemini<T extends GenAILike>(client: T, tracker: Tracker): T;
package/dist/index.js CHANGED
@@ -1379,7 +1379,11 @@ ${issues}`);
1379
1379
  }
1380
1380
  function track(entry) {
1381
1381
  const price = resolveModelPrice(entry.model);
1382
- const costUSD = calculateCost(entry.inputTokens, entry.outputTokens, price);
1382
+ const costUSD = calculateCost(
1383
+ entry.inputTokens,
1384
+ entry.outputTokens + (entry.reasoningTokens ?? 0),
1385
+ price
1386
+ );
1383
1387
  const full = {
1384
1388
  ...entry,
1385
1389
  costUSD,
@@ -1415,6 +1419,7 @@ ${issues}`);
1415
1419
  const byModel = {};
1416
1420
  const bySession = {};
1417
1421
  const byUser = {};
1422
+ const byFeature = {};
1418
1423
  let totalInput = 0;
1419
1424
  let totalOutput = 0;
1420
1425
  let totalCost = 0;
@@ -1424,11 +1429,12 @@ ${issues}`);
1424
1429
  totalOutput += e.outputTokens;
1425
1430
  totalCost += e.costUSD;
1426
1431
  if (e.timestamp > lastTimestamp) lastTimestamp = e.timestamp;
1427
- const m = byModel[e.model] ??= { costUSD: 0, calls: 0, tokens: { input: 0, output: 0 } };
1432
+ const m = byModel[e.model] ??= { costUSD: 0, calls: 0, tokens: { input: 0, output: 0, reasoning: 0 } };
1428
1433
  m.costUSD += e.costUSD;
1429
1434
  m.calls += 1;
1430
1435
  m.tokens.input += e.inputTokens;
1431
1436
  m.tokens.output += e.outputTokens;
1437
+ m.tokens.reasoning += e.reasoningTokens ?? 0;
1432
1438
  if (e.sessionId) {
1433
1439
  const s = bySession[e.sessionId] ??= { costUSD: 0, calls: 0 };
1434
1440
  s.costUSD += e.costUSD;
@@ -1439,6 +1445,11 @@ ${issues}`);
1439
1445
  u.costUSD += e.costUSD;
1440
1446
  u.calls += 1;
1441
1447
  }
1448
+ if (e.feature) {
1449
+ const f = byFeature[e.feature] ??= { costUSD: 0, calls: 0 };
1450
+ f.costUSD += e.costUSD;
1451
+ f.calls += 1;
1452
+ }
1442
1453
  }
1443
1454
  return {
1444
1455
  totalCostUSD: totalCost,
@@ -1446,6 +1457,7 @@ ${issues}`);
1446
1457
  byModel,
1447
1458
  bySession,
1448
1459
  byUser,
1460
+ byFeature,
1449
1461
  period: { from: startedAt, to: lastTimestamp }
1450
1462
  };
1451
1463
  }
@@ -1461,16 +1473,18 @@ ${issues}`);
1461
1473
  }
1462
1474
  async function exportCSV() {
1463
1475
  const entries = await Promise.resolve(storage.getAll());
1464
- const header = "timestamp,model,inputTokens,outputTokens,costUSD,sessionId,userId";
1476
+ const header = "timestamp,model,inputTokens,outputTokens,reasoningTokens,costUSD,sessionId,userId,feature";
1465
1477
  const rows = entries.map(
1466
1478
  (e) => [
1467
1479
  csvEscape(e.timestamp),
1468
1480
  csvEscape(e.model),
1469
1481
  e.inputTokens,
1470
1482
  e.outputTokens,
1483
+ e.reasoningTokens ?? 0,
1471
1484
  e.costUSD.toFixed(8),
1472
1485
  csvEscape(e.sessionId ?? ""),
1473
- csvEscape(e.userId ?? "")
1486
+ csvEscape(e.userId ?? ""),
1487
+ csvEscape(e.feature ?? "")
1474
1488
  ].join(",")
1475
1489
  );
1476
1490
  return [header, ...rows].join("\n");
@@ -1496,42 +1510,46 @@ function csvEscape(value) {
1496
1510
 
1497
1511
  // src/providers/openai.ts
1498
1512
  function extractMeta(params) {
1499
- const { __sessionId, __userId, ...cleaned } = params;
1513
+ const { __sessionId, __userId, __feature, ...cleaned } = params;
1500
1514
  return {
1501
1515
  cleaned,
1502
1516
  sessionId: typeof __sessionId === "string" ? __sessionId : void 0,
1503
- userId: typeof __userId === "string" ? __userId : void 0
1517
+ userId: typeof __userId === "string" ? __userId : void 0,
1518
+ feature: typeof __feature === "string" ? __feature : void 0
1504
1519
  };
1505
1520
  }
1506
1521
  function extractUsage(usage) {
1507
- if (!usage) return { inputTokens: 0, outputTokens: 0 };
1522
+ if (!usage) return { inputTokens: 0, outputTokens: 0, reasoningTokens: 0 };
1508
1523
  return {
1509
1524
  inputTokens: usage.prompt_tokens ?? usage.input_tokens ?? 0,
1510
- outputTokens: usage.completion_tokens ?? usage.output_tokens ?? 0
1525
+ outputTokens: usage.completion_tokens ?? usage.output_tokens ?? 0,
1526
+ reasoningTokens: usage.completion_tokens_details?.reasoning_tokens ?? 0
1511
1527
  };
1512
1528
  }
1513
- function trackWithMeta(tracker, model, inputTokens, outputTokens, sessionId, userId) {
1529
+ function trackWithMeta(tracker, model, inputTokens, outputTokens, reasoningTokens, sessionId, userId, feature) {
1514
1530
  tracker.track({
1515
1531
  model,
1516
1532
  inputTokens,
1517
1533
  outputTokens,
1534
+ ...reasoningTokens > 0 && { reasoningTokens },
1518
1535
  ...sessionId !== void 0 && { sessionId },
1519
- ...userId !== void 0 && { userId }
1536
+ ...userId !== void 0 && { userId },
1537
+ ...feature !== void 0 && { feature }
1520
1538
  });
1521
1539
  }
1522
- async function* wrapStream(stream, model, sessionId, userId, tracker) {
1540
+ async function* wrapStream(stream, model, sessionId, userId, feature, tracker) {
1523
1541
  let lastChunk;
1524
1542
  for await (const chunk of stream) {
1525
1543
  lastChunk = chunk;
1526
1544
  yield chunk;
1527
1545
  }
1528
- const { inputTokens, outputTokens } = extractUsage(lastChunk?.usage);
1546
+ const { inputTokens, outputTokens, reasoningTokens } = extractUsage(lastChunk?.usage);
1529
1547
  if (!lastChunk?.usage) {
1530
1548
  console.warn(
1531
1549
  `[tokenwatch] No usage data in stream for model "${model}". Cost recorded as $0. Pass stream_options: { include_usage: true } to get accurate costs.`
1532
1550
  );
1533
1551
  }
1534
- trackWithMeta(tracker, model, inputTokens, outputTokens, sessionId, userId);
1552
+ trackWithMeta(tracker, model, inputTokens, outputTokens, reasoningTokens, sessionId, userId, feature);
1535
1553
  }
1536
1554
  function wrapOpenAI(client, tracker) {
1537
1555
  const proxiedCompletions = new Proxy(client.chat.completions, {
@@ -1539,7 +1557,7 @@ function wrapOpenAI(client, tracker) {
1539
1557
  if (prop !== "create")
1540
1558
  return target[prop];
1541
1559
  return async function(params) {
1542
- const { cleaned, sessionId, userId } = extractMeta(params);
1560
+ const { cleaned, sessionId, userId, feature } = extractMeta(params);
1543
1561
  const model = typeof cleaned["model"] === "string" ? cleaned["model"] : "unknown";
1544
1562
  const result = await target.create(cleaned);
1545
1563
  if (result && typeof result === "object" && Symbol.asyncIterator in result) {
@@ -1548,18 +1566,21 @@ function wrapOpenAI(client, tracker) {
1548
1566
  model,
1549
1567
  sessionId,
1550
1568
  userId,
1569
+ feature,
1551
1570
  tracker
1552
1571
  );
1553
1572
  }
1554
1573
  const completion = result;
1555
- const { inputTokens, outputTokens } = extractUsage(completion.usage);
1574
+ const { inputTokens, outputTokens, reasoningTokens } = extractUsage(completion.usage);
1556
1575
  trackWithMeta(
1557
1576
  tracker,
1558
1577
  completion.model ?? model,
1559
1578
  inputTokens,
1560
1579
  outputTokens,
1580
+ reasoningTokens,
1561
1581
  sessionId,
1562
- userId
1582
+ userId,
1583
+ feature
1563
1584
  );
1564
1585
  return result;
1565
1586
  };
@@ -1571,9 +1592,25 @@ function wrapOpenAI(client, tracker) {
1571
1592
  return target[prop];
1572
1593
  }
1573
1594
  });
1595
+ const proxiedEmbeddings = client.embeddings ? new Proxy(client.embeddings, {
1596
+ get(target, prop) {
1597
+ if (prop !== "create")
1598
+ return target[prop];
1599
+ return async function(params) {
1600
+ const { cleaned, sessionId, userId, feature } = extractMeta(params);
1601
+ const model = typeof cleaned["model"] === "string" ? cleaned["model"] : "unknown";
1602
+ const result = await target.create(cleaned);
1603
+ const embedding = result;
1604
+ const inputTokens = embedding.usage?.total_tokens ?? 0;
1605
+ trackWithMeta(tracker, embedding.model ?? model, inputTokens, 0, 0, sessionId, userId, feature);
1606
+ return result;
1607
+ };
1608
+ }
1609
+ }) : void 0;
1574
1610
  return new Proxy(client, {
1575
1611
  get(target, prop) {
1576
1612
  if (prop === "chat") return proxiedChat;
1613
+ if (prop === "embeddings") return proxiedEmbeddings;
1577
1614
  return target[prop];
1578
1615
  }
1579
1616
  });
@@ -1581,11 +1618,12 @@ function wrapOpenAI(client, tracker) {
1581
1618
 
1582
1619
  // src/providers/anthropic.ts
1583
1620
  function extractMeta2(params) {
1584
- const { __sessionId, __userId, ...cleaned } = params;
1621
+ const { __sessionId, __userId, __feature, ...cleaned } = params;
1585
1622
  return {
1586
1623
  cleaned,
1587
1624
  sessionId: typeof __sessionId === "string" ? __sessionId : void 0,
1588
- userId: typeof __userId === "string" ? __userId : void 0
1625
+ userId: typeof __userId === "string" ? __userId : void 0,
1626
+ feature: typeof __feature === "string" ? __feature : void 0
1589
1627
  };
1590
1628
  }
1591
1629
  function extractUsage2(usage) {
@@ -1595,18 +1633,31 @@ function extractUsage2(usage) {
1595
1633
  outputTokens: usage.output_tokens ?? 0
1596
1634
  };
1597
1635
  }
1598
- function trackWithMeta2(tracker, model, inputTokens, outputTokens, sessionId, userId) {
1636
+ function extractThinkingTokenApprox(content) {
1637
+ if (!content) return 0;
1638
+ const chars = content.filter((b) => b.type === "thinking").reduce((sum, b) => sum + (b.thinking?.length ?? 0), 0);
1639
+ return chars > 0 ? Math.round(chars / 4) : 0;
1640
+ }
1641
+ function trackWithMeta2(tracker, model, inputTokens, outputTokens, reasoningTokens, sessionId, userId, feature) {
1599
1642
  tracker.track({
1600
1643
  model,
1601
1644
  inputTokens,
1602
1645
  outputTokens,
1646
+ // For Anthropic, reasoningTokens is informational (thinking already in outputTokens).
1647
+ // Pass 0 so tracker does not add it to cost (tracker only adds when > 0 AND separate).
1648
+ // We store it as a field but the tracker cost formula adds reasoningTokens to outputTokens,
1649
+ // so we must NOT pass it here to avoid double-counting.
1603
1650
  ...sessionId !== void 0 && { sessionId },
1604
- ...userId !== void 0 && { userId }
1651
+ ...userId !== void 0 && { userId },
1652
+ ...feature !== void 0 && { feature },
1653
+ ...reasoningTokens > 0 && { reasoningTokens }
1605
1654
  });
1606
1655
  }
1607
- async function* wrapStream2(stream, model, sessionId, userId, tracker) {
1656
+ async function* wrapStream2(stream, model, sessionId, userId, feature, tracker) {
1608
1657
  let inputTokens = 0;
1609
1658
  let outputTokens = 0;
1659
+ let currentBlockIsThinking = false;
1660
+ let thinkingCharCount = 0;
1610
1661
  for await (const event of stream) {
1611
1662
  yield event;
1612
1663
  if (event.type === "message_start" && event.message?.usage) {
@@ -1615,8 +1666,18 @@ async function* wrapStream2(stream, model, sessionId, userId, tracker) {
1615
1666
  if (event.type === "message_delta" && event.usage) {
1616
1667
  outputTokens = event.usage.output_tokens ?? 0;
1617
1668
  }
1669
+ if (event.type === "content_block_start") {
1670
+ currentBlockIsThinking = event.content_block?.type === "thinking";
1671
+ }
1672
+ if (event.type === "content_block_stop") {
1673
+ currentBlockIsThinking = false;
1674
+ }
1675
+ if (event.type === "content_block_delta" && currentBlockIsThinking && event.delta?.thinking) {
1676
+ thinkingCharCount += event.delta.thinking.length;
1677
+ }
1618
1678
  }
1619
- trackWithMeta2(tracker, model, inputTokens, outputTokens, sessionId, userId);
1679
+ const reasoningTokens = thinkingCharCount > 0 ? Math.round(thinkingCharCount / 4) : 0;
1680
+ trackWithMeta2(tracker, model, inputTokens, outputTokens, reasoningTokens, sessionId, userId, feature);
1620
1681
  }
1621
1682
  function wrapAnthropic(client, tracker) {
1622
1683
  const proxiedMessages = new Proxy(client.messages, {
@@ -1624,7 +1685,7 @@ function wrapAnthropic(client, tracker) {
1624
1685
  if (prop !== "create")
1625
1686
  return target[prop];
1626
1687
  return async function(params) {
1627
- const { cleaned, sessionId, userId } = extractMeta2(params);
1688
+ const { cleaned, sessionId, userId, feature } = extractMeta2(params);
1628
1689
  const model = typeof cleaned["model"] === "string" ? cleaned["model"] : "unknown";
1629
1690
  const result = await target.create(cleaned);
1630
1691
  if (result && typeof result === "object" && Symbol.asyncIterator in result) {
@@ -1633,18 +1694,22 @@ function wrapAnthropic(client, tracker) {
1633
1694
  model,
1634
1695
  sessionId,
1635
1696
  userId,
1697
+ feature,
1636
1698
  tracker
1637
1699
  );
1638
1700
  }
1639
1701
  const message = result;
1640
1702
  const { inputTokens, outputTokens } = extractUsage2(message.usage);
1703
+ const reasoningTokens = extractThinkingTokenApprox(message.content);
1641
1704
  trackWithMeta2(
1642
1705
  tracker,
1643
1706
  message.model ?? model,
1644
1707
  inputTokens,
1645
1708
  outputTokens,
1709
+ reasoningTokens,
1646
1710
  sessionId,
1647
- userId
1711
+ userId,
1712
+ feature
1648
1713
  );
1649
1714
  return result;
1650
1715
  };
@@ -1665,7 +1730,11 @@ function wrapGemini(client, tracker) {
1665
1730
  if (prop !== "getGenerativeModel")
1666
1731
  return target[prop];
1667
1732
  return function(modelParams) {
1668
- const modelInstance = target.getGenerativeModel(modelParams);
1733
+ const { __sessionId, __userId, __feature, ...cleanedParams } = modelParams;
1734
+ const feature = typeof __feature === "string" ? __feature : void 0;
1735
+ const sessionId = typeof __sessionId === "string" ? __sessionId : void 0;
1736
+ const userId = typeof __userId === "string" ? __userId : void 0;
1737
+ const modelInstance = target.getGenerativeModel(cleanedParams);
1669
1738
  const modelId = modelParams.model;
1670
1739
  return new Proxy(modelInstance, {
1671
1740
  get(mTarget, mProp) {
@@ -1676,7 +1745,10 @@ function wrapGemini(client, tracker) {
1676
1745
  tracker.track({
1677
1746
  model: modelId,
1678
1747
  inputTokens: meta?.promptTokenCount ?? 0,
1679
- outputTokens: meta?.candidatesTokenCount ?? 0
1748
+ outputTokens: meta?.candidatesTokenCount ?? 0,
1749
+ ...sessionId !== void 0 && { sessionId },
1750
+ ...userId !== void 0 && { userId },
1751
+ ...feature !== void 0 && { feature }
1680
1752
  });
1681
1753
  return result;
1682
1754
  };
@@ -1689,7 +1761,10 @@ function wrapGemini(client, tracker) {
1689
1761
  tracker.track({
1690
1762
  model: modelId,
1691
1763
  inputTokens: meta?.promptTokenCount ?? 0,
1692
- outputTokens: meta?.candidatesTokenCount ?? 0
1764
+ outputTokens: meta?.candidatesTokenCount ?? 0,
1765
+ ...sessionId !== void 0 && { sessionId },
1766
+ ...userId !== void 0 && { userId },
1767
+ ...feature !== void 0 && { feature }
1693
1768
  });
1694
1769
  }).catch(() => {
1695
1770
  });