adaptive-memory-multi-model-router 1.9.0 → 1.9.1

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.
@@ -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
- * Provider Registry v2 - Optimized
3
+ * A3M Router - Generic Provider Registry
3
4
  *
4
- * Improvements:
5
- * - Lazy loading of providers
6
- * - Cache for ready providers
7
- * - Faster model selection
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 = { ...DEFAULT_PROVIDER_CONFIG, ...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 envVars = {
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, env] of Object.entries(envVars)) {
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
- mode: env.mode,
40
- priority: this.modelPriority.findIndex(m => m.startsWith(name + "/")),
41
- enabled: Boolean(apiKey),
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
- for (const model of this.modelPriority) {
70
- const providerName = model.split("/")[0];
71
- if (this.isProviderReady(providerName)) {
72
- return model;
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
- modelPriority: this.modelPriority,
100
- readyProviders: this.getReadyProviders()
105
+ available: Object.keys(available),
106
+ ready: this.getReadyProviders(),
107
+ modelPriority: this.selectModel(),
101
108
  };
102
109
  }
103
- }
104
110
 
105
- const DEFAULT_PROVIDER_CONFIG = {
106
- providers: ["openai", "openrouter", "groq", "cerebras", "mistral", "deepseek", "fireworks", "perplexity", "cohere", "anthropic", "google"],
107
- modelPriority: ["openai/gpt-4o", "groq/llama-3.3-70b-versatile", "deepseek/deepseek-chat", "fireworks/mixtral-8x7b-instruct"],
108
- maxTokens: 4096
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
- module.exports = {
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
-