@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.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-B_EmA3K7.js';
2
+ export { F as FeatureStats, 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-B_EmA3K7.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
@@ -74,29 +74,40 @@ var SqliteStorage = class {
74
74
  migrate() {
75
75
  this.db.exec(`
76
76
  CREATE TABLE IF NOT EXISTS usage (
77
- id INTEGER PRIMARY KEY AUTOINCREMENT,
78
- model TEXT NOT NULL,
79
- input_tokens INTEGER NOT NULL,
80
- output_tokens INTEGER NOT NULL,
81
- cost_usd REAL NOT NULL,
82
- session_id TEXT,
83
- user_id TEXT,
84
- timestamp TEXT NOT NULL
77
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
78
+ model TEXT NOT NULL,
79
+ input_tokens INTEGER NOT NULL,
80
+ output_tokens INTEGER NOT NULL,
81
+ reasoning_tokens INTEGER NOT NULL DEFAULT 0,
82
+ cost_usd REAL NOT NULL,
83
+ session_id TEXT,
84
+ user_id TEXT,
85
+ feature TEXT,
86
+ timestamp TEXT NOT NULL
85
87
  )
86
88
  `);
89
+ const cols = this.db.prepare(`PRAGMA table_info(usage)`).all().map((c) => c.name);
90
+ if (!cols.includes("reasoning_tokens")) {
91
+ this.db.exec(`ALTER TABLE usage ADD COLUMN reasoning_tokens INTEGER NOT NULL DEFAULT 0`);
92
+ }
93
+ if (!cols.includes("feature")) {
94
+ this.db.exec(`ALTER TABLE usage ADD COLUMN feature TEXT`);
95
+ }
87
96
  }
88
97
  record(entry) {
89
98
  this.db.prepare(
90
99
  `INSERT INTO usage
91
- (model, input_tokens, output_tokens, cost_usd, session_id, user_id, timestamp)
92
- VALUES (?, ?, ?, ?, ?, ?, ?)`
100
+ (model, input_tokens, output_tokens, reasoning_tokens, cost_usd, session_id, user_id, feature, timestamp)
101
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
93
102
  ).run(
94
103
  entry.model,
95
104
  entry.inputTokens,
96
105
  entry.outputTokens,
106
+ entry.reasoningTokens ?? 0,
97
107
  entry.costUSD,
98
108
  entry.sessionId ?? null,
99
109
  entry.userId ?? null,
110
+ entry.feature ?? null,
100
111
  entry.timestamp
101
112
  );
102
113
  }
@@ -106,9 +117,11 @@ var SqliteStorage = class {
106
117
  model: r.model,
107
118
  inputTokens: r.input_tokens,
108
119
  outputTokens: r.output_tokens,
120
+ ...r.reasoning_tokens > 0 && { reasoningTokens: r.reasoning_tokens },
109
121
  costUSD: r.cost_usd,
110
122
  ...r.session_id != null && { sessionId: r.session_id },
111
123
  ...r.user_id != null && { userId: r.user_id },
124
+ ...r.feature != null && { feature: r.feature },
112
125
  timestamp: r.timestamp
113
126
  }));
114
127
  }
@@ -1415,6 +1428,7 @@ ${issues}`);
1415
1428
  const byModel = {};
1416
1429
  const bySession = {};
1417
1430
  const byUser = {};
1431
+ const byFeature = {};
1418
1432
  let totalInput = 0;
1419
1433
  let totalOutput = 0;
1420
1434
  let totalCost = 0;
@@ -1424,11 +1438,12 @@ ${issues}`);
1424
1438
  totalOutput += e.outputTokens;
1425
1439
  totalCost += e.costUSD;
1426
1440
  if (e.timestamp > lastTimestamp) lastTimestamp = e.timestamp;
1427
- const m = byModel[e.model] ??= { costUSD: 0, calls: 0, tokens: { input: 0, output: 0 } };
1441
+ const m = byModel[e.model] ??= { costUSD: 0, calls: 0, tokens: { input: 0, output: 0, reasoning: 0 } };
1428
1442
  m.costUSD += e.costUSD;
