@sulala/agent 0.1.7 → 0.1.8
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 +42 -28
- package/dist/agent/loop.js +1 -1
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/skills-config.js +1 -1
- package/dist/ai/orchestrator.d.ts +2 -0
- package/dist/ai/orchestrator.d.ts.map +1 -1
- package/dist/ai/orchestrator.js +324 -269
- package/dist/ai/orchestrator.js.map +1 -1
- package/dist/channels/telegram.js +1 -1
- package/dist/cli.js +3 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -4
- package/dist/config.js.map +1 -1
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/gateway/server.js +7 -4
- package/dist/gateway/server.js.map +1 -1
- package/dist/onboard-env.d.ts +1 -1
- package/dist/onboard-env.d.ts.map +1 -1
- package/dist/onboard-env.js +1 -0
- package/dist/onboard-env.js.map +1 -1
- package/dist/onboard.js +2 -2
- package/dist/plugins/index.d.ts +2 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +1 -1
- package/dist/plugins/index.js.map +1 -1
- package/dist/types.d.ts +3 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/ai/orchestrator.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { log, saveAiResult } from '../db/index.js';
|
|
2
2
|
const providers = new Map();
|
|
3
|
-
const defaultProvider = process.env.AI_DEFAULT_PROVIDER || 'ollama';
|
|
4
3
|
export function registerProvider(name, adapter) {
|
|
5
4
|
providers.set(name, adapter);
|
|
6
5
|
}
|
|
7
6
|
export function getProvider(name) {
|
|
8
|
-
const key = name ??
|
|
7
|
+
const key = name ?? process.env.AI_DEFAULT_PROVIDER ?? 'ollama';
|
|
9
8
|
const p = providers.get(key);
|
|
10
9
|
if (!p)
|
|
11
10
|
throw new Error(`Unknown AI provider: ${key}. Registered: ${[...providers.keys()].join(', ')}`);
|
|
@@ -13,7 +12,7 @@ export function getProvider(name) {
|
|
|
13
12
|
}
|
|
14
13
|
/** Stream completion (OpenAI, OpenRouter, Ollama). Falls back to non-streaming for other providers. */
|
|
15
14
|
export async function completeStream(options, onChunk) {
|
|
16
|
-
const provider = options.provider ??
|
|
15
|
+
const provider = options.provider ?? process.env.AI_DEFAULT_PROVIDER ?? 'ollama';
|
|
17
16
|
const messages = options.messages ?? [];
|
|
18
17
|
const model = options.model;
|
|
19
18
|
const max_tokens = options.max_tokens ?? 1024;
|
|
@@ -217,7 +216,7 @@ export async function completeStream(options, onChunk) {
|
|
|
217
216
|
let openAiStreamClient = null;
|
|
218
217
|
let openRouterStreamClient = null;
|
|
219
218
|
export async function complete(options = {}) {
|
|
220
|
-
const { provider =
|
|
219
|
+
const { provider = process.env.AI_DEFAULT_PROVIDER ?? 'ollama', model, messages, max_tokens = 1024, task_id = null, tools, signal, } = options;
|
|
221
220
|
const start = Date.now();
|
|
222
221
|
const adapter = getProvider(provider);
|
|
223
222
|
const id = `ai_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
@@ -263,210 +262,7 @@ function stubAdapter(defaultModel = 'stub') {
|
|
|
263
262
|
},
|
|
264
263
|
};
|
|
265
264
|
}
|
|
266
|
-
const OPENAI_DEFAULT = process.env.AI_OPENAI_DEFAULT_MODEL || 'gpt-4o-mini';
|
|
267
|
-
const OPENROUTER_DEFAULT = process.env.AI_OPENROUTER_DEFAULT_MODEL || 'openai/gpt-4o-mini';
|
|
268
|
-
registerProvider('stub', stubAdapter('stub'));
|
|
269
|
-
if (process.env.OPENAI_API_KEY) {
|
|
270
|
-
try {
|
|
271
|
-
const mod = await import('openai').catch(() => ({}));
|
|
272
|
-
const OpenAI = mod.OpenAI;
|
|
273
|
-
if (OpenAI) {
|
|
274
|
-
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
275
|
-
openAiStreamClient = client;
|
|
276
|
-
registerProvider('openai', {
|
|
277
|
-
defaultModel: OPENAI_DEFAULT,
|
|
278
|
-
async complete({ model, messages, max_tokens, tools: toolsOpt, signal }) {
|
|
279
|
-
// OpenAI expects tool_calls as { id, type: "function", function: { name, arguments } }
|
|
280
|
-
const openAiMessages = (messages ?? []).map((m) => {
|
|
281
|
-
if (!m.tool_calls?.length)
|
|
282
|
-
return m;
|
|
283
|
-
return {
|
|
284
|
-
...m,
|
|
285
|
-
tool_calls: m.tool_calls.map((tc) => ({
|
|
286
|
-
id: tc.id,
|
|
287
|
-
type: 'function',
|
|
288
|
-
function: { name: tc.name, arguments: tc.arguments ?? '' },
|
|
289
|
-
})),
|
|
290
|
-
};
|
|
291
|
-
});
|
|
292
|
-
const createOpts = {
|
|
293
|
-
model: model || OPENAI_DEFAULT,
|
|
294
|
-
messages: openAiMessages,
|
|
295
|
-
max_tokens: max_tokens || 1024,
|
|
296
|
-
};
|
|
297
|
-
if (toolsOpt?.length) {
|
|
298
|
-
createOpts.tools = toolsOpt.map((t) => ({
|
|
299
|
-
type: 'function',
|
|
300
|
-
function: { name: t.name, description: t.description, parameters: t.parameters ?? {} },
|
|
301
|
-
}));
|
|
302
|
-
}
|
|
303
|
-
const res = await client.chat.completions.create(createOpts, signal ? { signal } : undefined);
|
|
304
|
-
const choice = res.choices?.[0];
|
|
305
|
-
const msg = choice?.message;
|
|
306
|
-
const tool_calls = msg?.tool_calls?.map((tc) => ({
|
|
307
|
-
id: tc.id,
|
|
308
|
-
name: tc.function?.name ?? '',
|
|
309
|
-
arguments: tc.function?.arguments ?? '',
|
|
310
|
-
}));
|
|
311
|
-
return {
|
|
312
|
-
content: msg?.content ?? '',
|
|
313
|
-
usage: res.usage ?? {},
|
|
314
|
-
...(tool_calls?.length ? { tool_calls } : {}),
|
|
315
|
-
};
|
|
316
|
-
},
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
catch {
|
|
321
|
-
// import failed
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
if (process.env.OPENROUTER_API_KEY) {
|
|
325
|
-
try {
|
|
326
|
-
const mod = await import('openai').catch(() => ({}));
|
|
327
|
-
const OpenAI = mod.OpenAI;
|
|
328
|
-
if (OpenAI) {
|
|
329
|
-
const client = new OpenAI({
|
|
330
|
-
apiKey: process.env.OPENROUTER_API_KEY,
|
|
331
|
-
baseURL: 'https://openrouter.ai/api/v1',
|
|
332
|
-
});
|
|
333
|
-
openRouterStreamClient = client;
|
|
334
|
-
registerProvider('openrouter', {
|
|
335
|
-
defaultModel: OPENROUTER_DEFAULT,
|
|
336
|
-
async complete({ model, messages, max_tokens, tools: toolsOpt, signal }) {
|
|
337
|
-
const openAiMessages = (messages ?? []).map((m) => {
|
|
338
|
-
if (!m.tool_calls?.length)
|
|
339
|
-
return m;
|
|
340
|
-
return {
|
|
341
|
-
...m,
|
|
342
|
-
tool_calls: m.tool_calls.map((tc) => ({
|
|
343
|
-
id: tc.id,
|
|
344
|
-
type: 'function',
|
|
345
|
-
function: { name: tc.name, arguments: tc.arguments ?? '' },
|
|
346
|
-
})),
|
|
347
|
-
};
|
|
348
|
-
});
|
|
349
|
-
const createOpts = {
|
|
350
|
-
model: model || OPENROUTER_DEFAULT,
|
|
351
|
-
messages: openAiMessages,
|
|
352
|
-
max_tokens: max_tokens || 1024,
|
|
353
|
-
};
|
|
354
|
-
if (toolsOpt?.length) {
|
|
355
|
-
createOpts.tools = toolsOpt.map((t) => ({
|
|
356
|
-
type: 'function',
|
|
357
|
-
function: { name: t.name, description: t.description, parameters: t.parameters ?? {} },
|
|
358
|
-
}));
|
|
359
|
-
}
|
|
360
|
-
const res = await client.chat.completions.create(createOpts, signal ? { signal } : undefined);
|
|
361
|
-
const choice = res.choices?.[0];
|
|
362
|
-
const msg = choice?.message;
|
|
363
|
-
const tool_calls = msg?.tool_calls?.map((tc) => ({
|
|
364
|
-
id: tc.id,
|
|
365
|
-
name: tc.function?.name ?? '',
|
|
366
|
-
arguments: tc.function?.arguments ?? '',
|
|
367
|
-
}));
|
|
368
|
-
return {
|
|
369
|
-
content: msg?.content ?? '',
|
|
370
|
-
usage: res.usage ?? {},
|
|
371
|
-
...(tool_calls?.length ? { tool_calls } : {}),
|
|
372
|
-
};
|
|
373
|
-
},
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
catch {
|
|
378
|
-
// import failed
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
if (!providers.has('openai')) {
|
|
382
|
-
registerProvider('openai', stubAdapter(OPENAI_DEFAULT));
|
|
383
|
-
}
|
|
384
|
-
const CLAUDE_DEFAULT = process.env.AI_CLAUDE_DEFAULT_MODEL || 'claude-sonnet-4-6';
|
|
385
|
-
if (process.env.ANTHROPIC_API_KEY) {
|
|
386
|
-
registerProvider('claude', {
|
|
387
|
-
defaultModel: CLAUDE_DEFAULT,
|
|
388
|
-
async complete({ model, messages, max_tokens }) {
|
|
389
|
-
const key = process.env.ANTHROPIC_API_KEY;
|
|
390
|
-
const systemMsg = messages.find((m) => m.role === 'system');
|
|
391
|
-
const chatMessages = messages.filter((m) => m.role !== 'system').map((m) => ({
|
|
392
|
-
role: m.role === 'assistant' ? 'assistant' : 'user',
|
|
393
|
-
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
|
|
394
|
-
}));
|
|
395
|
-
const body = {
|
|
396
|
-
model: model || CLAUDE_DEFAULT,
|
|
397
|
-
max_tokens: max_tokens || 1024,
|
|
398
|
-
messages: chatMessages,
|
|
399
|
-
...(systemMsg && { system: typeof systemMsg.content === 'string' ? systemMsg.content : JSON.stringify(systemMsg.content) }),
|
|
400
|
-
};
|
|
401
|
-
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
402
|
-
method: 'POST',
|
|
403
|
-
headers: {
|
|
404
|
-
'Content-Type': 'application/json',
|
|
405
|
-
'x-api-key': key,
|
|
406
|
-
'anthropic-version': '2023-06-01',
|
|
407
|
-
},
|
|
408
|
-
body: JSON.stringify(body),
|
|
409
|
-
});
|
|
410
|
-
if (!res.ok) {
|
|
411
|
-
const err = await res.text();
|
|
412
|
-
throw new Error(`Anthropic API: ${res.status} ${err}`);
|
|
413
|
-
}
|
|
414
|
-
const data = (await res.json());
|
|
415
|
-
const content = data.content?.find((c) => c.type === 'text')?.text || '';
|
|
416
|
-
const usage = data.usage || {};
|
|
417
|
-
return { content, usage: { prompt_tokens: usage.input_tokens ?? 0, completion_tokens: usage.output_tokens ?? 0 } };
|
|
418
|
-
},
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
else {
|
|
422
|
-
registerProvider('claude', stubAdapter(CLAUDE_DEFAULT));
|
|
423
|
-
}
|
|
424
|
-
const GEMINI_DEFAULT = process.env.AI_GEMINI_DEFAULT_MODEL || 'gemini-2.5-flash';
|
|
425
|
-
const GEMINI_KEY = process.env.GOOGLE_GEMINI_API_KEY || process.env.GEMINI_API_KEY;
|
|
426
|
-
if (GEMINI_KEY) {
|
|
427
|
-
registerProvider('gemini', {
|
|
428
|
-
defaultModel: GEMINI_DEFAULT,
|
|
429
|
-
async complete({ model, messages, max_tokens }) {
|
|
430
|
-
const modelId = model || GEMINI_DEFAULT;
|
|
431
|
-
const url = `https://generativelanguage.googleapis.com/v1beta/models/${modelId}:generateContent?key=${GEMINI_KEY}`;
|
|
432
|
-
const contents = messages
|
|
433
|
-
.filter((m) => m.role !== 'system')
|
|
434
|
-
.map((m) => ({
|
|
435
|
-
role: (m.role === 'assistant' ? 'model' : 'user'),
|
|
436
|
-
parts: [{ text: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) }],
|
|
437
|
-
}));
|
|
438
|
-
const systemInstruction = messages.find((m) => m.role === 'system');
|
|
439
|
-
const body = {
|
|
440
|
-
contents: contents.length ? contents : [{ role: 'user', parts: [{ text: '' }] }],
|
|
441
|
-
generationConfig: { maxOutputTokens: max_tokens || 1024 },
|
|
442
|
-
...(systemInstruction && {
|
|
443
|
-
systemInstruction: { parts: [{ text: typeof systemInstruction.content === 'string' ? systemInstruction.content : JSON.stringify(systemInstruction.content) }] },
|
|
444
|
-
}),
|
|
445
|
-
};
|
|
446
|
-
const res = await fetch(url, {
|
|
447
|
-
method: 'POST',
|
|
448
|
-
headers: { 'Content-Type': 'application/json' },
|
|
449
|
-
body: JSON.stringify(body),
|
|
450
|
-
});
|
|
451
|
-
if (!res.ok) {
|
|
452
|
-
const err = await res.text();
|
|
453
|
-
throw new Error(`Gemini API: ${res.status} ${err}`);
|
|
454
|
-
}
|
|
455
|
-
const data = (await res.json());
|
|
456
|
-
const text = data.candidates?.[0]?.content?.parts?.[0]?.text ?? '';
|
|
457
|
-
const usage = data.usageMetadata || {};
|
|
458
|
-
return {
|
|
459
|
-
content: text,
|
|
460
|
-
usage: { prompt_tokens: usage.promptTokenCount ?? 0, completion_tokens: usage.candidatesTokenCount ?? 0 },
|
|
461
|
-
};
|
|
462
|
-
},
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
else {
|
|
466
|
-
registerProvider('gemini', stubAdapter(GEMINI_DEFAULT));
|
|
467
|
-
}
|
|
468
265
|
const OLLAMA_DEFAULT = process.env.AI_OLLAMA_DEFAULT_MODEL || 'llama3.2';
|
|
469
|
-
const OLLAMA_BASE = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
|
|
470
266
|
const OLLAMA_THINKING_MODELS = ['deepseek-r1', 'qwen3', 'deepseek-v3.1', 'gpt-oss'];
|
|
471
267
|
function ollamaSupportsThinking(model) {
|
|
472
268
|
const m = (model || OLLAMA_DEFAULT).toLowerCase();
|
|
@@ -514,79 +310,338 @@ function fromOllamaToolCalls(toolCalls) {
|
|
|
514
310
|
return { id: `ollama_${i}_${Date.now()}`, name, arguments: argsStr };
|
|
515
311
|
});
|
|
516
312
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
model: model || OLLAMA_DEFAULT,
|
|
524
|
-
messages: ollamaMessages,
|
|
525
|
-
options: { num_predict: max_tokens || 1024 },
|
|
526
|
-
stream: false,
|
|
527
|
-
think: think !== false && ollamaSupportsThinking(model),
|
|
528
|
-
};
|
|
529
|
-
if (toolsOpt?.length) {
|
|
530
|
-
body.tools = toolsOpt.map((t) => ({
|
|
531
|
-
type: 'function',
|
|
532
|
-
function: { name: t.name, description: t.description, parameters: t.parameters ?? {} },
|
|
533
|
-
}));
|
|
534
|
-
}
|
|
535
|
-
console.log('[Ollama] POST', url, 'model:', model || OLLAMA_DEFAULT, toolsOpt?.length ? `tools=${toolsOpt.length}` : '');
|
|
536
|
-
let res;
|
|
313
|
+
const OPENAI_DEFAULT = process.env.AI_OPENAI_DEFAULT_MODEL || 'gpt-4o-mini';
|
|
314
|
+
const OPENROUTER_DEFAULT = process.env.AI_OPENROUTER_DEFAULT_MODEL || 'openai/gpt-4o-mini';
|
|
315
|
+
async function registerAllProviders() {
|
|
316
|
+
providers.clear();
|
|
317
|
+
registerProvider('stub', stubAdapter('stub'));
|
|
318
|
+
if (process.env.OPENAI_API_KEY) {
|
|
537
319
|
try {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
320
|
+
const mod = await import('openai').catch(() => ({}));
|
|
321
|
+
const OpenAI = mod.OpenAI;
|
|
322
|
+
if (OpenAI) {
|
|
323
|
+
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
324
|
+
openAiStreamClient = client;
|
|
325
|
+
registerProvider('openai', {
|
|
326
|
+
defaultModel: OPENAI_DEFAULT,
|
|
327
|
+
async complete({ model, messages, max_tokens, tools: toolsOpt, signal }) {
|
|
328
|
+
// OpenAI expects tool_calls as { id, type: "function", function: { name, arguments } }
|
|
329
|
+
const openAiMessages = (messages ?? []).map((m) => {
|
|
330
|
+
if (!m.tool_calls?.length)
|
|
331
|
+
return m;
|
|
332
|
+
return {
|
|
333
|
+
...m,
|
|
334
|
+
tool_calls: m.tool_calls.map((tc) => ({
|
|
335
|
+
id: tc.id,
|
|
336
|
+
type: 'function',
|
|
337
|
+
function: { name: tc.name, arguments: tc.arguments ?? '' },
|
|
338
|
+
})),
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
const createOpts = {
|
|
342
|
+
model: model || OPENAI_DEFAULT,
|
|
343
|
+
messages: openAiMessages,
|
|
344
|
+
max_tokens: max_tokens || 1024,
|
|
345
|
+
};
|
|
346
|
+
if (toolsOpt?.length) {
|
|
347
|
+
createOpts.tools = toolsOpt.map((t) => ({
|
|
348
|
+
type: 'function',
|
|
349
|
+
function: { name: t.name, description: t.description, parameters: t.parameters ?? {} },
|
|
350
|
+
}));
|
|
351
|
+
}
|
|
352
|
+
const res = await client.chat.completions.create(createOpts, signal ? { signal } : undefined);
|
|
353
|
+
const choice = res.choices?.[0];
|
|
354
|
+
const msg = choice?.message;
|
|
355
|
+
const tool_calls = msg?.tool_calls?.map((tc) => ({
|
|
356
|
+
id: tc.id,
|
|
357
|
+
name: tc.function?.name ?? '',
|
|
358
|
+
arguments: tc.function?.arguments ?? '',
|
|
359
|
+
}));
|
|
360
|
+
return {
|
|
361
|
+
content: msg?.content ?? '',
|
|
362
|
+
usage: res.usage ?? {},
|
|
363
|
+
...(tool_calls?.length ? { tool_calls } : {}),
|
|
364
|
+
};
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
// import failed
|
|
543
371
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
372
|
+
}
|
|
373
|
+
if (process.env.OPENROUTER_API_KEY) {
|
|
374
|
+
try {
|
|
375
|
+
const mod = await import('openai').catch(() => ({}));
|
|
376
|
+
const OpenAI = mod.OpenAI;
|
|
377
|
+
if (OpenAI) {
|
|
378
|
+
const client = new OpenAI({
|
|
379
|
+
apiKey: process.env.OPENROUTER_API_KEY,
|
|
380
|
+
baseURL: 'https://openrouter.ai/api/v1',
|
|
381
|
+
});
|
|
382
|
+
openRouterStreamClient = client;
|
|
383
|
+
registerProvider('openrouter', {
|
|
384
|
+
defaultModel: OPENROUTER_DEFAULT,
|
|
385
|
+
async complete({ model, messages, max_tokens, tools: toolsOpt, signal }) {
|
|
386
|
+
const openAiMessages = (messages ?? []).map((m) => {
|
|
387
|
+
if (!m.tool_calls?.length)
|
|
388
|
+
return m;
|
|
389
|
+
return {
|
|
390
|
+
...m,
|
|
391
|
+
tool_calls: m.tool_calls.map((tc) => ({
|
|
392
|
+
id: tc.id,
|
|
393
|
+
type: 'function',
|
|
394
|
+
function: { name: tc.name, arguments: tc.arguments ?? '' },
|
|
395
|
+
})),
|
|
396
|
+
};
|
|
397
|
+
});
|
|
398
|
+
const createOpts = {
|
|
399
|
+
model: model || OPENROUTER_DEFAULT,
|
|
400
|
+
messages: openAiMessages,
|
|
401
|
+
max_tokens: max_tokens || 1024,
|
|
402
|
+
};
|
|
403
|
+
if (toolsOpt?.length) {
|
|
404
|
+
createOpts.tools = toolsOpt.map((t) => ({
|
|
405
|
+
type: 'function',
|
|
406
|
+
function: { name: t.name, description: t.description, parameters: t.parameters ?? {} },
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
const res = await client.chat.completions.create(createOpts, signal ? { signal } : undefined);
|
|
410
|
+
const choice = res.choices?.[0];
|
|
411
|
+
const msg = choice?.message;
|
|
412
|
+
const tool_calls = msg?.tool_calls?.map((tc) => ({
|
|
413
|
+
id: tc.id,
|
|
414
|
+
name: tc.function?.name ?? '',
|
|
415
|
+
arguments: tc.function?.arguments ?? '',
|
|
416
|
+
}));
|
|
417
|
+
return {
|
|
418
|
+
content: msg?.content ?? '',
|
|
419
|
+
usage: res.usage ?? {},
|
|
420
|
+
...(tool_calls?.length ? { tool_calls } : {}),
|
|
421
|
+
};
|
|
422
|
+
},
|
|
423
|
+
});
|
|
549
424
|
}
|
|
550
|
-
throw e;
|
|
551
425
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
426
|
+
catch {
|
|
427
|
+
// import failed
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (!providers.has('openai')) {
|
|
431
|
+
registerProvider('openai', stubAdapter(OPENAI_DEFAULT));
|
|
432
|
+
}
|
|
433
|
+
const CLAUDE_DEFAULT = process.env.AI_CLAUDE_DEFAULT_MODEL || 'claude-sonnet-4-6';
|
|
434
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
435
|
+
registerProvider('claude', {
|
|
436
|
+
defaultModel: CLAUDE_DEFAULT,
|
|
437
|
+
async complete({ model, messages, max_tokens }) {
|
|
438
|
+
const key = process.env.ANTHROPIC_API_KEY;
|
|
439
|
+
const systemMsg = messages.find((m) => m.role === 'system');
|
|
440
|
+
const chatMessages = messages.filter((m) => m.role !== 'system').map((m) => ({
|
|
441
|
+
role: m.role === 'assistant' ? 'assistant' : 'user',
|
|
442
|
+
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
|
|
443
|
+
}));
|
|
444
|
+
const body = {
|
|
445
|
+
model: model || CLAUDE_DEFAULT,
|
|
446
|
+
max_tokens: max_tokens || 1024,
|
|
447
|
+
messages: chatMessages,
|
|
448
|
+
...(systemMsg && { system: typeof systemMsg.content === 'string' ? systemMsg.content : JSON.stringify(systemMsg.content) }),
|
|
449
|
+
};
|
|
450
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
451
|
+
method: 'POST',
|
|
452
|
+
headers: {
|
|
453
|
+
'Content-Type': 'application/json',
|
|
454
|
+
'x-api-key': key,
|
|
455
|
+
'anthropic-version': '2023-06-01',
|
|
456
|
+
},
|
|
457
|
+
body: JSON.stringify(body),
|
|
458
|
+
});
|
|
459
|
+
if (!res.ok) {
|
|
460
|
+
const err = await res.text();
|
|
461
|
+
throw new Error(`Anthropic API: ${res.status} ${err}`);
|
|
462
|
+
}
|
|
463
|
+
const data = (await res.json());
|
|
464
|
+
const content = data.content?.find((c) => c.type === 'text')?.text || '';
|
|
465
|
+
const usage = data.usage || {};
|
|
466
|
+
return { content, usage: { prompt_tokens: usage.input_tokens ?? 0, completion_tokens: usage.output_tokens ?? 0 } };
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
registerProvider('claude', stubAdapter(CLAUDE_DEFAULT));
|
|
472
|
+
}
|
|
473
|
+
const GEMINI_DEFAULT = process.env.AI_GEMINI_DEFAULT_MODEL || 'gemini-2.5-flash';
|
|
474
|
+
const GEMINI_KEY = process.env.GOOGLE_GEMINI_API_KEY || process.env.GEMINI_API_KEY;
|
|
475
|
+
if (GEMINI_KEY) {
|
|
476
|
+
registerProvider('gemini', {
|
|
477
|
+
defaultModel: GEMINI_DEFAULT,
|
|
478
|
+
async complete({ model, messages, max_tokens }) {
|
|
479
|
+
const modelId = model || GEMINI_DEFAULT;
|
|
480
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${modelId}:generateContent?key=${GEMINI_KEY}`;
|
|
481
|
+
const contents = messages
|
|
482
|
+
.filter((m) => m.role !== 'system')
|
|
483
|
+
.map((m) => ({
|
|
484
|
+
role: (m.role === 'assistant' ? 'model' : 'user'),
|
|
485
|
+
parts: [{ text: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) }],
|
|
486
|
+
}));
|
|
487
|
+
const systemInstruction = messages.find((m) => m.role === 'system');
|
|
488
|
+
const body = {
|
|
489
|
+
contents: contents.length ? contents : [{ role: 'user', parts: [{ text: '' }] }],
|
|
490
|
+
generationConfig: { maxOutputTokens: max_tokens || 1024 },
|
|
491
|
+
...(systemInstruction && {
|
|
492
|
+
systemInstruction: { parts: [{ text: typeof systemInstruction.content === 'string' ? systemInstruction.content : JSON.stringify(systemInstruction.content) }] },
|
|
493
|
+
}),
|
|
494
|
+
};
|
|
495
|
+
const res = await fetch(url, {
|
|
496
|
+
method: 'POST',
|
|
497
|
+
headers: { 'Content-Type': 'application/json' },
|
|
498
|
+
body: JSON.stringify(body),
|
|
499
|
+
});
|
|
500
|
+
if (!res.ok) {
|
|
501
|
+
const err = await res.text();
|
|
502
|
+
throw new Error(`Gemini API: ${res.status} ${err}`);
|
|
503
|
+
}
|
|
504
|
+
const data = (await res.json());
|
|
505
|
+
const text = data.candidates?.[0]?.content?.parts?.[0]?.text ?? '';
|
|
506
|
+
const usage = data.usageMetadata || {};
|
|
507
|
+
return {
|
|
508
|
+
content: text,
|
|
509
|
+
usage: { prompt_tokens: usage.promptTokenCount ?? 0, completion_tokens: usage.candidatesTokenCount ?? 0 },
|
|
510
|
+
};
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
registerProvider('gemini', stubAdapter(GEMINI_DEFAULT));
|
|
516
|
+
}
|
|
517
|
+
const OLLAMA_DEFAULT = process.env.AI_OLLAMA_DEFAULT_MODEL || 'llama3.2';
|
|
518
|
+
const OLLAMA_BASE = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
|
|
519
|
+
const OLLAMA_THINKING_MODELS = ['deepseek-r1', 'qwen3', 'deepseek-v3.1', 'gpt-oss'];
|
|
520
|
+
function ollamaSupportsThinking(model) {
|
|
521
|
+
const m = (model || OLLAMA_DEFAULT).toLowerCase();
|
|
522
|
+
return OLLAMA_THINKING_MODELS.some((t) => m === t || m.startsWith(t + ':'));
|
|
523
|
+
}
|
|
524
|
+
/** Convert agent messages to Ollama format (tool_calls use index/object args; tool results use tool_name). */
|
|
525
|
+
function toOllamaMessages(messages) {
|
|
526
|
+
return messages.map((m) => {
|
|
527
|
+
const base = {
|
|
528
|
+
role: m.role,
|
|
529
|
+
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? ''),
|
|
530
|
+
};
|
|
531
|
+
if (m.role === 'assistant' && m.tool_calls?.length) {
|
|
532
|
+
base.tool_calls = m.tool_calls.map((tc, i) => ({
|
|
533
|
+
type: 'function',
|
|
534
|
+
function: {
|
|
535
|
+
index: i,
|
|
536
|
+
name: tc.name,
|
|
537
|
+
arguments: (() => {
|
|
538
|
+
try {
|
|
539
|
+
return typeof tc.arguments === 'string' ? JSON.parse(tc.arguments) : (tc.arguments ?? {});
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
return {};
|
|
543
|
+
}
|
|
544
|
+
})(),
|
|
545
|
+
},
|
|
546
|
+
}));
|
|
547
|
+
}
|
|
548
|
+
if (m.role === 'tool') {
|
|
549
|
+
base.tool_name = m.name ?? '';
|
|
550
|
+
}
|
|
551
|
+
return base;
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
/** Parse Ollama tool_calls (arguments may be object or string) into agent format. */
|
|
555
|
+
function fromOllamaToolCalls(toolCalls) {
|
|
556
|
+
if (!toolCalls?.length)
|
|
557
|
+
return undefined;
|
|
558
|
+
return toolCalls.map((tc, i) => {
|
|
559
|
+
const fn = tc.function;
|
|
560
|
+
const name = fn?.name ?? '';
|
|
561
|
+
const args = fn?.arguments;
|
|
562
|
+
const argsStr = typeof args === 'string' ? args : JSON.stringify(args ?? {});
|
|
563
|
+
return { id: `ollama_${i}_${Date.now()}`, name, arguments: argsStr };
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
registerProvider('ollama', {
|
|
567
|
+
defaultModel: OLLAMA_DEFAULT,
|
|
568
|
+
async complete({ model, messages, max_tokens, think, tools: toolsOpt }) {
|
|
569
|
+
const url = `${OLLAMA_BASE}/api/chat`;
|
|
570
|
+
const ollamaMessages = toOllamaMessages(messages ?? []);
|
|
571
|
+
const body = {
|
|
572
|
+
model: model || OLLAMA_DEFAULT,
|
|
573
|
+
messages: ollamaMessages,
|
|
574
|
+
options: { num_predict: max_tokens || 1024 },
|
|
575
|
+
stream: false,
|
|
576
|
+
think: think !== false && ollamaSupportsThinking(model),
|
|
577
|
+
};
|
|
578
|
+
if (toolsOpt?.length) {
|
|
579
|
+
body.tools = toolsOpt.map((t) => ({
|
|
580
|
+
type: 'function',
|
|
581
|
+
function: { name: t.name, description: t.description, parameters: t.parameters ?? {} },
|
|
582
|
+
}));
|
|
583
|
+
}
|
|
584
|
+
console.log('[Ollama] POST', url, 'model:', model || OLLAMA_DEFAULT, toolsOpt?.length ? `tools=${toolsOpt.length}` : '');
|
|
585
|
+
let res;
|
|
586
|
+
try {
|
|
558
587
|
res = await fetch(url, {
|
|
559
588
|
method: 'POST',
|
|
560
589
|
headers: { 'Content-Type': 'application/json' },
|
|
561
590
|
body: JSON.stringify(body),
|
|
562
591
|
});
|
|
563
|
-
|
|
592
|
+
}
|
|
593
|
+
catch (e) {
|
|
594
|
+
console.error('[Ollama] fetch failed:', e);
|
|
595
|
+
const cause = e?.cause;
|
|
596
|
+
if (cause?.code === 'ECONNREFUSED') {
|
|
597
|
+
throw new Error('Ollama is not running. Start it with: ollama serve (or open the Ollama app).');
|
|
598
|
+
}
|
|
599
|
+
throw e;
|
|
564
600
|
}
|
|
565
601
|
if (!res.ok) {
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
if (res.status ===
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
602
|
+
const errText = await res.text();
|
|
603
|
+
let didRetry = false;
|
|
604
|
+
if (res.status === 400 && errText.includes('does not support tools') && body.tools) {
|
|
605
|
+
delete body.tools;
|
|
606
|
+
console.log('[Ollama] Model does not support tools; retrying without tools');
|
|
607
|
+
res = await fetch(url, {
|
|
608
|
+
method: 'POST',
|
|
609
|
+
headers: { 'Content-Type': 'application/json' },
|
|
610
|
+
body: JSON.stringify(body),
|
|
611
|
+
});
|
|
612
|
+
didRetry = true;
|
|
613
|
+
}
|
|
614
|
+
if (!res.ok) {
|
|
615
|
+
const err = didRetry ? await res.text() : errText;
|
|
616
|
+
const modelName = model || OLLAMA_DEFAULT;
|
|
617
|
+
if (res.status === 404 && (err.includes('not found') || err.includes('model'))) {
|
|
618
|
+
const { pullOllamaModel } = await import('../ollama-setup.js');
|
|
619
|
+
pullOllamaModel(modelName);
|
|
620
|
+
throw new Error(`Model "${modelName}" not found. Pulling it now (see terminal); try again in 1–2 min. Or run: ollama pull ${modelName}`);
|
|
621
|
+
}
|
|
622
|
+
console.error('[Ollama]', res.status, err);
|
|
623
|
+
throw new Error(`Ollama: ${res.status} ${err}`);
|
|
572
624
|
}
|
|
573
|
-
console.error('[Ollama]', res.status, err);
|
|
574
|
-
throw new Error(`Ollama: ${res.status} ${err}`);
|
|
575
625
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
626
|
+
const data = (await res.json());
|
|
627
|
+
const msg = data.message;
|
|
628
|
+
const thinking = msg?.thinking ?? '';
|
|
629
|
+
const content = msg?.content ?? '';
|
|
630
|
+
const fullContent = thinking ? thinking + (content ? '\n\n' + content : '') : content;
|
|
631
|
+
const tool_calls = fromOllamaToolCalls(msg?.tool_calls);
|
|
632
|
+
return {
|
|
633
|
+
content: fullContent,
|
|
634
|
+
usage: data.eval_count != null ? { completion_tokens: data.eval_count } : undefined,
|
|
635
|
+
...(tool_calls?.length ? { tool_calls } : {}),
|
|
636
|
+
};
|
|
637
|
+
},
|
|
638
|
+
});
|
|
639
|
+
if (!providers.has('llama'))
|
|
640
|
+
registerProvider('llama', getProvider('ollama'));
|
|
641
|
+
}
|
|
642
|
+
await registerAllProviders();
|
|
643
|
+
/** Reload AI providers from current process.env (e.g. after onboarding saves new API keys). No restart needed. */
|
|
644
|
+
export async function reloadProviders() {
|
|
645
|
+
await registerAllProviders();
|
|
646
|
+
}
|
|
592
647
|
//# sourceMappingURL=orchestrator.js.map
|