adaptive-memory-multi-model-router 1.9.0 → 1.9.2
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/dist/cli.js +344 -109
- package/dist/index.js +14 -0
- package/dist/providers/providerConfig.js +452 -0
- package/dist/providers/registry.js +60 -41
- package/dist/routing/advancedRouter.js +368 -310
- package/package.json +6 -3
- package/test/benchmark.js +297 -0
- package/test/provider-test.js +472 -0
- package/test.js +376 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adaptive-memory-multi-model-router",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.2",
|
|
4
4
|
"shortName": "A3M Router",
|
|
5
5
|
"displayName": "A3M Router - Adaptive Memory Multi-Model Router",
|
|
6
6
|
"description": "A3M Router - Adaptive Memory Multi-Model Router with learned routing (RouteLLM), prefix caching (RadixAttention), speculative decoding (Medusa), TokenJuice-style compression. 14 LLM providers, 10 integrations, Python bindings. 20x more adaptable for ML/AI developers.",
|
|
@@ -174,7 +174,10 @@
|
|
|
174
174
|
},
|
|
175
175
|
"homepage": "https://github.com/Das-rebel/adaptive-memory-multi-model-router#readme",
|
|
176
176
|
"scripts": {
|
|
177
|
-
"test": "node test.js"
|
|
177
|
+
"test": "node test.js && node test/provider-test.js",
|
|
178
|
+
"test:providers": "node test/provider-test.js",
|
|
179
|
+
"benchmark": "node test/benchmark.js",
|
|
180
|
+
"benchmark:verbose": "node test/benchmark.js --verbose"
|
|
178
181
|
},
|
|
179
182
|
"engines": {
|
|
180
183
|
"node": ">=16.0.0"
|
|
@@ -182,4 +185,4 @@
|
|
|
182
185
|
"dependencies": {
|
|
183
186
|
"nanoid": "^5.0.0"
|
|
184
187
|
}
|
|
185
|
-
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* A3M Router - Provider Benchmark
|
|
4
|
+
*
|
|
5
|
+
* Benchmarks all available providers across:
|
|
6
|
+
* - Latency (response time)
|
|
7
|
+
* - Cost (per 1K tokens)
|
|
8
|
+
* - Quality (simple factual questions)
|
|
9
|
+
* - Cost-effectiveness (quality per dollar)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { execSync } = require('child_process');
|
|
13
|
+
const {
|
|
14
|
+
getAvailableProviders,
|
|
15
|
+
providerConfig,
|
|
16
|
+
countTokens,
|
|
17
|
+
estimateCost,
|
|
18
|
+
} = require('../dist/index.js');
|
|
19
|
+
|
|
20
|
+
// Benchmark configuration
|
|
21
|
+
const CONFIG = {
|
|
22
|
+
timeout: 60000,
|
|
23
|
+
maxTokens: 50,
|
|
24
|
+
verbose: process.argv.includes('--verbose') || process.argv.includes('-v'),
|
|
25
|
+
json: process.argv.includes('--json'),
|
|
26
|
+
provider: process.argv.find(arg => arg.startsWith('--provider='))?.split('=')[1],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Test queries for different scenarios
|
|
30
|
+
const QUERIES = {
|
|
31
|
+
simple: 'What is 2+2?',
|
|
32
|
+
code: 'Write a Python function to reverse a string.',
|
|
33
|
+
math: 'Calculate the square root of 144.',
|
|
34
|
+
creative: 'Write a haiku about programming.',
|
|
35
|
+
reasoning: 'Explain why the sky is blue.',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Results storage
|
|
39
|
+
const results = [];
|
|
40
|
+
|
|
41
|
+
// Helper: Call a provider
|
|
42
|
+
async function callProvider(id, provider, query) {
|
|
43
|
+
const model = provider.models[0];
|
|
44
|
+
const startTime = Date.now();
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
if (provider.type === 'cli') {
|
|
48
|
+
// CLI provider
|
|
49
|
+
if (id === 'commandcode') {
|
|
50
|
+
const raw = execSync(`commandcode -p "${query.replace(/"/g, '\\"')}" --skip-onboarding 2>&1`, {
|
|
51
|
+
timeout: CONFIG.timeout,
|
|
52
|
+
encoding: 'utf-8'
|
|
53
|
+
});
|
|
54
|
+
const content = raw.replace(/\x1b\[[0-9;]*m/g, '').trim();
|
|
55
|
+
const latency = Date.now() - startTime;
|
|
56
|
+
const tokens = Math.ceil(content.length / 4);
|
|
57
|
+
return {
|
|
58
|
+
content: content.substring(0, 200),
|
|
59
|
+
tokens,
|
|
60
|
+
cost: 0,
|
|
61
|
+
latency,
|
|
62
|
+
success: true,
|
|
63
|
+
};
|
|
64
|
+
} else {
|
|
65
|
+
// Generic CLI
|
|
66
|
+
const raw = execSync(`${provider.cliCommand} run "${query.replace(/"/g, '\\"')}" 2>&1`, {
|
|
67
|
+
timeout: CONFIG.timeout,
|
|
68
|
+
encoding: 'utf-8'
|
|
69
|
+
});
|
|
70
|
+
const lines = raw.replace(/\x1b\[[0-9;]*m/g, '').split('\n').filter(l => l.trim() && !l.startsWith('>'));
|
|
71
|
+
const content = lines.join(' ').trim();
|
|
72
|
+
const latency = Date.now() - startTime;
|
|
73
|
+
const tokens = Math.ceil(content.length / 4);
|
|
74
|
+
return {
|
|
75
|
+
content: content.substring(0, 200),
|
|
76
|
+
tokens,
|
|
77
|
+
cost: 0,
|
|
78
|
+
latency,
|
|
79
|
+
success: true,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// API provider
|
|
85
|
+
if (!provider.apiKey) {
|
|
86
|
+
return { error: 'No API key', success: false };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const resp = await fetch(provider.baseUrl, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: {
|
|
92
|
+
'Authorization': `Bearer ${provider.apiKey}`,
|
|
93
|
+
'Content-Type': 'application/json',
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify({
|
|
96
|
+
model,
|
|
97
|
+
messages: [{ role: 'user', content: query }],
|
|
98
|
+
max_tokens: CONFIG.maxTokens,
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const latency = Date.now() - startTime;
|
|
103
|
+
const data = await resp.json();
|
|
104
|
+
|
|
105
|
+
if (data.error) {
|
|
106
|
+
return { error: data.error.message, success: false };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const content = data.choices?.[0]?.message?.content || '';
|
|
110
|
+
const promptTokens = data.usage?.prompt_tokens || countTokens(query);
|
|
111
|
+
const completionTokens = data.usage?.completion_tokens || countTokens(content);
|
|
112
|
+
const cost = (promptTokens / 1000 * provider.costPerK.input) +
|
|
113
|
+
(completionTokens / 1000 * provider.costPerK.output);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
content: content.substring(0, 200),
|
|
117
|
+
tokens: promptTokens + completionTokens,
|
|
118
|
+
cost,
|
|
119
|
+
latency,
|
|
120
|
+
success: true,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
} catch (e) {
|
|
124
|
+
return { error: e.message, success: false };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Helper: Check answer quality (simple heuristic)
|
|
129
|
+
function checkQuality(query, response) {
|
|
130
|
+
const lower = response.toLowerCase();
|
|
131
|
+
|
|
132
|
+
if (query.includes('2+2')) {
|
|
133
|
+
return lower.includes('4') ? 1 : 0;
|
|
134
|
+
}
|
|
135
|
+
if (query.includes('square root of 144')) {
|
|
136
|
+
return lower.includes('12') ? 1 : 0;
|
|
137
|
+
}
|
|
138
|
+
if (query.includes('reverse a string')) {
|
|
139
|
+
return lower.includes('def') || lower.includes('function') ? 1 : 0;
|
|
140
|
+
}
|
|
141
|
+
if (query.includes('haiku')) {
|
|
142
|
+
// Check for 3 lines (rough haiku check)
|
|
143
|
+
const lines = response.split('\n').filter(l => l.trim());
|
|
144
|
+
return lines.length >= 2 ? 1 : 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Default: check for reasonable length
|
|
148
|
+
return response.length > 20 ? 0.8 : 0.5;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Main benchmark
|
|
152
|
+
async function runBenchmark() {
|
|
153
|
+
console.log('\n═══════════════════════════════════════════════════════════════');
|
|
154
|
+
console.log('📊 A3M Router - Provider Benchmark');
|
|
155
|
+
console.log('═══════════════════════════════════════════════════════════════\n');
|
|
156
|
+
|
|
157
|
+
const providers = getAvailableProviders();
|
|
158
|
+
const providerList = CONFIG.provider
|
|
159
|
+
? [[CONFIG.provider, providers[CONFIG.provider]]].filter(([_, p]) => p)
|
|
160
|
+
: Object.entries(providers);
|
|
161
|
+
|
|
162
|
+
if (providerList.length === 0) {
|
|
163
|
+
console.log('❌ No providers available. Configure API keys in ~/.config/a3m-router/providers.json');
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log(`Testing ${providerList.length} provider(s)...\n`);
|
|
168
|
+
|
|
169
|
+
for (const [id, provider] of providerList) {
|
|
170
|
+
if (CONFIG.verbose) {
|
|
171
|
+
console.log(`Testing ${provider.name}...`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const providerResults = {
|
|
175
|
+
id,
|
|
176
|
+
name: provider.name,
|
|
177
|
+
type: provider.type,
|
|
178
|
+
model: provider.models[0],
|
|
179
|
+
queries: {},
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
for (const [queryType, query] of Object.entries(QUERIES)) {
|
|
183
|
+
if (CONFIG.verbose) {
|
|
184
|
+
console.log(` ${queryType}: "${query.substring(0, 40)}..."`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const result = await callProvider(id, provider, query);
|
|
188
|
+
|
|
189
|
+
if (result.success) {
|
|
190
|
+
result.quality = checkQuality(query, result.content);
|
|
191
|
+
result.costEffectiveness = result.cost > 0
|
|
192
|
+
? result.quality / result.cost
|
|
193
|
+
: result.quality * 1000; // Free providers get high score
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
providerResults.queries[queryType] = result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Calculate averages
|
|
200
|
+
const successful = Object.values(providerResults.queries).filter(r => r.success);
|
|
201
|
+
if (successful.length > 0) {
|
|
202
|
+
providerResults.avgLatency = successful.reduce((a, r) => a + r.latency, 0) / successful.length;
|
|
203
|
+
providerResults.avgCost = successful.reduce((a, r) => a + r.cost, 0) / successful.length;
|
|
204
|
+
providerResults.avgQuality = successful.reduce((a, r) => a + r.quality, 0) / successful.length;
|
|
205
|
+
providerResults.avgCostEffectiveness = successful.reduce((a, r) => a + r.costEffectiveness, 0) / successful.length;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
results.push(providerResults);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Output results
|
|
212
|
+
if (CONFIG.json) {
|
|
213
|
+
console.log(JSON.stringify(results, null, 2));
|
|
214
|
+
} else {
|
|
215
|
+
printResults(results);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function printResults(results) {
|
|
220
|
+
// Summary table
|
|
221
|
+
console.log('┌─────────────────────────────────────────────────────────────────────────────┐');
|
|
222
|
+
console.log('│ Provider Type Model Latency Cost Quality │');
|
|
223
|
+
console.log('├─────────────────────────────────────────────────────────────────────────────┤');
|
|
224
|
+
|
|
225
|
+
// Sort by cost-effectiveness
|
|
226
|
+
const sorted = [...results].sort((a, b) => (b.avgCostEffectiveness || 0) - (a.avgCostEffectiveness || 0));
|
|
227
|
+
|
|
228
|
+
for (const r of sorted) {
|
|
229
|
+
const name = r.name.substring(0, 17).padEnd(17);
|
|
230
|
+
const type = r.type.padEnd(7);
|
|
231
|
+
const model = (r.model || 'N/A').substring(0, 22).padEnd(22);
|
|
232
|
+
const latency = r.avgLatency ? `${Math.round(r.avgLatency)}ms`.padEnd(8) : 'N/A ';
|
|
233
|
+
const cost = r.avgCost !== undefined ? `$${r.avgCost.toFixed(4)}`.padEnd(7) : 'N/A ';
|
|
234
|
+
const quality = r.avgQuality ? `${(r.avgQuality * 100).toFixed(0)}%`.padEnd(7) : 'N/A ';
|
|
235
|
+
|
|
236
|
+
console.log(`│ ${name} ${type} ${model} ${latency} ${cost} ${quality} │`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
console.log('└─────────────────────────────────────────────────────────────────────────────┘');
|
|
240
|
+
console.log('');
|
|
241
|
+
|
|
242
|
+
// Rankings
|
|
243
|
+
console.log('🏆 Rankings:');
|
|
244
|
+
console.log('─────────────────────────────────────────────────────────────');
|
|
245
|
+
|
|
246
|
+
// Fastest
|
|
247
|
+
const fastest = [...results].filter(r => r.avgLatency).sort((a, b) => a.avgLatency - b.avgLatency)[0];
|
|
248
|
+
if (fastest) {
|
|
249
|
+
console.log(` ⚡ Fastest: ${fastest.name} (${Math.round(fastest.avgLatency)}ms avg)`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Cheapest
|
|
253
|
+
const cheapest = [...results].filter(r => r.avgCost !== undefined).sort((a, b) => a.avgCost - b.avgCost)[0];
|
|
254
|
+
if (cheapest) {
|
|
255
|
+
console.log(` 💰 Cheapest: ${cheapest.name} ($${cheapest.avgCost.toFixed(6)} avg)`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Best quality
|
|
259
|
+
const bestQuality = [...results].filter(r => r.avgQuality).sort((a, b) => b.avgQuality - a.avgQuality)[0];
|
|
260
|
+
if (bestQuality) {
|
|
261
|
+
console.log(` 🎯 Best Quality: ${bestQuality.name} (${(bestQuality.avgQuality * 100).toFixed(0)}% correct)`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Best cost-effectiveness
|
|
265
|
+
const bestValue = sorted[0];
|
|
266
|
+
if (bestValue) {
|
|
267
|
+
console.log(` ⭐ Best Value: ${bestValue.name} (quality/$)`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
console.log('');
|
|
271
|
+
|
|
272
|
+
// Detailed results
|
|
273
|
+
if (CONFIG.verbose) {
|
|
274
|
+
console.log('📋 Detailed Results:');
|
|
275
|
+
console.log('─────────────────────────────────────────────────────────────');
|
|
276
|
+
|
|
277
|
+
for (const r of results) {
|
|
278
|
+
console.log(`\n${r.name} (${r.type}):`);
|
|
279
|
+
|
|
280
|
+
for (const [queryType, result] of Object.entries(r.queries)) {
|
|
281
|
+
if (result.success) {
|
|
282
|
+
console.log(` ${queryType.padEnd(10)} ${result.latency}ms $${result.cost.toFixed(6)} Q:${(result.quality * 100).toFixed(0)}% "${result.content.substring(0, 50)}..."`);
|
|
283
|
+
} else {
|
|
284
|
+
console.log(` ${queryType.padEnd(10)} ❌ ${result.error || 'Failed'}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
console.log('');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Run benchmark
|
|
294
|
+
runBenchmark().catch(e => {
|
|
295
|
+
console.error('Benchmark failed:', e.message);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
});
|