@diogonzafe/tokenwatch 0.1.7 → 0.1.9

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.
@@ -0,0 +1,189 @@
1
+ // src/adapters/postgres.ts
2
+ var PostgresStorage = class {
3
+ constructor(client) {
4
+ this.client = client;
5
+ }
6
+ client;
7
+ /** Creates the `tokenwatch_usage` table if it does not already exist. */
8
+ async migrate() {
9
+ await this.client.query(`
10
+ CREATE TABLE IF NOT EXISTS tokenwatch_usage (
11
+ id BIGSERIAL PRIMARY KEY,
12
+ model TEXT NOT NULL,
13
+ input_tokens INTEGER NOT NULL,
14
+ output_tokens INTEGER NOT NULL,
15
+ cost_usd NUMERIC NOT NULL,
16
+ session_id TEXT,
17
+ user_id TEXT,
18
+ timestamp TIMESTAMPTZ NOT NULL
19
+ )
20
+ `);
21
+ }
22
+ record(entry) {
23
+ this.client.query(
24
+ `INSERT INTO tokenwatch_usage
25
+ (model, input_tokens, output_tokens, cost_usd, session_id, user_id, timestamp)
26
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`,
27
+ [
28
+ entry.model,
29
+ entry.inputTokens,
30
+ entry.outputTokens,
31
+ entry.costUSD,
32
+ entry.sessionId ?? null,
33
+ entry.userId ?? null,
34
+ entry.timestamp
35
+ ]
36
+ ).catch((err) => {
37
+ console.warn("[tokenwatch] PostgresStorage.record failed:", err);
38
+ });
39
+ }
40
+ async getAll() {
41
+ const result = await this.client.query(
42
+ "SELECT * FROM tokenwatch_usage ORDER BY timestamp ASC"
43
+ );
44
+ return result.rows.map(rowToEntry);
45
+ }
46
+ async clearAll() {
47
+ await this.client.query("DELETE FROM tokenwatch_usage");
48
+ }
49
+ async clearSession(sessionId) {
50
+ await this.client.query(
51
+ "DELETE FROM tokenwatch_usage WHERE session_id = $1",
52
+ [sessionId]
53
+ );
54
+ }
55
+ };
56
+ function rowToEntry(r) {
57
+ return {
58
+ model: r["model"],
59
+ inputTokens: r["input_tokens"],
60
+ outputTokens: r["output_tokens"],
61
+ costUSD: Number(r["cost_usd"]),
62
+ ...r["session_id"] != null && { sessionId: r["session_id"] },
63
+ ...r["user_id"] != null && { userId: r["user_id"] },
64
+ timestamp: r["timestamp"] instanceof Date ? r["timestamp"].toISOString() : r["timestamp"]
65
+ };
66
+ }
67
+
68
+ // src/adapters/mysql.ts
69
+ var MySQLStorage = class {
70
+ constructor(client) {
71
+ this.client = client;
72
+ }
73
+ client;
74
+ /** Creates the `tokenwatch_usage` table if it does not already exist. */
75
+ async migrate() {
76
+ await this.client.execute(`
77
+ CREATE TABLE IF NOT EXISTS tokenwatch_usage (
78
+ id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
79
+ model VARCHAR(255) NOT NULL,
80
+ input_tokens INT NOT NULL,
81
+ output_tokens INT NOT NULL,
82
+ cost_usd DECIMAL(18,8) NOT NULL,
83
+ session_id VARCHAR(255),
84
+ user_id VARCHAR(255),
85
+ timestamp DATETIME(3) NOT NULL
86
+ )
87
+ `);
88
+ }
89
+ record(entry) {
90
+ this.client.execute(
91
+ `INSERT INTO tokenwatch_usage
92
+ (model, input_tokens, output_tokens, cost_usd, session_id, user_id, timestamp)
93
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
94
+ [
95
+ entry.model,
96
+ entry.inputTokens,
97
+ entry.outputTokens,
98
+ entry.costUSD,
99
+ entry.sessionId ?? null,
100
+ entry.userId ?? null,
101
+ entry.timestamp
102
+ ]
103
+ ).catch((err) => {
104
+ console.warn("[tokenwatch] MySQLStorage.record failed:", err);
105
+ });
106
+ }
107
+ async getAll() {
108
+ const [rows] = await this.client.execute(
109
+ "SELECT * FROM tokenwatch_usage ORDER BY timestamp ASC"
110
+ );
111
+ return rows.map(rowToEntry2);
112
+ }
113
+ async clearAll() {
114
+ await this.client.execute("DELETE FROM tokenwatch_usage");
115
+ }
116
+ async clearSession(sessionId) {
117
+ await this.client.execute(
118
+ "DELETE FROM tokenwatch_usage WHERE session_id = ?",
119
+ [sessionId]
120
+ );
121
+ }
122
+ };
123
+ function rowToEntry2(r) {
124
+ return {
125
+ model: r["model"],
126
+ inputTokens: r["input_tokens"],
127
+ outputTokens: r["output_tokens"],
128
+ costUSD: Number(r["cost_usd"]),
129
+ ...r["session_id"] != null && { sessionId: r["session_id"] },
130
+ ...r["user_id"] != null && { userId: r["user_id"] },
131
+ timestamp: r["timestamp"] instanceof Date ? r["timestamp"].toISOString() : r["timestamp"]
132
+ };
133
+ }
134
+
135
+ // src/adapters/mongodb.ts
136
+ var COLLECTION = "tokenwatch_usage";
137
+ var MongoStorage = class {
138
+ col;
139
+ constructor(db) {
140
+ this.col = db.collection(COLLECTION);
141
+ }
142
+ /** Creates recommended indexes for query performance. Call once at startup. */
143
+ async createIndexes() {
144
+ await this.col.createIndex({ timestamp: 1 });
145
+ await this.col.createIndex({ sessionId: 1 });
146
+ await this.col.createIndex({ userId: 1 });
147
+ await this.col.createIndex({ model: 1 });
148
+ }
149
+ record(entry) {
150
+ this.col.insertOne({
151
+ model: entry.model,
152
+ inputTokens: entry.inputTokens,
153
+ outputTokens: entry.outputTokens,
154
+ costUSD: entry.costUSD,
155
+ sessionId: entry.sessionId ?? null,
156
+ userId: entry.userId ?? null,
157
+ timestamp: entry.timestamp
158
+ }).catch((err) => {
159
+ console.warn("[tokenwatch] MongoStorage.record failed:", err);
160
+ });
161
+ }
162
+ async getAll() {
163
+ const docs = await this.col.find({}).toArray();
164
+ return docs.map(docToEntry);
165
+ }
166
+ async clearAll() {
167
+ await this.col.deleteMany({});
168
+ }
169
+ async clearSession(sessionId) {
170
+ await this.col.deleteMany({ sessionId });
171
+ }
172
+ };
173
+ function docToEntry(doc) {
174
+ return {
175
+ model: doc.model,
176
+ inputTokens: doc.inputTokens,
177
+ outputTokens: doc.outputTokens,
178
+ costUSD: doc.costUSD,
179
+ ...doc.sessionId != null && { sessionId: doc.sessionId },
180
+ ...doc.userId != null && { userId: doc.userId },
181
+ timestamp: doc.timestamp
182
+ };
183
+ }
184
+ export {
185
+ MongoStorage,
186
+ MySQLStorage,
187
+ PostgresStorage
188
+ };
189
+ //# sourceMappingURL=adapters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/postgres.ts","../src/adapters/mysql.ts","../src/adapters/mongodb.ts"],"sourcesContent":["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 Collection {\n insertOne(doc: MongoDocument): Promise<unknown>\n find(filter: Record<string, unknown>): { toArray(): Promise<MongoDocument[]> }\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({}).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":"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","names":["rowToEntry"]}
@@ -0,0 +1,89 @@
1
+ interface ModelPrice {
2
+ /** USD per 1 million input tokens */
3
+ input: number;
4
+ /** USD per 1 million output tokens */
5
+ output: number;
6
+ /** Maximum context window (input tokens) for this model */
7
+ maxInputTokens?: number;
8
+ }
9
+ type PriceMap = Record<string, ModelPrice>;
10
+ interface PricesFile {
11
+ updated_at: string;
12
+ source: string;
13
+ models: PriceMap;
14
+ }
15
+ interface TrackerConfig {
16
+ /** 'memory' (default), 'sqlite', or a custom IStorage instance (e.g. PostgresStorage, MySQLStorage, MongoStorage) */
17
+ storage?: 'memory' | 'sqlite' | IStorage;
18
+ /** USD threshold — fires webhookUrl when totalCostUSD exceeds this */
19
+ alertThreshold?: number;
20
+ /** Discord / Slack / generic webhook URL */
21
+ webhookUrl?: string;
22
+ /** Fetch fresh prices from remote GitHub source (default: true) */
23
+ syncPrices?: boolean;
24
+ /** Per-model price overrides — highest priority */
25
+ customPrices?: PriceMap;
26
+ }
27
+ interface UsageEntry {
28
+ model: string;
29
+ inputTokens: number;
30
+ outputTokens: number;
31
+ costUSD: number;
32
+ sessionId?: string;
33
+ userId?: string;
34
+ timestamp: string;
35
+ }
36
+ interface ModelStats {
37
+ costUSD: number;
38
+ calls: number;
39
+ tokens: {
40
+ input: number;
41
+ output: number;
42
+ };
43
+ }
44
+ interface SessionStats {
45
+ costUSD: number;
46
+ calls: number;
47
+ }
48
+ interface UserStats {
49
+ costUSD: number;
50
+ calls: number;
51
+ }
52
+ interface Report {
53
+ totalCostUSD: number;
54
+ totalTokens: {
55
+ input: number;
56
+ output: number;
57
+ };
58
+ byModel: Record<string, ModelStats>;
59
+ bySession: Record<string, SessionStats>;
60
+ byUser: Record<string, UserStats>;
61
+ period: {
62
+ from: string;
63
+ to: string;
64
+ };
65
+ }
66
+ interface IStorage {
67
+ /** Fire-and-forget — implementations may write async and swallow errors internally */
68
+ record(entry: UsageEntry): void | Promise<void>;
69
+ getAll(): UsageEntry[] | Promise<UsageEntry[]>;
70
+ clearAll(): void | Promise<void>;
71
+ clearSession(sessionId: string): void | Promise<void>;
72
+ }
73
+ interface Tracker {
74
+ /** Accumulate a usage entry (called by providers) */
75
+ track(entry: Omit<UsageEntry, 'costUSD' | 'timestamp'>): void;
76
+ getReport(): Promise<Report>;
77
+ reset(): Promise<void>;
78
+ resetSession(sessionId: string): Promise<void>;
79
+ exportJSON(): Promise<string>;
80
+ exportCSV(): Promise<string>;
81
+ /** Returns price and context window info for a model, or null if unknown */
82
+ getModelInfo(model: string): ModelPrice | null;
83
+ }
84
+ interface TrackingMeta {
85
+ __sessionId?: string;
86
+ __userId?: string;
87
+ }
88
+
89
+ export type { IStorage as I, ModelPrice as M, PriceMap as P, Report as R, SessionStats as S, TrackerConfig as T, UsageEntry as U, Tracker as a, TrackingMeta as b, ModelStats as c, PricesFile as d, UserStats as e };
@@ -0,0 +1,89 @@
1
+ interface ModelPrice {
2
+ /** USD per 1 million input tokens */
3
+ input: number;
4
+ /** USD per 1 million output tokens */
5
+ output: number;
6
+ /** Maximum context window (input tokens) for this model */
7
+ maxInputTokens?: number;
8
+ }
9
+ type PriceMap = Record<string, ModelPrice>;
10
+ interface PricesFile {
11
+ updated_at: string;
12
+ source: string;
13
+ models: PriceMap;
14
+ }
15
+ interface TrackerConfig {
16
+ /** 'memory' (default), 'sqlite', or a custom IStorage instance (e.g. PostgresStorage, MySQLStorage, MongoStorage) */
17
+ storage?: 'memory' | 'sqlite' | IStorage;
18
+ /** USD threshold — fires webhookUrl when totalCostUSD exceeds this */
19
+ alertThreshold?: number;
20
+ /** Discord / Slack / generic webhook URL */
21
+ webhookUrl?: string;
22
+ /** Fetch fresh prices from remote GitHub source (default: true) */
23
+ syncPrices?: boolean;
24
+ /** Per-model price overrides — highest priority */
25
+ customPrices?: PriceMap;
26
+ }
27
+ interface UsageEntry {
28
+ model: string;
29
+ inputTokens: number;
30
+ outputTokens: number;
31
+ costUSD: number;
32
+ sessionId?: string;
33
+ userId?: string;
34
+ timestamp: string;
35
+ }
36
+ interface ModelStats {
37
+ costUSD: number;
38
+ calls: number;
39
+ tokens: {
40
+ input: number;
41
+ output: number;
42
+ };
43
+ }
44
+ interface SessionStats {
45
+ costUSD: number;
46
+ calls: number;
47
+ }
48
+ interface UserStats {
49
+ costUSD: number;
50
+ calls: number;
51
+ }
52
+ interface Report {
53
+ totalCostUSD: number;
54
+ totalTokens: {
55
+ input: number;
56
+ output: number;
57
+ };
58
+ byModel: Record<string, ModelStats>;
59
+ bySession: Record<string, SessionStats>;
60
+ byUser: Record<string, UserStats>;
61
+ period: {
62
+ from: string;
63
+ to: string;
64
+ };
65
+ }
66
+ interface IStorage {
67
+ /** Fire-and-forget — implementations may write async and swallow errors internally */
68
+ record(entry: UsageEntry): void | Promise<void>;
69
+ getAll(): UsageEntry[] | Promise<UsageEntry[]>;
70
+ clearAll(): void | Promise<void>;
71
+ clearSession(sessionId: string): void | Promise<void>;
72
+ }
73
+ interface Tracker {
74
+ /** Accumulate a usage entry (called by providers) */
75
+ track(entry: Omit<UsageEntry, 'costUSD' | 'timestamp'>): void;
76
+ getReport(): Promise<Report>;
77
+ reset(): Promise<void>;
78
+ resetSession(sessionId: string): Promise<void>;
79
+ exportJSON(): Promise<string>;
80
+ exportCSV(): Promise<string>;
81
+ /** Returns price and context window info for a model, or null if unknown */
82
+ getModelInfo(model: string): ModelPrice | null;
83
+ }
84
+ interface TrackingMeta {
85
+ __sessionId?: string;
86
+ __userId?: string;
87
+ }
88
+
89
+ export type { IStorage as I, ModelPrice as M, PriceMap as P, Report as R, SessionStats as S, TrackerConfig as T, UsageEntry as U, Tracker as a, TrackingMeta as b, ModelStats as c, PricesFile as d, UserStats as e };
package/dist/index.cjs CHANGED
@@ -1369,7 +1369,9 @@ var ModelPriceSchema = import_zod.z.object({
1369
1369
  maxInputTokens: import_zod.z.number().positive().optional()
1370
1370
  });
1371
1371
  var TrackerConfigSchema = import_zod.z.object({
1372
- storage: import_zod.z.enum(["memory", "sqlite"]).optional().default("memory"),
1372
+ storage: import_zod.z.union([import_zod.z.enum(["memory", "sqlite"]), import_zod.z.custom((v) => {
1373
+ return v !== null && typeof v === "object" && typeof v.record === "function" && typeof v.getAll === "function" && typeof v.clearAll === "function" && typeof v.clearSession === "function";
1374
+ })]).optional().default("memory"),
1373
1375
  alertThreshold: import_zod.z.number().positive().optional(),
1374
1376
  webhookUrl: import_zod.z.string().url().optional(),
1375
1377
  syncPrices: import_zod.z.boolean().optional().default(true),
@@ -1383,13 +1385,13 @@ function createTracker(config = {}) {
1383
1385
  ${issues}`);
1384
1386
  }
1385
1387
  const {
1386
- storage: storageType,
1388
+ storage: storageOption,
1387
1389
  alertThreshold,
1388
1390
  webhookUrl,
1389
1391
  syncPrices,
1390
1392
  customPrices
1391
1393
  } = parsed.data;
1392
- const storage = createStorage(storageType);
1394
+ const storage = typeof storageOption === "object" ? storageOption : createStorage(storageOption);
1393
1395
  let remotePrices;
1394
1396
  if (syncPrices) {
1395
1397
  getRemotePrices().then((result) => {
@@ -1419,9 +1421,13 @@ ${issues}`);
1419
1421
  }
1420
1422
  function maybeFireAlert() {
1421
1423
  if (!alertThreshold || !webhookUrl || alertFired) return;
1422
- const total = computeTotal(storage.getAll());
1423
- if (total >= alertThreshold) {
1424
- alertFired = true;
1424
+ alertFired = true;
1425
+ Promise.resolve(storage.getAll()).then((entries) => {
1426
+ const total = computeTotal(entries);
1427
+ if (total < alertThreshold) {
1428
+ alertFired = false;
1429
+ return;
1430
+ }
1425
1431
  const payload = {
1426
1432
  text: `[tokenwatch] Alert: total cost reached $${total.toFixed(4)} USD (threshold: $${alertThreshold})`
1427
1433
  };
@@ -1431,10 +1437,12 @@ ${issues}`);
1431
1437
  body: JSON.stringify(payload)
1432
1438
  }).catch(() => {
1433
1439
  });
1434
- }
1440
+ }).catch(() => {
1441
+ alertFired = false;
1442
+ });
1435
1443
  }
