brains-cli 1.0.1 → 1.0.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/package.json +2 -1
- package/src/api.js +71 -59
- package/src/commands/config.js +153 -25
- package/src/commands/run.js +70 -18
- package/src/config.js +143 -11
- package/src/providers/anthropic.js +83 -0
- package/src/providers/openai-compat.js +96 -0
- package/src/providers/registry.js +125 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brains-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "🧠 Brains.io — Install AI-powered dev agents. Build, review, architect, and ship with one command.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
28
|
+
"openai": "^4.78.0",
|
|
28
29
|
"chalk": "^4.1.2",
|
|
29
30
|
"commander": "^11.1.0",
|
|
30
31
|
"inquirer": "^8.2.6",
|
package/src/api.js
CHANGED
|
@@ -1,36 +1,28 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const Anthropic = require('@anthropic-ai/sdk');
|
|
4
|
-
const { getApiKey, getModel } = require('./config');
|
|
5
|
-
|
|
6
|
-
let client = null;
|
|
7
|
-
|
|
8
3
|
/**
|
|
9
|
-
*
|
|
4
|
+
* API Router — delegates to the active provider.
|
|
5
|
+
*
|
|
6
|
+
* All brain commands call streamMessage/sendMessage from here.
|
|
7
|
+
* This module resolves which provider to use and routes accordingly.
|
|
10
8
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (!apiKey) {
|
|
15
|
-
throw new Error('NO_API_KEY');
|
|
16
|
-
}
|
|
17
|
-
client = new Anthropic({ apiKey });
|
|
18
|
-
return client;
|
|
19
|
-
}
|
|
9
|
+
|
|
10
|
+
const { getActiveProvider, getProviderApiKey, getProviderModel, getProviderBaseUrl } = require('./config');
|
|
11
|
+
const { getProviderDef } = require('./providers/registry');
|
|
20
12
|
|
|
21
13
|
/**
|
|
22
|
-
* Stream a
|
|
14
|
+
* Stream a response from the active AI provider.
|
|
23
15
|
* Returns the full assistant message text.
|
|
24
16
|
*
|
|
25
17
|
* @param {Object} opts
|
|
26
|
-
* @param {string} opts.systemPrompt
|
|
27
|
-
* @param {Array}
|
|
28
|
-
* @param {number} [opts.maxTokens]
|
|
29
|
-
* @param {string} [opts.model] -
|
|
30
|
-
* @param {Function} [opts.onText]
|
|
31
|
-
* @param {Function} [opts.onStart]
|
|
32
|
-
* @param {Function} [opts.onEnd]
|
|
33
|
-
* @returns {Promise<string>}
|
|
18
|
+
* @param {string} opts.systemPrompt
|
|
19
|
+
* @param {Array} opts.messages - [{role, content}]
|
|
20
|
+
* @param {number} [opts.maxTokens]
|
|
21
|
+
* @param {string} [opts.model] - Override model
|
|
22
|
+
* @param {Function} [opts.onText]
|
|
23
|
+
* @param {Function} [opts.onStart]
|
|
24
|
+
* @param {Function} [opts.onEnd]
|
|
25
|
+
* @returns {Promise<string>}
|
|
34
26
|
*/
|
|
35
27
|
async function streamMessage(opts) {
|
|
36
28
|
const {
|
|
@@ -43,34 +35,44 @@ async function streamMessage(opts) {
|
|
|
43
35
|
onEnd = () => {},
|
|
44
36
|
} = opts;
|
|
45
37
|
|
|
46
|
-
const
|
|
47
|
-
const
|
|
38
|
+
const providerName = getActiveProvider();
|
|
39
|
+
const providerDef = getProviderDef(providerName);
|
|
48
40
|
|
|
49
|
-
|
|
41
|
+
if (!providerDef) {
|
|
42
|
+
throw new Error(`Unknown provider "${providerName}". Run: brains config set provider <name>`);
|
|
43
|
+
}
|
|
50
44
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
onStart();
|
|
59
|
-
|
|
60
|
-
for await (const event of stream) {
|
|
61
|
-
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
|
|
62
|
-
const text = event.delta.text;
|
|
63
|
-
fullText += text;
|
|
64
|
-
onText(text);
|
|
65
|
-
}
|
|
45
|
+
const apiKey = getProviderApiKey(providerName);
|
|
46
|
+
const modelId = model || getProviderModel(providerName);
|
|
47
|
+
const baseURL = getProviderBaseUrl(providerName);
|
|
48
|
+
|
|
49
|
+
if (!apiKey && providerDef.requires_key !== false) {
|
|
50
|
+
throw new Error('NO_API_KEY');
|
|
66
51
|
}
|
|
67
52
|
|
|
68
|
-
|
|
69
|
-
|
|
53
|
+
const callOpts = {
|
|
54
|
+
apiKey,
|
|
55
|
+
systemPrompt,
|
|
56
|
+
messages,
|
|
57
|
+
maxTokens,
|
|
58
|
+
model: modelId,
|
|
59
|
+
onText,
|
|
60
|
+
onStart,
|
|
61
|
+
onEnd,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (providerDef.type === 'anthropic') {
|
|
65
|
+
const provider = require('./providers/anthropic');
|
|
66
|
+
return provider.streamMessage(callOpts);
|
|
67
|
+
} else {
|
|
68
|
+
const provider = require('./providers/openai-compat');
|
|
69
|
+
callOpts.baseURL = baseURL;
|
|
70
|
+
return provider.streamMessage(callOpts);
|
|
71
|
+
}
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
/**
|
|
73
|
-
* Send a single message (non-streaming).
|
|
75
|
+
* Send a single message (non-streaming).
|
|
74
76
|
*/
|
|
75
77
|
async function sendMessage(opts) {
|
|
76
78
|
const {
|
|
@@ -80,24 +82,34 @@ async function sendMessage(opts) {
|
|
|
80
82
|
model,
|
|
81
83
|
} = opts;
|
|
82
84
|
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
+
const providerName = getActiveProvider();
|
|
86
|
+
const providerDef = getProviderDef(providerName);
|
|
85
87
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
88
|
+
if (!providerDef) {
|
|
89
|
+
throw new Error(`Unknown provider "${providerName}".`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const apiKey = getProviderApiKey(providerName);
|
|
93
|
+
const modelId = model || getProviderModel(providerName);
|
|
94
|
+
const baseURL = getProviderBaseUrl(providerName);
|
|
95
|
+
|
|
96
|
+
if (!apiKey && providerDef.requires_key !== false) {
|
|
97
|
+
throw new Error('NO_API_KEY');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const callOpts = { apiKey, systemPrompt, messages, maxTokens, model: modelId };
|
|
101
|
+
|
|
102
|
+
if (providerDef.type === 'anthropic') {
|
|
103
|
+
const provider = require('./providers/anthropic');
|
|
104
|
+
return provider.sendMessage(callOpts);
|
|
105
|
+
} else {
|
|
106
|
+
const provider = require('./providers/openai-compat');
|
|
107
|
+
callOpts.baseURL = baseURL;
|
|
108
|
+
return provider.sendMessage(callOpts);
|
|
109
|
+
}
|
|
97
110
|
}
|
|
98
111
|
|
|
99
112
|
module.exports = {
|
|
100
|
-
getClient,
|
|
101
113
|
streamMessage,
|
|
102
114
|
sendMessage,
|
|
103
115
|
};
|
package/src/commands/config.js
CHANGED
|
@@ -2,46 +2,101 @@
|
|
|
2
2
|
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const Table = require('cli-table3');
|
|
5
|
-
const {
|
|
6
|
-
|
|
5
|
+
const {
|
|
6
|
+
loadConfig, saveConfig, setConfigValue,
|
|
7
|
+
getActiveProvider, getProviderApiKey, getProviderModel, getProviderBaseUrl,
|
|
8
|
+
setProviderApiKey, setProviderModel, setActiveProvider,
|
|
9
|
+
maskApiKey, DEFAULTS,
|
|
10
|
+
} = require('../config');
|
|
11
|
+
const { getAllProviders, getProviderDef, getAllProviderNames } = require('../providers/registry');
|
|
12
|
+
const { showSuccess, showError, showInfo, showWarning, ACCENT, DIM, SUCCESS } = require('../utils/ui');
|
|
7
13
|
|
|
8
14
|
async function configCommand(action, args) {
|
|
9
15
|
if (!action) {
|
|
10
|
-
// Show all config
|
|
11
16
|
showCurrentConfig();
|
|
12
17
|
return;
|
|
13
18
|
}
|
|
14
19
|
|
|
20
|
+
// ─── brains config set <key> <value> ───
|
|
15
21
|
if (action === 'set') {
|
|
16
22
|
if (args.length < 2) {
|
|
17
23
|
showError('Usage: brains config set <key> <value>');
|
|
18
|
-
|
|
19
|
-
showInfo(
|
|
24
|
+
console.log('');
|
|
25
|
+
showInfo(`${ACCENT('provider')} — Switch AI provider (groq, anthropic, gemini, openrouter, ollama)`);
|
|
26
|
+
showInfo(`${ACCENT('api_key')} — Set API key for current provider`);
|
|
27
|
+
showInfo(`${ACCENT('model')} — Set model for current provider`);
|
|
28
|
+
showInfo(`${ACCENT('max_tokens')} — Set max tokens`);
|
|
29
|
+
console.log('');
|
|
30
|
+
showInfo(`Example: ${ACCENT('brains config set provider groq')}`);
|
|
31
|
+
showInfo(`Example: ${ACCENT('brains config set api_key gsk_...')}`);
|
|
20
32
|
return;
|
|
21
33
|
}
|
|
22
34
|
|
|
23
35
|
const key = args[0];
|
|
24
36
|
let value = args.slice(1).join(' ');
|
|
25
37
|
|
|
26
|
-
//
|
|
27
|
-
|
|
38
|
+
// ── Switch provider ──
|
|
39
|
+
if (key === 'provider') {
|
|
40
|
+
const providerDef = getProviderDef(value);
|
|
41
|
+
if (!providerDef) {
|
|
42
|
+
showError(`Unknown provider "${value}".`);
|
|
43
|
+
showInfo(`Available: ${getAllProviderNames().map(n => ACCENT(n)).join(', ')}`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setActiveProvider(value);
|
|
48
|
+
showSuccess(`Provider switched to ${chalk.bold(providerDef.name)}`);
|
|
49
|
+
|
|
50
|
+
// Check if key is configured
|
|
51
|
+
const apiKey = getProviderApiKey(value);
|
|
52
|
+
if (!apiKey && providerDef.requires_key !== false) {
|
|
53
|
+
console.log('');
|
|
54
|
+
showWarning(`No API key set for ${providerDef.name}.`);
|
|
55
|
+
showInfo(`Get one at: ${ACCENT(providerDef.key_url)}`);
|
|
56
|
+
showInfo(`Then run: ${ACCENT(`brains config set api_key <your-key>`)}`);
|
|
57
|
+
} else {
|
|
58
|
+
showInfo(`Model: ${DIM(getProviderModel(value))}`);
|
|
59
|
+
}
|
|
60
|
+
console.log('');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Set API key (for current provider) ──
|
|
65
|
+
if (key === 'api_key') {
|
|
66
|
+
const currentProvider = getActiveProvider();
|
|
67
|
+
setProviderApiKey(currentProvider, value);
|
|
68
|
+
showSuccess(`API key saved for ${getProviderDef(currentProvider).name}: ${maskApiKey(value)}`);
|
|
69
|
+
console.log('');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Set model (for current provider) ──
|
|
74
|
+
if (key === 'model') {
|
|
75
|
+
const currentProvider = getActiveProvider();
|
|
76
|
+
setProviderModel(currentProvider, value);
|
|
77
|
+
showSuccess(`Model set for ${getProviderDef(currentProvider).name}: ${value}`);
|
|
78
|
+
console.log('');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Generic config keys ──
|
|
83
|
+
const validKeys = ['max_tokens', 'theme', 'telemetry'];
|
|
28
84
|
if (!validKeys.includes(key)) {
|
|
29
85
|
showError(`Unknown config key "${key}".`);
|
|
30
|
-
showInfo(`Valid keys: ${validKeys.map(k => ACCENT(k)).join(', ')}`);
|
|
86
|
+
showInfo(`Valid keys: provider, api_key, model, ${validKeys.map(k => ACCENT(k)).join(', ')}`);
|
|
31
87
|
return;
|
|
32
88
|
}
|
|
33
89
|
|
|
34
|
-
// Type coercion
|
|
35
90
|
if (key === 'max_tokens') value = parseInt(value, 10);
|
|
36
91
|
if (key === 'telemetry') value = value === 'true';
|
|
37
92
|
|
|
38
93
|
setConfigValue(key, value);
|
|
39
|
-
|
|
40
|
-
showSuccess(`${key} = ${displayValue}`);
|
|
94
|
+
showSuccess(`${key} = ${value}`);
|
|
41
95
|
console.log('');
|
|
42
96
|
return;
|
|
43
97
|
}
|
|
44
98
|
|
|
99
|
+
// ─── brains config get <key> ───
|
|
45
100
|
if (action === 'get') {
|
|
46
101
|
if (args.length < 1) {
|
|
47
102
|
showError('Usage: brains config get <key>');
|
|
@@ -49,13 +104,29 @@ async function configCommand(action, args) {
|
|
|
49
104
|
}
|
|
50
105
|
|
|
51
106
|
const key = args[0];
|
|
52
|
-
|
|
53
|
-
|
|
107
|
+
|
|
108
|
+
if (key === 'provider') {
|
|
109
|
+
const p = getActiveProvider();
|
|
110
|
+
const def = getProviderDef(p);
|
|
111
|
+
console.log(`\n ${chalk.bold('provider')} = ${p} (${def ? def.name : 'unknown'})\n`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
54
114
|
|
|
55
115
|
if (key === 'api_key') {
|
|
56
|
-
|
|
116
|
+
const p = getActiveProvider();
|
|
117
|
+
const k = getProviderApiKey(p);
|
|
118
|
+
console.log(`\n ${chalk.bold('api_key')} [${p}] = ${k ? maskApiKey(k) : chalk.red('(not set)')}\n`);
|
|
119
|
+
return;
|
|
57
120
|
}
|
|
58
121
|
|
|
122
|
+
if (key === 'model') {
|
|
123
|
+
const p = getActiveProvider();
|
|
124
|
+
console.log(`\n ${chalk.bold('model')} [${p}] = ${getProviderModel(p)}\n`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const config = loadConfig();
|
|
129
|
+
const value = config[key];
|
|
59
130
|
if (value === undefined) {
|
|
60
131
|
showInfo(`${key} is not set (default: ${DEFAULTS[key] || 'none'})`);
|
|
61
132
|
} else {
|
|
@@ -64,20 +135,33 @@ async function configCommand(action, args) {
|
|
|
64
135
|
return;
|
|
65
136
|
}
|
|
66
137
|
|
|
138
|
+
// ─── brains config providers ───
|
|
139
|
+
if (action === 'providers') {
|
|
140
|
+
showProviderList();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ─── brains config reset ───
|
|
67
145
|
if (action === 'reset') {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
showSuccess('Config reset to defaults.');
|
|
146
|
+
saveConfig({ ...DEFAULTS, providers: {} });
|
|
147
|
+
showSuccess('Config reset to defaults (provider: groq).');
|
|
71
148
|
console.log('');
|
|
72
149
|
return;
|
|
73
150
|
}
|
|
74
151
|
|
|
75
|
-
showError(`Unknown action "${action}". Use: set, get, reset, or no action to view all.`);
|
|
152
|
+
showError(`Unknown action "${action}". Use: set, get, providers, reset, or no action to view all.`);
|
|
76
153
|
}
|
|
77
154
|
|
|
155
|
+
// ═══════════════════════════════════════════
|
|
156
|
+
// Display helpers
|
|
157
|
+
// ═══════════════════════════════════════════
|
|
158
|
+
|
|
78
159
|
function showCurrentConfig() {
|
|
79
160
|
const config = loadConfig();
|
|
80
|
-
const
|
|
161
|
+
const currentProvider = getActiveProvider();
|
|
162
|
+
const providerDef = getProviderDef(currentProvider);
|
|
163
|
+
const apiKey = getProviderApiKey(currentProvider);
|
|
164
|
+
const model = getProviderModel(currentProvider);
|
|
81
165
|
|
|
82
166
|
console.log(`\n ${chalk.bold('Brains Configuration')}\n`);
|
|
83
167
|
|
|
@@ -87,22 +171,66 @@ function showCurrentConfig() {
|
|
|
87
171
|
});
|
|
88
172
|
|
|
89
173
|
table.push(
|
|
90
|
-
[chalk.bold('
|
|
91
|
-
[chalk.bold('
|
|
174
|
+
[chalk.bold('provider'), `${currentProvider} ${DIM(`(${providerDef ? providerDef.name : 'unknown'})`)}`],
|
|
175
|
+
[chalk.bold('api_key'), apiKey ? maskApiKey(apiKey) : (providerDef && providerDef.requires_key === false ? DIM('(not required)') : chalk.red('(not set)'))],
|
|
176
|
+
[chalk.bold('model'), model],
|
|
92
177
|
[chalk.bold('max_tokens'), String(config.max_tokens || DEFAULTS.max_tokens)],
|
|
93
|
-
[chalk.bold('theme'), config.theme || DEFAULTS.theme],
|
|
94
178
|
[chalk.bold('telemetry'), String(config.telemetry ?? DEFAULTS.telemetry)]
|
|
95
179
|
);
|
|
96
180
|
|
|
97
181
|
console.log(table.toString());
|
|
98
182
|
|
|
99
|
-
if (!apiKey) {
|
|
183
|
+
if (!apiKey && providerDef && providerDef.requires_key !== false) {
|
|
100
184
|
console.log('');
|
|
101
|
-
showInfo(`Set
|
|
102
|
-
|
|
185
|
+
showInfo(`Set API key: ${ACCENT(`brains config set api_key <your-${currentProvider}-key>`)}`);
|
|
186
|
+
if (providerDef.key_url) {
|
|
187
|
+
showInfo(`Get one at: ${ACCENT(providerDef.key_url)}`);
|
|
188
|
+
}
|
|
103
189
|
}
|
|
104
190
|
|
|
105
191
|
console.log('');
|
|
192
|
+
showInfo(`Switch provider: ${ACCENT('brains config set provider <name>')}`);
|
|
193
|
+
showInfo(`See all providers: ${ACCENT('brains config providers')}`);
|
|
194
|
+
console.log('');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function showProviderList() {
|
|
198
|
+
const currentProvider = getActiveProvider();
|
|
199
|
+
const allProviders = getAllProviders();
|
|
200
|
+
|
|
201
|
+
console.log(`\n ${chalk.bold('Available Providers')}\n`);
|
|
202
|
+
|
|
203
|
+
const table = new Table({
|
|
204
|
+
head: [
|
|
205
|
+
chalk.white.bold('Provider'),
|
|
206
|
+
chalk.white.bold('Type'),
|
|
207
|
+
chalk.white.bold('Default Model'),
|
|
208
|
+
chalk.white.bold('Free?'),
|
|
209
|
+
chalk.white.bold('Status'),
|
|
210
|
+
],
|
|
211
|
+
style: { head: [], border: ['dim'] },
|
|
212
|
+
colWidths: [16, 12, 36, 8, 14],
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
for (const [name, def] of Object.entries(allProviders)) {
|
|
216
|
+
const isActive = name === currentProvider;
|
|
217
|
+
const hasKey = !!getProviderApiKey(name) || def.requires_key === false;
|
|
218
|
+
|
|
219
|
+
table.push([
|
|
220
|
+
isActive ? chalk.hex('#7B2FFF').bold(`▸ ${name}`) : ` ${name}`,
|
|
221
|
+
DIM(def.type),
|
|
222
|
+
DIM(def.default_model),
|
|
223
|
+
def.free ? SUCCESS('Yes') : DIM('Paid'),
|
|
224
|
+
hasKey ? SUCCESS('Ready') : chalk.yellow('Need key'),
|
|
225
|
+
]);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
console.log(table.toString());
|
|
229
|
+
|
|
230
|
+
console.log('');
|
|
231
|
+
showInfo(`Active: ${ACCENT(currentProvider)}`);
|
|
232
|
+
showInfo(`Switch: ${ACCENT('brains config set provider <name>')}`);
|
|
233
|
+
console.log('');
|
|
106
234
|
}
|
|
107
235
|
|
|
108
236
|
module.exports = { config: configCommand };
|
package/src/commands/run.js
CHANGED
|
@@ -7,7 +7,12 @@ const path = require('path');
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const readline = require('readline');
|
|
9
9
|
const { getBrainById } = require('../registry');
|
|
10
|
-
const {
|
|
10
|
+
const {
|
|
11
|
+
getActiveProvider, getProviderApiKey, getProviderModel,
|
|
12
|
+
setProviderApiKey, setActiveProvider,
|
|
13
|
+
getModel, saveSession,
|
|
14
|
+
} = require('../config');
|
|
15
|
+
const { getProviderDef, getAllProviderNames } = require('../providers/registry');
|
|
11
16
|
const { streamMessage } = require('../api');
|
|
12
17
|
const { readFilesForReview, parseFileBlocks, writeFileBlocks, generateTreeView } = require('../utils/files');
|
|
13
18
|
const { showSuccess, showError, showInfo, showWarning, ACCENT, DIM, SUCCESS, WARN } = require('../utils/ui');
|
|
@@ -33,23 +38,45 @@ function loadBrainConfig(brainId) {
|
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
/**
|
|
36
|
-
* Ensure API key is available
|
|
41
|
+
* Ensure API key is available for the active provider.
|
|
42
|
+
* Prompts user if missing. Offers provider switching if no key.
|
|
37
43
|
*/
|
|
38
44
|
async function ensureApiKey() {
|
|
39
|
-
|
|
45
|
+
const providerName = getActiveProvider();
|
|
46
|
+
const providerDef = getProviderDef(providerName);
|
|
47
|
+
|
|
48
|
+
// Ollama doesn't need a key
|
|
49
|
+
if (providerDef && providerDef.requires_key === false) {
|
|
50
|
+
return 'not-required';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let key = getProviderApiKey(providerName);
|
|
40
54
|
if (key) return key;
|
|
41
55
|
|
|
42
56
|
console.log('');
|
|
43
|
-
showWarning(
|
|
44
|
-
|
|
57
|
+
showWarning(`No API key found for ${chalk.bold(providerDef ? providerDef.name : providerName)}.`);
|
|
58
|
+
|
|
59
|
+
if (providerDef && providerDef.key_url) {
|
|
60
|
+
showInfo(`Get one at ${ACCENT(providerDef.key_url)}\n`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const keyHint = providerDef && providerDef.key_prefix
|
|
64
|
+
? `Key should start with ${providerDef.key_prefix}`
|
|
65
|
+
: '';
|
|
45
66
|
|
|
46
67
|
const { apiKey } = await inquirer.prompt([
|
|
47
68
|
{
|
|
48
69
|
type: 'password',
|
|
49
70
|
name: 'apiKey',
|
|
50
|
-
message:
|
|
71
|
+
message: `Enter your ${providerDef ? providerDef.name : providerName} API key:`,
|
|
51
72
|
mask: '*',
|
|
52
|
-
validate: (v) =>
|
|
73
|
+
validate: (v) => {
|
|
74
|
+
if (!v || v.length < 5) return 'Please enter a valid API key';
|
|
75
|
+
if (providerDef && providerDef.key_prefix && !v.startsWith(providerDef.key_prefix)) {
|
|
76
|
+
return keyHint;
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
},
|
|
53
80
|
},
|
|
54
81
|
]);
|
|
55
82
|
|
|
@@ -63,12 +90,15 @@ async function ensureApiKey() {
|
|
|
63
90
|
]);
|
|
64
91
|
|
|
65
92
|
if (save) {
|
|
66
|
-
|
|
93
|
+
setProviderApiKey(providerName, apiKey);
|
|
67
94
|
showSuccess('API key saved.');
|
|
68
95
|
}
|
|
69
96
|
|
|
70
97
|
// Set in environment for this session
|
|
71
|
-
|
|
98
|
+
if (providerDef && providerDef.key_env) {
|
|
99
|
+
process.env[providerDef.key_env] = apiKey;
|
|
100
|
+
}
|
|
101
|
+
|
|
72
102
|
return apiKey;
|
|
73
103
|
}
|
|
74
104
|
|
|
@@ -126,8 +156,12 @@ function showSessionHeader(brain, stackedBrains) {
|
|
|
126
156
|
chalk.hex(color).bold(' │ ') + DIM(`+ ${stackedBrains.map((b) => b.name).join(', ')}`)
|
|
127
157
|
);
|
|
128
158
|
}
|
|
159
|
+
const providerName = getActiveProvider();
|
|
160
|
+
const providerDef = getProviderDef(providerName);
|
|
161
|
+
const providerLabel = providerDef ? providerDef.name : providerName;
|
|
162
|
+
|
|
129
163
|
console.log(chalk.hex(color).bold(' │ ') + DIM(`Stack: ${brain.stack.join(', ')}`));
|
|
130
|
-
console.log(chalk.hex(color).bold(' │ ') + DIM(`Model: ${getModel()}`));
|
|
164
|
+
console.log(chalk.hex(color).bold(' │ ') + DIM(`Provider: ${providerLabel} • Model: ${getModel()}`));
|
|
131
165
|
console.log(chalk.hex(color).bold(' └─────────────────────────────────────────────────────'));
|
|
132
166
|
console.log('');
|
|
133
167
|
}
|
|
@@ -553,15 +587,24 @@ async function runConversationalBrain(brain, config, stackedBrains, options) {
|
|
|
553
587
|
function handleApiError(err) {
|
|
554
588
|
console.log('');
|
|
555
589
|
|
|
590
|
+
const providerName = getActiveProvider();
|
|
591
|
+
const providerDef = getProviderDef(providerName);
|
|
592
|
+
const providerLabel = providerDef ? providerDef.name : providerName;
|
|
593
|
+
|
|
556
594
|
if (err.message === 'NO_API_KEY') {
|
|
557
|
-
showError(
|
|
558
|
-
showInfo(`Run ${ACCENT('brains config set api_key <your-key>')}
|
|
595
|
+
showError(`No API key configured for ${providerLabel}.`);
|
|
596
|
+
showInfo(`Run ${ACCENT('brains config set api_key <your-key>')}`);
|
|
597
|
+
if (providerDef && providerDef.key_env) {
|
|
598
|
+
showInfo(`Or set env var: ${ACCENT(`export ${providerDef.key_env}=<your-key>`)}`);
|
|
599
|
+
}
|
|
559
600
|
return;
|
|
560
601
|
}
|
|
561
602
|
|
|
562
|
-
if (err.status === 401) {
|
|
563
|
-
showError(
|
|
564
|
-
|
|
603
|
+
if (err.status === 401 || err.code === 'invalid_api_key') {
|
|
604
|
+
showError(`Invalid API key for ${providerLabel}.`);
|
|
605
|
+
if (providerDef && providerDef.key_url) {
|
|
606
|
+
showInfo(`Check your key at ${ACCENT(providerDef.key_url)}`);
|
|
607
|
+
}
|
|
565
608
|
showInfo(`Update with: ${ACCENT('brains config set api_key <your-key>')}`);
|
|
566
609
|
return;
|
|
567
610
|
}
|
|
@@ -569,18 +612,26 @@ function handleApiError(err) {
|
|
|
569
612
|
if (err.status === 429) {
|
|
570
613
|
showError('Rate limit exceeded.');
|
|
571
614
|
showInfo('Wait a moment and try again, or check your API usage limits.');
|
|
615
|
+
if (providerDef && providerDef.free) {
|
|
616
|
+
showInfo(`${providerLabel} free tier has rate limits. Consider upgrading or switching providers.`);
|
|
617
|
+
showInfo(`Switch: ${ACCENT('brains config set provider <name>')}`);
|
|
618
|
+
}
|
|
572
619
|
return;
|
|
573
620
|
}
|
|
574
621
|
|
|
575
622
|
if (err.status === 529 || err.status === 503) {
|
|
576
|
-
showError(
|
|
623
|
+
showError(`${providerLabel} is temporarily overloaded.`);
|
|
577
624
|
showInfo('Try again in a few seconds.');
|
|
578
625
|
return;
|
|
579
626
|
}
|
|
580
627
|
|
|
581
628
|
if (err.code === 'ENOTFOUND' || err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT') {
|
|
582
|
-
showError(
|
|
583
|
-
|
|
629
|
+
showError(`Network error — cannot reach ${providerLabel}.`);
|
|
630
|
+
if (providerName === 'ollama') {
|
|
631
|
+
showInfo('Is Ollama running? Start it with: ollama serve');
|
|
632
|
+
} else {
|
|
633
|
+
showInfo('Check your internet connection and try again.');
|
|
634
|
+
}
|
|
584
635
|
return;
|
|
585
636
|
}
|
|
586
637
|
|
|
@@ -589,6 +640,7 @@ function handleApiError(err) {
|
|
|
589
640
|
if (err.status) {
|
|
590
641
|
showInfo(`Status: ${err.status}`);
|
|
591
642
|
}
|
|
643
|
+
showInfo(`Provider: ${providerLabel} | Model: ${getModel()}`);
|
|
592
644
|
console.log('');
|
|
593
645
|
}
|
|
594
646
|
|
package/src/config.js
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const os = require('os');
|
|
6
|
+
const { getProviderDef } = require('./providers/registry');
|
|
6
7
|
|
|
7
8
|
const BRAINS_DIR = path.join(os.homedir(), '.brains');
|
|
8
9
|
const CONFIG_FILE = path.join(BRAINS_DIR, 'config.json');
|
|
9
10
|
const SESSIONS_DIR = path.join(BRAINS_DIR, 'sessions');
|
|
10
11
|
|
|
11
12
|
const DEFAULTS = {
|
|
12
|
-
|
|
13
|
+
provider: 'groq',
|
|
13
14
|
max_tokens: 8096,
|
|
14
15
|
theme: 'dark',
|
|
15
16
|
telemetry: false,
|
|
@@ -28,12 +29,50 @@ function loadConfig() {
|
|
|
28
29
|
ensureDirs();
|
|
29
30
|
try {
|
|
30
31
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
31
|
-
|
|
32
|
+
const raw = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
33
|
+
return migrateConfig(raw);
|
|
32
34
|
}
|
|
33
35
|
} catch (e) {
|
|
34
36
|
// corrupted config, reset
|
|
35
37
|
}
|
|
36
|
-
return { ...DEFAULTS };
|
|
38
|
+
return { ...DEFAULTS, providers: {} };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Migrate old config format (flat api_key/model) to new provider-based format.
|
|
43
|
+
*/
|
|
44
|
+
function migrateConfig(raw) {
|
|
45
|
+
const config = { ...DEFAULTS, ...raw };
|
|
46
|
+
|
|
47
|
+
// Ensure providers object exists
|
|
48
|
+
if (!config.providers) {
|
|
49
|
+
config.providers = {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Migrate old flat api_key → providers.anthropic.api_key
|
|
53
|
+
if (config.api_key && !config.providers.anthropic) {
|
|
54
|
+
config.providers.anthropic = {
|
|
55
|
+
api_key: config.api_key,
|
|
56
|
+
model: config.model || 'claude-sonnet-4-20250514',
|
|
57
|
+
};
|
|
58
|
+
delete config.api_key;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Migrate old flat model if it was an anthropic model
|
|
62
|
+
if (config.model && config.model.startsWith('claude-')) {
|
|
63
|
+
if (!config.providers.anthropic) config.providers.anthropic = {};
|
|
64
|
+
config.providers.anthropic.model = config.model;
|
|
65
|
+
delete config.model;
|
|
66
|
+
} else if (config.model) {
|
|
67
|
+
delete config.model;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// If no provider is set, default to groq
|
|
71
|
+
if (!config.provider) {
|
|
72
|
+
config.provider = DEFAULTS.provider;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return config;
|
|
37
76
|
}
|
|
38
77
|
|
|
39
78
|
function saveConfig(config) {
|
|
@@ -52,23 +91,109 @@ function setConfigValue(key, value) {
|
|
|
52
91
|
saveConfig(config);
|
|
53
92
|
}
|
|
54
93
|
|
|
94
|
+
// ═══════════════════════════════════════════
|
|
95
|
+
// PROVIDER-AWARE GETTERS
|
|
96
|
+
// ═══════════════════════════════════════════
|
|
97
|
+
|
|
55
98
|
/**
|
|
56
|
-
*
|
|
99
|
+
* Get the currently active provider name.
|
|
57
100
|
*/
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
|
|
101
|
+
function getActiveProvider() {
|
|
102
|
+
const config = loadConfig();
|
|
103
|
+
return config.provider || DEFAULTS.provider;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve API key for a provider: env var > config > null
|
|
108
|
+
*/
|
|
109
|
+
function getProviderApiKey(providerName) {
|
|
110
|
+
const providerDef = getProviderDef(providerName);
|
|
111
|
+
|
|
112
|
+
// 1. Check environment variable
|
|
113
|
+
if (providerDef && providerDef.key_env && process.env[providerDef.key_env]) {
|
|
114
|
+
return process.env[providerDef.key_env];
|
|
61
115
|
}
|
|
116
|
+
|
|
117
|
+
// 2. Check config file
|
|
62
118
|
const config = loadConfig();
|
|
63
|
-
|
|
64
|
-
|
|
119
|
+
const providerConfig = config.providers && config.providers[providerName];
|
|
120
|
+
if (providerConfig && providerConfig.api_key) {
|
|
121
|
+
return providerConfig.api_key;
|
|
65
122
|
}
|
|
123
|
+
|
|
66
124
|
return null;
|
|
67
125
|
}
|
|
68
126
|
|
|
69
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Get the model for a provider: config > default
|
|
129
|
+
*/
|
|
130
|
+
function getProviderModel(providerName) {
|
|
131
|
+
const config = loadConfig();
|
|
132
|
+
const providerConfig = config.providers && config.providers[providerName];
|
|
133
|
+
if (providerConfig && providerConfig.model) {
|
|
134
|
+
return providerConfig.model;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Fallback to provider's default model
|
|
138
|
+
const providerDef = getProviderDef(providerName);
|
|
139
|
+
return providerDef ? providerDef.default_model : 'unknown';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get the base URL for a provider: config override > provider default
|
|
144
|
+
*/
|
|
145
|
+
function getProviderBaseUrl(providerName) {
|
|
146
|
+
const config = loadConfig();
|
|
147
|
+
const providerConfig = config.providers && config.providers[providerName];
|
|
148
|
+
if (providerConfig && providerConfig.base_url) {
|
|
149
|
+
return providerConfig.base_url;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const providerDef = getProviderDef(providerName);
|
|
153
|
+
return providerDef ? providerDef.base_url : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Save API key for a specific provider.
|
|
158
|
+
*/
|
|
159
|
+
function setProviderApiKey(providerName, apiKey) {
|
|
160
|
+
const config = loadConfig();
|
|
161
|
+
if (!config.providers) config.providers = {};
|
|
162
|
+
if (!config.providers[providerName]) config.providers[providerName] = {};
|
|
163
|
+
config.providers[providerName].api_key = apiKey;
|
|
164
|
+
saveConfig(config);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Save model for a specific provider.
|
|
169
|
+
*/
|
|
170
|
+
function setProviderModel(providerName, model) {
|
|
171
|
+
const config = loadConfig();
|
|
172
|
+
if (!config.providers) config.providers = {};
|
|
173
|
+
if (!config.providers[providerName]) config.providers[providerName] = {};
|
|
174
|
+
config.providers[providerName].model = model;
|
|
175
|
+
saveConfig(config);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Switch the active provider.
|
|
180
|
+
*/
|
|
181
|
+
function setActiveProvider(providerName) {
|
|
70
182
|
const config = loadConfig();
|
|
71
|
-
|
|
183
|
+
config.provider = providerName;
|
|
184
|
+
saveConfig(config);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ═══════════════════════════════════════════
|
|
188
|
+
// BACKWARD-COMPAT GETTERS (used by run.js session header)
|
|
189
|
+
// ═══════════════════════════════════════════
|
|
190
|
+
|
|
191
|
+
function getModel() {
|
|
192
|
+
return getProviderModel(getActiveProvider());
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function getApiKey() {
|
|
196
|
+
return getProviderApiKey(getActiveProvider());
|
|
72
197
|
}
|
|
73
198
|
|
|
74
199
|
function maskApiKey(key) {
|
|
@@ -97,6 +222,13 @@ module.exports = {
|
|
|
97
222
|
saveConfig,
|
|
98
223
|
getConfigValue,
|
|
99
224
|
setConfigValue,
|
|
225
|
+
getActiveProvider,
|
|
226
|
+
getProviderApiKey,
|
|
227
|
+
getProviderModel,
|
|
228
|
+
getProviderBaseUrl,
|
|
229
|
+
setProviderApiKey,
|
|
230
|
+
setProviderModel,
|
|
231
|
+
setActiveProvider,
|
|
100
232
|
getApiKey,
|
|
101
233
|
getModel,
|
|
102
234
|
maskApiKey,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Anthropic = require('@anthropic-ai/sdk');
|
|
4
|
+
|
|
5
|
+
let client = null;
|
|
6
|
+
let cachedKey = null;
|
|
7
|
+
|
|
8
|
+
function getClient(apiKey) {
|
|
9
|
+
// Re-create if key changed
|
|
10
|
+
if (client && cachedKey === apiKey) return client;
|
|
11
|
+
client = new Anthropic({ apiKey });
|
|
12
|
+
cachedKey = apiKey;
|
|
13
|
+
return client;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Stream a message using Anthropic's native API.
|
|
18
|
+
* System prompt is a separate parameter (not a message).
|
|
19
|
+
*/
|
|
20
|
+
async function streamMessage(opts) {
|
|
21
|
+
const {
|
|
22
|
+
apiKey,
|
|
23
|
+
systemPrompt,
|
|
24
|
+
messages,
|
|
25
|
+
maxTokens,
|
|
26
|
+
model,
|
|
27
|
+
onText,
|
|
28
|
+
onStart,
|
|
29
|
+
onEnd,
|
|
30
|
+
} = opts;
|
|
31
|
+
|
|
32
|
+
const anthropic = getClient(apiKey);
|
|
33
|
+
let fullText = '';
|
|
34
|
+
|
|
35
|
+
const stream = await anthropic.messages.stream({
|
|
36
|
+
model,
|
|
37
|
+
max_tokens: maxTokens,
|
|
38
|
+
system: systemPrompt,
|
|
39
|
+
messages,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
onStart();
|
|
43
|
+
|
|
44
|
+
for await (const event of stream) {
|
|
45
|
+
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
|
|
46
|
+
const text = event.delta.text;
|
|
47
|
+
fullText += text;
|
|
48
|
+
onText(text);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
onEnd();
|
|
53
|
+
return fullText;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Send a single message (non-streaming).
|
|
58
|
+
*/
|
|
59
|
+
async function sendMessage(opts) {
|
|
60
|
+
const {
|
|
61
|
+
apiKey,
|
|
62
|
+
systemPrompt,
|
|
63
|
+
messages,
|
|
64
|
+
maxTokens,
|
|
65
|
+
model,
|
|
66
|
+
} = opts;
|
|
67
|
+
|
|
68
|
+
const anthropic = getClient(apiKey);
|
|
69
|
+
|
|
70
|
+
const response = await anthropic.messages.create({
|
|
71
|
+
model,
|
|
72
|
+
max_tokens: maxTokens,
|
|
73
|
+
system: systemPrompt,
|
|
74
|
+
messages,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return response.content
|
|
78
|
+
.filter((block) => block.type === 'text')
|
|
79
|
+
.map((block) => block.text)
|
|
80
|
+
.join('');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { streamMessage, sendMessage };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const OpenAI = require('openai');
|
|
4
|
+
|
|
5
|
+
const clients = {};
|
|
6
|
+
|
|
7
|
+
function getClient(baseURL, apiKey) {
|
|
8
|
+
const cacheKey = baseURL + '::' + (apiKey || 'nokey');
|
|
9
|
+
if (clients[cacheKey]) return clients[cacheKey];
|
|
10
|
+
|
|
11
|
+
clients[cacheKey] = new OpenAI({
|
|
12
|
+
apiKey: apiKey || 'ollama', // Ollama doesn't need a key but SDK requires one
|
|
13
|
+
baseURL,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return clients[cacheKey];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Stream a message using OpenAI-compatible API (Groq, Gemini, OpenRouter, Ollama).
|
|
21
|
+
* System prompt becomes a system message.
|
|
22
|
+
*/
|
|
23
|
+
async function streamMessage(opts) {
|
|
24
|
+
const {
|
|
25
|
+
apiKey,
|
|
26
|
+
baseURL,
|
|
27
|
+
systemPrompt,
|
|
28
|
+
messages,
|
|
29
|
+
maxTokens,
|
|
30
|
+
model,
|
|
31
|
+
onText,
|
|
32
|
+
onStart,
|
|
33
|
+
onEnd,
|
|
34
|
+
} = opts;
|
|
35
|
+
|
|
36
|
+
const openai = getClient(baseURL, apiKey);
|
|
37
|
+
|
|
38
|
+
// Convert Anthropic-style (separate system) to OpenAI-style (system message)
|
|
39
|
+
const fullMessages = [
|
|
40
|
+
{ role: 'system', content: systemPrompt },
|
|
41
|
+
...messages,
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
let fullText = '';
|
|
45
|
+
|
|
46
|
+
const stream = await openai.chat.completions.create({
|
|
47
|
+
model,
|
|
48
|
+
max_tokens: maxTokens,
|
|
49
|
+
messages: fullMessages,
|
|
50
|
+
stream: true,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
onStart();
|
|
54
|
+
|
|
55
|
+
for await (const chunk of stream) {
|
|
56
|
+
const text = chunk.choices[0]?.delta?.content;
|
|
57
|
+
if (text) {
|
|
58
|
+
fullText += text;
|
|
59
|
+
onText(text);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
onEnd();
|
|
64
|
+
return fullText;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Send a single message (non-streaming).
|
|
69
|
+
*/
|
|
70
|
+
async function sendMessage(opts) {
|
|
71
|
+
const {
|
|
72
|
+
apiKey,
|
|
73
|
+
baseURL,
|
|
74
|
+
systemPrompt,
|
|
75
|
+
messages,
|
|
76
|
+
maxTokens,
|
|
77
|
+
model,
|
|
78
|
+
} = opts;
|
|
79
|
+
|
|
80
|
+
const openai = getClient(baseURL, apiKey);
|
|
81
|
+
|
|
82
|
+
const fullMessages = [
|
|
83
|
+
{ role: 'system', content: systemPrompt },
|
|
84
|
+
...messages,
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const response = await openai.chat.completions.create({
|
|
88
|
+
model,
|
|
89
|
+
max_tokens: maxTokens,
|
|
90
|
+
messages: fullMessages,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return response.choices[0]?.message?.content || '';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { streamMessage, sendMessage };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Provider Registry — defines all supported AI providers.
|
|
5
|
+
*
|
|
6
|
+
* Each provider has:
|
|
7
|
+
* type — 'anthropic' or 'openai' (OpenAI-compatible API)
|
|
8
|
+
* name — Display name
|
|
9
|
+
* base_url — API endpoint (for openai-type providers)
|
|
10
|
+
* default_model — Fallback model
|
|
11
|
+
* models — Available models
|
|
12
|
+
* key_prefix — Expected API key prefix (for validation hint)
|
|
13
|
+
* key_env — Environment variable to check for API key
|
|
14
|
+
* key_url — Where to get an API key
|
|
15
|
+
* requires_key — false for providers that don't need a key (Ollama)
|
|
16
|
+
* free — true if provider has a free tier
|
|
17
|
+
*/
|
|
18
|
+
const PROVIDERS = {
|
|
19
|
+
groq: {
|
|
20
|
+
name: 'Groq',
|
|
21
|
+
type: 'openai',
|
|
22
|
+
base_url: 'https://api.groq.com/openai/v1',
|
|
23
|
+
default_model: 'llama-3.3-70b-versatile',
|
|
24
|
+
models: [
|
|
25
|
+
'llama-3.3-70b-versatile',
|
|
26
|
+
'llama-3.1-8b-instant',
|
|
27
|
+
'mixtral-8x7b-32768',
|
|
28
|
+
'gemma2-9b-it',
|
|
29
|
+
],
|
|
30
|
+
key_prefix: 'gsk_',
|
|
31
|
+
key_env: 'GROQ_API_KEY',
|
|
32
|
+
key_url: 'https://console.groq.com/',
|
|
33
|
+
requires_key: true,
|
|
34
|
+
free: true,
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
anthropic: {
|
|
38
|
+
name: 'Anthropic (Claude)',
|
|
39
|
+
type: 'anthropic',
|
|
40
|
+
base_url: null,
|
|
41
|
+
default_model: 'claude-sonnet-4-20250514',
|
|
42
|
+
models: [
|
|
43
|
+
'claude-sonnet-4-20250514',
|
|
44
|
+
'claude-haiku-4-20250514',
|
|
45
|
+
],
|
|
46
|
+
key_prefix: 'sk-ant-',
|
|
47
|
+
key_env: 'ANTHROPIC_API_KEY',
|
|
48
|
+
key_url: 'https://console.anthropic.com/',
|
|
49
|
+
requires_key: true,
|
|
50
|
+
free: false,
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
gemini: {
|
|
54
|
+
name: 'Google Gemini',
|
|
55
|
+
type: 'openai',
|
|
56
|
+
base_url: 'https://generativelanguage.googleapis.com/v1beta/openai/',
|
|
57
|
+
default_model: 'gemini-2.0-flash',
|
|
58
|
+
models: [
|
|
59
|
+
'gemini-2.0-flash',
|
|
60
|
+
'gemini-1.5-flash',
|
|
61
|
+
'gemini-1.5-pro',
|
|
62
|
+
],
|
|
63
|
+
key_prefix: '',
|
|
64
|
+
key_env: 'GEMINI_API_KEY',
|
|
65
|
+
key_url: 'https://aistudio.google.com/',
|
|
66
|
+
requires_key: true,
|
|
67
|
+
free: true,
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
openrouter: {
|
|
71
|
+
name: 'OpenRouter',
|
|
72
|
+
type: 'openai',
|
|
73
|
+
base_url: 'https://openrouter.ai/api/v1',
|
|
74
|
+
default_model: 'meta-llama/llama-3.3-70b-instruct:free',
|
|
75
|
+
models: [
|
|
76
|
+
'meta-llama/llama-3.3-70b-instruct:free',
|
|
77
|
+
'mistralai/mistral-7b-instruct:free',
|
|
78
|
+
'google/gemma-2-9b-it:free',
|
|
79
|
+
],
|
|
80
|
+
key_prefix: 'sk-or-',
|
|
81
|
+
key_env: 'OPENROUTER_API_KEY',
|
|
82
|
+
key_url: 'https://openrouter.ai/',
|
|
83
|
+
requires_key: true,
|
|
84
|
+
free: true,
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
ollama: {
|
|
88
|
+
name: 'Ollama (Local)',
|
|
89
|
+
type: 'openai',
|
|
90
|
+
base_url: 'http://localhost:11434/v1',
|
|
91
|
+
default_model: 'llama3',
|
|
92
|
+
models: [
|
|
93
|
+
'llama3',
|
|
94
|
+
'llama3.1',
|
|
95
|
+
'codellama',
|
|
96
|
+
'mistral',
|
|
97
|
+
'mixtral',
|
|
98
|
+
'phi3',
|
|
99
|
+
],
|
|
100
|
+
key_prefix: '',
|
|
101
|
+
key_env: '',
|
|
102
|
+
key_url: 'https://ollama.com/',
|
|
103
|
+
requires_key: false,
|
|
104
|
+
free: true,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
function getProviderDef(name) {
|
|
109
|
+
return PROVIDERS[name] || null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getAllProviderNames() {
|
|
113
|
+
return Object.keys(PROVIDERS);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getAllProviders() {
|
|
117
|
+
return PROVIDERS;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
PROVIDERS,
|
|
122
|
+
getProviderDef,
|
|
123
|
+
getAllProviderNames,
|
|
124
|
+
getAllProviders,
|
|
125
|
+
};
|