voyageai-cli 1.22.0 → 1.23.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/package.json +4 -2
- package/src/cli.js +4 -0
- package/src/commands/chat.js +503 -0
- package/src/commands/demo.js +75 -0
- package/src/commands/embed.js +10 -0
- package/src/commands/index.js +1 -1
- package/src/commands/init.js +34 -97
- package/src/commands/mcp-server.js +49 -0
- package/src/commands/ping.js +52 -0
- package/src/commands/pipeline.js +17 -3
- package/src/commands/playground.js +186 -0
- package/src/commands/purge.js +3 -1
- package/src/commands/refresh.js +3 -1
- package/src/commands/rerank.js +10 -0
- package/src/commands/scaffold.js +1 -2
- package/src/lib/chat.js +252 -0
- package/src/lib/codegen.js +5 -4
- package/src/lib/config.js +5 -1
- package/src/lib/cost.js +352 -0
- package/src/lib/explanations.js +260 -0
- package/src/lib/history.js +260 -0
- package/src/lib/llm.js +485 -0
- package/src/lib/preflight.js +281 -0
- package/src/lib/prompt.js +111 -0
- package/src/lib/wizard-cli.js +135 -0
- package/src/lib/wizard-steps-chat.js +171 -0
- package/src/lib/wizard-steps-init.js +174 -0
- package/src/lib/wizard.js +222 -0
- package/src/mcp/schemas/index.js +102 -0
- package/src/mcp/server.js +162 -0
- package/src/mcp/tools/embedding.js +67 -0
- package/src/mcp/tools/ingest.js +89 -0
- package/src/mcp/tools/management.js +132 -0
- package/src/mcp/tools/retrieval.js +209 -0
- package/src/mcp/tools/utility.js +219 -0
- package/src/playground/index.html +1195 -199
package/src/lib/llm.js
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getConfigValue } = require('./config');
|
|
4
|
+
const { loadProject } = require('./project');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* LLM Provider Adapter
|
|
8
|
+
*
|
|
9
|
+
* Provider-agnostic LLM client with streaming support.
|
|
10
|
+
* Uses native fetch — zero new dependencies.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Provider default models
|
|
14
|
+
const PROVIDER_DEFAULTS = {
|
|
15
|
+
anthropic: 'claude-sonnet-4-5-20250929',
|
|
16
|
+
openai: 'gpt-4o',
|
|
17
|
+
ollama: 'llama3.1',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const PROVIDER_BASE_URLS = {
|
|
21
|
+
anthropic: 'https://api.anthropic.com',
|
|
22
|
+
openai: 'https://api.openai.com',
|
|
23
|
+
ollama: 'http://localhost:11434',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve LLM configuration from CLI opts, env, config, and project.
|
|
28
|
+
* @param {object} [opts] - CLI options
|
|
29
|
+
* @returns {{ provider: string|null, apiKey: string|null, model: string, baseUrl: string }}
|
|
30
|
+
*/
|
|
31
|
+
function resolveLLMConfig(opts = {}) {
|
|
32
|
+
const { config: proj } = loadProject();
|
|
33
|
+
const chatConf = proj.chat || {};
|
|
34
|
+
|
|
35
|
+
const provider =
|
|
36
|
+
opts.llmProvider ||
|
|
37
|
+
process.env.VAI_LLM_PROVIDER ||
|
|
38
|
+
getConfigValue('llmProvider') ||
|
|
39
|
+
chatConf.provider ||
|
|
40
|
+
null;
|
|
41
|
+
|
|
42
|
+
const apiKey =
|
|
43
|
+
opts.llmApiKey ||
|
|
44
|
+
process.env.VAI_LLM_API_KEY ||
|
|
45
|
+
getConfigValue('llmApiKey') ||
|
|
46
|
+
null;
|
|
47
|
+
|
|
48
|
+
const model =
|
|
49
|
+
opts.llmModel ||
|
|
50
|
+
process.env.VAI_LLM_MODEL ||
|
|
51
|
+
getConfigValue('llmModel') ||
|
|
52
|
+
chatConf.model ||
|
|
53
|
+
(provider ? PROVIDER_DEFAULTS[provider] : null) ||
|
|
54
|
+
null;
|
|
55
|
+
|
|
56
|
+
const baseUrl =
|
|
57
|
+
opts.llmBaseUrl ||
|
|
58
|
+
process.env.VAI_LLM_BASE_URL ||
|
|
59
|
+
getConfigValue('llmBaseUrl') ||
|
|
60
|
+
(provider ? PROVIDER_BASE_URLS[provider] : null) ||
|
|
61
|
+
null;
|
|
62
|
+
|
|
63
|
+
return { provider, apiKey, model, baseUrl };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create an LLM provider instance.
|
|
68
|
+
* @param {object} [opts] - CLI options for overrides
|
|
69
|
+
* @returns {LLMProvider}
|
|
70
|
+
*/
|
|
71
|
+
function createLLMProvider(opts = {}) {
|
|
72
|
+
const config = resolveLLMConfig(opts);
|
|
73
|
+
|
|
74
|
+
if (!config.provider) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
switch (config.provider) {
|
|
79
|
+
case 'anthropic':
|
|
80
|
+
return new AnthropicProvider(config);
|
|
81
|
+
case 'openai':
|
|
82
|
+
return new OpenAIProvider(config);
|
|
83
|
+
case 'ollama':
|
|
84
|
+
return new OllamaProvider(config);
|
|
85
|
+
default:
|
|
86
|
+
throw new Error(`Unknown LLM provider: "${config.provider}". Supported: anthropic, openai, ollama`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================
|
|
91
|
+
// Anthropic Provider
|
|
92
|
+
// ============================================
|
|
93
|
+
|
|
94
|
+
class AnthropicProvider {
|
|
95
|
+
constructor(config) {
|
|
96
|
+
this.name = 'anthropic';
|
|
97
|
+
this.model = config.model || PROVIDER_DEFAULTS.anthropic;
|
|
98
|
+
this.apiKey = config.apiKey;
|
|
99
|
+
this.baseUrl = config.baseUrl || PROVIDER_BASE_URLS.anthropic;
|
|
100
|
+
|
|
101
|
+
if (!this.apiKey) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
'Anthropic API key required.\n' +
|
|
104
|
+
' vai config set llm-api-key YOUR_KEY\n' +
|
|
105
|
+
' or: export VAI_LLM_API_KEY=YOUR_KEY'
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async *chat(messages, options = {}) {
|
|
111
|
+
const model = options.model || this.model;
|
|
112
|
+
const maxTokens = options.maxTokens || 4096;
|
|
113
|
+
const stream = options.stream !== false;
|
|
114
|
+
|
|
115
|
+
// Anthropic uses separate system param
|
|
116
|
+
const systemMsg = messages.find(m => m.role === 'system');
|
|
117
|
+
const nonSystemMsgs = messages.filter(m => m.role !== 'system');
|
|
118
|
+
|
|
119
|
+
const body = {
|
|
120
|
+
model,
|
|
121
|
+
max_tokens: maxTokens,
|
|
122
|
+
stream,
|
|
123
|
+
messages: nonSystemMsgs,
|
|
124
|
+
};
|
|
125
|
+
if (systemMsg) {
|
|
126
|
+
body.system = systemMsg.content;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const res = await fetch(`${this.baseUrl}/v1/messages`, {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: {
|
|
132
|
+
'Content-Type': 'application/json',
|
|
133
|
+
'x-api-key': this.apiKey,
|
|
134
|
+
'anthropic-version': '2023-06-01',
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify(body),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
const errBody = await res.text();
|
|
141
|
+
throw new Error(`Anthropic API error (${res.status}): ${errBody}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!stream) {
|
|
145
|
+
const json = await res.json();
|
|
146
|
+
const text = json.content?.[0]?.text || '';
|
|
147
|
+
yield text;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
yield* parseSSE(res.body, (event, data) => {
|
|
152
|
+
if (event === 'content_block_delta' && data.delta?.text) {
|
|
153
|
+
return data.delta.text;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async ping() {
|
|
160
|
+
try {
|
|
161
|
+
const res = await fetch(`${this.baseUrl}/v1/messages`, {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: {
|
|
164
|
+
'Content-Type': 'application/json',
|
|
165
|
+
'x-api-key': this.apiKey,
|
|
166
|
+
'anthropic-version': '2023-06-01',
|
|
167
|
+
},
|
|
168
|
+
body: JSON.stringify({
|
|
169
|
+
model: this.model,
|
|
170
|
+
max_tokens: 1,
|
|
171
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
if (res.ok) {
|
|
175
|
+
return { ok: true, model: this.model };
|
|
176
|
+
}
|
|
177
|
+
const errBody = await res.text();
|
|
178
|
+
return { ok: false, model: this.model, error: `HTTP ${res.status}: ${errBody.substring(0, 200)}` };
|
|
179
|
+
} catch (err) {
|
|
180
|
+
return { ok: false, model: this.model, error: err.message };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ============================================
|
|
186
|
+
// OpenAI Provider
|
|
187
|
+
// ============================================
|
|
188
|
+
|
|
189
|
+
class OpenAIProvider {
|
|
190
|
+
constructor(config) {
|
|
191
|
+
this.name = 'openai';
|
|
192
|
+
this.model = config.model || PROVIDER_DEFAULTS.openai;
|
|
193
|
+
this.apiKey = config.apiKey;
|
|
194
|
+
this.baseUrl = config.baseUrl || PROVIDER_BASE_URLS.openai;
|
|
195
|
+
|
|
196
|
+
if (!this.apiKey) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
'OpenAI API key required.\n' +
|
|
199
|
+
' vai config set llm-api-key YOUR_KEY\n' +
|
|
200
|
+
' or: export VAI_LLM_API_KEY=YOUR_KEY'
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async *chat(messages, options = {}) {
|
|
206
|
+
const model = options.model || this.model;
|
|
207
|
+
const maxTokens = options.maxTokens || 4096;
|
|
208
|
+
const stream = options.stream !== false;
|
|
209
|
+
|
|
210
|
+
const body = {
|
|
211
|
+
model,
|
|
212
|
+
max_tokens: maxTokens,
|
|
213
|
+
stream,
|
|
214
|
+
messages,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
|
218
|
+
method: 'POST',
|
|
219
|
+
headers: {
|
|
220
|
+
'Content-Type': 'application/json',
|
|
221
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
222
|
+
},
|
|
223
|
+
body: JSON.stringify(body),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (!res.ok) {
|
|
227
|
+
const errBody = await res.text();
|
|
228
|
+
throw new Error(`OpenAI API error (${res.status}): ${errBody}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!stream) {
|
|
232
|
+
const json = await res.json();
|
|
233
|
+
const text = json.choices?.[0]?.message?.content || '';
|
|
234
|
+
yield text;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
yield* parseSSE(res.body, (_event, data) => {
|
|
239
|
+
if (data === '[DONE]') return null;
|
|
240
|
+
const content = data.choices?.[0]?.delta?.content;
|
|
241
|
+
return content || null;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async ping() {
|
|
246
|
+
try {
|
|
247
|
+
const res = await fetch(`${this.baseUrl}/v1/models`, {
|
|
248
|
+
headers: { 'Authorization': `Bearer ${this.apiKey}` },
|
|
249
|
+
});
|
|
250
|
+
if (res.ok) {
|
|
251
|
+
return { ok: true, model: this.model };
|
|
252
|
+
}
|
|
253
|
+
return { ok: false, model: this.model, error: `HTTP ${res.status}` };
|
|
254
|
+
} catch (err) {
|
|
255
|
+
return { ok: false, model: this.model, error: err.message };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ============================================
|
|
261
|
+
// Ollama Provider
|
|
262
|
+
// ============================================
|
|
263
|
+
|
|
264
|
+
class OllamaProvider {
|
|
265
|
+
constructor(config) {
|
|
266
|
+
this.name = 'ollama';
|
|
267
|
+
this.model = config.model || PROVIDER_DEFAULTS.ollama;
|
|
268
|
+
this.baseUrl = config.baseUrl || PROVIDER_BASE_URLS.ollama;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async *chat(messages, options = {}) {
|
|
272
|
+
const model = options.model || this.model;
|
|
273
|
+
const stream = options.stream !== false;
|
|
274
|
+
|
|
275
|
+
const body = {
|
|
276
|
+
model,
|
|
277
|
+
stream,
|
|
278
|
+
messages,
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
|
282
|
+
method: 'POST',
|
|
283
|
+
headers: { 'Content-Type': 'application/json' },
|
|
284
|
+
body: JSON.stringify(body),
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
if (!res.ok) {
|
|
288
|
+
const errBody = await res.text();
|
|
289
|
+
throw new Error(`Ollama API error (${res.status}): ${errBody}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!stream) {
|
|
293
|
+
const json = await res.json();
|
|
294
|
+
const text = json.choices?.[0]?.message?.content || '';
|
|
295
|
+
yield text;
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
yield* parseSSE(res.body, (_event, data) => {
|
|
300
|
+
if (data === '[DONE]') return null;
|
|
301
|
+
const content = data.choices?.[0]?.delta?.content;
|
|
302
|
+
return content || null;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async ping() {
|
|
307
|
+
try {
|
|
308
|
+
const res = await fetch(`${this.baseUrl}/v1/models`);
|
|
309
|
+
if (res.ok) {
|
|
310
|
+
return { ok: true, model: this.model };
|
|
311
|
+
}
|
|
312
|
+
return { ok: false, model: this.model, error: `HTTP ${res.status}` };
|
|
313
|
+
} catch (err) {
|
|
314
|
+
return { ok: false, model: this.model, error: err.message };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ============================================
|
|
320
|
+
// SSE Stream Parser
|
|
321
|
+
// ============================================
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Parse a Server-Sent Events stream.
|
|
325
|
+
* @param {ReadableStream} body - Response body stream
|
|
326
|
+
* @param {function} extractor - (event, parsedData) => string|null
|
|
327
|
+
* @yields {string} Text chunks
|
|
328
|
+
*/
|
|
329
|
+
async function* parseSSE(body, extractor) {
|
|
330
|
+
const decoder = new TextDecoder();
|
|
331
|
+
let buffer = '';
|
|
332
|
+
let currentEvent = null;
|
|
333
|
+
|
|
334
|
+
for await (const chunk of body) {
|
|
335
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
336
|
+
|
|
337
|
+
const lines = buffer.split('\n');
|
|
338
|
+
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
339
|
+
|
|
340
|
+
for (const line of lines) {
|
|
341
|
+
if (line.startsWith('event: ')) {
|
|
342
|
+
currentEvent = line.slice(7).trim();
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (line.startsWith('data: ')) {
|
|
347
|
+
const rawData = line.slice(6);
|
|
348
|
+
|
|
349
|
+
if (rawData === '[DONE]') {
|
|
350
|
+
const result = extractor(currentEvent, '[DONE]');
|
|
351
|
+
if (result) yield result;
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let parsed;
|
|
356
|
+
try {
|
|
357
|
+
parsed = JSON.parse(rawData);
|
|
358
|
+
} catch {
|
|
359
|
+
continue; // Skip non-JSON data lines
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const result = extractor(currentEvent, parsed);
|
|
363
|
+
if (result) yield result;
|
|
364
|
+
currentEvent = null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Process remaining buffer
|
|
370
|
+
if (buffer.trim()) {
|
|
371
|
+
if (buffer.startsWith('data: ')) {
|
|
372
|
+
const rawData = buffer.slice(6);
|
|
373
|
+
if (rawData !== '[DONE]') {
|
|
374
|
+
try {
|
|
375
|
+
const parsed = JSON.parse(rawData);
|
|
376
|
+
const result = extractor(currentEvent, parsed);
|
|
377
|
+
if (result) yield result;
|
|
378
|
+
} catch { /* skip */ }
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ============================================
|
|
385
|
+
// Model Discovery
|
|
386
|
+
// ============================================
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Known cloud provider models (curated, updated periodically).
|
|
390
|
+
* These don't require an API call to discover.
|
|
391
|
+
*/
|
|
392
|
+
const PROVIDER_MODELS = {
|
|
393
|
+
anthropic: [
|
|
394
|
+
{ id: 'claude-sonnet-4-5-20250929', name: 'Claude Sonnet 4.5', context: '200K' },
|
|
395
|
+
{ id: 'claude-opus-4-20250514', name: 'Claude Opus 4', context: '200K' },
|
|
396
|
+
{ id: 'claude-3-5-haiku-20241022', name: 'Claude 3.5 Haiku', context: '200K' },
|
|
397
|
+
],
|
|
398
|
+
openai: [
|
|
399
|
+
{ id: 'gpt-4o', name: 'GPT-4o', context: '128K' },
|
|
400
|
+
{ id: 'gpt-4o-mini', name: 'GPT-4o Mini', context: '128K' },
|
|
401
|
+
{ id: 'gpt-4-turbo', name: 'GPT-4 Turbo', context: '128K' },
|
|
402
|
+
{ id: 'o1', name: 'o1', context: '200K' },
|
|
403
|
+
{ id: 'o1-mini', name: 'o1 Mini', context: '128K' },
|
|
404
|
+
{ id: 'o3-mini', name: 'o3 Mini', context: '200K' },
|
|
405
|
+
],
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* List available models for a provider.
|
|
410
|
+
* - For Ollama: queries the local API for installed models
|
|
411
|
+
* - For cloud providers: returns the curated list
|
|
412
|
+
*
|
|
413
|
+
* @param {string} provider - 'anthropic' | 'openai' | 'ollama'
|
|
414
|
+
* @param {object} [opts]
|
|
415
|
+
* @param {string} [opts.baseUrl] - Ollama base URL override
|
|
416
|
+
* @param {number} [opts.timeoutMs] - Timeout for Ollama discovery (default 3000)
|
|
417
|
+
* @returns {Promise<Array<{id: string, name: string, size?: string, context?: string}>>}
|
|
418
|
+
*/
|
|
419
|
+
async function listModels(provider, opts = {}) {
|
|
420
|
+
if (provider === 'ollama') {
|
|
421
|
+
return listOllamaModels(opts);
|
|
422
|
+
}
|
|
423
|
+
return PROVIDER_MODELS[provider] || [];
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Query Ollama for locally installed models.
|
|
428
|
+
* @param {object} [opts]
|
|
429
|
+
* @returns {Promise<Array<{id: string, name: string, size: string, modified: string}>>}
|
|
430
|
+
*/
|
|
431
|
+
async function listOllamaModels(opts = {}) {
|
|
432
|
+
const baseUrl = opts.baseUrl || resolveLLMConfig({ llmProvider: 'ollama' }).baseUrl || 'http://localhost:11434';
|
|
433
|
+
const timeoutMs = opts.timeoutMs || 3000;
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
const controller = new AbortController();
|
|
437
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
438
|
+
|
|
439
|
+
const res = await fetch(`${baseUrl}/api/tags`, {
|
|
440
|
+
signal: controller.signal,
|
|
441
|
+
});
|
|
442
|
+
clearTimeout(timer);
|
|
443
|
+
|
|
444
|
+
if (!res.ok) return [];
|
|
445
|
+
|
|
446
|
+
const data = await res.json();
|
|
447
|
+
const models = (data.models || []).map(m => ({
|
|
448
|
+
id: m.name,
|
|
449
|
+
name: m.name.split(':')[0],
|
|
450
|
+
size: formatBytes(m.size),
|
|
451
|
+
modified: m.modified_at,
|
|
452
|
+
parameterSize: m.details?.parameter_size || null,
|
|
453
|
+
family: m.details?.family || null,
|
|
454
|
+
quantization: m.details?.quantization_level || null,
|
|
455
|
+
}));
|
|
456
|
+
|
|
457
|
+
// Sort by name, with latest tags first
|
|
458
|
+
models.sort((a, b) => a.name.localeCompare(b.name));
|
|
459
|
+
return models;
|
|
460
|
+
} catch {
|
|
461
|
+
return []; // Ollama not running or unreachable
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Format bytes to human-readable string.
|
|
467
|
+
* @param {number} bytes
|
|
468
|
+
* @returns {string}
|
|
469
|
+
*/
|
|
470
|
+
function formatBytes(bytes) {
|
|
471
|
+
if (!bytes) return '';
|
|
472
|
+
if (bytes >= 1e9) return (bytes / 1e9).toFixed(1) + ' GB';
|
|
473
|
+
if (bytes >= 1e6) return (bytes / 1e6).toFixed(1) + ' MB';
|
|
474
|
+
return (bytes / 1e3).toFixed(0) + ' KB';
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
module.exports = {
|
|
478
|
+
createLLMProvider,
|
|
479
|
+
resolveLLMConfig,
|
|
480
|
+
listModels,
|
|
481
|
+
listOllamaModels,
|
|
482
|
+
PROVIDER_DEFAULTS,
|
|
483
|
+
PROVIDER_BASE_URLS,
|
|
484
|
+
PROVIDER_MODELS,
|
|
485
|
+
};
|