@lov3kaizen/agentsea-costs 0.5.1
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/LICENSE +21 -0
- package/README.md +469 -0
- package/dist/index.d.ts +1607 -0
- package/dist/index.js +2867 -0
- package/dist/index.js.map +1 -0
- package/package.json +85 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2867 @@
|
|
|
1
|
+
import { EventEmitter } from 'eventemitter3';
|
|
2
|
+
import { nanoid } from 'nanoid';
|
|
3
|
+
import { Cron } from 'croner';
|
|
4
|
+
|
|
5
|
+
// src/core/CostManager.ts
|
|
6
|
+
|
|
7
|
+
// src/pricing/ModelPricingRegistry.ts
|
|
8
|
+
var DEFAULT_PRICING = [
|
|
9
|
+
// Anthropic Models
|
|
10
|
+
{
|
|
11
|
+
model: "claude-3-5-sonnet-20241022",
|
|
12
|
+
provider: "anthropic",
|
|
13
|
+
displayName: "Claude 3.5 Sonnet",
|
|
14
|
+
inputPricePerMillion: 3,
|
|
15
|
+
outputPricePerMillion: 15,
|
|
16
|
+
cacheReadPricePerMillion: 0.3,
|
|
17
|
+
cacheWritePricePerMillion: 3.75,
|
|
18
|
+
contextWindow: 2e5,
|
|
19
|
+
maxOutputTokens: 8192,
|
|
20
|
+
currency: "USD",
|
|
21
|
+
capabilities: {
|
|
22
|
+
vision: true,
|
|
23
|
+
functionCalling: true,
|
|
24
|
+
streaming: true,
|
|
25
|
+
jsonMode: true,
|
|
26
|
+
systemMessage: true,
|
|
27
|
+
computerUse: true
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
model: "claude-3-5-haiku-20241022",
|
|
32
|
+
provider: "anthropic",
|
|
33
|
+
displayName: "Claude 3.5 Haiku",
|
|
34
|
+
inputPricePerMillion: 0.8,
|
|
35
|
+
outputPricePerMillion: 4,
|
|
36
|
+
cacheReadPricePerMillion: 0.08,
|
|
37
|
+
cacheWritePricePerMillion: 1,
|
|
38
|
+
contextWindow: 2e5,
|
|
39
|
+
maxOutputTokens: 8192,
|
|
40
|
+
currency: "USD",
|
|
41
|
+
capabilities: {
|
|
42
|
+
vision: true,
|
|
43
|
+
functionCalling: true,
|
|
44
|
+
streaming: true,
|
|
45
|
+
jsonMode: true,
|
|
46
|
+
systemMessage: true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
model: "claude-3-opus-20240229",
|
|
51
|
+
provider: "anthropic",
|
|
52
|
+
displayName: "Claude 3 Opus",
|
|
53
|
+
inputPricePerMillion: 15,
|
|
54
|
+
outputPricePerMillion: 75,
|
|
55
|
+
cacheReadPricePerMillion: 1.5,
|
|
56
|
+
cacheWritePricePerMillion: 18.75,
|
|
57
|
+
contextWindow: 2e5,
|
|
58
|
+
maxOutputTokens: 4096,
|
|
59
|
+
currency: "USD",
|
|
60
|
+
capabilities: {
|
|
61
|
+
vision: true,
|
|
62
|
+
functionCalling: true,
|
|
63
|
+
streaming: true,
|
|
64
|
+
jsonMode: true,
|
|
65
|
+
systemMessage: true
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
model: "claude-3-sonnet-20240229",
|
|
70
|
+
provider: "anthropic",
|
|
71
|
+
displayName: "Claude 3 Sonnet",
|
|
72
|
+
inputPricePerMillion: 3,
|
|
73
|
+
outputPricePerMillion: 15,
|
|
74
|
+
contextWindow: 2e5,
|
|
75
|
+
maxOutputTokens: 4096,
|
|
76
|
+
currency: "USD",
|
|
77
|
+
capabilities: {
|
|
78
|
+
vision: true,
|
|
79
|
+
functionCalling: true,
|
|
80
|
+
streaming: true,
|
|
81
|
+
jsonMode: true,
|
|
82
|
+
systemMessage: true
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
model: "claude-3-haiku-20240307",
|
|
87
|
+
provider: "anthropic",
|
|
88
|
+
displayName: "Claude 3 Haiku",
|
|
89
|
+
inputPricePerMillion: 0.25,
|
|
90
|
+
outputPricePerMillion: 1.25,
|
|
91
|
+
cacheReadPricePerMillion: 0.03,
|
|
92
|
+
cacheWritePricePerMillion: 0.3,
|
|
93
|
+
contextWindow: 2e5,
|
|
94
|
+
maxOutputTokens: 4096,
|
|
95
|
+
currency: "USD",
|
|
96
|
+
capabilities: {
|
|
97
|
+
vision: true,
|
|
98
|
+
functionCalling: true,
|
|
99
|
+
streaming: true,
|
|
100
|
+
jsonMode: true,
|
|
101
|
+
systemMessage: true
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
// OpenAI Models
|
|
105
|
+
{
|
|
106
|
+
model: "gpt-4o",
|
|
107
|
+
provider: "openai",
|
|
108
|
+
displayName: "GPT-4o",
|
|
109
|
+
inputPricePerMillion: 2.5,
|
|
110
|
+
outputPricePerMillion: 10,
|
|
111
|
+
contextWindow: 128e3,
|
|
112
|
+
maxOutputTokens: 16384,
|
|
113
|
+
currency: "USD",
|
|
114
|
+
capabilities: {
|
|
115
|
+
vision: true,
|
|
116
|
+
functionCalling: true,
|
|
117
|
+
streaming: true,
|
|
118
|
+
jsonMode: true,
|
|
119
|
+
systemMessage: true
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
model: "gpt-4o-mini",
|
|
124
|
+
provider: "openai",
|
|
125
|
+
displayName: "GPT-4o Mini",
|
|
126
|
+
inputPricePerMillion: 0.15,
|
|
127
|
+
outputPricePerMillion: 0.6,
|
|
128
|
+
contextWindow: 128e3,
|
|
129
|
+
maxOutputTokens: 16384,
|
|
130
|
+
currency: "USD",
|
|
131
|
+
capabilities: {
|
|
132
|
+
vision: true,
|
|
133
|
+
functionCalling: true,
|
|
134
|
+
streaming: true,
|
|
135
|
+
jsonMode: true,
|
|
136
|
+
systemMessage: true
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
model: "gpt-4-turbo",
|
|
141
|
+
provider: "openai",
|
|
142
|
+
displayName: "GPT-4 Turbo",
|
|
143
|
+
inputPricePerMillion: 10,
|
|
144
|
+
outputPricePerMillion: 30,
|
|
145
|
+
contextWindow: 128e3,
|
|
146
|
+
maxOutputTokens: 4096,
|
|
147
|
+
currency: "USD",
|
|
148
|
+
capabilities: {
|
|
149
|
+
vision: true,
|
|
150
|
+
functionCalling: true,
|
|
151
|
+
streaming: true,
|
|
152
|
+
jsonMode: true,
|
|
153
|
+
systemMessage: true
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
model: "gpt-3.5-turbo",
|
|
158
|
+
provider: "openai",
|
|
159
|
+
displayName: "GPT-3.5 Turbo",
|
|
160
|
+
inputPricePerMillion: 0.5,
|
|
161
|
+
outputPricePerMillion: 1.5,
|
|
162
|
+
contextWindow: 16385,
|
|
163
|
+
maxOutputTokens: 4096,
|
|
164
|
+
currency: "USD",
|
|
165
|
+
capabilities: {
|
|
166
|
+
functionCalling: true,
|
|
167
|
+
streaming: true,
|
|
168
|
+
jsonMode: true,
|
|
169
|
+
systemMessage: true
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
model: "o1-preview",
|
|
174
|
+
provider: "openai",
|
|
175
|
+
displayName: "o1 Preview",
|
|
176
|
+
inputPricePerMillion: 15,
|
|
177
|
+
outputPricePerMillion: 60,
|
|
178
|
+
contextWindow: 128e3,
|
|
179
|
+
maxOutputTokens: 32768,
|
|
180
|
+
currency: "USD",
|
|
181
|
+
capabilities: {
|
|
182
|
+
streaming: true,
|
|
183
|
+
extendedThinking: true
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
model: "o1-mini",
|
|
188
|
+
provider: "openai",
|
|
189
|
+
displayName: "o1 Mini",
|
|
190
|
+
inputPricePerMillion: 3,
|
|
191
|
+
outputPricePerMillion: 12,
|
|
192
|
+
contextWindow: 128e3,
|
|
193
|
+
maxOutputTokens: 65536,
|
|
194
|
+
currency: "USD",
|
|
195
|
+
capabilities: {
|
|
196
|
+
streaming: true,
|
|
197
|
+
extendedThinking: true
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
// Google Models
|
|
201
|
+
{
|
|
202
|
+
model: "gemini-1.5-pro",
|
|
203
|
+
provider: "google",
|
|
204
|
+
displayName: "Gemini 1.5 Pro",
|
|
205
|
+
inputPricePerMillion: 1.25,
|
|
206
|
+
outputPricePerMillion: 5,
|
|
207
|
+
contextWindow: 2e6,
|
|
208
|
+
maxOutputTokens: 8192,
|
|
209
|
+
currency: "USD",
|
|
210
|
+
capabilities: {
|
|
211
|
+
vision: true,
|
|
212
|
+
functionCalling: true,
|
|
213
|
+
streaming: true,
|
|
214
|
+
jsonMode: true,
|
|
215
|
+
systemMessage: true
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
model: "gemini-1.5-flash",
|
|
220
|
+
provider: "google",
|
|
221
|
+
displayName: "Gemini 1.5 Flash",
|
|
222
|
+
inputPricePerMillion: 0.075,
|
|
223
|
+
outputPricePerMillion: 0.3,
|
|
224
|
+
contextWindow: 1e6,
|
|
225
|
+
maxOutputTokens: 8192,
|
|
226
|
+
currency: "USD",
|
|
227
|
+
capabilities: {
|
|
228
|
+
vision: true,
|
|
229
|
+
functionCalling: true,
|
|
230
|
+
streaming: true,
|
|
231
|
+
jsonMode: true,
|
|
232
|
+
systemMessage: true
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
model: "gemini-2.0-flash-exp",
|
|
237
|
+
provider: "google",
|
|
238
|
+
displayName: "Gemini 2.0 Flash",
|
|
239
|
+
inputPricePerMillion: 0,
|
|
240
|
+
outputPricePerMillion: 0,
|
|
241
|
+
contextWindow: 1e6,
|
|
242
|
+
maxOutputTokens: 8192,
|
|
243
|
+
currency: "USD",
|
|
244
|
+
capabilities: {
|
|
245
|
+
vision: true,
|
|
246
|
+
functionCalling: true,
|
|
247
|
+
streaming: true,
|
|
248
|
+
jsonMode: true,
|
|
249
|
+
systemMessage: true
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
// Mistral Models
|
|
253
|
+
{
|
|
254
|
+
model: "mistral-large-latest",
|
|
255
|
+
provider: "mistral",
|
|
256
|
+
displayName: "Mistral Large",
|
|
257
|
+
inputPricePerMillion: 2,
|
|
258
|
+
outputPricePerMillion: 6,
|
|
259
|
+
contextWindow: 128e3,
|
|
260
|
+
maxOutputTokens: 4096,
|
|
261
|
+
currency: "USD",
|
|
262
|
+
capabilities: {
|
|
263
|
+
functionCalling: true,
|
|
264
|
+
streaming: true,
|
|
265
|
+
jsonMode: true,
|
|
266
|
+
systemMessage: true
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
model: "mistral-small-latest",
|
|
271
|
+
provider: "mistral",
|
|
272
|
+
displayName: "Mistral Small",
|
|
273
|
+
inputPricePerMillion: 0.2,
|
|
274
|
+
outputPricePerMillion: 0.6,
|
|
275
|
+
contextWindow: 32e3,
|
|
276
|
+
maxOutputTokens: 4096,
|
|
277
|
+
currency: "USD",
|
|
278
|
+
capabilities: {
|
|
279
|
+
functionCalling: true,
|
|
280
|
+
streaming: true,
|
|
281
|
+
jsonMode: true,
|
|
282
|
+
systemMessage: true
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
model: "codestral-latest",
|
|
287
|
+
provider: "mistral",
|
|
288
|
+
displayName: "Codestral",
|
|
289
|
+
inputPricePerMillion: 0.2,
|
|
290
|
+
outputPricePerMillion: 0.6,
|
|
291
|
+
contextWindow: 32e3,
|
|
292
|
+
maxOutputTokens: 4096,
|
|
293
|
+
currency: "USD",
|
|
294
|
+
capabilities: {
|
|
295
|
+
streaming: true,
|
|
296
|
+
systemMessage: true
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
// Cohere Models
|
|
300
|
+
{
|
|
301
|
+
model: "command-r-plus",
|
|
302
|
+
provider: "cohere",
|
|
303
|
+
displayName: "Command R+",
|
|
304
|
+
inputPricePerMillion: 2.5,
|
|
305
|
+
outputPricePerMillion: 10,
|
|
306
|
+
contextWindow: 128e3,
|
|
307
|
+
maxOutputTokens: 4096,
|
|
308
|
+
currency: "USD",
|
|
309
|
+
capabilities: {
|
|
310
|
+
functionCalling: true,
|
|
311
|
+
streaming: true,
|
|
312
|
+
systemMessage: true
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
model: "command-r",
|
|
317
|
+
provider: "cohere",
|
|
318
|
+
displayName: "Command R",
|
|
319
|
+
inputPricePerMillion: 0.15,
|
|
320
|
+
outputPricePerMillion: 0.6,
|
|
321
|
+
contextWindow: 128e3,
|
|
322
|
+
maxOutputTokens: 4096,
|
|
323
|
+
currency: "USD",
|
|
324
|
+
capabilities: {
|
|
325
|
+
functionCalling: true,
|
|
326
|
+
streaming: true,
|
|
327
|
+
systemMessage: true
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
];
|
|
331
|
+
var ModelPricingRegistry = class {
|
|
332
|
+
pricing = /* @__PURE__ */ new Map();
|
|
333
|
+
config;
|
|
334
|
+
updateTimer;
|
|
335
|
+
constructor(config = {}) {
|
|
336
|
+
this.config = {
|
|
337
|
+
autoUpdate: config.autoUpdate ?? false,
|
|
338
|
+
updateInterval: config.updateInterval ?? 24 * 60 * 60 * 1e3,
|
|
339
|
+
// 24 hours
|
|
340
|
+
defaultCurrency: config.defaultCurrency ?? "USD",
|
|
341
|
+
...config
|
|
342
|
+
};
|
|
343
|
+
this.loadDefaultPricing();
|
|
344
|
+
if (config.customPricing) {
|
|
345
|
+
for (const pricing of config.customPricing) {
|
|
346
|
+
this.registerModel(pricing);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (this.config.autoUpdate && this.config.remotePricingUrl) {
|
|
350
|
+
this.startAutoUpdate();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Load default pricing data
|
|
355
|
+
*/
|
|
356
|
+
loadDefaultPricing() {
|
|
357
|
+
for (const pricing of DEFAULT_PRICING) {
|
|
358
|
+
const key = this.getKey(pricing.provider, pricing.model);
|
|
359
|
+
this.pricing.set(key, pricing);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Generate key for pricing lookup
|
|
364
|
+
*/
|
|
365
|
+
getKey(provider, model) {
|
|
366
|
+
return `${provider}:${model}`;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Register or update model pricing
|
|
370
|
+
*/
|
|
371
|
+
registerModel(pricing) {
|
|
372
|
+
const key = this.getKey(pricing.provider, pricing.model);
|
|
373
|
+
this.pricing.set(key, {
|
|
374
|
+
...pricing,
|
|
375
|
+
effectiveDate: pricing.effectiveDate ?? /* @__PURE__ */ new Date()
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Get pricing for a model
|
|
380
|
+
*/
|
|
381
|
+
getPricing(provider, model) {
|
|
382
|
+
const key = this.getKey(provider, model);
|
|
383
|
+
return this.pricing.get(key) ?? null;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Get pricing by model name (auto-detect provider)
|
|
387
|
+
*/
|
|
388
|
+
getPricingByModel(model) {
|
|
389
|
+
for (const pricing of this.pricing.values()) {
|
|
390
|
+
if (pricing.model === model) {
|
|
391
|
+
return pricing;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
for (const pricing of this.pricing.values()) {
|
|
395
|
+
if (pricing.model.includes(model) || model.includes(pricing.model)) {
|
|
396
|
+
return pricing;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Calculate cost for token usage
|
|
403
|
+
*/
|
|
404
|
+
calculateCost(provider, model, inputTokens, outputTokens, options) {
|
|
405
|
+
const pricing = this.getPricing(provider, model);
|
|
406
|
+
if (!pricing) {
|
|
407
|
+
throw new Error(`No pricing found for ${provider}:${model}`);
|
|
408
|
+
}
|
|
409
|
+
const inputCost = inputTokens / 1e6 * pricing.inputPricePerMillion;
|
|
410
|
+
const outputCost = outputTokens / 1e6 * pricing.outputPricePerMillion;
|
|
411
|
+
const cacheReadCost = options?.cacheReadTokens ? options.cacheReadTokens / 1e6 * (pricing.cacheReadPricePerMillion ?? 0) : 0;
|
|
412
|
+
const cacheCost = options?.cacheWriteTokens ? options.cacheWriteTokens / 1e6 * (pricing.cacheWritePricePerMillion ?? 0) : 0;
|
|
413
|
+
return {
|
|
414
|
+
inputCost,
|
|
415
|
+
outputCost,
|
|
416
|
+
cacheReadCost,
|
|
417
|
+
cacheCost,
|
|
418
|
+
totalCost: inputCost + outputCost + cacheReadCost + cacheCost,
|
|
419
|
+
currency: pricing.currency
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* List all models for a provider
|
|
424
|
+
*/
|
|
425
|
+
listModels(provider) {
|
|
426
|
+
const models = [];
|
|
427
|
+
for (const pricing of this.pricing.values()) {
|
|
428
|
+
if (!provider || pricing.provider === provider) {
|
|
429
|
+
models.push(pricing);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return models.sort((a, b) => a.model.localeCompare(b.model));
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* List all providers
|
|
436
|
+
*/
|
|
437
|
+
listProviders() {
|
|
438
|
+
const providers = /* @__PURE__ */ new Set();
|
|
439
|
+
for (const pricing of this.pricing.values()) {
|
|
440
|
+
providers.add(pricing.provider);
|
|
441
|
+
}
|
|
442
|
+
return Array.from(providers).sort();
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Get provider pricing summary
|
|
446
|
+
*/
|
|
447
|
+
getProviderSummary(provider) {
|
|
448
|
+
const models = this.listModels(provider);
|
|
449
|
+
if (models.length === 0) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
const inputPrices = models.map((m) => m.inputPricePerMillion);
|
|
453
|
+
const outputPrices = models.map((m) => m.outputPricePerMillion);
|
|
454
|
+
return {
|
|
455
|
+
provider,
|
|
456
|
+
modelCount: models.length,
|
|
457
|
+
minInputPrice: Math.min(...inputPrices),
|
|
458
|
+
maxInputPrice: Math.max(...inputPrices),
|
|
459
|
+
minOutputPrice: Math.min(...outputPrices),
|
|
460
|
+
maxOutputPrice: Math.max(...outputPrices),
|
|
461
|
+
models: models.map((m) => m.model)
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Compare pricing between two models
|
|
466
|
+
*/
|
|
467
|
+
comparePricing(modelA, modelB, sampleTokens) {
|
|
468
|
+
const pricingA = this.getPricingByModel(modelA);
|
|
469
|
+
const pricingB = this.getPricingByModel(modelB);
|
|
470
|
+
if (!pricingA || !pricingB) {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
const inputDiff = pricingA.inputPricePerMillion - pricingB.inputPricePerMillion;
|
|
474
|
+
const outputDiff = pricingA.outputPricePerMillion - pricingB.outputPricePerMillion;
|
|
475
|
+
const avgPriceA = (pricingA.inputPricePerMillion + pricingA.outputPricePerMillion) / 2;
|
|
476
|
+
const avgPriceB = (pricingB.inputPricePerMillion + pricingB.outputPricePerMillion) / 2;
|
|
477
|
+
const percentageDiff = (avgPriceA - avgPriceB) / avgPriceB * 100;
|
|
478
|
+
let estimatedSavings;
|
|
479
|
+
if (sampleTokens) {
|
|
480
|
+
const costA = sampleTokens.input / 1e6 * pricingA.inputPricePerMillion + sampleTokens.output / 1e6 * pricingA.outputPricePerMillion;
|
|
481
|
+
const costB = sampleTokens.input / 1e6 * pricingB.inputPricePerMillion + sampleTokens.output / 1e6 * pricingB.outputPricePerMillion;
|
|
482
|
+
estimatedSavings = Math.abs(costA - costB);
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
modelA,
|
|
486
|
+
modelB,
|
|
487
|
+
inputPriceDiff: inputDiff,
|
|
488
|
+
outputPriceDiff: outputDiff,
|
|
489
|
+
percentageDiff,
|
|
490
|
+
cheaperModel: avgPriceA < avgPriceB ? modelA : modelB,
|
|
491
|
+
estimatedSavings
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Find cheapest model with required capabilities
|
|
496
|
+
*/
|
|
497
|
+
findCheapestModel(options) {
|
|
498
|
+
const weightInput = options?.weightInput ?? 0.5;
|
|
499
|
+
const weightOutput = options?.weightOutput ?? 0.5;
|
|
500
|
+
let cheapest = null;
|
|
501
|
+
let cheapestScore = Infinity;
|
|
502
|
+
for (const pricing of this.pricing.values()) {
|
|
503
|
+
if (options?.provider && pricing.provider !== options.provider) {
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (options?.minContextWindow && pricing.contextWindow && pricing.contextWindow < options.minContextWindow) {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
if (options?.requireVision && !pricing.capabilities?.vision) {
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (options?.requireFunctionCalling && !pricing.capabilities?.functionCalling) {
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (pricing.deprecated) {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const score = pricing.inputPricePerMillion * weightInput + pricing.outputPricePerMillion * weightOutput;
|
|
519
|
+
if (score < cheapestScore) {
|
|
520
|
+
cheapestScore = score;
|
|
521
|
+
cheapest = pricing;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return cheapest;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Start auto-update timer
|
|
528
|
+
*/
|
|
529
|
+
startAutoUpdate() {
|
|
530
|
+
if (this.updateTimer) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
this.updateTimer = setInterval(() => {
|
|
534
|
+
void (async () => {
|
|
535
|
+
try {
|
|
536
|
+
await this.updateFromRemote();
|
|
537
|
+
} catch {
|
|
538
|
+
}
|
|
539
|
+
})();
|
|
540
|
+
}, this.config.updateInterval);
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Stop auto-update timer
|
|
544
|
+
*/
|
|
545
|
+
stopAutoUpdate() {
|
|
546
|
+
if (this.updateTimer) {
|
|
547
|
+
clearInterval(this.updateTimer);
|
|
548
|
+
this.updateTimer = void 0;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Update pricing from remote source
|
|
553
|
+
*/
|
|
554
|
+
async updateFromRemote() {
|
|
555
|
+
if (!this.config.remotePricingUrl) {
|
|
556
|
+
throw new Error("No remote pricing URL configured");
|
|
557
|
+
}
|
|
558
|
+
const response = await fetch(this.config.remotePricingUrl);
|
|
559
|
+
if (!response.ok) {
|
|
560
|
+
throw new Error(`Failed to fetch pricing: ${response.statusText}`);
|
|
561
|
+
}
|
|
562
|
+
const data = await response.json();
|
|
563
|
+
for (const pricing of data) {
|
|
564
|
+
this.registerModel(pricing);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Export all pricing data
|
|
569
|
+
*/
|
|
570
|
+
exportPricing() {
|
|
571
|
+
return Array.from(this.pricing.values());
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Import pricing data
|
|
575
|
+
*/
|
|
576
|
+
importPricing(data, replace = false) {
|
|
577
|
+
if (replace) {
|
|
578
|
+
this.pricing.clear();
|
|
579
|
+
}
|
|
580
|
+
for (const pricing of data) {
|
|
581
|
+
this.registerModel(pricing);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Clear all pricing data
|
|
586
|
+
*/
|
|
587
|
+
clear() {
|
|
588
|
+
this.pricing.clear();
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Reload default pricing
|
|
592
|
+
*/
|
|
593
|
+
reset() {
|
|
594
|
+
this.clear();
|
|
595
|
+
this.loadDefaultPricing();
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
// src/pricing/TokenCounter.ts
|
|
600
|
+
var cl100kEncoder = null;
|
|
601
|
+
async function getEncoder() {
|
|
602
|
+
if (!cl100kEncoder) {
|
|
603
|
+
try {
|
|
604
|
+
const tiktoken = await import('tiktoken');
|
|
605
|
+
cl100kEncoder = tiktoken.get_encoding("cl100k_base");
|
|
606
|
+
} catch {
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return cl100kEncoder;
|
|
611
|
+
}
|
|
612
|
+
var PROVIDER_STRATEGIES = {
|
|
613
|
+
openai: { encoding: "tiktoken", charsPerToken: 4 },
|
|
614
|
+
anthropic: { encoding: "approximate", charsPerToken: 3.5 },
|
|
615
|
+
google: { encoding: "approximate", charsPerToken: 4 },
|
|
616
|
+
azure: { encoding: "tiktoken", charsPerToken: 4 },
|
|
617
|
+
bedrock: { encoding: "approximate", charsPerToken: 3.5 },
|
|
618
|
+
cohere: { encoding: "approximate", charsPerToken: 4 },
|
|
619
|
+
mistral: { encoding: "approximate", charsPerToken: 4 },
|
|
620
|
+
replicate: { encoding: "approximate", charsPerToken: 4 },
|
|
621
|
+
custom: { encoding: "approximate", charsPerToken: 4 }
|
|
622
|
+
};
|
|
623
|
+
var TokenCounter = class {
|
|
624
|
+
pricingRegistry;
|
|
625
|
+
cache = /* @__PURE__ */ new Map();
|
|
626
|
+
maxCacheSize;
|
|
627
|
+
constructor(pricingRegistry, options) {
|
|
628
|
+
this.pricingRegistry = pricingRegistry;
|
|
629
|
+
this.maxCacheSize = options?.maxCacheSize ?? 1e3;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Count tokens in text
|
|
633
|
+
*/
|
|
634
|
+
async countTokens(request) {
|
|
635
|
+
const { text, model, provider } = request;
|
|
636
|
+
const effectiveProvider = provider ?? this.detectProvider(model);
|
|
637
|
+
const cacheKey = `${effectiveProvider}:${text.substring(0, 100)}:${text.length}`;
|
|
638
|
+
const cached = this.cache.get(cacheKey);
|
|
639
|
+
if (cached !== void 0) {
|
|
640
|
+
return this.buildResult(
|
|
641
|
+
text,
|
|
642
|
+
cached,
|
|
643
|
+
model ?? "default",
|
|
644
|
+
effectiveProvider
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
let tokens;
|
|
648
|
+
const strategy = PROVIDER_STRATEGIES[effectiveProvider];
|
|
649
|
+
if (strategy.encoding === "tiktoken") {
|
|
650
|
+
tokens = await this.countWithTiktoken(text, strategy.charsPerToken);
|
|
651
|
+
} else {
|
|
652
|
+
tokens = this.countApproximate(text, strategy.charsPerToken);
|
|
653
|
+
}
|
|
654
|
+
this.setCached(cacheKey, tokens);
|
|
655
|
+
return this.buildResult(
|
|
656
|
+
text,
|
|
657
|
+
tokens,
|
|
658
|
+
model ?? "default",
|
|
659
|
+
effectiveProvider
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Count tokens using tiktoken
|
|
664
|
+
*/
|
|
665
|
+
async countWithTiktoken(text, fallbackCharsPerToken) {
|
|
666
|
+
const encoder = await getEncoder();
|
|
667
|
+
if (encoder) {
|
|
668
|
+
try {
|
|
669
|
+
const encoded = encoder.encode(text);
|
|
670
|
+
return encoded.length;
|
|
671
|
+
} catch {
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return this.countApproximate(text, fallbackCharsPerToken);
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Count tokens using approximation
|
|
678
|
+
*/
|
|
679
|
+
countApproximate(text, charsPerToken) {
|
|
680
|
+
let adjustedLength = 0;
|
|
681
|
+
for (let i = 0; i < text.length; i++) {
|
|
682
|
+
const char = text[i];
|
|
683
|
+
const code = char.charCodeAt(0);
|
|
684
|
+
if (/[\s.,!?;:'"]/.test(char)) {
|
|
685
|
+
adjustedLength += 0.5;
|
|
686
|
+
} else if (/\d/.test(char)) {
|
|
687
|
+
adjustedLength += 0.7;
|
|
688
|
+
} else if (code > 127) {
|
|
689
|
+
adjustedLength += 2;
|
|
690
|
+
} else {
|
|
691
|
+
adjustedLength += 1;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return Math.ceil(adjustedLength / charsPerToken);
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Build token count result
|
|
698
|
+
*/
|
|
699
|
+
buildResult(text, tokens, model, _provider) {
|
|
700
|
+
const words = text.split(/\s+/).filter((w) => w.length > 0).length;
|
|
701
|
+
const characters = text.length;
|
|
702
|
+
let estimatedInputCost;
|
|
703
|
+
const pricing = this.pricingRegistry.getPricingByModel(model);
|
|
704
|
+
if (pricing) {
|
|
705
|
+
estimatedInputCost = tokens / 1e6 * pricing.inputPricePerMillion;
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
tokens,
|
|
709
|
+
model,
|
|
710
|
+
estimatedInputCost,
|
|
711
|
+
characters,
|
|
712
|
+
words
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Detect provider from model name
|
|
717
|
+
*/
|
|
718
|
+
detectProvider(model) {
|
|
719
|
+
if (!model) return "openai";
|
|
720
|
+
const modelLower = model.toLowerCase();
|
|
721
|
+
if (modelLower.includes("claude")) return "anthropic";
|
|
722
|
+
if (modelLower.includes("gpt") || modelLower.includes("o1"))
|
|
723
|
+
return "openai";
|
|
724
|
+
if (modelLower.includes("gemini")) return "google";
|
|
725
|
+
if (modelLower.includes("mistral") || modelLower.includes("codestral"))
|
|
726
|
+
return "mistral";
|
|
727
|
+
if (modelLower.includes("command")) return "cohere";
|
|
728
|
+
return "openai";
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Estimate cost for a request
|
|
732
|
+
*/
|
|
733
|
+
async estimateCost(request) {
|
|
734
|
+
const { input, estimatedOutputTokens = 500, model, provider } = request;
|
|
735
|
+
let inputTokens;
|
|
736
|
+
if (typeof input === "number") {
|
|
737
|
+
inputTokens = input;
|
|
738
|
+
} else {
|
|
739
|
+
const countResult = await this.countTokens({
|
|
740
|
+
text: input,
|
|
741
|
+
model,
|
|
742
|
+
provider
|
|
743
|
+
});
|
|
744
|
+
inputTokens = countResult.tokens;
|
|
745
|
+
}
|
|
746
|
+
const effectiveProvider = provider ?? this.detectProvider(model);
|
|
747
|
+
const pricing = this.pricingRegistry.getPricing(effectiveProvider, model);
|
|
748
|
+
if (!pricing) {
|
|
749
|
+
throw new Error(`No pricing found for ${effectiveProvider}:${model}`);
|
|
750
|
+
}
|
|
751
|
+
const inputCost = inputTokens / 1e6 * pricing.inputPricePerMillion;
|
|
752
|
+
const outputCost = estimatedOutputTokens / 1e6 * pricing.outputPricePerMillion;
|
|
753
|
+
const cacheCost = request.includeCache ? inputTokens / 1e6 * (pricing.cacheWritePricePerMillion ?? 0) : void 0;
|
|
754
|
+
const estimatedCost = inputCost + outputCost + (cacheCost ?? 0);
|
|
755
|
+
const confidence = estimatedOutputTokens === 500 ? 0.7 : 0.85;
|
|
756
|
+
return {
|
|
757
|
+
estimatedCost,
|
|
758
|
+
inputTokens,
|
|
759
|
+
outputTokens: estimatedOutputTokens,
|
|
760
|
+
breakdown: {
|
|
761
|
+
inputCost,
|
|
762
|
+
outputCost,
|
|
763
|
+
cacheCost
|
|
764
|
+
},
|
|
765
|
+
model,
|
|
766
|
+
provider: effectiveProvider,
|
|
767
|
+
currency: pricing.currency,
|
|
768
|
+
confidence
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Batch count tokens
|
|
773
|
+
*/
|
|
774
|
+
async countTokensBatch(texts, options) {
|
|
775
|
+
return Promise.all(
|
|
776
|
+
texts.map(
|
|
777
|
+
(text) => this.countTokens({
|
|
778
|
+
text,
|
|
779
|
+
model: options?.model,
|
|
780
|
+
provider: options?.provider
|
|
781
|
+
})
|
|
782
|
+
)
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Count tokens for messages (chat format)
|
|
787
|
+
*/
|
|
788
|
+
async countMessagesTokens(messages, options) {
|
|
789
|
+
const perMessage = [];
|
|
790
|
+
let totalContent = 0;
|
|
791
|
+
for (const message of messages) {
|
|
792
|
+
const result = await this.countTokens({
|
|
793
|
+
text: message.content,
|
|
794
|
+
model: options?.model,
|
|
795
|
+
provider: options?.provider
|
|
796
|
+
});
|
|
797
|
+
perMessage.push({ role: message.role, tokens: result.tokens });
|
|
798
|
+
totalContent += result.tokens;
|
|
799
|
+
}
|
|
800
|
+
const overhead = messages.length * 4 + 3;
|
|
801
|
+
const totalTokens = totalContent + overhead;
|
|
802
|
+
return {
|
|
803
|
+
totalTokens,
|
|
804
|
+
perMessage,
|
|
805
|
+
overhead
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Set cached value
|
|
810
|
+
*/
|
|
811
|
+
setCached(key, value) {
|
|
812
|
+
if (this.cache.size >= this.maxCacheSize) {
|
|
813
|
+
const firstKey = this.cache.keys().next().value;
|
|
814
|
+
if (firstKey) {
|
|
815
|
+
this.cache.delete(firstKey);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
this.cache.set(key, value);
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Clear cache
|
|
822
|
+
*/
|
|
823
|
+
clearCache() {
|
|
824
|
+
this.cache.clear();
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Get cache stats
|
|
828
|
+
*/
|
|
829
|
+
getCacheStats() {
|
|
830
|
+
return {
|
|
831
|
+
size: this.cache.size,
|
|
832
|
+
maxSize: this.maxCacheSize,
|
|
833
|
+
hitRate: 0
|
|
834
|
+
// Would need to track hits/misses for accurate rate
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
async function countTokens(text, options) {
|
|
839
|
+
const provider = options?.provider ?? "openai";
|
|
840
|
+
const strategy = PROVIDER_STRATEGIES[provider];
|
|
841
|
+
if (strategy.encoding === "tiktoken") {
|
|
842
|
+
const encoder = await getEncoder();
|
|
843
|
+
if (encoder) {
|
|
844
|
+
try {
|
|
845
|
+
return encoder.encode(text).length;
|
|
846
|
+
} catch {
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return Math.ceil(text.length / strategy.charsPerToken);
|
|
851
|
+
}
|
|
852
|
+
function countTokensApprox(text, charsPerToken = 4) {
|
|
853
|
+
return Math.ceil(text.length / charsPerToken);
|
|
854
|
+
}
|
|
855
|
+
var CostTracker = class extends EventEmitter {
|
|
856
|
+
pricingRegistry;
|
|
857
|
+
storage;
|
|
858
|
+
defaultAttribution;
|
|
859
|
+
buffer = [];
|
|
860
|
+
bufferSize;
|
|
861
|
+
autoFlushTimer;
|
|
862
|
+
realTimeEvents;
|
|
863
|
+
constructor(config) {
|
|
864
|
+
super();
|
|
865
|
+
this.pricingRegistry = config.pricingRegistry;
|
|
866
|
+
this.storage = config.storage;
|
|
867
|
+
this.defaultAttribution = config.defaultAttribution;
|
|
868
|
+
this.bufferSize = config.bufferSize ?? 100;
|
|
869
|
+
this.realTimeEvents = config.realTimeEvents ?? true;
|
|
870
|
+
if (config.autoFlushInterval && config.autoFlushInterval > 0) {
|
|
871
|
+
this.autoFlushTimer = setInterval(() => {
|
|
872
|
+
this.flush().catch((err) => {
|
|
873
|
+
this.emit("error", { message: "Auto-flush failed", cause: err });
|
|
874
|
+
});
|
|
875
|
+
}, config.autoFlushInterval);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Track an API call
|
|
880
|
+
*/
|
|
881
|
+
async track(options) {
|
|
882
|
+
const cost = this.calculateCost(
|
|
883
|
+
options.provider,
|
|
884
|
+
options.model,
|
|
885
|
+
options.tokens
|
|
886
|
+
);
|
|
887
|
+
const record = {
|
|
888
|
+
id: nanoid(),
|
|
889
|
+
timestamp: options.timestamp ?? /* @__PURE__ */ new Date(),
|
|
890
|
+
provider: options.provider,
|
|
891
|
+
model: options.model,
|
|
892
|
+
tokens: options.tokens,
|
|
893
|
+
cost,
|
|
894
|
+
latencyMs: options.latencyMs,
|
|
895
|
+
success: options.success ?? true,
|
|
896
|
+
error: options.error,
|
|
897
|
+
attribution: this.mergeAttribution(options.attribution),
|
|
898
|
+
metadata: options.metadata
|
|
899
|
+
};
|
|
900
|
+
this.buffer.push(record);
|
|
901
|
+
if (this.realTimeEvents) {
|
|
902
|
+
this.emit("cost:recorded", record);
|
|
903
|
+
}
|
|
904
|
+
if (this.buffer.length >= this.bufferSize) {
|
|
905
|
+
await this.flush();
|
|
906
|
+
}
|
|
907
|
+
return record;
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Track from Anthropic API response
|
|
911
|
+
*/
|
|
912
|
+
async trackAnthropicResponse(response, options) {
|
|
913
|
+
const usage = response.usage ?? { input_tokens: 0, output_tokens: 0 };
|
|
914
|
+
return this.track({
|
|
915
|
+
provider: "anthropic",
|
|
916
|
+
model: response.model,
|
|
917
|
+
tokens: {
|
|
918
|
+
inputTokens: usage.input_tokens,
|
|
919
|
+
outputTokens: usage.output_tokens,
|
|
920
|
+
totalTokens: usage.input_tokens + usage.output_tokens,
|
|
921
|
+
cacheReadTokens: usage.cache_read_input_tokens,
|
|
922
|
+
cacheWriteTokens: usage.cache_creation_input_tokens
|
|
923
|
+
},
|
|
924
|
+
latencyMs: options?.latencyMs,
|
|
925
|
+
attribution: options?.attribution,
|
|
926
|
+
metadata: options?.metadata
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Track from OpenAI API response
|
|
931
|
+
*/
|
|
932
|
+
async trackOpenAIResponse(response, options) {
|
|
933
|
+
const usage = response.usage ?? {
|
|
934
|
+
prompt_tokens: 0,
|
|
935
|
+
completion_tokens: 0,
|
|
936
|
+
total_tokens: 0
|
|
937
|
+
};
|
|
938
|
+
return this.track({
|
|
939
|
+
provider: "openai",
|
|
940
|
+
model: response.model,
|
|
941
|
+
tokens: {
|
|
942
|
+
inputTokens: usage.prompt_tokens,
|
|
943
|
+
outputTokens: usage.completion_tokens,
|
|
944
|
+
totalTokens: usage.total_tokens
|
|
945
|
+
},
|
|
946
|
+
latencyMs: options?.latencyMs,
|
|
947
|
+
attribution: options?.attribution,
|
|
948
|
+
metadata: options?.metadata
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Track a failed request
|
|
953
|
+
*/
|
|
954
|
+
async trackError(options) {
|
|
955
|
+
return this.track({
|
|
956
|
+
provider: options.provider,
|
|
957
|
+
model: options.model,
|
|
958
|
+
tokens: {
|
|
959
|
+
inputTokens: options.estimatedInputTokens ?? 0,
|
|
960
|
+
outputTokens: 0,
|
|
961
|
+
totalTokens: options.estimatedInputTokens ?? 0
|
|
962
|
+
},
|
|
963
|
+
latencyMs: options.latencyMs,
|
|
964
|
+
success: false,
|
|
965
|
+
error: options.error,
|
|
966
|
+
attribution: options.attribution,
|
|
967
|
+
metadata: options.metadata
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Calculate cost for token usage
|
|
972
|
+
*/
|
|
973
|
+
calculateCost(provider, model, tokens) {
|
|
974
|
+
try {
|
|
975
|
+
const result = this.pricingRegistry.calculateCost(
|
|
976
|
+
provider,
|
|
977
|
+
model,
|
|
978
|
+
tokens.inputTokens,
|
|
979
|
+
tokens.outputTokens,
|
|
980
|
+
{
|
|
981
|
+
cacheReadTokens: tokens.cacheReadTokens,
|
|
982
|
+
cacheWriteTokens: tokens.cacheWriteTokens
|
|
983
|
+
}
|
|
984
|
+
);
|
|
985
|
+
return {
|
|
986
|
+
inputCost: result.inputCost,
|
|
987
|
+
outputCost: result.outputCost,
|
|
988
|
+
cacheReadCost: result.cacheReadCost,
|
|
989
|
+
cacheCost: result.cacheCost,
|
|
990
|
+
totalCost: result.totalCost,
|
|
991
|
+
currency: result.currency
|
|
992
|
+
};
|
|
993
|
+
} catch {
|
|
994
|
+
return {
|
|
995
|
+
inputCost: 0,
|
|
996
|
+
outputCost: 0,
|
|
997
|
+
totalCost: 0,
|
|
998
|
+
currency: "USD"
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Merge attribution with defaults
|
|
1004
|
+
*/
|
|
1005
|
+
mergeAttribution(attribution) {
|
|
1006
|
+
if (!attribution && !this.defaultAttribution) {
|
|
1007
|
+
return void 0;
|
|
1008
|
+
}
|
|
1009
|
+
return {
|
|
1010
|
+
...this.defaultAttribution,
|
|
1011
|
+
...attribution,
|
|
1012
|
+
labels: {
|
|
1013
|
+
...this.defaultAttribution?.labels,
|
|
1014
|
+
...attribution?.labels
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Flush buffer to storage
|
|
1020
|
+
*/
|
|
1021
|
+
async flush() {
|
|
1022
|
+
if (this.buffer.length === 0) {
|
|
1023
|
+
return 0;
|
|
1024
|
+
}
|
|
1025
|
+
const records = [...this.buffer];
|
|
1026
|
+
this.buffer = [];
|
|
1027
|
+
if (this.storage) {
|
|
1028
|
+
await this.storage.saveCostRecords(records);
|
|
1029
|
+
}
|
|
1030
|
+
this.emit("cost:batch", { records });
|
|
1031
|
+
return records.length;
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Get buffered records
|
|
1035
|
+
*/
|
|
1036
|
+
getBuffer() {
|
|
1037
|
+
return [...this.buffer];
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Get buffer size
|
|
1041
|
+
*/
|
|
1042
|
+
getBufferSize() {
|
|
1043
|
+
return this.buffer.length;
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Clear buffer without flushing
|
|
1047
|
+
*/
|
|
1048
|
+
clearBuffer() {
|
|
1049
|
+
this.buffer = [];
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Set default attribution
|
|
1053
|
+
*/
|
|
1054
|
+
setDefaultAttribution(attribution) {
|
|
1055
|
+
this.defaultAttribution = attribution;
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Create a scoped tracker with preset attribution
|
|
1059
|
+
*/
|
|
1060
|
+
scoped(attribution) {
|
|
1061
|
+
return new ScopedCostTracker(this, attribution);
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Close tracker
|
|
1065
|
+
*/
|
|
1066
|
+
async close() {
|
|
1067
|
+
if (this.autoFlushTimer) {
|
|
1068
|
+
clearInterval(this.autoFlushTimer);
|
|
1069
|
+
this.autoFlushTimer = void 0;
|
|
1070
|
+
}
|
|
1071
|
+
await this.flush();
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
var ScopedCostTracker = class _ScopedCostTracker {
|
|
1075
|
+
parent;
|
|
1076
|
+
scopeAttribution;
|
|
1077
|
+
constructor(parent, attribution) {
|
|
1078
|
+
this.parent = parent;
|
|
1079
|
+
this.scopeAttribution = attribution;
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Track an API call with scoped attribution
|
|
1083
|
+
*/
|
|
1084
|
+
async track(options) {
|
|
1085
|
+
return this.parent.track({
|
|
1086
|
+
...options,
|
|
1087
|
+
attribution: {
|
|
1088
|
+
...this.scopeAttribution,
|
|
1089
|
+
...options.attribution,
|
|
1090
|
+
labels: {
|
|
1091
|
+
...this.scopeAttribution.labels,
|
|
1092
|
+
...options.attribution?.labels
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Track Anthropic response
|
|
1099
|
+
*/
|
|
1100
|
+
async trackAnthropicResponse(response, options) {
|
|
1101
|
+
return this.parent.trackAnthropicResponse(response, {
|
|
1102
|
+
...options,
|
|
1103
|
+
attribution: {
|
|
1104
|
+
...this.scopeAttribution,
|
|
1105
|
+
...options?.attribution
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Track OpenAI response
|
|
1111
|
+
*/
|
|
1112
|
+
async trackOpenAIResponse(response, options) {
|
|
1113
|
+
return this.parent.trackOpenAIResponse(response, {
|
|
1114
|
+
...options,
|
|
1115
|
+
attribution: {
|
|
1116
|
+
...this.scopeAttribution,
|
|
1117
|
+
...options?.attribution
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Create a nested scope
|
|
1123
|
+
*/
|
|
1124
|
+
scoped(attribution) {
|
|
1125
|
+
return new _ScopedCostTracker(this.parent, {
|
|
1126
|
+
...this.scopeAttribution,
|
|
1127
|
+
...attribution,
|
|
1128
|
+
labels: {
|
|
1129
|
+
...this.scopeAttribution.labels,
|
|
1130
|
+
...attribution.labels
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
|
|
1136
|
+
// src/core/CostManager.ts
|
|
1137
|
+
var CostManager = class extends EventEmitter {
|
|
1138
|
+
storage;
|
|
1139
|
+
pricingRegistry;
|
|
1140
|
+
tokenCounter;
|
|
1141
|
+
tracker;
|
|
1142
|
+
config;
|
|
1143
|
+
initialized = false;
|
|
1144
|
+
constructor(options = {}) {
|
|
1145
|
+
super();
|
|
1146
|
+
this.config = {
|
|
1147
|
+
currency: options.currency ?? "USD",
|
|
1148
|
+
autoFlushInterval: options.autoFlushInterval ?? 3e4,
|
|
1149
|
+
// 30 seconds
|
|
1150
|
+
bufferSize: options.bufferSize ?? 100,
|
|
1151
|
+
realTimeTracking: options.realTimeTracking ?? true,
|
|
1152
|
+
defaultAttribution: options.defaultAttribution
|
|
1153
|
+
};
|
|
1154
|
+
this.storage = options.storage;
|
|
1155
|
+
this.pricingRegistry = options.pricingRegistry ?? new ModelPricingRegistry();
|
|
1156
|
+
this.tokenCounter = new TokenCounter(this.pricingRegistry);
|
|
1157
|
+
this.tracker = new CostTracker({
|
|
1158
|
+
pricingRegistry: this.pricingRegistry,
|
|
1159
|
+
storage: this.storage,
|
|
1160
|
+
defaultAttribution: this.config.defaultAttribution,
|
|
1161
|
+
autoFlushInterval: this.config.autoFlushInterval,
|
|
1162
|
+
bufferSize: this.config.bufferSize,
|
|
1163
|
+
realTimeEvents: this.config.realTimeTracking
|
|
1164
|
+
});
|
|
1165
|
+
this.tracker.on("cost:recorded", (record) => {
|
|
1166
|
+
this.emit("cost:recorded", record);
|
|
1167
|
+
});
|
|
1168
|
+
this.tracker.on("cost:batch", (records) => {
|
|
1169
|
+
this.emit("cost:batch", records);
|
|
1170
|
+
});
|
|
1171
|
+
this.tracker.on("error", (error) => {
|
|
1172
|
+
this.emit("error", error);
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Initialize the cost manager
|
|
1177
|
+
*/
|
|
1178
|
+
async initialize() {
|
|
1179
|
+
if (this.initialized) return;
|
|
1180
|
+
if (this.storage) {
|
|
1181
|
+
await this.storage.initialize();
|
|
1182
|
+
}
|
|
1183
|
+
this.initialized = true;
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Close the cost manager
|
|
1187
|
+
*/
|
|
1188
|
+
async close() {
|
|
1189
|
+
await this.tracker.close();
|
|
1190
|
+
if (this.storage) {
|
|
1191
|
+
await this.storage.close();
|
|
1192
|
+
}
|
|
1193
|
+
this.initialized = false;
|
|
1194
|
+
}
|
|
1195
|
+
// ==================== Tracking ====================
|
|
1196
|
+
/**
|
|
1197
|
+
* Track an API call
|
|
1198
|
+
*/
|
|
1199
|
+
async track(options) {
|
|
1200
|
+
return this.tracker.track(options);
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Track from Anthropic API response
|
|
1204
|
+
*/
|
|
1205
|
+
async trackAnthropicResponse(response, options) {
|
|
1206
|
+
return this.tracker.trackAnthropicResponse(response, options);
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Track from OpenAI API response
|
|
1210
|
+
*/
|
|
1211
|
+
async trackOpenAIResponse(response, options) {
|
|
1212
|
+
return this.tracker.trackOpenAIResponse(response, options);
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Track a failed request
|
|
1216
|
+
*/
|
|
1217
|
+
async trackError(options) {
|
|
1218
|
+
return this.tracker.trackError(options);
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Create a scoped tracker
|
|
1222
|
+
*/
|
|
1223
|
+
scoped(attribution) {
|
|
1224
|
+
return this.tracker.scoped(attribution);
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Flush pending records to storage
|
|
1228
|
+
*/
|
|
1229
|
+
async flush() {
|
|
1230
|
+
return this.tracker.flush();
|
|
1231
|
+
}
|
|
1232
|
+
// ==================== Token Counting ====================
|
|
1233
|
+
/**
|
|
1234
|
+
* Count tokens in text
|
|
1235
|
+
*/
|
|
1236
|
+
async countTokens(text, options) {
|
|
1237
|
+
const result = await this.tokenCounter.countTokens({
|
|
1238
|
+
text,
|
|
1239
|
+
model: options?.model,
|
|
1240
|
+
provider: options?.provider
|
|
1241
|
+
});
|
|
1242
|
+
return result.tokens;
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Estimate cost before making a request
|
|
1246
|
+
*/
|
|
1247
|
+
async estimateCost(input, options) {
|
|
1248
|
+
const result = await this.tokenCounter.estimateCost({
|
|
1249
|
+
input,
|
|
1250
|
+
model: options.model,
|
|
1251
|
+
provider: options.provider,
|
|
1252
|
+
estimatedOutputTokens: options.estimatedOutputTokens
|
|
1253
|
+
});
|
|
1254
|
+
return {
|
|
1255
|
+
estimatedCost: result.estimatedCost,
|
|
1256
|
+
inputTokens: result.inputTokens,
|
|
1257
|
+
outputTokens: result.outputTokens,
|
|
1258
|
+
currency: result.currency
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
// ==================== Pricing ====================
|
|
1262
|
+
/**
|
|
1263
|
+
* Get pricing registry
|
|
1264
|
+
*/
|
|
1265
|
+
getPricingRegistry() {
|
|
1266
|
+
return this.pricingRegistry;
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Get token counter
|
|
1270
|
+
*/
|
|
1271
|
+
getTokenCounter() {
|
|
1272
|
+
return this.tokenCounter;
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Calculate cost for token usage
|
|
1276
|
+
*/
|
|
1277
|
+
calculateCost(provider, model, tokens) {
|
|
1278
|
+
const result = this.pricingRegistry.calculateCost(
|
|
1279
|
+
provider,
|
|
1280
|
+
model,
|
|
1281
|
+
tokens.inputTokens,
|
|
1282
|
+
tokens.outputTokens,
|
|
1283
|
+
{
|
|
1284
|
+
cacheReadTokens: tokens.cacheReadTokens,
|
|
1285
|
+
cacheWriteTokens: tokens.cacheWriteTokens
|
|
1286
|
+
}
|
|
1287
|
+
);
|
|
1288
|
+
return {
|
|
1289
|
+
totalCost: result.totalCost,
|
|
1290
|
+
inputCost: result.inputCost,
|
|
1291
|
+
outputCost: result.outputCost,
|
|
1292
|
+
currency: result.currency
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
// ==================== Queries ====================
|
|
1296
|
+
/**
|
|
1297
|
+
* Get cost summary
|
|
1298
|
+
*/
|
|
1299
|
+
async getSummary(options = {}) {
|
|
1300
|
+
if (!this.storage) {
|
|
1301
|
+
throw new Error("Storage adapter required for queries");
|
|
1302
|
+
}
|
|
1303
|
+
return this.storage.getCostSummary(options);
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Get costs by dimension
|
|
1307
|
+
*/
|
|
1308
|
+
async getCostsByDimension(dimension, options = {}) {
|
|
1309
|
+
if (!this.storage) {
|
|
1310
|
+
throw new Error("Storage adapter required for queries");
|
|
1311
|
+
}
|
|
1312
|
+
return this.storage.getCostsByDimension(dimension, options);
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Get cost trends
|
|
1316
|
+
*/
|
|
1317
|
+
async getCostTrends(options = {}) {
|
|
1318
|
+
if (!this.storage) {
|
|
1319
|
+
throw new Error("Storage adapter required for queries");
|
|
1320
|
+
}
|
|
1321
|
+
return this.storage.getCostTrends(options);
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Query cost records
|
|
1325
|
+
*/
|
|
1326
|
+
async queryRecords(options = {}) {
|
|
1327
|
+
if (!this.storage) {
|
|
1328
|
+
throw new Error("Storage adapter required for queries");
|
|
1329
|
+
}
|
|
1330
|
+
return this.storage.queryCostRecords(options);
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Get a specific cost record
|
|
1334
|
+
*/
|
|
1335
|
+
async getRecord(id) {
|
|
1336
|
+
if (!this.storage) {
|
|
1337
|
+
throw new Error("Storage adapter required for queries");
|
|
1338
|
+
}
|
|
1339
|
+
return this.storage.getCostRecord(id);
|
|
1340
|
+
}
|
|
1341
|
+
// ==================== Statistics ====================
|
|
1342
|
+
/**
|
|
1343
|
+
* Get total cost for a time period
|
|
1344
|
+
*/
|
|
1345
|
+
async getTotalCost(options = {}) {
|
|
1346
|
+
const summary = await this.getSummary(options);
|
|
1347
|
+
return summary.totalCost;
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Get total tokens for a time period
|
|
1351
|
+
*/
|
|
1352
|
+
async getTotalTokens(options = {}) {
|
|
1353
|
+
const summary = await this.getSummary(options);
|
|
1354
|
+
return summary.totalTokens;
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* Get request count for a time period
|
|
1358
|
+
*/
|
|
1359
|
+
async getRequestCount(options = {}) {
|
|
1360
|
+
const summary = await this.getSummary(options);
|
|
1361
|
+
return summary.requestCount;
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Get error rate for a time period
|
|
1365
|
+
*/
|
|
1366
|
+
async getErrorRate(options = {}) {
|
|
1367
|
+
const summary = await this.getSummary(options);
|
|
1368
|
+
if (summary.requestCount === 0) return 0;
|
|
1369
|
+
return summary.errorCount / summary.requestCount;
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Get average cost per request
|
|
1373
|
+
*/
|
|
1374
|
+
async getAvgCostPerRequest(options = {}) {
|
|
1375
|
+
const summary = await this.getSummary(options);
|
|
1376
|
+
return summary.avgCostPerRequest;
|
|
1377
|
+
}
|
|
1378
|
+
// ==================== Top Consumers ====================
|
|
1379
|
+
/**
|
|
1380
|
+
* Get top models by cost
|
|
1381
|
+
*/
|
|
1382
|
+
async getTopModels(options = {}) {
|
|
1383
|
+
const results = await this.getCostsByDimension("model", options);
|
|
1384
|
+
return results.sort((a, b) => b.totalCost - a.totalCost).slice(0, options.limit ?? 10);
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Get top users by cost
|
|
1388
|
+
*/
|
|
1389
|
+
async getTopUsers(options = {}) {
|
|
1390
|
+
const results = await this.getCostsByDimension("user", options);
|
|
1391
|
+
return results.sort((a, b) => b.totalCost - a.totalCost).slice(0, options.limit ?? 10);
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Get top features by cost
|
|
1395
|
+
*/
|
|
1396
|
+
async getTopFeatures(options = {}) {
|
|
1397
|
+
const results = await this.getCostsByDimension("feature", options);
|
|
1398
|
+
return results.sort((a, b) => b.totalCost - a.totalCost).slice(0, options.limit ?? 10);
|
|
1399
|
+
}
|
|
1400
|
+
// ==================== Maintenance ====================
|
|
1401
|
+
/**
|
|
1402
|
+
* Cleanup old records
|
|
1403
|
+
*/
|
|
1404
|
+
async cleanup(olderThan) {
|
|
1405
|
+
if (!this.storage) {
|
|
1406
|
+
throw new Error("Storage adapter required for cleanup");
|
|
1407
|
+
}
|
|
1408
|
+
return this.storage.cleanup(olderThan);
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Get storage stats
|
|
1412
|
+
*/
|
|
1413
|
+
async getStorageStats() {
|
|
1414
|
+
if (!this.storage) {
|
|
1415
|
+
return { recordCount: 0 };
|
|
1416
|
+
}
|
|
1417
|
+
const stats = await this.storage.getStats();
|
|
1418
|
+
return {
|
|
1419
|
+
recordCount: stats.costRecordCount,
|
|
1420
|
+
storageSizeBytes: stats.storageSizeBytes,
|
|
1421
|
+
oldestRecord: stats.oldestRecord,
|
|
1422
|
+
newestRecord: stats.newestRecord
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Optimize storage
|
|
1427
|
+
*/
|
|
1428
|
+
async optimizeStorage() {
|
|
1429
|
+
if (!this.storage) {
|
|
1430
|
+
throw new Error("Storage adapter required for optimization");
|
|
1431
|
+
}
|
|
1432
|
+
await this.storage.optimize();
|
|
1433
|
+
}
|
|
1434
|
+
// ==================== Export/Import ====================
|
|
1435
|
+
/**
|
|
1436
|
+
* Export cost records
|
|
1437
|
+
*/
|
|
1438
|
+
async exportRecords(options = {}) {
|
|
1439
|
+
const [records, summary] = await Promise.all([
|
|
1440
|
+
this.queryRecords(options),
|
|
1441
|
+
this.getSummary(options)
|
|
1442
|
+
]);
|
|
1443
|
+
return {
|
|
1444
|
+
records,
|
|
1445
|
+
summary,
|
|
1446
|
+
exportedAt: /* @__PURE__ */ new Date()
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Import cost records
|
|
1451
|
+
*/
|
|
1452
|
+
async importRecords(records) {
|
|
1453
|
+
if (!this.storage) {
|
|
1454
|
+
throw new Error("Storage adapter required for import");
|
|
1455
|
+
}
|
|
1456
|
+
await this.storage.saveCostRecords(records);
|
|
1457
|
+
return records.length;
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
function createCostManager(options) {
|
|
1461
|
+
return new CostManager(options);
|
|
1462
|
+
}
|
|
1463
|
+
var BudgetManager = class extends EventEmitter {
|
|
1464
|
+
budgets = /* @__PURE__ */ new Map();
|
|
1465
|
+
usage = /* @__PURE__ */ new Map();
|
|
1466
|
+
resetJobs = /* @__PURE__ */ new Map();
|
|
1467
|
+
storage;
|
|
1468
|
+
config;
|
|
1469
|
+
constructor(config = {}, storage) {
|
|
1470
|
+
super();
|
|
1471
|
+
this.config = {
|
|
1472
|
+
enforceOnRequest: config.enforceOnRequest ?? true,
|
|
1473
|
+
defaultAction: config.defaultAction ?? "allow",
|
|
1474
|
+
checkInterval: config.checkInterval ?? 6e4,
|
|
1475
|
+
// 1 minute
|
|
1476
|
+
enableProjections: config.enableProjections ?? true,
|
|
1477
|
+
...config
|
|
1478
|
+
};
|
|
1479
|
+
this.storage = storage;
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Initialize from storage
|
|
1483
|
+
*/
|
|
1484
|
+
async initialize() {
|
|
1485
|
+
if (this.storage) {
|
|
1486
|
+
const budgets = await this.storage.listBudgets();
|
|
1487
|
+
for (const budget of budgets) {
|
|
1488
|
+
this.budgets.set(budget.id, budget);
|
|
1489
|
+
this.scheduleReset(budget);
|
|
1490
|
+
}
|
|
1491
|
+
for (const budget of this.budgets.values()) {
|
|
1492
|
+
await this.refreshUsage(budget.id);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Create a new budget
|
|
1498
|
+
*/
|
|
1499
|
+
async createBudget(request) {
|
|
1500
|
+
const budget = {
|
|
1501
|
+
id: nanoid(),
|
|
1502
|
+
name: request.name,
|
|
1503
|
+
description: request.description,
|
|
1504
|
+
limit: request.limit,
|
|
1505
|
+
currency: request.currency ?? "USD",
|
|
1506
|
+
period: request.period,
|
|
1507
|
+
scope: request.scope,
|
|
1508
|
+
scopeId: request.scopeId,
|
|
1509
|
+
warningThresholds: request.warningThresholds ?? [50, 80, 90],
|
|
1510
|
+
actions: request.actions,
|
|
1511
|
+
filters: request.filters,
|
|
1512
|
+
rollover: request.rollover ?? false,
|
|
1513
|
+
maxRollover: request.maxRollover,
|
|
1514
|
+
enabled: true,
|
|
1515
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1516
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1517
|
+
};
|
|
1518
|
+
this.budgets.set(budget.id, budget);
|
|
1519
|
+
const usage = this.initializeUsage(budget);
|
|
1520
|
+
this.usage.set(budget.id, usage);
|
|
1521
|
+
this.scheduleReset(budget);
|
|
1522
|
+
if (this.storage) {
|
|
1523
|
+
await this.storage.saveBudget(budget);
|
|
1524
|
+
}
|
|
1525
|
+
this.emit("budget:created", budget);
|
|
1526
|
+
return budget;
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Update a budget
|
|
1530
|
+
*/
|
|
1531
|
+
async updateBudget(budgetId, updates) {
|
|
1532
|
+
const budget = this.budgets.get(budgetId);
|
|
1533
|
+
if (!budget) {
|
|
1534
|
+
throw new Error(`Budget not found: ${budgetId}`);
|
|
1535
|
+
}
|
|
1536
|
+
const updated = {
|
|
1537
|
+
...budget,
|
|
1538
|
+
...updates,
|
|
1539
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1540
|
+
};
|
|
1541
|
+
this.budgets.set(budgetId, updated);
|
|
1542
|
+
if (updates.enabled !== void 0 || updates.limit) {
|
|
1543
|
+
this.scheduleReset(updated);
|
|
1544
|
+
}
|
|
1545
|
+
if (this.storage) {
|
|
1546
|
+
await this.storage.updateBudget(budgetId, updated);
|
|
1547
|
+
}
|
|
1548
|
+
this.emit("budget:updated", { budgetId, updates });
|
|
1549
|
+
return updated;
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Delete a budget
|
|
1553
|
+
*/
|
|
1554
|
+
async deleteBudget(budgetId) {
|
|
1555
|
+
const budget = this.budgets.get(budgetId);
|
|
1556
|
+
if (!budget) {
|
|
1557
|
+
return false;
|
|
1558
|
+
}
|
|
1559
|
+
const job = this.resetJobs.get(budgetId);
|
|
1560
|
+
if (job) {
|
|
1561
|
+
job.stop();
|
|
1562
|
+
this.resetJobs.delete(budgetId);
|
|
1563
|
+
}
|
|
1564
|
+
this.budgets.delete(budgetId);
|
|
1565
|
+
this.usage.delete(budgetId);
|
|
1566
|
+
if (this.storage) {
|
|
1567
|
+
await this.storage.deleteBudget(budgetId);
|
|
1568
|
+
}
|
|
1569
|
+
this.emit("budget:deleted", budgetId);
|
|
1570
|
+
return true;
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* Get a budget
|
|
1574
|
+
*/
|
|
1575
|
+
getBudget(budgetId) {
|
|
1576
|
+
return this.budgets.get(budgetId) ?? null;
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* List all budgets
|
|
1580
|
+
*/
|
|
1581
|
+
listBudgets(options) {
|
|
1582
|
+
let budgets = Array.from(this.budgets.values());
|
|
1583
|
+
if (options?.scope) {
|
|
1584
|
+
budgets = budgets.filter((b) => b.scope === options.scope);
|
|
1585
|
+
}
|
|
1586
|
+
if (options?.scopeId) {
|
|
1587
|
+
budgets = budgets.filter((b) => b.scopeId === options.scopeId);
|
|
1588
|
+
}
|
|
1589
|
+
if (options?.enabled !== void 0) {
|
|
1590
|
+
budgets = budgets.filter((b) => b.enabled === options.enabled);
|
|
1591
|
+
}
|
|
1592
|
+
return budgets;
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Get budget usage
|
|
1596
|
+
*/
|
|
1597
|
+
async getUsage(budgetId) {
|
|
1598
|
+
const budget = this.budgets.get(budgetId);
|
|
1599
|
+
if (!budget) {
|
|
1600
|
+
return null;
|
|
1601
|
+
}
|
|
1602
|
+
await this.refreshUsage(budgetId);
|
|
1603
|
+
return this.usage.get(budgetId) ?? null;
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Check if a request is within budget
|
|
1607
|
+
*/
|
|
1608
|
+
async checkBudget(request) {
|
|
1609
|
+
const matchingBudgets = [];
|
|
1610
|
+
const exceededBudgets = [];
|
|
1611
|
+
const warningBudgets = [];
|
|
1612
|
+
const budgetsToCheck = request.budgetIds ? request.budgetIds.map((id) => this.budgets.get(id)).filter(Boolean) : this.findMatchingBudgets(request.attribution);
|
|
1613
|
+
for (const budget of budgetsToCheck) {
|
|
1614
|
+
if (!budget.enabled) continue;
|
|
1615
|
+
await this.refreshUsage(budget.id);
|
|
1616
|
+
const usage = this.usage.get(budget.id);
|
|
1617
|
+
if (!usage) continue;
|
|
1618
|
+
matchingBudgets.push(usage);
|
|
1619
|
+
const projectedUsage = usage.currentUsage + request.estimatedCost;
|
|
1620
|
+
const projectedPercentage = projectedUsage / usage.limit * 100;
|
|
1621
|
+
if (projectedPercentage >= 100) {
|
|
1622
|
+
exceededBudgets.push(budget.id);
|
|
1623
|
+
} else if (projectedPercentage >= (budget.warningThresholds?.[0] ?? 50)) {
|
|
1624
|
+
warningBudgets.push(budget.id);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
let action = "allow";
|
|
1628
|
+
let reason;
|
|
1629
|
+
if (exceededBudgets.length > 0) {
|
|
1630
|
+
const budget = this.budgets.get(exceededBudgets[0]);
|
|
1631
|
+
const budgetAction = budget?.actions?.find((a) => a.threshold >= 100);
|
|
1632
|
+
if (budgetAction?.action === "block") {
|
|
1633
|
+
action = "block";
|
|
1634
|
+
reason = `Budget limit exceeded for "${budget?.name}"`;
|
|
1635
|
+
} else if (budgetAction?.action === "throttle") {
|
|
1636
|
+
action = "throttle";
|
|
1637
|
+
reason = `Budget limit exceeded for "${budget?.name}"`;
|
|
1638
|
+
} else {
|
|
1639
|
+
action = this.config.defaultAction ?? "allow";
|
|
1640
|
+
if (action !== "allow") {
|
|
1641
|
+
reason = `Budget limit exceeded for "${budget?.name}"`;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
return {
|
|
1646
|
+
allowed: action === "allow" || action === "warn",
|
|
1647
|
+
reason,
|
|
1648
|
+
matchingBudgets,
|
|
1649
|
+
exceededBudgets,
|
|
1650
|
+
warningBudgets,
|
|
1651
|
+
action
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Record cost against budgets
|
|
1656
|
+
*/
|
|
1657
|
+
async recordCost(cost, attribution) {
|
|
1658
|
+
const matchingBudgets = this.findMatchingBudgets(attribution);
|
|
1659
|
+
for (const budget of matchingBudgets) {
|
|
1660
|
+
if (!budget.enabled) continue;
|
|
1661
|
+
const usage = this.usage.get(budget.id);
|
|
1662
|
+
if (!usage) continue;
|
|
1663
|
+
usage.currentUsage += cost;
|
|
1664
|
+
usage.remaining = Math.max(0, usage.limit - usage.currentUsage);
|
|
1665
|
+
usage.usagePercentage = usage.currentUsage / usage.limit * 100;
|
|
1666
|
+
await this.checkThresholds(budget, usage);
|
|
1667
|
+
if (this.config.enableProjections) {
|
|
1668
|
+
this.updateProjections(usage);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Find budgets matching attribution
|
|
1674
|
+
*/
|
|
1675
|
+
findMatchingBudgets(attribution) {
|
|
1676
|
+
const matching = [];
|
|
1677
|
+
for (const budget of this.budgets.values()) {
|
|
1678
|
+
if (!budget.enabled) continue;
|
|
1679
|
+
if (budget.scope === "global") {
|
|
1680
|
+
matching.push(budget);
|
|
1681
|
+
continue;
|
|
1682
|
+
}
|
|
1683
|
+
if (!attribution) continue;
|
|
1684
|
+
let matches = false;
|
|
1685
|
+
switch (budget.scope) {
|
|
1686
|
+
case "user":
|
|
1687
|
+
matches = budget.scopeId === attribution.userId;
|
|
1688
|
+
break;
|
|
1689
|
+
case "agent":
|
|
1690
|
+
matches = budget.scopeId === attribution.agentId;
|
|
1691
|
+
break;
|
|
1692
|
+
case "project":
|
|
1693
|
+
matches = budget.scopeId === attribution.projectId;
|
|
1694
|
+
break;
|
|
1695
|
+
case "team":
|
|
1696
|
+
matches = budget.scopeId === attribution.teamId;
|
|
1697
|
+
break;
|
|
1698
|
+
case "feature":
|
|
1699
|
+
matches = budget.scopeId === attribution.feature;
|
|
1700
|
+
break;
|
|
1701
|
+
}
|
|
1702
|
+
if (!matches) continue;
|
|
1703
|
+
if (budget.filters) {
|
|
1704
|
+
if (budget.filters.environment && budget.filters.environment !== attribution.environment) {
|
|
1705
|
+
continue;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
matching.push(budget);
|
|
1709
|
+
}
|
|
1710
|
+
return matching;
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Initialize usage for a budget
|
|
1714
|
+
*/
|
|
1715
|
+
initializeUsage(budget) {
|
|
1716
|
+
const period = this.getPeriodDates(budget.period);
|
|
1717
|
+
return {
|
|
1718
|
+
budgetId: budget.id,
|
|
1719
|
+
currentUsage: 0,
|
|
1720
|
+
limit: budget.limit,
|
|
1721
|
+
usagePercentage: 0,
|
|
1722
|
+
remaining: budget.limit,
|
|
1723
|
+
periodStart: period.start,
|
|
1724
|
+
periodEnd: period.end,
|
|
1725
|
+
timeRemaining: this.getTimeRemaining(period.end),
|
|
1726
|
+
status: "active",
|
|
1727
|
+
triggeredThresholds: []
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Refresh usage from storage
|
|
1732
|
+
*/
|
|
1733
|
+
async refreshUsage(budgetId) {
|
|
1734
|
+
const budget = this.budgets.get(budgetId);
|
|
1735
|
+
if (!budget) return;
|
|
1736
|
+
let usage = this.usage.get(budgetId);
|
|
1737
|
+
if (!usage) {
|
|
1738
|
+
usage = this.initializeUsage(budget);
|
|
1739
|
+
this.usage.set(budgetId, usage);
|
|
1740
|
+
}
|
|
1741
|
+
usage.timeRemaining = this.getTimeRemaining(usage.periodEnd);
|
|
1742
|
+
if (this.storage) {
|
|
1743
|
+
const queryOptions = {
|
|
1744
|
+
startDate: usage.periodStart,
|
|
1745
|
+
endDate: usage.periodEnd
|
|
1746
|
+
};
|
|
1747
|
+
if (budget.scope !== "global" && budget.scopeId) {
|
|
1748
|
+
switch (budget.scope) {
|
|
1749
|
+
case "user":
|
|
1750
|
+
queryOptions.userIds = [budget.scopeId];
|
|
1751
|
+
break;
|
|
1752
|
+
case "agent":
|
|
1753
|
+
queryOptions.agentIds = [budget.scopeId];
|
|
1754
|
+
break;
|
|
1755
|
+
case "project":
|
|
1756
|
+
queryOptions.projectIds = [budget.scopeId];
|
|
1757
|
+
break;
|
|
1758
|
+
case "team":
|
|
1759
|
+
queryOptions.teamIds = [budget.scopeId];
|
|
1760
|
+
break;
|
|
1761
|
+
case "feature":
|
|
1762
|
+
queryOptions.features = [budget.scopeId];
|
|
1763
|
+
break;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
try {
|
|
1767
|
+
const summary = await this.storage.getCostSummary(queryOptions);
|
|
1768
|
+
usage.currentUsage = summary.totalCost;
|
|
1769
|
+
usage.remaining = Math.max(0, budget.limit - usage.currentUsage);
|
|
1770
|
+
usage.usagePercentage = usage.currentUsage / budget.limit * 100;
|
|
1771
|
+
if (usage.usagePercentage >= 100) {
|
|
1772
|
+
usage.status = "exceeded";
|
|
1773
|
+
} else if (!budget.enabled) {
|
|
1774
|
+
usage.status = "paused";
|
|
1775
|
+
} else {
|
|
1776
|
+
usage.status = "active";
|
|
1777
|
+
}
|
|
1778
|
+
} catch {
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Check thresholds and emit alerts
|
|
1784
|
+
*/
|
|
1785
|
+
async checkThresholds(budget, usage) {
|
|
1786
|
+
if (!budget.warningThresholds) return;
|
|
1787
|
+
for (const threshold of budget.warningThresholds) {
|
|
1788
|
+
if (usage.usagePercentage >= threshold && !usage.triggeredThresholds.includes(threshold)) {
|
|
1789
|
+
usage.triggeredThresholds.push(threshold);
|
|
1790
|
+
const alert = {
|
|
1791
|
+
id: nanoid(),
|
|
1792
|
+
budgetId: budget.id,
|
|
1793
|
+
budgetName: budget.name,
|
|
1794
|
+
type: threshold >= 100 ? "exceeded" : "warning",
|
|
1795
|
+
threshold,
|
|
1796
|
+
usage: usage.currentUsage,
|
|
1797
|
+
limit: usage.limit,
|
|
1798
|
+
percentage: usage.usagePercentage,
|
|
1799
|
+
message: threshold >= 100 ? `Budget "${budget.name}" has been exceeded (${usage.usagePercentage.toFixed(1)}%)` : `Budget "${budget.name}" has reached ${threshold}% (${usage.usagePercentage.toFixed(1)}%)`,
|
|
1800
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1801
|
+
acknowledged: false
|
|
1802
|
+
};
|
|
1803
|
+
if (this.storage) {
|
|
1804
|
+
await this.storage.saveBudgetAlert(alert);
|
|
1805
|
+
}
|
|
1806
|
+
if (threshold >= 100) {
|
|
1807
|
+
this.emit("budget:exceeded", alert);
|
|
1808
|
+
} else {
|
|
1809
|
+
this.emit("budget:warning", alert);
|
|
1810
|
+
}
|
|
1811
|
+
const action = budget.actions?.find((a) => a.threshold === threshold);
|
|
1812
|
+
if (action) {
|
|
1813
|
+
await this.executeAction(action, alert);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
/**
|
|
1819
|
+
* Execute threshold action
|
|
1820
|
+
*/
|
|
1821
|
+
async executeAction(action, alert) {
|
|
1822
|
+
if (action.notifyEmails && action.notifyEmails.length > 0) {
|
|
1823
|
+
console.log(
|
|
1824
|
+
`Would send email to ${action.notifyEmails.join(", ")}:`,
|
|
1825
|
+
alert.message
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
if (action.webhookUrl) {
|
|
1829
|
+
try {
|
|
1830
|
+
await fetch(action.webhookUrl, {
|
|
1831
|
+
method: "POST",
|
|
1832
|
+
headers: { "Content-Type": "application/json" },
|
|
1833
|
+
body: JSON.stringify(alert)
|
|
1834
|
+
});
|
|
1835
|
+
} catch {
|
|
1836
|
+
this.emit("error", {
|
|
1837
|
+
message: `Failed to send webhook to ${action.webhookUrl}`
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Update projections
|
|
1844
|
+
*/
|
|
1845
|
+
updateProjections(usage) {
|
|
1846
|
+
if (usage.timeRemaining <= 0) return;
|
|
1847
|
+
const elapsedMs = Date.now() - usage.periodStart.getTime();
|
|
1848
|
+
const totalMs = usage.periodEnd.getTime() - usage.periodStart.getTime();
|
|
1849
|
+
if (elapsedMs <= 0) return;
|
|
1850
|
+
const rate = usage.currentUsage / elapsedMs;
|
|
1851
|
+
usage.projectedUsage = rate * totalMs;
|
|
1852
|
+
usage.projectedExceed = usage.projectedUsage > usage.limit;
|
|
1853
|
+
}
|
|
1854
|
+
/**
|
|
1855
|
+
* Schedule budget reset
|
|
1856
|
+
*/
|
|
1857
|
+
scheduleReset(budget) {
|
|
1858
|
+
const existingJob = this.resetJobs.get(budget.id);
|
|
1859
|
+
if (existingJob) {
|
|
1860
|
+
existingJob.stop();
|
|
1861
|
+
}
|
|
1862
|
+
if (!budget.enabled) return;
|
|
1863
|
+
const pattern = this.getCronPattern(budget.period, budget.resetSchedule);
|
|
1864
|
+
if (!pattern) return;
|
|
1865
|
+
const job = new Cron(pattern, async () => {
|
|
1866
|
+
await this.resetBudget(budget.id);
|
|
1867
|
+
});
|
|
1868
|
+
this.resetJobs.set(budget.id, job);
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* Reset a budget
|
|
1872
|
+
*/
|
|
1873
|
+
async resetBudget(budgetId) {
|
|
1874
|
+
const budget = this.budgets.get(budgetId);
|
|
1875
|
+
if (!budget) return;
|
|
1876
|
+
const usage = this.usage.get(budgetId);
|
|
1877
|
+
if (!usage) return;
|
|
1878
|
+
const previousUsage = usage.currentUsage;
|
|
1879
|
+
if (this.storage) {
|
|
1880
|
+
const historyEntry = {
|
|
1881
|
+
budgetId,
|
|
1882
|
+
periodStart: usage.periodStart,
|
|
1883
|
+
periodEnd: usage.periodEnd,
|
|
1884
|
+
usage: previousUsage,
|
|
1885
|
+
limit: usage.limit,
|
|
1886
|
+
usagePercentage: usage.usagePercentage,
|
|
1887
|
+
exceeded: previousUsage > usage.limit,
|
|
1888
|
+
rolloverIn: usage.rolloverIn,
|
|
1889
|
+
rolloverOut: budget.rollover ? Math.min(usage.remaining, budget.maxRollover ?? Infinity) : void 0
|
|
1890
|
+
};
|
|
1891
|
+
await this.storage.saveBudgetHistory(historyEntry);
|
|
1892
|
+
}
|
|
1893
|
+
let newLimit = budget.limit;
|
|
1894
|
+
if (budget.rollover && usage.remaining > 0) {
|
|
1895
|
+
const rolloverAmount = Math.min(
|
|
1896
|
+
usage.remaining,
|
|
1897
|
+
budget.maxRollover ?? Infinity
|
|
1898
|
+
);
|
|
1899
|
+
newLimit += rolloverAmount;
|
|
1900
|
+
}
|
|
1901
|
+
const period = this.getPeriodDates(budget.period);
|
|
1902
|
+
usage.currentUsage = 0;
|
|
1903
|
+
usage.limit = newLimit;
|
|
1904
|
+
usage.remaining = newLimit;
|
|
1905
|
+
usage.usagePercentage = 0;
|
|
1906
|
+
usage.periodStart = period.start;
|
|
1907
|
+
usage.periodEnd = period.end;
|
|
1908
|
+
usage.timeRemaining = this.getTimeRemaining(period.end);
|
|
1909
|
+
usage.status = "active";
|
|
1910
|
+
usage.triggeredThresholds = [];
|
|
1911
|
+
usage.projectedUsage = void 0;
|
|
1912
|
+
usage.projectedExceed = void 0;
|
|
1913
|
+
this.emit("budget:reset", { budgetId, previousUsage });
|
|
1914
|
+
}
|
|
1915
|
+
/**
|
|
1916
|
+
* Get period dates
|
|
1917
|
+
*/
|
|
1918
|
+
getPeriodDates(period) {
|
|
1919
|
+
const now = /* @__PURE__ */ new Date();
|
|
1920
|
+
let start;
|
|
1921
|
+
let end;
|
|
1922
|
+
switch (period) {
|
|
1923
|
+
case "hourly":
|
|
1924
|
+
start = new Date(
|
|
1925
|
+
now.getFullYear(),
|
|
1926
|
+
now.getMonth(),
|
|
1927
|
+
now.getDate(),
|
|
1928
|
+
now.getHours()
|
|
1929
|
+
);
|
|
1930
|
+
end = new Date(start.getTime() + 60 * 60 * 1e3);
|
|
1931
|
+
break;
|
|
1932
|
+
case "daily":
|
|
1933
|
+
start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
1934
|
+
end = new Date(start.getTime() + 24 * 60 * 60 * 1e3);
|
|
1935
|
+
break;
|
|
1936
|
+
case "weekly": {
|
|
1937
|
+
const dayOfWeek = now.getDay();
|
|
1938
|
+
start = new Date(
|
|
1939
|
+
now.getFullYear(),
|
|
1940
|
+
now.getMonth(),
|
|
1941
|
+
now.getDate() - dayOfWeek
|
|
1942
|
+
);
|
|
1943
|
+
end = new Date(start.getTime() + 7 * 24 * 60 * 60 * 1e3);
|
|
1944
|
+
break;
|
|
1945
|
+
}
|
|
1946
|
+
case "monthly":
|
|
1947
|
+
start = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
1948
|
+
end = new Date(now.getFullYear(), now.getMonth() + 1, 1);
|
|
1949
|
+
break;
|
|
1950
|
+
case "quarterly": {
|
|
1951
|
+
const quarter = Math.floor(now.getMonth() / 3);
|
|
1952
|
+
start = new Date(now.getFullYear(), quarter * 3, 1);
|
|
1953
|
+
end = new Date(now.getFullYear(), (quarter + 1) * 3, 1);
|
|
1954
|
+
break;
|
|
1955
|
+
}
|
|
1956
|
+
case "yearly":
|
|
1957
|
+
start = new Date(now.getFullYear(), 0, 1);
|
|
1958
|
+
end = new Date(now.getFullYear() + 1, 0, 1);
|
|
1959
|
+
break;
|
|
1960
|
+
default:
|
|
1961
|
+
start = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
1962
|
+
end = new Date(now.getFullYear(), now.getMonth() + 1, 1);
|
|
1963
|
+
}
|
|
1964
|
+
return { start, end };
|
|
1965
|
+
}
|
|
1966
|
+
/**
|
|
1967
|
+
* Get time remaining in period
|
|
1968
|
+
*/
|
|
1969
|
+
getTimeRemaining(endDate) {
|
|
1970
|
+
return Math.max(0, endDate.getTime() - Date.now());
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* Get cron pattern for period
|
|
1974
|
+
*/
|
|
1975
|
+
getCronPattern(period, customSchedule) {
|
|
1976
|
+
if (customSchedule) return customSchedule;
|
|
1977
|
+
switch (period) {
|
|
1978
|
+
case "hourly":
|
|
1979
|
+
return "0 * * * *";
|
|
1980
|
+
// Every hour at minute 0
|
|
1981
|
+
case "daily":
|
|
1982
|
+
return "0 0 * * *";
|
|
1983
|
+
// Every day at midnight
|
|
1984
|
+
case "weekly":
|
|
1985
|
+
return "0 0 * * 0";
|
|
1986
|
+
// Every Sunday at midnight
|
|
1987
|
+
case "monthly":
|
|
1988
|
+
return "0 0 1 * *";
|
|
1989
|
+
// First of every month
|
|
1990
|
+
case "quarterly":
|
|
1991
|
+
return "0 0 1 1,4,7,10 *";
|
|
1992
|
+
// First of Jan, Apr, Jul, Oct
|
|
1993
|
+
case "yearly":
|
|
1994
|
+
return "0 0 1 1 *";
|
|
1995
|
+
// First of January
|
|
1996
|
+
default:
|
|
1997
|
+
return null;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
/**
|
|
2001
|
+
* Close budget manager
|
|
2002
|
+
*/
|
|
2003
|
+
close() {
|
|
2004
|
+
for (const job of this.resetJobs.values()) {
|
|
2005
|
+
job.stop();
|
|
2006
|
+
}
|
|
2007
|
+
this.resetJobs.clear();
|
|
2008
|
+
}
|
|
2009
|
+
};
|
|
2010
|
+
|
|
2011
|
+
// src/storage/adapters/BufferStorage.ts
|
|
2012
|
+
var BufferStorage = class {
|
|
2013
|
+
records = /* @__PURE__ */ new Map();
|
|
2014
|
+
budgets = /* @__PURE__ */ new Map();
|
|
2015
|
+
budgetHistory = /* @__PURE__ */ new Map();
|
|
2016
|
+
budgetAlerts = /* @__PURE__ */ new Map();
|
|
2017
|
+
attributedCosts = [];
|
|
2018
|
+
alertRules = /* @__PURE__ */ new Map();
|
|
2019
|
+
alerts = /* @__PURE__ */ new Map();
|
|
2020
|
+
config;
|
|
2021
|
+
flushTimer;
|
|
2022
|
+
constructor(config = {}) {
|
|
2023
|
+
this.config = {
|
|
2024
|
+
maxRecords: config.maxRecords ?? 1e4,
|
|
2025
|
+
autoFlushInterval: config.autoFlushInterval ?? 0,
|
|
2026
|
+
onFlush: config.onFlush
|
|
2027
|
+
};
|
|
2028
|
+
if (this.config.autoFlushInterval && this.config.autoFlushInterval > 0) {
|
|
2029
|
+
this.flushTimer = setInterval(() => {
|
|
2030
|
+
void (async () => {
|
|
2031
|
+
if (this.config.onFlush) {
|
|
2032
|
+
const records = Array.from(this.records.values());
|
|
2033
|
+
await this.config.onFlush(records);
|
|
2034
|
+
}
|
|
2035
|
+
})();
|
|
2036
|
+
}, this.config.autoFlushInterval);
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
async initialize() {
|
|
2040
|
+
}
|
|
2041
|
+
close() {
|
|
2042
|
+
if (this.flushTimer) {
|
|
2043
|
+
clearInterval(this.flushTimer);
|
|
2044
|
+
}
|
|
2045
|
+
return Promise.resolve();
|
|
2046
|
+
}
|
|
2047
|
+
// ==================== Cost Records ====================
|
|
2048
|
+
saveCostRecord(record) {
|
|
2049
|
+
this.enforceLimit();
|
|
2050
|
+
this.records.set(record.id, record);
|
|
2051
|
+
return Promise.resolve();
|
|
2052
|
+
}
|
|
2053
|
+
async saveCostRecords(records) {
|
|
2054
|
+
for (const record of records) {
|
|
2055
|
+
await this.saveCostRecord(record);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
getCostRecord(id) {
|
|
2059
|
+
return Promise.resolve(this.records.get(id) ?? null);
|
|
2060
|
+
}
|
|
2061
|
+
queryCostRecords(options) {
|
|
2062
|
+
let results = Array.from(this.records.values());
|
|
2063
|
+
results = this.applyFilters(results, options);
|
|
2064
|
+
results = this.applySort(results, options);
|
|
2065
|
+
results = this.applyPagination(results, options);
|
|
2066
|
+
return Promise.resolve(results);
|
|
2067
|
+
}
|
|
2068
|
+
async getCostSummary(options) {
|
|
2069
|
+
const records = await this.queryCostRecords(options);
|
|
2070
|
+
const summary = {
|
|
2071
|
+
periodStart: options.startDate ?? /* @__PURE__ */ new Date(0),
|
|
2072
|
+
periodEnd: options.endDate ?? /* @__PURE__ */ new Date(),
|
|
2073
|
+
totalCost: 0,
|
|
2074
|
+
totalTokens: 0,
|
|
2075
|
+
inputTokens: 0,
|
|
2076
|
+
outputTokens: 0,
|
|
2077
|
+
requestCount: records.length,
|
|
2078
|
+
successCount: 0,
|
|
2079
|
+
errorCount: 0,
|
|
2080
|
+
avgCostPerRequest: 0,
|
|
2081
|
+
avgTokensPerRequest: 0,
|
|
2082
|
+
currency: "USD"
|
|
2083
|
+
};
|
|
2084
|
+
let totalLatency = 0;
|
|
2085
|
+
let latencyCount = 0;
|
|
2086
|
+
for (const record of records) {
|
|
2087
|
+
summary.totalCost += record.cost.totalCost;
|
|
2088
|
+
summary.totalTokens += record.tokens.totalTokens;
|
|
2089
|
+
summary.inputTokens += record.tokens.inputTokens;
|
|
2090
|
+
summary.outputTokens += record.tokens.outputTokens;
|
|
2091
|
+
if (record.success) {
|
|
2092
|
+
summary.successCount++;
|
|
2093
|
+
} else {
|
|
2094
|
+
summary.errorCount++;
|
|
2095
|
+
}
|
|
2096
|
+
if (record.latencyMs) {
|
|
2097
|
+
totalLatency += record.latencyMs;
|
|
2098
|
+
latencyCount++;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
if (summary.requestCount > 0) {
|
|
2102
|
+
summary.avgCostPerRequest = summary.totalCost / summary.requestCount;
|
|
2103
|
+
summary.avgTokensPerRequest = summary.totalTokens / summary.requestCount;
|
|
2104
|
+
}
|
|
2105
|
+
if (latencyCount > 0) {
|
|
2106
|
+
summary.avgLatencyMs = totalLatency / latencyCount;
|
|
2107
|
+
}
|
|
2108
|
+
return summary;
|
|
2109
|
+
}
|
|
2110
|
+
async getCostsByDimension(dimension, options) {
|
|
2111
|
+
const records = await this.queryCostRecords(options);
|
|
2112
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2113
|
+
let totalCost = 0;
|
|
2114
|
+
for (const record of records) {
|
|
2115
|
+
const value = this.getDimensionValue(record, dimension);
|
|
2116
|
+
if (!value) continue;
|
|
2117
|
+
const existing = groups.get(value) ?? { cost: 0, tokens: 0, count: 0 };
|
|
2118
|
+
existing.cost += record.cost.totalCost;
|
|
2119
|
+
existing.tokens += record.tokens.totalTokens;
|
|
2120
|
+
existing.count++;
|
|
2121
|
+
groups.set(value, existing);
|
|
2122
|
+
totalCost += record.cost.totalCost;
|
|
2123
|
+
}
|
|
2124
|
+
return Array.from(groups.entries()).map(([value, data]) => ({
|
|
2125
|
+
dimension,
|
|
2126
|
+
value,
|
|
2127
|
+
totalCost: data.cost,
|
|
2128
|
+
totalTokens: data.tokens,
|
|
2129
|
+
requestCount: data.count,
|
|
2130
|
+
percentage: totalCost > 0 ? data.cost / totalCost * 100 : 0
|
|
2131
|
+
}));
|
|
2132
|
+
}
|
|
2133
|
+
async getCostTrends(options) {
|
|
2134
|
+
const records = await this.queryCostRecords(options);
|
|
2135
|
+
const granularity = options.granularity ?? "day";
|
|
2136
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
2137
|
+
for (const record of records) {
|
|
2138
|
+
const bucketTime = this.getBucketTime(record.timestamp, granularity);
|
|
2139
|
+
const existing = buckets.get(bucketTime) ?? {
|
|
2140
|
+
timestamp: new Date(bucketTime),
|
|
2141
|
+
cost: 0,
|
|
2142
|
+
tokens: 0,
|
|
2143
|
+
requests: 0
|
|
2144
|
+
};
|
|
2145
|
+
existing.cost += record.cost.totalCost;
|
|
2146
|
+
existing.tokens += record.tokens.totalTokens;
|
|
2147
|
+
existing.requests++;
|
|
2148
|
+
buckets.set(bucketTime, existing);
|
|
2149
|
+
}
|
|
2150
|
+
return Array.from(buckets.values()).sort(
|
|
2151
|
+
(a, b) => a.timestamp.getTime() - b.timestamp.getTime()
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2154
|
+
deleteCostRecords(ids) {
|
|
2155
|
+
let deleted = 0;
|
|
2156
|
+
for (const id of ids) {
|
|
2157
|
+
if (this.records.delete(id)) {
|
|
2158
|
+
deleted++;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
return Promise.resolve(deleted);
|
|
2162
|
+
}
|
|
2163
|
+
async deleteCostRecordsByFilter(options) {
|
|
2164
|
+
const toDelete = await this.queryCostRecords(options);
|
|
2165
|
+
return this.deleteCostRecords(toDelete.map((r) => r.id));
|
|
2166
|
+
}
|
|
2167
|
+
// ==================== Budgets ====================
|
|
2168
|
+
saveBudget(budget) {
|
|
2169
|
+
this.budgets.set(budget.id, budget);
|
|
2170
|
+
return Promise.resolve();
|
|
2171
|
+
}
|
|
2172
|
+
getBudget(id) {
|
|
2173
|
+
return Promise.resolve(this.budgets.get(id) ?? null);
|
|
2174
|
+
}
|
|
2175
|
+
listBudgets(options) {
|
|
2176
|
+
let budgets = Array.from(this.budgets.values());
|
|
2177
|
+
if (options?.scope) {
|
|
2178
|
+
budgets = budgets.filter((b) => b.scope === options.scope);
|
|
2179
|
+
}
|
|
2180
|
+
if (options?.scopeId) {
|
|
2181
|
+
budgets = budgets.filter((b) => b.scopeId === options.scopeId);
|
|
2182
|
+
}
|
|
2183
|
+
if (options?.enabled !== void 0) {
|
|
2184
|
+
budgets = budgets.filter((b) => b.enabled === options.enabled);
|
|
2185
|
+
}
|
|
2186
|
+
return Promise.resolve(budgets);
|
|
2187
|
+
}
|
|
2188
|
+
updateBudget(id, updates) {
|
|
2189
|
+
const budget = this.budgets.get(id);
|
|
2190
|
+
if (budget) {
|
|
2191
|
+
this.budgets.set(id, { ...budget, ...updates });
|
|
2192
|
+
}
|
|
2193
|
+
return Promise.resolve();
|
|
2194
|
+
}
|
|
2195
|
+
deleteBudget(id) {
|
|
2196
|
+
return Promise.resolve(this.budgets.delete(id));
|
|
2197
|
+
}
|
|
2198
|
+
getBudgetUsage(budgetId) {
|
|
2199
|
+
const budget = this.budgets.get(budgetId);
|
|
2200
|
+
if (!budget) {
|
|
2201
|
+
throw new Error(`Budget not found: ${budgetId}`);
|
|
2202
|
+
}
|
|
2203
|
+
const now = /* @__PURE__ */ new Date();
|
|
2204
|
+
const periodStart = this.getPeriodStart(budget.period, now);
|
|
2205
|
+
const records = Array.from(this.records.values()).filter(
|
|
2206
|
+
(r) => r.timestamp >= periodStart && this.matchesBudgetScope(r, budget)
|
|
2207
|
+
);
|
|
2208
|
+
const currentUsage = records.reduce((sum, r) => sum + r.cost.totalCost, 0);
|
|
2209
|
+
return Promise.resolve({
|
|
2210
|
+
budgetId,
|
|
2211
|
+
currentUsage,
|
|
2212
|
+
limit: budget.limit,
|
|
2213
|
+
usagePercentage: currentUsage / budget.limit * 100,
|
|
2214
|
+
remaining: Math.max(0, budget.limit - currentUsage),
|
|
2215
|
+
periodStart,
|
|
2216
|
+
periodEnd: this.getPeriodEnd(budget.period, periodStart),
|
|
2217
|
+
timeRemaining: this.getPeriodEnd(budget.period, periodStart).getTime() - now.getTime(),
|
|
2218
|
+
status: currentUsage >= budget.limit ? "exceeded" : "active",
|
|
2219
|
+
triggeredThresholds: []
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
saveBudgetHistory(entry) {
|
|
2223
|
+
const existing = this.budgetHistory.get(entry.budgetId) ?? [];
|
|
2224
|
+
existing.push(entry);
|
|
2225
|
+
this.budgetHistory.set(entry.budgetId, existing);
|
|
2226
|
+
return Promise.resolve();
|
|
2227
|
+
}
|
|
2228
|
+
getBudgetHistory(budgetId, limit) {
|
|
2229
|
+
const history = this.budgetHistory.get(budgetId) ?? [];
|
|
2230
|
+
return Promise.resolve(limit ? history.slice(-limit) : history);
|
|
2231
|
+
}
|
|
2232
|
+
saveBudgetAlert(alert) {
|
|
2233
|
+
const existing = this.budgetAlerts.get(alert.budgetId) ?? [];
|
|
2234
|
+
existing.push(alert);
|
|
2235
|
+
this.budgetAlerts.set(alert.budgetId, existing);
|
|
2236
|
+
return Promise.resolve();
|
|
2237
|
+
}
|
|
2238
|
+
getBudgetAlerts(budgetId) {
|
|
2239
|
+
return Promise.resolve(this.budgetAlerts.get(budgetId) ?? []);
|
|
2240
|
+
}
|
|
2241
|
+
// ==================== Attribution ====================
|
|
2242
|
+
saveAttributedCost(attributed) {
|
|
2243
|
+
this.attributedCosts.push(attributed);
|
|
2244
|
+
return Promise.resolve();
|
|
2245
|
+
}
|
|
2246
|
+
saveAttributedCosts(attributed) {
|
|
2247
|
+
this.attributedCosts.push(...attributed);
|
|
2248
|
+
return Promise.resolve();
|
|
2249
|
+
}
|
|
2250
|
+
getAttributionSummary(dimension, options) {
|
|
2251
|
+
const costs = this.attributedCosts.filter(
|
|
2252
|
+
(c) => c.dimension === dimension && (!options.startDate || c.timestamp >= options.startDate) && (!options.endDate || c.timestamp <= options.endDate)
|
|
2253
|
+
);
|
|
2254
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2255
|
+
let totalCost = 0;
|
|
2256
|
+
for (const cost of costs) {
|
|
2257
|
+
const existing = groups.get(cost.dimensionValue) ?? {
|
|
2258
|
+
cost: 0,
|
|
2259
|
+
tokens: 0,
|
|
2260
|
+
requests: 0
|
|
2261
|
+
};
|
|
2262
|
+
existing.cost += cost.attributedCost;
|
|
2263
|
+
existing.requests++;
|
|
2264
|
+
groups.set(cost.dimensionValue, existing);
|
|
2265
|
+
totalCost += cost.attributedCost;
|
|
2266
|
+
}
|
|
2267
|
+
return Promise.resolve({
|
|
2268
|
+
dimension,
|
|
2269
|
+
breakdown: Array.from(groups.entries()).map(([value, data]) => ({
|
|
2270
|
+
value,
|
|
2271
|
+
cost: data.cost,
|
|
2272
|
+
tokens: data.tokens,
|
|
2273
|
+
requests: data.requests,
|
|
2274
|
+
percentage: totalCost > 0 ? data.cost / totalCost * 100 : 0
|
|
2275
|
+
})),
|
|
2276
|
+
totalCost,
|
|
2277
|
+
periodStart: options.startDate ?? /* @__PURE__ */ new Date(0),
|
|
2278
|
+
periodEnd: options.endDate ?? /* @__PURE__ */ new Date()
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
// ==================== Alerts ====================
|
|
2282
|
+
saveAlertRule(rule) {
|
|
2283
|
+
this.alertRules.set(rule.id, rule);
|
|
2284
|
+
return Promise.resolve();
|
|
2285
|
+
}
|
|
2286
|
+
getAlertRule(id) {
|
|
2287
|
+
return Promise.resolve(this.alertRules.get(id) ?? null);
|
|
2288
|
+
}
|
|
2289
|
+
listAlertRules(options) {
|
|
2290
|
+
let rules = Array.from(this.alertRules.values());
|
|
2291
|
+
if (options?.enabled !== void 0) {
|
|
2292
|
+
rules = rules.filter((r) => r.enabled === options.enabled);
|
|
2293
|
+
}
|
|
2294
|
+
return Promise.resolve(rules);
|
|
2295
|
+
}
|
|
2296
|
+
updateAlertRule(id, updates) {
|
|
2297
|
+
const rule = this.alertRules.get(id);
|
|
2298
|
+
if (rule) {
|
|
2299
|
+
this.alertRules.set(id, { ...rule, ...updates });
|
|
2300
|
+
}
|
|
2301
|
+
return Promise.resolve();
|
|
2302
|
+
}
|
|
2303
|
+
deleteAlertRule(id) {
|
|
2304
|
+
return Promise.resolve(this.alertRules.delete(id));
|
|
2305
|
+
}
|
|
2306
|
+
saveAlert(alert) {
|
|
2307
|
+
this.alerts.set(alert.id, alert);
|
|
2308
|
+
return Promise.resolve();
|
|
2309
|
+
}
|
|
2310
|
+
getAlert(id) {
|
|
2311
|
+
return Promise.resolve(this.alerts.get(id) ?? null);
|
|
2312
|
+
}
|
|
2313
|
+
queryAlerts(options) {
|
|
2314
|
+
let alerts = Array.from(this.alerts.values());
|
|
2315
|
+
if (options?.status) {
|
|
2316
|
+
alerts = alerts.filter((a) => options.status.includes(a.status));
|
|
2317
|
+
}
|
|
2318
|
+
if (options?.severity) {
|
|
2319
|
+
alerts = alerts.filter((a) => options.severity.includes(a.severity));
|
|
2320
|
+
}
|
|
2321
|
+
if (options?.startDate) {
|
|
2322
|
+
alerts = alerts.filter((a) => a.triggeredAt >= options.startDate);
|
|
2323
|
+
}
|
|
2324
|
+
if (options?.endDate) {
|
|
2325
|
+
alerts = alerts.filter((a) => a.triggeredAt <= options.endDate);
|
|
2326
|
+
}
|
|
2327
|
+
alerts.sort((a, b) => b.triggeredAt.getTime() - a.triggeredAt.getTime());
|
|
2328
|
+
if (options?.offset) {
|
|
2329
|
+
alerts = alerts.slice(options.offset);
|
|
2330
|
+
}
|
|
2331
|
+
if (options?.limit) {
|
|
2332
|
+
alerts = alerts.slice(0, options.limit);
|
|
2333
|
+
}
|
|
2334
|
+
return Promise.resolve(alerts);
|
|
2335
|
+
}
|
|
2336
|
+
updateAlert(id, updates) {
|
|
2337
|
+
const alert = this.alerts.get(id);
|
|
2338
|
+
if (alert) {
|
|
2339
|
+
this.alerts.set(id, { ...alert, ...updates });
|
|
2340
|
+
}
|
|
2341
|
+
return Promise.resolve();
|
|
2342
|
+
}
|
|
2343
|
+
// ==================== Maintenance ====================
|
|
2344
|
+
cleanup(olderThan) {
|
|
2345
|
+
const toDelete = [];
|
|
2346
|
+
for (const [id, record] of this.records) {
|
|
2347
|
+
if (record.timestamp < olderThan) {
|
|
2348
|
+
toDelete.push(id);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
for (const id of toDelete) {
|
|
2352
|
+
this.records.delete(id);
|
|
2353
|
+
}
|
|
2354
|
+
return Promise.resolve(toDelete.length);
|
|
2355
|
+
}
|
|
2356
|
+
getStats() {
|
|
2357
|
+
const records = Array.from(this.records.values());
|
|
2358
|
+
let oldestRecord;
|
|
2359
|
+
let newestRecord;
|
|
2360
|
+
for (const record of records) {
|
|
2361
|
+
if (!oldestRecord || record.timestamp < oldestRecord) {
|
|
2362
|
+
oldestRecord = record.timestamp;
|
|
2363
|
+
}
|
|
2364
|
+
if (!newestRecord || record.timestamp > newestRecord) {
|
|
2365
|
+
newestRecord = record.timestamp;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
return Promise.resolve({
|
|
2369
|
+
costRecordCount: this.records.size,
|
|
2370
|
+
budgetCount: this.budgets.size,
|
|
2371
|
+
alertRuleCount: this.alertRules.size,
|
|
2372
|
+
alertCount: this.alerts.size,
|
|
2373
|
+
oldestRecord,
|
|
2374
|
+
newestRecord
|
|
2375
|
+
});
|
|
2376
|
+
}
|
|
2377
|
+
async optimize() {
|
|
2378
|
+
}
|
|
2379
|
+
// ==================== Helper Methods ====================
|
|
2380
|
+
enforceLimit() {
|
|
2381
|
+
if (this.config.maxRecords && this.records.size >= this.config.maxRecords) {
|
|
2382
|
+
const sorted = Array.from(this.records.entries()).sort(
|
|
2383
|
+
(a, b) => a[1].timestamp.getTime() - b[1].timestamp.getTime()
|
|
2384
|
+
);
|
|
2385
|
+
const toRemove = sorted.slice(
|
|
2386
|
+
0,
|
|
2387
|
+
Math.floor(this.config.maxRecords * 0.1)
|
|
2388
|
+
);
|
|
2389
|
+
for (const [id] of toRemove) {
|
|
2390
|
+
this.records.delete(id);
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
applyFilters(records, options) {
|
|
2395
|
+
return records.filter((record) => {
|
|
2396
|
+
if (options.startDate && record.timestamp < options.startDate)
|
|
2397
|
+
return false;
|
|
2398
|
+
if (options.endDate && record.timestamp > options.endDate) return false;
|
|
2399
|
+
if (options.providers && !options.providers.includes(record.provider))
|
|
2400
|
+
return false;
|
|
2401
|
+
if (options.models && !options.models.includes(record.model))
|
|
2402
|
+
return false;
|
|
2403
|
+
if (options.userIds && !options.userIds.includes(record.attribution?.userId ?? ""))
|
|
2404
|
+
return false;
|
|
2405
|
+
if (options.agentIds && !options.agentIds.includes(record.attribution?.agentId ?? ""))
|
|
2406
|
+
return false;
|
|
2407
|
+
if (options.sessionIds && !options.sessionIds.includes(record.attribution?.sessionId ?? ""))
|
|
2408
|
+
return false;
|
|
2409
|
+
if (options.projectIds && !options.projectIds.includes(record.attribution?.projectId ?? ""))
|
|
2410
|
+
return false;
|
|
2411
|
+
if (options.teamIds && !options.teamIds.includes(record.attribution?.teamId ?? ""))
|
|
2412
|
+
return false;
|
|
2413
|
+
if (options.features && !options.features.includes(record.attribution?.feature ?? ""))
|
|
2414
|
+
return false;
|
|
2415
|
+
if (options.environment && record.attribution?.environment !== options.environment)
|
|
2416
|
+
return false;
|
|
2417
|
+
if (options.success !== void 0 && record.success !== options.success)
|
|
2418
|
+
return false;
|
|
2419
|
+
return true;
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
applySort(records, options) {
|
|
2423
|
+
const sortBy = options.sortBy ?? "timestamp";
|
|
2424
|
+
const sortOrder = options.sortOrder ?? "desc";
|
|
2425
|
+
return records.sort((a, b) => {
|
|
2426
|
+
let comparison = 0;
|
|
2427
|
+
switch (sortBy) {
|
|
2428
|
+
case "cost":
|
|
2429
|
+
comparison = a.cost.totalCost - b.cost.totalCost;
|
|
2430
|
+
break;
|
|
2431
|
+
case "tokens":
|
|
2432
|
+
comparison = a.tokens.totalTokens - b.tokens.totalTokens;
|
|
2433
|
+
break;
|
|
2434
|
+
case "timestamp":
|
|
2435
|
+
default:
|
|
2436
|
+
comparison = a.timestamp.getTime() - b.timestamp.getTime();
|
|
2437
|
+
break;
|
|
2438
|
+
}
|
|
2439
|
+
return sortOrder === "asc" ? comparison : -comparison;
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
applyPagination(records, options) {
|
|
2443
|
+
let result = records;
|
|
2444
|
+
if (options.offset) {
|
|
2445
|
+
result = result.slice(options.offset);
|
|
2446
|
+
}
|
|
2447
|
+
if (options.limit) {
|
|
2448
|
+
result = result.slice(0, options.limit);
|
|
2449
|
+
}
|
|
2450
|
+
return result;
|
|
2451
|
+
}
|
|
2452
|
+
getDimensionValue(record, dimension) {
|
|
2453
|
+
switch (dimension) {
|
|
2454
|
+
case "provider":
|
|
2455
|
+
return record.provider;
|
|
2456
|
+
case "model":
|
|
2457
|
+
return record.model;
|
|
2458
|
+
case "user":
|
|
2459
|
+
return record.attribution?.userId;
|
|
2460
|
+
case "agent":
|
|
2461
|
+
return record.attribution?.agentId;
|
|
2462
|
+
case "session":
|
|
2463
|
+
return record.attribution?.sessionId;
|
|
2464
|
+
case "project":
|
|
2465
|
+
return record.attribution?.projectId;
|
|
2466
|
+
case "team":
|
|
2467
|
+
return record.attribution?.teamId;
|
|
2468
|
+
case "feature":
|
|
2469
|
+
return record.attribution?.feature;
|
|
2470
|
+
case "environment":
|
|
2471
|
+
return record.attribution?.environment;
|
|
2472
|
+
default:
|
|
2473
|
+
return record.attribution?.labels?.[dimension];
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
getBucketTime(date, granularity) {
|
|
2477
|
+
const d = new Date(date);
|
|
2478
|
+
switch (granularity) {
|
|
2479
|
+
case "minute":
|
|
2480
|
+
d.setSeconds(0, 0);
|
|
2481
|
+
break;
|
|
2482
|
+
case "hour":
|
|
2483
|
+
d.setMinutes(0, 0, 0);
|
|
2484
|
+
break;
|
|
2485
|
+
case "day":
|
|
2486
|
+
d.setHours(0, 0, 0, 0);
|
|
2487
|
+
break;
|
|
2488
|
+
case "week": {
|
|
2489
|
+
const day = d.getDay();
|
|
2490
|
+
d.setDate(d.getDate() - day);
|
|
2491
|
+
d.setHours(0, 0, 0, 0);
|
|
2492
|
+
break;
|
|
2493
|
+
}
|
|
2494
|
+
case "month":
|
|
2495
|
+
d.setDate(1);
|
|
2496
|
+
d.setHours(0, 0, 0, 0);
|
|
2497
|
+
break;
|
|
2498
|
+
}
|
|
2499
|
+
return d.getTime();
|
|
2500
|
+
}
|
|
2501
|
+
matchesBudgetScope(record, budget) {
|
|
2502
|
+
if (budget.scope === "global") return true;
|
|
2503
|
+
switch (budget.scope) {
|
|
2504
|
+
case "user":
|
|
2505
|
+
return record.attribution?.userId === budget.scopeId;
|
|
2506
|
+
case "agent":
|
|
2507
|
+
return record.attribution?.agentId === budget.scopeId;
|
|
2508
|
+
case "project":
|
|
2509
|
+
return record.attribution?.projectId === budget.scopeId;
|
|
2510
|
+
case "team":
|
|
2511
|
+
return record.attribution?.teamId === budget.scopeId;
|
|
2512
|
+
case "feature":
|
|
2513
|
+
return record.attribution?.feature === budget.scopeId;
|
|
2514
|
+
case "model":
|
|
2515
|
+
return record.model === budget.scopeId;
|
|
2516
|
+
case "provider":
|
|
2517
|
+
return record.provider === budget.scopeId;
|
|
2518
|
+
default:
|
|
2519
|
+
return false;
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
getPeriodStart(period, now) {
|
|
2523
|
+
const d = new Date(now);
|
|
2524
|
+
switch (period) {
|
|
2525
|
+
case "hourly":
|
|
2526
|
+
d.setMinutes(0, 0, 0);
|
|
2527
|
+
break;
|
|
2528
|
+
case "daily":
|
|
2529
|
+
d.setHours(0, 0, 0, 0);
|
|
2530
|
+
break;
|
|
2531
|
+
case "weekly":
|
|
2532
|
+
d.setDate(d.getDate() - d.getDay());
|
|
2533
|
+
d.setHours(0, 0, 0, 0);
|
|
2534
|
+
break;
|
|
2535
|
+
case "monthly":
|
|
2536
|
+
d.setDate(1);
|
|
2537
|
+
d.setHours(0, 0, 0, 0);
|
|
2538
|
+
break;
|
|
2539
|
+
case "quarterly":
|
|
2540
|
+
d.setMonth(Math.floor(d.getMonth() / 3) * 3, 1);
|
|
2541
|
+
d.setHours(0, 0, 0, 0);
|
|
2542
|
+
break;
|
|
2543
|
+
case "yearly":
|
|
2544
|
+
d.setMonth(0, 1);
|
|
2545
|
+
d.setHours(0, 0, 0, 0);
|
|
2546
|
+
break;
|
|
2547
|
+
}
|
|
2548
|
+
return d;
|
|
2549
|
+
}
|
|
2550
|
+
getPeriodEnd(period, start) {
|
|
2551
|
+
const d = new Date(start);
|
|
2552
|
+
switch (period) {
|
|
2553
|
+
case "hourly":
|
|
2554
|
+
d.setHours(d.getHours() + 1);
|
|
2555
|
+
break;
|
|
2556
|
+
case "daily":
|
|
2557
|
+
d.setDate(d.getDate() + 1);
|
|
2558
|
+
break;
|
|
2559
|
+
case "weekly":
|
|
2560
|
+
d.setDate(d.getDate() + 7);
|
|
2561
|
+
break;
|
|
2562
|
+
case "monthly":
|
|
2563
|
+
d.setMonth(d.getMonth() + 1);
|
|
2564
|
+
break;
|
|
2565
|
+
case "quarterly":
|
|
2566
|
+
d.setMonth(d.getMonth() + 3);
|
|
2567
|
+
break;
|
|
2568
|
+
case "yearly":
|
|
2569
|
+
d.setFullYear(d.getFullYear() + 1);
|
|
2570
|
+
break;
|
|
2571
|
+
}
|
|
2572
|
+
return d;
|
|
2573
|
+
}
|
|
2574
|
+
/**
|
|
2575
|
+
* Clear all data
|
|
2576
|
+
*/
|
|
2577
|
+
clear() {
|
|
2578
|
+
this.records.clear();
|
|
2579
|
+
this.budgets.clear();
|
|
2580
|
+
this.budgetHistory.clear();
|
|
2581
|
+
this.budgetAlerts.clear();
|
|
2582
|
+
this.attributedCosts = [];
|
|
2583
|
+
this.alertRules.clear();
|
|
2584
|
+
this.alerts.clear();
|
|
2585
|
+
}
|
|
2586
|
+
};
|
|
2587
|
+
var CostProvider = class extends EventEmitter {
|
|
2588
|
+
costManager;
|
|
2589
|
+
budgetManager;
|
|
2590
|
+
enforceBudgets;
|
|
2591
|
+
defaultAttribution;
|
|
2592
|
+
initialized = false;
|
|
2593
|
+
constructor(config = {}) {
|
|
2594
|
+
super();
|
|
2595
|
+
this.enforceBudgets = config.enforceBudgets ?? false;
|
|
2596
|
+
this.defaultAttribution = config.defaultAttribution;
|
|
2597
|
+
this.costManager = new CostManager({
|
|
2598
|
+
...config.costManagerOptions,
|
|
2599
|
+
storage: config.storage,
|
|
2600
|
+
defaultAttribution: config.defaultAttribution
|
|
2601
|
+
});
|
|
2602
|
+
if (this.enforceBudgets) {
|
|
2603
|
+
this.budgetManager = new BudgetManager(
|
|
2604
|
+
config.budgetManagerOptions ?? {},
|
|
2605
|
+
config.storage
|
|
2606
|
+
);
|
|
2607
|
+
}
|
|
2608
|
+
this.setupEventForwarding();
|
|
2609
|
+
}
|
|
2610
|
+
/**
|
|
2611
|
+
* Initialize the provider
|
|
2612
|
+
*/
|
|
2613
|
+
async initialize() {
|
|
2614
|
+
if (this.initialized) return;
|
|
2615
|
+
await this.costManager.initialize();
|
|
2616
|
+
if (this.budgetManager) {
|
|
2617
|
+
await this.budgetManager.initialize();
|
|
2618
|
+
}
|
|
2619
|
+
this.initialized = true;
|
|
2620
|
+
}
|
|
2621
|
+
/**
|
|
2622
|
+
* Close the provider
|
|
2623
|
+
*/
|
|
2624
|
+
async close() {
|
|
2625
|
+
await this.costManager.close();
|
|
2626
|
+
if (this.budgetManager) {
|
|
2627
|
+
this.budgetManager.close();
|
|
2628
|
+
}
|
|
2629
|
+
this.initialized = false;
|
|
2630
|
+
}
|
|
2631
|
+
/**
|
|
2632
|
+
* Create an agent cost tracker
|
|
2633
|
+
*/
|
|
2634
|
+
createAgentTracker(context) {
|
|
2635
|
+
return new AgentCostTracker(
|
|
2636
|
+
this.costManager.scoped({
|
|
2637
|
+
agentId: context.agentId,
|
|
2638
|
+
sessionId: context.sessionId,
|
|
2639
|
+
userId: context.userId,
|
|
2640
|
+
labels: context.labels
|
|
2641
|
+
}),
|
|
2642
|
+
this.budgetManager,
|
|
2643
|
+
this.enforceBudgets
|
|
2644
|
+
);
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* Track an API call
|
|
2648
|
+
*/
|
|
2649
|
+
async track(provider, model, inputTokens, outputTokens, options) {
|
|
2650
|
+
return this.costManager.track({
|
|
2651
|
+
provider,
|
|
2652
|
+
model,
|
|
2653
|
+
tokens: {
|
|
2654
|
+
inputTokens,
|
|
2655
|
+
outputTokens,
|
|
2656
|
+
totalTokens: inputTokens + outputTokens
|
|
2657
|
+
},
|
|
2658
|
+
latencyMs: options?.latencyMs,
|
|
2659
|
+
success: options?.success ?? true,
|
|
2660
|
+
error: options?.error,
|
|
2661
|
+
attribution: options?.attribution,
|
|
2662
|
+
metadata: options?.metadata
|
|
2663
|
+
});
|
|
2664
|
+
}
|
|
2665
|
+
/**
|
|
2666
|
+
* Check budget before making a call
|
|
2667
|
+
*/
|
|
2668
|
+
async checkBudget(estimatedCost, attribution) {
|
|
2669
|
+
if (!this.budgetManager) {
|
|
2670
|
+
return {
|
|
2671
|
+
allowed: true,
|
|
2672
|
+
matchingBudgets: [],
|
|
2673
|
+
exceededBudgets: [],
|
|
2674
|
+
warningBudgets: [],
|
|
2675
|
+
action: "allow"
|
|
2676
|
+
};
|
|
2677
|
+
}
|
|
2678
|
+
return this.budgetManager.checkBudget({
|
|
2679
|
+
estimatedCost,
|
|
2680
|
+
attribution: {
|
|
2681
|
+
...this.defaultAttribution,
|
|
2682
|
+
...attribution
|
|
2683
|
+
}
|
|
2684
|
+
});
|
|
2685
|
+
}
|
|
2686
|
+
/**
|
|
2687
|
+
* Get cost summary
|
|
2688
|
+
*/
|
|
2689
|
+
async getSummary(options) {
|
|
2690
|
+
return this.costManager.getSummary({
|
|
2691
|
+
startDate: options?.startDate,
|
|
2692
|
+
endDate: options?.endDate,
|
|
2693
|
+
agentIds: options?.agentId ? [options.agentId] : void 0,
|
|
2694
|
+
userIds: options?.userId ? [options.userId] : void 0
|
|
2695
|
+
});
|
|
2696
|
+
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Get cost manager
|
|
2699
|
+
*/
|
|
2700
|
+
getCostManager() {
|
|
2701
|
+
return this.costManager;
|
|
2702
|
+
}
|
|
2703
|
+
/**
|
|
2704
|
+
* Get budget manager
|
|
2705
|
+
*/
|
|
2706
|
+
getBudgetManager() {
|
|
2707
|
+
return this.budgetManager;
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Estimate cost for a request
|
|
2711
|
+
*/
|
|
2712
|
+
async estimateCost(input, model, options) {
|
|
2713
|
+
const result = await this.costManager.estimateCost(input, {
|
|
2714
|
+
model,
|
|
2715
|
+
provider: options?.provider,
|
|
2716
|
+
estimatedOutputTokens: options?.estimatedOutputTokens
|
|
2717
|
+
});
|
|
2718
|
+
return result.estimatedCost;
|
|
2719
|
+
}
|
|
2720
|
+
/**
|
|
2721
|
+
* Setup event forwarding
|
|
2722
|
+
*/
|
|
2723
|
+
setupEventForwarding() {
|
|
2724
|
+
this.costManager.on("cost:recorded", (record) => {
|
|
2725
|
+
this.emit("cost:recorded", record);
|
|
2726
|
+
});
|
|
2727
|
+
this.costManager.on("cost:batch", (batch) => {
|
|
2728
|
+
this.emit("cost:batch", batch);
|
|
2729
|
+
});
|
|
2730
|
+
this.costManager.on("error", (error) => {
|
|
2731
|
+
this.emit("error", error);
|
|
2732
|
+
});
|
|
2733
|
+
if (this.budgetManager) {
|
|
2734
|
+
this.budgetManager.on("budget:warning", (alert) => {
|
|
2735
|
+
this.emit("budget:warning", {
|
|
2736
|
+
budgetId: alert.budgetId,
|
|
2737
|
+
message: alert.message
|
|
2738
|
+
});
|
|
2739
|
+
});
|
|
2740
|
+
this.budgetManager.on("budget:exceeded", (alert) => {
|
|
2741
|
+
this.emit("budget:exceeded", {
|
|
2742
|
+
budgetId: alert.budgetId,
|
|
2743
|
+
message: alert.message
|
|
2744
|
+
});
|
|
2745
|
+
});
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
};
|
|
2749
|
+
var AgentCostTracker = class _AgentCostTracker {
|
|
2750
|
+
tracker;
|
|
2751
|
+
budgetManager;
|
|
2752
|
+
enforceBudgets;
|
|
2753
|
+
constructor(tracker, budgetManager, enforceBudgets = false) {
|
|
2754
|
+
this.tracker = tracker;
|
|
2755
|
+
this.budgetManager = budgetManager;
|
|
2756
|
+
this.enforceBudgets = enforceBudgets;
|
|
2757
|
+
}
|
|
2758
|
+
/**
|
|
2759
|
+
* Track an API call
|
|
2760
|
+
*/
|
|
2761
|
+
async track(provider, model, inputTokens, outputTokens, options) {
|
|
2762
|
+
return this.tracker.track({
|
|
2763
|
+
provider,
|
|
2764
|
+
model,
|
|
2765
|
+
tokens: {
|
|
2766
|
+
inputTokens,
|
|
2767
|
+
outputTokens,
|
|
2768
|
+
totalTokens: inputTokens + outputTokens
|
|
2769
|
+
},
|
|
2770
|
+
latencyMs: options?.latencyMs,
|
|
2771
|
+
success: options?.success ?? true,
|
|
2772
|
+
error: options?.error,
|
|
2773
|
+
metadata: options?.metadata
|
|
2774
|
+
});
|
|
2775
|
+
}
|
|
2776
|
+
/**
|
|
2777
|
+
* Track Anthropic response
|
|
2778
|
+
*/
|
|
2779
|
+
async trackAnthropicResponse(response, options) {
|
|
2780
|
+
return this.tracker.trackAnthropicResponse(response, options);
|
|
2781
|
+
}
|
|
2782
|
+
/**
|
|
2783
|
+
* Track OpenAI response
|
|
2784
|
+
*/
|
|
2785
|
+
async trackOpenAIResponse(response, options) {
|
|
2786
|
+
return this.tracker.trackOpenAIResponse(response, options);
|
|
2787
|
+
}
|
|
2788
|
+
/**
|
|
2789
|
+
* Check budget before making a call
|
|
2790
|
+
*/
|
|
2791
|
+
async checkBudget(estimatedCost) {
|
|
2792
|
+
if (!this.budgetManager) {
|
|
2793
|
+
return {
|
|
2794
|
+
allowed: true,
|
|
2795
|
+
matchingBudgets: [],
|
|
2796
|
+
exceededBudgets: [],
|
|
2797
|
+
warningBudgets: [],
|
|
2798
|
+
action: "allow"
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
return this.budgetManager.checkBudget({
|
|
2802
|
+
estimatedCost
|
|
2803
|
+
});
|
|
2804
|
+
}
|
|
2805
|
+
/**
|
|
2806
|
+
* Wrap an async function with cost tracking
|
|
2807
|
+
*/
|
|
2808
|
+
wrap(fn, options) {
|
|
2809
|
+
return async (...args) => {
|
|
2810
|
+
const startTime = Date.now();
|
|
2811
|
+
try {
|
|
2812
|
+
const result = await fn(...args);
|
|
2813
|
+
const latencyMs = Date.now() - startTime;
|
|
2814
|
+
let inputTokens = 0;
|
|
2815
|
+
let outputTokens = 0;
|
|
2816
|
+
if (options.extractUsage) {
|
|
2817
|
+
const usage = options.extractUsage(result);
|
|
2818
|
+
inputTokens = usage.inputTokens;
|
|
2819
|
+
outputTokens = usage.outputTokens;
|
|
2820
|
+
} else if (result.usage) {
|
|
2821
|
+
inputTokens = result.usage.input_tokens ?? result.usage.prompt_tokens ?? 0;
|
|
2822
|
+
outputTokens = result.usage.output_tokens ?? result.usage.completion_tokens ?? 0;
|
|
2823
|
+
}
|
|
2824
|
+
await this.track(
|
|
2825
|
+
options.provider,
|
|
2826
|
+
result.model,
|
|
2827
|
+
inputTokens,
|
|
2828
|
+
outputTokens,
|
|
2829
|
+
{ latencyMs }
|
|
2830
|
+
);
|
|
2831
|
+
return result;
|
|
2832
|
+
} catch (error) {
|
|
2833
|
+
const latencyMs = Date.now() - startTime;
|
|
2834
|
+
await this.tracker.track({
|
|
2835
|
+
provider: options.provider,
|
|
2836
|
+
model: "unknown",
|
|
2837
|
+
tokens: {
|
|
2838
|
+
inputTokens: 0,
|
|
2839
|
+
outputTokens: 0,
|
|
2840
|
+
totalTokens: 0
|
|
2841
|
+
},
|
|
2842
|
+
latencyMs,
|
|
2843
|
+
success: false,
|
|
2844
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2845
|
+
});
|
|
2846
|
+
throw error;
|
|
2847
|
+
}
|
|
2848
|
+
};
|
|
2849
|
+
}
|
|
2850
|
+
/**
|
|
2851
|
+
* Create a nested scope
|
|
2852
|
+
*/
|
|
2853
|
+
scoped(attribution) {
|
|
2854
|
+
return new _AgentCostTracker(
|
|
2855
|
+
this.tracker.scoped(attribution),
|
|
2856
|
+
this.budgetManager,
|
|
2857
|
+
this.enforceBudgets
|
|
2858
|
+
);
|
|
2859
|
+
}
|
|
2860
|
+
};
|
|
2861
|
+
function createCostProvider(config = {}) {
|
|
2862
|
+
return new CostProvider(config);
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
export { AgentCostTracker, BudgetManager, BufferStorage, CostManager, CostProvider, CostTracker, ModelPricingRegistry, ScopedCostTracker, TokenCounter, countTokens, countTokensApprox, createCostManager, createCostProvider };
|
|
2866
|
+
//# sourceMappingURL=index.js.map
|
|
2867
|
+
//# sourceMappingURL=index.js.map
|