@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
package/src/installer.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { execSync, exec } from 'child_process';
|
|
2
|
-
import { existsSync } from 'fs';
|
|
2
|
+
import { existsSync, mkdirSync, symlinkSync, chmodSync, writeFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { homedir, platform } from 'os';
|
|
3
5
|
import { Catalog } from './catalog.js';
|
|
4
6
|
import { Config } from './config.js';
|
|
5
7
|
|
|
@@ -9,13 +11,13 @@ function runSilent(cmd) {
|
|
|
9
11
|
});
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
function
|
|
14
|
+
function isEngineRunning(host) {
|
|
13
15
|
return fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(3000) })
|
|
14
16
|
.then((r) => r.ok)
|
|
15
17
|
.catch(() => false);
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
function
|
|
20
|
+
function isEngineInstalled() {
|
|
19
21
|
try {
|
|
20
22
|
execSync('which ollama 2>/dev/null', { stdio: 'ignore' });
|
|
21
23
|
return true;
|
|
@@ -24,8 +26,9 @@ function isOllamaInstalled() {
|
|
|
24
26
|
return paths.some((p) => existsSync(p));
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
async function
|
|
29
|
+
async function installEngine() {
|
|
28
30
|
await runSilent('curl -fsSL https://ollama.com/install.sh | sh');
|
|
31
|
+
return isEngineInstalled();
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
async function pullModel(host, model) {
|
|
@@ -53,27 +56,136 @@ async function getInstalledModels(host) {
|
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
// Scripts in the same model family share a base name before any variant
|
|
60
|
+
// suffix (e.g. qwen2.5-coder:1.5b is the sibling of qwen2.5:1.5b).
|
|
61
|
+
function familyOf(model) {
|
|
62
|
+
return model
|
|
63
|
+
.split(':')[0]
|
|
64
|
+
.toLowerCase()
|
|
65
|
+
.replace(/-(coder|instruct|reasoning|chat|base|tiny|nano)$/, '');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sizeOf(model) {
|
|
69
|
+
const size = model.split(':')[1] || '';
|
|
70
|
+
const match = size.match(/(\d+(?:\.\d+)?)(m|b)/i);
|
|
71
|
+
if (!match) return Number.POSITIVE_INFINITY;
|
|
72
|
+
const value = parseFloat(match[1]);
|
|
73
|
+
return match[2].toLowerCase() === 'b' ? value * 1000 : value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Find the already-installed model that best matches the requested one.
|
|
77
|
+
// Prefers an exact match, then any same-family sibling (closest size).
|
|
78
|
+
function chooseModel(installed, realModel) {
|
|
79
|
+
if (installed.includes(realModel)) {
|
|
80
|
+
return { model: realModel, substituted: false, reason: null };
|
|
81
|
+
}
|
|
82
|
+
const fam = familyOf(realModel);
|
|
83
|
+
let best = null;
|
|
84
|
+
let bestDist = Number.POSITIVE_INFINITY;
|
|
85
|
+
for (const m of installed) {
|
|
86
|
+
if (familyOf(m) !== fam) continue;
|
|
87
|
+
const dist = Math.abs(sizeOf(m) - sizeOf(realModel)) + 0.5;
|
|
88
|
+
if (dist < bestDist) {
|
|
89
|
+
bestDist = dist;
|
|
90
|
+
best = m;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (best) {
|
|
94
|
+
return {
|
|
95
|
+
model: best,
|
|
96
|
+
substituted: true,
|
|
97
|
+
reason: `"${realModel}" is not installed; using ${best} (same family)`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return { model: null, substituted: true, reason: `"${realModel}" is not installed` };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function resolveModel(host, realModel) {
|
|
104
|
+
const installed = await getInstalledModels(host);
|
|
105
|
+
return chooseModel(installed, realModel);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Best local bin dir for a globally-callable command on this platform.
|
|
109
|
+
function localBinDir() {
|
|
110
|
+
if (process.env.XDG_BIN_HOME) return process.env.XDG_BIN_HOME;
|
|
111
|
+
if (process.env.HOME) return join(homedir(), '.local', 'bin');
|
|
112
|
+
return '/usr/local/bin';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function installAppCommand() {
|
|
116
|
+
try {
|
|
117
|
+
const dir = localBinDir();
|
|
118
|
+
mkdirSync(dir, { recursive: true, mode: 0o755 });
|
|
119
|
+
const launcherPath = join(dir, 'vierrataleai');
|
|
120
|
+
if (platform() === 'win32') {
|
|
121
|
+
if (existsSync(launcherPath)) return launcherPath;
|
|
122
|
+
writeFileSync(launcherPath, `@echo off\r\nnode "${process.argv[1]}" %*\r\n`);
|
|
123
|
+
return launcherPath;
|
|
124
|
+
}
|
|
125
|
+
// Prefer a real symlink; fall back to a small shim that calls the node bin.
|
|
126
|
+
if (existsSync(launcherPath)) return launcherPath;
|
|
127
|
+
const target = process.argv[1] && existsSync(process.argv[1]) ? process.argv[1] : join(Config.getConfigDir(), '..', '..', 'src', 'cli.js');
|
|
128
|
+
try {
|
|
129
|
+
symlinkSync(target, launcherPath);
|
|
130
|
+
chmodSync(launcherPath, 0o755);
|
|
131
|
+
} catch {
|
|
132
|
+
writeFileSync(launcherPath, `#!/bin/sh\nnode "${target}" "$@"\n`);
|
|
133
|
+
chmodSync(launcherPath, 0o755);
|
|
134
|
+
}
|
|
135
|
+
return launcherPath;
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
56
141
|
export const Installer = {
|
|
142
|
+
async pullModel(host, model) {
|
|
143
|
+
return pullModel(host, model);
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
async getInstalledModels(host) {
|
|
147
|
+
return getInstalledModels(host);
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
async resolveModel(host, realModel) {
|
|
151
|
+
return resolveModel(host, realModel);
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
async firstInstalled(host) {
|
|
155
|
+
const installed = await getInstalledModels(host);
|
|
156
|
+
return installed[0] || null;
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
chooseModel,
|
|
160
|
+
|
|
57
161
|
async ensure() {
|
|
58
|
-
const host = Config.get('
|
|
162
|
+
const host = Config.get('engineHost');
|
|
59
163
|
|
|
60
|
-
if (!
|
|
61
|
-
await
|
|
164
|
+
if (!isEngineInstalled()) {
|
|
165
|
+
await installEngine();
|
|
62
166
|
}
|
|
63
167
|
|
|
64
|
-
const running = await
|
|
168
|
+
const running = await isEngineRunning(host);
|
|
65
169
|
if (!running) {
|
|
66
170
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
171
|
+
// No systemd available here, so launch the daemon directly, detached,
|
|
172
|
+
// and poll until it responds instead of relying on a service manager.
|
|
173
|
+
const child = exec('ollama serve', {
|
|
174
|
+
stdio: 'ignore',
|
|
175
|
+
detached: true,
|
|
176
|
+
});
|
|
177
|
+
if (child.unref) child.unref();
|
|
178
|
+
for (let i = 0; i < 30; i++) {
|
|
179
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
180
|
+
if (await isEngineRunning(host)) break;
|
|
181
|
+
}
|
|
69
182
|
} catch {}
|
|
70
183
|
}
|
|
71
184
|
|
|
72
185
|
const realModel = Catalog.getRealModel(Config.get('model'));
|
|
73
186
|
const installed = await getInstalledModels(host);
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
);
|
|
187
|
+
const resolved = chooseModel(installed, realModel);
|
|
188
|
+
const needsPull = resolved.model == null;
|
|
77
189
|
|
|
78
190
|
if (needsPull) {
|
|
79
191
|
await pullModel(host, realModel);
|
|
@@ -83,7 +195,39 @@ export const Installer = {
|
|
|
83
195
|
},
|
|
84
196
|
|
|
85
197
|
async isReady() {
|
|
86
|
-
const host = Config.get('
|
|
87
|
-
return
|
|
198
|
+
const host = Config.get('engineHost');
|
|
199
|
+
return isEngineRunning(host);
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
// Full install: engine + model + a global "vierrataleai" command.
|
|
203
|
+
async install({ includeCommand = true } = {}) {
|
|
204
|
+
const host = Config.get('engineHost');
|
|
205
|
+
|
|
206
|
+
if (!isEngineInstalled()) {
|
|
207
|
+
await installEngine();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const running = await isEngineRunning(host);
|
|
211
|
+
if (!running) {
|
|
212
|
+
try {
|
|
213
|
+
const child = exec('ollama serve', { stdio: 'ignore', detached: true });
|
|
214
|
+
if (child.unref) child.unref();
|
|
215
|
+
for (let i = 0; i < 30; i++) {
|
|
216
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
217
|
+
if (await isEngineRunning(host)) break;
|
|
218
|
+
}
|
|
219
|
+
} catch {}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const realModel = Catalog.getRealModel(Config.get('model'));
|
|
223
|
+
const installed = await getInstalledModels(host);
|
|
224
|
+
const resolved = chooseModel(installed, realModel);
|
|
225
|
+
if (resolved.model == null) {
|
|
226
|
+
await pullModel(host, realModel);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let command = null;
|
|
230
|
+
if (includeCommand) command = installAppCommand();
|
|
231
|
+
return { installed: await isEngineInstalled(), running: await isEngineRunning(host), command };
|
|
88
232
|
},
|
|
89
233
|
};
|
package/src/prompts/system.json
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
{
|
|
2
|
-
"system": "You are VierrataleAI, an intelligent AI assistant created by Vierratale. You are helpful, concise, and accurate. You provide clear answers and help users with their questions and tasks. When asked about your identity, always respond that you are VierrataleAI, an AI assistant created by Vierratale."
|
|
3
|
-
}
|
|
2
|
+
"system": "You are VierrataleAI, an intelligent AI assistant created by Vierratale. You are helpful, concise, and accurate. You provide clear answers and help users with their questions and tasks. When asked about your identity, always respond that you are VierrataleAI, an AI assistant created by Vierratale. You CAN write files and folders directly to the user's disk. When the user asks you to create, save, or generate code, configs, scripts, projects, web pages, or any other files, DO NOT refuse — answer with the FILE:/FOLDER: format instead. Use this exact format for EACH file: a line \"FILE: relative/path/to/file\" on its own line, immediately followed by a markdown code fence containing the file content. This works for any file type, including JSON config files and HTML pages: write a config as \"FILE: config.json\" followed by a ```json fence, and a web page as \"FILE: index.html\" followed by a ```html fence with the full <!doctype html>... markup inside. Use paths relative to the user's working directory. Emit one FILE: block per file; nested folders are created automatically. If you need an empty folder, emit \"FOLDER: relative/path\" on its own line. When asked for a story, folk tale, legend, or myth, give the real, well-known version using the web search results you were given when available, and also save the full story to a .txt file using FILE: <title>.txt followed by a ```text fence. Keep any explanation short and outside of the FILE:/FOLDER: blocks."
|
|
3
|
+
}
|
package/src/prompts/system.md
CHANGED
|
@@ -13,3 +13,45 @@ You are **VierrataleAI**, an intelligent AI assistant created by Vierratale.
|
|
|
13
13
|
- Help users with tasks including coding, writing, analysis, and general knowledge.
|
|
14
14
|
- If you don't know something, say so honestly.
|
|
15
15
|
- Use markdown formatting when appropriate for readability.
|
|
16
|
+
|
|
17
|
+
## Writing Files and Folders
|
|
18
|
+
|
|
19
|
+
You CAN write files and folders directly to the user's disk. When the user asks
|
|
20
|
+
you to create, save, or generate code, configs, scripts, projects, or any other
|
|
21
|
+
files, DO NOT refuse — answer with the format below. Use this exact format for
|
|
22
|
+
EACH file:
|
|
23
|
+
|
|
24
|
+
FILE: relative/path/to/file.js
|
|
25
|
+
```js
|
|
26
|
+
// file content here
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This works for any file type, including JSON and HTML. For example, write a
|
|
30
|
+
config file exactly like this:
|
|
31
|
+
|
|
32
|
+
FILE: config.json
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"name": "my-app",
|
|
36
|
+
"port": 3000
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
And a web page like this:
|
|
41
|
+
|
|
42
|
+
FILE: index.html
|
|
43
|
+
```html
|
|
44
|
+
<!doctype html>
|
|
45
|
+
<html>
|
|
46
|
+
<head><title>My Page</title></head>
|
|
47
|
+
<body><h1>Hello</h1></body>
|
|
48
|
+
</html>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Rules:
|
|
52
|
+
- Put `FILE: <path>` on its own line, immediately followed by a markdown code fence containing the file content. No text between the header and the fence.
|
|
53
|
+
- Use paths relative to the user's working directory (e.g. `src/app.js`, `config.json`).
|
|
54
|
+
- Emit one `FILE:` block per file; nested folders are created automatically.
|
|
55
|
+
- If you need an empty folder, emit `FOLDER: <path>` on its own line.
|
|
56
|
+
- When asked for a story, folk tale, legend, or myth, give the real, well-known version — use the web search results you were given when available — and also save the full story to a .txt file using `FILE: <title>.txt` followed by a ```text fence.
|
|
57
|
+
- Keep any explanation short and outside of the `FILE:`/`FOLDER:` blocks.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { BaseProvider } from './base.js';
|
|
2
|
+
import { Catalog } from '../catalog.js';
|
|
3
|
+
import { Config } from '../config.js';
|
|
4
|
+
|
|
5
|
+
const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages';
|
|
6
|
+
|
|
7
|
+
export class AnthropicProvider extends BaseProvider {
|
|
8
|
+
constructor() {
|
|
9
|
+
super('anthropic');
|
|
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('claude-') ? real : 'claude-3-5-haiku-20241022';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async isAvailable() {
|
|
26
|
+
const key = Config.get('anthropicApiKey');
|
|
27
|
+
return Boolean(key);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async listModels() {
|
|
31
|
+
const key = Config.get('anthropicApiKey');
|
|
32
|
+
if (!key) return [];
|
|
33
|
+
const models = Object.keys(Catalog.getAllModels()).filter((m) => m.startsWith('claude-'));
|
|
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('anthropicApiKey');
|
|
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 system = [];
|
|
48
|
+
if (systemPrompt) system.push({ type: 'text', text: systemPrompt });
|
|
49
|
+
const apiMessages = messages.map((m) => ({
|
|
50
|
+
role: m.role === 'assistant' ? 'assistant' : 'user',
|
|
51
|
+
content: m.content,
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
const connectTimer = setTimeout(() => controller.abort(), 240000);
|
|
56
|
+
|
|
57
|
+
let resp;
|
|
58
|
+
try {
|
|
59
|
+
resp = await fetch(ANTHROPIC_URL, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: {
|
|
62
|
+
'Content-Type': 'application/json',
|
|
63
|
+
'x-api-key': key,
|
|
64
|
+
'anthropic-version': '2023-06-01',
|
|
65
|
+
},
|
|
66
|
+
body: JSON.stringify({
|
|
67
|
+
model,
|
|
68
|
+
system: system.length ? system : undefined,
|
|
69
|
+
messages: apiMessages,
|
|
70
|
+
max_tokens: options.maxTokens || Config.get('maxTokens'),
|
|
71
|
+
stream: true,
|
|
72
|
+
temperature: options.temperature || Config.get('temperature'),
|
|
73
|
+
}),
|
|
74
|
+
signal: controller.signal,
|
|
75
|
+
});
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err.name === 'AbortError') {
|
|
78
|
+
throw new Error('[ERR-0001] Could not reach the API (no response within 60s). Check your network connection.');
|
|
79
|
+
}
|
|
80
|
+
throw new Error('[ERR-0001] Could not reach the API (no response). Check your network connection.');
|
|
81
|
+
} finally {
|
|
82
|
+
clearTimeout(connectTimer);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!resp.ok) {
|
|
86
|
+
throw new Error('[ERR-0003] The cloud provider returned HTTP ' + resp.status + '. Check your API key in config.');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const reader = resp.body.getReader();
|
|
90
|
+
const decoder = new TextDecoder();
|
|
91
|
+
let buffer = '';
|
|
92
|
+
let idle = null;
|
|
93
|
+
const settle = () => {
|
|
94
|
+
if (idle) clearTimeout(idle);
|
|
95
|
+
idle = setTimeout(() => controller.abort(), 240000);
|
|
96
|
+
};
|
|
97
|
+
settle();
|
|
98
|
+
try {
|
|
99
|
+
while (true) {
|
|
100
|
+
let value, done;
|
|
101
|
+
try {
|
|
102
|
+
({ done, value } = await reader.read());
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (err.name === 'AbortError') {
|
|
105
|
+
throw new Error('[ERR-0001] Lost the connection to the cloud API (no data for 60s).');
|
|
106
|
+
}
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
if (done) break;
|
|
110
|
+
settle();
|
|
111
|
+
buffer += decoder.decode(value, { stream: true });
|
|
112
|
+
const lines = buffer.split('\n');
|
|
113
|
+
buffer = lines.pop() || '';
|
|
114
|
+
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
if (!line.startsWith('data: ')) continue;
|
|
117
|
+
const data = line.slice(6);
|
|
118
|
+
try {
|
|
119
|
+
const json = JSON.parse(data);
|
|
120
|
+
if (json.type === 'content_block_delta' && json.delta?.text) {
|
|
121
|
+
yield json.delta.text;
|
|
122
|
+
}
|
|
123
|
+
} catch {}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} finally {
|
|
127
|
+
if (idle) clearTimeout(idle);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
package/src/providers/base.js
CHANGED
|
@@ -3,10 +3,28 @@ export class BaseProvider {
|
|
|
3
3
|
this.name = name;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
get displayName() {
|
|
7
|
+
return 'Cortex';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
get isLocal() {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
async *stream(messages, options = {}) {
|
|
7
15
|
throw new Error('stream() must be implemented');
|
|
8
16
|
}
|
|
9
17
|
|
|
18
|
+
// Non-streaming convenience: collects the stream. Providers may override
|
|
19
|
+
// with a proper single-shot request (see CortexProvider).
|
|
20
|
+
async complete(messages, options = {}) {
|
|
21
|
+
let out = '';
|
|
22
|
+
for await (const chunk of this.stream(messages, options)) {
|
|
23
|
+
out += chunk;
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
10
28
|
async listModels() {
|
|
11
29
|
throw new Error('listModels() must be implemented');
|
|
12
30
|
}
|