@ducci/jarvis 1.0.97 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ducci/jarvis",
3
- "version": "1.0.97",
3
+ "version": "1.0.98",
4
4
  "description": "A fully automated agent system that lives on a server.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -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 === 'z-ai') {
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
  }
@@ -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 || '';
@@ -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,
@@ -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',