adaptive-memory-multi-model-router 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +2 -0
- package/dist/index.js +5 -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/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 +2 -0
- package/src/routing/providerHealth.ts +483 -0
- package/test/test_budgetEnforcer.ts +310 -0
- package/test/test_providerHealth.ts +523 -0
- package/test/test_semanticCache.ts +507 -0
|
@@ -1,140 +1,273 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* A3M Router - Semantic Cache
|
|
3
|
+
* A3M Router - Semantic Cache (Embedding-based)
|
|
4
4
|
*
|
|
5
5
|
* Stores previous query->response pairs and returns cached responses
|
|
6
|
-
* for semantically similar queries using
|
|
6
|
+
* for semantically similar queries using cosine similarity on embeddings.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* "Write a sort fn" ≈ "Create a sorting fn" ≈ "How to sort an array"
|
|
8
|
+
* Supports embedders: nomic (via Ollama), openai, or local Ollama.
|
|
9
|
+
* Uses nomic-embed-text by default via local Ollama.
|
|
11
10
|
*/
|
|
12
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
12
|
exports.SemanticCache = void 0;
|
|
13
|
+
class NomicEmbedder {
|
|
14
|
+
url;
|
|
15
|
+
constructor(url = 'http://127.0.0.1:11434/api/embeddings') {
|
|
16
|
+
this.url = url;
|
|
17
|
+
}
|
|
18
|
+
async embed(text) {
|
|
19
|
+
const response = await fetch(this.url, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
body: JSON.stringify({
|
|
23
|
+
model: 'nomic-embed-text',
|
|
24
|
+
prompt: text,
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
throw new Error(`Nomic embedder error: ${response.status} ${response.statusText}`);
|
|
29
|
+
}
|
|
30
|
+
const data = await response.json();
|
|
31
|
+
return data.embedding;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
class OpenAIEmbedder {
|
|
35
|
+
apiKey;
|
|
36
|
+
url;
|
|
37
|
+
constructor(apiKey, url = 'https://api.openai.com/v1/embeddings') {
|
|
38
|
+
this.apiKey = apiKey;
|
|
39
|
+
this.url = url;
|
|
40
|
+
}
|
|
41
|
+
async embed(text) {
|
|
42
|
+
const response = await fetch(this.url, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: {
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
model: 'text-embedding-3-small',
|
|
50
|
+
input: text,
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
throw new Error(`OpenAI embedder error: ${response.status} ${response.statusText}`);
|
|
55
|
+
}
|
|
56
|
+
const data = await response.json();
|
|
57
|
+
return data.data[0]?.embedding ?? [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
class LocalOllamaEmbedder {
|
|
61
|
+
baseUrl;
|
|
62
|
+
model;
|
|
63
|
+
constructor(baseUrl = 'http://127.0.0.1:11434/api/embeddings', model = 'nomic-embed-text') {
|
|
64
|
+
this.baseUrl = baseUrl;
|
|
65
|
+
this.model = model;
|
|
66
|
+
}
|
|
67
|
+
async embed(text) {
|
|
68
|
+
const response = await fetch(this.baseUrl, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: { 'Content-Type': 'application/json' },
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
model: this.model,
|
|
73
|
+
prompt: text,
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
throw new Error(`Local Ollama embedder error: ${response.status} ${response.statusText}`);
|
|
78
|
+
}
|
|
79
|
+
const data = await response.json();
|
|
80
|
+
return data.embedding;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
14
83
|
// ============================================================
|
|
15
|
-
//
|
|
84
|
+
// Utilities
|
|
16
85
|
// ============================================================
|
|
17
86
|
/**
|
|
18
|
-
*
|
|
87
|
+
* Compute cosine similarity between two vectors.
|
|
88
|
+
*/
|
|
89
|
+
function cosineSimilarity(a, b) {
|
|
90
|
+
if (a.length !== b.length)
|
|
91
|
+
return 0;
|
|
92
|
+
if (a.length === 0)
|
|
93
|
+
return 0;
|
|
94
|
+
let dotProduct = 0;
|
|
95
|
+
let normA = 0;
|
|
96
|
+
let normB = 0;
|
|
97
|
+
for (let i = 0; i < a.length; i++) {
|
|
98
|
+
dotProduct += a[i] * b[i];
|
|
99
|
+
normA += a[i] * a[i];
|
|
100
|
+
normB += b[i] * b[i];
|
|
101
|
+
}
|
|
102
|
+
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
|
|
103
|
+
return denominator === 0 ? 0 : dotProduct / denominator;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Normalize text for hashing: lowercase, collapse whitespace.
|
|
19
107
|
*/
|
|
20
108
|
function normalize(text) {
|
|
21
109
|
return text
|
|
22
110
|
.toLowerCase()
|
|
23
|
-
.replace(/\s+/g,
|
|
111
|
+
.replace(/\s+/g, ' ')
|
|
24
112
|
.trim();
|
|
25
113
|
}
|
|
26
114
|
/**
|
|
27
|
-
*
|
|
28
|
-
* Pads with spaces so short words still produce trigrams.
|
|
115
|
+
* Simple hash for cache key (deterministic, fast).
|
|
29
116
|
*/
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
for (let i = 0; i
|
|
34
|
-
|
|
117
|
+
function hashKey(text) {
|
|
118
|
+
let hash = 0;
|
|
119
|
+
const normalized = normalize(text);
|
|
120
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
121
|
+
const char = normalized.charCodeAt(i);
|
|
122
|
+
hash = ((hash << 5) - hash) + char;
|
|
123
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
35
124
|
}
|
|
36
|
-
return
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Compute Jaccard similarity between two sets.
|
|
40
|
-
* |A ∩ B| / |A ∪ B|
|
|
41
|
-
*/
|
|
42
|
-
function jaccard(a, b) {
|
|
43
|
-
if (a.size === 0 && b.size === 0)
|
|
44
|
-
return 1.0;
|
|
45
|
-
let intersection = 0;
|
|
46
|
-
for (const item of a) {
|
|
47
|
-
if (b.has(item))
|
|
48
|
-
intersection++;
|
|
49
|
-
}
|
|
50
|
-
const union = a.size + b.size - intersection;
|
|
51
|
-
return union === 0 ? 0 : intersection / union;
|
|
125
|
+
return hash.toString(36) + '_' + normalized.slice(0, 20);
|
|
52
126
|
}
|
|
53
127
|
// ============================================================
|
|
54
128
|
// Semantic Cache
|
|
55
129
|
// ============================================================
|
|
56
130
|
class SemanticCache {
|
|
57
|
-
entries =
|
|
58
|
-
|
|
131
|
+
entries = new Map();
|
|
132
|
+
accessOrder = []; // track LRU order
|
|
133
|
+
embedder;
|
|
59
134
|
similarityThreshold;
|
|
60
|
-
|
|
135
|
+
ttlMs;
|
|
136
|
+
maxEntries;
|
|
61
137
|
hits = 0;
|
|
62
138
|
misses = 0;
|
|
63
|
-
constructor(
|
|
64
|
-
this.
|
|
65
|
-
this.
|
|
66
|
-
this.
|
|
139
|
+
constructor(config) {
|
|
140
|
+
this.similarityThreshold = config.similarityThreshold;
|
|
141
|
+
this.ttlMs = (config.ttlSeconds || 3600) * 1000;
|
|
142
|
+
this.maxEntries = config.maxEntries || 1000;
|
|
143
|
+
// Initialize embedder based on config
|
|
144
|
+
switch (config.embedder) {
|
|
145
|
+
case 'openai':
|
|
146
|
+
if (!config.embedderApiKey) {
|
|
147
|
+
throw new Error('OpenAI embedder requires apiKey in config.embedderApiKey');
|
|
148
|
+
}
|
|
149
|
+
this.embedder = new OpenAIEmbedder(config.embedderApiKey, config.embedderUrl);
|
|
150
|
+
break;
|
|
151
|
+
case 'local':
|
|
152
|
+
this.embedder = new LocalOllamaEmbedder(config.embedderUrl || 'http://127.0.0.1:11434/api/embeddings', 'nomic-embed-text');
|
|
153
|
+
break;
|
|
154
|
+
case 'nomic':
|
|
155
|
+
default:
|
|
156
|
+
this.embedder = new NomicEmbedder(config.embedderUrl || 'http://127.0.0.1:11434/api/embeddings');
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
67
159
|
}
|
|
68
160
|
/**
|
|
69
161
|
* Get cached response for a semantically similar query.
|
|
70
|
-
* Returns the best match above the similarity threshold, or
|
|
162
|
+
* Returns the best match above the similarity threshold, or { hit: false }.
|
|
71
163
|
*/
|
|
72
164
|
async get(query) {
|
|
73
165
|
const now = Date.now();
|
|
74
|
-
|
|
166
|
+
// Generate embedding for the query
|
|
167
|
+
let queryEmbedding;
|
|
168
|
+
try {
|
|
169
|
+
queryEmbedding = await this.embedder.embed(query);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
console.warn('SemanticCache: Failed to generate embedding for query:', error);
|
|
173
|
+
this.misses++;
|
|
174
|
+
return { hit: false };
|
|
175
|
+
}
|
|
75
176
|
let bestEntry = null;
|
|
76
|
-
let
|
|
77
|
-
for (const entry of this.entries) {
|
|
78
|
-
// Skip expired
|
|
79
|
-
if (now > entry.
|
|
177
|
+
let bestSimilarity = 0;
|
|
178
|
+
for (const entry of this.entries.values()) {
|
|
179
|
+
// Skip expired entries
|
|
180
|
+
if (now > entry.createdAt + entry.ttl)
|
|
80
181
|
continue;
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
83
|
-
|
|
182
|
+
const similarity = cosineSimilarity(queryEmbedding, entry.embedding);
|
|
183
|
+
if (similarity > bestSimilarity) {
|
|
184
|
+
bestSimilarity = similarity;
|
|
84
185
|
bestEntry = entry;
|
|
85
186
|
}
|
|
86
187
|
}
|
|
87
|
-
if (bestEntry &&
|
|
188
|
+
if (bestEntry && bestSimilarity >= this.similarityThreshold) {
|
|
88
189
|
this.hits++;
|
|
89
190
|
bestEntry.hitCount++;
|
|
191
|
+
bestEntry.lastAccessedAt = now;
|
|
192
|
+
this.updateAccessOrder(bestEntry.key);
|
|
90
193
|
return {
|
|
91
|
-
|
|
194
|
+
hit: true,
|
|
92
195
|
response: bestEntry.response,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
196
|
+
provider: bestEntry.provider,
|
|
197
|
+
model: bestEntry.model,
|
|
198
|
+
cost: bestEntry.cost,
|
|
199
|
+
similarity: bestSimilarity,
|
|
96
200
|
};
|
|
97
201
|
}
|
|
98
202
|
this.misses++;
|
|
99
|
-
return
|
|
203
|
+
return { hit: false };
|
|
100
204
|
}
|
|
101
205
|
/**
|
|
102
206
|
* Store a query->response pair in the cache.
|
|
103
207
|
*/
|
|
104
208
|
async set(query, response, metadata) {
|
|
105
209
|
const now = Date.now();
|
|
106
|
-
|
|
210
|
+
const key = hashKey(query);
|
|
211
|
+
// Generate embedding
|
|
212
|
+
let embedding;
|
|
213
|
+
try {
|
|
214
|
+
embedding = await this.embedder.embed(query);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
console.warn('SemanticCache: Failed to generate embedding for set:', error);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// Evict expired entries
|
|
107
221
|
this.evictExpired();
|
|
108
|
-
// Evict oldest if at capacity
|
|
109
|
-
if (this.entries.
|
|
110
|
-
this.
|
|
222
|
+
// Evict oldest if at capacity (LRU)
|
|
223
|
+
if (this.entries.size >= this.maxEntries) {
|
|
224
|
+
this.evictLRU();
|
|
111
225
|
}
|
|
112
|
-
// Check if
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (existing) {
|
|
226
|
+
// Check if entry already exists and update it
|
|
227
|
+
if (this.entries.has(key)) {
|
|
228
|
+
const existing = this.entries.get(key);
|
|
116
229
|
existing.response = response;
|
|
117
|
-
existing.
|
|
118
|
-
existing.
|
|
119
|
-
existing.
|
|
120
|
-
existing.
|
|
230
|
+
existing.embedding = embedding;
|
|
231
|
+
existing.provider = metadata.provider;
|
|
232
|
+
existing.model = metadata.model;
|
|
233
|
+
existing.cost = metadata.cost;
|
|
234
|
+
existing.createdAt = now;
|
|
235
|
+
existing.ttl = (metadata.ttl !== undefined ? metadata.ttl : this.ttlMs / 1000) * 1000;
|
|
236
|
+
existing.lastAccessedAt = now;
|
|
237
|
+
existing.hitCount = existing.hitCount; // preserve hit count
|
|
121
238
|
return;
|
|
122
239
|
}
|
|
123
|
-
|
|
124
|
-
|
|
240
|
+
const entry = {
|
|
241
|
+
key,
|
|
242
|
+
embedding,
|
|
125
243
|
response,
|
|
126
|
-
metadata,
|
|
127
|
-
|
|
128
|
-
|
|
244
|
+
provider: metadata.provider,
|
|
245
|
+
model: metadata.model,
|
|
246
|
+
cost: metadata.cost,
|
|
247
|
+
createdAt: now,
|
|
248
|
+
ttl: (metadata.ttl !== undefined ? metadata.ttl : this.ttlMs / 1000) * 1000,
|
|
129
249
|
hitCount: 0,
|
|
130
|
-
|
|
131
|
-
}
|
|
250
|
+
lastAccessedAt: now,
|
|
251
|
+
};
|
|
252
|
+
this.entries.set(key, entry);
|
|
253
|
+
this.accessOrder.push(key);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Delete a specific query from the cache.
|
|
257
|
+
*/
|
|
258
|
+
async delete(query) {
|
|
259
|
+
const key = hashKey(query);
|
|
260
|
+
if (this.entries.has(key)) {
|
|
261
|
+
this.entries.delete(key);
|
|
262
|
+
this.accessOrder = this.accessOrder.filter(k => k !== key);
|
|
263
|
+
}
|
|
132
264
|
}
|
|
133
265
|
/**
|
|
134
266
|
* Clear all cache entries.
|
|
135
267
|
*/
|
|
136
268
|
clear() {
|
|
137
|
-
this.entries
|
|
269
|
+
this.entries.clear();
|
|
270
|
+
this.accessOrder = [];
|
|
138
271
|
}
|
|
139
272
|
/**
|
|
140
273
|
* Get cache statistics.
|
|
@@ -142,35 +275,46 @@ class SemanticCache {
|
|
|
142
275
|
getStats() {
|
|
143
276
|
const total = this.hits + this.misses;
|
|
144
277
|
return {
|
|
278
|
+
size: this.entries.size,
|
|
145
279
|
hits: this.hits,
|
|
146
280
|
misses: this.misses,
|
|
147
281
|
hitRate: total > 0 ? this.hits / total : 0,
|
|
148
|
-
size: this.entries.length,
|
|
149
282
|
};
|
|
150
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* Update access order for LRU tracking.
|
|
286
|
+
*/
|
|
287
|
+
updateAccessOrder(key) {
|
|
288
|
+
this.accessOrder = this.accessOrder.filter(k => k !== key);
|
|
289
|
+
this.accessOrder.push(key);
|
|
290
|
+
}
|
|
151
291
|
/**
|
|
152
292
|
* Purge expired entries.
|
|
153
293
|
*/
|
|
154
294
|
evictExpired() {
|
|
155
295
|
const now = Date.now();
|
|
156
|
-
|
|
296
|
+
for (const [key, entry] of this.entries.entries()) {
|
|
297
|
+
if (now > entry.createdAt + entry.ttl) {
|
|
298
|
+
this.entries.delete(key);
|
|
299
|
+
this.accessOrder = this.accessOrder.filter(k => k !== key);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
157
302
|
}
|
|
158
303
|
/**
|
|
159
|
-
* Evict the
|
|
304
|
+
* Evict the least recently used entry.
|
|
160
305
|
*/
|
|
161
|
-
|
|
162
|
-
if (this.
|
|
306
|
+
evictLRU() {
|
|
307
|
+
if (this.accessOrder.length === 0)
|
|
163
308
|
return;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (this.entries[i].cachedAt < oldestTime) {
|
|
168
|
-
oldestTime = this.entries[i].cachedAt;
|
|
169
|
-
oldestIdx = i;
|
|
170
|
-
}
|
|
309
|
+
const lruKey = this.accessOrder.shift();
|
|
310
|
+
if (lruKey && this.entries.has(lruKey)) {
|
|
311
|
+
this.entries.delete(lruKey);
|
|
171
312
|
}
|
|
172
|
-
this.entries.splice(oldestIdx, 1);
|
|
173
313
|
}
|
|
174
314
|
}
|
|
175
315
|
exports.SemanticCache = SemanticCache;
|
|
316
|
+
// ============================================================
|
|
317
|
+
// Exports
|
|
318
|
+
// ============================================================
|
|
319
|
+
exports.default = SemanticCache;
|
|
176
320
|
//# sourceMappingURL=semanticCache.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"semanticCache.js","sourceRoot":"","sources":["../../src/cache/semanticCache.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"semanticCache.js","sourceRoot":"","sources":["../../src/cache/semanticCache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
import { EventEmitter } from 'events';
|
|
12
|
+
export interface BudgetConfig {
|
|
13
|
+
apiKey: string;
|
|
14
|
+
monthlyLimit: number;
|
|
15
|
+
alertThresholds?: number[];
|
|
16
|
+
hardCap?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface SpendRecord {
|
|
19
|
+
apiKey: string;
|
|
20
|
+
spent: number;
|
|
21
|
+
budget: number;
|
|
22
|
+
remaining: number;
|
|
23
|
+
resetDate: Date;
|
|
24
|
+
alertEmitted: Set<number>;
|
|
25
|
+
}
|
|
26
|
+
export interface BudgetCheckResult {
|
|
27
|
+
allowed: boolean;
|
|
28
|
+
reason?: string;
|
|
29
|
+
remaining: number;
|
|
30
|
+
}
|
|
31
|
+
export declare class BudgetEnforcer extends EventEmitter {
|
|
32
|
+
private budgets;
|
|
33
|
+
private spend;
|
|
34
|
+
private static readonly DEFAULT_THRESHOLDS;
|
|
35
|
+
constructor();
|
|
36
|
+
/**
|
|
37
|
+
* Set budget for an API key
|
|
38
|
+
*/
|
|
39
|
+
setBudget(apiKey: string, monthlyLimit: number, options?: {
|
|
40
|
+
alertThresholds?: number[];
|
|
41
|
+
hardCap?: boolean;
|
|
42
|
+
}): void;
|
|
43
|
+
/**
|
|
44
|
+
* Get current budget config for an API key
|
|
45
|
+
*/
|
|
46
|
+
getBudgetConfig(apiKey: string): BudgetConfig | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Remove budget config (stops tracking)
|
|
49
|
+
*/
|
|
50
|
+
removeBudget(apiKey: string): void;
|
|
51
|
+
/**
|
|
52
|
+
* Check if a request is allowed within budget
|
|
53
|
+
* @param apiKey - The API key making the request
|
|
54
|
+
* @param additionalCost - The cost of the request in cents
|
|
55
|
+
* @returns BudgetCheckResult with allowed status and remaining budget
|
|
56
|
+
*/
|
|
57
|
+
checkBudget(apiKey: string, additionalCost: number): BudgetCheckResult;
|
|
58
|
+
/**
|
|
59
|
+
* Record spend for an API key
|
|
60
|
+
* @param apiKey - The API key that incurred the cost
|
|
61
|
+
* @param cost - The cost in cents
|
|
62
|
+
*/
|
|
63
|
+
recordSpend(apiKey: string, cost: number): void;
|
|
64
|
+
/**
|
|
65
|
+
* Get current spend record for an API key
|
|
66
|
+
*/
|
|
67
|
+
getSpend(apiKey: string): SpendRecord | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Get all spend records
|
|
70
|
+
*/
|
|
71
|
+
getAllSpend(): SpendRecord[];
|
|
72
|
+
/**
|
|
73
|
+
* Reset budget for an API key (manual reset)
|
|
74
|
+
*/
|
|
75
|
+
resetBudget(apiKey: string): void;
|
|
76
|
+
/**
|
|
77
|
+
* Reset all budgets
|
|
78
|
+
*/
|
|
79
|
+
resetAll(): void;
|
|
80
|
+
/**
|
|
81
|
+
* Update monthly limit for an API key
|
|
82
|
+
*/
|
|
83
|
+
updateLimit(apiKey: string, monthlyLimit: number): void;
|
|
84
|
+
/**
|
|
85
|
+
* Check if reset is needed and perform it
|
|
86
|
+
*/
|
|
87
|
+
private checkAndReset;
|
|
88
|
+
/**
|
|
89
|
+
* Check if monthly reset is due
|
|
90
|
+
*/
|
|
91
|
+
private isResetDue;
|
|
92
|
+
/**
|
|
93
|
+
* Get next monthly reset date
|
|
94
|
+
*/
|
|
95
|
+
private getNextResetDate;
|
|
96
|
+
/**
|
|
97
|
+
* Get days until next reset
|
|
98
|
+
*/
|
|
99
|
+
getDaysUntilReset(apiKey: string): number | undefined;
|
|
100
|
+
}
|
|
101
|
+
export declare function createBudgetEnforcer(): BudgetEnforcer;
|
|
102
|
+
export declare class BudgetExceededError extends Error {
|
|
103
|
+
readonly apiKey: string;
|
|
104
|
+
readonly spent: number;
|
|
105
|
+
readonly budget: number;
|
|
106
|
+
readonly remaining: number;
|
|
107
|
+
constructor(apiKey: string, spent: number, budget: number);
|
|
108
|
+
}
|