@diogonzafe/tokenwatch 0.2.0 → 0.4.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @diogonzafe/tokenwatch
2
2
 
3
- Transparent TypeScript wrapper that intercepts LLM API calls and tracks cost in real-time by session, user and model — without changing anything in your existing code.
3
+ Transparent TypeScript wrapper that intercepts LLM API calls and tracks cost in real-time by session, user, model and feature — without changing anything in your existing code.
4
4
 
5
5
  Supports **OpenAI**, **Anthropic**, **Google Gemini** and **DeepSeek**.
6
6
 
@@ -34,17 +34,44 @@ import { createTracker } from '@diogonzafe/tokenwatch'
34
34
  const tracker = createTracker({
35
35
  // All fields are optional
36
36
  storage: 'memory', // 'memory' (default) | 'sqlite' | IStorage instance
37
- alertThreshold: 1.00, // USD — fires webhookUrl when exceeded
37
+ alertThreshold: 1.00, // USD — fires webhookUrl when total cost exceeded
38
38
  webhookUrl: 'https://...', // Discord / Slack webhook
39
39
  syncPrices: true, // fetch fresh prices from GitHub (default: true)
40
+ warnIfStaleAfterHours: 72, // warn if prices are older than N hours (0 = disable)
40
41
  customPrices: {
41
42
  'my-model': { input: 0.50, output: 1.50, maxInputTokens: 32000 } // USD per 1M tokens
42
- }
43
+ },
44
+ budgets: {
45
+ perUser: { threshold: 1.00, webhookUrl: 'https://...' }, // per-user alert
46
+ perSession: { threshold: 0.10, webhookUrl: 'https://...' }, // per-session alert
47
+ },
48
+ suggestions: true, // log hints for cheaper models in same family (>50% savings)
43
49
  })
44
50
  ```
45
51
 
46
52
  ---
47
53
 
54
+ ## Lazy Initialization
55
+
56
+ For module-level singletons where the tracker needs to be imported before `createTracker()` can run (e.g. shared modules, Jest test environments):
57
+
58
+ ```ts
59
+ import { createLazyTracker } from '@diogonzafe/tokenwatch'
60
+
61
+ // Safe to import anywhere — all methods are no-ops until init() is called
62
+ export const tracker = createLazyTracker()
63
+
64
+ // At app startup (e.g. index.ts / server.ts):
65
+ tracker.init({ storage: 'sqlite', syncPrices: true })
66
+
67
+ // In tests — never call init(), track() silently no-ops:
68
+ tracker.track({ model: 'gpt-4o', inputTokens: 100, outputTokens: 50 }) // does nothing
69
+ ```
70
+
71
+ `init()` may only be called once — a second call throws. A failed `init()` (e.g. invalid config) leaves the tracker in no-op mode. `LazyTracker` satisfies the `Tracker` interface, so it can be used anywhere a `Tracker` is expected.
72
+
73
+ ---
74
+
48
75
  ## OpenAI
49
76
 
50
77
  ```ts
@@ -124,7 +151,12 @@ import { wrapGemini } from '@diogonzafe/tokenwatch'
124
151
 
125
152
  const genAI = wrapGemini(new GoogleGenerativeAI(process.env.GEMINI_API_KEY!), tracker)
126
153
 
127
- const model = genAI.getGenerativeModel({ model: 'gemini-2.5-flash' })
154
+ // __sessionId, __userId, __feature are passed to getGenerativeModel (not per-call)
155
+ const model = genAI.getGenerativeModel({
156
+ model: 'gemini-2.5-flash',
157
+ __sessionId: 'session_abc',
158
+ __feature: 'rag',
159
+ })
128
160
  const result = await model.generateContent('Explain quantum computing')
