adaptive-memory-multi-model-router 2.2.6 → 2.2.8
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 +146 -105
- 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/analytics/costAnalytics.d.ts +0 -1
- package/dist/cache/semanticCache.d.ts +0 -41
- package/dist/cache/semanticCache.d.ts.map +1 -1
- package/dist/cache/semanticCache.js +0 -142
- package/dist/cache/semanticCache.js.map +1 -1
- package/dist/cli.js +478 -35
- package/dist/cost/costTracker.js +3 -0
- package/dist/index.d.ts +0 -16
- package/dist/index.js +64 -264
- package/dist/integrations/langchainAdapter.d.ts +0 -1
- package/dist/integrations/oauth.d.ts +0 -1
- package/dist/memory/autoFetch.d.ts +0 -1
- package/dist/memory/memoryTree.d.ts +0 -1
- package/dist/memory/obsidianVault.d.ts +0 -1
- package/dist/providers/providerConfig.d.ts +0 -1
- package/dist/providers/providerConfig.js +0 -2
- package/dist/providers/registry.js +128 -126
- package/dist/routing/advancedRouter.js +427 -310
- package/dist/sdk.js +100 -109
- package/dist/security/guardrails.d.ts +0 -1
- package/dist/server/dashboard.d.ts +0 -1
- package/dist/server/modelMapper.d.ts +0 -1
- package/dist/server/proxyServer.d.ts +0 -1
- package/package.json +96 -389
- package/scripts/run-mmlu-benchmark.js +176 -0
- package/scripts/run-provider-benchmark.js +244 -0
- package/src/cache/semanticCache.ts +0 -148
- package/src/index.ts +99 -0
- package/test/provider-test.js +70 -91
- package/test.js +41 -67
- package/tsconfig.json +5 -15
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/sdk.d.ts.map +0 -1
- package/dist/sdk.js.map +0 -1
- package/test.js.bak +0 -376
|
@@ -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;
|