@diogonzafe/tokenwatch 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters.cjs +218 -0
- package/dist/adapters.cjs.map +1 -0
- package/dist/adapters.d.cts +125 -0
- package/dist/adapters.d.ts +125 -0
- package/dist/adapters.js +189 -0
- package/dist/adapters.js.map +1 -0
- package/dist/index-Cy_sl3FI.d.cts +89 -0
- package/dist/index-Cy_sl3FI.d.ts +89 -0
- package/dist/index.cjs +29 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -87
- package/dist/index.d.ts +3 -87
- package/dist/index.js +29 -41
- package/dist/index.js.map +1 -1
- package/package.json +19 -2
|
@@ -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"]).
|
|
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:
|
|
1388
|
+
storage: storageOption,
|
|
1387
1389
|
alertThreshold,
|
|
1388
1390
|
webhookUrl,
|
|
1389
1391
|
syncPrices,
|
|
1390
1392
|
customPrices
|
|
1391
1393
|
} = parsed.data;
|
|
1392
|
-
const storage = createStorage(
|
|
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
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
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) => [
|
|
@@ -1558,12 +1566,7 @@ function wrapOpenAI(client, tracker) {
|
|
|
1558
1566
|
return async function(params) {
|
|
1559
1567
|
const { cleaned, sessionId, userId } = extractMeta(params);
|
|
1560
1568
|
const model = typeof cleaned["model"] === "string" ? cleaned["model"] : "unknown";
|
|
1561
|
-
|
|
1562
|
-
try {
|
|
1563
|
-
result = await target.create(cleaned);
|
|
1564
|
-
} catch (err) {
|
|
1565
|
-
throw err;
|
|
1566
|
-
}
|
|
1569
|
+
const result = await target.create(cleaned);
|
|
1567
1570
|
if (result && typeof result === "object" && Symbol.asyncIterator in result) {
|
|
1568
1571
|
return wrapStream(
|
|
1569
1572
|
result,
|
|
@@ -1648,12 +1651,7 @@ function wrapAnthropic(client, tracker) {
|
|
|
1648
1651
|
return async function(params) {
|
|
1649
1652
|
const { cleaned, sessionId, userId } = extractMeta2(params);
|
|
1650
1653
|
const model = typeof cleaned["model"] === "string" ? cleaned["model"] : "unknown";
|
|
1651
|
-
|
|
1652
|
-
try {
|
|
1653
|
-
result = await target.create(cleaned);
|
|
1654
|
-
} catch (err) {
|
|
1655
|
-
throw err;
|
|
1656
|
-
}
|
|
1654
|
+
const result = await target.create(cleaned);
|
|
1657
1655
|
if (result && typeof result === "object" && Symbol.asyncIterator in result) {
|
|
1658
1656
|
return wrapStream2(
|
|
1659
1657
|
result,
|
|
@@ -1698,12 +1696,7 @@ function wrapGemini(client, tracker) {
|
|
|
1698
1696
|
get(mTarget, mProp) {
|
|
1699
1697
|
if (mProp === "generateContent") {
|
|
1700
1698
|
return async function(params) {
|
|
1701
|
-
|
|
1702
|
-
try {
|
|
1703
|
-
result = await mTarget.generateContent(params);
|
|
1704
|
-
} catch (err) {
|
|
1705
|
-
throw err;
|
|
1706
|
-
}
|
|
1699
|
+
const result = await mTarget.generateContent(params);
|
|
1707
1700
|
const meta = result.response.usageMetadata;
|
|
1708
1701
|
tracker.track({
|
|
1709
1702
|
model: modelId,
|
|
@@ -1715,12 +1708,7 @@ function wrapGemini(client, tracker) {
|
|
|
1715
1708
|
}
|
|
1716
1709
|
if (mProp === "generateContentStream") {
|
|
1717
1710
|
return async function(params) {
|
|
1718
|
-
|
|
1719
|
-
try {
|
|
1720
|
-
streamResult = await mTarget.generateContentStream(params);
|
|
1721
|
-
} catch (err) {
|
|
1722
|
-
throw err;
|
|
1723
|
-
}
|
|
1711
|
+
const streamResult = await mTarget.generateContentStream(params);
|
|
1724
1712
|
streamResult.response.then((res) => {
|
|
1725
1713
|
const meta = res.usageMetadata;
|
|
1726
1714
|
tracker.track({
|