1429
1443
  m.calls += 1;
1430
1444
  m.tokens.input += e.inputTokens;
1431
1445
  m.tokens.output += e.outputTokens;
1446
+ m.tokens.reasoning += e.reasoningTokens ?? 0;
1432
1447
  if (e.sessionId) {
1433
1448
  const s = bySession[e.sessionId] ??= { costUSD: 0, calls: 0 };
1434
1449
  s.costUSD += e.costUSD;
@@ -1439,6 +1454,11 @@ ${issues}`);
1439
1454
  u.costUSD += e.costUSD;
1440
1455
  u.calls += 1;
1441
1456
  }
1457
+ if (e.feature) {
1458
+ const f = byFeature[e.feature] ??= { costUSD: 0, calls: 0 };
1459
+ f.costUSD += e.costUSD;
1460
+ f.calls += 1;
1461
+ }
1442
1462
  }
1443
1463
  return {
1444
1464
  totalCostUSD: totalCost,
@@ -1446,6 +1466,7 @@ ${issues}`);
1446
1466
  byModel,
1447
1467
  bySession,
1448
1468
  byUser,
1469
+ byFeature,
1449
1470
  period: { from: startedAt, to: lastTimestamp }
1450
1471
  };
1451
1472
  }
@@ -1461,16 +1482,18 @@ ${issues}`);
1461
1482
  }
1462
1483
  async function exportCSV() {
1463
1484
  const entries = await Promise.resolve(storage.getAll());
1464
- const header = "timestamp,model,inputTokens,outputTokens,costUSD,sessionId,userId";
1485
+ const header = "timestamp,model,inputTokens,outputTokens,reasoningTokens,costUSD,sessionId,userId,feature";
1465
1486
  const rows = entries.map(
1466
1487
  (e) => [
1467
1488
  csvEscape(e.timestamp),
1468
1489
  csvEscape(e.model),
1469
1490
  e.inputTokens,
1470
1491
  e.outputTokens,
1492
+ e.reasoningTokens ?? 0,
1471
1493
  e.costUSD.toFixed(8),
1472
1494
  csvEscape(e.sessionId ?? ""),
1473
- csvEscape(e.userId ?? "")
1495
+ csvEscape(e.userId ?? ""),
1496
+ csvEscape(e.feature ?? "")
1474
1497
  ].join(",")
1475
1498
  );
1476
1499
  return [header, ...rows].join("\n");
@@ -1496,42 +1519,46 @@ function csvEscape(value) {
1496
1519
 
1497
1520
  // src/providers/openai.ts
1498
1521
  function extractMeta(params) {
1499
- const { __sessionId, __userId, ...cleaned } = params;
1522
+ const { __sessionId, __userId, __feature, ...cleaned } = params;
1500
1523
  return {
1501
1524
  cleaned,
1502
1525
  sessionId: typeof __sessionId === "string" ? __sessionId : void 0,
1503
- userId: typeof __userId === "string" ? __userId : void 0
1526
+ userId: typeof __userId === "string" ? __userId : void 0,
1527
+ feature: typeof __feature === "string" ? __feature : void 0
1504
1528
  };
1505
1529
  }
1506
1530
  function extractUsage(usage) {
1507
- if (!usage) return { inputTokens: 0, outputTokens: 0 };
1531
+ if (!usage) return { inputTokens: 0, outputTokens: 0, reasoningTokens: 0 };
1508
1532
  return {
1509
1533
  inputTokens: usage.prompt_tokens ?? usage.input_tokens ?? 0,
1510
- outputTokens: usage.completion_tokens ?? usage.output_tokens ?? 0
1534
+ outputTokens: usage.completion_tokens ?? usage.output_tokens ?? 0,
1535
+ reasoningTokens: usage.completion_tokens_details?.reasoning_tokens ?? 0
1511
1536
  };
1512
1537
  }
1513
- function trackWithMeta(tracker, model, inputTokens, outputTokens, sessionId, userId) {
1538
+ function trackWithMeta(tracker, model, inputTokens, outputTokens, reasoningTokens, sessionId, userId, feature) {
1514
1539
  tracker.track({
1515
1540
  model,
1516
1541
  inputTokens,
1517
- outputTokens,
1542
+ outputTokens: outputTokens + reasoningTokens,
1543
+ ...reasoningTokens > 0 && { reasoningTokens },
1518
1544
  ...sessionId !== void 0 && { sessionId },
1519
- ...userId !== void 0 && { userId }
1545
+ ...userId !== void 0 && { userId },
1546
+ ...feature !== void 0 && { feature }
1520
1547
  });
1521
1548
  }
1522
- async function* wrapStream(stream, model, sessionId, userId, tracker) {
1549
+ async function* wrapStream(stream, model, sessionId, userId, feature, tracker) {
1523
1550
  let lastChunk;
1524
1551
  for await (const chunk of stream) {
1525
1552
  lastChunk = chunk;
1526
1553
  yield chunk;
1527
1554
  }
1528
- const { inputTokens, outputTokens } = extractUsage(lastChunk?.usage);
1555
+ const { inputTokens, outputTokens, reasoningTokens } = extractUsage(lastChunk?.usage);
1529
1556
  if (!lastChunk?.usage) {
1530
1557
  console.warn(
1531
1558
  `[tokenwatch] No usage data in stream for model "${model}". Cost recorded as $0. Pass stream_options: { include_usage: true } to get accurate costs.`
1532
1559
  );
1533
1560
  }
1534
- trackWithMeta(tracker, model, inputTokens, outputTokens, sessionId, userId);
1561
+ trackWithMeta(tracker, model, inputTokens, outputTokens, reasoningTokens, sessionId, userId, feature);
1535
1562
  }
1536
1563
  function wrapOpenAI(client, tracker) {
1537
1564
  const proxiedCompletions = new Proxy(client.chat.completions, {
@@ -1539,7 +1566,7 @@ function wrapOpenAI(client, tracker) {
1539
1566
  if (prop !== "create")
1540
1567
  return target[prop];
1541
1568
  return async function(params) {
1542
- const { cleaned, sessionId, userId } = extractMeta(params);
1569
+ const { cleaned, sessionId, userId, feature } = extractMeta(params);
1543
1570
  const model = typeof cleaned["model"] === "string" ? cleaned["model"] : "unknown";
1544
1571
  const result = await target.create(cleaned);
1545
1572
  if (result && typeof result === "object" && Symbol.asyncIterator in result) {
@@ -1548,18 +1575,21 @@ function wrapOpenAI(client, tracker) {
1548
1575
  model,
1549
1576
  sessionId,
1550
1577
  userId,
1578
+ feature,
1551
1579
  tracker
1552
1580
  );
1553
1581
  }
1554
1582
  const completion = result;
1555
- const { inputTokens, outputTokens } = extractUsage(completion.usage);
1583
+ const { inputTokens, outputTokens, reasoningTokens } = extractUsage(completion.usage);
1556
1584
  trackWithMeta(
1557
1585
  tracker,
1558
1586
  completion.model ?? model,
1559
1587
  inputTokens,
1560
1588
  outputTokens,
1589
+ reasoningTokens,
1561
1590
  sessionId,
1562
- userId
1591
+ userId,
1592
+ feature
1563
1593
  );
1564
1594
  return result;
1565
1595
  };
@@ -1571,9 +1601,25 @@ function wrapOpenAI(client, tracker) {
1571
1601
  return target[prop];
1572
1602
  }
1573
1603
  });
1604
+ const proxiedEmbeddings = client.embeddings ? new Proxy(client.embeddings, {
1605
+ get(target, prop) {
1606
+ if (prop !== "create")
1607
+ return target[prop];
1608
+ return async function(params) {
1609
+ const { cleaned, sessionId, userId, feature } = extractMeta(params);
1610
+ const model = typeof cleaned["model"] === "string" ? cleaned["model"] : "unknown";
1611
+ const result = await target.create(cleaned);
1612
+ const embedding = result;
1613
+ const inputTokens = embedding.usage?.total_tokens ?? 0;
1614
+ trackWithMeta(tracker, embedding.model ?? model, inputTokens, 0, 0, sessionId, userId, feature);
1615
+ return result;
1616
+ };
1617
+ }
1618
+ }) : void 0;
1574
1619
  return new Proxy(client, {
1575
1620
  get(target, prop) {
1576
1621
  if (prop === "chat") return proxiedChat;
1622
+ if (prop === "embeddings") return proxiedEmbeddings;
1577
1623
  return target[prop];
1578
1624
  }
1579
1625
  });
@@ -1581,11 +1627,12 @@ function wrapOpenAI(client, tracker) {
1581
1627
 
1582
1628
  // src/providers/anthropic.ts
1583
1629
  function extractMeta2(params) {
1584
- const { __sessionId, __userId, ...cleaned } = params;
1630
+ const { __sessionId, __userId, __feature, ...cleaned } = params;
1585
1631
  return {
1586
1632
  cleaned,
1587
1633
  sessionId: typeof __sessionId === "string" ? __sessionId : void 0,
1588
- userId: typeof __userId === "string" ? __userId : void 0
1634
+ userId: typeof __userId === "string" ? __userId : void 0,
1635
+ feature: typeof __feature === "string" ? __feature : void 0
1589
1636
  };
1590
1637
  }
1591
1638
  function extractUsage2(usage) {
@@ -1595,18 +1642,27 @@ function extractUsage2(usage) {
1595
1642
  outputTokens: usage.output_tokens ?? 0
1596
1643
  };
1597
1644
  }
1598
- function trackWithMeta2(tracker, model, inputTokens, outputTokens, sessionId, userId) {
1645
+ function extractThinkingTokenApprox(content) {
1646
+ if (!content) return 0;
1647
+ const chars = content.filter((b) => b.type === "thinking").reduce((sum, b) => sum + (b.thinking?.length ?? 0), 0);
1648
+ return chars > 0 ? Math.round(chars / 4) : 0;
1649
+ }
1650
+ function trackWithMeta2(tracker, model, inputTokens, outputTokens, reasoningTokens, sessionId, userId, feature) {
1599
1651
  tracker.track({
1600
1652
  model,
1601
1653
  inputTokens,
1602
1654
  outputTokens,
1655
+ ...reasoningTokens > 0 && { reasoningTokens },
1603
1656
  ...sessionId !== void 0 && { sessionId },
1604
- ...userId !== void 0 && { userId }
1657
+ ...userId !== void 0 && { userId },
1658
+ ...feature !== void 0 && { feature }
1605
1659
  });
1606
1660
  }
1607
- async function* wrapStream2(stream, model, sessionId, userId, tracker) {
1661
+ async function* wrapStream2(stream, model, sessionId, userId, feature, tracker) {
1608
1662
  let inputTokens = 0;
1609
1663
  let outputTokens = 0;
1664
+ let currentBlockIsThinking = false;
1665
+ let thinkingCharCount = 0;
1610
1666
  for await (const event of stream) {
1611
1667
  yield event;
1612
1668
  if (event.type === "message_start" && event.message?.usage) {
@@ -1615,8 +1671,18 @@ async function* wrapStream2(stream, model, sessionId, userId, tracker) {
1615
1671
  if (event.type === "message_delta" && event.usage) {
1616
1672
  outputTokens = event.usage.output_tokens ?? 0;
1617
1673
  }
1674
+ if (event.type === "content_block_start") {
1675
+ currentBlockIsThinking = event.content_block?.type === "thinking";
1676
+ }
1677
+ if (event.type === "content_block_stop") {
1678
+ currentBlockIsThinking = false;
1679
+ }
1680
+ if (event.type === "content_block_delta" && currentBlockIsThinking && event.delta?.thinking) {
1681
+ thinkingCharCount += event.delta.thinking.length;
1682
+ }
1618
1683
  }
1619
- trackWithMeta2(tracker, model, inputTokens, outputTokens, sessionId, userId);
1684
+ const reasoningTokens = thinkingCharCount > 0 ? Math.round(thinkingCharCount / 4) : 0;
1685
+ trackWithMeta2(tracker, model, inputTokens, outputTokens, reasoningTokens, sessionId, userId, feature);
1620
1686
  }
1621
1687
  function wrapAnthropic(client, tracker) {
1622
1688
  const proxiedMessages = new Proxy(client.messages, {
@@ -1624,7 +1690,7 @@ function wrapAnthropic(client, tracker) {
1624
1690
  if (prop !== "create")
1625
1691
  return target[prop];
1626
1692
  return async function(params) {
1627
- const { cleaned, sessionId, userId } = extractMeta2(params);
1693
+ const { cleaned, sessionId, userId, feature } = extractMeta2(params);
1628
1694
  const model = typeof cleaned["model"] === "string" ? cleaned["model"] : "unknown";
1629
1695
  const result = await target.create(cleaned);
1630
1696
  if (result && typeof result === "object" && Symbol.asyncIterator in result) {
@@ -1633,18 +1699,22 @@ function wrapAnthropic(client, tracker) {
1633
1699
  model,
1634
1700
  sessionId,
1635
1701
  userId,
1702
+ feature,
1636
1703
  tracker
1637
1704
  );
1638
1705
  }
1639
1706
  const message = result;
1640
1707
  const { inputTokens, outputTokens } = extractUsage2(message.usage);
1708
+ const reasoningTokens = extractThinkingTokenApprox(message.content);
1641
1709
  trackWithMeta2(
1642
1710
  tracker,
1643
1711
  message.model ?? model,
1644
1712
  inputTokens,
1645
1713
  outputTokens,
1714
+ reasoningTokens,
1646
1715
  sessionId,
1647
- userId
1716
+ userId,
1717
+ feature
1648
1718
  );
1649
1719
  return result;
1650
1720
  };
@@ -1665,7 +1735,11 @@ function wrapGemini(client, tracker) {
1665
1735
  if (prop !== "getGenerativeModel")
1666
1736
  return target[prop];
1667
1737
  return function(modelParams) {
1668
- const modelInstance = target.getGenerativeModel(modelParams);
1738
+ const { __sessionId, __userId, __feature, ...cleanedParams } = modelParams;
1739
+ const feature = typeof __feature === "string" ? __feature : void 0;
1740
+ const sessionId = typeof __sessionId === "string" ? __sessionId : void 0;
1741
+ const userId = typeof __userId === "string" ? __userId : void 0;
1742
+ const modelInstance = target.getGenerativeModel(cleanedParams);
1669
1743
  const modelId = modelParams.model;
1670
1744
  return new Proxy(modelInstance, {
1671
1745
  get(mTarget, mProp) {
@@ -1676,7 +1750,10 @@ function wrapGemini(client, tracker) {
1676
1750
  tracker.track({
1677
1751
  model: modelId,
1678
1752
  inputTokens: meta?.promptTokenCount ?? 0,
1679
- outputTokens: meta?.candidatesTokenCount ?? 0
1753
+ outputTokens: meta?.candidatesTokenCount ?? 0,
1754
+ ...sessionId !== void 0 && { sessionId },
1755
+ ...userId !== void 0 && { userId },
1756
+ ...feature !== void 0 && { feature }
1680
1757
  });
1681
1758
  return result;
1682
1759
  };
@@ -1689,7 +1766,10 @@ function wrapGemini(client, tracker) {
1689
1766
  tracker.track({
1690
1767
  model: modelId,
1691
1768
  inputTokens: meta?.promptTokenCount ?? 0,
1692
- outputTokens: meta?.candidatesTokenCount ?? 0
1769
+ outputTokens: meta?.candidatesTokenCount ?? 0,
1770
+ ...sessionId !== void 0 && { sessionId },
1771
+ ...userId !== void 0 && { userId },
1772
+ ...feature !== void 0 && { feature }
1693
1773
  });
1694
1774
  }).catch(() => {
1695
1775
  });