@ducci/jarvis 1.0.97 → 1.0.99
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 +1 -1
- package/src/scripts/onboarding.js +81 -1
- package/src/server/agent.js +7 -2
- package/src/server/config.js +6 -1
- package/src/server/provider.js +6 -0
package/package.json
CHANGED
|
@@ -121,6 +121,7 @@ async function run() {
|
|
|
121
121
|
{ name: 'OpenRouter (access many models via one key)', value: 'openrouter' },
|
|
122
122
|
{ name: 'Anthropic Direct (use your Anthropic API key)', value: 'anthropic' },
|
|
123
123
|
{ name: 'Z.AI Direct (GLM models, use your Z.AI API key)', value: 'z-ai' },
|
|
124
|
+
{ name: 'OpenAI-compatible (any provider with an OpenAI-compatible API)', value: 'openai-compatible' },
|
|
124
125
|
],
|
|
125
126
|
default: settings.provider || 'openrouter',
|
|
126
127
|
}
|
|
@@ -187,6 +188,46 @@ async function run() {
|
|
|
187
188
|
saveEnvVar('ZAI_API_KEY', apiKey);
|
|
188
189
|
console.log(chalk.green('Z.AI API key saved.'));
|
|
189
190
|
}
|
|
191
|
+
} else if (provider === 'openai-compatible') {
|
|
192
|
+
const { baseURL } = await inquirer.prompt([
|
|
193
|
+
{
|
|
194
|
+
type: 'input',
|
|
195
|
+
name: 'baseURL',
|
|
196
|
+
message: 'Enter the base URL of the OpenAI-compatible API (e.g., https://api.example.com/v1):',
|
|
197
|
+
default: settings.customBaseURL || '',
|
|
198
|
+
validate: (input) => input.trim().length > 0 || 'Base URL cannot be empty.',
|
|
199
|
+
}
|
|
200
|
+
]);
|
|
201
|
+
settings.customBaseURL = baseURL.trim();
|
|
202
|
+
|
|
203
|
+
const existingKey = loadEnvVar('CUSTOM_API_KEY');
|
|
204
|
+
apiKey = existingKey;
|
|
205
|
+
|
|
206
|
+
if (existingKey) {
|
|
207
|
+
const { keepKey } = await inquirer.prompt([
|
|
208
|
+
{
|
|
209
|
+
type: 'confirm',
|
|
210
|
+
name: 'keepKey',
|
|
211
|
+
message: 'A CUSTOM_API_KEY is already configured. Do you want to keep it?',
|
|
212
|
+
default: true,
|
|
213
|
+
}
|
|
214
|
+
]);
|
|
215
|
+
if (!keepKey) apiKey = null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!apiKey) {
|
|
219
|
+
const { newKey } = await inquirer.prompt([
|
|
220
|
+
{
|
|
221
|
+
type: 'password',
|
|
222
|
+
name: 'newKey',
|
|
223
|
+
message: 'Enter your API key:',
|
|
224
|
+
validate: (input) => input.length >= 1 || 'API key cannot be empty.',
|
|
225
|
+
}
|
|
226
|
+
]);
|
|
227
|
+
apiKey = newKey;
|
|
228
|
+
saveEnvVar('CUSTOM_API_KEY', apiKey);
|
|
229
|
+
console.log(chalk.green('API key saved.'));
|
|
230
|
+
}
|
|
190
231
|
} else {
|
|
191
232
|
const existingKey = loadEnvVar('OPENROUTER_API_KEY');
|
|
192
233
|
apiKey = existingKey;
|
|
@@ -238,7 +279,17 @@ async function run() {
|
|
|
238
279
|
}
|
|
239
280
|
|
|
240
281
|
if (!selectedModel) {
|
|
241
|
-
if (provider === '
|
|
282
|
+
if (provider === 'openai-compatible') {
|
|
283
|
+
const { manualModel } = await inquirer.prompt([
|
|
284
|
+
{
|
|
285
|
+
type: 'input',
|
|
286
|
+
name: 'manualModel',
|
|
287
|
+
message: 'Enter model ID (e.g., gpt-4o, mistral-large):',
|
|
288
|
+
validate: (input) => input.trim().length > 0 || 'Model ID cannot be empty.',
|
|
289
|
+
}
|
|
290
|
+
]);
|
|
291
|
+
selectedModel = manualModel.trim();
|
|
292
|
+
} else if (provider === 'z-ai') {
|
|
242
293
|
const models = await fetchZaiModels(apiKey);
|
|
243
294
|
let choices;
|
|
244
295
|
if (models.length > 0) {
|
|
@@ -374,8 +425,37 @@ async function run() {
|
|
|
374
425
|
if (!settings.fallbackModel || previousProvider !== provider) {
|
|
375
426
|
if (provider === 'anthropic') settings.fallbackModel = 'claude-haiku-4-5-20251001';
|
|
376
427
|
else if (provider === 'z-ai') settings.fallbackModel = 'glm-4-flash';
|
|
428
|
+
else if (provider === 'openai-compatible') settings.fallbackModel = null;
|
|
377
429
|
else settings.fallbackModel = 'openrouter/free';
|
|
378
430
|
}
|
|
431
|
+
|
|
432
|
+
if (provider === 'openai-compatible') {
|
|
433
|
+
const currentFallback = settings.fallbackModel;
|
|
434
|
+
if (currentFallback) {
|
|
435
|
+
const { keepFallback } = await inquirer.prompt([
|
|
436
|
+
{
|
|
437
|
+
type: 'list',
|
|
438
|
+
name: 'keepFallback',
|
|
439
|
+
message: `Current fallback model is ${chalk.yellow(currentFallback)}. Keep it or change it?`,
|
|
440
|
+
choices: [
|
|
441
|
+
{ name: 'Keep current fallback model', value: true },
|
|
442
|
+
{ name: 'Change fallback model', value: false },
|
|
443
|
+
],
|
|
444
|
+
}
|
|
445
|
+
]);
|
|
446
|
+
if (!keepFallback) settings.fallbackModel = null;
|
|
447
|
+
}
|
|
448
|
+
if (!settings.fallbackModel) {
|
|
449
|
+
const { fallbackModel } = await inquirer.prompt([
|
|
450
|
+
{
|
|
451
|
+
type: 'input',
|
|
452
|
+
name: 'fallbackModel',
|
|
453
|
+
message: 'Enter fallback model ID (leave empty to skip):',
|
|
454
|
+
}
|
|
455
|
+
]);
|
|
456
|
+
settings.fallbackModel = fallbackModel.trim() || null;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
379
459
|
if (settings.maxIterations === undefined) {
|
|
380
460
|
settings.maxIterations = 20;
|
|
381
461
|
}
|
package/src/server/agent.js
CHANGED
|
@@ -22,7 +22,8 @@ function stripCodeFence(text) {
|
|
|
22
22
|
// JSON string values — models sometimes forget to escape \n as \\n
|
|
23
23
|
// 3. Remove trailing commas before } and ] (JS-valid but JSON-invalid)
|
|
24
24
|
function sanitizeJson(text) {
|
|
25
|
-
|
|
25
|
+
// Strip <think>...</think> blocks (e.g. MiniMax reasoning traces) before JSON parsing
|
|
26
|
+
let s = stripCodeFence(text).replace(/<think>[\s\S]*?<\/think>/g, '').trim();
|
|
26
27
|
|
|
27
28
|
// State machine: walk the string and fix unescaped control chars inside strings
|
|
28
29
|
let result = '';
|
|
@@ -150,6 +151,9 @@ async function callModelWithFallback(client, config, messages, tools) {
|
|
|
150
151
|
} catch (err) {
|
|
151
152
|
primaryErr = err;
|
|
152
153
|
}
|
|
154
|
+
if (!config.fallbackModel) {
|
|
155
|
+
throw primaryErr;
|
|
156
|
+
}
|
|
153
157
|
try {
|
|
154
158
|
return await callModel(client, config.fallbackModel, messages, tools);
|
|
155
159
|
} catch (fallbackErr) {
|
|
@@ -536,8 +540,9 @@ export async function runAgentLoop(client, config, session, prepareMessages, usa
|
|
|
536
540
|
try {
|
|
537
541
|
parsed = JSON.parse(sanitizeJson(content));
|
|
538
542
|
} catch {
|
|
539
|
-
// Step 1: retry with fallback model
|
|
543
|
+
// Step 1: retry with fallback model (if configured)
|
|
540
544
|
try {
|
|
545
|
+
if (!config.fallbackModel) throw new Error('no fallback model configured');
|
|
541
546
|
const fallbackResult = await callModel(client, config.fallbackModel, preparedMessages, toolDefs);
|
|
542
547
|
accumulateUsage(usageAccum, fallbackResult);
|
|
543
548
|
const fallbackContent = fallbackResult.choices[0]?.message?.content || '';
|
package/src/server/config.js
CHANGED
|
@@ -50,6 +50,10 @@ export function loadConfig() {
|
|
|
50
50
|
} else if (provider === 'z-ai') {
|
|
51
51
|
apiKey = process.env.ZAI_API_KEY;
|
|
52
52
|
if (!apiKey) throw new Error('ZAI_API_KEY not found. Add it to ~/.jarvis/.env first.');
|
|
53
|
+
} else if (provider === 'openai-compatible') {
|
|
54
|
+
apiKey = process.env.CUSTOM_API_KEY;
|
|
55
|
+
if (!apiKey) throw new Error('CUSTOM_API_KEY not found. Run `jarvis setup` first.');
|
|
56
|
+
if (!settings.customBaseURL) throw new Error('customBaseURL not set in settings.json. Run `jarvis setup` first.');
|
|
53
57
|
} else {
|
|
54
58
|
apiKey = process.env.OPENROUTER_API_KEY;
|
|
55
59
|
if (!apiKey) throw new Error('OPENROUTER_API_KEY not found. Run `jarvis setup` first.');
|
|
@@ -71,8 +75,9 @@ export function loadConfig() {
|
|
|
71
75
|
return {
|
|
72
76
|
provider,
|
|
73
77
|
apiKey,
|
|
78
|
+
baseURL: settings.customBaseURL || null,
|
|
74
79
|
selectedModel: settings.selectedModel,
|
|
75
|
-
fallbackModel: settings.fallbackModel || (provider === 'anthropic' ? 'claude-haiku-4-5-20251001' : 'openrouter/free'),
|
|
80
|
+
fallbackModel: settings.fallbackModel || (provider === 'anthropic' ? 'claude-haiku-4-5-20251001' : provider === 'openai-compatible' ? null : 'openrouter/free'),
|
|
76
81
|
maxIterations: settings.maxIterations || 20,
|
|
77
82
|
maxHandoffs: settings.maxHandoffs || 3,
|
|
78
83
|
messageWindow: settings.messageWindow || 300,
|
package/src/server/provider.js
CHANGED
|
@@ -155,6 +155,12 @@ export function createClient(config) {
|
|
|
155
155
|
apiKey: config.apiKey,
|
|
156
156
|
});
|
|
157
157
|
}
|
|
158
|
+
if (config.provider === 'openai-compatible') {
|
|
159
|
+
return new OpenAI({
|
|
160
|
+
baseURL: config.baseURL,
|
|
161
|
+
apiKey: config.apiKey,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
158
164
|
// Default: OpenRouter (OpenAI-compatible)
|
|
159
165
|
return new OpenAI({
|
|
160
166
|
baseURL: 'https://openrouter.ai/api/v1',
|