@ducci/jarvis 1.0.96 → 1.0.98
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
|
@@ -150,6 +150,9 @@ async function callModelWithFallback(client, config, messages, tools) {
|
|
|
150
150
|
} catch (err) {
|
|
151
151
|
primaryErr = err;
|
|
152
152
|
}
|
|
153
|
+
if (!config.fallbackModel) {
|
|
154
|
+
throw primaryErr;
|
|
155
|
+
}
|
|
153
156
|
try {
|
|
154
157
|
return await callModel(client, config.fallbackModel, messages, tools);
|
|
155
158
|
} catch (fallbackErr) {
|
|
@@ -536,8 +539,9 @@ export async function runAgentLoop(client, config, session, prepareMessages, usa
|
|
|
536
539
|
try {
|
|
537
540
|
parsed = JSON.parse(sanitizeJson(content));
|
|
538
541
|
} catch {
|
|
539
|
-
// Step 1: retry with fallback model
|
|
542
|
+
// Step 1: retry with fallback model (if configured)
|
|
540
543
|
try {
|
|
544
|
+
if (!config.fallbackModel) throw new Error('no fallback model configured');
|
|
541
545
|
const fallbackResult = await callModel(client, config.fallbackModel, preparedMessages, toolDefs);
|
|
542
546
|
accumulateUsage(usageAccum, fallbackResult);
|
|
543
547
|
const fallbackContent = fallbackResult.choices[0]?.message?.content || '';
|
|
@@ -782,6 +786,7 @@ async function _runHandleChat(config, sessionId, userMessage, attachments = [],
|
|
|
782
786
|
} else {
|
|
783
787
|
userContent = userMessageWithContext;
|
|
784
788
|
}
|
|
789
|
+
const interactionStartIndex = session.messages.length;
|
|
785
790
|
session.messages.push({ role: 'user', content: userContent });
|
|
786
791
|
session.metadata.handoffCount = 0;
|
|
787
792
|
session.metadata.failedApproaches = [];
|
|
@@ -820,7 +825,7 @@ async function _runHandleChat(config, sessionId, userMessage, attachments = [],
|
|
|
820
825
|
// Safety check: if the last two assistant messages are both model_error
|
|
821
826
|
// synthetic notes, we are in a confirmed failure loop. Escalate immediately
|
|
822
827
|
// rather than burning more iterations on a stuck session.
|
|
823
|
-
if (hasConsecutiveModelErrors(session.messages)) {
|
|
828
|
+
if (hasConsecutiveModelErrors(session.messages.slice(interactionStartIndex))) {
|
|
824
829
|
finalResponse = 'The model has failed twice in a row. This is likely due to the conversation being too long for the model to process. Please start a new session or switch to a model with a larger context window.';
|
|
825
830
|
finalLogSummary = 'Consecutive model_error detected: session escalated to intervention_required without running another agent loop.';
|
|
826
831
|
finalStatus = 'intervention_required';
|
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',
|