1436
- function getReport() {
1437
- const entries = storage.getAll();
1444
+ async function getReport() {
1445
+ const entries = await Promise.resolve(storage.getAll());
1438
1446
  const byModel = {};
1439
1447
  const bySession = {};
1440
1448
  const byUser = {};
@@ -1472,18 +1480,18 @@ ${issues}`);
1472
1480
  period: { from: startedAt, to: lastTimestamp }
1473
1481
  };
1474
1482
  }
1475
- function reset() {
1476
- storage.clearAll();
1483
+ async function reset() {
1484
+ await Promise.resolve(storage.clearAll());
1477
1485
  alertFired = false;
1478
1486
  }
1479
- function resetSession(sessionId) {
1480
- storage.clearSession(sessionId);
1487
+ async function resetSession(sessionId) {
1488
+ await Promise.resolve(storage.clearSession(sessionId));
1481
1489
  }
1482
- function exportJSON() {
1483
- return JSON.stringify(getReport(), null, 2);
1490
+ async function exportJSON() {
1491
+ return JSON.stringify(await getReport(), null, 2);
1484
1492
  }
1485
- function exportCSV() {
1486
- const entries = storage.getAll();
1493
+ async function exportCSV() {
1494
+ const entries = await Promise.resolve(storage.getAll());
1487
1495
  const header = "timestamp,model,inputTokens,outputTokens,costUSD,sessionId,userId";
1488
1496
  const rows = entries.map(
1489
1497
  (e) => [