@diogonzafe/tokenwatch 0.5.0 → 0.7.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 +137 -2
- package/dist/adapters.d.cts +1 -1
- package/dist/adapters.d.ts +1 -1
- package/dist/cli.js +467 -41
- package/dist/cli.js.map +1 -1
- package/dist/exporters.cjs +76 -0
- package/dist/exporters.cjs.map +1 -0
- package/dist/exporters.d.cts +60 -0
- package/dist/exporters.d.ts +60 -0
- package/dist/exporters.js +56 -0
- package/dist/exporters.js.map +1 -0
- package/dist/{index-CJKk1hHw.d.cts → index-D9xq0RNg.d.cts} +19 -1
- package/dist/{index-CJKk1hHw.d.ts → index-D9xq0RNg.d.ts} +19 -1
- package/dist/index.cjs +69 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +69 -3
- package/dist/index.js.map +1 -1
- package/dist/langchain.d.cts +1 -1
- package/dist/langchain.d.ts +1 -1
- package/package.json +13 -3
- package/prices.json +7 -1
- package/dist/cli.d.ts +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,286 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/adapters/postgres.ts
|
|
13
|
+
var postgres_exports = {};
|
|
14
|
+
__export(postgres_exports, {
|
|
15
|
+
PostgresStorage: () => PostgresStorage
|
|
16
|
+
});
|
|
17
|
+
function rowToEntry(r) {
|
|
18
|
+
const reasoningTokens = r["reasoning_tokens"] ?? 0;
|
|
19
|
+
const cachedTokens = r["cached_tokens"] ?? 0;
|
|
20
|
+
const cacheCreationTokens = r["cache_creation_tokens"] ?? 0;
|
|
21
|
+
return {
|
|
22
|
+
model: r["model"],
|
|
23
|
+
inputTokens: r["input_tokens"],
|
|
24
|
+
outputTokens: r["output_tokens"],
|
|
25
|
+
...reasoningTokens > 0 && { reasoningTokens },
|
|
26
|
+
...cachedTokens > 0 && { cachedTokens },
|
|
27
|
+
...cacheCreationTokens > 0 && { cacheCreationTokens },
|
|
28
|
+
costUSD: Number(r["cost_usd"]),
|
|
29
|
+
...r["session_id"] != null && { sessionId: r["session_id"] },
|
|
30
|
+
...r["user_id"] != null && { userId: r["user_id"] },
|
|
31
|
+
...r["feature"] != null && { feature: r["feature"] },
|
|
32
|
+
timestamp: r["timestamp"] instanceof Date ? r["timestamp"].toISOString() : r["timestamp"]
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
var PostgresStorage;
|
|
36
|
+
var init_postgres = __esm({
|
|
37
|
+
"src/adapters/postgres.ts"() {
|
|
38
|
+
"use strict";
|
|
39
|
+
PostgresStorage = class {
|
|
40
|
+
constructor(client) {
|
|
41
|
+
this.client = client;
|
|
42
|
+
}
|
|
43
|
+
client;
|
|
44
|
+
/** Creates the `tokenwatch_usage` table if it does not already exist.
|
|
45
|
+
* Also adds new columns for databases created before v0.2.0 / v0.3.0. */
|
|
46
|
+
async migrate() {
|
|
47
|
+
await this.client.query(`
|
|
48
|
+
CREATE TABLE IF NOT EXISTS tokenwatch_usage (
|
|
49
|
+
id BIGSERIAL PRIMARY KEY,
|
|
50
|
+
model TEXT NOT NULL,
|
|
51
|
+
input_tokens INTEGER NOT NULL,
|
|
52
|
+
output_tokens INTEGER NOT NULL,
|
|
53
|
+
reasoning_tokens INTEGER NOT NULL DEFAULT 0,
|
|
54
|
+
cached_tokens INTEGER NOT NULL DEFAULT 0,
|
|
55
|
+
cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
|
|
56
|
+
cost_usd NUMERIC NOT NULL,
|
|
57
|
+
session_id TEXT,
|
|
58
|
+
user_id TEXT,
|
|
59
|
+
feature TEXT,
|
|
60
|
+
timestamp TIMESTAMPTZ NOT NULL
|
|
61
|
+
)
|
|
62
|
+
`);
|
|
63
|
+
for (const col of [
|
|
64
|
+
"ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS reasoning_tokens INTEGER NOT NULL DEFAULT 0",
|
|
65
|
+
"ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS feature TEXT",
|
|
66
|
+
"ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS cached_tokens INTEGER NOT NULL DEFAULT 0",
|
|
67
|
+
"ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS cache_creation_tokens INTEGER NOT NULL DEFAULT 0"
|
|
68
|
+
]) {
|
|
69
|
+
await this.client.query(col).catch(() => {
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
record(entry) {
|
|
74
|
+
this.client.query(
|
|
75
|
+
`INSERT INTO tokenwatch_usage
|
|
76
|
+
(model, input_tokens, output_tokens, reasoning_tokens, cached_tokens, cache_creation_tokens,
|
|
77
|
+
cost_usd, session_id, user_id, feature, timestamp)
|
|
78
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
|
|
79
|
+
[
|
|
80
|
+
entry.model,
|
|
81
|
+
entry.inputTokens,
|
|
82
|
+
entry.outputTokens,
|
|
83
|
+
entry.reasoningTokens ?? 0,
|
|
84
|
+
entry.cachedTokens ?? 0,
|
|
85
|
+
entry.cacheCreationTokens ?? 0,
|
|
86
|
+
entry.costUSD,
|
|
87
|
+
entry.sessionId ?? null,
|
|
88
|
+
entry.userId ?? null,
|
|
89
|
+
entry.feature ?? null,
|
|
90
|
+
entry.timestamp
|
|
91
|
+
]
|
|
92
|
+
).catch((err) => {
|
|
93
|
+
console.warn("[tokenwatch] PostgresStorage.record failed:", err);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
async getAll() {
|
|
97
|
+
const result = await this.client.query(
|
|
98
|
+
"SELECT * FROM tokenwatch_usage ORDER BY timestamp ASC"
|
|
99
|
+
);
|
|
100
|
+
return result.rows.map(rowToEntry);
|
|
101
|
+
}
|
|
102
|
+
async clearAll() {
|
|
103
|
+
await this.client.query("DELETE FROM tokenwatch_usage");
|
|
104
|
+
}
|
|
105
|
+
async clearSession(sessionId) {
|
|
106
|
+
await this.client.query(
|
|
107
|
+
"DELETE FROM tokenwatch_usage WHERE session_id = $1",
|
|
108
|
+
[sessionId]
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// src/adapters/mysql.ts
|
|
116
|
+
var mysql_exports = {};
|
|
117
|
+
__export(mysql_exports, {
|
|
118
|
+
MySQLStorage: () => MySQLStorage
|
|
119
|
+
});
|
|
120
|
+
function rowToEntry2(r) {
|
|
121
|
+
const reasoningTokens = r["reasoning_tokens"] ?? 0;
|
|
122
|
+
const cachedTokens = r["cached_tokens"] ?? 0;
|
|
123
|
+
const cacheCreationTokens = r["cache_creation_tokens"] ?? 0;
|
|
124
|
+
return {
|
|
125
|
+
model: r["model"],
|
|
126
|
+
inputTokens: r["input_tokens"],
|
|
127
|
+
outputTokens: r["output_tokens"],
|
|
128
|
+
...reasoningTokens > 0 && { reasoningTokens },
|
|
129
|
+
...cachedTokens > 0 && { cachedTokens },
|
|
130
|
+
...cacheCreationTokens > 0 && { cacheCreationTokens },
|
|
131
|
+
costUSD: Number(r["cost_usd"]),
|
|
132
|
+
...r["session_id"] != null && { sessionId: r["session_id"] },
|
|
133
|
+
...r["user_id"] != null && { userId: r["user_id"] },
|
|
134
|
+
...r["feature"] != null && { feature: r["feature"] },
|
|
135
|
+
timestamp: r["timestamp"] instanceof Date ? r["timestamp"].toISOString() : r["timestamp"]
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
var MySQLStorage;
|
|
139
|
+
var init_mysql = __esm({
|
|
140
|
+
"src/adapters/mysql.ts"() {
|
|
141
|
+
"use strict";
|
|
142
|
+
MySQLStorage = class {
|
|
143
|
+
constructor(client) {
|
|
144
|
+
this.client = client;
|
|
145
|
+
}
|
|
146
|
+
client;
|
|
147
|
+
/** Creates the `tokenwatch_usage` table if it does not already exist.
|
|
148
|
+
* Also adds new columns for databases created before v0.2.0 / v0.3.0. */
|
|
149
|
+
async migrate() {
|
|
150
|
+
await this.client.execute(`
|
|
151
|
+
CREATE TABLE IF NOT EXISTS tokenwatch_usage (
|
|
152
|
+
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
153
|
+
model VARCHAR(255) NOT NULL,
|
|
154
|
+
input_tokens INT NOT NULL,
|
|
155
|
+
output_tokens INT NOT NULL,
|
|
156
|
+
reasoning_tokens INT NOT NULL DEFAULT 0,
|
|
157
|
+
cached_tokens INT NOT NULL DEFAULT 0,
|
|
158
|
+
cache_creation_tokens INT NOT NULL DEFAULT 0,
|
|
159
|
+
cost_usd DECIMAL(18,8) NOT NULL,
|
|
160
|
+
session_id VARCHAR(255),
|
|
161
|
+
user_id VARCHAR(255),
|
|
162
|
+
feature VARCHAR(255),
|
|
163
|
+
timestamp DATETIME(3) NOT NULL
|
|
164
|
+
)
|
|
165
|
+
`);
|
|
166
|
+
await this.client.execute(`
|
|
167
|
+
ALTER TABLE tokenwatch_usage
|
|
168
|
+
ADD COLUMN IF NOT EXISTS reasoning_tokens INT NOT NULL DEFAULT 0,
|
|
169
|
+
ADD COLUMN IF NOT EXISTS feature VARCHAR(255),
|
|
170
|
+
ADD COLUMN IF NOT EXISTS cached_tokens INT NOT NULL DEFAULT 0,
|
|
171
|
+
ADD COLUMN IF NOT EXISTS cache_creation_tokens INT NOT NULL DEFAULT 0
|
|
172
|
+
`).catch(() => {
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
record(entry) {
|
|
176
|
+
this.client.execute(
|
|
177
|
+
`INSERT INTO tokenwatch_usage
|
|
178
|
+
(model, input_tokens, output_tokens, reasoning_tokens, cached_tokens, cache_creation_tokens,
|
|
179
|
+
cost_usd, session_id, user_id, feature, timestamp)
|
|
180
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
181
|
+
[
|
|
182
|
+
entry.model,
|
|
183
|
+
entry.inputTokens,
|
|
184
|
+
entry.outputTokens,
|
|
185
|
+
entry.reasoningTokens ?? 0,
|
|
186
|
+
entry.cachedTokens ?? 0,
|
|
187
|
+
entry.cacheCreationTokens ?? 0,
|
|
188
|
+
entry.costUSD,
|
|
189
|
+
entry.sessionId ?? null,
|
|
190
|
+
entry.userId ?? null,
|
|
191
|
+
entry.feature ?? null,
|
|
192
|
+
entry.timestamp
|
|
193
|
+
]
|
|
194
|
+
).catch((err) => {
|
|
195
|
+
console.warn("[tokenwatch] MySQLStorage.record failed:", err);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
async getAll() {
|
|
199
|
+
const [rows] = await this.client.execute(
|
|
200
|
+
"SELECT * FROM tokenwatch_usage ORDER BY timestamp ASC"
|
|
201
|
+
);
|
|
202
|
+
return rows.map(rowToEntry2);
|
|
203
|
+
}
|
|
204
|
+
async clearAll() {
|
|
205
|
+
await this.client.execute("DELETE FROM tokenwatch_usage");
|
|
206
|
+
}
|
|
207
|
+
async clearSession(sessionId) {
|
|
208
|
+
await this.client.execute(
|
|
209
|
+
"DELETE FROM tokenwatch_usage WHERE session_id = ?",
|
|
210
|
+
[sessionId]
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// src/adapters/mongodb.ts
|
|
218
|
+
var mongodb_exports = {};
|
|
219
|
+
__export(mongodb_exports, {
|
|
220
|
+
MongoStorage: () => MongoStorage
|
|
221
|
+
});
|
|
222
|
+
function docToEntry(doc) {
|
|
223
|
+
return {
|
|
224
|
+
model: doc.model,
|
|
225
|
+
inputTokens: doc.inputTokens,
|
|
226
|
+
outputTokens: doc.outputTokens,
|
|
227
|
+
...doc.reasoningTokens != null && doc.reasoningTokens > 0 && { reasoningTokens: doc.reasoningTokens },
|
|
228
|
+
...doc.cachedTokens != null && doc.cachedTokens > 0 && { cachedTokens: doc.cachedTokens },
|
|
229
|
+
...doc.cacheCreationTokens != null && doc.cacheCreationTokens > 0 && { cacheCreationTokens: doc.cacheCreationTokens },
|
|
230
|
+
costUSD: doc.costUSD,
|
|
231
|
+
...doc.sessionId != null && { sessionId: doc.sessionId },
|
|
232
|
+
...doc.userId != null && { userId: doc.userId },
|
|
233
|
+
...doc.feature != null && { feature: doc.feature },
|
|
234
|
+
timestamp: doc.timestamp
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
var COLLECTION, MongoStorage;
|
|
238
|
+
var init_mongodb = __esm({
|
|
239
|
+
"src/adapters/mongodb.ts"() {
|
|
240
|
+
"use strict";
|
|
241
|
+
COLLECTION = "tokenwatch_usage";
|
|
242
|
+
MongoStorage = class {
|
|
243
|
+
col;
|
|
244
|
+
constructor(db) {
|
|
245
|
+
this.col = db.collection(COLLECTION);
|
|
246
|
+
}
|
|
247
|
+
/** Creates recommended indexes for query performance. Call once at startup. */
|
|
248
|
+
async createIndexes() {
|
|
249
|
+
await this.col.createIndex({ timestamp: 1 });
|
|
250
|
+
await this.col.createIndex({ sessionId: 1 });
|
|
251
|
+
await this.col.createIndex({ userId: 1 });
|
|
252
|
+
await this.col.createIndex({ model: 1 });
|
|
253
|
+
}
|
|
254
|
+
record(entry) {
|
|
255
|
+
this.col.insertOne({
|
|
256
|
+
model: entry.model,
|
|
257
|
+
inputTokens: entry.inputTokens,
|
|
258
|
+
outputTokens: entry.outputTokens,
|
|
259
|
+
...entry.reasoningTokens !== void 0 && { reasoningTokens: entry.reasoningTokens },
|
|
260
|
+
...entry.cachedTokens !== void 0 && { cachedTokens: entry.cachedTokens },
|
|
261
|
+
...entry.cacheCreationTokens !== void 0 && { cacheCreationTokens: entry.cacheCreationTokens },
|
|
262
|
+
costUSD: entry.costUSD,
|
|
263
|
+
sessionId: entry.sessionId ?? null,
|
|
264
|
+
userId: entry.userId ?? null,
|
|
265
|
+
...entry.feature !== void 0 && { feature: entry.feature },
|
|
266
|
+
timestamp: entry.timestamp
|
|
267
|
+
}).catch((err) => {
|
|
268
|
+
console.warn("[tokenwatch] MongoStorage.record failed:", err);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
async getAll() {
|
|
272
|
+
const docs = await this.col.find({}).sort({ timestamp: 1 }).toArray();
|
|
273
|
+
return docs.map(docToEntry);
|
|
274
|
+
}
|
|
275
|
+
async clearAll() {
|
|
276
|
+
await this.col.deleteMany({});
|
|
277
|
+
}
|
|
278
|
+
async clearSession(sessionId) {
|
|
279
|
+
await this.col.deleteMany({ sessionId });
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
});
|
|
2
284
|
|
|
3
285
|
// bin/cli.ts
|
|
4
286
|
import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
@@ -245,7 +527,7 @@ function maybeSuggestCheaperModel(model, costUSD, inputTokens, outputTokens, lay
|
|
|
245
527
|
|
|
246
528
|
// prices.json
|
|
247
529
|
var prices_default = {
|
|
248
|
-
updated_at: "2026-04-
|
|
530
|
+
updated_at: "2026-04-24",
|
|
249
531
|
source: "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json",
|
|
250
532
|
models: {
|
|
251
533
|
"gpt-4o": {
|
|
@@ -1562,6 +1844,12 @@ var prices_default = {
|
|
|
1562
1844
|
cachedInput: 0.3,
|
|
1563
1845
|
cacheCreationInput: 3.75,
|
|
1564
1846
|
maxInputTokens: 1e6
|
|
1847
|
+
},
|
|
1848
|
+
"gpt-5.5": {
|
|
1849
|
+
input: 5,
|
|
1850
|
+
output: 30,
|
|
1851
|
+
cachedInput: 0.5,
|
|
1852
|
+
maxInputTokens: 272e3
|
|
1565
1853
|
}
|
|
1566
1854
|
}
|
|
1567
1855
|
};
|
|
@@ -1594,7 +1882,14 @@ var TrackerConfigSchema = z.object({
|
|
|
1594
1882
|
perUser: BudgetConfigSchema.optional(),
|
|
1595
1883
|
perSession: BudgetConfigSchema.optional()
|
|
1596
1884
|
}).optional(),
|
|
1597
|
-
suggestions: z.boolean().optional().default(false)
|
|
1885
|
+
suggestions: z.boolean().optional().default(false),
|
|
1886
|
+
anomalyDetection: z.object({
|
|
1887
|
+
multiplierThreshold: z.number().positive(),
|
|
1888
|
+
webhookUrl: z.string().url(),
|
|
1889
|
+
windowHours: z.number().positive().optional().default(24),
|
|
1890
|
+
mode: z.enum(["once", "always"]).optional().default("once")
|
|
1891
|
+
}).optional(),
|
|
1892
|
+
exporter: z.custom((v) => v !== null && typeof v === "object" && typeof v.export === "function").optional()
|
|
1598
1893
|
});
|
|
1599
1894
|
function createTracker(config = {}) {
|
|
1600
1895
|
const parsed = TrackerConfigSchema.safeParse(config);
|
|
@@ -1611,7 +1906,9 @@ ${issues}`);
|
|
|
1611
1906
|
customPrices,
|
|
1612
1907
|
warnIfStaleAfterHours,
|
|
1613
1908
|
budgets,
|
|
1614
|
-
suggestions
|
|
1909
|
+
suggestions,
|
|
1910
|
+
anomalyDetection,
|
|
1911
|
+
exporter
|
|
1615
1912
|
} = parsed.data;
|
|
1616
1913
|
const storage = typeof storageOption === "object" ? storageOption : createStorage(storageOption);
|
|
1617
1914
|
let remotePrices;
|
|
@@ -1644,6 +1941,7 @@ ${issues}`);
|
|
|
1644
1941
|
let alertFired = false;
|
|
1645
1942
|
const firedUserAlerts = /* @__PURE__ */ new Set();
|
|
1646
1943
|
const firedSessionAlerts = /* @__PURE__ */ new Set();
|
|
1944
|
+
const firedAnomalyKeys = /* @__PURE__ */ new Set();
|
|
1647
1945
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1648
1946
|
function resolveModelPrice(model) {
|
|
1649
1947
|
maybeWarnStaleness();
|
|
@@ -1668,7 +1966,12 @@ ${issues}`);
|
|
|
1668
1966
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1669
1967
|
};
|
|
1670
1968
|
storage.record(full);
|
|
1969
|
+
if (exporter) {
|
|
1970
|
+
Promise.resolve(exporter.export(full)).catch(() => {
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1671
1973
|
maybeFireAlerts(full);
|
|
1974
|
+
if (anomalyDetection) maybeDetectAnomaly(full);
|
|
1672
1975
|
if (suggestions) {
|
|
1673
1976
|
maybeSuggestCheaperModel(entry.model, costUSD, entry.inputTokens, entry.outputTokens, {
|
|
1674
1977
|
bundledPrices,
|
|
@@ -1838,11 +2141,56 @@ ${issues}`);
|
|
|
1838
2141
|
basedOnPeriod: { from: first, to: last }
|
|
1839
2142
|
};
|
|
1840
2143
|
}
|
|
2144
|
+
function maybeDetectAnomaly(entry) {
|
|
2145
|
+
if (entry.costUSD <= 0) return;
|
|
2146
|
+
const { multiplierThreshold, webhookUrl: aUrl, windowHours: wh, mode: modeRaw } = anomalyDetection;
|
|
2147
|
+
const wHours = wh ?? 24;
|
|
2148
|
+
const mode = modeRaw ?? "once";
|
|
2149
|
+
const windowStart = Date.now() - wHours * 60 * 60 * 1e3;
|
|
2150
|
+
const entryTs = new Date(entry.timestamp).getTime();
|
|
2151
|
+
function checkEntity(key, label, predicate) {
|
|
2152
|
+
if (mode !== "always" && firedAnomalyKeys.has(key)) return;
|
|
2153
|
+
if (mode !== "always") firedAnomalyKeys.add(key);
|
|
2154
|
+
Promise.resolve(storage.getAll()).then((all) => {
|
|
2155
|
+
const history = all.filter(
|
|
2156
|
+
(e) => predicate(e) && new Date(e.timestamp).getTime() >= windowStart && new Date(e.timestamp).getTime() !== entryTs
|
|
2157
|
+
);
|
|
2158
|
+
if (history.length === 0) {
|
|
2159
|
+
if (mode !== "always") firedAnomalyKeys.delete(key);
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
const avg = history.reduce((s, e) => s + e.costUSD, 0) / history.length;
|
|
2163
|
+
if (avg <= 0 || entry.costUSD <= avg * multiplierThreshold) {
|
|
2164
|
+
if (mode !== "always") firedAnomalyKeys.delete(key);
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
const multiple = (entry.costUSD / avg).toFixed(1);
|
|
2168
|
+
fireWebhook(aUrl, {
|
|
2169
|
+
text: `[tokenwatch] Anomaly: ${label} call cost $${entry.costUSD.toFixed(4)} is ${multiple}x above ${wHours}h average ($${avg.toFixed(4)})`
|
|
2170
|
+
});
|
|
2171
|
+
}).catch(() => {
|
|
2172
|
+
if (mode !== "always") firedAnomalyKeys.delete(key);
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
if (entry.userId) {
|
|
2176
|
+
checkEntity(
|
|
2177
|
+
`user:${entry.userId}`,
|
|
2178
|
+
`user "${entry.userId}"`,
|
|
2179
|
+
(e) => e.userId === entry.userId
|
|
2180
|
+
);
|
|
2181
|
+
}
|
|
2182
|
+
checkEntity(
|
|
2183
|
+
`model:${entry.model}`,
|
|
2184
|
+
`model "${entry.model}"`,
|
|
2185
|
+
(e) => e.model === entry.model
|
|
2186
|
+
);
|
|
2187
|
+
}
|
|
1841
2188
|
async function reset() {
|
|
1842
2189
|
await Promise.resolve(storage.clearAll());
|
|
1843
2190
|
alertFired = false;
|
|
1844
2191
|
firedUserAlerts.clear();
|
|
1845
2192
|
firedSessionAlerts.clear();
|
|
2193
|
+
firedAnomalyKeys.clear();
|
|
1846
2194
|
}
|
|
1847
2195
|
async function resetSession(sessionId) {
|
|
1848
2196
|
await Promise.resolve(storage.clearSession(sessionId));
|
|
@@ -2686,7 +3034,82 @@ function startDashboardServer(storage, port) {
|
|
|
2686
3034
|
|
|
2687
3035
|
// bin/cli.ts
|
|
2688
3036
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
2689
|
-
var
|
|
3037
|
+
var DEFAULT_DB_PATH = join3(homedir3(), ".tokenwatch", "usage.db");
|
|
3038
|
+
function getFlag(args, flag) {
|
|
3039
|
+
const idx = args.indexOf(flag);
|
|
3040
|
+
if (idx === -1) return void 0;
|
|
3041
|
+
const value = args[idx + 1];
|
|
3042
|
+
return value !== void 0 && !value.startsWith("--") ? value : void 0;
|
|
3043
|
+
}
|
|
3044
|
+
async function openStorage(dbUrl) {
|
|
3045
|
+
if (!dbUrl) {
|
|
3046
|
+
if (!existsSync2(DEFAULT_DB_PATH)) {
|
|
3047
|
+
console.error(`No SQLite database found at ${DEFAULT_DB_PATH}`);
|
|
3048
|
+
console.error("Start your app with storage: 'sqlite' to begin recording usage.");
|
|
3049
|
+
console.error("Or pass --db <url> to connect to Postgres, MySQL, or MongoDB.");
|
|
3050
|
+
process.exit(1);
|
|
3051
|
+
}
|
|
3052
|
+
let storage;
|
|
3053
|
+
try {
|
|
3054
|
+
storage = new SqliteStorage(DEFAULT_DB_PATH);
|
|
3055
|
+
} catch {
|
|
3056
|
+
console.error("Failed to open SQLite database. Is better-sqlite3 installed?");
|
|
3057
|
+
console.error("Run: npm install better-sqlite3");
|
|
3058
|
+
process.exit(1);
|
|
3059
|
+
}
|
|
3060
|
+
return { storage, close: async () => {
|
|
3061
|
+
} };
|
|
3062
|
+
}
|
|
3063
|
+
if (dbUrl.startsWith("postgres://") || dbUrl.startsWith("postgresql://")) {
|
|
3064
|
+
let pgMod;
|
|
3065
|
+
try {
|
|
3066
|
+
pgMod = (await import("pg")).default;
|
|
3067
|
+
} catch {
|
|
3068
|
+
console.error("[tokenwatch] Postgres requires the pg package.");
|
|
3069
|
+
console.error("Run: npm install pg");
|
|
3070
|
+
process.exit(1);
|
|
3071
|
+
}
|
|
3072
|
+
const { PostgresStorage: PostgresStorage2 } = await Promise.resolve().then(() => (init_postgres(), postgres_exports));
|
|
3073
|
+
const pool = new pgMod.Pool({ connectionString: dbUrl });
|
|
3074
|
+
const storage = new PostgresStorage2(pool);
|
|
3075
|
+
return { storage, close: () => pool.end() };
|
|
3076
|
+
}
|
|
3077
|
+
if (dbUrl.startsWith("mysql://")) {
|
|
3078
|
+
let mysqlMod;
|
|
3079
|
+
try {
|
|
3080
|
+
mysqlMod = await import("mysql2/promise");
|
|
3081
|
+
} catch {
|
|
3082
|
+
console.error("[tokenwatch] MySQL requires the mysql2 package.");
|
|
3083
|
+
console.error("Run: npm install mysql2");
|
|
3084
|
+
process.exit(1);
|
|
3085
|
+
}
|
|
3086
|
+
const { MySQLStorage: MySQLStorage2 } = await Promise.resolve().then(() => (init_mysql(), mysql_exports));
|
|
3087
|
+
const pool = mysqlMod.createPool(dbUrl);
|
|
3088
|
+
const storage = new MySQLStorage2(pool);
|
|
3089
|
+
return { storage, close: () => pool.end() };
|
|
3090
|
+
}
|
|
3091
|
+
if (dbUrl.startsWith("mongodb://") || dbUrl.startsWith("mongodb+srv://")) {
|
|
3092
|
+
let mongoMod;
|
|
3093
|
+
try {
|
|
3094
|
+
mongoMod = await import("mongodb");
|
|
3095
|
+
} catch {
|
|
3096
|
+
console.error("[tokenwatch] MongoDB requires the mongodb package.");
|
|
3097
|
+
console.error("Run: npm install mongodb");
|
|
3098
|
+
process.exit(1);
|
|
3099
|
+
}
|
|
3100
|
+
const { MongoStorage: MongoStorage2 } = await Promise.resolve().then(() => (init_mongodb(), mongodb_exports));
|
|
3101
|
+
const urlObj = new URL(dbUrl);
|
|
3102
|
+
const dbName = urlObj.pathname.replace(/^\//, "") || "tokenwatch";
|
|
3103
|
+
const client = new mongoMod.MongoClient(dbUrl);
|
|
3104
|
+
await client.connect();
|
|
3105
|
+
const db = client.db(dbName);
|
|
3106
|
+
const storage = new MongoStorage2(db);
|
|
3107
|
+
return { storage, close: () => client.close() };
|
|
3108
|
+
}
|
|
3109
|
+
console.error(`[tokenwatch] Unsupported database URL: "${dbUrl}"`);
|
|
3110
|
+
console.error("Supported protocols: postgres://, mysql://, mongodb://, mongodb+srv://");
|
|
3111
|
+
process.exit(1);
|
|
3112
|
+
}
|
|
2690
3113
|
function loadBundledPrices() {
|
|
2691
3114
|
const pricesPath = join3(__dirname, "..", "prices.json");
|
|
2692
3115
|
const raw = readFileSync(pricesPath, "utf8");
|
|
@@ -2719,22 +3142,16 @@ function cmdPrices() {
|
|
|
2719
3142
|
console.log(`${row.model.padEnd(maxName)} ${row.input.padStart(12)} ${row.output.padStart(12)}`);
|
|
2720
3143
|
}
|
|
2721
3144
|
}
|
|
2722
|
-
async function cmdReport() {
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
return;
|
|
2727
|
-
}
|
|
2728
|
-
let storage;
|
|
3145
|
+
async function cmdReport(args) {
|
|
3146
|
+
const dbUrl = getFlag(args, "--db");
|
|
3147
|
+
const { storage, close } = await openStorage(dbUrl);
|
|
3148
|
+
let report;
|
|
2729
3149
|
try {
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
process.exit(1);
|
|
3150
|
+
const tracker = createTracker({ storage, syncPrices: false });
|
|
3151
|
+
report = await tracker.getReport();
|
|
3152
|
+
} finally {
|
|
3153
|
+
await close();
|
|
2735
3154
|
}
|
|
2736
|
-
const tracker = createTracker({ storage, syncPrices: false });
|
|
2737
|
-
const report = await tracker.getReport();
|
|
2738
3155
|
if (report.totalCostUSD === 0 && Object.keys(report.byModel).length === 0) {
|
|
2739
3156
|
console.log("No usage recorded yet.");
|
|
2740
3157
|
return;
|
|
@@ -2772,20 +3189,20 @@ async function cmdReport() {
|
|
|
2772
3189
|
}
|
|
2773
3190
|
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
2774
3191
|
}
|
|
2775
|
-
async function cmdDashboard(
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
}
|
|
2781
|
-
let storage;
|
|
2782
|
-
try {
|
|
2783
|
-
storage = new SqliteStorage(DB_PATH2);
|
|
2784
|
-
} catch {
|
|
2785
|
-
console.error("Failed to open SQLite database. Is better-sqlite3 installed?");
|
|
2786
|
-
console.error("Run: npm install better-sqlite3");
|
|
3192
|
+
async function cmdDashboard(args) {
|
|
3193
|
+
const portFlag = getFlag(args, "--port");
|
|
3194
|
+
const port = portFlag !== void 0 ? parseInt(portFlag, 10) : 4242;
|
|
3195
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
3196
|
+
console.error(`[tokenwatch] Invalid port: "${portFlag}". Must be a number between 1 and 65535.`);
|
|
2787
3197
|
process.exit(1);
|
|
2788
3198
|
}
|
|
3199
|
+
const dbUrl = getFlag(args, "--db");
|
|
3200
|
+
const { storage, close } = await openStorage(dbUrl);
|
|
3201
|
+
const shutdown = () => {
|
|
3202
|
+
void close().then(() => process.exit(0));
|
|
3203
|
+
};
|
|
3204
|
+
process.on("SIGINT", shutdown);
|
|
3205
|
+
process.on("SIGTERM", shutdown);
|
|
2789
3206
|
startDashboardServer(storage, port);
|
|
2790
3207
|
}
|
|
2791
3208
|
function cmdHelp() {
|
|
@@ -2793,11 +3210,23 @@ function cmdHelp() {
|
|
|
2793
3210
|
tokenwatch \u2014 CLI
|
|
2794
3211
|
|
|
2795
3212
|
Commands:
|
|
2796
|
-
sync
|
|
2797
|
-
prices
|
|
2798
|
-
report
|
|
2799
|
-
dashboard [--port N]
|
|
2800
|
-
|
|
3213
|
+
sync Fetch and cache latest model prices from remote
|
|
3214
|
+
prices List all bundled models and their current prices
|
|
3215
|
+
report [--db <url>] Show usage report (default: SQLite at ~/.tokenwatch/usage.db)
|
|
3216
|
+
dashboard [--port N] Open local web dashboard (default port: 4242)
|
|
3217
|
+
[--db <url>] Connect to a database instead of the default SQLite
|
|
3218
|
+
|
|
3219
|
+
Database URL formats:
|
|
3220
|
+
(none) ~/.tokenwatch/usage.db (SQLite, default)
|
|
3221
|
+
postgres://user:pass@host:5432/dbname
|
|
3222
|
+
mysql://user:pass@host:3306/dbname
|
|
3223
|
+
mongodb://user:pass@host:27017/dbname
|
|
3224
|
+
|
|
3225
|
+
Examples:
|
|
3226
|
+
tokenwatch dashboard
|
|
3227
|
+
tokenwatch dashboard --port 8080
|
|
3228
|
+
tokenwatch dashboard --db postgres://user:pass@localhost:5432/myapp
|
|
3229
|
+
tokenwatch report --db mysql://root:pass@localhost:3306/myapp
|
|
2801
3230
|
`.trim());
|
|
2802
3231
|
}
|
|
2803
3232
|
async function main() {
|
|
@@ -2810,14 +3239,11 @@ async function main() {
|
|
|
2810
3239
|
cmdPrices();
|
|
2811
3240
|
break;
|
|
2812
3241
|
case "report":
|
|
2813
|
-
await cmdReport();
|
|
3242
|
+
await cmdReport(args);
|
|
2814
3243
|
break;
|
|
2815
|
-
case "dashboard":
|
|
2816
|
-
|
|
2817
|
-
const port = portFlagIdx !== -1 ? parseInt(args[portFlagIdx + 1] ?? "4242", 10) : 4242;
|
|
2818
|
-
await cmdDashboard(port);
|
|
3244
|
+
case "dashboard":
|
|
3245
|
+
await cmdDashboard(args);
|
|
2819
3246
|
break;
|
|
2820
|
-
}
|
|
2821
3247
|
case "help":
|
|
2822
3248
|
case void 0:
|
|
2823
3249
|
cmdHelp();
|