adaptive-memory-multi-model-router 2.2.5 → 2.2.7
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 -116
- package/README.md.bak +836 -0
- package/assets/benchmark-results.png +0 -0
- package/assets/complexity-scoring-v2.png +0 -0
- package/assets/complexity-scoring.png +0 -0
- package/assets/cost-comparison-chart.png +0 -0
- package/assets/cost-comparison-v2.png +0 -0
- package/assets/feature-comparison-v2.png +0 -0
- package/assets/feature-comparison-v3.png +0 -0
- package/assets/provider-health-chart.png +0 -0
- package/assets/provider-health-v2.png +0 -0
- package/assets/routing-flow-v2.png +0 -0
- package/assets/routing-flow-v3.png +0 -0
- package/assets/routing-flow.png +0 -0
- package/assets/tier-distribution.png +0 -0
- package/benchmark-results.json +620 -46
- package/dist/cache/cacheKeyGenerator.d.ts +67 -0
- package/dist/cache/cacheKeyGenerator.d.ts.map +1 -0
- package/dist/cache/cacheKeyGenerator.js +211 -0
- package/dist/cache/cacheKeyGenerator.js.map +1 -0
- package/dist/cli.js +0 -0
- package/dist/cost/preCallCostEstimator.d.ts +114 -0
- package/dist/cost/preCallCostEstimator.d.ts.map +1 -0
- package/dist/cost/preCallCostEstimator.js +256 -0
- package/dist/cost/preCallCostEstimator.js.map +1 -0
- package/dist/inference/speculativeDecoding.d.ts +133 -0
- package/dist/inference/speculativeDecoding.d.ts.map +1 -0
- package/dist/inference/speculativeDecoding.js +276 -0
- package/dist/inference/speculativeDecoding.js.map +1 -0
- package/dist/providers/providerHealth.d.ts +117 -0
- package/dist/providers/providerHealth.d.ts.map +1 -0
- package/dist/providers/providerHealth.js +309 -0
- package/dist/providers/providerHealth.js.map +1 -0
- package/dist/routing/difficultyClassifier.d.ts +79 -0
- package/dist/routing/difficultyClassifier.d.ts.map +1 -0
- package/dist/routing/difficultyClassifier.js +329 -0
- package/dist/routing/difficultyClassifier.js.map +1 -0
- package/dist/sdk.d.ts +125 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js.map +1 -0
- package/package.json +2 -322
- package/scripts/run-mmlu-benchmark.js +176 -0
- package/scripts/run-provider-benchmark.js +244 -0
- package/src/cache/cacheKeyGenerator.ts +242 -0
- package/src/cost/preCallCostEstimator.ts +345 -0
- package/src/inference/speculativeDecoding.ts +373 -0
- package/src/providers/providerHealth.ts +397 -0
- package/src/routing/difficultyClassifier.ts +420 -0
- package/test/provider-test.js +69 -90
- package/test.js +43 -69
- package/test.js.bak +376 -0
- package/src/skills/__tests__/skill_manager.test.ts +0 -328
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* A3M Router — Multi-Provider Benchmark
|
|
4
|
+
* Tests Groq, Cerebras, OpenCode free endpoints
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const https = require('https');
|
|
8
|
+
const http = require('http');
|
|
9
|
+
|
|
10
|
+
const QUESTIONS = [
|
|
11
|
+
{ id:1, prompt:"What is 2+2?" },
|
|
12
|
+
{ id:2, prompt:"Write a Python function to check prime" },
|
|
13
|
+
{ id:3, prompt:"Explain what an API is in 1 sentence" },
|
|
14
|
+
{ id:4, prompt:"What causes climate change?" },
|
|
15
|
+
{ id:5, prompt:"Write a haiku about programming" },
|
|
16
|
+
{ id:6, prompt:"Summarize: AI models improve with data" },
|
|
17
|
+
{ id:7, prompt:"List 3 programming languages" },
|
|
18
|
+
{ id:8, prompt:"What is machine learning?" },
|
|
19
|
+
{ id:9, prompt:"Code: reverse a string in Python" },
|
|
20
|
+
{ id:10, prompt:"What is the capital of Japan?" },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const PROVIDERS = {
|
|
24
|
+
// Groq (free)
|
|
25
|
+
'groq-llama-3.3-70b': {
|
|
26
|
+
name: 'Groq Llama 3.3 70B',
|
|
27
|
+
endpoint: 'https://api.groq.com/openai/v1/chat/completions',
|
|
28
|
+
model: 'llama-3.3-70b-versatile',
|
|
29
|
+
apiKeyEnv: 'GROQ_API_KEY',
|
|
30
|
+
inputCostPer1M: 0,
|
|
31
|
+
},
|
|
32
|
+
'groq-llama-3.1-8b': {
|
|
33
|
+
name: 'Groq Llama 3.1 8B',
|
|
34
|
+
endpoint: 'https://api.groq.com/openai/v1/chat/completions',
|
|
35
|
+
model: 'llama-3.1-8b-instant',
|
|
36
|
+
apiKeyEnv: 'GROQ_API_KEY',
|
|
37
|
+
inputCostPer1M: 0,
|
|
38
|
+
},
|
|
39
|
+
'groq-qwen-3-32b': {
|
|
40
|
+
name: 'Groq Qwen 3 32B',
|
|
41
|
+
endpoint: 'https://api.groq.com/openai/v1/chat/completions',
|
|
42
|
+
model: 'qwen/qwen3-32b',
|
|
43
|
+
apiKeyEnv: 'GROQ_API_KEY',
|
|
44
|
+
inputCostPer1M: 0,
|
|
45
|
+
},
|
|
46
|
+
'groq-allam-2-7b': {
|
|
47
|
+
name: 'Groq Allam 2 7B',
|
|
48
|
+
endpoint: 'https://api.groq.com/openai/v1/chat/completions',
|
|
49
|
+
model: 'allam-2-7b',
|
|
50
|
+
apiKeyEnv: 'GROQ_API_KEY',
|
|
51
|
+
inputCostPer1M: 0,
|
|
52
|
+
},
|
|
53
|
+
'groq-compound-mini': {
|
|
54
|
+
name: 'Groq Compound Mini',
|
|
55
|
+
endpoint: 'https://api.groq.com/openai/v1/chat/completions',
|
|
56
|
+
model: 'groq/compound-mini',
|
|
57
|
+
apiKeyEnv: 'GROQ_API_KEY',
|
|
58
|
+
inputCostPer1M: 0,
|
|
59
|
+
},
|
|
60
|
+
// Cerebras (free)
|
|
61
|
+
'cerebras-llama3.1-8b': {
|
|
62
|
+
name: 'Cerebras Llama 3.1 8B',
|
|
63
|
+
endpoint: 'https://api.cerebras.ai/v1/chat/completions',
|
|
64
|
+
model: 'llama3.1-8b',
|
|
65
|
+
apiKeyEnv: 'CEREBRAS_API_KEY',
|
|
66
|
+
inputCostPer1M: 0,
|
|
67
|
+
},
|
|
68
|
+
'cerebras-qwen-3-235b': {
|
|
69
|
+
name: 'Cerebras Qwen 3 235B',
|
|
70
|
+
endpoint: 'https://api.cerebras.ai/v1/chat/completions',
|
|
71
|
+
model: 'qwen-3-235b-a22b-instruct-2507',
|
|
72
|
+
apiKeyEnv: 'CEREBRAS_API_KEY',
|
|
73
|
+
inputCostPer1M: 0,
|
|
74
|
+
},
|
|
75
|
+
// OpenCode (via API server running on 18787)
|
|
76
|
+
'opencode-deepseek-v4': {
|
|
77
|
+
name: 'OpenCode DeepSeek V4 Flash',
|
|
78
|
+
endpoint: 'http://127.0.0.1:18787/v1/chat/completions',
|
|
79
|
+
model: 'opencode/deepseek-v4-flash-free',
|
|
80
|
+
apiKeyEnv: null,
|
|
81
|
+
local: true,
|
|
82
|
+
inputCostPer1M: 0,
|
|
83
|
+
},
|
|
84
|
+
'opencode-minimax-m2.5': {
|
|
85
|
+
name: 'OpenCode MiniMax M2.5',
|
|
86
|
+
endpoint: 'http://127.0.0.1:18787/v1/chat/completions',
|
|
87
|
+
model: 'minimax/MiniMax-M2.5',
|
|
88
|
+
apiKeyEnv: null,
|
|
89
|
+
local: true,
|
|
90
|
+
inputCostPer1M: 0,
|
|
91
|
+
},
|
|
92
|
+
'opencode-nemotron': {
|
|
93
|
+
name: 'OpenCode Nemotron Super',
|
|
94
|
+
endpoint: 'http://127.0.0.1:18787/v1/chat/completions',
|
|
95
|
+
model: 'opencode/nemotron-3-super-free',
|
|
96
|
+
apiKeyEnv: null,
|
|
97
|
+
local: true,
|
|
98
|
+
inputCostPer1M: 0,
|
|
99
|
+
},
|
|
100
|
+
'opencode-qwen3.6-plus': {
|
|
101
|
+
name: 'OpenCode Qwen 3.6 Plus',
|
|
102
|
+
endpoint: 'http://127.0.0.1:18787/v1/chat/completions',
|
|
103
|
+
model: 'opencode/qwen3.6-plus-free',
|
|
104
|
+
apiKeyEnv: null,
|
|
105
|
+
local: true,
|
|
106
|
+
inputCostPer1M: 0,
|
|
107
|
+
},
|
|
108
|
+
// Groq via OpenCode (same provider, different path)
|
|
109
|
+
'opencode-groq-llama-3.3-70b': {
|
|
110
|
+
name: 'OpenCode Groq Llama 3.3 70B',
|
|
111
|
+
endpoint: 'http://127.0.0.1:18787/v1/chat/completions',
|
|
112
|
+
model: 'groq/llama-3.3-70b-versatile',
|
|
113
|
+
apiKeyEnv: null,
|
|
114
|
+
local: true,
|
|
115
|
+
inputCostPer1M: 0,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
function apiCall(provider, body) {
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
const config = PROVIDERS[provider];
|
|
122
|
+
const startTime = Date.now();
|
|
123
|
+
const bodyStr = JSON.stringify(body);
|
|
124
|
+
const url = new URL(config.endpoint);
|
|
125
|
+
const isHttp = url.protocol === 'http:';
|
|
126
|
+
|
|
127
|
+
const headers = {
|
|
128
|
+
'Content-Type': 'application/json',
|
|
129
|
+
'Content-Length': Buffer.byteLength(bodyStr),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
if (config.apiKeyEnv) {
|
|
133
|
+
const key = process.env[config.apiKeyEnv];
|
|
134
|
+
if (key) headers['Authorization'] = `Bearer ${key}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const opts = {
|
|
138
|
+
hostname: url.hostname,
|
|
139
|
+
port: url.port || (isHttp ? 80 : 443),
|
|
140
|
+
path: url.pathname,
|
|
141
|
+
method: 'POST',
|
|
142
|
+
headers,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const req = (isHttp ? http : https).request(opts, (res) => {
|
|
146
|
+
let data = '';
|
|
147
|
+
res.on('data', c => data += c);
|
|
148
|
+
res.on('end', () => {
|
|
149
|
+
const latency = Date.now() - startTime;
|
|
150
|
+
try {
|
|
151
|
+
const json = JSON.parse(data);
|
|
152
|
+
const content = json.choices?.[0]?.message?.content || '';
|
|
153
|
+
resolve({ success: true, latency, content, status: res.statusCode });
|
|
154
|
+
} catch {
|
|
155
|
+
resolve({ success: false, latency, error: data.slice(0, 100), status: res.statusCode });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
req.on('error', e => resolve({ success: false, latency: 0, error: e.message }));
|
|
161
|
+
req.setTimeout(60000, () => { req.destroy(); resolve({ success: false, latency: 60000, error: 'Timeout' }); });
|
|
162
|
+
req.write(bodyStr);
|
|
163
|
+
req.end();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function runBench(providerIds) {
|
|
168
|
+
console.log('\n🧪 A3M Router Multi-Provider Benchmark\n');
|
|
169
|
+
|
|
170
|
+
const results = {};
|
|
171
|
+
|
|
172
|
+
for (const pid of providerIds) {
|
|
173
|
+
const config = PROVIDERS[pid];
|
|
174
|
+
if (!config) continue;
|
|
175
|
+
|
|
176
|
+
// Check API key
|
|
177
|
+
if (config.apiKeyEnv && (!process.env[config.apiKeyEnv] || process.env[config.apiKeyEnv].length < 20)) {
|
|
178
|
+
console.log(`⏭️ ${config.name}: No API key (${config.apiKeyEnv})`);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log(`\n📡 ${config.name}...`);
|
|
183
|
+
const qResults = [];
|
|
184
|
+
|
|
185
|
+
for (const q of QUESTIONS) {
|
|
186
|
+
process.stdout.write(` Q${q.id}...`);
|
|
187
|
+
const result = await apiCall(pid, {
|
|
188
|
+
model: config.model,
|
|
189
|
+
messages: [{ role: 'user', content: q.prompt }],
|
|
190
|
+
max_tokens: 100,
|
|
191
|
+
});
|
|
192
|
+
qResults.push({ id: q.id, ...result });
|
|
193
|
+
process.stdout.write(` ${result.latency}ms ${result.success ? '✅' : '❌'}\n`);
|
|
194
|
+
await new Promise(r => setTimeout(r, 300));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const success = qResults.filter(r => r.success).length;
|
|
198
|
+
const avgLat = qResults.reduce((s, r) => s + r.latency, 0) / qResults.length;
|
|
199
|
+
const avgLen = qResults.filter(r => r.success).reduce((s, r) => s + (r.content?.length || 0), 0) / Math.max(success, 1);
|
|
200
|
+
|
|
201
|
+
results[pid] = {
|
|
202
|
+
name: config.name,
|
|
203
|
+
successRate: success / QUESTIONS.length,
|
|
204
|
+
avgLatency: Math.round(avgLat),
|
|
205
|
+
avgOutputLen: Math.round(avgLen),
|
|
206
|
+
inputCostPer1M: config.inputCostPer1M,
|
|
207
|
+
questions: qResults,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
console.log(` → ${success}/${QUESTIONS.length}, avg ${avgLat}ms, ~${avgLen} chars output\n`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Summary table
|
|
214
|
+
console.log('\n📊 Results Summary\n');
|
|
215
|
+
console.log('Provider | Success | Avg Latency | $/1M');
|
|
216
|
+
console.log('--------------------------|---------|-------------|--------');
|
|
217
|
+
|
|
218
|
+
const sorted = Object.values(results).sort((a, b) => a.avgLatency - b.avgLatency);
|
|
219
|
+
for (const r of sorted) {
|
|
220
|
+
const sr = `${(r.successRate * 100).toFixed(0)}%`;
|
|
221
|
+
console.log(`${r.name.padEnd(25)}| ${sr.padStart(7)} | ${String(r.avgLatency).padStart(8)}ms | $${r.inputCostPer1M}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
require('fs').writeFileSync('benchmark-provider-results.json', JSON.stringify({
|
|
225
|
+
meta: { date: new Date().toISOString(), questions: QUESTIONS.length },
|
|
226
|
+
results,
|
|
227
|
+
}, null, 2));
|
|
228
|
+
|
|
229
|
+
console.log('\n💾 Saved to benchmark-provider-results.json');
|
|
230
|
+
return results;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const args = process.argv.slice(2);
|
|
234
|
+
const all = args.includes('--all');
|
|
235
|
+
const pids = all ? Object.keys(PROVIDERS) : (args.filter(a => PROVIDERS[a]) || []);
|
|
236
|
+
|
|
237
|
+
if (pids.length === 0) {
|
|
238
|
+
console.log('Usage: node run-provider-benchmark.js [--all] [pid1] [pid2] ...');
|
|
239
|
+
console.log('\nAvailable providers:');
|
|
240
|
+
Object.entries(PROVIDERS).forEach(([id, c]) => console.log(` ${id}: ${c.name}`));
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
runBench(pids).catch(console.error);
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A3M Router - Cross-Provider Cache Key Generator
|
|
3
|
+
*
|
|
4
|
+
* Normalizes prompts so same semantic content maps to same cache key
|
|
5
|
+
* regardless of provider-specific formatting, system prompts, etc.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const cacheKey = generateCacheKey("What is Python?", { provider: "openai" });
|
|
9
|
+
* const key2 = generateCacheKey("What is Python?", { provider: "anthropic" });
|
|
10
|
+
* // key === key2 (same semantic content = same key)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as crypto from 'crypto';
|
|
14
|
+
|
|
15
|
+
// ============================================================
|
|
16
|
+
// Types
|
|
17
|
+
// ============================================================
|
|
18
|
+
|
|
19
|
+
export interface CacheKeyOptions {
|
|
20
|
+
/** Target provider (affects normalization rules) */
|
|
21
|
+
provider?: string;
|
|
22
|
+
/** Target model (for model-specific normalization) */
|
|
23
|
+
model?: string;
|
|
24
|
+
/** Whether to include system prompt in normalization */
|
|
25
|
+
includeSystemPrompt?: boolean;
|
|
26
|
+
/** Custom normalization rules */
|
|
27
|
+
customRules?: NormalizationRule[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface NormalizationRule {
|
|
31
|
+
pattern: RegExp;
|
|
32
|
+
replacement: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CacheKeyResult {
|
|
36
|
+
/** The normalized cache key string */
|
|
37
|
+
key: string;
|
|
38
|
+
/** Hash of the normalized content */
|
|
39
|
+
hash: string;
|
|
40
|
+
/** Metadata about what was normalized */
|
|
41
|
+
metadata: {
|
|
42
|
+
originalLength: number;
|
|
43
|
+
normalizedLength: number;
|
|
44
|
+
rulesApplied: number;
|
|
45
|
+
provider?: string;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================================
|
|
50
|
+
// Provider-specific system prompt patterns
|
|
51
|
+
// ============================================================
|
|
52
|
+
|
|
53
|
+
const PROVIDER_SYSTEM_PATTERNS: Record<string, RegExp[]> = {
|
|
54
|
+
anthropic: [
|
|
55
|
+
/<anthropic_thinking>[\s\S]*?<\/anthropic_thinking>/gi,
|
|
56
|
+
/<thinking>[\s\S]*?<\/thinking>/gi,
|
|
57
|
+
/Human:/gi,
|
|
58
|
+
/Assistant:/gi,
|
|
59
|
+
],
|
|
60
|
+
openai: [
|
|
61
|
+
/<|im_start|>/gi,
|
|
62
|
+
/<|im_end|>/gi,
|
|
63
|
+
],
|
|
64
|
+
google: [
|
|
65
|
+
/<content>[\s\S]*?<\/content>/gi,
|
|
66
|
+
/[\Parts|thought]/gi,
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// ============================================================
|
|
71
|
+
// Core normalizer
|
|
72
|
+
// ============================================================
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Normalize text for cross-provider cache key generation.
|
|
76
|
+
* Removes provider-specific formatting while preserving semantic content.
|
|
77
|
+
*/
|
|
78
|
+
export function normalizeForCacheKey(
|
|
79
|
+
text: string,
|
|
80
|
+
options: CacheKeyOptions = {}
|
|
81
|
+
): string {
|
|
82
|
+
let normalized = text;
|
|
83
|
+
|
|
84
|
+
// Step 1: Unicode normalization (NFC)
|
|
85
|
+
normalized = normalized.normalize('NFC');
|
|
86
|
+
|
|
87
|
+
// Step 2: Collapse whitespace
|
|
88
|
+
normalized = normalized.replace(/\s+/g, ' ');
|
|
89
|
+
|
|
90
|
+
// Step 3: Remove control characters
|
|
91
|
+
normalized = normalized.replace(/[\x00-\x1F\x7F]/g, '');
|
|
92
|
+
|
|
93
|
+
// Step 4: Strip provider-specific formatting
|
|
94
|
+
if (options.provider) {
|
|
95
|
+
const patterns = PROVIDER_SYSTEM_PATTERNS[options.provider] || [];
|
|
96
|
+
for (const pattern of patterns) {
|
|
97
|
+
normalized = normalized.replace(pattern, '');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Step 5: General system/assistant role removal
|
|
102
|
+
normalized = normalized
|
|
103
|
+
.replace(/\b(system|user|assistant|human|bot)\s*:/gi, '')
|
|
104
|
+
.replace(/^(system|user|assistant|human|bot)\s*/gim, '');
|
|
105
|
+
|
|
106
|
+
// Step 6: Remove markdown formatting (often provider-specific)
|
|
107
|
+
normalized = normalized
|
|
108
|
+
.replace(/```[\s\S]*?```/g, '[CODE_BLOCK]') // Preserve code block indicator
|
|
109
|
+
.replace(/`([^`]+)`/g, '$1') // Inline code content
|
|
110
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1') // Bold
|
|
111
|
+
.replace(/_([^_]+)_/g, '$1') // Italic
|
|
112
|
+
.replace(/#+\s*/g, '') // Headers
|
|
113
|
+
.replace(/^\s*[-*+]\s+/gm, '') // List bullets
|
|
114
|
+
.replace(/^\s*\d+\.\s+/gm, ''); // Numbered lists
|
|
115
|
+
|
|
116
|
+
// Step 7: Apply custom rules
|
|
117
|
+
if (options.customRules) {
|
|
118
|
+
for (const rule of options.customRules) {
|
|
119
|
+
normalized = normalized.replace(rule.pattern, rule.replacement);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Step 8: Collapse whitespace again after removals
|
|
124
|
+
normalized = normalized.replace(/\s+/g, ' ').trim();
|
|
125
|
+
|
|
126
|
+
return normalized;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Generate a deterministic cache key from a query.
|
|
131
|
+
* Same semantic content = same key across providers.
|
|
132
|
+
*/
|
|
133
|
+
export function generateCacheKey(
|
|
134
|
+
query: string,
|
|
135
|
+
options: CacheKeyOptions = {}
|
|
136
|
+
): CacheKeyResult {
|
|
137
|
+
const originalLength = query.length;
|
|
138
|
+
|
|
139
|
+
// Normalize the query
|
|
140
|
+
let normalized = normalizeForCacheKey(query, {
|
|
141
|
+
...options,
|
|
142
|
+
includeSystemPrompt: false, // Always exclude for user query matching
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Count rules that were applied (approximate)
|
|
146
|
+
let rulesApplied = 3; // Base normalizations
|
|
147
|
+
if (options.provider) rulesApplied += 2;
|
|
148
|
+
if (options.customRules) rulesApplied += options.customRules.length;
|
|
149
|
+
|
|
150
|
+
// Generate hash
|
|
151
|
+
const hash = crypto
|
|
152
|
+
.createHash('sha256')
|
|
153
|
+
.update(normalized)
|
|
154
|
+
.digest('hex')
|
|
155
|
+
.substring(0, 16); // First 16 chars = 64-bit key
|
|
156
|
+
|
|
157
|
+
// Final key format: v1:{hash}:{provider?[:model]?}
|
|
158
|
+
let key = `v1:${hash}`;
|
|
159
|
+
if (options.provider) {
|
|
160
|
+
key += `:${options.provider}`;
|
|
161
|
+
if (options.model) {
|
|
162
|
+
key += `:${options.model}`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
key,
|
|
168
|
+
hash,
|
|
169
|
+
metadata: {
|
|
170
|
+
originalLength,
|
|
171
|
+
normalizedLength: normalized.length,
|
|
172
|
+
rulesApplied,
|
|
173
|
+
provider: options.provider,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ============================================================
|
|
179
|
+
// SemanticCache enhancement
|
|
180
|
+
// ============================================================
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Add cross-provider cache key methods to existing SemanticCache.
|
|
184
|
+
* Call this to enhance the cache with provider-normalized lookups.
|
|
185
|
+
*/
|
|
186
|
+
export function createCacheKeyGenerator(
|
|
187
|
+
defaultOptions?: CacheKeyOptions
|
|
188
|
+
): {
|
|
189
|
+
generateKey: (query: string, options?: CacheKeyOptions) => CacheKeyResult;
|
|
190
|
+
createNormalizedMatcher: (cache: Map<string, any>) => (query: string, options?: CacheKeyOptions) => string | null;
|
|
191
|
+
} {
|
|
192
|
+
return {
|
|
193
|
+
/**
|
|
194
|
+
* Generate a cache key for a query.
|
|
195
|
+
*/
|
|
196
|
+
generateKey: (query: string, options?: CacheKeyOptions): CacheKeyResult => {
|
|
197
|
+
return generateCacheKey(query, { ...defaultOptions, ...options });
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Create a matcher function that finds existing cache entries
|
|
202
|
+
* by comparing normalized keys.
|
|
203
|
+
*/
|
|
204
|
+
createNormalizedMatcher: (cache: Map<string, any>) => {
|
|
205
|
+
return (query: string, options?: CacheKeyOptions): string | null => {
|
|
206
|
+
const { key } = generateCacheKey(query, { ...defaultOptions, ...options });
|
|
207
|
+
|
|
208
|
+
// Check exact match
|
|
209
|
+
if (cache.has(key)) {
|
|
210
|
+
return key;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check hash-only match (v1:{hash} prefix)
|
|
214
|
+
const hashPrefix = key.split(':').slice(0, 2).join(':');
|
|
215
|
+
for (const cachedKey of cache.keys()) {
|
|
216
|
+
if (cachedKey.startsWith(hashPrefix + ':')) {
|
|
217
|
+
return cachedKey;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return null;
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ============================================================
|
|
228
|
+
// Convenience exports
|
|
229
|
+
// ============================================================
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Quick cache key generation (simplified API).
|
|
233
|
+
* Use this for simple cross-provider cache lookups.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* const key1 = toCacheKey("What is Python?", "openai");
|
|
237
|
+
* const key2 = toCacheKey("What is Python?", "anthropic");
|
|
238
|
+
* console.log(key1 === key2); // true
|
|
239
|
+
*/
|
|
240
|
+
export function toCacheKey(query: string, provider?: string): string {
|
|
241
|
+
return generateCacheKey(query, { provider }).key;
|
|
242
|
+
}
|