adaptive-memory-multi-model-router 2.11.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.
Files changed (38) hide show
  1. package/.github/workflows/ci.yml +56 -0
  2. package/LANDING.md +46 -0
  3. package/MANIFESTO.md +54 -0
  4. package/demo.sh +85 -0
  5. package/dist/cli/setupWizard.js +194 -0
  6. package/dist/cli.js +9 -2
  7. package/dist/routing/providerRetry.d.ts +5 -0
  8. package/dist/routing/providerRetry.js +37 -0
  9. package/dist/routing/providerRetry.js.map +1 -1
  10. package/docs/CHINESE_PROVIDER_RELIABILITY.md +37 -0
  11. package/docs/CLAIMS_AND_EVIDENCE.md +58 -0
  12. package/docs/ENGINEERING_SPEC.md +55 -0
  13. package/docs/RELEASE_CHECKLIST.md +32 -0
  14. package/docs/REPRODUCIBILITY.md +63 -0
  15. package/eval/README.md +46 -0
  16. package/eval/baselines/main.json +12 -0
  17. package/eval/benchmark_dataset.jsonl +16 -0
  18. package/eval/check_golden_routes.js +64 -0
  19. package/eval/datasets/catalog.json +33 -0
  20. package/eval/datasets/slices/cn_provider_reliability_v1.jsonl +3 -0
  21. package/eval/datasets/slices/cost_pressure_v1.jsonl +3 -0
  22. package/eval/datasets/slices/safety_guardrails_v1.jsonl +3 -0
  23. package/eval/fault_injection_thresholds.json +3 -0
  24. package/eval/generate_report.js +128 -0
  25. package/eval/golden_routes.json +114 -0
  26. package/eval/lib/experiment_registry.js +24 -0
  27. package/eval/run_eval.js +197 -0
  28. package/eval/run_fault_injection.js +201 -0
  29. package/eval/run_shadow_eval.js +85 -0
  30. package/eval/thresholds.json +9 -0
  31. package/package.json +9 -1
  32. package/pytest.ini +2 -0
  33. package/src/cli/setupWizard.ts +194 -0
  34. package/src/routing/providerRetry.ts +41 -1
  35. package/python/a3m/__pycache__/__init__.cpython-312.pyc +0 -0
  36. package/python/a3m/__pycache__/client.cpython-312.pyc +0 -0
  37. package/python/a3m/__pycache__/models.cpython-312.pyc +0 -0
  38. package/python/a3m/__pycache__/sync_client.cpython-312.pyc +0 -0
@@ -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
+ };