@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 +260 -15
- package/dist/adapters.cjs +81 -22
- package/dist/adapters.cjs.map +1 -1
- package/dist/adapters.d.cts +9 -3
- package/dist/adapters.d.ts +9 -3
- package/dist/adapters.js +81 -22
- package/dist/adapters.js.map +1 -1
- package/dist/cli.js +469 -50
- package/dist/cli.js.map +1 -1
- package/dist/{index-BQZaFcHQ.d.ts → index-CJKk1hHw.d.cts} +62 -2
- package/dist/{index-BQZaFcHQ.d.cts → index-CJKk1hHw.d.ts} +62 -2
- package/dist/index.cjs +559 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -3
- package/dist/index.d.ts +20 -3
- package/dist/index.js +558 -70
- package/dist/index.js.map +1 -1
- package/dist/langchain.cjs +62 -0
- package/dist/langchain.cjs.map +1 -0
- package/dist/langchain.d.cts +68 -0
- package/dist/langchain.d.ts +68 -0
- package/dist/langchain.js +35 -0
- package/dist/langchain.js.map +1 -0
- package/package.json +11 -3
- package/prices.json +167 -3
- package/dist/cli.cjs +0 -1639
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.d.cts +0 -1
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
|
|
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
|
-
|
|
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':
|
|
168
|
-
// 'o3':
|
|
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
|
|
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,
|
|
210
|
-
"claude-sonnet-4-6": { "input": 3.00,
|
|
211
|
-
"gemini-2.5-pro": { "input": 1.25,
|
|
212
|
-
"deepseek-chat": { "input": 0.28,
|
|
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** —
|
|
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
|
|
40
|
-
model
|
|
41
|
-
input_tokens
|
|
42
|
-
output_tokens
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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,
|
|
54
|
-
|
|
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
|
|
107
|
-
model
|
|
108
|
-
input_tokens
|
|
109
|
-
output_tokens
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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,
|
|
121
|
-
|
|
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
|
}
|
package/dist/adapters.cjs.map
CHANGED
|
@@ -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"]}
|