129
161
  ```
130
162
 
@@ -154,6 +186,111 @@ const res = await deepseek.chat.completions.create({
154
186
 
155
187
  ---
156
188
 
189
+ ## Agent Frameworks
190
+
191
+ Frameworks like **Mastra**, **Vercel AI SDK**, **LlamaIndex**, and **LangChain** use their own internal LLM abstractions — they never expose the raw OpenAI/Anthropic client. `wrapOpenAI` and `wrapAnthropic` do not apply. Use `tracker.track()` manually via each framework's usage callback instead.
192
+
193
+ > `tracker.track()` always expects `inputTokens` and `outputTokens`. The field names exposed by each framework differ — see the mappings below.
194
+
195
+ ### Mastra
196
+
197
+ `agent.generate()` and `agent.stream()` expose usage in `onStepFinish`:
198
+
199
+ ```ts
200
+ import { Agent } from '@mastra/core/agent'
201
+ import { createTracker } from '@diogonzafe/tokenwatch'
202
+
203
+ const tracker = createTracker({ storage: 'sqlite' })
204
+
205
+ const agent = new Agent({ model: 'openai/gpt-4o', instructions: '...' })
206
+
207
+ const result = await agent.generate('Hello', {
208
+ onStepFinish: ({ usage }) => {
209
+ tracker.track({
210
+ model: 'gpt-4o',
211
+ inputTokens: usage.promptTokens, // Mastra uses promptTokens
212
+ outputTokens: usage.completionTokens, // Mastra uses completionTokens
213
+ sessionId: 'sess-abc',
214
+ })
215
+ },
216
+ })
217
+ ```
218
+
219
+ ### Vercel AI SDK
220
+
221
+ `streamText` / `generateText` expose usage in `onFinish`. As of `ai` v5, the fields are `inputTokens` / `outputTokens`:
222
+
223
+ ```ts
224
+ import { createOpenAI } from '@ai-sdk/openai'
225
+ import { streamText } from 'ai'
226
+ import { createTracker } from '@diogonzafe/tokenwatch'
227
+
228
+ const tracker = createTracker({ storage: 'sqlite' })
229
+ const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY! })
230
+
231
+ await streamText({
232
+ model: openai('gpt-4o'),
233
+ prompt: 'Hello',
234
+ onFinish: ({ usage }) => {
235
+ tracker.track({
236
+ model: 'gpt-4o',
237
+ inputTokens: usage.inputTokens ?? 0,
238
+ outputTokens: usage.outputTokens ?? 0,
239
+ })
240
+ },
241
+ })
242
+ ```
243
+
244
+ For multi-step agents, use `totalUsage` instead of `usage` in `onFinish` to get the aggregate across all steps.
245
+
246
+ ### LlamaIndex TypeScript
247
+
248
+ Use `Settings.callbackManager` to intercept `llm-end` events. The raw OpenAI response is available as `response.raw` with snake_case field names:
249
+
250
+ ```ts
251
+ import { Settings } from 'llamaindex'
252
+ import { createTracker } from '@diogonzafe/tokenwatch'
253
+
254
+ const tracker = createTracker({ storage: 'sqlite' })
255
+
256
+ Settings.callbackManager.on('llm-end', (event) => {
257
+ const raw = event.detail.response.raw as { model?: string; usage?: { prompt_tokens: number; completion_tokens: number } }
258
+ if (raw?.usage) {
259
+ tracker.track({
260
+ model: raw.model ?? 'unknown',
261
+ inputTokens: raw.usage.prompt_tokens, // LlamaIndex exposes snake_case
262
+ outputTokens: raw.usage.completion_tokens,
263
+ })
264
+ }
265
+ })
266
+ ```
267
+
268
+ ### LangChain.js
269
+
270
+ Use the built-in `TokenwatchCallbackHandler` from the `/langchain` sub-path:
271
+
272
+ ```ts
273
+ import { ChatOpenAI } from '@langchain/openai'
274
+ import { createTracker } from '@diogonzafe/tokenwatch'
275
+ import { TokenwatchCallbackHandler } from '@diogonzafe/tokenwatch/langchain'
276
+
277
+ const tracker = createTracker({ storage: 'sqlite' })
278
+ const handler = new TokenwatchCallbackHandler(tracker, {
279
+ defaultModel: 'gpt-4o', // fallback when the response doesn't include the model name
280
+ sessionId: 'sess_abc', // optional — tag all calls from this handler
281
+ userId: 'user_123',
282
+ feature: 'chat',
283
+ })
284
+
285
+ const llm = new ChatOpenAI({ model: 'gpt-4o', callbacks: [handler] })
286
+ ```
287
+
288
+ The handler extracts `promptTokens` / `completionTokens` from `llmOutput.tokenUsage` (non-streaming) and falls back to `estimatedTokenUsage` for streaming calls. No `@langchain/core` compile-time dependency is required in tokenwatch itself — it is an optional peer dependency.
289
+
290
+ > **Note:** This requires `@langchain/core >= 0.1.0` to be installed in your project.
291
+
292
+ ---
293
+
157
294
  ## Reports
158
295
 
159
296
  All report methods are async:
@@ -164,24 +301,36 @@ const report = await tracker.getReport()
164
301
  // totalCostUSD: 0.087,
165
302
  // totalTokens: { input: 24000, output: 6000 },
166
303
  // byModel: {
167
- // 'gpt-4o': { costUSD: 0.062, calls: 5, tokens: { input: 20000, output: 5000, reasoning: 0 } },
168
- // 'o3': { costUSD: 0.041, calls: 1, tokens: { input: 1000, output: 200, reasoning: 800 } },
169
- // 'claude-sonnet-4-6': { costUSD: 0.025, calls: 2, tokens: { input: 4000, output: 1000, reasoning: 0 } }
304
+ // 'gpt-4o': { costUSD: 0.062, calls: 5, tokens: { input: 20000, output: 5000, reasoning: 0, cached: 4000 } },
305
+ // 'o3': { costUSD: 0.041, calls: 1, tokens: { input: 1000, output: 200, reasoning: 800, cached: 0 } },
170
306
  // },
171
307
  // bySession: { 'session_abc': { costUSD: 0.045, calls: 4 } },
172
308
  // byUser: { 'user_123': { costUSD: 0.087, calls: 7 } },
173
309
  // byFeature: { 'chat': { costUSD: 0.062, calls: 5 }, 'rag': { costUSD: 0.025, calls: 3 } },
174
- // period: { from: '2026-04-16T10:00:00Z', to: '2026-04-16T11:00:00Z' }
310
+ // period: { from: '2026-04-16T10:00:00Z', to: '2026-04-16T11:00:00Z' },
311
+ // pricesUpdatedAt: '2026-04-22' // date of the price data in use
175
312
  // }
176
313
 
314
+ // Time-filtered reports
315
+ await tracker.getReport({ last: '24h' }) // last 24 hours
316
+ await tracker.getReport({ last: '7d' }) // last 7 days
317
+ await tracker.getReport({ since: '2026-04-01' }) // since a specific date
318
+ await tracker.getReport({ since: '2026-04-01', until: '2026-04-30' })
319
+
320
+ // Cost forecast — burn rate + projected daily/monthly spend
321
+ const forecast = await tracker.getCostForecast()
322
+ // { burnRatePerHour: 0.043, projectedDailyCostUSD: 1.03, projectedMonthlyCostUSD: 31.20, basedOnHours: 6 }
323
+
324
+ await tracker.getCostForecast({ windowHours: 1 }) // use last 1h for burn rate calculation
325
+
177
326
  tracker.getModelInfo('gpt-4o')
178
- // { input: 2.5, output: 10, maxInputTokens: 128000 }
327
+ // { input: 2.5, output: 10, cachedInput: 1.25, maxInputTokens: 128000 }
179
328
  // Returns null if the model is unknown (synchronous)
180
329
 
181
330
  await tracker.reset() // clear all data
182
331
  await tracker.resetSession('session_abc') // clear one session
183
332
  await tracker.exportJSON() // full report as JSON string
184
- await tracker.exportCSV() // all raw calls as CSV (RFC 4180 — fields with commas/quotes are escaped)
333
+ await tracker.exportCSV() // all raw calls as CSV (RFC 4180)
185
334
  ```
186
335
 
187
336
  ---
@@ -206,10 +355,10 @@ Prices are in **USD per 1 million tokens**.
206
355
 
207
356
  ```json
208
357
  {
209
- "gpt-4o": { "input": 2.50, "output": 10.00, "maxInputTokens": 128000 },
210
- "claude-sonnet-4-6": { "input": 3.00, "output": 15.00, "maxInputTokens": 1000000 },
211
- "gemini-2.5-pro": { "input": 1.25, "output": 10.00, "maxInputTokens": 1048576 },
212
- "deepseek-chat": { "input": 0.28, "output": 0.42, "maxInputTokens": 131072 }
358
+ "gpt-4o": { "input": 2.50, "output": 10.00, "cachedInput": 1.25, "maxInputTokens": 128000 },
359
+ "claude-sonnet-4-6": { "input": 3.00, "output": 15.00, "cachedInput": 0.30, "cacheCreationInput": 3.75, "maxInputTokens": 1000000 },
360
+ "gemini-2.5-pro": { "input": 1.25, "output": 10.00, "maxInputTokens": 1048576 },
361
+ "deepseek-chat": { "input": 0.28, "output": 0.42, "maxInputTokens": 131072 }
213
362
  }
214
363
  ```
215
364
 
@@ -311,8 +460,45 @@ const tracker = createTracker({ storage: new RedisStorage() })
311
460
 
312
461
  ---
313
462
 
463
+ ## Prompt Caching
464
+
465
+ OpenAI and Anthropic offer discounted pricing for cached prompt tokens. tokenwatch tracks these automatically — no changes needed at the call site.
466
+
467
+ **OpenAI** — cached reads billed at 50% of input price:
468
+ ```ts
469
+ // prompt_tokens_details.cached_tokens is extracted automatically
470
+ const res = await openai.chat.completions.create({
471
+ model: 'gpt-4o',
472
+ messages: [{ role: 'user', content: '...' }],
473
+ })
474
+ // report.byModel['gpt-4o'].tokens.cached shows how many tokens were served from cache
475
+ ```
476
+
477
+ **Anthropic** — cache reads at 10% of input price, cache creation at 125%:
478
+ ```ts
479
+ const res = await anthropic.messages.create({
480
+ model: 'claude-sonnet-4-6',
481
+ max_tokens: 1024,
482
+ messages: [{ role: 'user', content: '...' }],
483
+ // cache_read_input_tokens and cache_creation_input_tokens extracted from usage automatically
484
+ })
485
+ ```
486
+
487
+ Cached token prices are included in `prices.json` for all models that support caching. You can also override them:
488
+ ```ts
489
+ const tracker = createTracker({
490
+ customPrices: {
491
+ 'my-model': { input: 2.50, output: 10.00, cachedInput: 1.25, cacheCreationInput: 3.13 }
492
+ }
493
+ })
494
+ ```
495
+
496
+ ---
497
+
314
498
  ## Alerts & Webhooks
315
499
 
500
+ ### Global threshold
501
+
316
502
  ```ts
317
503
  const tracker = createTracker({
318
504
  alertThreshold: 5.00, // USD
@@ -326,6 +512,36 @@ Webhook payload:
326
512
  { "text": "[tokenwatch] Alert: total cost reached $5.0012 USD (threshold: $5)" }
327
513
  ```
328
514
 
