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
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* A3M Router - Generic Provider Configuration System
|
|
4
|
+
*
|
|
5
|
+
* Users can configure their available LLM providers via:
|
|
6
|
+
* 1. Environment variables (*_API_KEY patterns)
|
|
7
|
+
* 2. Config file at ~/.config/a3m-router/providers.json
|
|
8
|
+
* 3. Runtime registration via registerProvider()
|
|
9
|
+
*
|
|
10
|
+
* All provider references are generic and configurable.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
// ============================================================
|
|
17
|
+
// DEFAULT PROVIDER DEFINITIONS (generic, user-configurable)
|
|
18
|
+
// ============================================================
|
|
19
|
+
|
|
20
|
+
const DEFAULT_PROVIDERS = {
|
|
21
|
+
// ===== API Providers =====
|
|
22
|
+
groq: {
|
|
23
|
+
id: 'groq',
|
|
24
|
+
name: 'Groq',
|
|
25
|
+
baseUrl: 'https://api.groq.com/openai/v1/chat/completions',
|
|
26
|
+
apiKeyEnv: 'GROQ_API_KEY',
|
|
27
|
+
models: [
|
|
28
|
+
'llama-3.3-70b-versatile',
|
|
29
|
+
'llama-3.1-8b-instant',
|
|
30
|
+
'openai/gpt-oss-120b',
|
|
31
|
+
'openai/gpt-oss-20b',
|
|
32
|
+
'qwen/qwen3-32b',
|
|
33
|
+
'meta-llama/llama-4-scout-17b-16e-instruct',
|
|
34
|
+
],
|
|
35
|
+
costPerK: { input: 0.59, output: 0.79 },
|
|
36
|
+
type: 'api',
|
|
37
|
+
priority: 1,
|
|
38
|
+
maxTokens: 8192,
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
cerebras: {
|
|
42
|
+
id: 'cerebras',
|
|
43
|
+
name: 'Cerebras',
|
|
44
|
+
baseUrl: 'https://api.cerebras.ai/v1/chat/completions',
|
|
45
|
+
apiKeyEnv: 'CEREBRAS_API_KEY',
|
|
46
|
+
models: [
|
|
47
|
+
'llama3.1-8b',
|
|
48
|
+
'qwen-3-235b-a22b-instruct-2507',
|
|
49
|
+
'gpt-oss-120b',
|
|
50
|
+
'zai-glm-4.7',
|
|
51
|
+
],
|
|
52
|
+
costPerK: { input: 0.6, output: 0.6 },
|
|
53
|
+
type: 'api',
|
|
54
|
+
priority: 2,
|
|
55
|
+
maxTokens: 8192,
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
mistral: {
|
|
59
|
+
id: 'mistral',
|
|
60
|
+
name: 'Mistral',
|
|
61
|
+
baseUrl: 'https://api.mistral.ai/v1/chat/completions',
|
|
62
|
+
apiKeyEnv: 'MISTRAL_API_KEY',
|
|
63
|
+
models: [
|
|
64
|
+
'mistral-small-latest',
|
|
65
|
+
'mistral-medium-latest',
|
|
66
|
+
'mistral-large-latest',
|
|
67
|
+
'mistral-small-2506',
|
|
68
|
+
'devstral-small-2507',
|
|
69
|
+
'ministral-3b-latest',
|
|
70
|
+
'ministral-8b-latest',
|
|
71
|
+
'codestral-latest',
|
|
72
|
+
],
|
|
73
|
+
costPerK: { input: 0.2, output: 0.6 },
|
|
74
|
+
type: 'api',
|
|
75
|
+
priority: 3,
|
|
76
|
+
maxTokens: 8192,
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
openai: {
|
|
80
|
+
id: 'openai',
|
|
81
|
+
name: 'OpenAI',
|
|
82
|
+
baseUrl: 'https://api.openai.com/v1/chat/completions',
|
|
83
|
+
apiKeyEnv: 'OPENAI_API_KEY',
|
|
84
|
+
models: [
|
|
85
|
+
'gpt-4o',
|
|
86
|
+
'gpt-4o-mini',
|
|
87
|
+
'gpt-4-turbo',
|
|
88
|
+
'gpt-3.5-turbo',
|
|
89
|
+
],
|
|
90
|
+
costPerK: { input: 2.5, output: 10 },
|
|
91
|
+
type: 'api',
|
|
92
|
+
priority: 4,
|
|
93
|
+
maxTokens: 8192,
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
anthropic: {
|
|
97
|
+
id: 'anthropic',
|
|
98
|
+
name: 'Anthropic',
|
|
99
|
+
baseUrl: 'https://api.anthropic.com/v1/messages',
|
|
100
|
+
apiKeyEnv: 'ANTHROPIC_API_KEY',
|
|
101
|
+
models: [
|
|
102
|
+
'claude-3.5-sonnet',
|
|
103
|
+
'claude-3-opus',
|
|
104
|
+
'claude-3-haiku',
|
|
105
|
+
],
|
|
106
|
+
costPerK: { input: 3, output: 15 },
|
|
107
|
+
type: 'api',
|
|
108
|
+
priority: 5,
|
|
109
|
+
maxTokens: 8192,
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
google: {
|
|
113
|
+
id: 'google',
|
|
114
|
+
name: 'Google',
|
|
115
|
+
baseUrl: 'https://generativelanguage.googleapis.com/v1beta/models',
|
|
116
|
+
apiKeyEnv: 'GOOGLE_API_KEY',
|
|
117
|
+
models: [
|
|
118
|
+
'gemini-2.5-flash',
|
|
119
|
+
'gemini-2.5-pro',
|
|
120
|
+
'gemini-2.0-flash',
|
|
121
|
+
'gemini-1.5-flash',
|
|
122
|
+
'gemini-1.5-pro',
|
|
123
|
+
'gemma-3-27b-it',
|
|
124
|
+
],
|
|
125
|
+
costPerK: { input: 0, output: 0 }, // Free tier available
|
|
126
|
+
type: 'api',
|
|
127
|
+
priority: 6,
|
|
128
|
+
maxTokens: 8192,
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
deepseek: {
|
|
132
|
+
id: 'deepseek',
|
|
133
|
+
name: 'DeepSeek',
|
|
134
|
+
baseUrl: 'https://api.deepseek.com/v1/chat/completions',
|
|
135
|
+
apiKeyEnv: 'DEEPSEEK_API_KEY',
|
|
136
|
+
models: ['deepseek-chat', 'deepseek-reasoner'],
|
|
137
|
+
costPerK: { input: 0.14, output: 0.28 },
|
|
138
|
+
type: 'api',
|
|
139
|
+
priority: 7,
|
|
140
|
+
maxTokens: 8192,
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
// ===== CLI Providers (local tools) =====
|
|
144
|
+
opencode: {
|
|
145
|
+
id: 'opencode',
|
|
146
|
+
name: 'OpenCode',
|
|
147
|
+
cliCommand: 'opencode',
|
|
148
|
+
models: [], // Populated dynamically via `opencode models`
|
|
149
|
+
costPerK: { input: 0, output: 0 }, // Free tier available
|
|
150
|
+
type: 'cli',
|
|
151
|
+
priority: 8,
|
|
152
|
+
maxTokens: 8192,
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
commandcode: {
|
|
156
|
+
id: 'commandcode',
|
|
157
|
+
name: 'CommandCode',
|
|
158
|
+
baseUrl: 'https://api.commandcode.ai/v1',
|
|
159
|
+
apiKeyEnv: 'COMMANDCODE_API_KEY',
|
|
160
|
+
models: ['taste-1'],
|
|
161
|
+
costPerK: { input: 0, output: 0 }, // Free for now
|
|
162
|
+
type: 'cli',
|
|
163
|
+
cliCommand: 'commandcode',
|
|
164
|
+
priority: 9,
|
|
165
|
+
maxTokens: 8192,
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
// ===== Local Providers =====
|
|
169
|
+
ollama: {
|
|
170
|
+
id: 'ollama',
|
|
171
|
+
name: 'Ollama',
|
|
172
|
+
baseUrl: 'http://127.0.0.1:11434/api/generate',
|
|
173
|
+
models: [], // Populated dynamically via ollama list
|
|
174
|
+
costPerK: { input: 0, output: 0 },
|
|
175
|
+
type: 'local',
|
|
176
|
+
priority: 10,
|
|
177
|
+
maxTokens: 8192,
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
vllm: {
|
|
181
|
+
id: 'vllm',
|
|
182
|
+
name: 'vLLM',
|
|
183
|
+
baseUrl: 'http://127.0.0.1:8000/v1/chat/completions',
|
|
184
|
+
models: [],
|
|
185
|
+
costPerK: { input: 0, output: 0 },
|
|
186
|
+
type: 'local',
|
|
187
|
+
priority: 11,
|
|
188
|
+
maxTokens: 8192,
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
lmstudio: {
|
|
192
|
+
id: 'lmstudio',
|
|
193
|
+
name: 'LM Studio',
|
|
194
|
+
baseUrl: 'http://127.0.0.1:1234/v1/chat/completions',
|
|
195
|
+
models: [],
|
|
196
|
+
costPerK: { input: 0, output: 0 },
|
|
197
|
+
type: 'local',
|
|
198
|
+
priority: 12,
|
|
199
|
+
maxTokens: 8192,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// ============================================================
|
|
204
|
+
// RUNTIME STATE
|
|
205
|
+
// ============================================================
|
|
206
|
+
|
|
207
|
+
let _registeredProviders = { ...DEFAULT_PROVIDERS };
|
|
208
|
+
let _configLoaded = false;
|
|
209
|
+
|
|
210
|
+
// ============================================================
|
|
211
|
+
// CONFIGURATION LOADING
|
|
212
|
+
// ============================================================
|
|
213
|
+
|
|
214
|
+
function loadConfig(configPath) {
|
|
215
|
+
const paths = [];
|
|
216
|
+
|
|
217
|
+
// 1. Provided path
|
|
218
|
+
if (configPath && fs.existsSync(configPath)) {
|
|
219
|
+
paths.push(configPath);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 2. User config directory
|
|
223
|
+
const userConfig = path.join(
|
|
224
|
+
process.env.HOME || process.env.USERPROFILE || '.',
|
|
225
|
+
'.config', 'a3m-router', 'providers.json'
|
|
226
|
+
);
|
|
227
|
+
if (fs.existsSync(userConfig)) {
|
|
228
|
+
paths.push(userConfig);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 3. Project config
|
|
232
|
+
const projectConfig = path.join(process.cwd(), 'a3m-providers.json');
|
|
233
|
+
if (fs.existsSync(projectConfig)) {
|
|
234
|
+
paths.push(projectConfig);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 4. .env file
|
|
238
|
+
const envPath = path.join(process.env.HOME || '.', '.env');
|
|
239
|
+
if (fs.existsSync(envPath)) {
|
|
240
|
+
try {
|
|
241
|
+
require('dotenv').config({ path: envPath });
|
|
242
|
+
} catch (e) {
|
|
243
|
+
// dotenv not installed - env vars still work
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Load config from first found file
|
|
248
|
+
for (const p of paths) {
|
|
249
|
+
try {
|
|
250
|
+
const config = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
251
|
+
if (config.providers) {
|
|
252
|
+
for (const [id, provider] of Object.entries(config.providers)) {
|
|
253
|
+
if (_registeredProviders[id]) {
|
|
254
|
+
// Merge with defaults
|
|
255
|
+
_registeredProviders[id] = { ..._registeredProviders[id], ...provider };
|
|
256
|
+
} else {
|
|
257
|
+
// Register new provider
|
|
258
|
+
_registeredProviders[id] = {
|
|
259
|
+
id,
|
|
260
|
+
type: 'api',
|
|
261
|
+
priority: 50,
|
|
262
|
+
maxTokens: 8192,
|
|
263
|
+
costPerK: { input: 0, output: 0 },
|
|
264
|
+
models: [],
|
|
265
|
+
...provider,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
_configLoaded = true;
|
|
271
|
+
break;
|
|
272
|
+
} catch (e) {
|
|
273
|
+
// Skip invalid config files
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Load API keys from environment
|
|
278
|
+
for (const [id, provider] of Object.entries(_registeredProviders)) {
|
|
279
|
+
if (provider.apiKeyEnv) {
|
|
280
|
+
provider.apiKey = process.env[provider.apiKeyEnv] || null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return _registeredProviders;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function getAvailableProviders() {
|
|
288
|
+
if (!_configLoaded) {
|
|
289
|
+
loadConfig();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const available = {};
|
|
293
|
+
|
|
294
|
+
for (const [id, provider] of Object.entries(_registeredProviders)) {
|
|
295
|
+
if (provider.type === 'api') {
|
|
296
|
+
// API providers need a key
|
|
297
|
+
if (provider.apiKey) {
|
|
298
|
+
available[id] = provider;
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
// CLI/local providers are always available if the command exists
|
|
302
|
+
available[id] = provider;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Sort by priority
|
|
307
|
+
return Object.entries(available)
|
|
308
|
+
.sort(([, a], [, b]) => a.priority - b.priority)
|
|
309
|
+
.reduce((acc, [k, v]) => { acc[k] = v; return acc; }, {});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ============================================================
|
|
313
|
+
// RUNTIME REGISTRATION
|
|
314
|
+
// ============================================================
|
|
315
|
+
|
|
316
|
+
function registerProvider(id, config) {
|
|
317
|
+
_registeredProviders[id] = {
|
|
318
|
+
id,
|
|
319
|
+
type: 'api',
|
|
320
|
+
priority: 50,
|
|
321
|
+
maxTokens: 8192,
|
|
322
|
+
costPerK: { input: 0, output: 0 },
|
|
323
|
+
models: [],
|
|
324
|
+
...config,
|
|
325
|
+
};
|
|
326
|
+
return _registeredProviders[id];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function deregisterProvider(id) {
|
|
330
|
+
delete _registeredProviders[id];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function updateProvider(id, updates) {
|
|
334
|
+
if (_registeredProviders[id]) {
|
|
335
|
+
_registeredProviders[id] = { ..._registeredProviders[id], ...updates };
|
|
336
|
+
return _registeredProviders[id];
|
|
337
|
+
}
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ============================================================
|
|
342
|
+
// HEALTH CHECK
|
|
343
|
+
// ============================================================
|
|
344
|
+
|
|
345
|
+
async function healthCheck(providerId) {
|
|
346
|
+
const provider = _registeredProviders[providerId];
|
|
347
|
+
if (!provider) {
|
|
348
|
+
return { healthy: false, error: 'Provider not found: ' + providerId };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (provider.type === 'cli') {
|
|
352
|
+
// CLI provider - check if command exists
|
|
353
|
+
const { execSync } = require('child_process');
|
|
354
|
+
try {
|
|
355
|
+
execSync(`which ${provider.cliCommand || provider.id}`, { stdio: 'pipe' });
|
|
356
|
+
return { healthy: true, latency: 0, type: 'cli' };
|
|
357
|
+
} catch (e) {
|
|
358
|
+
return { healthy: false, error: 'Command not found: ' + (provider.cliCommand || provider.id) };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (provider.type === 'api') {
|
|
363
|
+
if (!provider.apiKey) {
|
|
364
|
+
return { healthy: false, error: 'No API key for ' + provider.name };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Simple health check
|
|
368
|
+
const startTime = Date.now();
|
|
369
|
+
try {
|
|
370
|
+
const model = provider.models[0];
|
|
371
|
+
const resp = await fetch(provider.baseUrl, {
|
|
372
|
+
method: 'POST',
|
|
373
|
+
headers: {
|
|
374
|
+
'Authorization': 'Bearer ' + provider.apiKey,
|
|
375
|
+
'Content-Type': 'application/json',
|
|
376
|
+
},
|
|
377
|
+
body: JSON.stringify({
|
|
378
|
+
model,
|
|
379
|
+
messages: [{ role: 'user', content: 'test' }],
|
|
380
|
+
max_tokens: 5,
|
|
381
|
+
}),
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const latency = Date.now() - startTime;
|
|
385
|
+
const data = await resp.json();
|
|
386
|
+
|
|
387
|
+
if (data.error) {
|
|
388
|
+
return { healthy: false, error: data.error.message, latency };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return { healthy: true, latency, model: data.model || model };
|
|
392
|
+
} catch (e) {
|
|
393
|
+
return { healthy: false, error: e.message, latency: Date.now() - startTime };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return { healthy: false, error: 'Unknown provider type: ' + provider.type };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function checkAllProviders() {
|
|
401
|
+
const results = {};
|
|
402
|
+
const available = getAvailableProviders();
|
|
403
|
+
|
|
404
|
+
for (const [id, provider] of Object.entries(available)) {
|
|
405
|
+
results[id] = await healthCheck(id);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return results;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ============================================================
|
|
412
|
+
// SAVE CONFIG
|
|
413
|
+
// ============================================================
|
|
414
|
+
|
|
415
|
+
function saveConfig(configPath) {
|
|
416
|
+
const target = configPath || path.join(
|
|
417
|
+
process.env.HOME || '.',
|
|
418
|
+
'.config', 'a3m-router', 'providers.json'
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
const dir = path.dirname(target);
|
|
422
|
+
if (!fs.existsSync(dir)) {
|
|
423
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Save without API keys for security
|
|
427
|
+
const safeConfig = {};
|
|
428
|
+
for (const [id, provider] of Object.entries(_registeredProviders)) {
|
|
429
|
+
safeConfig[id] = { ...provider };
|
|
430
|
+
delete safeConfig[id].apiKey;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
fs.writeFileSync(target, JSON.stringify({ providers: safeConfig }, null, 2));
|
|
434
|
+
return target;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ============================================================
|
|
438
|
+
// EXPORTS
|
|
439
|
+
// ============================================================
|
|
440
|
+
|
|
441
|
+
module.exports = {
|
|
442
|
+
DEFAULT_PROVIDERS,
|
|
443
|
+
loadConfig,
|
|
444
|
+
getAvailableProviders,
|
|
445
|
+
registerProvider,
|
|
446
|
+
deregisterProvider,
|
|
447
|
+
updateProvider,
|
|
448
|
+
healthCheck,
|
|
449
|
+
checkAllProviders,
|
|
450
|
+
saveConfig,
|
|
451
|
+
_providers: _registeredProviders,
|
|
452
|
+
};
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
+
* A3M Router - Generic Provider Registry
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
5
|
+
* Dynamically discovers and manages available LLM providers.
|
|
6
|
+
* Users configure providers via:
|
|
7
|
+
* - Environment variables (*_API_KEY patterns)
|
|
8
|
+
* - ~/.config/a3m-router/providers.json
|
|
9
|
+
* - Runtime registration via registerProvider()
|
|
10
|
+
*
|
|
11
|
+
* No hardcoded provider references - all loaded from providerConfig.
|
|
8
12
|
*/
|
|
13
|
+
|
|
14
|
+
const { getAvailableProviders, loadConfig, healthCheck, registerProvider, deregisterProvider } = require("./providerConfig");
|
|
15
|
+
const { routeQuery, routeBatch, recommendForTask, extractQueryFeatures, MODEL_PROFILES } = require("../routing/advancedRouter");
|
|
16
|
+
|
|
9
17
|
class ProviderRegistry {
|
|
10
18
|
constructor(config = {}) {
|
|
11
|
-
this.config =
|
|
12
|
-
this.modelPriority = this.config.modelPriority;
|
|
19
|
+
this.config = config;
|
|
13
20
|
this.providers = new Map();
|
|
14
21
|
this.readyCache = [];
|
|
15
22
|
this.cacheTime = 0;
|
|
16
23
|
this.cacheDuration = 60000; // 1 minute
|
|
24
|
+
|
|
25
|
+
// Load configuration from env vars and config files
|
|
26
|
+
loadConfig();
|
|
17
27
|
this.initializeProviders();
|
|
18
28
|
}
|
|
19
29
|
|
|
20
30
|
initializeProviders() {
|
|
21
|
-
const
|
|
22
|
-
openai: { key: "OPENAI_API_KEY", mode: "openai" },
|
|
23
|
-
anthropic: { key: "ANTHROPIC_API_KEY", mode: "anthropic" },
|
|
24
|
-
groq: { key: "GROQ_API_KEY", mode: "openai" },
|
|
25
|
-
cerebras: { key: "CEREBRAS_API_KEY", mode: "openai" },
|
|
26
|
-
deepseek: { key: "DEEPSEEK_API_KEY", mode: "openai" },
|
|
27
|
-
fireworks: { key: "FIREWORKS_API_KEY", mode: "openai" },
|
|
28
|
-
perplexity: { key: "PERPLEXITY_API_KEY", mode: "openai" },
|
|
29
|
-
cohere: { key: "COHERE_API_KEY", mode: "openai" },
|
|
30
|
-
google: { key: "GOOGLE_API_KEY", mode: "gemini" },
|
|
31
|
-
mistral: { key: "MISTRAL_API_KEY", mode: "openai" }
|
|
32
|
-
};
|
|
31
|
+
const available = getAvailableProviders();
|
|
33
32
|
|
|
34
|
-
for (const [name,
|
|
35
|
-
const apiKey = process.env[env.key] || '';
|
|
33
|
+
for (const [name, provider] of Object.entries(available)) {
|
|
36
34
|
this.providers.set(name, {
|
|
37
35
|
name,
|
|
38
|
-
apiKey,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
apiKey: provider.apiKey || null,
|
|
37
|
+
baseUrl: provider.baseUrl || null,
|
|
38
|
+
models: provider.models,
|
|
39
|
+
type: provider.type,
|
|
40
|
+
priority: provider.priority,
|
|
41
|
+
enabled: true,
|
|
42
42
|
cooldownUntil: 0,
|
|
43
|
-
failureCount: 0
|
|
43
|
+
failureCount: 0,
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -66,10 +66,15 @@ class ProviderRegistry {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
selectModel() {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
const available = getAvailableProviders();
|
|
70
|
+
const sorted = Object.entries(available).sort(([, a], [, b]) => a.priority - b.priority);
|
|
71
|
+
|
|
72
|
+
for (const [name, provider] of sorted) {
|
|
73
|
+
for (const model of provider.models) {
|
|
74
|
+
const modelKey = model.includes('/') ? model : name + '/' + model;
|
|
75
|
+
if (this.isProviderReady(name)) {
|
|
76
|
+
return modelKey;
|
|
77
|
+
}
|
|
73
78
|
}
|
|
74
79
|
}
|
|
75
80
|
return null;
|
|
@@ -94,22 +99,38 @@ class ProviderRegistry {
|
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
getStatus() {
|
|
102
|
+
const available = getAvailableProviders();
|
|
97
103
|
return {
|
|
98
104
|
providers: Array.from(this.providers.keys()),
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
available: Object.keys(available),
|
|
106
|
+
ready: this.getReadyProviders(),
|
|
107
|
+
modelPriority: this.selectModel(),
|
|
101
108
|
};
|
|
102
109
|
}
|
|
103
|
-
}
|
|
104
110
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
111
|
+
// Dynamic provider management
|
|
112
|
+
addProvider(id, config) {
|
|
113
|
+
registerProvider(id, config);
|
|
114
|
+
this.initializeProviders();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
removeProvider(id) {
|
|
118
|
+
deregisterProvider(id);
|
|
119
|
+
this.initializeProviders();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async checkHealth() {
|
|
123
|
+
const results = {};
|
|
124
|
+
for (const [name] of this.providers) {
|
|
125
|
+
results[name] = await healthCheck(name);
|
|
126
|
+
}
|
|
127
|
+
return results;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
110
130
|
|
|
111
131
|
const _routing = require("../routing/advancedRouter");
|
|
112
|
-
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
113
134
|
ProviderRegistry,
|
|
114
135
|
routeQuery: _routing.routeQuery,
|
|
115
136
|
routeBatch: _routing.routeBatch,
|
|
@@ -117,5 +138,3 @@ module.exports = {
|
|
|
117
138
|
extractQueryFeatures: _routing.extractQueryFeatures,
|
|
118
139
|
MODEL_PROFILES: _routing.MODEL_PROFILES,
|
|
119
140
|
};
|
|
120
|
-
|
|
121
|
-
|