Skip to content

Commit e1513ae

Browse files
Subhajit dasSubhajit das
authored andcommitted
feat: add interactive setup wizard
- Auto-detects API keys from environment - Interactive provider selection - Saves config to ~/.config/a3m-router/providers.json - New command: npx a3m-router setup Run: npx a3m-router setup
1 parent 137aff5 commit e1513ae

3 files changed

Lines changed: 397 additions & 2 deletions

File tree

‎dist/cli.js‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
* A3M Router CLI - Adaptive Memory Multi-Model Router
44
*
55
* Commands:
6-
* npx a3m-router serve [--port 8787] Start OpenAI-compatible proxy server
6+
* npx a3m-router serve [--port 8787] Start OpenAI-compatible proxy server
77
* npx a3m-router route <query> Route query to best provider
8-
* npx a3m-router batch <q1> <q2>.. Route multiple queries
8+
* npx a3m-router setup Interactive setup wizard (auto-detect API keys)
9+
* npx a3m-router batch <q1> <q2>.. Route multiple queries
910
* npx a3m-router providers List all configured providers
1011
* npx a3m-router test Test all providers
1112
* npx a3m-router compare <query> Compare providers side by side
@@ -155,6 +156,12 @@ async function main() {
155156
const router = createA3MRouter({ memory: { maxSize: 1000 } });
156157

157158
switch (command) {
159+
case 'setup': {
160+
const { runWizard } = require('./cli/setupWizard.js');
161+
runWizard();
162+
break;
163+
}
164+
158165
case 'providers': {
159166
const providers = providerConfig.getAvailableProviders();
160167
const allProviders = providerConfig._providers;

‎dist/cli/setupWizard.js‎

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 };

0 commit comments

Comments
 (0)