@vierratale/ai 0.1.0-beta.1 → 0.1.0-beta.11
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/README.md +157 -15
- package/bin/vierrataleai.js +74 -1
- package/package.json +10 -2
- package/scripts/postinstall.js +35 -0
- package/src/catalog.js +94 -45
- package/src/cli.js +1098 -51
- package/src/cmd/agent.js +1711 -0
- package/src/cmd/executor.js +257 -0
- package/src/cmd/tools.js +153 -0
- package/src/config.js +54 -15
- package/src/index.js +2 -1
- package/src/installer.js +159 -15
- package/src/prompts/system.json +2 -2
- package/src/prompts/system.md +42 -0
- package/src/providers/anthropic.js +130 -0
- package/src/providers/base.js +18 -0
- package/src/providers/cortex.js +298 -0
- package/src/providers/gemini.js +128 -0
- package/src/providers/index.js +7 -3
- package/src/providers/openai.js +79 -34
- package/src/session.js +221 -0
- package/src/ui/banner.js +23 -12
- package/src/ui/branding.js +8 -9
- package/src/ui/chatbox.js +280 -0
- package/src/ui/input.js +501 -0
- package/src/ui/terminal.js +84 -8
- package/src/utils/downloader.js +364 -0
- package/src/utils/filewriter.js +97 -0
- package/src/utils/intents.js +102 -0
- package/src/utils/logger.js +67 -0
- package/src/utils/math.js +142 -0
- package/src/utils/webfetch.js +667 -0
- package/src/utils/websearch.js +262 -0
- package/build.sh +0 -17
- package/src/providers/ollama.js +0 -89
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { BaseProvider } from './base.js';
|
|
2
|
+
import { Catalog } from '../catalog.js';
|
|
3
|
+
import { Config } from '../config.js';
|
|
4
|
+
|
|
5
|
+
// Keep the prompt a local engine must re-process per call bounded: the
|
|
6
|
+
// system prompt plus the most recent messages. Sending the whole growing
|
|
7
|
+
// history makes prompt-processing time unbounded on slow hardware.
|
|
8
|
+
const MAX_CONTEXT_MESSAGES = 12;
|
|
9
|
+
|
|
10
|
+
function buildEngineMessages(systemPrompt, messages) {
|
|
11
|
+
const engineMessages = [];
|
|
12
|
+
if (systemPrompt) {
|
|
13
|
+
engineMessages.push({ role: 'system', content: systemPrompt });
|
|
14
|
+
}
|
|
15
|
+
for (const msg of messages) {
|
|
16
|
+
engineMessages.push({ role: msg.role, content: msg.content });
|
|
17
|
+
}
|
|
18
|
+
if (engineMessages.length > MAX_CONTEXT_MESSAGES) {
|
|
19
|
+
return [engineMessages[0], ...engineMessages.slice(-(MAX_CONTEXT_MESSAGES - 1))];
|
|
20
|
+
}
|
|
21
|
+
return engineMessages;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class CortexProvider extends BaseProvider {
|
|
25
|
+
constructor() {
|
|
26
|
+
super('cortex');
|
|
27
|
+
this.host = Config.get('engineHost');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get displayName() {
|
|
31
|
+
return 'Cortex';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get isLocal() {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async isAvailable() {
|
|
39
|
+
try {
|
|
40
|
+
const resp = await fetch(`${this.host}/api/tags`, { signal: AbortSignal.timeout(3000) });
|
|
41
|
+
return resp.ok;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async listModels() {
|
|
48
|
+
try {
|
|
49
|
+
const resp = await fetch(`${this.host}/api/tags`);
|
|
50
|
+
if (!resp.ok) return [];
|
|
51
|
+
const data = await resp.json();
|
|
52
|
+
return (data.models || []).map((m) => ({
|
|
53
|
+
realName: m.name,
|
|
54
|
+
displayName: Catalog.getDisplayName(m.name),
|
|
55
|
+
size: m.size,
|
|
56
|
+
}));
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async *stream(messages, options = {}) {
|
|
63
|
+
const model = Catalog.getRealModel(options.model || Config.get('model'));
|
|
64
|
+
const engineMessages = buildEngineMessages(options.systemPrompt || '', messages);
|
|
65
|
+
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const connectTimer = setTimeout(() => controller.abort(), 600000);
|
|
68
|
+
|
|
69
|
+
let resp;
|
|
70
|
+
try {
|
|
71
|
+
resp = await fetch(`${this.host}/api/chat`, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: { 'Content-Type': 'application/json' },
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
model,
|
|
76
|
+
messages: engineMessages,
|
|
77
|
+
stream: true,
|
|
78
|
+
keep_alive: Config.get('keepAlive'),
|
|
79
|
+
options: {
|
|
80
|
+
num_ctx: Config.get('numCtx'),
|
|
81
|
+
num_predict: options.maxTokens ?? Config.get('maxTokens'),
|
|
82
|
+
temperature: options.temperature || Config.get('temperature'),
|
|
83
|
+
num_thread: Config.get('numThreads'),
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
signal: controller.signal,
|
|
87
|
+
});
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (err.name === 'AbortError') {
|
|
90
|
+
throw new Error('[ERR-0001] Could not reach the engine (no response within 600s). Make sure the local engine is running.');
|
|
91
|
+
}
|
|
92
|
+
throw new Error('[ERR-0001] Could not reach the engine (no response). Make sure the local engine is running.');
|
|
93
|
+
} finally {
|
|
94
|
+
clearTimeout(connectTimer);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!resp.ok) {
|
|
98
|
+
throw new Error(await describeEngineError(resp, model));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const reader = resp.body.getReader();
|
|
102
|
+
const decoder = new TextDecoder();
|
|
103
|
+
let buffer = '';
|
|
104
|
+
let idle = null;
|
|
105
|
+
const settle = () => {
|
|
106
|
+
if (idle) clearTimeout(idle);
|
|
107
|
+
idle = setTimeout(() => controller.abort(), 420000);
|
|
108
|
+
};
|
|
109
|
+
settle();
|
|
110
|
+
try {
|
|
111
|
+
while (true) {
|
|
112
|
+
let value, done;
|
|
113
|
+
try {
|
|
114
|
+
({ done, value } = await reader.read());
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (err.name === 'AbortError') {
|
|
117
|
+
throw new Error('[ERR-0001] Lost the connection to the engine (no data for 420s).');
|
|
118
|
+
}
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
if (done) break;
|
|
122
|
+
settle();
|
|
123
|
+
buffer += decoder.decode(value, { stream: true });
|
|
124
|
+
const lines = buffer.split('\n');
|
|
125
|
+
buffer = lines.pop() || '';
|
|
126
|
+
|
|
127
|
+
for (const line of lines) {
|
|
128
|
+
if (!line.trim()) continue;
|
|
129
|
+
try {
|
|
130
|
+
const json = JSON.parse(line);
|
|
131
|
+
if (json.message?.content) {
|
|
132
|
+
yield json.message.content;
|
|
133
|
+
}
|
|
134
|
+
if (json.done) return;
|
|
135
|
+
} catch {}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} finally {
|
|
139
|
+
if (idle) clearTimeout(idle);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Non-streaming completion (used by the tool-planning step). Reads the
|
|
144
|
+
// streamed reply incrementally: on a slow engine a full answer can take
|
|
145
|
+
// minutes, so only a connection with no response for 180s (or an idle
|
|
146
|
+
// stream for 120s) counts as a failure - not a slowly progressing one.
|
|
147
|
+
async complete(messages, options = {}) {
|
|
148
|
+
const model = Catalog.getRealModel(options.model || Config.get('model'));
|
|
149
|
+
const engineMessages = buildEngineMessages(options.systemPrompt || '', messages);
|
|
150
|
+
|
|
151
|
+
const controller = new AbortController();
|
|
152
|
+
const connectTimer = setTimeout(() => controller.abort(), 600000);
|
|
153
|
+
|
|
154
|
+
if (options.signal) {
|
|
155
|
+
if (options.signal.aborted) controller.abort();
|
|
156
|
+
else options.signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let resp;
|
|
160
|
+
try {
|
|
161
|
+
resp = await fetch(`${this.host}/api/chat`, {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: { 'Content-Type': 'application/json' },
|
|
164
|
+
body: JSON.stringify({
|
|
165
|
+
model,
|
|
166
|
+
messages: engineMessages,
|
|
167
|
+
stream: true,
|
|
168
|
+
keep_alive: Config.get('keepAlive'),
|
|
169
|
+
options: {
|
|
170
|
+
num_ctx: Config.get('numCtx'),
|
|
171
|
+
num_predict: options.maxTokens ?? Config.get('maxTokens'),
|
|
172
|
+
temperature: options.temperature || Config.get('temperature'),
|
|
173
|
+
num_thread: Config.get('numThreads'),
|
|
174
|
+
},
|
|
175
|
+
}),
|
|
176
|
+
signal: controller.signal,
|
|
177
|
+
});
|
|
178
|
+
} catch (err) {
|
|
179
|
+
if (err.name === 'AbortError') {
|
|
180
|
+
throw new Error('[ERR-0001] Could not complete the request (no response within 600s).');
|
|
181
|
+
}
|
|
182
|
+
throw new Error('[ERR-0001] Could not reach the engine (no response). Make sure the local engine is running.');
|
|
183
|
+
} finally {
|
|
184
|
+
clearTimeout(connectTimer);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!resp.ok) {
|
|
188
|
+
throw new Error(await describeEngineError(resp, model));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const reader = resp.body.getReader();
|
|
192
|
+
const decoder = new TextDecoder();
|
|
193
|
+
let buffer = '';
|
|
194
|
+
let idle = null;
|
|
195
|
+
const settle = () => {
|
|
196
|
+
if (idle) clearTimeout(idle);
|
|
197
|
+
idle = setTimeout(() => controller.abort(), 120000);
|
|
198
|
+
};
|
|
199
|
+
settle();
|
|
200
|
+
let output = '';
|
|
201
|
+
try {
|
|
202
|
+
while (true) {
|
|
203
|
+
let value, done;
|
|
204
|
+
try {
|
|
205
|
+
({ done, value } = await reader.read());
|
|
206
|
+
} catch (err) {
|
|
207
|
+
if (err.name === 'AbortError') {
|
|
208
|
+
throw new Error('[ERR-0001] Lost the connection to the engine (no data for 420s).');
|
|
209
|
+
}
|
|
210
|
+
throw err;
|
|
211
|
+
}
|
|
212
|
+
if (done) break;
|
|
213
|
+
settle();
|
|
214
|
+
buffer += decoder.decode(value, { stream: true });
|
|
215
|
+
const lines = buffer.split('\n');
|
|
216
|
+
buffer = lines.pop() || '';
|
|
217
|
+
|
|
218
|
+
for (const line of lines) {
|
|
219
|
+
if (!line.trim()) continue;
|
|
220
|
+
try {
|
|
221
|
+
const json = JSON.parse(line);
|
|
222
|
+
if (json.message?.content) {
|
|
223
|
+
output += json.message.content;
|
|
224
|
+
}
|
|
225
|
+
if (json.done) {
|
|
226
|
+
buffer = '';
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
} catch {}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} finally {
|
|
233
|
+
if (idle) clearTimeout(idle);
|
|
234
|
+
controller.abort();
|
|
235
|
+
}
|
|
236
|
+
return output;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async warmup() {
|
|
240
|
+
// Fire-and-forget: ask the engine to load the model eagerly, then abort as
|
|
241
|
+
// soon as generation starts. The engine runs with a single slot, so an
|
|
242
|
+
// in-flight non-streaming 'hi' would block the first real request.
|
|
243
|
+
const model = Catalog.getRealModel(Config.get('model'));
|
|
244
|
+
const controller = new AbortController();
|
|
245
|
+
fetch(`${this.host}/api/chat`, {
|
|
246
|
+
method: 'POST',
|
|
247
|
+
headers: { 'Content-Type': 'application/json' },
|
|
248
|
+
body: JSON.stringify({
|
|
249
|
+
model,
|
|
250
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
251
|
+
stream: true,
|
|
252
|
+
keep_alive: Config.get('keepAlive'),
|
|
253
|
+
options: { num_ctx: Config.get('numCtx') },
|
|
254
|
+
}),
|
|
255
|
+
signal: controller.signal,
|
|
256
|
+
})
|
|
257
|
+
.then(async (resp) => {
|
|
258
|
+
if (!resp.ok || !resp.body) return;
|
|
259
|
+
const reader = resp.body.getReader();
|
|
260
|
+
const decoder = new TextDecoder();
|
|
261
|
+
let buffer = '';
|
|
262
|
+
for (let i = 0; i < 100; i++) {
|
|
263
|
+
const { done, value } = await reader.read();
|
|
264
|
+
if (done) break;
|
|
265
|
+
buffer += decoder.decode(value, { stream: true });
|
|
266
|
+
const lines = buffer.split('\n');
|
|
267
|
+
buffer = lines.pop() || '';
|
|
268
|
+
for (const line of lines) {
|
|
269
|
+
if (!line.trim()) continue;
|
|
270
|
+
try {
|
|
271
|
+
if (JSON.parse(line).message?.content) {
|
|
272
|
+
controller.abort();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
} catch {}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
.catch(() => {});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Build a user-actionable message when the engine rejects a request (e.g. a
|
|
284
|
+
// cloud model name posted to a local engine -> 404 "model not found"). The
|
|
285
|
+
// engine host is deliberately kept out of the message; errors carry codes.
|
|
286
|
+
async function describeEngineError(resp, model) {
|
|
287
|
+
if (/not found|does not exist|model.*missing/i.test(String(resp.statusText))) {
|
|
288
|
+
return `[ERR-0002] Model "${model}" is not installed on the engine. If you picked a cloud model, use /model VRTL-6.pro or /provider openai.`;
|
|
289
|
+
}
|
|
290
|
+
let body = '';
|
|
291
|
+
try {
|
|
292
|
+
body = (await resp.text()).slice(0, 300);
|
|
293
|
+
} catch {}
|
|
294
|
+
if (/not found|does not exist|model.*missing/i.test(body)) {
|
|
295
|
+
return `[ERR-0002] Model "${model}" is not installed on the engine. If you picked a cloud model, use /model VRTL-6.pro or /provider openai.`;
|
|
296
|
+
}
|
|
297
|
+
return `[ERR-0003] The engine returned HTTP ${resp.status}. Check the engine status or run /model VRTL-2.fast.`;
|
|
298
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { BaseProvider } from './base.js';
|
|
2
|
+
import { Catalog } from '../catalog.js';
|
|
3
|
+
import { Config } from '../config.js';
|
|
4
|
+
|
|
5
|
+
const GEMINI_URL = 'https://generativelanguage.googleapis.com/v1beta/models';
|
|
6
|
+
|
|
7
|
+
export class GeminiProvider extends BaseProvider {
|
|
8
|
+
constructor() {
|
|
9
|
+
super('gemini');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
get displayName() {
|
|
13
|
+
return 'Nebula';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
_resolveModel(options) {
|
|
17
|
+
const requested = options.model || Config.get('model');
|
|
18
|
+
if (Catalog.isLocalModel(requested) || !Catalog.isCloudModel(requested)) {
|
|
19
|
+
return Catalog.getRealModel(Catalog.getDefaultCloudModel());
|
|
20
|
+
}
|
|
21
|
+
const real = Catalog.getRealModel(requested);
|
|
22
|
+
return real.startsWith('gemini-') ? real : 'gemini-2.0-flash';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async isAvailable() {
|
|
26
|
+
const key = Config.get('geminiApiKey');
|
|
27
|
+
return Boolean(key);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async listModels() {
|
|
31
|
+
const key = Config.get('geminiApiKey');
|
|
32
|
+
if (!key) return [];
|
|
33
|
+
const models = Object.keys(Catalog.getAllModels()).filter((m) => m.startsWith('gemini-'));
|
|
34
|
+
return models.map((real) => ({
|
|
35
|
+
realName: real,
|
|
36
|
+
displayName: Catalog.getDisplayName(real),
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async *stream(messages, options = {}) {
|
|
41
|
+
const key = Config.get('geminiApiKey');
|
|
42
|
+
if (!key) throw new Error('[ERR-0004] Cloud API key not configured. Add it to the config and try again.');
|
|
43
|
+
|
|
44
|
+
const model = this._resolveModel(options);
|
|
45
|
+
const systemPrompt = options.systemPrompt || '';
|
|
46
|
+
|
|
47
|
+
const contents = messages.map((m) => ({
|
|
48
|
+
role: m.role === 'assistant' ? 'model' : 'user',
|
|
49
|
+
parts: [{ text: m.content }],
|
|
50
|
+
}));
|
|
51
|
+
if (systemPrompt) {
|
|
52
|
+
contents.unshift({ role: 'user', parts: [{ text: `System: ${systemPrompt}` }] });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const connectTimer = setTimeout(() => controller.abort(), 240000);
|
|
57
|
+
|
|
58
|
+
let resp;
|
|
59
|
+
try {
|
|
60
|
+
resp = await fetch(
|
|
61
|
+
`${GEMINI_URL}/${model}:streamGenerateContent?key=${key}&alt=sse`,
|
|
62
|
+
{
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'Content-Type': 'application/json' },
|
|
65
|
+
body: JSON.stringify({
|
|
66
|
+
contents,
|
|
67
|
+
generationConfig: {
|
|
68
|
+
temperature: options.temperature || Config.get('temperature'),
|
|
69
|
+
maxOutputTokens: options.maxTokens || Config.get('maxTokens'),
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
signal: controller.signal,
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (err.name === 'AbortError') {
|
|
77
|
+
throw new Error('[ERR-0001] Could not reach the API (no response within 60s). Check your network connection.');
|
|
78
|
+
}
|
|
79
|
+
throw new Error('[ERR-0001] Could not reach the API (no response). Check your network connection.');
|
|
80
|
+
} finally {
|
|
81
|
+
clearTimeout(connectTimer);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!resp.ok) {
|
|
85
|
+
throw new Error('[ERR-0003] The cloud provider returned HTTP ' + resp.status + '. Check your API key in config.');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const reader = resp.body.getReader();
|
|
89
|
+
const decoder = new TextDecoder();
|
|
90
|
+
let buffer = '';
|
|
91
|
+
let idle = null;
|
|
92
|
+
const settle = () => {
|
|
93
|
+
if (idle) clearTimeout(idle);
|
|
94
|
+
idle = setTimeout(() => controller.abort(), 240000);
|
|
95
|
+
};
|
|
96
|
+
settle();
|
|
97
|
+
try {
|
|
98
|
+
while (true) {
|
|
99
|
+
let value, done;
|
|
100
|
+
try {
|
|
101
|
+
({ done, value } = await reader.read());
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if (err.name === 'AbortError') {
|
|
104
|
+
throw new Error('[ERR-0001] Lost the connection to the cloud API (no data for 60s).');
|
|
105
|
+
}
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
if (done) break;
|
|
109
|
+
settle();
|
|
110
|
+
buffer += decoder.decode(value, { stream: true });
|
|
111
|
+
const lines = buffer.split('\n');
|
|
112
|
+
buffer = lines.pop() || '';
|
|
113
|
+
|
|
114
|
+
for (const line of lines) {
|
|
115
|
+
if (!line.startsWith('data: ')) continue;
|
|
116
|
+
const data = line.slice(6);
|
|
117
|
+
try {
|
|
118
|
+
const json = JSON.parse(data);
|
|
119
|
+
const text = json.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
120
|
+
if (text) yield text;
|
|
121
|
+
} catch {}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} finally {
|
|
125
|
+
if (idle) clearTimeout(idle);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
package/src/providers/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CortexProvider } from './cortex.js';
|
|
2
2
|
import { OpenAIProvider } from './openai.js';
|
|
3
|
+
import { AnthropicProvider } from './anthropic.js';
|
|
4
|
+
import { GeminiProvider } from './gemini.js';
|
|
3
5
|
import { Config } from '../config.js';
|
|
4
6
|
|
|
5
7
|
let providers = {};
|
|
@@ -7,8 +9,10 @@ let providers = {};
|
|
|
7
9
|
function getProviders() {
|
|
8
10
|
if (Object.keys(providers).length === 0) {
|
|
9
11
|
providers = {
|
|
10
|
-
|
|
12
|
+
cortex: new CortexProvider(),
|
|
11
13
|
openai: new OpenAIProvider(),
|
|
14
|
+
anthropic: new AnthropicProvider(),
|
|
15
|
+
gemini: new GeminiProvider(),
|
|
12
16
|
};
|
|
13
17
|
}
|
|
14
18
|
return providers;
|
|
@@ -23,7 +27,7 @@ export const ProviderFactory = {
|
|
|
23
27
|
if (await ps[requested].isAvailable()) return ps[requested];
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
for (const name of ['
|
|
30
|
+
for (const name of ['cortex', 'openai', 'anthropic', 'gemini']) {
|
|
27
31
|
if (await ps[name].isAvailable()) return ps[name];
|
|
28
32
|
}
|
|
29
33
|
|
package/src/providers/openai.js
CHANGED
|
@@ -7,6 +7,10 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
7
7
|
super('openai');
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
get displayName() {
|
|
11
|
+
return 'Nebula';
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
async isAvailable() {
|
|
11
15
|
const key = Config.get('openaiApiKey');
|
|
12
16
|
if (!key) return false;
|
|
@@ -41,11 +45,21 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
41
45
|
}
|
|
42
46
|
}
|
|
43
47
|
|
|
48
|
+
// If the configured model is a local (ollama) model, use a cloud default so
|
|
49
|
+
// we never send a qwen model name to the OpenAI API.
|
|
50
|
+
_resolveModel(options) {
|
|
51
|
+
const requested = options.model || Config.get('model');
|
|
52
|
+
if (Catalog.isLocalModel(requested) || !Catalog.isCloudModel(requested)) {
|
|
53
|
+
return Catalog.getRealModel(Catalog.getDefaultCloudModel());
|
|
54
|
+
}
|
|
55
|
+
return Catalog.getRealModel(requested);
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
async *stream(messages, options = {}) {
|
|
45
59
|
const key = Config.get('openaiApiKey');
|
|
46
|
-
if (!key) throw new Error('Cloud API key not configured');
|
|
60
|
+
if (!key) throw new Error('[ERR-0004] Cloud API key not configured. Add it to the config and try again.');
|
|
47
61
|
|
|
48
|
-
const model =
|
|
62
|
+
const model = this._resolveModel(options);
|
|
49
63
|
const systemPrompt = options.systemPrompt || '';
|
|
50
64
|
|
|
51
65
|
const apiMessages = [];
|
|
@@ -56,47 +70,78 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
56
70
|
apiMessages.push({ role: msg.role, content: msg.content });
|
|
57
71
|
}
|
|
58
72
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
const connectTimer = setTimeout(() => controller.abort(), 240000);
|
|
75
|
+
|
|
76
|
+
let resp;
|
|
77
|
+
try {
|
|
78
|
+
resp = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: {
|
|
81
|
+
'Content-Type': 'application/json',
|
|
82
|
+
Authorization: `Bearer ${key}`,
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
model,
|
|
86
|
+
messages: apiMessages,
|
|
87
|
+
stream: true,
|
|
88
|
+
temperature: options.temperature || Config.get('temperature'),
|
|
89
|
+
max_tokens: options.maxTokens || Config.get('maxTokens'),
|
|
90
|
+
}),
|
|
91
|
+
signal: controller.signal,
|
|
92
|
+
});
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (err.name === 'AbortError') {
|
|
95
|
+
throw new Error('[ERR-0001] Could not reach the API (no response within 60s). Check your network connection.');
|
|
96
|
+
}
|
|
97
|
+
throw new Error('[ERR-0001] Could not reach the API (no response). Check your network connection.');
|
|
98
|
+
} finally {
|
|
99
|
+
clearTimeout(connectTimer);
|
|
100
|
+
}
|
|
73
101
|
|
|
74
102
|
if (!resp.ok) {
|
|
75
|
-
throw new Error('
|
|
103
|
+
throw new Error('[ERR-0003] The cloud provider returned HTTP ' + resp.status + '. Check your API key in config.');
|
|
76
104
|
}
|
|
77
105
|
|
|
78
106
|
const reader = resp.body.getReader();
|
|
79
107
|
const decoder = new TextDecoder();
|
|
80
108
|
let buffer = '';
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
for (const line of lines) {
|
|
91
|
-
if (!line.startsWith('data: ')) continue;
|
|
92
|
-
const data = line.slice(6);
|
|
93
|
-
if (data === '[DONE]') return;
|
|
109
|
+
let idle = null;
|
|
110
|
+
const settle = () => {
|
|
111
|
+
if (idle) clearTimeout(idle);
|
|
112
|
+
idle = setTimeout(() => controller.abort(), 240000);
|
|
113
|
+
};
|
|
114
|
+
settle();
|
|
115
|
+
try {
|
|
116
|
+
while (true) {
|
|
117
|
+
let value, done;
|
|
94
118
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (
|
|
98
|
-
|
|
119
|
+
({ done, value } = await reader.read());
|
|
120
|
+
} catch (err) {
|
|
121
|
+
if (err.name === 'AbortError') {
|
|
122
|
+
throw new Error('[ERR-0001] Lost the connection to the cloud API (no data for 60s).');
|
|
123
|
+
}
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
if (done) break;
|
|
127
|
+
settle();
|
|
128
|
+
buffer += decoder.decode(value, { stream: true });
|
|
129
|
+
const lines = buffer.split('\n');
|
|
130
|
+
buffer = lines.pop() || '';
|
|
131
|
+
|
|
132
|
+
for (const line of lines) {
|
|
133
|
+
if (!line.startsWith('data: ')) continue;
|
|
134
|
+
const data = line.slice(6);
|
|
135
|
+
if (data === '[DONE]') return;
|
|
136
|
+
try {
|
|
137
|
+
const json = JSON.parse(data);
|
|
138
|
+
const content = json.choices?.[0]?.delta?.content;
|
|
139
|
+
if (content) yield content;
|
|
140
|
+
} catch {}
|
|
141
|
+
}
|
|
99
142
|
}
|
|
143
|
+
} finally {
|
|
144
|
+
if (idle) clearTimeout(idle);
|
|
100
145
|
}
|
|
101
146
|
}
|
|
102
147
|
}
|