adaptive-memory-multi-model-router 2.6.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -22
- package/dist/cache/semanticCache.d.ts +54 -22
- package/dist/cache/semanticCache.js +230 -86
- package/dist/cache/semanticCache.js.map +1 -1
- package/dist/cost/budgetEnforcer.d.ts +108 -0
- package/dist/cost/budgetEnforcer.js +295 -0
- package/dist/cost/budgetEnforcer.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +15 -1
- package/dist/routing/providerHealth.d.ts +154 -0
- package/dist/routing/providerHealth.js +371 -0
- package/dist/routing/providerHealth.js.map +1 -0
- package/dist/routing/providerRetry.d.ts +110 -0
- package/dist/routing/providerRetry.js +460 -0
- package/dist/routing/providerRetry.js.map +1 -0
- package/dist/sdk.d.ts +124 -0
- package/dist/sdk.js +109 -100
- package/package.json +3 -2
- package/src/cache/semanticCache.ts +293 -103
- package/src/cost/budgetEnforcer.ts +358 -0
- package/src/index.ts +20 -0
- package/src/routing/providerHealth.ts +483 -0
- package/src/routing/providerRetry.ts +578 -0
- package/test/test_budgetEnforcer.ts +310 -0
- package/test/test_providerHealth.ts +523 -0
- package/test/test_providerRetry.ts +348 -0
- package/test/test_semanticCache.ts +507 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A3M Router - Budget Enforcer
|
|
3
|
+
*
|
|
4
|
+
* Hard budget enforcement for API key spend management:
|
|
5
|
+
* - Track spend per API key with monthly reset
|
|
6
|
+
* - Check budget before each request
|
|
7
|
+
* - Emit alerts at configurable thresholds (50%, 80%, 100%)
|
|
8
|
+
* - Support hard cap (reject requests) or soft cap (warn only)
|
|
9
|
+
* - In-memory storage (Redis backup can be added later)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { EventEmitter } from 'events';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Types
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export interface BudgetConfig {
|
|
19
|
+
apiKey: string;
|
|
20
|
+
monthlyLimit: number; // in cents
|
|
21
|
+
alertThresholds?: number[]; // e.g., [0.5, 0.8, 1.0] for 50%, 80%, 100%
|
|
22
|
+
hardCap?: boolean; // reject requests when exceeded (default: false)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SpendRecord {
|
|
26
|
+
apiKey: string;
|
|
27
|
+
spent: number; // cents
|
|
28
|
+
budget: number; // cents
|
|
29
|
+
remaining: number; // cents
|
|
30
|
+
resetDate: Date;
|
|
31
|
+
alertEmitted: Set<number>; // track which thresholds have fired
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface BudgetCheckResult {
|
|
35
|
+
allowed: boolean;
|
|
36
|
+
reason?: string;
|
|
37
|
+
remaining: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// BudgetEnforcer
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
export class BudgetEnforcer extends EventEmitter {
|
|
45
|
+
private budgets: Map<string, BudgetConfig> = new Map();
|
|
46
|
+
private spend: Map<string, SpendRecord> = new Map();
|
|
47
|
+
|
|
48
|
+
// Default alert thresholds: 50%, 80%, 100%
|
|
49
|
+
private static readonly DEFAULT_THRESHOLDS = [0.5, 0.8, 1.0];
|
|
50
|
+
|
|
51
|
+
constructor() {
|
|
52
|
+
super();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---- Configuration ----
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Set budget for an API key
|
|
59
|
+
*/
|
|
60
|
+
setBudget(apiKey: string, monthlyLimit: number, options?: {
|
|
61
|
+
alertThresholds?: number[];
|
|
62
|
+
hardCap?: boolean;
|
|
63
|
+
}): void {
|
|
64
|
+
const config: BudgetConfig = {
|
|
65
|
+
apiKey,
|
|
66
|
+
monthlyLimit,
|
|
67
|
+
alertThresholds: options?.alertThresholds ?? BudgetEnforcer.DEFAULT_THRESHOLDS,
|
|
68
|
+
hardCap: options?.hardCap ?? false,
|
|
69
|
+
};
|
|
70
|
+
this.budgets.set(apiKey, config);
|
|
71
|
+
|
|
72
|
+
// Initialize spend record if not exists
|
|
73
|
+
if (!this.spend.has(apiKey)) {
|
|
74
|
+
this.spend.set(apiKey, {
|
|
75
|
+
apiKey,
|
|
76
|
+
spent: 0,
|
|
77
|
+
budget: monthlyLimit,
|
|
78
|
+
remaining: monthlyLimit,
|
|
79
|
+
resetDate: this.getNextResetDate(),
|
|
80
|
+
alertEmitted: new Set(),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get current budget config for an API key
|
|
87
|
+
*/
|
|
88
|
+
getBudgetConfig(apiKey: string): BudgetConfig | undefined {
|
|
89
|
+
return this.budgets.get(apiKey);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Remove budget config (stops tracking)
|
|
94
|
+
*/
|
|
95
|
+
removeBudget(apiKey: string): void {
|
|
96
|
+
this.budgets.delete(apiKey);
|
|
97
|
+
this.spend.delete(apiKey);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---- Budget Checking ----
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if a request is allowed within budget
|
|
104
|
+
* @param apiKey - The API key making the request
|
|
105
|
+
* @param additionalCost - The cost of the request in cents
|
|
106
|
+
* @returns BudgetCheckResult with allowed status and remaining budget
|
|
107
|
+
*/
|
|
108
|
+
checkBudget(apiKey: string, additionalCost: number): BudgetCheckResult {
|
|
109
|
+
// Auto-initialize if no budget set (permissive default)
|
|
110
|
+
if (!this.budgets.has(apiKey)) {
|
|
111
|
+
return {
|
|
112
|
+
allowed: true,
|
|
113
|
+
remaining: Infinity,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check for reset
|
|
118
|
+
this.checkAndReset(apiKey);
|
|
119
|
+
|
|
120
|
+
const record = this.spend.get(apiKey)!;
|
|
121
|
+
const config = this.budgets.get(apiKey)!;
|
|
122
|
+
const projectedSpend = record.spent + additionalCost;
|
|
123
|
+
|
|
124
|
+
// Check if would exceed budget
|
|
125
|
+
if (projectedSpend > record.budget) {
|
|
126
|
+
if (config.hardCap) {
|
|
127
|
+
return {
|
|
128
|
+
allowed: false,
|
|
129
|
+
reason: `Budget exceeded. Spent: ${record.spent} cents, Budget: ${record.budget} cents, Additional: ${additionalCost} cents`,
|
|
130
|
+
remaining: record.remaining,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// Soft cap: allow but warn
|
|
134
|
+
this.emit('budget:warning', {
|
|
135
|
+
apiKey,
|
|
136
|
+
threshold: 1.0,
|
|
137
|
+
spent: record.spent,
|
|
138
|
+
budget: record.budget,
|
|
139
|
+
remaining: record.remaining,
|
|
140
|
+
message: `Soft cap: Budget would be exceeded by ${projectedSpend - record.budget} cents`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
allowed: true,
|
|
146
|
+
remaining: record.remaining - additionalCost,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---- Spend Recording ----
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Record spend for an API key
|
|
154
|
+
* @param apiKey - The API key that incurred the cost
|
|
155
|
+
* @param cost - The cost in cents
|
|
156
|
+
*/
|
|
157
|
+
recordSpend(apiKey: string, cost: number): void {
|
|
158
|
+
// Auto-initialize if no budget set
|
|
159
|
+
if (!this.budgets.has(apiKey)) {
|
|
160
|
+
return; // No budget to track
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check for reset
|
|
164
|
+
this.checkAndReset(apiKey);
|
|
165
|
+
|
|
166
|
+
const record = this.spend.get(apiKey)!;
|
|
167
|
+
const config = this.budgets.get(apiKey)!;
|
|
168
|
+
|
|
169
|
+
const previousSpent = record.spent;
|
|
170
|
+
record.spent += cost;
|
|
171
|
+
record.remaining = record.budget - record.spent;
|
|
172
|
+
|
|
173
|
+
// Check thresholds
|
|
174
|
+
const utilization = record.spent / record.budget;
|
|
175
|
+
for (const threshold of config.alertThresholds ?? BudgetEnforcer.DEFAULT_THRESHOLDS) {
|
|
176
|
+
const thresholdKey = Math.round(threshold * 1000);
|
|
177
|
+
if (!record.alertEmitted.has(thresholdKey)) {
|
|
178
|
+
const previousUtilization = previousSpent / record.budget;
|
|
179
|
+
if (utilization >= threshold && previousUtilization < threshold) {
|
|
180
|
+
record.alertEmitted.add(thresholdKey);
|
|
181
|
+
this.emit('budget:warning', {
|
|
182
|
+
apiKey,
|
|
183
|
+
threshold,
|
|
184
|
+
spent: record.spent,
|
|
185
|
+
budget: record.budget,
|
|
186
|
+
remaining: record.remaining,
|
|
187
|
+
message: `Budget ${Math.round(threshold * 100)}% threshold reached`,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Fire 100% event if budget exceeded (for hard cap tracking)
|
|
194
|
+
if (record.spent >= record.budget && previousSpent < record.budget) {
|
|
195
|
+
this.emit('budget:exceeded', {
|
|
196
|
+
apiKey,
|
|
197
|
+
spent: record.spent,
|
|
198
|
+
budget: record.budget,
|
|
199
|
+
remaining: 0,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ---- Spend Queries ----
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get current spend record for an API key
|
|
208
|
+
*/
|
|
209
|
+
getSpend(apiKey: string): SpendRecord | undefined {
|
|
210
|
+
if (!this.spend.has(apiKey)) {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
const record = { ...this.spend.get(apiKey)! };
|
|
214
|
+
// Convert Set to Array for serialization
|
|
215
|
+
(record as any).alertEmitted = Array.from(record.alertEmitted);
|
|
216
|
+
return record;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get all spend records
|
|
221
|
+
*/
|
|
222
|
+
getAllSpend(): SpendRecord[] {
|
|
223
|
+
return Array.from(this.spend.values()).map((record) => {
|
|
224
|
+
const copy = { ...record };
|
|
225
|
+
(copy as any).alertEmitted = Array.from(record.alertEmitted);
|
|
226
|
+
return copy;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ---- Budget Management ----
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Reset budget for an API key (manual reset)
|
|
234
|
+
*/
|
|
235
|
+
resetBudget(apiKey: string): void {
|
|
236
|
+
if (!this.spend.has(apiKey)) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const record = this.spend.get(apiKey)!;
|
|
240
|
+
record.spent = 0;
|
|
241
|
+
record.remaining = record.budget;
|
|
242
|
+
record.resetDate = this.getNextResetDate();
|
|
243
|
+
record.alertEmitted.clear();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Reset all budgets
|
|
248
|
+
*/
|
|
249
|
+
resetAll(): void {
|
|
250
|
+
for (const record of this.spend.values()) {
|
|
251
|
+
record.spent = 0;
|
|
252
|
+
record.remaining = record.budget;
|
|
253
|
+
record.resetDate = this.getNextResetDate();
|
|
254
|
+
record.alertEmitted.clear();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Update monthly limit for an API key
|
|
260
|
+
*/
|
|
261
|
+
updateLimit(apiKey: string, monthlyLimit: number): void {
|
|
262
|
+
if (!this.budgets.has(apiKey)) {
|
|
263
|
+
this.setBudget(apiKey, monthlyLimit);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const config = this.budgets.get(apiKey)!;
|
|
268
|
+
config.monthlyLimit = monthlyLimit;
|
|
269
|
+
|
|
270
|
+
const record = this.spend.get(apiKey)!;
|
|
271
|
+
const oldBudget = record.budget;
|
|
272
|
+
record.budget = monthlyLimit;
|
|
273
|
+
record.remaining = monthlyLimit - record.spent;
|
|
274
|
+
|
|
275
|
+
// Re-check if any thresholds are now exceeded
|
|
276
|
+
const utilization = record.spent / record.budget;
|
|
277
|
+
for (const threshold of config.alertThresholds ?? BudgetEnforcer.DEFAULT_THRESHOLDS) {
|
|
278
|
+
const thresholdKey = Math.round(threshold * 1000);
|
|
279
|
+
if (utilization >= threshold) {
|
|
280
|
+
record.alertEmitted.add(thresholdKey);
|
|
281
|
+
} else {
|
|
282
|
+
record.alertEmitted.delete(thresholdKey);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ---- Internal Helpers ----
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Check if reset is needed and perform it
|
|
291
|
+
*/
|
|
292
|
+
private checkAndReset(apiKey: string): void {
|
|
293
|
+
const record = this.spend.get(apiKey)!;
|
|
294
|
+
if (this.isResetDue(record.resetDate)) {
|
|
295
|
+
record.spent = 0;
|
|
296
|
+
record.remaining = record.budget;
|
|
297
|
+
record.resetDate = this.getNextResetDate();
|
|
298
|
+
record.alertEmitted.clear();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Check if monthly reset is due
|
|
304
|
+
*/
|
|
305
|
+
private isResetDue(resetDate: Date): boolean {
|
|
306
|
+
const now = new Date();
|
|
307
|
+
return now >= resetDate;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Get next monthly reset date
|
|
312
|
+
*/
|
|
313
|
+
private getNextResetDate(): Date {
|
|
314
|
+
const now = new Date();
|
|
315
|
+
const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
|
|
316
|
+
return nextMonth;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Get days until next reset
|
|
321
|
+
*/
|
|
322
|
+
getDaysUntilReset(apiKey: string): number | undefined {
|
|
323
|
+
const record = this.spend.get(apiKey);
|
|
324
|
+
if (!record) return undefined;
|
|
325
|
+
const now = Date.now();
|
|
326
|
+
const resetMs = record.resetDate.getTime();
|
|
327
|
+
return Math.max(0, Math.ceil((resetMs - now) / (1000 * 60 * 60 * 24)));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// Convenience Factory
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
|
|
335
|
+
export function createBudgetEnforcer(): BudgetEnforcer {
|
|
336
|
+
return new BudgetEnforcer();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
// Error Class for Hard Cap Violations
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
export class BudgetExceededError extends Error {
|
|
344
|
+
public readonly apiKey: string;
|
|
345
|
+
public readonly spent: number;
|
|
346
|
+
public readonly budget: number;
|
|
347
|
+
public readonly remaining: number;
|
|
348
|
+
|
|
349
|
+
constructor(apiKey: string, spent: number, budget: number) {
|
|
350
|
+
const message = `Budget exceeded for API key ${apiKey}. Spent: ${spent} cents, Budget: ${budget} cents`;
|
|
351
|
+
super(message);
|
|
352
|
+
this.name = 'BudgetExceededError';
|
|
353
|
+
this.apiKey = apiKey;
|
|
354
|
+
this.spent = spent;
|
|
355
|
+
this.budget = budget;
|
|
356
|
+
this.remaining = budget - spent;
|
|
357
|
+
}
|
|
358
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,24 @@ export {
|
|
|
14
14
|
getProviderHealth,
|
|
15
15
|
} from './routing/advancedRouter';
|
|
16
16
|
|
|
17
|
+
// ============================================================
|
|
18
|
+
// ROUTING
|
|
19
|
+
// ============================================================
|
|
20
|
+
export {
|
|
21
|
+
ProviderRetryHandler,
|
|
22
|
+
createRetryHandler,
|
|
23
|
+
getDefaultRetryHandler,
|
|
24
|
+
DEFAULT_RETRY_CONFIG,
|
|
25
|
+
DEFAULT_PROVIDER_CONFIG,
|
|
26
|
+
PROVIDER_CONTEXT_LIMITS,
|
|
27
|
+
} from './routing/providerRetry';
|
|
28
|
+
export type {
|
|
29
|
+
RetryConfig,
|
|
30
|
+
ProviderRetryConfig,
|
|
31
|
+
RetryStats,
|
|
32
|
+
ContextWindowValidation,
|
|
33
|
+
} from './routing/providerRetry';
|
|
34
|
+
|
|
17
35
|
// ============================================================
|
|
18
36
|
// PROVIDERS
|
|
19
37
|
// ============================================================
|
|
@@ -43,6 +61,8 @@ export type {
|
|
|
43
61
|
// COST TRACKING
|
|
44
62
|
// ============================================================
|
|
45
63
|
export { CostTracker } from './cost/costTracker';
|
|
64
|
+
export { BudgetEnforcer, BudgetExceededError, createBudgetEnforcer } from './cost/budgetEnforcer';
|
|
65
|
+
export type { BudgetConfig, SpendRecord, BudgetCheckResult } from './cost/budgetEnforcer';
|
|
46
66
|
|
|
47
67
|
// ============================================================
|
|
48
68
|
// MEMORY
|