515
+ ### Per-user and per-session budgets
516
+
517
+ ```ts
518
+ const tracker = createTracker({
519
+ budgets: {
520
+ perUser: {
521
+ threshold: 1.00,
522
+ webhookUrl: 'https://hooks.slack.com/...',
523
+ mode: 'once', // default — fires once per user; use 'always' to fire on every call that exceeds
524
+ },
525
+ perSession: {
526
+ threshold: 0.10,
527
+ webhookUrl: 'https://hooks.slack.com/...',
528
+ },
529
+ },
530
+ })
531
+
532
+ await openai.chat.completions.create({
533
+ model: 'gpt-4o',
534
+ messages: [...],
535
+ __userId: 'user_123', // required for perUser alert to fire
536
+ __sessionId: 'sess_abc', // required for perSession alert to fire
537
+ })
538
+ ```
539
+
540
+ Budget webhook payload:
541
+ ```json
542
+ { "text": "[tokenwatch] Budget alert: user \"user_123\" reached $1.0031 USD (threshold: $1)" }
543
+ ```
544
+
329
545
  ---
330
546
 
331
547
  ## CLI
@@ -351,6 +567,10 @@ npx tokenwatch help # show help
351
567
 
352
568
  By user:
353
569
  user_123 $0.004231 (11 calls)
570
+
571
+ By feature:
572
+ chat $0.002500 (5 calls)
573
+ rag $0.001731 (6 calls)
354
574
  ───────────────────────────────────────────────────
355
575
  ```
356
576
 
@@ -367,11 +587,36 @@ Requires `storage: 'sqlite'` in your app and `better-sqlite3` installed.
367
587
 
368
588
  ---
369
589
 
590
+ ## TypeScript
591
+
592
+ `__sessionId`, `__userId`, and `__feature` are typed via the `TrackingMeta` interface, which is automatically merged into the `create` params type by the wrapper. In most setups they just work with no cast required.
593
+
594
+ If you hit a type error (e.g. with stricter SDK versions), use `as Record<string, unknown>`:
595
+
596
+ ```ts
597
+ await openai.chat.completions.create({
598
+ model: 'gpt-4o',
599
+ messages: [],
600
+ __sessionId: 'sess-1',
601
+ __feature: 'chat',
602
+ } as Record<string, unknown>)
603
+ ```
604
+
605
+ `TrackingMeta` is exported if you need to annotate your own helper types:
606
+
607
+ ```ts
608
+ import type { TrackingMeta } from '@diogonzafe/tokenwatch'
609
+
610
+ type MyParams = { model: string; messages: Message[] } & TrackingMeta
611
+ ```
612
+
613
+ ---
614
+
370
615
  ## Behaviour Guarantees
371
616
 
372
617
  - `__sessionId`, `__userId`, and `__feature` are **stripped before** the request reaches the API
373
618
  - The response object returned is **identical** to the original SDK response
374
- - `track()` is **synchronous and non-blocking** — zero latency added to API calls
619
+ - `track()` is **synchronous and non-blocking** — negligible sub-millisecond overhead; no proxy server or network hop
375
620
  - If the API call **fails**, no cost is recorded and the original error is re-thrown unchanged
376
621
  - Streaming is fully supported — usage is accumulated from the final stream event
377
622
  - Database writes from `record()` are **fire-and-forget** — a storage failure never interrupts your API call
package/dist/adapters.cjs CHANGED
@@ -32,33 +32,52 @@ var PostgresStorage = class {
32
32
  this.client = client;
33
33
  }
34
34
  client;
35
- /** Creates the `tokenwatch_usage` table if it does not already exist. */
35
+ /** Creates the `tokenwatch_usage` table if it does not already exist.
36
+ * Also adds new columns for databases created before v0.2.0 / v0.3.0. */
36
37
  async migrate() {
37
38
  await this.client.query(`
38
39
  CREATE TABLE IF NOT EXISTS tokenwatch_usage (
39
- id BIGSERIAL PRIMARY KEY,
40
- model TEXT NOT NULL,
41
- input_tokens INTEGER NOT NULL,
42
- output_tokens INTEGER NOT NULL,
43
- cost_usd NUMERIC NOT NULL,
44
- session_id TEXT,
45
- user_id TEXT,
46
- timestamp TIMESTAMPTZ NOT NULL
40
+ id BIGSERIAL PRIMARY KEY,
41
+ model TEXT NOT NULL,
42
+ input_tokens INTEGER NOT NULL,
43
+ output_tokens INTEGER NOT NULL,
44
+ reasoning_tokens INTEGER NOT NULL DEFAULT 0,
45
+ cached_tokens INTEGER NOT NULL DEFAULT 0,
46
+ cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
47
+ cost_usd NUMERIC NOT NULL,
48
+ session_id TEXT,
49
+ user_id TEXT,
50
+ feature TEXT,
51
+ timestamp TIMESTAMPTZ NOT NULL
47
52
  )
48
53
  `);
54
+ for (const col of [
55
+ "ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS reasoning_tokens INTEGER NOT NULL DEFAULT 0",
56
+ "ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS feature TEXT",
57
+ "ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS cached_tokens INTEGER NOT NULL DEFAULT 0",
58
+ "ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS cache_creation_tokens INTEGER NOT NULL DEFAULT 0"
59
+ ]) {
60
+ await this.client.query(col).catch(() => {
61
+ });
62
+ }
49
63
  }
50
64
  record(entry) {
51
65
  this.client.query(
52
66
  `INSERT INTO tokenwatch_usage
53
- (model, input_tokens, output_tokens, cost_usd, session_id, user_id, timestamp)
54
- VALUES ($1, $2, $3, $4, $5, $6, $7)`,
67
+ (model, input_tokens, output_tokens, reasoning_tokens, cached_tokens, cache_creation_tokens,
68
+ cost_usd, session_id, user_id, feature, timestamp)
69
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
55
70
  [
56
71
  entry.model,
57
72
  entry.inputTokens,
58
73
  entry.outputTokens,
74
+ entry.reasoningTokens ?? 0,
75
+ entry.cachedTokens ?? 0,
76
+ entry.cacheCreationTokens ?? 0,
59
77
  entry.costUSD,
60
78
  entry.sessionId ?? null,
61
79
  entry.userId ?? null,
80
+ entry.feature ?? null,
62
81
  entry.timestamp
63
82
  ]
64
83
  ).catch((err) => {
@@ -82,13 +101,20 @@ var PostgresStorage = class {
82
101
  }
83
102
  };
84
103
  function rowToEntry(r) {
104
+ const reasoningTokens = r["reasoning_tokens"] ?? 0;
105
+ const cachedTokens = r["cached_tokens"] ?? 0;
106
+ const cacheCreationTokens = r["cache_creation_tokens"] ?? 0;
85
107
  return {
86
108
  model: r["model"],
87
109
  inputTokens: r["input_tokens"],
88
110
  outputTokens: r["output_tokens"],
111
+ ...reasoningTokens > 0 && { reasoningTokens },
112
+ ...cachedTokens > 0 && { cachedTokens },
113
+ ...cacheCreationTokens > 0 && { cacheCreationTokens },
89
114
  costUSD: Number(r["cost_usd"]),
90
115
  ...r["session_id"] != null && { sessionId: r["session_id"] },
91
116
  ...r["user_id"] != null && { userId: r["user_id"] },
117
+ ...r["feature"] != null && { feature: r["feature"] },
92
118
  timestamp: r["timestamp"] instanceof Date ? r["timestamp"].toISOString() : r["timestamp"]
93
119
  };
94
120
  }
@@ -99,33 +125,51 @@ var MySQLStorage = class {
99
125
  this.client = client;
100
126
  }
101
127
  client;
102
- /** Creates the `tokenwatch_usage` table if it does not already exist. */
128
+ /** Creates the `tokenwatch_usage` table if it does not already exist.
129
+ * Also adds new columns for databases created before v0.2.0 / v0.3.0. */
103
130
  async migrate() {
104
131
  await this.client.execute(`
