adaptive-memory-multi-model-router 2.2.6 → 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.
Files changed (48) hide show
  1. package/README.md +132 -94
  2. package/assets/benchmark-results.png +0 -0
  3. package/assets/complexity-scoring-v2.png +0 -0
  4. package/assets/complexity-scoring.png +0 -0
  5. package/assets/cost-comparison-chart.png +0 -0
  6. package/assets/cost-comparison-v2.png +0 -0
  7. package/assets/feature-comparison-v2.png +0 -0
  8. package/assets/feature-comparison-v3.png +0 -0
  9. package/assets/provider-health-chart.png +0 -0
  10. package/assets/provider-health-v2.png +0 -0
  11. package/assets/routing-flow-v2.png +0 -0
  12. package/assets/routing-flow-v3.png +0 -0
  13. package/assets/routing-flow.png +0 -0
  14. package/assets/tier-distribution.png +0 -0
  15. package/benchmark-results.json +620 -46
  16. package/dist/analytics/costAnalytics.d.ts +0 -1
  17. package/dist/cache/semanticCache.d.ts +0 -41
  18. package/dist/cache/semanticCache.d.ts.map +1 -1
  19. package/dist/cache/semanticCache.js +0 -142
  20. package/dist/cache/semanticCache.js.map +1 -1
  21. package/dist/cli.js +478 -35
  22. package/dist/cost/costTracker.js +3 -0
  23. package/dist/index.d.ts +0 -16
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +64 -264
  26. package/dist/index.js.map +1 -1
  27. package/dist/integrations/langchainAdapter.d.ts +0 -1
  28. package/dist/integrations/oauth.d.ts +0 -1
  29. package/dist/memory/autoFetch.d.ts +0 -1
  30. package/dist/memory/memoryTree.d.ts +0 -1
  31. package/dist/memory/obsidianVault.d.ts +0 -1
  32. package/dist/providers/providerConfig.d.ts +0 -1
  33. package/dist/providers/providerConfig.js +0 -2
  34. package/dist/providers/registry.js +128 -126
  35. package/dist/routing/advancedRouter.js +427 -310
  36. package/dist/sdk.js +100 -109
  37. package/dist/security/guardrails.d.ts +0 -1
  38. package/dist/server/dashboard.d.ts +0 -1
  39. package/dist/server/modelMapper.d.ts +0 -1
  40. package/dist/server/proxyServer.d.ts +0 -1
  41. package/package.json +3 -325
  42. package/scripts/run-mmlu-benchmark.js +176 -0
  43. package/scripts/run-provider-benchmark.js +244 -0
  44. package/src/cache/semanticCache.ts +0 -148
  45. package/src/index.ts +99 -0
  46. package/test/provider-test.js +70 -91
  47. package/test.js +41 -67
  48. package/tsconfig.json +5 -15
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * A3M Router — MMLU Benchmark
4
+ * MMLU: 15 multiple choice questions (actual MMLU methodology)
5
+ * MT-Bench proxy: measures response quality via completeness scoring
6
+ */
7
+
8
+ const https = require('https');
9
+
10
+ const MMLU_QUESTIONS = [
11
+ { q: "What is the derivative of x²?", choices: ["x", "2x", "2", "x²"], answer: 1, subject: "math" },
12
+ { q: "Solve for x: 2x + 6 = 14", choices: ["4", "3", "5", "10"], answer: 0, subject: "math" },
13
+ { q: "Time complexity of binary search?", choices: ["O(n)", "O(log n)", "O(n²)", "O(1)"], answer: 1, subject: "cs" },
14
+ { q: "In Python, list.append() does what?", choices: ["Adds to start", "Adds to end", "Removes last", "Sorts list"], answer: 1, subject: "cs" },
15
+ { q: "Output of type([])?", choices: ["<class 'array'>", "<class 'list'>", "<class 'tuple'>", "<class 'dict'>"], answer: 1, subject: "cs" },
16
+ { q: "GDP stands for?", choices: ["Gross Domestic Product", "General Domestic Price", "Government Debt Payment", "Global Demand Protocol"], answer: 0, subject: "economics" },
17
+ { q: "If supply increases and demand stays same, price:", choices: ["Increases", "Decreases", "Stays same", "Doubles"], answer: 1, subject: "economics" },
18
+ { q: "Powerhouse of the cell?", choices: ["Nucleus", "Ribosome", "Mitochondria", "Golgi body"], answer: 2, subject: "biology" },
19
+ { q: "DNA stands for?", choices: ["Deoxyribonucleic Acid", "Dinitrogen Acid", "Dynamic Nuclear Acid", "Dual Nucleotide"], answer: 0, subject: "biology" },
20
+ { q: "Primary purpose of a contract?", choices: ["Entertainment", "Create legal obligations", "Tax avoidance", "Social bonding"], answer: 1, subject: "law" },
21
+ { q: "Which US amendment protects free speech?", choices: ["1st", "2nd", "4th", "5th"], answer: 0, subject: "law" },
22
+ { q: "Car accelerates from rest at 2 m/s². Velocity after 5s?", choices: ["5 m/s", "10 m/s", "7 m/s", "2.5 m/s"], answer: 1, subject: "physics" },
23
+ { q: "Unit of force in SI?", choices: ["Joule", "Watt", "Newton", "Pascal"], answer: 2, subject: "physics" },
24
+ { q: "If gas temp increases at constant volume, pressure:", choices: ["Decreases", "Increases", "Stays constant", "Becomes zero"], answer: 1, subject: "physics" },
25
+ { q: "What is π to two decimal places?", choices: ["3.14", "3.16", "3.12", "3.18"], answer: 0, subject: "math" },
26
+ ];
27
+
28
+ const QUALITY_QUESTIONS = [
29
+ { id: 1, prompt: "If John has 5 apples and gives 3 to Mary, how many does he have left?", expected: "2" },
30
+ { id: 2, prompt: "Write a Python function that checks if a string is a palindrome.", expected: "def" },
31
+ { id: 3, prompt: "Compare supervised vs unsupervised learning in 2 sentences.", expected: "supervised" },
32
+ { id: 4, prompt: "What is the capital of France?", expected: "Paris" },
33
+ { id: 5, prompt: "Train travels 300km in 4 hours. Average speed?", expected: "75" },
34
+ { id: 6, prompt: "All cats are animals. Some animals are black. Are some cats black?", expected: "maybe/not definite" },
35
+ { id: 7, prompt: "What is machine learning?", expected: "learn" },
36
+ { id: 8, prompt: "Write a haiku about programming.", expected: "\n" },
37
+ ];
38
+
39
+ const PROVIDERS = {
40
+ 'groq-llama-3.1-8b': { name: 'Groq Llama 3.1 8B', endpoint: 'https://api.groq.com/openai/v1/chat/completions', model: 'llama-3.1-8b-instant', apiKeyEnv: 'GROQ_API_KEY' },
41
+ 'groq-llama-3.3-70b': { name: 'Groq Llama 3.3 70B', endpoint: 'https://api.groq.com/openai/v1/chat/completions', model: 'llama-3.3-70b-versatile', apiKeyEnv: 'GROQ_API_KEY' },
42
+ 'groq-allam-2-7b': { name: 'Groq Allam 2 7B', endpoint: 'https://api.groq.com/openai/v1/chat/completions', model: 'allam-2-7b', apiKeyEnv: 'GROQ_API_KEY' },
43
+ 'cerebras-llama3.1-8b': { name: 'Cerebras Llama 3.1 8B', endpoint: 'https://api.cerebras.ai/v1/chat/completions', model: 'llama3.1-8b', apiKeyEnv: 'CEREBRAS_API_KEY' },
44
+ 'cerebras-qwen-3-235b': { name: 'Cerebras Qwen 3 235B', endpoint: 'https://api.cerebras.ai/v1/chat/completions', model: 'qwen-3-235b-a22b-instruct-2507', apiKeyEnv: 'CEREBRAS_API_KEY' },
45
+ };
46
+
47
+ function callAPI(provider, prompt, maxTokens = 60) {
48
+ return new Promise((resolve) => {
49
+ const config = PROVIDERS[provider];
50
+ const start = Date.now();
51
+ const body = JSON.stringify({ model: config.model, messages: [{ role: 'user', content: prompt }], max_tokens: maxTokens, temperature: 0 });
52
+ const url = new URL(config.endpoint);
53
+
54
+ const req = https.request({
55
+ hostname: url.hostname, path: url.pathname, method: 'POST',
56
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env[config.apiKeyEnv]}`, 'Content-Length': Buffer.byteLength(body) }
57
+ }, (res) => {
58
+ let d = '';
59
+ res.on('data', c => d += c);
60
+ res.on('end', () => {
61
+ try {
62
+ const json = JSON.parse(d);
63
+ resolve({ success: true, latency: Date.now() - start, answer: json.choices?.[0]?.message?.content || '', status: res.statusCode });
64
+ } catch { resolve({ success: false, latency: Date.now() - start, error: d.slice(0, 80) }); }
65
+ });
66
+ });
67
+ req.on('error', e => resolve({ success: false, latency: 0, error: e.message }));
68
+ req.setTimeout(30000, () => { req.destroy(); resolve({ success: false, latency: 30000, error: 'Timeout' }); });
69
+ req.write(body); req.end();
70
+ });
71
+ }
72
+
73
+ function gradeMMLU(question, response) {
74
+ if (!response) return 0;
75
+ const text = response.toLowerCase().trim();
76
+
77
+ // Look for letter A/B/C/D
78
+ const letterMatch = text.match(/^(?:the answer is\s+)?([a-d])(?:\s|$)/i) || text.match(/^\s*([a-d])\s*$/i);
79
+ if (letterMatch) {
80
+ const answered = letterMatch[1].toLowerCase().charCodeAt(0) - 97;
81
+ return answered === question.answer ? 1 : 0;
82
+ }
83
+
84
+ // Full text match
85
+ const correctText = question.choices[question.answer].toLowerCase();
86
+ if (text.includes(correctText)) return 1;
87
+
88
+ // Key word match
89
+ const keyWords = correctText.split(' ').filter(w => w.length > 4);
90
+ if (keyWords.length > 0 && keyWords.some(w => text.includes(w))) return 1;
91
+
92
+ return 0;
93
+ }
94
+
95
+ function scoreQuality(response, question) {
96
+ if (!response) return 0;
97
+ const text = response.toLowerCase();
98
+ const expected = question.expected.toLowerCase();
99
+
100
+ // Check for expected keyword
101
+ if (text.includes(expected)) return 10;
102
+
103
+ // Partial credit based on length (well-formed response)
104
+ const len = response.length;
105
+ if (len > 20) return 5;
106
+ if (len > 5) return 2;
107
+ return 0;
108
+ }
109
+
110
+ async function main() {
111
+ const fs = require('fs');
112
+ console.log('\n🧠 A3M Router — Benchmark\n');
113
+
114
+ const results = {};
115
+
116
+ for (const [pid, config] of Object.entries(PROVIDERS)) {
117
+ if (!process.env[config.apiKeyEnv] || process.env[config.apiKeyEnv].length < 20) {
118
+ console.log(`⏭️ ${config.name}: No API key`);
119
+ continue;
120
+ }
121
+
122
+ console.log(`\n📡 ${config.name}...`);
123
+
124
+ // MMLU
125
+ let correct = 0;
126
+ const mmluResults = [];
127
+ for (const q of MMLU_QUESTIONS) {
128
+ const prompt = `${q.q}\nA) ${q.choices[0]}\nB) ${q.choices[1]}\nC) ${q.choices[2]}\nD) ${q.choices[3]}\n\nAnswer (just letter A/B/C/D):`;
129
+ const r = await callAPI(pid, prompt, 15);
130
+ const graded = gradeMMLU(q, r.answer);
131
+ correct += graded;
132
+ mmluResults.push({ q: q.q.slice(0, 35), response: r.answer?.slice(0, 20), correct: graded === 1 });
133
+ process.stdout.write(`${graded ? '✅' : '❌'} `);
134
+ await new Promise(r => setTimeout(r, 250));
135
+ }
136
+ console.log(`\n MMLU: ${correct}/15`);
137
+
138
+ // Quality test
139
+ let qualityScore = 0;
140
+ const qualResults = [];
141
+ for (const q of QUALITY_QUESTIONS) {
142
+ const r = await callAPI(pid, q.prompt, 100);
143
+ const score = scoreQuality(r.answer, q);
144
+ qualityScore += score;
145
+ qualResults.push({ prompt: q.prompt.slice(0, 30), response: r.answer?.slice(0, 30), score });
146
+ await new Promise(r2 => setTimeout(r2, 250));
147
+ }
148
+ const qualityAvg = qualityScore / QUALITY_QUESTIONS.length;
149
+ console.log(` Quality: ${qualityAvg.toFixed(1)}/10`);
150
+
151
+ results[pid] = {
152
+ name: config.name,
153
+ mmlu_accuracy: correct / 15,
154
+ quality_score: Math.round(qualityAvg * 10) / 10,
155
+ mmlu_detail: mmluResults,
156
+ quality_detail: qualResults,
157
+ };
158
+ }
159
+
160
+ console.log('\n📊 Results\n');
161
+ console.log('Provider | MMLU | Quality | Notes');
162
+ console.log('--------------------------|-------|---------|------');
163
+
164
+ for (const [pid, r] of Object.entries(results)) {
165
+ const mmlu = `${(r.mmlu_accuracy * 100).toFixed(0)}%`;
166
+ console.log(`${r.name.padEnd(25)}| ${mmlu.padStart(5)} | ${r.quality_score.toFixed(1).padStart(7)}/10 |`);
167
+ }
168
+
169
+ fs.writeFileSync('benchmark-results.json', JSON.stringify({
170
+ meta: { date: new Date().toISOString(), type: 'mmlu_quality' },
171
+ results
172
+ }, null, 2));
173
+ console.log('\n💾 Saved to benchmark-results.json');
174
+ }
175
+
176
+ main().catch(console.error);
@@ -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);
@@ -7,12 +7,8 @@
7
7
  * No external embedding API needed. Trigram overlap catches paraphrases like:
8
8
  * "What is Python?" ≈ "Tell me about Python" ≈ "Explain Python"
9
9
  * "Write a sort fn" ≈ "Create a sorting fn" ≈ "How to sort an array"
10
- *
11
- * Also supports cross-provider cache key generation via generateCacheKey().
12
10
  */
13
11
 
14
- import * as crypto from 'crypto';
15
-
16
12
  // ============================================================
17
13
  // Types
18
14
  // ============================================================
@@ -57,150 +53,6 @@ function normalize(text: string): string {
57
53
  .trim();
58
54
  }
59
55
 
60
- // ============================================================
61
- // Cross-Provider Cache Key Generation
62
- // ============================================================
63
-
64
- /**
65
- * Provider-specific formatting patterns to strip for cross-provider cache keys.
66
- */
67
- const PROVIDER_SYSTEM_PATTERNS: Record<string, RegExp[]> = {
68
- anthropic: [
69
- /<anthropic_thinking>[\s\S]*?<\/anthropic_thinking>/gi,
70
- /<thinking>[\s\S]*?<\/thinking>/gi,
71
- /Human:/gi,
72
- /Assistant:/gi,
73
- ],
74
- openai: [
75
- /<|im_start|>/gi,
76
- /<|im_end|>/gi,
77
- ],
78
- google: [
79
- /<content>[\s\S]*?<\/content>/gi,
80
- /[Parts|thought]/gi,
81
- ],
82
- };
83
-
84
- export interface GenerateCacheKeyOptions {
85
- /** Target provider (affects normalization rules) */
86
- provider?: string;
87
- /** Target model (for model-specific normalization) */
88
- model?: string;
89
- /** Custom normalization rules */
90
- customRules?: Array<{ pattern: RegExp; replacement: string }>;
91
- }
92
-
93
- export interface CacheKeyResult {
94
- /** The normalized cache key string */
95
- key: string;
96
- /** Hash of the normalized content */
97
- hash: string;
98
- /** Metadata about what was normalized */
99
- metadata: {
100
- originalLength: number;
101
- normalizedLength: number;
102
- rulesApplied: number;
103
- provider?: string;
104
- };
105
- }
106
-
107
- /**
108
- * Generate a deterministic cache key from a query.
109
- * Same semantic content = same key across providers.
110
- *
111
- * @example
112
- * const key1 = semanticCache.generateCacheKey("What is Python?", { provider: "openai" });
113
- * const key2 = semanticCache.generateCacheKey("What is Python?", { provider: "anthropic" });
114
- * // key1.key === key2.key (same semantic content = same key)
115
- */
116
- export function generateCacheKey(
117
- query: string,
118
- options: GenerateCacheKeyOptions = {}
119
- ): CacheKeyResult {
120
- const originalLength = query.length;
121
-
122
- // Step 1: Unicode normalization (NFC)
123
- let normalized = query.normalize('NFC');
124
-
125
- // Step 2: Collapse whitespace
126
- normalized = normalized.replace(/\s+/g, ' ');
127
-
128
- // Step 3: Remove control characters
129
- normalized = normalized.replace(/[\x00-\x1F\x7F]/g, '');
130
-
131
- // Step 4: Strip provider-specific formatting
132
- if (options.provider) {
133
- const patterns = PROVIDER_SYSTEM_PATTERNS[options.provider] || [];
134
- for (const pattern of patterns) {
135
- normalized = normalized.replace(pattern, '');
136
- }
137
- }
138
-
139
- // Step 5: General system/assistant role removal
140
- normalized = normalized
141
- .replace(/\b(system|user|assistant|human|bot)\s*:/gi, '')
142
- .replace(/^(system|user|assistant|human|bot)\s*/gim, '');
143
-
144
- // Step 6: Remove markdown formatting
145
- normalized = normalized
146
- .replace(/```[\s\S]*?```/g, '[CODE_BLOCK]')
147
- .replace(/`([^`]+)`/g, '$1')
148
- .replace(/\*\*([^*]+)\*\*/g, '$1')
149
- .replace(/_([^_]+)_/g, '$1')
150
- .replace(/#+\s*/g, '')
151
- .replace(/^\s*[-*+]\s+/gm, '')
152
- .replace(/^\s*\d+\.\s+/gm, '');
153
-
154
- // Step 7: Apply custom rules
155
- if (options.customRules) {
156
- for (const rule of options.customRules) {
157
- normalized = normalized.replace(rule.pattern, rule.replacement);
158
- }
159
- }
160
-
161
- // Step 8: Final whitespace collapse
162
- normalized = normalized.replace(/\s+/g, ' ').trim();
163
-
164
- // Count rules applied
165
- let rulesApplied = 3; // Base normalizations
166
- if (options.provider) rulesApplied += 2;
167
- if (options.customRules) rulesApplied += options.customRules.length;
168
-
169
- // Generate hash
170
- const hash = crypto
171
- .createHash('sha256')
172
- .update(normalized)
173
- .digest('hex')
174
- .substring(0, 16);
175
-
176
- // Build key
177
- let key = `v1:${hash}`;
178
- if (options.provider) {
179
- key += `:${options.provider}`;
180
- if (options.model) {
181
- key += `:${options.model}`;
182
- }
183
- }
184
-
185
- return {
186
- key,
187
- hash,
188
- metadata: {
189
- originalLength,
190
- normalizedLength: normalized.length,
191
- rulesApplied,
192
- provider: options.provider,
193
- },
194
- };
195
- }
196
-
197
- /**
198
- * Quick cache key generation (simplified API).
199
- */
200
- export function toCacheKey(query: string, provider?: string): string {
201
- return generateCacheKey(query, { provider }).key;
202
- }
203
-
204
56
  /**
205
57
  * Extract character trigrams from text.
206
58
  * Pads with spaces so short words still produce trigrams.
package/src/index.ts ADDED
@@ -0,0 +1,99 @@
1
+ // A3M Router - Main Entry Point
2
+ // Version: 2.0.0
3
+
4
+ // ============================================================
5
+ // ROUTING ENGINE
6
+ // ============================================================
7
+ export {
8
+ routeQuery,
9
+ routeBatch,
10
+ recommendForTask,
11
+ extractQueryFeatures,
12
+ MODEL_PROFILES,
13
+ updateModelProfile,
14
+ getProviderHealth,
15
+ } from './routing/advancedRouter';
16
+
17
+ // ============================================================
18
+ // PROVIDERS
19
+ // ============================================================
20
+ export {
21
+ DEFAULT_PROVIDERS,
22
+ getAvailableProviders,
23
+ registerProvider,
24
+ deregisterProvider,
25
+ updateProvider,
26
+ healthCheck,
27
+ checkAllProviders,
28
+ findCheapestAvailableProvider,
29
+ findFastestAvailableProvider,
30
+ loadConfig,
31
+ saveConfig,
32
+ } from './providers/providerConfig';
33
+
34
+ export type {
35
+ ProviderTier,
36
+ ProviderFormat,
37
+ ProviderType,
38
+ ProviderCost,
39
+ ProviderDefinition,
40
+ } from './providers/providerConfig';
41
+
42
+ // ============================================================
43
+ // COST TRACKING
44
+ // ============================================================
45
+ export { CostTracker } from './cost/costTracker';
46
+
47
+ // ============================================================
48
+ // MEMORY
49
+ // ============================================================
50
+ export { MemoryTree } from './memory/memoryTree';
51
+ export type { MemoryChunk, TreeNode } from './memory/memoryTree';
52
+
53
+ // ============================================================
54
+ // UTILITIES
55
+ // ============================================================
56
+ export { countTokens, estimateTokens } from './utils/tokenUtils';
57
+ export { MODEL_COSTS } from './utils/tokenUtils';
58
+
59
+ // ============================================================
60
+ // v2.0.0 FEATURES
61
+ // ============================================================
62
+ export { SemanticCache } from './cache/semanticCache';
63
+ export { GuardrailEngine } from './security/guardrails';
64
+ export { CostAnalytics } from './analytics/costAnalytics';
65
+ export { createProxyServer } from './server/proxyServer';
66
+
67
+ // ============================================================
68
+ // CONVENIENCE: Create a router instance
69
+ // ============================================================
70
+ import { routeQuery, routeBatch, recommendForTask } from './routing/advancedRouter';
71
+ import { getAvailableProviders, healthCheck } from './providers/providerConfig';
72
+ import { CostTracker } from './cost/costTracker';
73
+ import { MemoryTree } from './memory/memoryTree';
74
+
75
+ export interface A3MRouterOptions {
76
+ defaultProvider?: string;
77
+ enableCache?: boolean;
78
+ enableGuardrails?: boolean;
79
+ costLimit?: number;
80
+ }
81
+
82
+ export function createA3MRouter(options?: A3MRouterOptions) {
83
+ const costTracker = new CostTracker();
84
+ const memoryTree = new MemoryTree();
85
+
86
+ return {
87
+ route: routeQuery,
88
+ routeBatch,
89
+ recommendForTask,
90
+ getAvailableProviders,
91
+ healthCheck,
92
+ costTracker,
93
+ memoryTree,
94
+ options: options || {},
95
+ };
96
+ }
97
+
98
+ // Default export
99
+ export default createA3MRouter;