recoder-code 2.4.6 ā 2.5.0
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/src/commands/agents/create.d.ts +4 -0
- package/dist/src/commands/agents/create.js +175 -0
- package/dist/src/commands/agents.js +15 -1
- package/dist/src/commands/connect-cmd.d.ts +5 -0
- package/dist/src/commands/connect-cmd.js +12 -0
- package/dist/src/commands/connect.d.ts +2 -5
- package/dist/src/commands/connect.js +78 -353
- package/dist/src/commands/models/compare.d.ts +4 -0
- package/dist/src/commands/models/compare.js +92 -0
- package/dist/src/commands/models/select.d.ts +4 -0
- package/dist/src/commands/models/select.js +62 -0
- package/dist/src/commands/models-cmd.js +20 -0
- package/dist/src/commands/providers/health.d.ts +4 -0
- package/dist/src/commands/providers/health.js +90 -0
- package/dist/src/commands/providers.js +51 -0
- package/dist/src/config/config.js +4 -1
- package/dist/src/gemini.js +20 -0
- package/dist/src/providers/custom-provider-manager.d.ts +18 -0
- package/dist/src/providers/custom-provider-manager.js +105 -0
- package/dist/src/providers/local-detection.d.ts +6 -0
- package/dist/src/providers/local-detection.js +86 -0
- package/dist/src/providers/registry.js +4 -19
- package/dist/src/services/RecoderAuthService.js +25 -17
- package/dist/src/zed-integration/schema.d.ts +1466 -1466
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -1,363 +1,88 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Interactive provider configuration command
|
|
3
3
|
*/
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
else if (provider.id === 'anthropic') {
|
|
42
|
-
url = 'https://api.anthropic.com/v1/messages';
|
|
43
|
-
headers['x-api-key'] = apiKey;
|
|
44
|
-
headers['anthropic-version'] = '2023-06-01';
|
|
45
|
-
// For Anthropic, we just check if key format is valid
|
|
46
|
-
if (apiKey.startsWith('sk-ant-')) {
|
|
47
|
-
return { provider, available: true, latency: Date.now() - start };
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import { customProviderManager } from '../providers/custom-provider-manager.js';
|
|
6
|
+
export async function connectProvider() {
|
|
7
|
+
console.log('š Add Custom Provider\n');
|
|
8
|
+
const answers = await inquirer.prompt([
|
|
9
|
+
{
|
|
10
|
+
type: 'input',
|
|
11
|
+
name: 'id',
|
|
12
|
+
message: 'Provider ID (e.g., my-llm):',
|
|
13
|
+
validate: (input) => input.length > 0 || 'ID is required',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
type: 'input',
|
|
17
|
+
name: 'name',
|
|
18
|
+
message: 'Provider Name:',
|
|
19
|
+
validate: (input) => input.length > 0 || 'Name is required',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
type: 'list',
|
|
23
|
+
name: 'engine',
|
|
24
|
+
message: 'API Compatibility:',
|
|
25
|
+
choices: [
|
|
26
|
+
{ name: 'OpenAI-compatible (LM Studio, vLLM, llama.cpp)', value: 'openai' },
|
|
27
|
+
{ name: 'Anthropic-compatible', value: 'anthropic' },
|
|
28
|
+
{ name: 'Ollama-compatible', value: 'ollama' },
|
|
29
|
+
{ name: 'Google Gemini-compatible', value: 'google' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
type: 'input',
|
|
34
|
+
name: 'baseUrl',
|
|
35
|
+
message: 'Base URL:',
|
|
36
|
+
default: 'http://localhost:1234/v1',
|
|
37
|
+
validate: (input) => {
|
|
38
|
+
try {
|
|
39
|
+
new URL(input);
|
|
40
|
+
return true;
|
|
48
41
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
url = 'https://api.groq.com/openai/v1/models';
|
|
52
|
-
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
// Generic OpenAI-compatible check
|
|
56
|
-
url = `${provider.baseUrl}/models`;
|
|
57
|
-
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
const response = await fetch(url, {
|
|
61
|
-
headers,
|
|
62
|
-
signal: AbortSignal.timeout(5000),
|
|
63
|
-
});
|
|
64
|
-
const latency = Date.now() - start;
|
|
65
|
-
if (response.ok || response.status === 401) {
|
|
66
|
-
// 401 means the endpoint exists but key is wrong
|
|
67
|
-
return {
|
|
68
|
-
provider,
|
|
69
|
-
available: response.ok,
|
|
70
|
-
latency,
|
|
71
|
-
error: response.status === 401 ? 'Invalid API key' : undefined
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
return { provider, available: false, latency, error: `HTTP ${response.status}` };
|
|
75
|
-
}
|
|
76
|
-
catch (error) {
|
|
77
|
-
return {
|
|
78
|
-
provider,
|
|
79
|
-
available: false,
|
|
80
|
-
error: error.code === 'ECONNREFUSED' ? 'Not running' : error.message
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Interactive connect command
|
|
86
|
-
*/
|
|
87
|
-
export async function connectCommand(args = []) {
|
|
88
|
-
const registry = getProviderRegistry();
|
|
89
|
-
const subcommand = args[0];
|
|
90
|
-
if (!subcommand || subcommand === 'status') {
|
|
91
|
-
await showProviderStatus();
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
if (subcommand === 'add') {
|
|
95
|
-
await addProvider(args[1]);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
if (subcommand === 'test') {
|
|
99
|
-
await testProvider(args[1]);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
if (subcommand === 'local') {
|
|
103
|
-
await detectLocalProviders();
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
if (subcommand === 'custom') {
|
|
107
|
-
await addCustomProvider();
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
// If provider name given, configure it
|
|
111
|
-
await configureProviderKey(subcommand);
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Show status of all providers
|
|
115
|
-
*/
|
|
116
|
-
async function showProviderStatus() {
|
|
117
|
-
const registry = getProviderRegistry();
|
|
118
|
-
const providers = registry.getAllProviders();
|
|
119
|
-
console.log(chalk.bold.cyan('\nš Provider Status\n'));
|
|
120
|
-
// Group by type
|
|
121
|
-
const local = providers.filter(p => p.isLocal);
|
|
122
|
-
const cloud = providers.filter(p => !p.isLocal && p.isBuiltin);
|
|
123
|
-
const custom = providers.filter(p => !p.isBuiltin);
|
|
124
|
-
// Check local providers
|
|
125
|
-
console.log(chalk.bold('Local AI:'));
|
|
126
|
-
for (const provider of local) {
|
|
127
|
-
const status = await checkProviderHealth(provider);
|
|
128
|
-
const icon = status.available ? chalk.green('ā') : chalk.gray('ā');
|
|
129
|
-
const latencyStr = status.latency ? chalk.gray(` (${status.latency}ms)`) : '';
|
|
130
|
-
const errorStr = status.error ? chalk.red(` - ${status.error}`) : '';
|
|
131
|
-
console.log(` ${icon} ${provider.name}${latencyStr}${errorStr}`);
|
|
132
|
-
}
|
|
133
|
-
// Check cloud providers
|
|
134
|
-
console.log(chalk.bold('\nCloud Providers:'));
|
|
135
|
-
for (const provider of cloud) {
|
|
136
|
-
const hasKey = provider.apiKeyEnv ? !!process.env[provider.apiKeyEnv] : false;
|
|
137
|
-
const icon = hasKey ? chalk.green('ā') : chalk.gray('ā');
|
|
138
|
-
const keyStatus = hasKey ? chalk.gray(' (configured)') : chalk.yellow(' (no key)');
|
|
139
|
-
console.log(` ${icon} ${provider.name}${keyStatus}`);
|
|
140
|
-
}
|
|
141
|
-
// Custom providers
|
|
142
|
-
if (custom.length > 0) {
|
|
143
|
-
console.log(chalk.bold('\nCustom Providers:'));
|
|
144
|
-
for (const provider of custom) {
|
|
145
|
-
const status = await checkProviderHealth(provider);
|
|
146
|
-
const icon = status.available ? chalk.green('ā') : chalk.gray('ā');
|
|
147
|
-
console.log(` ${icon} ${provider.name} (${provider.baseUrl})`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
console.log(chalk.gray('\nCommands:'));
|
|
151
|
-
console.log(chalk.gray(' /connect <provider> - Configure API key'));
|
|
152
|
-
console.log(chalk.gray(' /connect local - Detect local AI servers'));
|
|
153
|
-
console.log(chalk.gray(' /connect custom - Add custom provider'));
|
|
154
|
-
console.log(chalk.gray(' /connect test <provider> - Test connection'));
|
|
155
|
-
console.log();
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Configure provider API key
|
|
159
|
-
*/
|
|
160
|
-
async function configureProviderKey(providerId) {
|
|
161
|
-
const registry = getProviderRegistry();
|
|
162
|
-
const provider = registry.getProvider(providerId);
|
|
163
|
-
if (!provider) {
|
|
164
|
-
console.log(chalk.red(`\nā Unknown provider: ${providerId}`));
|
|
165
|
-
console.log(chalk.gray('Run /connect to see available providers\n'));
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
if (provider.isLocal) {
|
|
169
|
-
console.log(chalk.yellow(`\n${provider.name} is a local provider - no API key needed.`));
|
|
170
|
-
console.log(chalk.gray('Run /connect local to check if it\'s running.\n'));
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
if (!provider.apiKeyEnv) {
|
|
174
|
-
console.log(chalk.yellow(`\n${provider.name} doesn't require an API key.\n`));
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
178
|
-
const question = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
179
|
-
console.log(chalk.bold.cyan(`\nāļø Configure ${provider.name}\n`));
|
|
180
|
-
// Show help for getting API key
|
|
181
|
-
const keyUrls = {
|
|
182
|
-
openrouter: 'https://openrouter.ai/keys',
|
|
183
|
-
anthropic: 'https://console.anthropic.com/settings/keys',
|
|
184
|
-
openai: 'https://platform.openai.com/api-keys',
|
|
185
|
-
groq: 'https://console.groq.com/keys',
|
|
186
|
-
deepseek: 'https://platform.deepseek.com/api_keys',
|
|
187
|
-
together: 'https://api.together.xyz/settings/api-keys',
|
|
188
|
-
fireworks: 'https://fireworks.ai/account/api-keys',
|
|
189
|
-
mistral: 'https://console.mistral.ai/api-keys',
|
|
190
|
-
google: 'https://aistudio.google.com/apikey',
|
|
191
|
-
};
|
|
192
|
-
if (keyUrls[provider.id]) {
|
|
193
|
-
console.log(chalk.gray(`Get your key: ${keyUrls[provider.id]}\n`));
|
|
194
|
-
}
|
|
195
|
-
const currentKey = process.env[provider.apiKeyEnv];
|
|
196
|
-
if (currentKey) {
|
|
197
|
-
const masked = currentKey.substring(0, 8) + '...' + currentKey.substring(currentKey.length - 4);
|
|
198
|
-
console.log(chalk.gray(`Current key: ${masked}\n`));
|
|
199
|
-
}
|
|
200
|
-
const apiKey = await question(chalk.white(`Enter ${provider.apiKeyEnv}: `));
|
|
201
|
-
rl.close();
|
|
202
|
-
if (!apiKey.trim()) {
|
|
203
|
-
console.log(chalk.yellow('\nNo key provided, skipping.\n'));
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
// Test the key before saving
|
|
207
|
-
console.log(chalk.gray('\nTesting connection...'));
|
|
208
|
-
process.env[provider.apiKeyEnv] = apiKey.trim();
|
|
209
|
-
const status = await checkProviderHealth(provider);
|
|
210
|
-
if (!status.available && status.error === 'Invalid API key') {
|
|
211
|
-
console.log(chalk.red('\nā API key appears to be invalid.'));
|
|
212
|
-
const save = await new Promise((resolve) => {
|
|
213
|
-
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
214
|
-
rl2.question(chalk.yellow('Save anyway? (y/N): '), (answer) => {
|
|
215
|
-
rl2.close();
|
|
216
|
-
resolve(answer);
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
if (save.toLowerCase() !== 'y') {
|
|
220
|
-
console.log(chalk.gray('Key not saved.\n'));
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
else if (status.available) {
|
|
225
|
-
console.log(chalk.green(`ā Connected! (${status.latency}ms)`));
|
|
226
|
-
}
|
|
227
|
-
// Save to env file
|
|
228
|
-
saveApiKey(provider.apiKeyEnv, apiKey.trim());
|
|
229
|
-
console.log(chalk.green(`\nā Saved to ${ENV_FILE}`));
|
|
230
|
-
console.log(chalk.gray(`\nTo use immediately, run:`));
|
|
231
|
-
console.log(chalk.cyan(` export ${provider.apiKeyEnv}="${apiKey.trim()}"\n`));
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Save API key to env file
|
|
235
|
-
*/
|
|
236
|
-
function saveApiKey(envVar, value) {
|
|
237
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
238
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
239
|
-
}
|
|
240
|
-
let envContent = '';
|
|
241
|
-
if (fs.existsSync(ENV_FILE)) {
|
|
242
|
-
envContent = fs.readFileSync(ENV_FILE, 'utf-8');
|
|
243
|
-
}
|
|
244
|
-
const regex = new RegExp(`^${envVar}=.*$`, 'm');
|
|
245
|
-
if (regex.test(envContent)) {
|
|
246
|
-
envContent = envContent.replace(regex, `${envVar}=${value}`);
|
|
247
|
-
}
|
|
248
|
-
else {
|
|
249
|
-
envContent += `\n${envVar}=${value}`;
|
|
250
|
-
}
|
|
251
|
-
fs.writeFileSync(ENV_FILE, envContent.trim() + '\n');
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Detect local AI servers
|
|
255
|
-
*/
|
|
256
|
-
async function detectLocalProviders() {
|
|
257
|
-
console.log(chalk.bold.cyan('\nš Detecting Local AI Servers\n'));
|
|
258
|
-
const localServers = [
|
|
259
|
-
{ name: 'Ollama', url: 'http://localhost:11434/api/tags', port: 11434 },
|
|
260
|
-
{ name: 'LM Studio', url: 'http://localhost:1234/v1/models', port: 1234 },
|
|
261
|
-
{ name: 'llama.cpp', url: 'http://localhost:8080/v1/models', port: 8080 },
|
|
262
|
-
{ name: 'vLLM', url: 'http://localhost:8000/v1/models', port: 8000 },
|
|
263
|
-
{ name: 'LocalAI', url: 'http://localhost:8080/v1/models', port: 8080 },
|
|
264
|
-
];
|
|
265
|
-
for (const server of localServers) {
|
|
266
|
-
try {
|
|
267
|
-
const start = Date.now();
|
|
268
|
-
const response = await fetch(server.url, { signal: AbortSignal.timeout(2000) });
|
|
269
|
-
const latency = Date.now() - start;
|
|
270
|
-
if (response.ok) {
|
|
271
|
-
console.log(chalk.green(`ā ${server.name}`), chalk.gray(`(localhost:${server.port}, ${latency}ms)`));
|
|
272
|
-
// Get model list for Ollama
|
|
273
|
-
if (server.name === 'Ollama') {
|
|
274
|
-
const data = await response.json();
|
|
275
|
-
if (data.models && data.models.length > 0) {
|
|
276
|
-
console.log(chalk.gray(` Models: ${data.models.slice(0, 5).map(m => m.name).join(', ')}${data.models.length > 5 ? '...' : ''}`));
|
|
277
|
-
}
|
|
42
|
+
catch {
|
|
43
|
+
return 'Invalid URL';
|
|
278
44
|
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
console.log(chalk.yellow('\nUsage: /connect test <provider>\n'));
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
const registry = getProviderRegistry();
|
|
298
|
-
const provider = registry.getProvider(providerId);
|
|
299
|
-
if (!provider) {
|
|
300
|
-
console.log(chalk.red(`\nā Unknown provider: ${providerId}\n`));
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
console.log(chalk.cyan(`\nTesting ${provider.name}...`));
|
|
304
|
-
const status = await checkProviderHealth(provider);
|
|
305
|
-
if (status.available) {
|
|
306
|
-
console.log(chalk.green(`ā Connected! (${status.latency}ms)\n`));
|
|
307
|
-
}
|
|
308
|
-
else {
|
|
309
|
-
console.log(chalk.red(`ā Failed: ${status.error}\n`));
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
/**
|
|
313
|
-
* Add custom provider interactively
|
|
314
|
-
*/
|
|
315
|
-
async function addCustomProvider() {
|
|
316
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
317
|
-
const question = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
318
|
-
console.log(chalk.bold.cyan('\nā Add Custom Provider\n'));
|
|
319
|
-
console.log(chalk.gray('Add any OpenAI-compatible API endpoint.\n'));
|
|
320
|
-
const id = await question(chalk.white('Provider ID (e.g., my-api): '));
|
|
321
|
-
if (!id.trim()) {
|
|
322
|
-
rl.close();
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
const name = await question(chalk.white('Display Name: ')) || id;
|
|
326
|
-
const baseUrl = await question(chalk.white('Base URL (e.g., http://localhost:8000/v1): '));
|
|
327
|
-
if (!baseUrl.trim()) {
|
|
328
|
-
console.log(chalk.yellow('\nBase URL required.\n'));
|
|
329
|
-
rl.close();
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
const apiKeyEnv = await question(chalk.white('API Key env var (optional, e.g., MY_API_KEY): '));
|
|
333
|
-
const isLocalStr = await question(chalk.white('Is local server? (y/N): '));
|
|
334
|
-
rl.close();
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: 'confirm',
|
|
49
|
+
name: 'isLocal',
|
|
50
|
+
message: 'Is this a local provider?',
|
|
51
|
+
default: true,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'input',
|
|
55
|
+
name: 'apiKey',
|
|
56
|
+
message: 'API Key (leave empty for local):',
|
|
57
|
+
default: '',
|
|
58
|
+
},
|
|
59
|
+
]);
|
|
335
60
|
const config = {
|
|
336
|
-
id: id
|
|
337
|
-
name: name
|
|
338
|
-
engine:
|
|
339
|
-
baseUrl: baseUrl
|
|
340
|
-
|
|
341
|
-
isLocal: isLocalStr.toLowerCase() === 'y',
|
|
61
|
+
id: answers.id,
|
|
62
|
+
name: answers.name,
|
|
63
|
+
engine: answers.engine,
|
|
64
|
+
baseUrl: answers.baseUrl,
|
|
65
|
+
isLocal: answers.isLocal,
|
|
342
66
|
models: [],
|
|
343
67
|
supportsStreaming: true,
|
|
344
68
|
};
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
console.log(
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
69
|
+
if (answers.apiKey) {
|
|
70
|
+
config.apiKeyEnv = `RECODER_${answers.id.toUpperCase().replace(/-/g, '_')}_API_KEY`;
|
|
71
|
+
console.log(`\nš” Set your API key: export ${config.apiKeyEnv}="${answers.apiKey}"`);
|
|
72
|
+
}
|
|
73
|
+
await customProviderManager.addProvider(config);
|
|
74
|
+
console.log(`\nā
Provider "${answers.name}" added successfully!`);
|
|
75
|
+
console.log(` Use it with: recoder --provider ${answers.id}`);
|
|
76
|
+
// Test connection
|
|
77
|
+
const provider = customProviderManager.get(answers.id);
|
|
78
|
+
if (provider) {
|
|
79
|
+
console.log('\nš Testing connection...');
|
|
80
|
+
const isConnected = await customProviderManager.testConnection(provider);
|
|
81
|
+
if (isConnected) {
|
|
82
|
+
console.log('ā
Connection successful!');
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
console.log('ā ļø Could not connect. Check your configuration.');
|
|
86
|
+
}
|
|
361
87
|
}
|
|
362
|
-
await configureProviderKey(providerId);
|
|
363
88
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-model comparison mode
|
|
3
|
+
*/
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { getProviderRegistry } from '../../providers/registry.js';
|
|
7
|
+
export async function compareModels() {
|
|
8
|
+
console.log(chalk.bold.cyan('\nāļø Multi-Model Comparison\n'));
|
|
9
|
+
const registry = getProviderRegistry();
|
|
10
|
+
const providers = registry.getAllProviders();
|
|
11
|
+
// Get available models
|
|
12
|
+
const modelChoices = [];
|
|
13
|
+
for (const provider of providers) {
|
|
14
|
+
try {
|
|
15
|
+
const models = await registry.getModels(provider.id);
|
|
16
|
+
models.forEach(m => {
|
|
17
|
+
modelChoices.push(`${provider.id}/${m.id}`);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Skip
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (modelChoices.length < 2) {
|
|
25
|
+
console.log(chalk.red('ā Need at least 2 models to compare'));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const answers = await inquirer.prompt([
|
|
29
|
+
{
|
|
30
|
+
type: 'checkbox',
|
|
31
|
+
name: 'models',
|
|
32
|
+
message: 'Select models to compare (2-4):',
|
|
33
|
+
choices: modelChoices.slice(0, 20),
|
|
34
|
+
validate: (input) => {
|
|
35
|
+
if (input.length < 2)
|
|
36
|
+
return 'Select at least 2 models';
|
|
37
|
+
if (input.length > 4)
|
|
38
|
+
return 'Select at most 4 models';
|
|
39
|
+
return true;
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
type: 'input',
|
|
44
|
+
name: 'prompt',
|
|
45
|
+
message: 'Enter prompt to test:',
|
|
46
|
+
validate: (input) => input.length > 0 || 'Prompt required',
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
console.log(chalk.cyan('\nš Running comparison...\n'));
|
|
50
|
+
const results = [];
|
|
51
|
+
for (const model of answers.models) {
|
|
52
|
+
const start = Date.now();
|
|
53
|
+
try {
|
|
54
|
+
// Simulate API call (replace with actual implementation)
|
|
55
|
+
const response = `Response from ${model}`;
|
|
56
|
+
const responseTime = Date.now() - start;
|
|
57
|
+
results.push({
|
|
58
|
+
model,
|
|
59
|
+
response,
|
|
60
|
+
responseTime,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
results.push({
|
|
65
|
+
model,
|
|
66
|
+
response: '',
|
|
67
|
+
responseTime: Date.now() - start,
|
|
68
|
+
error: err.message,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Display results
|
|
73
|
+
console.log(chalk.bold('Comparison Results:'));
|
|
74
|
+
console.log(chalk.gray('ā'.repeat(60)));
|
|
75
|
+
results.forEach((result, i) => {
|
|
76
|
+
console.log(chalk.bold.cyan(`\n${i + 1}. ${result.model}`));
|
|
77
|
+
console.log(chalk.gray(` Response time: ${result.responseTime}ms`));
|
|
78
|
+
if (result.error) {
|
|
79
|
+
console.log(chalk.red(` Error: ${result.error}`));
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.log(chalk.white(` ${result.response.substring(0, 200)}...`));
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
console.log(chalk.gray('\nā'.repeat(60)));
|
|
86
|
+
// Show fastest
|
|
87
|
+
const fastest = results.filter(r => !r.error).sort((a, b) => a.responseTime - b.responseTime)[0];
|
|
88
|
+
if (fastest) {
|
|
89
|
+
console.log(chalk.green(`\nā” Fastest: ${fastest.model} (${fastest.responseTime}ms)`));
|
|
90
|
+
}
|
|
91
|
+
console.log();
|
|
92
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive model selection with fuzzy search
|
|
3
|
+
*/
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import { getProviderRegistry } from '../../providers/registry.js';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
export async function selectModel() {
|
|
8
|
+
console.log(chalk.bold.cyan('\nš¤ Select AI Model\n'));
|
|
9
|
+
const registry = getProviderRegistry();
|
|
10
|
+
const providers = registry.getAllProviders();
|
|
11
|
+
// Gather all models from all providers
|
|
12
|
+
const allModels = [];
|
|
13
|
+
for (const provider of providers) {
|
|
14
|
+
try {
|
|
15
|
+
const models = await registry.getModels(provider.id);
|
|
16
|
+
models.forEach(model => {
|
|
17
|
+
const contextInfo = model.contextLength ? ` (${Math.round(model.contextLength / 1000)}k ctx)` : '';
|
|
18
|
+
const freeTag = model.isFree ? chalk.green(' [FREE]') : '';
|
|
19
|
+
const providerTag = chalk.gray(` - ${provider.name}`);
|
|
20
|
+
allModels.push({
|
|
21
|
+
name: `${model.id}${contextInfo}${freeTag}${providerTag}`,
|
|
22
|
+
value: `${provider.id}/${model.id}`,
|
|
23
|
+
provider: provider.id,
|
|
24
|
+
contextLength: model.contextLength,
|
|
25
|
+
isFree: model.isFree,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
// Skip providers that fail to load models
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (allModels.length === 0) {
|
|
34
|
+
console.log(chalk.red('ā No models available'));
|
|
35
|
+
console.log(chalk.gray(' Run: recoder providers detect'));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// Sort: free models first, then by context length
|
|
39
|
+
allModels.sort((a, b) => {
|
|
40
|
+
if (a.isFree && !b.isFree)
|
|
41
|
+
return -1;
|
|
42
|
+
if (!a.isFree && b.isFree)
|
|
43
|
+
return 1;
|
|
44
|
+
return (b.contextLength || 0) - (a.contextLength || 0);
|
|
45
|
+
});
|
|
46
|
+
const answer = await inquirer.prompt([
|
|
47
|
+
{
|
|
48
|
+
type: 'list',
|
|
49
|
+
name: 'model',
|
|
50
|
+
message: 'Select a model:',
|
|
51
|
+
choices: allModels.slice(0, 30),
|
|
52
|
+
pageSize: 15,
|
|
53
|
+
},
|
|
54
|
+
]);
|
|
55
|
+
const selected = allModels.find(m => m.value === answer.model);
|
|
56
|
+
if (selected) {
|
|
57
|
+
console.log(chalk.green(`\nā
Selected: ${selected.value}`));
|
|
58
|
+
console.log(chalk.cyan('\nš” Usage:'));
|
|
59
|
+
console.log(chalk.gray(` recoder --model ${selected.value}`));
|
|
60
|
+
console.log(chalk.gray(` Or set as default: recoder models set-default ${selected.value}`));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -6,6 +6,8 @@ import * as fs from 'fs';
|
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import * as os from 'os';
|
|
8
8
|
import { getOllamaProvider, getOpenRouterProvider, getAnthropicProvider, getOpenAIProvider, getGroqProvider, parseModelId, } from '../providers/index.js';
|
|
9
|
+
import { selectModel } from './models/select.js';
|
|
10
|
+
import { compareModels } from './models/compare.js';
|
|
9
11
|
const CONFIG_DIR = path.join(os.homedir(), '.recoder-code');
|
|
10
12
|
const CUSTOM_MODELS_FILE = path.join(CONFIG_DIR, 'custom-models.json');
|
|
11
13
|
const DEFAULT_MODEL_FILE = path.join(CONFIG_DIR, 'default-model.json');
|
|
@@ -162,11 +164,29 @@ const setDefaultCommand = {
|
|
|
162
164
|
process.exit(0);
|
|
163
165
|
},
|
|
164
166
|
};
|
|
167
|
+
const selectCommand = {
|
|
168
|
+
command: 'select',
|
|
169
|
+
describe: 'Interactive model selection with fuzzy search',
|
|
170
|
+
handler: async () => {
|
|
171
|
+
await selectModel();
|
|
172
|
+
process.exit(0);
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
const compareCommand = {
|
|
176
|
+
command: 'compare',
|
|
177
|
+
describe: 'Compare multiple models side-by-side',
|
|
178
|
+
handler: async () => {
|
|
179
|
+
await compareModels();
|
|
180
|
+
process.exit(0);
|
|
181
|
+
},
|
|
182
|
+
};
|
|
165
183
|
export const modelsCommand = {
|
|
166
184
|
command: 'models',
|
|
167
185
|
describe: 'Manage AI models',
|
|
168
186
|
builder: (yargs) => yargs
|
|
169
187
|
.command(listCommand)
|
|
188
|
+
.command(selectCommand)
|
|
189
|
+
.command(compareCommand)
|
|
170
190
|
.command(addCommand)
|
|
171
191
|
.command(removeCommand)
|
|
172
192
|
.command(setDefaultCommand)
|