105
132
  CREATE TABLE IF NOT EXISTS tokenwatch_usage (
106
- id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
107
- model VARCHAR(255) NOT NULL,
108
- input_tokens INT NOT NULL,
109
- output_tokens INT NOT NULL,
110
- cost_usd DECIMAL(18,8) NOT NULL,
111
- session_id VARCHAR(255),
112
- user_id VARCHAR(255),
113
- timestamp DATETIME(3) NOT NULL
133
+ id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
134
+ model VARCHAR(255) NOT NULL,
135
+ input_tokens INT NOT NULL,
136
+ output_tokens INT NOT NULL,
137
+ reasoning_tokens INT NOT NULL DEFAULT 0,
138
+ cached_tokens INT NOT NULL DEFAULT 0,
139
+ cache_creation_tokens INT NOT NULL DEFAULT 0,
140
+ cost_usd DECIMAL(18,8) NOT NULL,
141
+ session_id VARCHAR(255),
142
+ user_id VARCHAR(255),
143
+ feature VARCHAR(255),
144
+ timestamp DATETIME(3) NOT NULL
114
145
  )
115
146
  `);
147
+ await this.client.execute(`
148
+ ALTER TABLE tokenwatch_usage
149
+ ADD COLUMN IF NOT EXISTS reasoning_tokens INT NOT NULL DEFAULT 0,
150
+ ADD COLUMN IF NOT EXISTS feature VARCHAR(255),
151
+ ADD COLUMN IF NOT EXISTS cached_tokens INT NOT NULL DEFAULT 0,
152
+ ADD COLUMN IF NOT EXISTS cache_creation_tokens INT NOT NULL DEFAULT 0
153
+ `).catch(() => {
154
+ });
116
155
  }
117
156
  record(entry) {
118
157
  this.client.execute(
119
158
  `INSERT INTO tokenwatch_usage
