@vierratale/ai 0.1.0-beta.5 → 0.1.0-beta.6
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 +32 -13
- package/package.json +4 -2
- package/scripts/postinstall.js +35 -0
- package/src/catalog.js +20 -0
- package/src/cli.js +25 -8
- package/src/config.js +19 -0
- package/src/providers/anthropic.js +99 -0
- package/src/providers/base.js +4 -0
- package/src/providers/cortex.js +4 -0
- package/src/providers/gemini.js +97 -0
- package/src/providers/index.js +5 -1
- package/src/providers/openai.js +11 -1
- package/src/ui/banner.js +10 -5
- package/src/ui/branding.js +1 -1
package/README.md
CHANGED
|
@@ -12,27 +12,43 @@ npm install -g @vierratale/ai
|
|
|
12
12
|
npx @vierratale/ai
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
The first launch detects and, if needed, installs and starts the local engine
|
|
16
|
+
(**Cortex** - "secretly" Ollama under the hood), then pulls the model you use.
|
|
17
|
+
|
|
15
18
|
## Usage
|
|
16
19
|
|
|
17
20
|
```bash
|
|
18
21
|
vierrataleai # Start chat (auto-detect provider)
|
|
19
22
|
vierrataleai --provider cortex # Use local models
|
|
20
|
-
vierrataleai --provider openai # Use cloud models
|
|
23
|
+
vierrataleai --provider openai # Use cloud models (OpenAI)
|
|
24
|
+
vierrataleai --provider anthropic # Use cloud models (Claude)
|
|
25
|
+
vierrataleai --provider gemini # Use cloud models (Gemini)
|
|
21
26
|
vierrataleai --model vierratale-pro # Use specific model
|
|
22
27
|
```
|
|
23
28
|
|
|
24
29
|
## Models
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
|
29
|
-
|
|
30
|
-
| vierratale-
|
|
31
|
-
| vierratale-
|
|
32
|
-
| vierratale-
|
|
33
|
-
| vierratale-
|
|
34
|
-
| vierratale-
|
|
35
|
-
| vierratale-
|
|
31
|
+
Local models run through the **Cortex** engine:
|
|
32
|
+
|
|
33
|
+
| Model | Backend |
|
|
34
|
+
|-------|---------|
|
|
35
|
+
| vierratale-lite | qwen2.5-coder:0.5b |
|
|
36
|
+
| vierratale-fast | qwen2.5-coder:1.5b |
|
|
37
|
+
| vierratale-small | qwen2.5-coder:3b |
|
|
38
|
+
| vierratale-balanced | qwen2.5-coder:7b |
|
|
39
|
+
| vierratale-plus | qwen2.5-coder:14b |
|
|
40
|
+
| vierratale-pro | qwen2.5-coder:32b |
|
|
41
|
+
|
|
42
|
+
Cloud models are available when the matching provider + API key is configured:
|
|
43
|
+
|
|
44
|
+
| Model | Backend |
|
|
45
|
+
|-------|---------|
|
|
46
|
+
| vierratale-cloud-mini | gpt-4o-mini (OpenAI) |
|
|
47
|
+
| vierratale-cloud | gpt-4o (OpenAI) |
|
|
48
|
+
| vierratale-cloud-fast | claude-3-5-haiku-20241022 (Anthropic) |
|
|
49
|
+
| vierratale-cloud-pro | claude-sonnet-4-20250514 (Anthropic) |
|
|
50
|
+
| vierratale-cloud-lite | gemini-2.0-flash (Gemini) |
|
|
51
|
+
| vierratale-cloud-plus | gemini-1.5-pro (Gemini) |
|
|
36
52
|
|
|
37
53
|
## Commands
|
|
38
54
|
|
|
@@ -40,6 +56,7 @@ vierrataleai --model vierratale-pro # Use specific model
|
|
|
40
56
|
- `/model [name]` - Switch model
|
|
41
57
|
- `/provider [name]` - Switch provider
|
|
42
58
|
- `/models` - List available models
|
|
59
|
+
- `/system [text]` - Set a custom system prompt / persona
|
|
43
60
|
- `/search <query>` - Search the web and summarize with AI
|
|
44
61
|
- `/clear` - Clear screen
|
|
45
62
|
- `/new` - Start a new conversation (clear memory)
|
|
@@ -67,9 +84,11 @@ you can continue where you left off. Use `/new` to start fresh.
|
|
|
67
84
|
Config file: `~/.config/vierrataleai/config.json`
|
|
68
85
|
|
|
69
86
|
Environment variables:
|
|
70
|
-
- `VIERRATALE_PROVIDER` - Default provider
|
|
87
|
+
- `VIERRATALE_PROVIDER` - Default provider (auto/cortex/openai/anthropic/gemini)
|
|
71
88
|
- `VIERRATALE_MODEL` - Default model
|
|
72
|
-
- `OPENAI_API_KEY` - OpenAI API key (
|
|
89
|
+
- `OPENAI_API_KEY` - OpenAI API key (openai provider)
|
|
90
|
+
- `ANTHROPIC_API_KEY` - Anthropic API key (anthropic provider)
|
|
91
|
+
- `GEMINI_API_KEY` - Google AI API key (gemini provider)
|
|
73
92
|
|
|
74
93
|
## License
|
|
75
94
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vierratale/ai",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.6",
|
|
4
4
|
"description": "VierrataleAI - Intelligent terminal assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
".": "./src/index.js"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"start": "node bin/vierrataleai.js"
|
|
14
|
+
"start": "node bin/vierrataleai.js",
|
|
15
|
+
"test": "node --test test/",
|
|
16
|
+
"postinstall": "node scripts/postinstall.js"
|
|
15
17
|
},
|
|
16
18
|
"keywords": ["ai", "chatbot", "terminal", "assistant", "llm"],
|
|
17
19
|
"author": "Vierratale",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Optional engine setup after install. Never fails the package install.
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
|
|
6
|
+
const isInstalled = () => {
|
|
7
|
+
try {
|
|
8
|
+
execSync('which ollama 2>/dev/null', { stdio: 'ignore' });
|
|
9
|
+
return true;
|
|
10
|
+
} catch {}
|
|
11
|
+
return existsSync('/usr/local/bin/ollama') || existsSync('/usr/bin/ollama');
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
if (isInstalled()) {
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const nonInteractive =
|
|
20
|
+
!process.stdout.isTTY || process.env.CI || process.env.NODE_ENV === 'test';
|
|
21
|
+
|
|
22
|
+
// Only auto-install when explicitly requested. Otherwise just inform the user
|
|
23
|
+
// that the engine will be installed on first launch.
|
|
24
|
+
if (process.env.VIERRATALE_INSTALL_ENGINE === '1' && !nonInteractive) {
|
|
25
|
+
execSync('curl -fsSL https://ollama.com/install.sh | sh', { stdio: 'inherit' });
|
|
26
|
+
} else {
|
|
27
|
+
console.log(
|
|
28
|
+
'\n[vierrataleai] The local engine (ollama) was not detected.\n' +
|
|
29
|
+
' - Run the CLI: it will attempt to install and start the engine automatically.\n' +
|
|
30
|
+
' - Or install it yourself: curl -fsSL https://ollama.com/install.sh | sh\n'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Never fail the install because of engine setup.
|
|
35
|
+
}
|
package/src/catalog.js
CHANGED
|
@@ -60,6 +60,26 @@ export const Catalog = {
|
|
|
60
60
|
return 'vierratale-fast';
|
|
61
61
|
},
|
|
62
62
|
|
|
63
|
+
getDefaultCloudModel() {
|
|
64
|
+
return 'vierratale-cloud-mini';
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
isLocalModel(displayName) {
|
|
68
|
+
return LOCAL_MODELS.includes(displayName);
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
isCloudModel(displayName) {
|
|
72
|
+
return CLOUD_MODELS.includes(displayName);
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
getVendorForCloudModel(displayName) {
|
|
76
|
+
const real = REVERSE[displayName];
|
|
77
|
+
if (real && real.startsWith('claude-')) return 'anthropic';
|
|
78
|
+
if (real && real.startsWith('gemini-')) return 'gemini';
|
|
79
|
+
if (real && real.startsWith('gpt-')) return 'openai';
|
|
80
|
+
return 'openai';
|
|
81
|
+
},
|
|
82
|
+
|
|
63
83
|
getModelInfo(displayName) {
|
|
64
84
|
const real = REVERSE[displayName];
|
|
65
85
|
if (!real) return null;
|
package/src/cli.js
CHANGED
|
@@ -72,7 +72,7 @@ async function answerWithSearch(messages, provider, query, systemPrompt) {
|
|
|
72
72
|
let response = '';
|
|
73
73
|
try {
|
|
74
74
|
for await (const chunk of provider.stream(messages, {
|
|
75
|
-
model: Config.
|
|
75
|
+
model: Config.getEffectiveModel(provider.name),
|
|
76
76
|
systemPrompt,
|
|
77
77
|
})) {
|
|
78
78
|
Terminal.printAIChunk(chunk);
|
|
@@ -140,7 +140,7 @@ async function chat(provider, systemPrompt) {
|
|
|
140
140
|
|
|
141
141
|
if (cmd === '/clear') {
|
|
142
142
|
Terminal.clear();
|
|
143
|
-
showBanner(provider.name, Config.
|
|
143
|
+
showBanner(provider.name, Config.getEffectiveModel(provider.name), provider.displayName);
|
|
144
144
|
continue;
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -157,13 +157,13 @@ async function chat(provider, systemPrompt) {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
if (cmd === '/models') {
|
|
160
|
-
const models = provider.
|
|
160
|
+
const models = provider.isLocal
|
|
161
161
|
? Catalog.getLocalModels()
|
|
162
162
|
: Catalog.getCloudModels();
|
|
163
163
|
Terminal.printInfo('Available models:');
|
|
164
164
|
for (const m of models) {
|
|
165
165
|
const info = Catalog.getModelInfo(m);
|
|
166
|
-
const current = m === Config.
|
|
166
|
+
const current = m === Config.getEffectiveModel(provider.name) ? ' (active)' : '';
|
|
167
167
|
console.log(` ${Branding.colors.accent}${m}${Branding.colors.dim}${current} - ${info.tier}${Branding.colors.reset}`);
|
|
168
168
|
}
|
|
169
169
|
continue;
|
|
@@ -174,6 +174,7 @@ async function chat(provider, systemPrompt) {
|
|
|
174
174
|
if (modelName) {
|
|
175
175
|
if (Catalog.getModelInfo(modelName)) {
|
|
176
176
|
Config.save({ model: modelName });
|
|
177
|
+
Config.setProviderModel(provider.name, modelName);
|
|
177
178
|
Terminal.printSuccess(`Model: ${modelName}`);
|
|
178
179
|
} else {
|
|
179
180
|
Terminal.printError(`Unknown model: ${modelName}`);
|
|
@@ -183,13 +184,13 @@ async function chat(provider, systemPrompt) {
|
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
if (cmd === '/model') {
|
|
186
|
-
Terminal.printInfo(`Current model: ${Config.
|
|
187
|
+
Terminal.printInfo(`Current model: ${Config.getEffectiveModel(provider.name)}`);
|
|
187
188
|
continue;
|
|
188
189
|
}
|
|
189
190
|
|
|
190
191
|
if (cmd.startsWith('/provider ')) {
|
|
191
192
|
const prov = cmd.split(' ')[1];
|
|
192
|
-
if (['cortex', 'openai'].includes(prov)) {
|
|
193
|
+
if (['cortex', 'openai', 'anthropic', 'gemini'].includes(prov)) {
|
|
193
194
|
Config.save({ provider: prov });
|
|
194
195
|
const next = ProviderFactory.create(prov);
|
|
195
196
|
if (next && await next.isAvailable()) {
|
|
@@ -209,6 +210,22 @@ async function chat(provider, systemPrompt) {
|
|
|
209
210
|
continue;
|
|
210
211
|
}
|
|
211
212
|
|
|
213
|
+
if (cmd.startsWith('/system')) {
|
|
214
|
+
const prompt = trimmed.slice(7).trim();
|
|
215
|
+
if (prompt.toLowerCase() === 'reset') {
|
|
216
|
+
Config.save({ systemPrompt: '' });
|
|
217
|
+
systemPrompt = loadSystemPrompt();
|
|
218
|
+
Terminal.printSuccess('System prompt reset to default.');
|
|
219
|
+
} else if (prompt) {
|
|
220
|
+
Config.save({ systemPrompt: prompt });
|
|
221
|
+
systemPrompt = prompt;
|
|
222
|
+
Terminal.printSuccess('System prompt updated.');
|
|
223
|
+
} else {
|
|
224
|
+
Terminal.printInfo(`Current system prompt: ${systemPrompt}`);
|
|
225
|
+
}
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
212
229
|
if (cmd.startsWith('/search ')) {
|
|
213
230
|
const query = trimmed.slice(8).trim();
|
|
214
231
|
if (!query) {
|
|
@@ -233,7 +250,7 @@ async function chat(provider, systemPrompt) {
|
|
|
233
250
|
let response = '';
|
|
234
251
|
try {
|
|
235
252
|
for await (const chunk of provider.stream(messages, {
|
|
236
|
-
model: Config.
|
|
253
|
+
model: Config.getEffectiveModel(provider.name),
|
|
237
254
|
systemPrompt,
|
|
238
255
|
})) {
|
|
239
256
|
Terminal.printAIChunk(chunk);
|
|
@@ -280,7 +297,7 @@ export async function run() {
|
|
|
280
297
|
Config.save({ provider: provider.name });
|
|
281
298
|
|
|
282
299
|
if (args.clear) Terminal.clear();
|
|
283
|
-
showBanner(provider.name, Config.
|
|
300
|
+
showBanner(provider.name, Config.getEffectiveModel(provider.name), provider.displayName);
|
|
284
301
|
|
|
285
302
|
const systemPrompt = loadSystemPrompt();
|
|
286
303
|
await chat(provider, systemPrompt);
|
package/src/config.js
CHANGED
|
@@ -44,6 +44,9 @@ export const Config = {
|
|
|
44
44
|
maxTokens: parseInt(getEnv('VIERRATALE_MAX_TOKENS', String(file.maxTokens || DEFAULTS.maxTokens))),
|
|
45
45
|
openaiApiKey: getEnv('OPENAI_API_KEY', file.openaiApiKey || ''),
|
|
46
46
|
anthropicApiKey: getEnv('ANTHROPIC_API_KEY', file.anthropicApiKey || ''),
|
|
47
|
+
geminiApiKey: getEnv('GEMINI_API_KEY', file.geminiApiKey || ''),
|
|
48
|
+
systemPrompt: file.systemPrompt || '',
|
|
49
|
+
providerModels: file.providerModels || {},
|
|
47
50
|
};
|
|
48
51
|
return this._config;
|
|
49
52
|
},
|
|
@@ -58,6 +61,22 @@ export const Config = {
|
|
|
58
61
|
return { ...this._config };
|
|
59
62
|
},
|
|
60
63
|
|
|
64
|
+
getProviderModel(provider) {
|
|
65
|
+
if (!this._config) this.load();
|
|
66
|
+
return this._config.providerModels?.[provider] || null;
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
setProviderModel(provider, model) {
|
|
70
|
+
if (!this._config) this.load();
|
|
71
|
+
this._config.providerModels = this._config.providerModels || {};
|
|
72
|
+
this._config.providerModels[provider] = model;
|
|
73
|
+
this.save();
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
getEffectiveModel(provider) {
|
|
77
|
+
return this.getProviderModel(provider) || this.get('model');
|
|
78
|
+
},
|
|
79
|
+
|
|
61
80
|
save(overrides = {}) {
|
|
62
81
|
if (!existsSync(CONFIG_DIR)) {
|
|
63
82
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
@@ -0,0 +1,99 @@
|
|
|
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('Anthropic API key not configured');
|
|
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 resp = await fetch(ANTHROPIC_URL, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: {
|
|
57
|
+
'Content-Type': 'application/json',
|
|
58
|
+
'x-api-key': key,
|
|
59
|
+
'anthropic-version': '2023-06-01',
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify({
|
|
62
|
+
model,
|
|
63
|
+
system: system.length ? system : undefined,
|
|
64
|
+
messages: apiMessages,
|
|
65
|
+
max_tokens: options.maxTokens || Config.get('maxTokens'),
|
|
66
|
+
stream: true,
|
|
67
|
+
temperature: options.temperature || Config.get('temperature'),
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!resp.ok) {
|
|
72
|
+
throw new Error('Failed to connect to cloud engine');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const reader = resp.body.getReader();
|
|
76
|
+
const decoder = new TextDecoder();
|
|
77
|
+
let buffer = '';
|
|
78
|
+
|
|
79
|
+
while (true) {
|
|
80
|
+
const { done, value } = await reader.read();
|
|
81
|
+
if (done) break;
|
|
82
|
+
|
|
83
|
+
buffer += decoder.decode(value, { stream: true });
|
|
84
|
+
const lines = buffer.split('\n');
|
|
85
|
+
buffer = lines.pop() || '';
|
|
86
|
+
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
if (!line.startsWith('data: ')) continue;
|
|
89
|
+
const data = line.slice(6);
|
|
90
|
+
try {
|
|
91
|
+
const json = JSON.parse(data);
|
|
92
|
+
if (json.type === 'content_block_delta' && json.delta?.text) {
|
|
93
|
+
yield json.delta.text;
|
|
94
|
+
}
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
package/src/providers/base.js
CHANGED
package/src/providers/cortex.js
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
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('Gemini API key not configured');
|
|
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 resp = await fetch(
|
|
56
|
+
`${GEMINI_URL}/${model}:streamGenerateContent?key=${key}&alt=sse`,
|
|
57
|
+
{
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { 'Content-Type': 'application/json' },
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
contents,
|
|
62
|
+
generationConfig: {
|
|
63
|
+
temperature: options.temperature || Config.get('temperature'),
|
|
64
|
+
maxOutputTokens: options.maxTokens || Config.get('maxTokens'),
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (!resp.ok) {
|
|
71
|
+
throw new Error('Failed to connect to cloud engine');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const reader = resp.body.getReader();
|
|
75
|
+
const decoder = new TextDecoder();
|
|
76
|
+
let buffer = '';
|
|
77
|
+
|
|
78
|
+
while (true) {
|
|
79
|
+
const { done, value } = await reader.read();
|
|
80
|
+
if (done) break;
|
|
81
|
+
|
|
82
|
+
buffer += decoder.decode(value, { stream: true });
|
|
83
|
+
const lines = buffer.split('\n');
|
|
84
|
+
buffer = lines.pop() || '';
|
|
85
|
+
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
if (!line.startsWith('data: ')) continue;
|
|
88
|
+
const data = line.slice(6);
|
|
89
|
+
try {
|
|
90
|
+
const json = JSON.parse(data);
|
|
91
|
+
const text = json.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
92
|
+
if (text) yield text;
|
|
93
|
+
} catch {}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
package/src/providers/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
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 = {};
|
|
@@ -9,6 +11,8 @@ function getProviders() {
|
|
|
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 ['cortex', 'openai']) {
|
|
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
|
@@ -45,11 +45,21 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
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
|
+
|
|
48
58
|
async *stream(messages, options = {}) {
|
|
49
59
|
const key = Config.get('openaiApiKey');
|
|
50
60
|
if (!key) throw new Error('Cloud API key not configured');
|
|
51
61
|
|
|
52
|
-
const model =
|
|
62
|
+
const model = this._resolveModel(options);
|
|
53
63
|
const systemPrompt = options.systemPrompt || '';
|
|
54
64
|
|
|
55
65
|
const apiMessages = [];
|
package/src/ui/banner.js
CHANGED
|
@@ -3,10 +3,13 @@ import { join, dirname } from 'path';
|
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { Branding } from './branding.js';
|
|
5
5
|
import { Terminal } from './terminal.js';
|
|
6
|
+
import { Config } from '../config.js';
|
|
6
7
|
|
|
7
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
|
|
9
10
|
export function loadSystemPrompt() {
|
|
11
|
+
const custom = Config.get('systemPrompt');
|
|
12
|
+
if (custom) return custom;
|
|
10
13
|
try {
|
|
11
14
|
const jsonPath = join(__dirname, '..', 'prompts', 'system.json');
|
|
12
15
|
const data = JSON.parse(readFileSync(jsonPath, 'utf-8'));
|
|
@@ -20,16 +23,18 @@ export function showBanner(providerName, model, providerDisplayName) {
|
|
|
20
23
|
const C = Branding.colors;
|
|
21
24
|
const width = 44;
|
|
22
25
|
const border = ` ╔${'═'.repeat(width)}╗`;
|
|
26
|
+
const visible = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
27
|
+
const pad = (s) => s + ' '.repeat(Math.max(1, width - visible(s).length));
|
|
23
28
|
|
|
24
29
|
console.log();
|
|
25
30
|
console.log(`${C.primary}${Branding.BANNER}${C.reset}`);
|
|
26
|
-
console.log(` ${C.bold}${C.accent}▸ Vierratale AI ▸ Cortex Engine${C.reset}`);
|
|
31
|
+
console.log(` ${C.bold}${C.accent}▸ Vierratale AI ▸ ${providerDisplayName || 'Cortex'} Engine${C.reset}`);
|
|
27
32
|
console.log();
|
|
28
33
|
console.log(border);
|
|
29
|
-
console.log(`
|
|
30
|
-
console.log(`
|
|
31
|
-
console.log(`
|
|
32
|
-
console.log(`
|
|
34
|
+
console.log(` ║${pad(' ' + C.bold + C.primary + 'Model' + C.reset)}║`);
|
|
35
|
+
console.log(` ║${pad(' ' + C.accent + model + C.reset)}║`);
|
|
36
|
+
console.log(` ║${pad(' ' + C.bold + C.primary + 'Engine' + C.reset)}║`);
|
|
37
|
+
console.log(` ║${pad(' ' + C.accent + (providerDisplayName || 'Cortex') + C.reset)}║`);
|
|
33
38
|
console.log(` ╚${'═'.repeat(width)}╝`);
|
|
34
39
|
console.log();
|
|
35
40
|
console.log(` ${C.dim}Type /help for commands · /quit to exit${C.reset}`);
|