adaptive-memory-multi-model-router 2.5.5 โ 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 +165 -24
- 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/docs/UPDATE_TOPICS.md +15 -0
- package/package.json +164 -3
- 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
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A3M Router - Semantic Cache Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import assert from 'assert';
|
|
6
|
+
import { SemanticCache, SemanticCacheConfig } from '../src/cache/semanticCache';
|
|
7
|
+
|
|
8
|
+
// ============================================================
|
|
9
|
+
// Mock Embedder for testing
|
|
10
|
+
// ============================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Simple mock embedder that produces deterministic vectors.
|
|
14
|
+
* Each unique text gets a consistent embedding.
|
|
15
|
+
*/
|
|
16
|
+
class MockEmbedder {
|
|
17
|
+
private embeddings = new Map<string, number[]>();
|
|
18
|
+
private dimension: number;
|
|
19
|
+
|
|
20
|
+
constructor(dimension = 4) {
|
|
21
|
+
this.dimension = dimension;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Seed-based random for deterministic vectors
|
|
25
|
+
private seedRandom(seed: string): () => number {
|
|
26
|
+
let hash = 0;
|
|
27
|
+
for (let i = 0; i < seed.length; i++) {
|
|
28
|
+
hash = ((hash << 5) - hash) + seed.charCodeAt(i);
|
|
29
|
+
hash = hash & hash;
|
|
30
|
+
}
|
|
31
|
+
return () => {
|
|
32
|
+
hash = (hash * 1103515245 + 12345) & 0x7fffffff;
|
|
33
|
+
return (hash / 0x7fffffff) * 2 - 1; // -1 to 1
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async embed(text: string): Promise<number[]> {
|
|
38
|
+
if (this.embeddings.has(text)) {
|
|
39
|
+
return this.embeddings.get(text)!;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const rng = this.seedRandom(text);
|
|
43
|
+
const vector: number[] = [];
|
|
44
|
+
for (let i = 0; i < this.dimension; i++) {
|
|
45
|
+
vector.push(rng());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Normalize to unit length for cosine similarity to work predictably
|
|
49
|
+
const norm = Math.sqrt(vector.reduce((sum, v) => sum + v * v, 0));
|
|
50
|
+
const normalized = vector.map(v => v / (norm || 1));
|
|
51
|
+
|
|
52
|
+
this.embeddings.set(text, normalized);
|
|
53
|
+
return normalized;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Manually set an embedding for controlled testing
|
|
57
|
+
setEmbedding(text: string, vector: number[]): void {
|
|
58
|
+
this.embeddings.set(text, vector);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ============================================================
|
|
63
|
+
// Helper: Create cache with mock embedder
|
|
64
|
+
// ============================================================
|
|
65
|
+
|
|
66
|
+
function createTestCache(
|
|
67
|
+
config: Partial<SemanticCacheConfig> = {},
|
|
68
|
+
embedder?: MockEmbedder
|
|
69
|
+
): { cache: SemanticCache; mockEmbedder: MockEmbedder } {
|
|
70
|
+
const mock = embedder || new MockEmbedder(4);
|
|
71
|
+
|
|
72
|
+
// Create a custom cache class that uses our mock embedder
|
|
73
|
+
const TestCache = class extends SemanticCache {
|
|
74
|
+
constructor(cfg: SemanticCacheConfig) {
|
|
75
|
+
super({
|
|
76
|
+
...cfg,
|
|
77
|
+
embedder: 'nomic', // will be overridden
|
|
78
|
+
});
|
|
79
|
+
// Replace the embedder with our mock
|
|
80
|
+
(this as any).embedder = mock;
|
|
81
|
+
}
|
|
82
|
+
} as any;
|
|
83
|
+
|
|
84
|
+
const cacheConfig: SemanticCacheConfig = {
|
|
85
|
+
similarityThreshold: config.similarityThreshold ?? 0.92,
|
|
86
|
+
ttlSeconds: config.ttlSeconds ?? 60,
|
|
87
|
+
maxEntries: config.maxEntries ?? 100,
|
|
88
|
+
embedder: 'nomic',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const cache = new TestCache(cacheConfig);
|
|
92
|
+
return { cache, mockEmbedder: mock };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============================================================
|
|
96
|
+
// Test Suite
|
|
97
|
+
// ============================================================
|
|
98
|
+
|
|
99
|
+
async function runTests() {
|
|
100
|
+
console.log('๐งช Running SemanticCache Tests...\n');
|
|
101
|
+
|
|
102
|
+
let passed = 0;
|
|
103
|
+
let failed = 0;
|
|
104
|
+
|
|
105
|
+
// ----------------------------------------
|
|
106
|
+
// Test 1: Basic set/get
|
|
107
|
+
// ----------------------------------------
|
|
108
|
+
try {
|
|
109
|
+
const { cache } = createTestCache({ ttlSeconds: 60 });
|
|
110
|
+
|
|
111
|
+
await cache.set('What is Python?', 'Python is a programming language.', {
|
|
112
|
+
provider: 'openai',
|
|
113
|
+
model: 'gpt-4',
|
|
114
|
+
cost: 0.01,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const result = await cache.get('What is Python?');
|
|
118
|
+
|
|
119
|
+
assert(result.hit === true, 'Should be a hit');
|
|
120
|
+
assert(result.response === 'Python is a programming language.', 'Response should match');
|
|
121
|
+
assert(result.provider === 'openai', 'Provider should match');
|
|
122
|
+
assert(result.model === 'gpt-4', 'Model should match');
|
|
123
|
+
|
|
124
|
+
console.log(' โ
Test 1: Basic set/get');
|
|
125
|
+
passed++;
|
|
126
|
+
} catch (e: any) {
|
|
127
|
+
console.log(' โ Test 1: Basic set/get -', e.message);
|
|
128
|
+
failed++;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ----------------------------------------
|
|
132
|
+
// Test 2: Similarity threshold - similar queries
|
|
133
|
+
// ----------------------------------------
|
|
134
|
+
try {
|
|
135
|
+
const mock = new MockEmbedder(8);
|
|
136
|
+
|
|
137
|
+
// Set two vectors that are similar (cosine sim ~0.95)
|
|
138
|
+
const vec1 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8];
|
|
139
|
+
const vec2 = [0.11, 0.21, 0.31, 0.41, 0.51, 0.61, 0.71, 0.81]; // slightly different
|
|
140
|
+
|
|
141
|
+
// Normalize
|
|
142
|
+
const norm1 = Math.sqrt(vec1.reduce((s, v) => s + v * v, 0));
|
|
143
|
+
const norm2 = Math.sqrt(vec2.reduce((s, v) => s + v * v, 0));
|
|
144
|
+
const n1 = vec1.map(v => v / norm1);
|
|
145
|
+
const n2 = vec2.map(v => v / norm2);
|
|
146
|
+
|
|
147
|
+
mock.setEmbedding('query A', n1);
|
|
148
|
+
mock.setEmbedding('query B', n2);
|
|
149
|
+
|
|
150
|
+
const { cache } = createTestCache({ similarityThreshold: 0.92 }, mock);
|
|
151
|
+
|
|
152
|
+
await cache.set('query A', 'Response for A', {
|
|
153
|
+
provider: 'groq',
|
|
154
|
+
model: 'llama-3.3-70b',
|
|
155
|
+
cost: 0.001,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const result = await cache.get('query B');
|
|
159
|
+
|
|
160
|
+
assert(result.hit === true, 'Should be a hit for similar query');
|
|
161
|
+
assert(result.similarity !== undefined && result.similarity > 0.99, 'Should have high similarity');
|
|
162
|
+
|
|
163
|
+
console.log(' โ
Test 2: Similarity threshold - similar queries');
|
|
164
|
+
passed++;
|
|
165
|
+
} catch (e: any) {
|
|
166
|
+
console.log(' โ Test 2: Similarity threshold -', e.message);
|
|
167
|
+
failed++;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ----------------------------------------
|
|
171
|
+
// Test 3: Similarity threshold - dissimilar queries (should miss)
|
|
172
|
+
// ----------------------------------------
|
|
173
|
+
try {
|
|
174
|
+
const mock = new MockEmbedder(8);
|
|
175
|
+
|
|
176
|
+
// Two very different vectors
|
|
177
|
+
const vec1 = [1, 0, 0, 0, 0, 0, 0, 0]; // along x-axis
|
|
178
|
+
const vec2 = [0, 1, 0, 0, 0, 0, 0, 0]; // along y-axis
|
|
179
|
+
|
|
180
|
+
mock.setEmbedding('very different A', vec1);
|
|
181
|
+
mock.setEmbedding('very different B', vec2);
|
|
182
|
+
|
|
183
|
+
const { cache } = createTestCache({ similarityThreshold: 0.92 }, mock);
|
|
184
|
+
|
|
185
|
+
await cache.set('very different A', 'Response A', {
|
|
186
|
+
provider: 'openai',
|
|
187
|
+
model: 'gpt-4',
|
|
188
|
+
cost: 0.01,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const result = await cache.get('very different B');
|
|
192
|
+
|
|
193
|
+
assert(result.hit === false, 'Should be a miss for dissimilar queries');
|
|
194
|
+
|
|
195
|
+
console.log(' โ
Test 3: Similarity threshold - dissimilar queries miss');
|
|
196
|
+
passed++;
|
|
197
|
+
} catch (e: any) {
|
|
198
|
+
console.log(' โ Test 3: Similarity threshold - dissimilar -', e.message);
|
|
199
|
+
failed++;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ----------------------------------------
|
|
203
|
+
// Test 4: TTL expiration
|
|
204
|
+
// ----------------------------------------
|
|
205
|
+
try {
|
|
206
|
+
const mock = new MockEmbedder(4);
|
|
207
|
+
const { cache } = createTestCache({ ttlSeconds: 1 }, mock); // 1 second TTL
|
|
208
|
+
|
|
209
|
+
await cache.set('Expiring query', 'This will expire', {
|
|
210
|
+
provider: 'openai',
|
|
211
|
+
model: 'gpt-4',
|
|
212
|
+
cost: 0.01,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Immediate get should work
|
|
216
|
+
let result = await cache.get('Expiring query');
|
|
217
|
+
assert(result.hit === true, 'Should be a hit before expiration');
|
|
218
|
+
|
|
219
|
+
// Wait for TTL to pass
|
|
220
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
221
|
+
|
|
222
|
+
result = await cache.get('Expiring query');
|
|
223
|
+
assert(result.hit === false, 'Should be a miss after TTL expiration');
|
|
224
|
+
|
|
225
|
+
console.log(' โ
Test 4: TTL expiration');
|
|
226
|
+
passed++;
|
|
227
|
+
} catch (e: any) {
|
|
228
|
+
console.log(' โ Test 4: TTL expiration -', e.message);
|
|
229
|
+
failed++;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ----------------------------------------
|
|
233
|
+
// Test 5: LRU eviction when maxEntries reached
|
|
234
|
+
// ----------------------------------------
|
|
235
|
+
try {
|
|
236
|
+
const mock = new MockEmbedder(4);
|
|
237
|
+
const { cache } = createTestCache({ maxEntries: 3, ttlSeconds: 60 }, mock);
|
|
238
|
+
|
|
239
|
+
// Fill up to max
|
|
240
|
+
await cache.set('Query 1', 'Response 1', { provider: 'a', model: 'm1', cost: 0.001 });
|
|
241
|
+
await cache.set('Query 2', 'Response 2', { provider: 'a', model: 'm1', cost: 0.001 });
|
|
242
|
+
await cache.set('Query 3', 'Response 3', { provider: 'a', model: 'm1', cost: 0.001 });
|
|
243
|
+
|
|
244
|
+
let stats = cache.getStats();
|
|
245
|
+
assert(stats.size === 3, `Should have 3 entries, got ${stats.size}`);
|
|
246
|
+
|
|
247
|
+
// Add one more - should evict oldest (Query 1)
|
|
248
|
+
await cache.set('Query 4', 'Response 4', { provider: 'a', model: 'm1', cost: 0.001 });
|
|
249
|
+
|
|
250
|
+
stats = cache.getStats();
|
|
251
|
+
assert(stats.size === 3, `Should still have 3 entries after eviction, got ${stats.size}`);
|
|
252
|
+
|
|
253
|
+
// Query 1 should be gone, others should remain
|
|
254
|
+
const r1 = await cache.get('Query 1');
|
|
255
|
+
const r2 = await cache.get('Query 2');
|
|
256
|
+
const r3 = await cache.get('Query 3');
|
|
257
|
+
const r4 = await cache.get('Query 4');
|
|
258
|
+
|
|
259
|
+
assert(r1.hit === false, 'Query 1 should be evicted');
|
|
260
|
+
assert(r2.hit === true, 'Query 2 should still be present');
|
|
261
|
+
assert(r3.hit === true, 'Query 3 should still be present');
|
|
262
|
+
assert(r4.hit === true, 'Query 4 should be present');
|
|
263
|
+
|
|
264
|
+
console.log(' โ
Test 5: LRU eviction when maxEntries reached');
|
|
265
|
+
passed++;
|
|
266
|
+
} catch (e: any) {
|
|
267
|
+
console.log(' โ Test 5: LRU eviction -', e.message);
|
|
268
|
+
failed++;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ----------------------------------------
|
|
272
|
+
// Test 6: Access order update (recently accessed stays)
|
|
273
|
+
// ----------------------------------------
|
|
274
|
+
try {
|
|
275
|
+
const mock = new MockEmbedder(4);
|
|
276
|
+
const { cache } = createTestCache({ maxEntries: 3, ttlSeconds: 60 }, mock);
|
|
277
|
+
|
|
278
|
+
await cache.set('Query A', 'Response A', { provider: 'a', model: 'm1', cost: 0.001 });
|
|
279
|
+
await cache.set('Query B', 'Response B', { provider: 'a', model: 'm1', cost: 0.001 });
|
|
280
|
+
await cache.set('Query C', 'Response C', { provider: 'a', model: 'm1', cost: 0.001 });
|
|
281
|
+
|
|
282
|
+
// Access Query A (makes it most recent)
|
|
283
|
+
await cache.get('Query A');
|
|
284
|
+
|
|
285
|
+
// Add new entry - should evict B (least recent)
|
|
286
|
+
await cache.set('Query D', 'Response D', { provider: 'a', model: 'm1', cost: 0.001 });
|
|
287
|
+
|
|
288
|
+
const rA = await cache.get('Query A');
|
|
289
|
+
const rB = await cache.get('Query B');
|
|
290
|
+
const rC = await cache.get('Query C');
|
|
291
|
+
const rD = await cache.get('Query D');
|
|
292
|
+
|
|
293
|
+
assert(rA.hit === true, 'Query A should still be present (was accessed recently)');
|
|
294
|
+
assert(rB.hit === false, 'Query B should be evicted (least recent)');
|
|
295
|
+
assert(rC.hit === true, 'Query C should still be present');
|
|
296
|
+
assert(rD.hit === true, 'Query D should be present');
|
|
297
|
+
|
|
298
|
+
console.log(' โ
Test 6: LRU access order update');
|
|
299
|
+
passed++;
|
|
300
|
+
} catch (e: any) {
|
|
301
|
+
console.log(' โ Test 6: LRU access order -', e.message);
|
|
302
|
+
failed++;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ----------------------------------------
|
|
306
|
+
// Test 7: Statistics tracking - hits and misses
|
|
307
|
+
// ----------------------------------------
|
|
308
|
+
try {
|
|
309
|
+
const mock = new MockEmbedder(4);
|
|
310
|
+
const { cache } = createTestCache({ ttlSeconds: 60 }, mock);
|
|
311
|
+
|
|
312
|
+
await cache.set('Stat Query 1', 'Response 1', { provider: 'a', model: 'm1', cost: 0.001 });
|
|
313
|
+
|
|
314
|
+
// Miss (query not in cache)
|
|
315
|
+
await cache.get('Non-existent query');
|
|
316
|
+
|
|
317
|
+
// Hit
|
|
318
|
+
const hitResult = await cache.get('Stat Query 1');
|
|
319
|
+
assert(hitResult.hit === true, 'Should be a hit');
|
|
320
|
+
|
|
321
|
+
// Another miss
|
|
322
|
+
await cache.get('Another non-existent');
|
|
323
|
+
|
|
324
|
+
const stats = cache.getStats();
|
|
325
|
+
|
|
326
|
+
assert(stats.hits === 1, `Should have 1 hit, got ${stats.hits}`);
|
|
327
|
+
assert(stats.misses === 2, `Should have 2 misses, got ${stats.misses}`);
|
|
328
|
+
assert(stats.hitRate === 1 / 3, `Hit rate should be ~0.333, got ${stats.hitRate}`);
|
|
329
|
+
|
|
330
|
+
console.log(' โ
Test 7: Statistics tracking');
|
|
331
|
+
passed++;
|
|
332
|
+
} catch (e: any) {
|
|
333
|
+
console.log(' โ Test 7: Statistics tracking -', e.message);
|
|
334
|
+
failed++;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ----------------------------------------
|
|
338
|
+
// Test 8: Clear all entries
|
|
339
|
+
// ----------------------------------------
|
|
340
|
+
try {
|
|
341
|
+
const mock = new MockEmbedder(4);
|
|
342
|
+
const { cache } = createTestCache({ ttlSeconds: 60 }, mock);
|
|
343
|
+
|
|
344
|
+
await cache.set('Clear 1', 'Response 1', { provider: 'a', model: 'm1', cost: 0.001 });
|
|
345
|
+
await cache.set('Clear 2', 'Response 2', { provider: 'a', model: 'm1', cost: 0.001 });
|
|
346
|
+
await cache.set('Clear 3', 'Response 3', { provider: 'a', model: 'm1', cost: 0.001 });
|
|
347
|
+
|
|
348
|
+
let stats = cache.getStats();
|
|
349
|
+
assert(stats.size === 3, `Should have 3 entries before clear`);
|
|
350
|
+
|
|
351
|
+
cache.clear();
|
|
352
|
+
|
|
353
|
+
stats = cache.getStats();
|
|
354
|
+
assert(stats.size === 0, 'Should have 0 entries after clear');
|
|
355
|
+
assert(stats.hits === 0, 'Hits should be reset');
|
|
356
|
+
assert(stats.misses === 0, 'Misses should be reset');
|
|
357
|
+
|
|
358
|
+
console.log(' โ
Test 8: Clear all entries');
|
|
359
|
+
passed++;
|
|
360
|
+
} catch (e: any) {
|
|
361
|
+
console.log(' โ Test 8: Clear all entries -', e.message);
|
|
362
|
+
failed++;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ----------------------------------------
|
|
366
|
+
// Test 9: Update existing entry
|
|
367
|
+
// ----------------------------------------
|
|
368
|
+
try {
|
|
369
|
+
const mock = new MockEmbedder(4);
|
|
370
|
+
const { cache } = createTestCache({ ttlSeconds: 60 }, mock);
|
|
371
|
+
|
|
372
|
+
await cache.set('Update Test', 'Original Response', {
|
|
373
|
+
provider: 'groq',
|
|
374
|
+
model: 'llama-3.1',
|
|
375
|
+
cost: 0.001,
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Update the same query with new response
|
|
379
|
+
await cache.set('Update Test', 'Updated Response', {
|
|
380
|
+
provider: 'openai',
|
|
381
|
+
model: 'gpt-4o',
|
|
382
|
+
cost: 0.02,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const stats = cache.getStats();
|
|
386
|
+
assert(stats.size === 1, 'Should still have only 1 entry (updated, not new)');
|
|
387
|
+
|
|
388
|
+
const result = await cache.get('Update Test');
|
|
389
|
+
assert(result.hit === true, 'Should be a hit');
|
|
390
|
+
assert(result.response === 'Updated Response', 'Should have updated response');
|
|
391
|
+
assert(result.provider === 'openai', 'Should have updated provider');
|
|
392
|
+
|
|
393
|
+
console.log(' โ
Test 9: Update existing entry');
|
|
394
|
+
passed++;
|
|
395
|
+
} catch (e: any) {
|
|
396
|
+
console.log(' โ Test 9: Update existing entry -', e.message);
|
|
397
|
+
failed++;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ----------------------------------------
|
|
401
|
+
// Test 10: Delete specific entry
|
|
402
|
+
// ----------------------------------------
|
|
403
|
+
try {
|
|
404
|
+
const mock = new MockEmbedder(4);
|
|
405
|
+
const { cache } = createTestCache({ ttlSeconds: 60 }, mock);
|
|
406
|
+
|
|
407
|
+
await cache.set('Delete Me', 'Response', { provider: 'a', model: 'm1', cost: 0.001 });
|
|
408
|
+
|
|
409
|
+
let result = await cache.get('Delete Me');
|
|
410
|
+
assert(result.hit === true, 'Should be a hit before delete');
|
|
411
|
+
|
|
412
|
+
await cache.delete('Delete Me');
|
|
413
|
+
|
|
414
|
+
result = await cache.get('Delete Me');
|
|
415
|
+
assert(result.hit === false, 'Should be a miss after delete');
|
|
416
|
+
|
|
417
|
+
console.log(' โ
Test 10: Delete specific entry');
|
|
418
|
+
passed++;
|
|
419
|
+
} catch (e: any) {
|
|
420
|
+
console.log(' โ Test 10: Delete specific entry -', e.message);
|
|
421
|
+
failed++;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ----------------------------------------
|
|
425
|
+
// Test 11: Custom TTL per entry
|
|
426
|
+
// ----------------------------------------
|
|
427
|
+
try {
|
|
428
|
+
const mock = new MockEmbedder(4);
|
|
429
|
+
const { cache } = createTestCache({ ttlSeconds: 60 }, mock); // default 60s
|
|
430
|
+
|
|
431
|
+
// Set with custom TTL of 1 second
|
|
432
|
+
await cache.set('Custom TTL', 'Expiring soon', {
|
|
433
|
+
provider: 'a',
|
|
434
|
+
model: 'm1',
|
|
435
|
+
cost: 0.001,
|
|
436
|
+
ttl: 1, // 1 second
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
let result = await cache.get('Custom TTL');
|
|
440
|
+
assert(result.hit === true, 'Should be a hit initially');
|
|
441
|
+
|
|
442
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
443
|
+
|
|
444
|
+
result = await cache.get('Custom TTL');
|
|
445
|
+
assert(result.hit === false, 'Should be a miss after custom TTL');
|
|
446
|
+
|
|
447
|
+
console.log(' โ
Test 11: Custom TTL per entry');
|
|
448
|
+
passed++;
|
|
449
|
+
} catch (e: any) {
|
|
450
|
+
console.log(' โ Test 11: Custom TTL per entry -', e.message);
|
|
451
|
+
failed++;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ----------------------------------------
|
|
455
|
+
// Test 12: Different similarity thresholds
|
|
456
|
+
// ----------------------------------------
|
|
457
|
+
try {
|
|
458
|
+
const mock = new MockEmbedder(8);
|
|
459
|
+
|
|
460
|
+
// Create two moderately similar vectors (cosine ~0.85)
|
|
461
|
+
const vec1 = [0.8, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.1];
|
|
462
|
+
const vec2 = [0.7, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.2];
|
|
463
|
+
|
|
464
|
+
const norm1 = Math.sqrt(vec1.reduce((s, v) => s + v * v, 0));
|
|
465
|
+
const norm2 = Math.sqrt(vec2.reduce((s, v) => s + v * v, 0));
|
|
466
|
+
const n1 = vec1.map(v => v / norm1);
|
|
467
|
+
const n2 = vec2.map(v => v / norm2);
|
|
468
|
+
|
|
469
|
+
mock.setEmbedding('moderate A', n1);
|
|
470
|
+
mock.setEmbedding('moderate B', n2);
|
|
471
|
+
|
|
472
|
+
// Test with high threshold (should miss)
|
|
473
|
+
const { cache: highThresholdCache } = createTestCache(
|
|
474
|
+
{ similarityThreshold: 0.95, ttlSeconds: 60 },
|
|
475
|
+
mock
|
|
476
|
+
);
|
|
477
|
+
await highThresholdCache.set('moderate A', 'Response', { provider: 'a', model: 'm1', cost: 0.001 });
|
|
478
|
+
let result = await highThresholdCache.get('moderate B');
|
|
479
|
+
assert(result.hit === false, 'Should miss with high threshold');
|
|
480
|
+
|
|
481
|
+
// Test with low threshold (should hit)
|
|
482
|
+
const { cache: lowThresholdCache } = createTestCache(
|
|
483
|
+
{ similarityThreshold: 0.80, ttlSeconds: 60 },
|
|
484
|
+
mock
|
|
485
|
+
);
|
|
486
|
+
await lowThresholdCache.set('moderate A', 'Response', { provider: 'a', model: 'm1', cost: 0.001 });
|
|
487
|
+
result = await lowThresholdCache.get('moderate B');
|
|
488
|
+
assert(result.hit === true, 'Should hit with low threshold');
|
|
489
|
+
|
|
490
|
+
console.log(' โ
Test 12: Different similarity thresholds');
|
|
491
|
+
passed++;
|
|
492
|
+
} catch (e: any) {
|
|
493
|
+
console.log(' โ Test 12: Different similarity thresholds -', e.message);
|
|
494
|
+
failed++;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ----------------------------------------
|
|
498
|
+
// Summary
|
|
499
|
+
// ----------------------------------------
|
|
500
|
+
console.log(`\n๐ Results: ${passed} passed, ${failed} failed\n`);
|
|
501
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
runTests().catch(e => {
|
|
505
|
+
console.error('Test runner error:', e);
|
|
506
|
+
process.exit(1);
|
|
507
|
+
});
|