adaptive-memory-multi-model-router 2.10.0 β 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +56 -0
- package/LANDING.md +46 -0
- package/MANIFESTO.md +54 -0
- package/demo.sh +85 -0
- package/dist/cli/setupWizard.js +194 -0
- package/dist/cli.js +9 -2
- package/dist/routing/providerRetry.d.ts +5 -0
- package/dist/routing/providerRetry.js +37 -0
- package/dist/routing/providerRetry.js.map +1 -1
- package/docs/CHINESE_PROVIDER_RELIABILITY.md +37 -0
- package/docs/CLAIMS_AND_EVIDENCE.md +58 -0
- package/docs/ENGINEERING_SPEC.md +55 -0
- package/docs/RELEASE_CHECKLIST.md +32 -0
- package/docs/REPRODUCIBILITY.md +63 -0
- package/eval/README.md +46 -0
- package/eval/baselines/main.json +12 -0
- package/eval/benchmark_dataset.jsonl +16 -0
- package/eval/check_golden_routes.js +64 -0
- package/eval/datasets/catalog.json +33 -0
- package/eval/datasets/slices/cn_provider_reliability_v1.jsonl +3 -0
- package/eval/datasets/slices/cost_pressure_v1.jsonl +3 -0
- package/eval/datasets/slices/safety_guardrails_v1.jsonl +3 -0
- package/eval/fault_injection_thresholds.json +3 -0
- package/eval/generate_report.js +128 -0
- package/eval/golden_routes.json +114 -0
- package/eval/lib/experiment_registry.js +24 -0
- package/eval/run_eval.js +197 -0
- package/eval/run_fault_injection.js +201 -0
- package/eval/run_shadow_eval.js +85 -0
- package/eval/thresholds.json +9 -0
- package/package.json +541 -458
- package/pytest.ini +2 -0
- package/src/cli/setupWizard.ts +194 -0
- package/src/routing/providerRetry.ts +41 -1
- package/python/a3m/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/a3m/__pycache__/client.cpython-312.pyc +0 -0
- package/python/a3m/__pycache__/models.cpython-312.pyc +0 -0
- package/python/a3m/__pycache__/sync_client.cpython-312.pyc +0 -0
package/pytest.ini
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A3M Router Setup Wizard
|
|
3
|
+
* Interactive configuration wizard
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const readline = require('readline');
|
|
9
|
+
|
|
10
|
+
const CONFIG_DIR = path.join(process.env.HOME || '/tmp', '.config', 'a3m-router');
|
|
11
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'providers.json');
|
|
12
|
+
|
|
13
|
+
// API key environment variable mappings
|
|
14
|
+
const API_KEY_ENV_MAP = {
|
|
15
|
+
'GROQ_API_KEY': 'groq',
|
|
16
|
+
'OPENAI_API_KEY': 'openai',
|
|
17
|
+
'ANTHROPIC_API_KEY': 'anthropic',
|
|
18
|
+
'DEEPSEEK_API_KEY': 'deepseek',
|
|
19
|
+
'MISTRAL_API_KEY': 'mistral',
|
|
20
|
+
'GOOGLE_API_KEY': 'google',
|
|
21
|
+
'CEREBRAS_API_KEY': 'cerebras',
|
|
22
|
+
'TOGETHER_API_KEY': 'together',
|
|
23
|
+
'AI21_API_KEY': 'ai21',
|
|
24
|
+
'COHERE_API_KEY': 'cohere',
|
|
25
|
+
'MINIMAX_API_KEY': 'minimax',
|
|
26
|
+
'KIMI_API_KEY': 'kimi',
|
|
27
|
+
'MOONSHOT_API_KEY': 'moonshot',
|
|
28
|
+
'QWEN_API_KEY': 'qwen',
|
|
29
|
+
'ZHIPU_API_KEY': 'zhipu',
|
|
30
|
+
'YI_API_KEY': 'yi',
|
|
31
|
+
'BAICHUAN_API_KEY': 'baichuan',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Provider metadata
|
|
35
|
+
const PROVIDER_INFO = {
|
|
36
|
+
groq: { name: 'Groq', models: 'llama-3.3-70b-versatile', tier: 'free', strength: 'Fast, free tier' },
|
|
37
|
+
openai: { name: 'OpenAI', models: 'gpt-4o-mini', tier: 'paid', strength: 'GPT-4, most capable' },
|
|
38
|
+
anthropic: { name: 'Anthropic', models: 'claude-3.5-haiku', tier: 'paid', strength: 'Claude, best reasoning' },
|
|
39
|
+
deepseek: { name: 'DeepSeek', models: 'deepseek-chat-v3', tier: 'cheap', strength: 'Cheap, good code' },
|
|
40
|
+
mistral: { name: 'Mistral', models: 'mistral-small-latest', tier: 'cheap', strength: 'European, balanced' },
|
|
41
|
+
google: { name: 'Google AI', models: 'gemini-1.5-flash', tier: 'free', strength: 'Gemini, multimodal' },
|
|
42
|
+
cerebras: { name: 'Cerebras', models: 'llama-3.3-70b', tier: 'free', strength: 'Fastest inference' },
|
|
43
|
+
together: { name: 'Together AI', models: 'Llama-3.3-70B-Instruct', tier: 'cheap', strength: 'Managed, reliable' },
|
|
44
|
+
ai21: { name: 'AI21', models: 'jamba-1.5-medium', tier: 'paid', strength: 'Jamba, long context' },
|
|
45
|
+
cohere: { name: 'Cohere', models: 'command-r7b', tier: 'cheap', strength: 'Command series, fast' },
|
|
46
|
+
minimax: { name: 'MiniMax', models: 'abab6.5s-chat', tier: 'cheap', strength: 'Chinese, cheap' },
|
|
47
|
+
kimi: { name: 'Kimi/Moonshot', models: 'moonshot-v1-8k', tier: 'cheap', strength: 'Chinese, 128k context' },
|
|
48
|
+
moonshot: { name: 'Moonshot', models: 'moonshot-v1-8k', tier: 'cheap', strength: 'Chinese, good' },
|
|
49
|
+
qwen: { name: 'Qwen', models: 'qwen-turbo', tier: 'cheap', strength: 'Alibaba, multilingual' },
|
|
50
|
+
zhipu: { name: 'Zhipu GLM', models: 'glm-4', tier: 'cheap', strength: 'Chinese, smart' },
|
|
51
|
+
yi: { name: 'Yi', models: 'yi-large', tier: 'cheap', strength: 'Chinese, good reasoning' },
|
|
52
|
+
baichuan: { name: 'Baichuan', models: 'baichuan-4', tier: 'cheap', strength: 'Chinese, balanced' },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function createInterface() {
|
|
56
|
+
return readline.createInterface({
|
|
57
|
+
input: process.stdin,
|
|
58
|
+
output: process.stdout
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function question(rl, text) {
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
rl.question(text, (answer) => resolve(answer));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function detectApiKeys() {
|
|
69
|
+
const detected = [];
|
|
70
|
+
for (const [envVar, providerId] of Object.entries(API_KEY_ENV_MAP)) {
|
|
71
|
+
if (process.env[envVar]) {
|
|
72
|
+
detected.push({ envVar, providerId, info: PROVIDER_INFO[providerId] });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return detected;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function runWizard() {
|
|
79
|
+
console.log('\nπ§ A3M Router Setup Wizard');
|
|
80
|
+
console.log('βββββββββββββββββββββββββ\n');
|
|
81
|
+
|
|
82
|
+
const rl = createInterface();
|
|
83
|
+
|
|
84
|
+
// Ensure config directory exists
|
|
85
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
86
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check for existing config
|
|
90
|
+
let existingConfig = {};
|
|
91
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
92
|
+
try {
|
|
93
|
+
existingConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
94
|
+
console.log('β Found existing config at', CONFIG_FILE);
|
|
95
|
+
console.log(' Providers:', Object.keys(existingConfig.providers || {}).join(', '));
|
|
96
|
+
console.log('');
|
|
97
|
+
} catch (e) {
|
|
98
|
+
console.log('β Could not read existing config, starting fresh\n');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Auto-detect API keys
|
|
103
|
+
console.log('π Scanning for API keys in environment...');
|
|
104
|
+
const detected = await detectApiKeys();
|
|
105
|
+
|
|
106
|
+
if (detected.length === 0) {
|
|
107
|
+
console.log('β No API keys detected in environment.');
|
|
108
|
+
console.log(' Set any of: GROQ_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, etc.\n');
|
|
109
|
+
} else {
|
|
110
|
+
console.log('β Found', detected.length, 'API key(s):');
|
|
111
|
+
detected.forEach(({ envVar, providerId, info }) => {
|
|
112
|
+
console.log(' β', envVar, 'β', info?.name || providerId);
|
|
113
|
+
});
|
|
114
|
+
console.log('');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Provider selection
|
|
118
|
+
const allProviders = Object.keys(PROVIDER_INFO);
|
|
119
|
+
const selected = new Set();
|
|
120
|
+
|
|
121
|
+
// Pre-select providers with detected keys
|
|
122
|
+
detected.forEach(({ providerId }) => selected.add(providerId));
|
|
123
|
+
|
|
124
|
+
console.log('π‘ Select providers to configure (comma-separated numbers, or "all"):');
|
|
125
|
+
console.log('');
|
|
126
|
+
|
|
127
|
+
const numbered = allProviders.map((id, i) => ({ id, i }));
|
|
128
|
+
numbered.forEach(({ id, i }) => {
|
|
129
|
+
const info = PROVIDER_INFO[id];
|
|
130
|
+
const selected_mark = selected.has(id) ? '[x]' : '[ ]';
|
|
131
|
+
const tier_mark = info?.tier === 'free' ? '(FREE)' : info?.tier === 'cheap' ? '(cheap)' : '(paid)';
|
|
132
|
+
console.log(` ${String(i + 1).padStart(2)}. ${selected_mark} ${id.padEnd(12)} ${tier_mark} - ${info?.strength || ''}`);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
console.log('');
|
|
136
|
+
const answer = await question(rl, ' Enter numbers or "all" [all with keys detected]: ');
|
|
137
|
+
|
|
138
|
+
if (answer.toLowerCase().trim() === 'all') {
|
|
139
|
+
allProviders.forEach(id => selected.add(id));
|
|
140
|
+
} else if (answer.trim()) {
|
|
141
|
+
const nums = answer.split(',').map(s => parseInt(s.trim())).filter(n => !isNaN(n));
|
|
142
|
+
nums.forEach(n => {
|
|
143
|
+
const idx = n - 1;
|
|
144
|
+
if (idx >= 0 && idx < allProviders.length) {
|
|
145
|
+
selected.add(allProviders[idx]);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log('\nβ Selected providers:', Array.from(selected).join(', '));
|
|
151
|
+
|
|
152
|
+
// Build config
|
|
153
|
+
const config = {
|
|
154
|
+
version: '1.0',
|
|
155
|
+
providers: {}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
selected.forEach(providerId => {
|
|
159
|
+
const info = PROVIDER_INFO[providerId];
|
|
160
|
+
const envKey = Object.entries(API_KEY_ENV_MAP).find(([k, v]) => v === providerId)?.[0];
|
|
161
|
+
|
|
162
|
+
config.providers[providerId] = {
|
|
163
|
+
name: info?.name || providerId,
|
|
164
|
+
apiKey: envKey ? process.env[envKey] : '',
|
|
165
|
+
models: [info?.models || 'default'],
|
|
166
|
+
type: 'api',
|
|
167
|
+
enabled: true
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Save config
|
|
172
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
173
|
+
console.log('\nβ Config saved to', CONFIG_FILE);
|
|
174
|
+
|
|
175
|
+
// Test connections
|
|
176
|
+
console.log('\nπ§ͺ Testing connections...');
|
|
177
|
+
console.log(' (Skipped in wizard mode - run "npx a3m-router test" to verify)\n');
|
|
178
|
+
|
|
179
|
+
// Ready message
|
|
180
|
+
console.log('βββββββββββββββββββββββββ');
|
|
181
|
+
console.log('β
A3M Router is ready!');
|
|
182
|
+
console.log('');
|
|
183
|
+
console.log(' Next steps:');
|
|
184
|
+
console.log(' 1. npx a3m-router serve # Start proxy server');
|
|
185
|
+
console.log(' 2. npx a3m-router test # Test provider connections');
|
|
186
|
+
console.log(' 3. npx a3m-router route "hi" # Try routing a query');
|
|
187
|
+
console.log('');
|
|
188
|
+
console.log(' Docs: https://github.com/Das-rebel/adaptive-memory-multi-model-router');
|
|
189
|
+
console.log('');
|
|
190
|
+
|
|
191
|
+
rl.close();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = { runWizard };
|
|
@@ -357,6 +357,11 @@ export class ProviderRetryHandler {
|
|
|
357
357
|
isRetryableError(error: any): boolean {
|
|
358
358
|
if (!error) return false;
|
|
359
359
|
|
|
360
|
+
// Known non-retryable account/quota/policy states seen in Chinese provider APIs.
|
|
361
|
+
if (this.isPermanentProviderStateError(error)) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
|
|
360
365
|
const config = this.configs.get('default')!.retry;
|
|
361
366
|
const retryableErrors = config.retryableErrors || DEFAULT_RETRY_CONFIG.retryableErrors!;
|
|
362
367
|
|
|
@@ -382,6 +387,41 @@ export class ProviderRetryHandler {
|
|
|
382
387
|
return false;
|
|
383
388
|
}
|
|
384
389
|
|
|
390
|
+
/**
|
|
391
|
+
* Detect hard non-retryable account/policy states.
|
|
392
|
+
* These should fail fast instead of wasting retries.
|
|
393
|
+
*/
|
|
394
|
+
private isPermanentProviderStateError(error: any): boolean {
|
|
395
|
+
const status = Number(error?.status ?? error?.statusCode ?? 0);
|
|
396
|
+
const message = String(error?.message || '').toLowerCase();
|
|
397
|
+
const code = String(error?.code || '').toLowerCase();
|
|
398
|
+
|
|
399
|
+
// Payment/balance/account states are generally non-retryable without human action.
|
|
400
|
+
if (status === 402) return true;
|
|
401
|
+
if (status === 401 || status === 403) return true;
|
|
402
|
+
|
|
403
|
+
const hardPatterns = [
|
|
404
|
+
'insufficient',
|
|
405
|
+
'balance',
|
|
406
|
+
'invalid api key',
|
|
407
|
+
'incorrect api key',
|
|
408
|
+
'access terminated',
|
|
409
|
+
'account abnormal',
|
|
410
|
+
'θΏθ§',
|
|
411
|
+
'quota exhausted',
|
|
412
|
+
'hour allocated quota exceeded',
|
|
413
|
+
'week allocated quota exceeded',
|
|
414
|
+
'month allocated quota exceeded',
|
|
415
|
+
'tpd rate limit'
|
|
416
|
+
];
|
|
417
|
+
|
|
418
|
+
for (const p of hardPatterns) {
|
|
419
|
+
if (message.includes(p) || code.includes(p)) return true;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
|
|
385
425
|
/**
|
|
386
426
|
* Check if error is a rate limit (429)
|
|
387
427
|
*/
|
|
@@ -575,4 +615,4 @@ export function getDefaultRetryHandler(): ProviderRetryHandler {
|
|
|
575
615
|
export {
|
|
576
616
|
DEFAULT_RETRY_CONFIG,
|
|
577
617
|
PROVIDER_CONTEXT_LIMITS,
|
|
578
|
-
};
|
|
618
|
+
};
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|