adaptive-memory-multi-model-router 2.13.18 → 2.13.20
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/.dockerignore +82 -0
- package/.env.example +303 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +83 -12
- package/.github/ISSUE_TEMPLATE/config.yml +12 -6
- package/.github/ISSUE_TEMPLATE/feature_request.md +61 -10
- package/.github/PULL_REQUEST_TEMPLATE.md +53 -26
- package/.github/dependabot.yml +9 -0
- package/.github/workflows/codeql.yml +38 -0
- package/.github/workflows/npm-publish.yml +20 -0
- package/.github/workflows/stale.yml +56 -0
- package/ARCHITECTURE.md +346 -0
- package/AUDIT_REPORT.md +28 -0
- package/CHANGELOG.md +386 -22
- package/CONTRIBUTORS.md +20 -0
- package/Dockerfile +53 -0
- package/Dockerfile.proxy +33 -0
- package/PR_STATUS_REPORT.md +148 -0
- package/README.md +22 -0
- package/RUNKIT.md +83 -0
- package/_schema.html +61 -15
- package/articles/AI_AGENT_LLM_ROUTING.md +150 -0
- package/articles/FROM_ZERO_TO_10K.md +107 -0
- package/articles/LLM_BENCHMARK_DEEP_DIVE.md +153 -0
- package/articles/TWEETS_10K_DOWNLOADS.md +47 -0
- package/articles/TWEETS_BENCHMARK_FIRST.md +46 -0
- package/articles/TWEETS_MCP_PLAY.md +51 -0
- package/articles/TWEETS_SEQUENTIAL_BROKEN.md +49 -0
- package/articles/TWEETS_WHY_BUILD.md +54 -0
- package/benchmark-results.json +26 -45
- package/cli/a3m +840 -0
- package/demo/package.json +13 -0
- package/demo/public/index.html +762 -0
- package/demo/server.js +405 -0
- package/dist/cli.js +4 -0
- package/docker-compose.yml +74 -0
- package/docs/.nojekyll +0 -0
- package/docs/BENCHMARK.md +96 -22
- package/docs/_config.yml +49 -0
- package/docs/api.html +513 -0
- package/docs/benchmark.html +387 -0
- package/docs/cli-cheatsheet.md +339 -0
- package/docs/comparison.md +108 -0
- package/docs/curl-examples.md +247 -0
- package/docs/index.html +390 -99
- package/docs/openapi.yaml +1318 -0
- package/docs/quick-start.html +366 -0
- package/docs/robots.txt +1 -1
- package/docs/sitemap.xml +23 -5
- package/docs/styles.css +682 -0
- package/examples/README.md +61 -0
- package/examples/a3m-sdk.js +124 -0
- package/examples/basic-route.js +54 -0
- package/examples/chat-loop.js +202 -0
- package/examples/classify-then-route.js +102 -0
- package/examples/cost-compare.js +120 -0
- package/examples/ensemble.js +160 -0
- package/integrations/langchain/README.md +216 -0
- package/integrations/langchain/a3m_langchain.ts +1360 -0
- package/integrations/langchain/example.ts +287 -0
- package/integrations/vercel-ai-sdk/README.md +49 -0
- package/integrations/vercel-ai-sdk/a3m_provider.ts +78 -0
- package/integrations/vercel-ai-sdk/example.ts +25 -0
- package/llms-full.txt +43 -0
- package/llms.txt +9 -0
- package/mcp-server/README.md +188 -0
- package/mcp-server/package.json +29 -0
- package/mcp-server/src/index.ts +744 -0
- package/mcp-server/tsconfig.json +19 -0
- package/package.json +3 -3
- package/proxy/README.md +227 -0
- package/proxy/package-lock.json +831 -0
- package/proxy/package.json +17 -0
- package/proxy/rate-limit.js +145 -0
- package/proxy/rate-limit.test.js +311 -0
- package/proxy/server.js +970 -0
- package/scripts/banner.js +29 -0
- package/scripts/compare-providers.sh +230 -0
- package/scripts/cross_post.py +443 -0
- package/scripts/publish_fcc.py +106 -0
- package/scripts/push-to-gitee.sh +52 -0
- package/src/tui/dashboard.ts +13 -0
- package/tests/__mocks__/tokenUtils.ts +22 -0
- package/tests/memory/episodicMemory.test.ts +227 -0
- package/tests/package-lock.json +1628 -0
- package/tests/package.json +18 -0
- package/tests/routing/ensembleVoting.test.ts +236 -0
- package/tests/routing/providerRetry.test.ts +360 -0
- package/tests/routing/queryTypePresets.test.ts +206 -0
- package/tests/tsconfig.json +21 -0
- package/tests/vitest.config.ts +18 -0
- package/.env +0 -2
package/cli/a3m
ADDED
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* A3M Router — Standalone CLI
|
|
4
|
+
* =============================
|
|
5
|
+
* Zero dependencies. Just: chmod +x a3m && ./a3m "your query"
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - 14 built-in providers (auto-detects API keys from env)
|
|
9
|
+
* - Parallel ensemble mode (run N providers simultaneously)
|
|
10
|
+
* - Automatic fallback (try A -> B -> C on failure)
|
|
11
|
+
* - Rich ANSI output without chalk
|
|
12
|
+
* - Single file, no npm install required
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* ./a3m "what is the capital of France?"
|
|
16
|
+
* ./a3m --provider groq "tell me a joke"
|
|
17
|
+
* ./a3m --ensemble "compare responses across providers"
|
|
18
|
+
* ./a3m --fallback "try providers until one works"
|
|
19
|
+
* ./a3m --list-providers
|
|
20
|
+
* ./a3m --help
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// =====================================================================
|
|
24
|
+
// ANSI COLORS (no chalk dependency)
|
|
25
|
+
// =====================================================================
|
|
26
|
+
const C = {
|
|
27
|
+
reset: '\x1b[0m',
|
|
28
|
+
bold: '\x1b[1m',
|
|
29
|
+
dim: '\x1b[2m',
|
|
30
|
+
italic: '\x1b[3m',
|
|
31
|
+
ul: '\x1b[4m',
|
|
32
|
+
|
|
33
|
+
red: '\x1b[31m',
|
|
34
|
+
green: '\x1b[32m',
|
|
35
|
+
yellow: '\x1b[33m',
|
|
36
|
+
blue: '\x1b[34m',
|
|
37
|
+
magenta: '\x1b[35m',
|
|
38
|
+
cyan: '\x1b[36m',
|
|
39
|
+
white: '\x1b[37m',
|
|
40
|
+
|
|
41
|
+
bgRed: '\x1b[41m',
|
|
42
|
+
bgGreen: '\x1b[42m',
|
|
43
|
+
bgYellow: '\x1b[43m',
|
|
44
|
+
bgBlue: '\x1b[44m',
|
|
45
|
+
bgMagenta: '\x1b[45m',
|
|
46
|
+
bgCyan: '\x1b[46m',
|
|
47
|
+
|
|
48
|
+
// Bright variants
|
|
49
|
+
brightRed: '\x1b[91m',
|
|
50
|
+
brightGreen: '\x1b[92m',
|
|
51
|
+
brightYellow: '\x1b[93m',
|
|
52
|
+
brightBlue: '\x1b[94m',
|
|
53
|
+
brightMagenta: '\x1b[95m',
|
|
54
|
+
brightCyan: '\x1b[96m',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function c(code, text) {
|
|
58
|
+
return code + text + C.reset;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function colorize(providerName, text) {
|
|
62
|
+
const colors = [C.cyan, C.green, C.yellow, C.magenta, C.brightBlue, C.brightCyan, C.brightGreen, C.brightMagenta];
|
|
63
|
+
let hash = 0;
|
|
64
|
+
for (let i = 0; i < providerName.length; i++) hash = ((hash << 5) - hash) + providerName.charCodeAt(i);
|
|
65
|
+
return colors[Math.abs(hash) % colors.length] + text + C.reset;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// =====================================================================
|
|
69
|
+
// PROVIDER DEFINITIONS
|
|
70
|
+
// =====================================================================
|
|
71
|
+
// Each provider has:
|
|
72
|
+
// - id: short name for --provider flag
|
|
73
|
+
// - name: human-readable
|
|
74
|
+
// - envKey: env var that holds the API key
|
|
75
|
+
// - baseUrl: API endpoint
|
|
76
|
+
// - model: default model
|
|
77
|
+
// - format: 'openai' (standard chat completions) | 'anthropic' | 'google'
|
|
78
|
+
// - headers(): returns {Authorization, Content-Type} etc.
|
|
79
|
+
// - parseResponse(json): extracts {content, model, usage}
|
|
80
|
+
// =====================================================================
|
|
81
|
+
|
|
82
|
+
function getEnv(key, fallback) {
|
|
83
|
+
// Also checks .env file in cwd
|
|
84
|
+
const val = process.env[key];
|
|
85
|
+
if (val && val.length > 0 && val !== '...' && val !== 'sk-...' && val !== 'gsk_...' && val !== 'nvapi-...') return val;
|
|
86
|
+
return fallback || '';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function makeProvider(id, name, envKey, baseUrl, model, opts) {
|
|
90
|
+
opts = opts || {};
|
|
91
|
+
return {
|
|
92
|
+
id,
|
|
93
|
+
name,
|
|
94
|
+
envKey,
|
|
95
|
+
baseUrl,
|
|
96
|
+
model: opts.model || model,
|
|
97
|
+
format: opts.format || 'openai',
|
|
98
|
+
maxTokens: opts.maxTokens || 2048,
|
|
99
|
+
priority: opts.priority || 50,
|
|
100
|
+
tier: opts.tier || 'unknown',
|
|
101
|
+
costPerK: opts.costPerK || { input: 0, output: 0 },
|
|
102
|
+
getApiKey() {
|
|
103
|
+
return getEnv(this.envKey);
|
|
104
|
+
},
|
|
105
|
+
isAvailable() {
|
|
106
|
+
return !!this.getApiKey();
|
|
107
|
+
},
|
|
108
|
+
buildBody(prompt, systemPrompt) {
|
|
109
|
+
const msgs = [];
|
|
110
|
+
if (systemPrompt) msgs.push({ role: 'system', content: systemPrompt });
|
|
111
|
+
msgs.push({ role: 'user', content: prompt });
|
|
112
|
+
return { model: this.model, messages: msgs, max_tokens: this.maxTokens };
|
|
113
|
+
},
|
|
114
|
+
buildHeaders() {
|
|
115
|
+
return {
|
|
116
|
+
'Authorization': 'Bearer ' + this.getApiKey(),
|
|
117
|
+
'Content-Type': 'application/json',
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
parseResponse(json) {
|
|
121
|
+
if (json.error) throw new Error(json.error.message || JSON.stringify(json.error));
|
|
122
|
+
const choice = json.choices && json.choices[0];
|
|
123
|
+
if (!choice) throw new Error('No response choices returned');
|
|
124
|
+
return {
|
|
125
|
+
content: choice.message ? (choice.message.content || '') : '',
|
|
126
|
+
model: json.model || this.model,
|
|
127
|
+
usage: json.usage || {},
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function makeGoogleProvider(id, name, envKey, baseUrl, model, opts) {
|
|
134
|
+
opts = opts || {};
|
|
135
|
+
const p = makeProvider(id, name, envKey, baseUrl, model, opts);
|
|
136
|
+
p.format = 'google';
|
|
137
|
+
p.buildHeaders = function () {
|
|
138
|
+
return { 'Content-Type': 'application/json' };
|
|
139
|
+
};
|
|
140
|
+
p.buildBody = function (prompt, systemPrompt) {
|
|
141
|
+
const contents = [];
|
|
142
|
+
if (systemPrompt) {
|
|
143
|
+
contents.push({ role: 'user', parts: [{ text: systemPrompt + '\n\n' + prompt }] });
|
|
144
|
+
} else {
|
|
145
|
+
contents.push({ role: 'user', parts: [{ text: prompt }] });
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
contents,
|
|
149
|
+
generationConfig: { maxOutputTokens: this.maxTokens },
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
p.parseResponse = function (json) {
|
|
153
|
+
if (json.error) throw new Error(json.error.message || JSON.stringify(json.error));
|
|
154
|
+
const candidate = json.candidates && json.candidates[0];
|
|
155
|
+
if (!candidate) throw new Error('No candidates returned');
|
|
156
|
+
const parts = candidate.content && candidate.content.parts;
|
|
157
|
+
const text = parts ? parts.map(function (p) { return p.text || ''; }).join('') : '';
|
|
158
|
+
return {
|
|
159
|
+
content: text,
|
|
160
|
+
model: json.modelVersion || this.model,
|
|
161
|
+
usage: {},
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
return p;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function makeAnthropicProvider(id, name, envKey, baseUrl, model, opts) {
|
|
168
|
+
opts = opts || {};
|
|
169
|
+
const p = makeProvider(id, name, envKey, baseUrl, model, opts);
|
|
170
|
+
p.format = 'anthropic';
|
|
171
|
+
p.buildHeaders = function () {
|
|
172
|
+
return {
|
|
173
|
+
'x-api-key': this.getApiKey(),
|
|
174
|
+
'anthropic-version': '2023-06-01',
|
|
175
|
+
'Content-Type': 'application/json',
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
p.buildBody = function (prompt, systemPrompt) {
|
|
179
|
+
return {
|
|
180
|
+
model: this.model,
|
|
181
|
+
max_tokens: this.maxTokens,
|
|
182
|
+
system: systemPrompt || '',
|
|
183
|
+
messages: [{ role: 'user', content: prompt }],
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
p.parseResponse = function (json) {
|
|
187
|
+
if (json.error) throw new Error(json.error.message || JSON.stringify(json.error));
|
|
188
|
+
return {
|
|
189
|
+
content: json.content && json.content[0] ? json.content[0].text : '',
|
|
190
|
+
model: json.model || this.model,
|
|
191
|
+
usage: json.usage || {},
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
return p;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function makeNvidiaProvider(id, name, envKey, baseUrl, model, opts) {
|
|
198
|
+
opts = opts || {};
|
|
199
|
+
const p = makeProvider(id, name, envKey, baseUrl, model, opts);
|
|
200
|
+
// NVIDIA uses nvapi- prefix but still Bearer auth
|
|
201
|
+
p.getApiKey = function () {
|
|
202
|
+
const val = getEnv('NVIDIA_API_KEY');
|
|
203
|
+
return val;
|
|
204
|
+
};
|
|
205
|
+
return p;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// =====================================================================
|
|
209
|
+
// ALL PROVIDERS
|
|
210
|
+
// =====================================================================
|
|
211
|
+
|
|
212
|
+
const PROVIDERS = [
|
|
213
|
+
// -- Fast inference (cheap/free) --
|
|
214
|
+
{
|
|
215
|
+
id: 'groq',
|
|
216
|
+
name: 'Groq',
|
|
217
|
+
envKey: 'GROQ_API_KEY',
|
|
218
|
+
baseUrl: 'https://api.groq.com/openai/v1/chat/completions',
|
|
219
|
+
model: 'llama-3.3-70b-versatile',
|
|
220
|
+
priority: 1,
|
|
221
|
+
tier: 'cheap',
|
|
222
|
+
costPerK: { input: 0.59, output: 0.79 },
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: 'cerebras',
|
|
226
|
+
name: 'Cerebras',
|
|
227
|
+
envKey: 'CEREBRAS_API_KEY',
|
|
228
|
+
baseUrl: 'https://api.cerebras.ai/v1/chat/completions',
|
|
229
|
+
model: 'llama-3.3-70b',
|
|
230
|
+
priority: 2,
|
|
231
|
+
tier: 'cheap',
|
|
232
|
+
costPerK: { input: 0.60, output: 0.60 },
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: 'deepinfra',
|
|
236
|
+
name: 'DeepInfra',
|
|
237
|
+
envKey: 'DEEPINFRA_API_KEY',
|
|
238
|
+
baseUrl: 'https://api.deepinfra.com/v1/openai/chat/completions',
|
|
239
|
+
model: 'meta-llama/Meta-Llama-3.1-70B-Instruct',
|
|
240
|
+
priority: 3,
|
|
241
|
+
tier: 'cheap',
|
|
242
|
+
costPerK: { input: 0.05, output: 0.05 },
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
id: 'together',
|
|
246
|
+
name: 'Together AI',
|
|
247
|
+
envKey: 'TOGETHER_API_KEY',
|
|
248
|
+
baseUrl: 'https://api.together.xyz/v1/chat/completions',
|
|
249
|
+
model: 'meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo',
|
|
250
|
+
priority: 4,
|
|
251
|
+
tier: 'cheap',
|
|
252
|
+
costPerK: { input: 0.18, output: 0.18 },
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
id: 'fireworks',
|
|
256
|
+
name: 'Fireworks AI',
|
|
257
|
+
envKey: 'FIREWORKS_API_KEY',
|
|
258
|
+
baseUrl: 'https://api.fireworks.ai/inference/v1/chat/completions',
|
|
259
|
+
model: 'accounts/fireworks/models/llama-v3p1-70b-instruct',
|
|
260
|
+
priority: 5,
|
|
261
|
+
tier: 'cheap',
|
|
262
|
+
costPerK: { input: 0.20, output: 0.20 },
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
// -- Free tier providers --
|
|
266
|
+
{
|
|
267
|
+
id: 'nvidia',
|
|
268
|
+
name: 'NVIDIA NIM',
|
|
269
|
+
envKey: 'NVIDIA_API_KEY',
|
|
270
|
+
baseUrl: 'https://integrate.api.nvidia.com/v1/chat/completions',
|
|
271
|
+
model: 'meta/llama-3.3-70b-instruct',
|
|
272
|
+
priority: 6,
|
|
273
|
+
tier: 'free',
|
|
274
|
+
costPerK: { input: 0, output: 0 },
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
id: 'google',
|
|
278
|
+
name: 'Google Gemini',
|
|
279
|
+
envKey: 'GOOGLE_API_KEY',
|
|
280
|
+
baseUrl: 'https://generativelanguage.googleapis.com/v1beta/models/',
|
|
281
|
+
model: 'gemini-2.5-flash',
|
|
282
|
+
priority: 7,
|
|
283
|
+
tier: 'free',
|
|
284
|
+
format: 'google',
|
|
285
|
+
costPerK: { input: 0, output: 0 },
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
id: 'openrouter',
|
|
289
|
+
name: 'OpenRouter',
|
|
290
|
+
envKey: 'OPENROUTER_API_KEY',
|
|
291
|
+
baseUrl: 'https://openrouter.ai/api/v1/chat/completions',
|
|
292
|
+
model: 'openai/gpt-4o',
|
|
293
|
+
priority: 8,
|
|
294
|
+
tier: 'cheap',
|
|
295
|
+
costPerK: { input: 0, output: 0 },
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
// -- Mid tier --
|
|
299
|
+
{
|
|
300
|
+
id: 'mistral',
|
|
301
|
+
name: 'Mistral',
|
|
302
|
+
envKey: 'MISTRAL_API_KEY',
|
|
303
|
+
baseUrl: 'https://api.mistral.ai/v1/chat/completions',
|
|
304
|
+
model: 'mistral-large-latest',
|
|
305
|
+
priority: 9,
|
|
306
|
+
tier: 'mid',
|
|
307
|
+
costPerK: { input: 0.20, output: 0.60 },
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
id: 'deepseek',
|
|
311
|
+
name: 'DeepSeek',
|
|
312
|
+
envKey: 'DEEPSEEK_API_KEY',
|
|
313
|
+
baseUrl: 'https://api.deepseek.com/v1/chat/completions',
|
|
314
|
+
model: 'deepseek-v4-flash',
|
|
315
|
+
priority: 10,
|
|
316
|
+
tier: 'mid',
|
|
317
|
+
costPerK: { input: 0.14, output: 0.28 },
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
id: 'perplexity',
|
|
321
|
+
name: 'Perplexity',
|
|
322
|
+
envKey: 'PERPLEXITY_API_KEY',
|
|
323
|
+
baseUrl: 'https://api.perplexity.ai/chat/completions',
|
|
324
|
+
model: 'llama-3.1-sonar-large-128k-online',
|
|
325
|
+
priority: 11,
|
|
326
|
+
tier: 'mid',
|
|
327
|
+
costPerK: { input: 1.0, output: 1.0 },
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
// -- Premium --
|
|
331
|
+
{
|
|
332
|
+
id: 'openai',
|
|
333
|
+
name: 'OpenAI',
|
|
334
|
+
envKey: 'OPENAI_API_KEY',
|
|
335
|
+
baseUrl: 'https://api.openai.com/v1/chat/completions',
|
|
336
|
+
model: 'gpt-4o',
|
|
337
|
+
priority: 12,
|
|
338
|
+
tier: 'premium',
|
|
339
|
+
costPerK: { input: 2.50, output: 10.0 },
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
id: 'anthropic',
|
|
343
|
+
name: 'Anthropic',
|
|
344
|
+
envKey: 'ANTHROPIC_API_KEY',
|
|
345
|
+
baseUrl: 'https://api.anthropic.com/v1/messages',
|
|
346
|
+
model: 'claude-sonnet-4-20250514',
|
|
347
|
+
priority: 13,
|
|
348
|
+
tier: 'premium',
|
|
349
|
+
format: 'anthropic',
|
|
350
|
+
costPerK: { input: 3.0, output: 15.0 },
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
id: 'xai',
|
|
354
|
+
name: 'xAI Grok',
|
|
355
|
+
envKey: 'XAI_API_KEY',
|
|
356
|
+
baseUrl: 'https://api.x.ai/v1/chat/completions',
|
|
357
|
+
model: 'grok-3',
|
|
358
|
+
priority: 14,
|
|
359
|
+
tier: 'premium',
|
|
360
|
+
costPerK: { input: 3.0, output: 15.0 },
|
|
361
|
+
},
|
|
362
|
+
];
|
|
363
|
+
|
|
364
|
+
// =====================================================================
|
|
365
|
+
// PROVIDER FACTORY
|
|
366
|
+
// =====================================================================
|
|
367
|
+
|
|
368
|
+
function buildProvider(cfg) {
|
|
369
|
+
if (cfg.format === 'google') {
|
|
370
|
+
const url = cfg.baseUrl + cfg.model + ':generateContent?key=' + getEnv(cfg.envKey);
|
|
371
|
+
return makeGoogleProvider(cfg.id, cfg.name, cfg.envKey, url, cfg.model, cfg);
|
|
372
|
+
}
|
|
373
|
+
if (cfg.format === 'anthropic') {
|
|
374
|
+
return makeAnthropicProvider(cfg.id, cfg.name, cfg.envKey, cfg.baseUrl, cfg.model, cfg);
|
|
375
|
+
}
|
|
376
|
+
if (cfg.id === 'nvidia') {
|
|
377
|
+
return makeNvidiaProvider(cfg.id, cfg.name, cfg.envKey, cfg.baseUrl, cfg.model, cfg);
|
|
378
|
+
}
|
|
379
|
+
return makeProvider(cfg.id, cfg.name, cfg.envKey, cfg.baseUrl, cfg.model, cfg);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function getAvailableProviders() {
|
|
383
|
+
return PROVIDERS
|
|
384
|
+
.filter(function (p) { return buildProvider(p).isAvailable(); })
|
|
385
|
+
.sort(function (a, b) { return a.priority - b.priority; });
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// =====================================================================
|
|
389
|
+
// HTTP REQUEST (no dependencies)
|
|
390
|
+
// =====================================================================
|
|
391
|
+
|
|
392
|
+
function httpsRequest(url, method, headers, body, timeout) {
|
|
393
|
+
return new Promise(function (resolve, reject) {
|
|
394
|
+
timeout = timeout || 30000;
|
|
395
|
+
var u = new URL(url);
|
|
396
|
+
var mod = u.protocol === 'https:' ? require('https') : require('http');
|
|
397
|
+
|
|
398
|
+
var options = {
|
|
399
|
+
hostname: u.hostname,
|
|
400
|
+
port: u.port || (u.protocol === 'https:' ? 443 : 80),
|
|
401
|
+
path: u.pathname + u.search,
|
|
402
|
+
method: method || 'GET',
|
|
403
|
+
headers: headers || {},
|
|
404
|
+
timeout: timeout,
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
var req = mod.request(options, function (res) {
|
|
408
|
+
var data = '';
|
|
409
|
+
res.on('data', function (chunk) { data += chunk; });
|
|
410
|
+
res.on('end', function () {
|
|
411
|
+
try {
|
|
412
|
+
var parsed = JSON.parse(data);
|
|
413
|
+
resolve({ status: res.statusCode, headers: res.headers, data: parsed, raw: data });
|
|
414
|
+
} catch (e) {
|
|
415
|
+
reject(new Error('Invalid JSON response (' + res.statusCode + '): ' + data.substring(0, 200)));
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
req.on('error', function (e) { reject(e); });
|
|
421
|
+
req.on('timeout', function () { req.destroy(); reject(new Error('Request timed out after ' + timeout + 'ms')); });
|
|
422
|
+
|
|
423
|
+
if (body) {
|
|
424
|
+
req.write(typeof body === 'string' ? body : JSON.stringify(body));
|
|
425
|
+
}
|
|
426
|
+
req.end();
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// =====================================================================
|
|
431
|
+
// CALL A SINGLE PROVIDER
|
|
432
|
+
// =====================================================================
|
|
433
|
+
|
|
434
|
+
async function callProvider(providerCfg, prompt, systemPrompt) {
|
|
435
|
+
var provider = buildProvider(providerCfg);
|
|
436
|
+
var startTime = Date.now();
|
|
437
|
+
|
|
438
|
+
var body = provider.buildBody(prompt, systemPrompt);
|
|
439
|
+
var headers = provider.buildHeaders();
|
|
440
|
+
|
|
441
|
+
var resp = await httpsRequest(provider.baseUrl, 'POST', headers, body, 60000);
|
|
442
|
+
|
|
443
|
+
if (resp.status >= 400) {
|
|
444
|
+
var errMsg = resp.data && resp.data.error ? (resp.data.error.message || JSON.stringify(resp.data.error)) : ('HTTP ' + resp.status);
|
|
445
|
+
throw new Error(errMsg);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
var parsed = provider.parseResponse(resp.data);
|
|
449
|
+
var latency = Date.now() - startTime;
|
|
450
|
+
|
|
451
|
+
var usage = parsed.usage || {};
|
|
452
|
+
var promptTokens = usage.prompt_tokens || 0;
|
|
453
|
+
var completionTokens = usage.completion_tokens || 0;
|
|
454
|
+
var totalTokens = usage.total_tokens || (promptTokens + completionTokens) || estimateTokens(prompt) + estimateTokens(parsed.content);
|
|
455
|
+
|
|
456
|
+
var cost = (promptTokens / 1000000 * providerCfg.costPerK.input) +
|
|
457
|
+
(completionTokens / 1000000 * providerCfg.costPerK.output);
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
provider: providerCfg.name,
|
|
461
|
+
model: parsed.model,
|
|
462
|
+
content: parsed.content || '',
|
|
463
|
+
latency: latency,
|
|
464
|
+
totalTokens: totalTokens,
|
|
465
|
+
cost: cost,
|
|
466
|
+
usage: usage,
|
|
467
|
+
success: true,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// =====================================================================
|
|
472
|
+
// TOKEN ESTIMATION (rough)
|
|
473
|
+
// =====================================================================
|
|
474
|
+
|
|
475
|
+
function estimateTokens(text) {
|
|
476
|
+
if (!text) return 0;
|
|
477
|
+
// Rough: ~4 chars per token for English
|
|
478
|
+
return Math.ceil(text.length / 4);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// =====================================================================
|
|
482
|
+
// FALLBACK MODE (try providers in priority order)
|
|
483
|
+
// =====================================================================
|
|
484
|
+
|
|
485
|
+
async function withFallback(prompt, opts) {
|
|
486
|
+
opts = opts || {};
|
|
487
|
+
var available = getAvailableProviders();
|
|
488
|
+
if (opts.providerFilter) {
|
|
489
|
+
available = available.filter(function (p) { return opts.providerFilter === p.id; });
|
|
490
|
+
}
|
|
491
|
+
if (available.length === 0) {
|
|
492
|
+
return { success: false, error: 'No providers available. Set at least one API key.', errors: [] };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
var errors = [];
|
|
496
|
+
var systemPrompt = opts.systemPrompt || '';
|
|
497
|
+
|
|
498
|
+
// If specific provider requested, try just that one first
|
|
499
|
+
if (opts.providerFilter) {
|
|
500
|
+
var target = available[0];
|
|
501
|
+
try {
|
|
502
|
+
var result = await callProvider(target, prompt, systemPrompt);
|
|
503
|
+
return result;
|
|
504
|
+
} catch (e) {
|
|
505
|
+
errors.push({ provider: target.name, error: e.message });
|
|
506
|
+
return { success: false, error: e.message, errors: errors, provider: target.name };
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Try each provider in priority order
|
|
511
|
+
for (var i = 0; i < available.length; i++) {
|
|
512
|
+
var p = available[i];
|
|
513
|
+
try {
|
|
514
|
+
var result = await callProvider(p, prompt, systemPrompt);
|
|
515
|
+
result.fallbackAttempts = i;
|
|
516
|
+
return result;
|
|
517
|
+
} catch (e) {
|
|
518
|
+
var emsg = e.message.substring(0, 200);
|
|
519
|
+
errors.push({ provider: p.name, error: emsg });
|
|
520
|
+
// Continue to next provider
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return { success: false, error: 'All providers failed', errors: errors };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// =====================================================================
|
|
528
|
+
// ENSEMBLE MODE (run N providers in parallel, merge results)
|
|
529
|
+
// =====================================================================
|
|
530
|
+
|
|
531
|
+
async function runEnsemble(prompt, opts) {
|
|
532
|
+
opts = opts || {};
|
|
533
|
+
var available = getAvailableProviders();
|
|
534
|
+
var systemPrompt = opts.systemPrompt || '';
|
|
535
|
+
|
|
536
|
+
// Limit to max providers
|
|
537
|
+
var maxProviders = opts.maxProviders || 5;
|
|
538
|
+
var selected = available.slice(0, maxProviders);
|
|
539
|
+
|
|
540
|
+
if (selected.length === 0) {
|
|
541
|
+
return { success: false, error: 'No providers available.', results: [] };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Fire all requests in parallel
|
|
545
|
+
var results = await Promise.allSettled(
|
|
546
|
+
selected.map(function (p) {
|
|
547
|
+
return callProvider(p, prompt, systemPrompt);
|
|
548
|
+
})
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
var successes = [];
|
|
552
|
+
var failures = [];
|
|
553
|
+
|
|
554
|
+
for (var i = 0; i < results.length; i++) {
|
|
555
|
+
var r = results[i];
|
|
556
|
+
if (r.status === 'fulfilled') {
|
|
557
|
+
successes.push(r.value);
|
|
558
|
+
} else {
|
|
559
|
+
failures.push({ provider: selected[i].name, error: r.reason.message || String(r.reason) });
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
success: successes.length > 0,
|
|
565
|
+
totalResults: successes.length,
|
|
566
|
+
totalFailures: failures.length,
|
|
567
|
+
results: successes,
|
|
568
|
+
failures: failures,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// =====================================================================
|
|
573
|
+
// OUTPUT FORMATTING
|
|
574
|
+
// =====================================================================
|
|
575
|
+
|
|
576
|
+
function printBanner() {
|
|
577
|
+
var banner = '';
|
|
578
|
+
banner += c(C.cyan, ' ╔═══════════════════════════════════════════════╗\n');
|
|
579
|
+
banner += c(C.cyan, ' ║') + c(C.bold + C.brightCyan, ' A3M Router — Adaptive Multi-Model Router ') + c(C.dim + C.cyan, ' v2.13 ') + c(C.cyan, ' ║\n');
|
|
580
|
+
banner += c(C.cyan, ' ║') + c(C.dim + C.cyan, ' Parallel multi-LLM execution · 14 providers ') + c(C.cyan, ' ║\n');
|
|
581
|
+
banner += c(C.cyan, ' ╚═══════════════════════════════════════════════╝\n');
|
|
582
|
+
console.log(banner);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function printHelp() {
|
|
586
|
+
printBanner();
|
|
587
|
+
console.log(c(C.bold, ' USAGE:'));
|
|
588
|
+
console.log(' ./a3m ' + c(C.cyan, '"your query"') + ' Route query to best available provider');
|
|
589
|
+
console.log(' ./a3m ' + c(C.green, '--provider groq') + ' ' + c(C.cyan, '"query"') + ' Use a specific provider');
|
|
590
|
+
console.log(' ./a3m ' + c(C.yellow, '--ensemble') + ' ' + c(C.cyan, '"query"') + ' Run ALL providers in parallel, compare results');
|
|
591
|
+
console.log(' ./a3m ' + c(C.magenta, '--fallback') + ' ' + c(C.cyan, '"query"') + ' Try providers in priority order until one works');
|
|
592
|
+
console.log(' ./a3m ' + c(C.brightBlue, '--list-providers') + ' Show all providers and their status');
|
|
593
|
+
console.log(' ./a3m ' + c(C.blue, '--help') + ' Show this help');
|
|
594
|
+
console.log('');
|
|
595
|
+
console.log(c(C.bold, ' EXAMPLES:'));
|
|
596
|
+
console.log(' ./a3m "explain quantum computing"');
|
|
597
|
+
console.log(' ./a3m --provider groq "write a poem"');
|
|
598
|
+
console.log(' ./a3m --ensemble "compare these models for me"');
|
|
599
|
+
console.log(' ./a3m --fallback "try until one works"');
|
|
600
|
+
console.log('');
|
|
601
|
+
console.log(c(C.bold, ' CONFIGURATION:'));
|
|
602
|
+
console.log(' Set any of these env vars (or in .env file):');
|
|
603
|
+
console.log(' ' + c(C.dim, 'GROQ_API_KEY CEREBRAS_API_KEY MISTRAL_API_KEY'));
|
|
604
|
+
console.log(' ' + c(C.dim, 'NVIDIA_API_KEY GOOGLE_API_KEY OPENAI_API_KEY'));
|
|
605
|
+
console.log(' ' + c(C.dim, 'ANTHROPIC_API_KEY DEEPSEEK_API_KEY DEEPINFRA_API_KEY'));
|
|
606
|
+
console.log(' ' + c(C.dim, 'TOGETHER_API_KEY PERPLEXITY_API_KEY XAI_API_KEY'));
|
|
607
|
+
console.log(' ' + c(C.dim, 'FIREWORKS_API_KEY OPENROUTER_API_KEY'));
|
|
608
|
+
console.log('');
|
|
609
|
+
process.exit(0);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function printProviders() {
|
|
613
|
+
printBanner();
|
|
614
|
+
console.log(c(C.bold, ' AVAILABLE PROVIDERS:\n'));
|
|
615
|
+
|
|
616
|
+
var allProviders = PROVIDERS;
|
|
617
|
+
var available = getAvailableProviders();
|
|
618
|
+
var availableIds = {};
|
|
619
|
+
for (var i = 0; i < available.length; i++) availableIds[available[i].id] = true;
|
|
620
|
+
|
|
621
|
+
for (var pi = 0; pi < allProviders.length; pi++) {
|
|
622
|
+
var p = allProviders[pi];
|
|
623
|
+
var avail = availableIds[p.id];
|
|
624
|
+
var statusStr = avail ? c(C.green, ' ✅') : c(C.dim + C.white, ' ❌');
|
|
625
|
+
var keyStr = avail ? c(C.green, ' ✅ KEY SET') : (function () {
|
|
626
|
+
// Check if env var exists at all
|
|
627
|
+
var val = process.env[p.envKey];
|
|
628
|
+
if (val) return c(C.yellow, ' ⚠ no key');
|
|
629
|
+
return c(C.dim, ' not set');
|
|
630
|
+
})();
|
|
631
|
+
var tierColor = p.tier === 'free' ? C.green : p.tier === 'cheap' ? C.cyan : p.tier === 'mid' ? C.yellow : C.magenta;
|
|
632
|
+
var modelStr = p.model.length > 35 ? p.model.substring(0, 33) + '..' : p.model;
|
|
633
|
+
|
|
634
|
+
console.log(
|
|
635
|
+
statusStr + ' ' +
|
|
636
|
+
c(C.bold, p.name.padEnd(18)) +
|
|
637
|
+
c(tierColor, ('[' + p.tier + ']').padEnd(8)) +
|
|
638
|
+
c(C.dim, modelStr.padEnd(38)) +
|
|
639
|
+
c(C.dim, '#' + p.envKey) + ' ' +
|
|
640
|
+
keyStr
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
console.log('\n ' + c(C.bold, String(available.length)) + ' of ' + allProviders.length + ' providers available\n');
|
|
645
|
+
process.exit(0);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function printResult(result, verbose) {
|
|
649
|
+
if (!result.success) {
|
|
650
|
+
console.log(c(C.red, ' ERROR: ') + result.error);
|
|
651
|
+
if (result.errors && result.errors.length > 0) {
|
|
652
|
+
console.log('');
|
|
653
|
+
for (var i = 0; i < result.errors.length; i++) {
|
|
654
|
+
var e = result.errors[i];
|
|
655
|
+
console.log(' ' + c(C.yellow, e.provider + ':') + ' ' + c(C.dim, e.error));
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
var providerColor = colorize(result.provider, result.provider);
|
|
662
|
+
|
|
663
|
+
console.log('');
|
|
664
|
+
console.log(' ' + c(C.dim, '┌─') + c(C.bold, ' A3M Response ') + c(C.dim, '─────────────────────────────────────────────'));
|
|
665
|
+
console.log(' ' + c(C.dim, '│'));
|
|
666
|
+
console.log(' ' + c(C.dim, '│ Provider: ') + providerColor);
|
|
667
|
+
console.log(' ' + c(C.dim, '│ Model: ') + c(C.italic, result.model));
|
|
668
|
+
console.log(' ' + c(C.dim, '│ Latency: ') + c(C.cyan, result.latency + 'ms'));
|
|
669
|
+
console.log(' ' + c(C.dim, '│ Tokens: ') + c(C.yellow, String(result.totalTokens)));
|
|
670
|
+
console.log(' ' + c(C.dim, '│ Cost: ') + c(C.magenta, '$' + result.cost.toFixed(6)));
|
|
671
|
+
if (result.fallbackAttempts !== undefined && result.fallbackAttempts > 0) {
|
|
672
|
+
console.log(' ' + c(C.dim, '│ Fallback: ') + c(C.yellow, 'attempted ' + result.fallbackAttempts + ' provider(s) before success'));
|
|
673
|
+
}
|
|
674
|
+
console.log(' ' + c(C.dim, '│'));
|
|
675
|
+
console.log(' ' + c(C.dim, '│ Response:'));
|
|
676
|
+
console.log(' ' + c(C.dim, '│'));
|
|
677
|
+
var lines = (result.content || '').split('\n');
|
|
678
|
+
for (var li = 0; li < lines.length; li++) {
|
|
679
|
+
console.log(' ' + c(C.dim, '│ ') + lines[li]);
|
|
680
|
+
}
|
|
681
|
+
console.log(' ' + c(C.dim, '│'));
|
|
682
|
+
console.log(' ' + c(C.dim, '└──────────────────────────────────────────────────────'));
|
|
683
|
+
console.log('');
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function printEnsembleResults(ensembleResult) {
|
|
687
|
+
console.log('');
|
|
688
|
+
console.log(' ' + c(C.dim, '┌─') + c(C.bold, ' A3M Ensemble (') + c(C.cyan, String(ensembleResult.totalResults)) + c(C.bold, ' responses, ') + c(C.yellow, String(ensembleResult.totalFailures)) + c(C.bold, ' failed) ') + c(C.dim, '────'));
|
|
689
|
+
console.log('');
|
|
690
|
+
|
|
691
|
+
var results = ensembleResult.results || [];
|
|
692
|
+
|
|
693
|
+
for (var ri = 0; ri < results.length; ri++) {
|
|
694
|
+
var r = results[ri];
|
|
695
|
+
var pColor = colorize(r.provider, r.provider);
|
|
696
|
+
|
|
697
|
+
console.log(' ' + c(C.dim, '── ') + c(C.bold, '[' + String(ri + 1) + '] ') + pColor + c(C.dim, ' (') + r.model + ', ' + r.latency + 'ms, ' + (r.cost > 0 ? '$' + r.cost.toFixed(6) : 'free') + ')');
|
|
698
|
+
console.log('');
|
|
699
|
+
|
|
700
|
+
var lines2 = (r.content || '').substring(0, 1000).split('\n');
|
|
701
|
+
for (var lj = 0; lj < Math.min(lines2.length, 20); lj++) {
|
|
702
|
+
console.log(' ' + lines2[lj]);
|
|
703
|
+
}
|
|
704
|
+
if (r.content.length > 1000) {
|
|
705
|
+
console.log(c(C.dim, ' ... (' + (r.content.length - 1000) + ' more chars)'));
|
|
706
|
+
}
|
|
707
|
+
console.log('');
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (ensembleResult.failures && ensembleResult.failures.length > 0) {
|
|
711
|
+
console.log(' ' + c(C.yellow, ' Failed:'));
|
|
712
|
+
for (var fi = 0; fi < ensembleResult.failures.length; fi++) {
|
|
713
|
+
console.log(' ' + c(C.red, '✗ ') + c(C.bold, ensembleResult.failures[fi].provider) + c(C.dim, ': ' + ensembleResult.failures[fi].error.substring(0, 120)));
|
|
714
|
+
}
|
|
715
|
+
console.log('');
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
console.log(' ' + c(C.dim, '└──────────────────────────────────────────────────────'));
|
|
719
|
+
console.log('');
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// =====================================================================
|
|
723
|
+
// MAIN CLI
|
|
724
|
+
// =====================================================================
|
|
725
|
+
|
|
726
|
+
async function main() {
|
|
727
|
+
var args = process.argv.slice(2);
|
|
728
|
+
|
|
729
|
+
// Extract flags
|
|
730
|
+
var flags = { provider: null, ensemble: false, fallback: false, listProviders: false, help: false, verbose: false, maxEnsemble: 5 };
|
|
731
|
+
|
|
732
|
+
var queryParts = [];
|
|
733
|
+
|
|
734
|
+
for (var ai = 0; ai < args.length; ai++) {
|
|
735
|
+
var arg = args[ai];
|
|
736
|
+
if (arg === '--help' || arg === '-h') {
|
|
737
|
+
flags.help = true;
|
|
738
|
+
} else if (arg === '--list-providers') {
|
|
739
|
+
flags.listProviders = true;
|
|
740
|
+
} else if (arg === '--ensemble' || arg === '-e') {
|
|
741
|
+
flags.ensemble = true;
|
|
742
|
+
} else if (arg === '--fallback' || arg === '-f') {
|
|
743
|
+
flags.fallback = true;
|
|
744
|
+
} else if (arg === '--verbose' || arg === '-v') {
|
|
745
|
+
flags.verbose = true;
|
|
746
|
+
} else if (arg === '--provider' || arg === '-p') {
|
|
747
|
+
flags.provider = args[++ai];
|
|
748
|
+
} else if (arg === '--max-ensemble' || arg === '-m') {
|
|
749
|
+
flags.maxEnsemble = parseInt(args[++ai], 10) || 5;
|
|
750
|
+
} else if (arg.startsWith('--')) {
|
|
751
|
+
// unknown flag, skip
|
|
752
|
+
} else {
|
|
753
|
+
queryParts.push(arg);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (flags.help) {
|
|
758
|
+
printHelp();
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (flags.listProviders) {
|
|
763
|
+
printProviders();
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
var query = queryParts.join(' ');
|
|
768
|
+
|
|
769
|
+
// If no query, show interactive mode
|
|
770
|
+
if (!query) {
|
|
771
|
+
// Check if we should read from stdin
|
|
772
|
+
var stdin = '';
|
|
773
|
+
if (!process.stdin.isTTY) {
|
|
774
|
+
// Read from pipe
|
|
775
|
+
var chunks = [];
|
|
776
|
+
for await (var chunk of process.stdin) chunks.push(chunk);
|
|
777
|
+
stdin = Buffer.concat(chunks).toString('utf-8').trim();
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (stdin) {
|
|
781
|
+
query = stdin;
|
|
782
|
+
} else {
|
|
783
|
+
printHelp();
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Print banner
|
|
789
|
+
printBanner();
|
|
790
|
+
|
|
791
|
+
// Check if any providers available
|
|
792
|
+
var available = getAvailableProviders();
|
|
793
|
+
if (available.length === 0) {
|
|
794
|
+
console.log(c(C.red, ' ERROR: No providers available.'));
|
|
795
|
+
console.log(' Set at least one API key as an environment variable:');
|
|
796
|
+
console.log(' export GROQ_API_KEY=gsk_...');
|
|
797
|
+
console.log(' export NVIDIA_API_KEY=nvapi-...');
|
|
798
|
+
console.log(' export GOOGLE_API_KEY=...');
|
|
799
|
+
console.log('');
|
|
800
|
+
console.log(' Or run with --list-providers to see all options.\n');
|
|
801
|
+
process.exit(1);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
console.log(c(C.dim, ' Query: ') + c(C.italic, '"' + query.substring(0, 80) + (query.length > 80 ? '...' : '') + '"'));
|
|
805
|
+
console.log(c(C.dim, ' Providers available: ') + c(C.green, String(available.length)));
|
|
806
|
+
console.log('');
|
|
807
|
+
|
|
808
|
+
// Route based on mode
|
|
809
|
+
if (flags.ensemble) {
|
|
810
|
+
var ensembleResult = await runEnsemble(query, { maxProviders: flags.maxEnsemble });
|
|
811
|
+
printEnsembleResults(ensembleResult);
|
|
812
|
+
} else if (flags.fallback || flags.provider) {
|
|
813
|
+
var result = await withFallback(query, { providerFilter: flags.provider });
|
|
814
|
+
if (result.success) {
|
|
815
|
+
printResult(result, flags.verbose);
|
|
816
|
+
} else {
|
|
817
|
+
printResult(result, flags.verbose);
|
|
818
|
+
}
|
|
819
|
+
} else {
|
|
820
|
+
// Default: use highest priority available provider
|
|
821
|
+
var defaultResult = await withFallback(query, { providerFilter: available[0].id });
|
|
822
|
+
if (defaultResult.success) {
|
|
823
|
+
printResult(defaultResult, flags.verbose);
|
|
824
|
+
// Show tip about ensemble
|
|
825
|
+
console.log(c(C.dim, ' 💡 Tip: Try ') + c(C.cyan, '--ensemble') + c(C.dim, ' to compare responses from all providers.'));
|
|
826
|
+
console.log(c(C.dim, ' 💡 Tip: Try ') + c(C.green, '--provider mistral') + c(C.dim, ' to use a specific provider.'));
|
|
827
|
+
console.log('');
|
|
828
|
+
} else {
|
|
829
|
+
// Fallback: try without filter (full fallback chain)
|
|
830
|
+
console.log(c(C.yellow, ' Primary provider failed. Trying full fallback chain...\n'));
|
|
831
|
+
var fallbackResult = await withFallback(query);
|
|
832
|
+
printResult(fallbackResult, flags.verbose);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
main().catch(function (err) {
|
|
838
|
+
console.error(c(C.red, '\n UNEXPECTED ERROR: ') + err.message + '\n');
|
|
839
|
+
process.exit(1);
|
|
840
|
+
});
|