120
- (model, input_tokens, output_tokens, cost_usd, session_id, user_id, timestamp)
121
- VALUES (?, ?, ?, ?, ?, ?, ?)`,
159
+ (model, input_tokens, output_tokens, reasoning_tokens, cached_tokens, cache_creation_tokens,
160
+ cost_usd, session_id, user_id, feature, timestamp)
161
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
122
162
  [
123
163
  entry.model,
124
164
  entry.inputTokens,
125
165
  entry.outputTokens,
166
+ entry.reasoningTokens ?? 0,
167
+ entry.cachedTokens ?? 0,
168
+ entry.cacheCreationTokens ?? 0,
126
169
  entry.costUSD,
127
170
  entry.sessionId ?? null,
128
171
  entry.userId ?? null,
172
+ entry.feature ?? null,
129
173
  entry.timestamp
130
174
  ]
131
175
  ).catch((err) => {
@@ -149,13 +193,20 @@ var MySQLStorage = class {
149
193
  }
150
194
  };
151
195
  function rowToEntry2(r) {
196
+ const reasoningTokens = r["reasoning_tokens"] ?? 0;
197
+ const cachedTokens = r["cached_tokens"] ?? 0;
198
+ const cacheCreationTokens = r["cache_creation_tokens"] ?? 0;
152
199
  return {
153
200
  model: r["model"],
154
201
  inputTokens: r["input_tokens"],
155
202
  outputTokens: r["output_tokens"],
203
+ ...reasoningTokens > 0 && { reasoningTokens },
204
+ ...cachedTokens > 0 && { cachedTokens },
205
+ ...cacheCreationTokens > 0 && { cacheCreationTokens },
156
206
  costUSD: Number(r["cost_usd"]),
157
207
  ...r["session_id"] != null && { sessionId: r["session_id"] },
158
208
  ...r["user_id"] != null && { userId: r["user_id"] },
209
+ ...r["feature"] != null && { feature: r["feature"] },
159
210
  timestamp: r["timestamp"] instanceof Date ? r["timestamp"].toISOString() : r["timestamp"]
160
211
  };
161
212
  }
@@ -179,9 +230,13 @@ var MongoStorage = class {
179
230
  model: entry.model,
180
231
  inputTokens: entry.inputTokens,
181
232
  outputTokens: entry.outputTokens,
233
+ ...entry.reasoningTokens !== void 0 && { reasoningTokens: entry.reasoningTokens },
234
+ ...entry.cachedTokens !== void 0 && { cachedTokens: entry.cachedTokens },
235
+ ...entry.cacheCreationTokens !== void 0 && { cacheCreationTokens: entry.cacheCreationTokens },
182
236
  costUSD: entry.costUSD,
183
237
  sessionId: entry.sessionId ?? null,
184
238
  userId: entry.userId ?? null,
239
+ ...entry.feature !== void 0 && { feature: entry.feature },
185
240
  timestamp: entry.timestamp
186
241
  }).catch((err) => {
187
242
  console.warn("[tokenwatch] MongoStorage.record failed:", err);
@@ -203,9 +258,13 @@ function docToEntry(doc) {
203
258
  model: doc.model,
204
259
  inputTokens: doc.inputTokens,
205
260
  outputTokens: doc.outputTokens,
261
+ ...doc.reasoningTokens != null && doc.reasoningTokens > 0 && { reasoningTokens: doc.reasoningTokens },
262
+ ...doc.cachedTokens != null && doc.cachedTokens > 0 && { cachedTokens: doc.cachedTokens },
263
+ ...doc.cacheCreationTokens != null && doc.cacheCreationTokens > 0 && { cacheCreationTokens: doc.cacheCreationTokens },
206
264
  costUSD: doc.costUSD,
207
265
  ...doc.sessionId != null && { sessionId: doc.sessionId },
208
266
  ...doc.userId != null && { userId: doc.userId },
267
+ ...doc.feature != null && { feature: doc.feature },
209
268
  timestamp: doc.timestamp
210
269
  };
211
270
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/adapters/index.ts","../src/adapters/postgres.ts","../src/adapters/mysql.ts","../src/adapters/mongodb.ts"],"sourcesContent":["export { PostgresStorage } from './postgres.js'\nexport { MySQLStorage } from './mysql.js'\nexport { MongoStorage } from './mongodb.js'\n","import type { IStorage, UsageEntry } from '../types/index.js'\n\n/**\n * IStorage adapter for PostgreSQL using the `pg` driver.\n *\n * Install peer dep: npm install pg\n * Types (optional): npm install -D @types/pg\n *\n * @example\n * ```ts\n * import { Pool } from 'pg'\n * import { createTracker } from '@diogonzafe/tokenwatch'\n * import { PostgresStorage } from '@diogonzafe/tokenwatch/adapters'\n *\n * const pool = new Pool({ connectionString: process.env.DATABASE_URL })\n * const storage = new PostgresStorage(pool)\n * await storage.migrate() // create table if it doesn't exist\n *\n * const tracker = createTracker({ storage })\n * ```\n */\n\n// Minimal structural types so the adapter compiles without `pg` installed\ninterface QueryClient {\n query(sql: string, values?: unknown[]): Promise<{ rows: unknown[] }>\n}\n\nexport class PostgresStorage implements IStorage {\n constructor(private readonly client: QueryClient) {}\n\n /** Creates the `tokenwatch_usage` table if it does not already exist. */\n async migrate(): Promise<void> {\n await this.client.query(`\n CREATE TABLE IF NOT EXISTS tokenwatch_usage (\n id BIGSERIAL PRIMARY KEY,\n model TEXT NOT NULL,\n input_tokens INTEGER NOT NULL,\n output_tokens INTEGER NOT NULL,\n cost_usd NUMERIC NOT NULL,\n session_id TEXT,\n user_id TEXT,\n timestamp TIMESTAMPTZ NOT NULL\n )\n `)\n }\n\n record(entry: UsageEntry): void {\n this.client\n .query(\n `INSERT INTO tokenwatch_usage\n (model, input_tokens, output_tokens, cost_usd, session_id, user_id, timestamp)\n VALUES ($1, $2, $3, $4, $5, $6, $7)`,\n [\n entry.model,\n entry.inputTokens,\n entry.outputTokens,\n entry.costUSD,\n entry.sessionId ?? null,\n entry.userId ?? null,\n entry.timestamp,\n ],\n )\n .catch((err: unknown) => {\n console.warn('[tokenwatch] PostgresStorage.record failed:', err)\n })\n }\n\n async getAll(): Promise<UsageEntry[]> {\n const result = await this.client.query(\n 'SELECT * FROM tokenwatch_usage ORDER BY timestamp ASC',\n )\n return (result.rows as Array<Record<string, unknown>>).map(rowToEntry)\n }\n\n async clearAll(): Promise<void> {\n await this.client.query('DELETE FROM tokenwatch_usage')\n }\n\n async clearSession(sessionId: string): Promise<void> {\n await this.client.query(\n 'DELETE FROM tokenwatch_usage WHERE session_id = $1',\n [sessionId],\n )\n }\n}\n\nfunction rowToEntry(r: Record<string, unknown>): UsageEntry {\n return {\n model: r['model'] as string,\n inputTokens: r['input_tokens'] as number,\n outputTokens: r['output_tokens'] as number,\n costUSD: Number(r['cost_usd']),\n ...(r['session_id'] != null && { sessionId: r['session_id'] as string }),\n ...(r['user_id'] != null && { userId: r['user_id'] as string }),\n timestamp:\n r['timestamp'] instanceof Date\n ? (r['timestamp'] as Date).toISOString()\n : (r['timestamp'] as string),\n }\n}\n","import type { IStorage, UsageEntry } from '../types/index.js'\n\n/**\n * IStorage adapter for MySQL / MariaDB using the `mysql2` driver.\n *\n * Install peer dep: npm install mysql2\n *\n * @example\n * ```ts\n * import mysql from 'mysql2/promise'\n * import { createTracker } from '@diogonzafe/tokenwatch'\n * import { MySQLStorage } from '@diogonzafe/tokenwatch/adapters'\n *\n * const pool = mysql.createPool({ uri: process.env.MYSQL_URL })\n * const storage = new MySQLStorage(pool)\n * await storage.migrate() // create table if it doesn't exist\n *\n * const tracker = createTracker({ storage })\n * ```\n */\n\n// Minimal structural type so the adapter compiles without `mysql2` installed\ninterface QueryClient {\n execute(sql: string, values?: unknown[]): Promise<[unknown]>\n}\n\nexport class MySQLStorage implements IStorage {\n constructor(private readonly client: QueryClient) {}\n\n /** Creates the `tokenwatch_usage` table if it does not already exist. */\n async migrate(): Promise<void> {\n await this.client.execute(`\n CREATE TABLE IF NOT EXISTS tokenwatch_usage (\n id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,\n model VARCHAR(255) NOT NULL,\n input_tokens INT NOT NULL,\n output_tokens INT NOT NULL,\n cost_usd DECIMAL(18,8) NOT NULL,\n session_id VARCHAR(255),\n user_id VARCHAR(255),\n timestamp DATETIME(3) NOT NULL\n )\n `)\n }\n\n record(entry: UsageEntry): void {\n this.client\n .execute(\n `INSERT INTO tokenwatch_usage\n (model, input_tokens, output_tokens, cost_usd, session_id, user_id, timestamp)\n VALUES (?, ?, ?, ?, ?, ?, ?)`,\n [\n entry.model,\n entry.inputTokens,\n entry.outputTokens,\n entry.costUSD,\n entry.sessionId ?? null,\n entry.userId ?? null,\n entry.timestamp,\n ],\n )\n .catch((err: unknown) => {\n console.warn('[tokenwatch] MySQLStorage.record failed:', err)\n })\n }\n\n async getAll(): Promise<UsageEntry[]> {\n const [rows] = await this.client.execute(\n 'SELECT * FROM tokenwatch_usage ORDER BY timestamp ASC',\n )\n return (rows as Array<Record<string, unknown>>).map(rowToEntry)\n }\n\n async clearAll(): Promise<void> {\n await this.client.execute('DELETE FROM tokenwatch_usage')\n }\n\n async clearSession(sessionId: string): Promise<void> {\n await this.client.execute(\n 'DELETE FROM tokenwatch_usage WHERE session_id = ?',\n [sessionId],\n )\n }\n}\n\nfunction rowToEntry(r: Record<string, unknown>): UsageEntry {\n return {\n model: r['model'] as string,\n inputTokens: r['input_tokens'] as number,\n outputTokens: r['output_tokens'] as number,\n costUSD: Number(r['cost_usd']),\n ...(r['session_id'] != null && { sessionId: r['session_id'] as string }),\n ...(r['user_id'] != null && { userId: r['user_id'] as string }),\n timestamp:\n r['timestamp'] instanceof Date\n ? (r['timestamp'] as Date).toISOString()\n : (r['timestamp'] as string),\n }\n}\n","import type { IStorage, UsageEntry } from '../types/index.js'\n\n/**\n * IStorage adapter for MongoDB using the official `mongodb` driver.\n *\n * Install peer dep: npm install mongodb\n *\n * @example\n * ```ts\n * import { MongoClient } from 'mongodb'\n * import { createTracker } from '@diogonzafe/tokenwatch'\n * import { MongoStorage } from '@diogonzafe/tokenwatch/adapters'\n *\n * const client = new MongoClient(process.env.MONGO_URL!)\n * await client.connect()\n *\n * const storage = new MongoStorage(client.db('myapp'))\n * const tracker = createTracker({ storage })\n * ```\n *\n * Recommended index (run once at startup):\n * ```ts\n * await storage.createIndexes()\n * ```\n */\n\n// Minimal structural types so the adapter compiles without `mongodb` installed\ninterface MongoDocument {\n _id?: unknown\n model: string\n inputTokens: number\n outputTokens: number\n costUSD: number\n sessionId?: string | null\n userId?: string | null\n timestamp: string\n}\n\ninterface MongoCursor {\n sort(sort: Record<string, unknown>): MongoCursor\n toArray(): Promise<MongoDocument[]>\n}\n\ninterface Collection {\n insertOne(doc: MongoDocument): Promise<unknown>\n find(filter: Record<string, unknown>): MongoCursor\n deleteMany(filter: Record<string, unknown>): Promise<unknown>\n createIndex(index: Record<string, unknown>): Promise<unknown>\n}\n\ninterface Database {\n collection(name: string): Collection\n}\n\nconst COLLECTION = 'tokenwatch_usage'\n\nexport class MongoStorage implements IStorage {\n private readonly col: Collection\n\n constructor(db: Database) {\n this.col = db.collection(COLLECTION)\n }\n\n /** Creates recommended indexes for query performance. Call once at startup. */\n async createIndexes(): Promise<void> {\n await this.col.createIndex({ timestamp: 1 })\n await this.col.createIndex({ sessionId: 1 })\n await this.col.createIndex({ userId: 1 })\n await this.col.createIndex({ model: 1 })\n }\n\n record(entry: UsageEntry): void {\n this.col\n .insertOne({\n model: entry.model,\n inputTokens: entry.inputTokens,\n outputTokens: entry.outputTokens,\n costUSD: entry.costUSD,\n sessionId: entry.sessionId ?? null,\n userId: entry.userId ?? null,\n timestamp: entry.timestamp,\n })\n .catch((err: unknown) => {\n console.warn('[tokenwatch] MongoStorage.record failed:', err)\n })\n }\n\n async getAll(): Promise<UsageEntry[]> {\n const docs = await this.col.find({}).sort({ timestamp: 1 }).toArray()\n return docs.map(docToEntry)\n }\n\n async clearAll(): Promise<void> {\n await this.col.deleteMany({})\n }\n\n async clearSession(sessionId: string): Promise<void> {\n await this.col.deleteMany({ sessionId })\n }\n}\n\nfunction docToEntry(doc: MongoDocument): UsageEntry {\n return {\n model: doc.model,\n inputTokens: doc.inputTokens,\n outputTokens: doc.outputTokens,\n costUSD: doc.costUSD,\n ...(doc.sessionId != null && { sessionId: doc.sessionId }),\n ...(doc.userId != null && { userId: doc.userId }),\n timestamp: doc.timestamp,\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC2BO,IAAM,kBAAN,MAA0C;AAAA,EAC/C,YAA6B,QAAqB;AAArB;AAAA,EAAsB;AAAA,EAAtB;AAAA;AAAA,EAG7B,MAAM,UAAyB;AAC7B,UAAM,KAAK,OAAO,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAWvB;AAAA,EACH;AAAA,EAEA,OAAO,OAAyB;AAC9B,SAAK,OACF;AAAA,MACC;AAAA;AAAA;AAAA,MAGA;AAAA,QACE,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM,aAAa;AAAA,QACnB,MAAM,UAAU;AAAA,QAChB,MAAM;AAAA,MACR;AAAA,IACF,EACC,MAAM,CAAC,QAAiB;AACvB,cAAQ,KAAK,+CAA+C,GAAG;AAAA,IACjE,CAAC;AAAA,EACL;AAAA,EAEA,MAAM,SAAgC;AACpC,UAAM,SAAS,MAAM,KAAK,OAAO;AAAA,MAC/B;AAAA,IACF;AACA,WAAQ,OAAO,KAAwC,IAAI,UAAU;AAAA,EACvE;AAAA,EAEA,MAAM,WAA0B;AAC9B,UAAM,KAAK,OAAO,MAAM,8BAA8B;AAAA,EACxD;AAAA,EAEA,MAAM,aAAa,WAAkC;AACnD,UAAM,KAAK,OAAO;AAAA,MAChB;AAAA,MACA,CAAC,SAAS;AAAA,IACZ;AAAA,EACF;AACF;AAEA,SAAS,WAAW,GAAwC;AAC1D,SAAO;AAAA,IACL,OAAO,EAAE,OAAO;AAAA,IAChB,aAAa,EAAE,cAAc;AAAA,IAC7B,cAAc,EAAE,eAAe;AAAA,IAC/B,SAAS,OAAO,EAAE,UAAU,CAAC;AAAA,IAC7B,GAAI,EAAE,YAAY,KAAK,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAY;AAAA,IACtE,GAAI,EAAE,SAAS,KAAK,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAY;AAAA,IAC7D,WACE,EAAE,WAAW,aAAa,OACrB,EAAE,WAAW,EAAW,YAAY,IACpC,EAAE,WAAW;AAAA,EACtB;AACF;;;ACzEO,IAAM,eAAN,MAAuC;AAAA,EAC5C,YAA6B,QAAqB;AAArB;AAAA,EAAsB;AAAA,EAAtB;AAAA;AAAA,EAG7B,MAAM,UAAyB;AAC7B,UAAM,KAAK,OAAO,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAWzB;AAAA,EACH;AAAA,EAEA,OAAO,OAAyB;AAC9B,SAAK,OACF;AAAA,MACC;AAAA;AAAA;AAAA,MAGA;AAAA,QACE,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM,aAAa;AAAA,QACnB,MAAM,UAAU;AAAA,QAChB,MAAM;AAAA,MACR;AAAA,IACF,EACC,MAAM,CAAC,QAAiB;AACvB,cAAQ,KAAK,4CAA4C,GAAG;AAAA,IAC9D,CAAC;AAAA,EACL;AAAA,EAEA,MAAM,SAAgC;AACpC,UAAM,CAAC,IAAI,IAAI,MAAM,KAAK,OAAO;AAAA,MAC/B;AAAA,IACF;AACA,WAAQ,KAAwC,IAAIA,WAAU;AAAA,EAChE;AAAA,EAEA,MAAM,WAA0B;AAC9B,UAAM,KAAK,OAAO,QAAQ,8BAA8B;AAAA,EAC1D;AAAA,EAEA,MAAM,aAAa,WAAkC;AACnD,UAAM,KAAK,OAAO;AAAA,MAChB;AAAA,MACA,CAAC,SAAS;AAAA,IACZ;AAAA,EACF;AACF;AAEA,SAASA,YAAW,GAAwC;AAC1D,SAAO;AAAA,IACL,OAAO,EAAE,OAAO;AAAA,IAChB,aAAa,EAAE,cAAc;AAAA,IAC7B,cAAc,EAAE,eAAe;AAAA,IAC/B,SAAS,OAAO,EAAE,UAAU,CAAC;AAAA,IAC7B,GAAI,EAAE,YAAY,KAAK,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAY;AAAA,IACtE,GAAI,EAAE,SAAS,KAAK,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAY;AAAA,IAC7D,WACE,EAAE,WAAW,aAAa,OACrB,EAAE,WAAW,EAAW,YAAY,IACpC,EAAE,WAAW;AAAA,EACtB;AACF;;;AC5CA,IAAM,aAAa;AAEZ,IAAM,eAAN,MAAuC;AAAA,EAC3B;AAAA,EAEjB,YAAY,IAAc;AACxB,SAAK,MAAM,GAAG,WAAW,UAAU;AAAA,EACrC;AAAA;AAAA,EAGA,MAAM,gBAA+B;AACnC,UAAM,KAAK,IAAI,YAAY,EAAE,WAAW,EAAE,CAAC;AAC3C,UAAM,KAAK,IAAI,YAAY,EAAE,WAAW,EAAE,CAAC;AAC3C,UAAM,KAAK,IAAI,YAAY,EAAE,QAAQ,EAAE,CAAC;AACxC,UAAM,KAAK,IAAI,YAAY,EAAE,OAAO,EAAE,CAAC;AAAA,EACzC;AAAA,EAEA,OAAO,OAAyB;AAC9B,SAAK,IACF,UAAU;AAAA,MACT,OAAO,MAAM;AAAA,MACb,aAAa,MAAM;AAAA,MACnB,cAAc,MAAM;AAAA,MACpB,SAAS,MAAM;AAAA,MACf,WAAW,MAAM,aAAa;AAAA,MAC9B,QAAQ,MAAM,UAAU;AAAA,MACxB,WAAW,MAAM;AAAA,IACnB,CAAC,EACA,MAAM,CAAC,QAAiB;AACvB,cAAQ,KAAK,4CAA4C,GAAG;AAAA,IAC9D,CAAC;AAAA,EACL;AAAA,EAEA,MAAM,SAAgC;AACpC,UAAM,OAAO,MAAM,KAAK,IAAI,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,EAAE,QAAQ;AACpE,WAAO,KAAK,IAAI,UAAU;AAAA,EAC5B;AAAA,EAEA,MAAM,WAA0B;AAC9B,UAAM,KAAK,IAAI,WAAW,CAAC,CAAC;AAAA,EAC9B;AAAA,EAEA,MAAM,aAAa,WAAkC;AACnD,UAAM,KAAK,IAAI,WAAW,EAAE,UAAU,CAAC;AAAA,EACzC;AACF;AAEA,SAAS,WAAW,KAAgC;AAClD,SAAO;AAAA,IACL,OAAO,IAAI;AAAA,IACX,aAAa,IAAI;AAAA,IACjB,cAAc,IAAI;AAAA,IAClB,SAAS,IAAI;AAAA,IACb,GAAI,IAAI,aAAa,QAAQ,EAAE,WAAW,IAAI,UAAU;AAAA,IACxD,GAAI,IAAI,UAAU,QAAQ,EAAE,QAAQ,IAAI,OAAO;AAAA,IAC/C,WAAW,IAAI;AAAA,EACjB;AACF;","names":["rowToEntry"]}
1
+ {"version":3,"sources":["../src/adapters/index.ts","../src/adapters/postgres.ts","../src/adapters/mysql.ts","../src/adapters/mongodb.ts"],"sourcesContent":["export { PostgresStorage } from './postgres.js'\nexport { MySQLStorage } from './mysql.js'\nexport { MongoStorage } from './mongodb.js'\n","import type { IStorage, UsageEntry } from '../types/index.js'\n\n/**\n * IStorage adapter for PostgreSQL using the `pg` driver.\n *\n * Install peer dep: npm install pg\n * Types (optional): npm install -D @types/pg\n *\n * @example\n * ```ts\n * import { Pool } from 'pg'\n * import { createTracker } from '@diogonzafe/tokenwatch'\n * import { PostgresStorage } from '@diogonzafe/tokenwatch/adapters'\n *\n * const pool = new Pool({ connectionString: process.env.DATABASE_URL })\n * const storage = new PostgresStorage(pool)\n * await storage.migrate() // create table if it doesn't exist\n *\n * const tracker = createTracker({ storage })\n * ```\n */\n\n// Minimal structural types so the adapter compiles without `pg` installed\ninterface QueryClient {\n query(sql: string, values?: unknown[]): Promise<{ rows: unknown[] }>\n}\n\nexport class PostgresStorage implements IStorage {\n constructor(private readonly client: QueryClient) {}\n\n /** Creates the `tokenwatch_usage` table if it does not already exist.\n * Also adds new columns for databases created before v0.2.0 / v0.3.0. */\n async migrate(): Promise<void> {\n await this.client.query(`\n CREATE TABLE IF NOT EXISTS tokenwatch_usage (\n id BIGSERIAL PRIMARY KEY,\n model TEXT NOT NULL,\n input_tokens INTEGER NOT NULL,\n output_tokens INTEGER NOT NULL,\n reasoning_tokens INTEGER NOT NULL DEFAULT 0,\n cached_tokens INTEGER NOT NULL DEFAULT 0,\n cache_creation_tokens INTEGER NOT NULL DEFAULT 0,\n cost_usd NUMERIC NOT NULL,\n session_id TEXT,\n user_id TEXT,\n feature TEXT,\n timestamp TIMESTAMPTZ NOT NULL\n )\n `)\n // Incremental migrations for databases created before v0.2.0 / v0.3.0\n for (const col of [\n 'ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS reasoning_tokens INTEGER NOT NULL DEFAULT 0',\n 'ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS feature TEXT',\n 'ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS cached_tokens INTEGER NOT NULL DEFAULT 0',\n 'ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS cache_creation_tokens INTEGER NOT NULL DEFAULT 0',\n ]) {\n await this.client.query(col).catch(() => { /* column already exists */ })\n }\n }\n\n record(entry: UsageEntry): void {\n this.client\n .query(\n `INSERT INTO tokenwatch_usage\n (model, input_tokens, output_tokens, reasoning_tokens, cached_tokens, cache_creation_tokens,\n cost_usd, session_id, user_id, feature, timestamp)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,\n [\n entry.model,\n entry.inputTokens,\n entry.outputTokens,\n entry.reasoningTokens ?? 0,\n entry.cachedTokens ?? 0,\n entry.cacheCreationTokens ?? 0,\n entry.costUSD,\n entry.sessionId ?? null,\n entry.userId ?? null,\n entry.feature ?? null,\n entry.timestamp,\n ],\n )\n .catch((err: unknown) => {\n console.warn('[tokenwatch] PostgresStorage.record failed:', err)\n })\n }\n\n async getAll(): Promise<UsageEntry[]> {\n const result = await this.client.query(\n 'SELECT * FROM tokenwatch_usage ORDER BY timestamp ASC',\n )\n return (result.rows as Array<Record<string, unknown>>).map(rowToEntry)\n }\n\n async clearAll(): Promise<void> {\n await this.client.query('DELETE FROM tokenwatch_usage')\n }\n\n async clearSession(sessionId: string): Promise<void> {\n await this.client.query(\n 'DELETE FROM tokenwatch_usage WHERE session_id = $1',\n [sessionId],\n )\n }\n}\n\nfunction rowToEntry(r: Record<string, unknown>): UsageEntry {\n const reasoningTokens = (r['reasoning_tokens'] as number | null) ?? 0\n const cachedTokens = (r['cached_tokens'] as number | null) ?? 0\n const cacheCreationTokens = (r['cache_creation_tokens'] as number | null) ?? 0\n return {\n model: r['model'] as string,\n inputTokens: r['input_tokens'] as number,\n outputTokens: r['output_tokens'] as number,\n ...(reasoningTokens > 0 && { reasoningTokens }),\n ...(cachedTokens > 0 && { cachedTokens }),\n ...(cacheCreationTokens > 0 && { cacheCreationTokens }),\n costUSD: Number(r['cost_usd']),\n ...(r['session_id'] != null && { sessionId: r['session_id'] as string }),\n ...(r['user_id'] != null && { userId: r['user_id'] as string }),\n ...(r['feature'] != null && { feature: r['feature'] as string }),\n timestamp:\n r['timestamp'] instanceof Date\n ? (r['timestamp'] as Date).toISOString()\n : (r['timestamp'] as string),\n }\n}\n","import type { IStorage, UsageEntry } from '../types/index.js'\n\n/**\n * IStorage adapter for MySQL / MariaDB using the `mysql2` driver.\n *\n * Install peer dep: npm install mysql2\n *\n * @example\n * ```ts\n * import mysql from 'mysql2/promise'\n * import { createTracker } from '@diogonzafe/tokenwatch'\n * import { MySQLStorage } from '@diogonzafe/tokenwatch/adapters'\n *\n * const pool = mysql.createPool({ uri: process.env.MYSQL_URL })\n * const storage = new MySQLStorage(pool)\n * await storage.migrate() // create table if it doesn't exist\n *\n * const tracker = createTracker({ storage })\n * ```\n */\n\n// Minimal structural type so the adapter compiles without `mysql2` installed\ninterface QueryClient {\n execute(sql: string, values?: unknown[]): Promise<[unknown]>\n}\n\nexport class MySQLStorage implements IStorage {\n constructor(private readonly client: QueryClient) {}\n\n /** Creates the `tokenwatch_usage` table if it does not already exist.\n * Also adds new columns for databases created before v0.2.0 / v0.3.0. */\n async migrate(): Promise<void> {\n await this.client.execute(`\n CREATE TABLE IF NOT EXISTS tokenwatch_usage (\n id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,\n model VARCHAR(255) NOT NULL,\n input_tokens INT NOT NULL,\n output_tokens INT NOT NULL,\n reasoning_tokens INT NOT NULL DEFAULT 0,\n cached_tokens INT NOT NULL DEFAULT 0,\n cache_creation_tokens INT NOT NULL DEFAULT 0,\n cost_usd DECIMAL(18,8) NOT NULL,\n session_id VARCHAR(255),\n user_id VARCHAR(255),\n feature VARCHAR(255),\n timestamp DATETIME(3) NOT NULL\n )\n `)\n // Incremental migrations for databases created before v0.2.0 / v0.3.0\n await this.client.execute(`\n ALTER TABLE tokenwatch_usage\n ADD COLUMN IF NOT EXISTS reasoning_tokens INT NOT NULL DEFAULT 0,\n ADD COLUMN IF NOT EXISTS feature VARCHAR(255),\n ADD COLUMN IF NOT EXISTS cached_tokens INT NOT NULL DEFAULT 0,\n ADD COLUMN IF NOT EXISTS cache_creation_tokens INT NOT NULL DEFAULT 0\n `).catch(() => { /* MySQL < 8.0 may not support IF NOT EXISTS — ignore if columns already exist */ })\n }\n\n record(entry: UsageEntry): void {\n this.client\n .execute(\n `INSERT INTO tokenwatch_usage\n (model, input_tokens, output_tokens, reasoning_tokens, cached_tokens, cache_creation_tokens,\n cost_usd, session_id, user_id, feature, timestamp)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [\n entry.model,\n entry.inputTokens,\n entry.outputTokens,\n entry.reasoningTokens ?? 0,\n entry.cachedTokens ?? 0,\n entry.cacheCreationTokens ?? 0,\n entry.costUSD,\n entry.sessionId ?? null,\n entry.userId ?? null,\n entry.feature ?? null,\n entry.timestamp,\n ],\n )\n .catch((err: unknown) => {\n console.warn('[tokenwatch] MySQLStorage.record failed:', err)\n })\n }\n\n async getAll(): Promise<UsageEntry[]> {\n const [rows] = await this.client.execute(\n 'SELECT * FROM tokenwatch_usage ORDER BY timestamp ASC',\n )\n return (rows as Array<Record<string, unknown>>).map(rowToEntry)\n }\n\n async clearAll(): Promise<void> {\n await this.client.execute('DELETE FROM tokenwatch_usage')\n }\n\n async clearSession(sessionId: string): Promise<void> {\n await this.client.execute(\n 'DELETE FROM tokenwatch_usage WHERE session_id = ?',\n [sessionId],\n )\n }\n}\n\nfunction rowToEntry(r: Record<string, unknown>): UsageEntry {\n const reasoningTokens = (r['reasoning_tokens'] as number | null) ?? 0\n const cachedTokens = (r['cached_tokens'] as number | null) ?? 0\n const cacheCreationTokens = (r['cache_creation_tokens'] as number | null) ?? 0\n return {\n model: r['model'] as string,\n inputTokens: r['input_tokens'] as number,\n outputTokens: r['output_tokens'] as number,\n ...(reasoningTokens > 0 && { reasoningTokens }),\n ...(cachedTokens > 0 && { cachedTokens }),\n ...(cacheCreationTokens > 0 && { cacheCreationTokens }),\n costUSD: Number(r['cost_usd']),\n ...(r['session_id'] != null && { sessionId: r['session_id'] as string }),\n ...(r['user_id'] != null && { userId: r['user_id'] as string }),\n ...(r['feature'] != null && { feature: r['feature'] as string }),\n timestamp:\n r['timestamp'] instanceof Date\n ? (r['timestamp'] as Date).toISOString()\n : (r['timestamp'] as string),\n }\n}\n","import type { IStorage, UsageEntry } from '../types/index.js'\n\n/**\n * IStorage adapter for MongoDB using the official `mongodb` driver.\n *\n * Install peer dep: npm install mongodb\n *\n * @example\n * ```ts\n * import { MongoClient } from 'mongodb'\n * import { createTracker } from '@diogonzafe/tokenwatch'\n * import { MongoStorage } from '@diogonzafe/tokenwatch/adapters'\n *\n * const client = new MongoClient(process.env.MONGO_URL!)\n * await client.connect()\n *\n * const storage = new MongoStorage(client.db('myapp'))\n * const tracker = createTracker({ storage })\n * ```\n *\n * Recommended index (run once at startup):\n * ```ts\n * await storage.createIndexes()\n * ```\n */\n\n// Minimal structural types so the adapter compiles without `mongodb` installed\ninterface MongoDocument {\n _id?: unknown\n model: string\n inputTokens: number\n outputTokens: number\n reasoningTokens?: number\n cachedTokens?: number\n cacheCreationTokens?: number\n costUSD: number\n sessionId?: string | null\n userId?: string | null\n feature?: string | null\n timestamp: string\n}\n\ninterface MongoCursor {\n sort(sort: Record<string, unknown>): MongoCursor\n toArray(): Promise<MongoDocument[]>\n}\n\ninterface Collection {\n insertOne(doc: MongoDocument): Promise<unknown>\n find(filter: Record<string, unknown>): MongoCursor\n deleteMany(filter: Record<string, unknown>): Promise<unknown>\n createIndex(index: Record<string, unknown>): Promise<unknown>\n}\n\ninterface Database {\n collection(name: string): Collection\n}\n\nconst COLLECTION = 'tokenwatch_usage'\n\nexport class MongoStorage implements IStorage {\n private readonly col: Collection\n\n constructor(db: Database) {\n this.col = db.collection(COLLECTION)\n }\n\n /** Creates recommended indexes for query performance. Call once at startup. */\n async createIndexes(): Promise<void> {\n await this.col.createIndex({ timestamp: 1 })\n await this.col.createIndex({ sessionId: 1 })\n await this.col.createIndex({ userId: 1 })\n await this.col.createIndex({ model: 1 })\n }\n\n record(entry: UsageEntry): void {\n this.col\n .insertOne({\n model: entry.model,\n inputTokens: entry.inputTokens,\n outputTokens: entry.outputTokens,\n ...(entry.reasoningTokens !== undefined && { reasoningTokens: entry.reasoningTokens }),\n ...(entry.cachedTokens !== undefined && { cachedTokens: entry.cachedTokens }),\n ...(entry.cacheCreationTokens !== undefined && { cacheCreationTokens: entry.cacheCreationTokens }),\n costUSD: entry.costUSD,\n sessionId: entry.sessionId ?? null,\n userId: entry.userId ?? null,\n ...(entry.feature !== undefined && { feature: entry.feature }),\n timestamp: entry.timestamp,\n })\n .catch((err: unknown) => {\n console.warn('[tokenwatch] MongoStorage.record failed:', err)\n })\n }\n\n async getAll(): Promise<UsageEntry[]> {\n const docs = await this.col.find({}).sort({ timestamp: 1 }).toArray()\n return docs.map(docToEntry)\n }\n\n async clearAll(): Promise<void> {\n await this.col.deleteMany({})\n }\n\n async clearSession(sessionId: string): Promise<void> {\n await this.col.deleteMany({ sessionId })\n }\n}\n\nfunction docToEntry(doc: MongoDocument): UsageEntry {\n return {\n model: doc.model,\n inputTokens: doc.inputTokens,\n outputTokens: doc.outputTokens,\n ...(doc.reasoningTokens != null && doc.reasoningTokens > 0 && { reasoningTokens: doc.reasoningTokens }),\n ...(doc.cachedTokens != null && doc.cachedTokens > 0 && { cachedTokens: doc.cachedTokens }),\n ...(doc.cacheCreationTokens != null && doc.cacheCreationTokens > 0 && { cacheCreationTokens: doc.cacheCreationTokens }),\n costUSD: doc.costUSD,\n ...(doc.sessionId != null && { sessionId: doc.sessionId }),\n ...(doc.userId != null && { userId: doc.userId }),\n ...(doc.feature != null && { feature: doc.feature }),\n timestamp: doc.timestamp,\n }\n}\n"],"mappings":"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","names":["rowToEntry"]}