omnikey-cli 1.5.8 → 1.6.1
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/backend-dist/__tests__/ai-client.nemotron.test.js +127 -0
- package/backend-dist/agent/agentPrompts.js +4 -3
- package/backend-dist/agent/utils.js +6 -5
- package/backend-dist/ai-client.js +151 -16
- package/backend-dist/aiProviderRoutes.js +247 -0
- package/backend-dist/config.js +16 -1
- package/backend-dist/db.js +5 -1
- package/backend-dist/index.js +27 -3
- package/backend-dist/mcpServerRoutes.js +16 -4
- package/backend-dist/scheduledJobRoutes.js +5 -2
- package/dist/index.js +1 -1
- package/dist/onboard.js +38 -0
- package/dist/telegramClient.js +1 -1
- package/dist/telegramDaemon.js +6 -4
- package/package.json +8 -6
- package/src/index.ts +1 -1
- package/src/onboard.ts +38 -0
- package/src/telegramClient.ts +1 -1
- package/src/telegramDaemon.ts +6 -8
- package/telegram-client-dist/{dist/agentClient.js → agentClient.js} +69 -75
- package/telegram-client-dist/{dist/config.js → config.js} +10 -12
- package/telegram-client-dist/{dist/index.js → index.js} +23 -23
- package/telegram-client-dist/{dist/notifyTelegram.js → notifyTelegram.js} +177 -191
- package/telegram-client-dist/{dist/omnikeyAuth.js → omnikeyAuth.js} +8 -13
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tests for the Nemotron adapter.
|
|
4
|
+
*
|
|
5
|
+
* The Nemotron adapter delegates to the `openai` SDK with a custom `baseURL`
|
|
6
|
+
* (NVIDIA NIM is OpenAI-compatible), so these tests mock the same surface as
|
|
7
|
+
* `ai-client.adapters.test.ts` and verify that:
|
|
8
|
+
* - the OpenAI client is constructed with the correct `baseURL`,
|
|
9
|
+
* - chat completions are routed through the Nemotron model id, and
|
|
10
|
+
* - streaming is wired up end-to-end including usage accounting.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
const vitest_1 = require("vitest");
|
|
14
|
+
const mocks = vitest_1.vi.hoisted(() => ({
|
|
15
|
+
openaiCreate: vitest_1.vi.fn(),
|
|
16
|
+
openaiCtor: vitest_1.vi.fn(),
|
|
17
|
+
}));
|
|
18
|
+
vitest_1.vi.mock('openai', () => ({
|
|
19
|
+
default: class MockOpenAI {
|
|
20
|
+
constructor(opts) {
|
|
21
|
+
this.chat = { completions: { create: mocks.openaiCreate } };
|
|
22
|
+
this.images = { generate: vitest_1.vi.fn() };
|
|
23
|
+
mocks.openaiCtor(opts);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
}));
|
|
27
|
+
vitest_1.vi.mock('@anthropic-ai/sdk', () => ({
|
|
28
|
+
default: class MockAnthropic {
|
|
29
|
+
constructor(_opts) {
|
|
30
|
+
this.messages = { create: vitest_1.vi.fn(), stream: vitest_1.vi.fn() };
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
vitest_1.vi.mock('@google/genai', () => ({
|
|
35
|
+
GoogleGenAI: class MockGoogleGenAI {
|
|
36
|
+
constructor(_opts) {
|
|
37
|
+
this.models = {
|
|
38
|
+
generateContent: vitest_1.vi.fn(),
|
|
39
|
+
generateContentStream: vitest_1.vi.fn(),
|
|
40
|
+
generateImages: vitest_1.vi.fn(),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
Content: class {
|
|
45
|
+
},
|
|
46
|
+
Tool: class {
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
const ai_client_1 = require("../ai-client");
|
|
50
|
+
const messages = [{ role: 'user', content: 'hello' }];
|
|
51
|
+
function asAsyncIterable(chunks) {
|
|
52
|
+
return {
|
|
53
|
+
[Symbol.asyncIterator]: async function* () {
|
|
54
|
+
for (const c of chunks)
|
|
55
|
+
yield c;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
(0, vitest_1.beforeEach)(() => {
|
|
60
|
+
mocks.openaiCreate.mockReset();
|
|
61
|
+
mocks.openaiCtor.mockReset();
|
|
62
|
+
});
|
|
63
|
+
(0, vitest_1.describe)('NemotronAdapter', () => {
|
|
64
|
+
(0, vitest_1.it)('targets the public NVIDIA NIM endpoint by default', () => {
|
|
65
|
+
new ai_client_1.AIClient('nemotron', 'nvapi-test');
|
|
66
|
+
(0, vitest_1.expect)(mocks.openaiCtor).toHaveBeenCalledTimes(1);
|
|
67
|
+
const opts = mocks.openaiCtor.mock.calls[0][0];
|
|
68
|
+
(0, vitest_1.expect)(opts).toMatchObject({
|
|
69
|
+
apiKey: 'nvapi-test',
|
|
70
|
+
baseURL: 'https://integrate.api.nvidia.com/v1',
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
(0, vitest_1.it)('honours a custom NEMOTRON_BASE_URL for self-hosted NIM', () => {
|
|
74
|
+
new ai_client_1.AIClient('nemotron', 'nvapi-test', {
|
|
75
|
+
nemotronBaseURL: 'http://my-nim:8000/v1',
|
|
76
|
+
});
|
|
77
|
+
const opts = mocks.openaiCtor.mock.calls[0][0];
|
|
78
|
+
(0, vitest_1.expect)(opts.baseURL).toBe('http://my-nim:8000/v1');
|
|
79
|
+
});
|
|
80
|
+
(0, vitest_1.it)('complete: sends the Nemotron model id and passes temperature', async () => {
|
|
81
|
+
mocks.openaiCreate.mockResolvedValueOnce({
|
|
82
|
+
choices: [{ message: { content: 'hi', tool_calls: undefined }, finish_reason: 'stop' }],
|
|
83
|
+
usage: { prompt_tokens: 4, completion_tokens: 2, total_tokens: 6 },
|
|
84
|
+
});
|
|
85
|
+
const client = new ai_client_1.AIClient('nemotron', 'nvapi-test');
|
|
86
|
+
const result = await client.complete('nvidia/nemotron-3-super-120b-a12b', messages, {
|
|
87
|
+
temperature: 0.42,
|
|
88
|
+
});
|
|
89
|
+
const body = mocks.openaiCreate.mock.calls[0][0];
|
|
90
|
+
(0, vitest_1.expect)(body).toMatchObject({
|
|
91
|
+
model: 'nvidia/nemotron-3-super-120b-a12b',
|
|
92
|
+
temperature: 0.42,
|
|
93
|
+
});
|
|
94
|
+
(0, vitest_1.expect)(result.content).toBe('hi');
|
|
95
|
+
(0, vitest_1.expect)(result.usage?.total_tokens).toBe(6);
|
|
96
|
+
});
|
|
97
|
+
(0, vitest_1.it)('streamComplete: forwards deltas and captures usage', async () => {
|
|
98
|
+
mocks.openaiCreate.mockResolvedValueOnce(asAsyncIterable([
|
|
99
|
+
{ choices: [{ delta: { content: 'he' } }] },
|
|
100
|
+
{ choices: [{ delta: { content: 'llo' } }] },
|
|
101
|
+
{
|
|
102
|
+
choices: [{ delta: {} }],
|
|
103
|
+
usage: { prompt_tokens: 3, completion_tokens: 2, total_tokens: 5 },
|
|
104
|
+
},
|
|
105
|
+
]));
|
|
106
|
+
const client = new ai_client_1.AIClient('nemotron', 'nvapi-test');
|
|
107
|
+
const received = [];
|
|
108
|
+
const { usage } = await client.streamComplete('nvidia/nemotron-3-nano-30b-a3b', messages, {}, (d) => received.push(d));
|
|
109
|
+
(0, vitest_1.expect)(received.join('')).toBe('hello');
|
|
110
|
+
(0, vitest_1.expect)(usage).toEqual({ prompt_tokens: 3, completion_tokens: 2, total_tokens: 5 });
|
|
111
|
+
const body = mocks.openaiCreate.mock.calls[0][0];
|
|
112
|
+
(0, vitest_1.expect)(body).toMatchObject({ stream: true });
|
|
113
|
+
});
|
|
114
|
+
(0, vitest_1.it)('exposes fast and smart defaults via getDefaultModel', () => {
|
|
115
|
+
(0, vitest_1.expect)((0, ai_client_1.getDefaultModel)('nemotron', 'fast')).toBe('nvidia/nemotron-3-nano-30b-a3b');
|
|
116
|
+
(0, vitest_1.expect)((0, ai_client_1.getDefaultModel)('nemotron', 'smart')).toBe('nvidia/nemotron-3-ultra-550b-a55b');
|
|
117
|
+
});
|
|
118
|
+
(0, vitest_1.it)('reports image generation as unsupported', () => {
|
|
119
|
+
const client = new ai_client_1.AIClient('nemotron', 'nvapi-test');
|
|
120
|
+
(0, vitest_1.expect)(client.supportsImageGeneration()).toBe(false);
|
|
121
|
+
(0, vitest_1.expect)((0, ai_client_1.providerSupportsImageGeneration)('nemotron')).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
(0, vitest_1.it)('generateImage rejects with an unsupported-provider error', async () => {
|
|
124
|
+
const client = new ai_client_1.AIClient('nemotron', 'nvapi-test');
|
|
125
|
+
await (0, vitest_1.expect)(client.generateImage({ prompt: 'a test image' })).rejects.toThrow(/not supported for provider "nemotron"/);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getAgentPrompt = getAgentPrompt;
|
|
4
|
+
const ai_client_1 = require("../ai-client");
|
|
4
5
|
const config_1 = require("../config");
|
|
5
6
|
// MCP server names and descriptions are user-controlled and embedded into the agent
|
|
6
7
|
// system prompt. Sanitize them to mitigate prompt-injection: strip control characters
|
|
@@ -28,7 +29,7 @@ function getAgentPrompt(platform, hasTaskInstructions, installedMcps = []) {
|
|
|
28
29
|
return `
|
|
29
30
|
You are an AI agent with the following capabilities:
|
|
30
31
|
- **Shell execution** (\`<shell_script>\` XML tag) — runs commands on the user's machine; output returns as \`TERMINAL OUTPUT:\`.
|
|
31
|
-
- **Web tools** — call \`web_search\` and \`web_fetch\` via native function calling to retrieve live information from the internet.${config_1.config.aiProvider
|
|
32
|
+
- **Web tools** — call \`web_search\` and \`web_fetch\` via native function calling to retrieve live information from the internet.${(0, ai_client_1.providerSupportsImageGeneration)(config_1.config.aiProvider) ? '\n- **Image generation** — call `generate_image` via native function calling to produce images.' : ''}${config_1.config.browserDebugPort !== undefined ? "\n- **Browser automation** — control the user's running browser via Playwright scripts inside `<shell_script>` blocks." : ''}
|
|
32
33
|
${installedMcps.length > 0 ? '- **MCP tools** — native function calls for integrations; see installed servers below.' : ''}
|
|
33
34
|
|
|
34
35
|
Use these capabilities to take real action. Default to doing rather than asking.
|
|
@@ -78,7 +79,7 @@ ${config_1.config.browserDebugPort !== undefined
|
|
|
78
79
|
- Create the directory first if needed: \`mkdir -p ~/.omnikey/garbage\`.
|
|
79
80
|
- Always tell the user the exact path where the configuration was saved in your \`<final_answer>\`.
|
|
80
81
|
|
|
81
|
-
${config_1.config.aiProvider
|
|
82
|
+
${!(0, ai_client_1.providerSupportsImageGeneration)(config_1.config.aiProvider)
|
|
82
83
|
? `**Image generation:**
|
|
83
84
|
- No image-generation tool is available in this environment. Do **not** call any tool whose name suggests image, picture, render, draw, or visual asset creation (e.g., \`generate_image\`, \`image_generate\`, \`create_image\`). If the user asks for an image, respond in \`<final_answer>\` explaining that image generation is not supported with the current provider.
|
|
84
85
|
`
|
|
@@ -122,7 +123,7 @@ ${installedMcps
|
|
|
122
123
|
|
|
123
124
|
**Response format — every response must be exactly one of:**
|
|
124
125
|
1. \`<shell_script>...</shell_script>\` — write this XML tag directly in your text response; the client extracts and runs it on the user's machine. Never generate a script as an internal tool call or function call. Always use the \`<shell_script>\` tag for scripts — do NOT wrap them in any other tags or envelopes. The script must be the entire content of your response, with no extra text before or after.
|
|
125
|
-
2. ${config_1.config.aiProvider
|
|
126
|
+
2. ${(0, ai_client_1.providerSupportsImageGeneration)(config_1.config.aiProvider) ? 'A `web_search`, `web_fetch`, or `generate_image`' : 'A `web_search` or `web_fetch`'} **native function call** — use the function-calling API for these only; do NOT wrap them in XML tags.${installedMcps.length > 0 ? ' Same for MCP tools (`mcp_<server>__<tool>`).' : ''}
|
|
126
127
|
3. \`<final_answer>...</final_answer>\` — your conclusion once you have enough information.
|
|
127
128
|
|
|
128
129
|
**Critical rule — zero tolerance for text outside tags or extra wrappers:**
|
|
@@ -16,17 +16,18 @@ const imageTool_1 = require("./imageTool");
|
|
|
16
16
|
* `web_search` is always included because DuckDuckGo is used as a free
|
|
17
17
|
* fallback when no third-party search key is configured.
|
|
18
18
|
*
|
|
19
|
-
* `generate_image` is omitted for
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* `generate_image` is omitted for providers without image-generation
|
|
20
|
+
* support (currently Anthropic and Nemotron) because the underlying
|
|
21
|
+
* `aiClient.generateImage()` only supports OpenAI and Gemini — registering
|
|
22
|
+
* an unsupported tool would invite the model to call it and fail at
|
|
23
|
+
* execution time. The system prompt for those providers is built without
|
|
23
24
|
* the image-tool section to match this tool set.
|
|
24
25
|
*
|
|
25
26
|
* @returns An array of `AITool` definitions ready to pass to the AI client.
|
|
26
27
|
*/
|
|
27
28
|
function buildAvailableTools(extraTools = []) {
|
|
28
29
|
const baseTools = [web_search_provider_1.WEB_FETCH_TOOL, web_search_provider_1.WEB_SEARCH_TOOL];
|
|
29
|
-
if (config_1.config.aiProvider
|
|
30
|
+
if ((0, ai_client_1.providerSupportsImageGeneration)(config_1.config.aiProvider)) {
|
|
30
31
|
baseTools.push(imageTool_1.IMAGE_GENERATE_TOOL);
|
|
31
32
|
}
|
|
32
33
|
return [...baseTools, ...extraTools];
|
|
@@ -9,6 +9,7 @@ exports.modelSupportsTemperature = modelSupportsTemperature;
|
|
|
9
9
|
exports.getMaxMessageContentLength = getMaxMessageContentLength;
|
|
10
10
|
exports.getMaxHistoryLength = getMaxHistoryLength;
|
|
11
11
|
exports.getContextWindowSize = getContextWindowSize;
|
|
12
|
+
exports.providerSupportsImageGeneration = providerSupportsImageGeneration;
|
|
12
13
|
const openai_1 = __importDefault(require("openai"));
|
|
13
14
|
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
14
15
|
const genai_1 = require("@google/genai");
|
|
@@ -27,6 +28,17 @@ const DEFAULT_MODELS = {
|
|
|
27
28
|
openai: { fast: 'gpt-4o-mini', smart: 'gpt-5.5' },
|
|
28
29
|
gemini: { fast: 'gemini-2.5-flash', smart: 'gemini-2.5-pro' },
|
|
29
30
|
anthropic: { fast: 'claude-haiku-4-5-20251001', smart: 'claude-opus-4-7' },
|
|
31
|
+
// NVIDIA Nemotron is exposed through the OpenAI-compatible NIM endpoint at
|
|
32
|
+
// https://integrate.api.nvidia.com/v1. The "fast" tier maps to Nemotron Nano
|
|
33
|
+
// for high-throughput sub-agent workloads; the "smart" tier maps to Nemotron
|
|
34
|
+
// Ultra — the frontier-level model in the family — for complex multi-agent
|
|
35
|
+
// reasoning, planning, code generation, and deep research. Drop down to
|
|
36
|
+
// Nemotron Super (`nvidia/nemotron-3-super-120b-a12b`) here if single-GPU
|
|
37
|
+
// data-center deployment is required.
|
|
38
|
+
nemotron: {
|
|
39
|
+
fast: 'nvidia/nemotron-3-nano-30b-a3b',
|
|
40
|
+
smart: 'nvidia/nemotron-3-ultra-550b-a55b',
|
|
41
|
+
},
|
|
30
42
|
};
|
|
31
43
|
function getDefaultModel(provider, tier) {
|
|
32
44
|
return DEFAULT_MODELS[provider][tier];
|
|
@@ -78,6 +90,9 @@ const MAX_MESSAGE_CONTENT_LENGTH_BY_PROVIDER = {
|
|
|
78
90
|
anthropic: 10000000,
|
|
79
91
|
openai: 800000,
|
|
80
92
|
gemini: 3500000,
|
|
93
|
+
// Nemotron 3 ships a 1M-token context window via NIM; mirror Gemini's
|
|
94
|
+
// per-string cap (no documented hard limit, bounded by the context window).
|
|
95
|
+
nemotron: 3500000,
|
|
81
96
|
};
|
|
82
97
|
/**
|
|
83
98
|
* Maximum total character length across all messages in the conversation
|
|
@@ -95,6 +110,8 @@ const MAX_HISTORY_LENGTH_BY_PROVIDER = {
|
|
|
95
110
|
anthropic: 1800000,
|
|
96
111
|
openai: 460000,
|
|
97
112
|
gemini: 1800000,
|
|
113
|
+
// 1M-token context with 100K reserved for output → 900K target × 2 chars
|
|
114
|
+
nemotron: 1800000,
|
|
98
115
|
};
|
|
99
116
|
/**
|
|
100
117
|
* Hard token limit of the context window for each provider/model tier.
|
|
@@ -104,6 +121,8 @@ const CONTEXT_WINDOW_BY_PROVIDER = {
|
|
|
104
121
|
anthropic: 1000000,
|
|
105
122
|
openai: 272000,
|
|
106
123
|
gemini: 1000000,
|
|
124
|
+
// Nemotron 3 hybrid Mamba-Transformer MoE family ships with 1M-token context.
|
|
125
|
+
nemotron: 1000000,
|
|
107
126
|
};
|
|
108
127
|
function getMaxMessageContentLength(provider) {
|
|
109
128
|
return MAX_MESSAGE_CONTENT_LENGTH_BY_PROVIDER[provider];
|
|
@@ -165,9 +184,7 @@ class OpenAIAdapter {
|
|
|
165
184
|
const stream = await this.client.chat.completions.create({
|
|
166
185
|
model,
|
|
167
186
|
messages: oaiMessages,
|
|
168
|
-
...(modelSupportsTemperature(model)
|
|
169
|
-
? { temperature: options.temperature ?? 0.3 }
|
|
170
|
-
: {}),
|
|
187
|
+
...(modelSupportsTemperature(model) ? { temperature: options.temperature ?? 0.3 } : {}),
|
|
171
188
|
stream: true,
|
|
172
189
|
stream_options: { include_usage: true },
|
|
173
190
|
});
|
|
@@ -277,9 +294,7 @@ class AnthropicAdapter {
|
|
|
277
294
|
max_tokens: options.maxTokens ?? 8192,
|
|
278
295
|
...(system ? { system } : {}),
|
|
279
296
|
messages: anthropicMessages,
|
|
280
|
-
...(modelSupportsTemperature(model)
|
|
281
|
-
? { temperature: options.temperature ?? 0.3 }
|
|
282
|
-
: {}),
|
|
297
|
+
...(modelSupportsTemperature(model) ? { temperature: options.temperature ?? 0.3 } : {}),
|
|
283
298
|
});
|
|
284
299
|
for await (const event of stream) {
|
|
285
300
|
if (event.type === 'content_block_delta' &&
|
|
@@ -298,6 +313,96 @@ class AnthropicAdapter {
|
|
|
298
313
|
}
|
|
299
314
|
}
|
|
300
315
|
// ---------------------------------------------------------------------------
|
|
316
|
+
// Nemotron adapter (NVIDIA NIM — OpenAI-compatible REST API)
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
/**
|
|
319
|
+
* NVIDIA Nemotron models are served behind an OpenAI-compatible endpoint at
|
|
320
|
+
* `https://integrate.api.nvidia.com/v1` (the NVIDIA NIM gateway, also used by
|
|
321
|
+
* self-hosted NIM microservices). Because the wire protocol matches OpenAI's
|
|
322
|
+
* Chat Completions API, we reuse the `openai` SDK by constructing a client
|
|
323
|
+
* with a custom `baseURL`. This keeps the message/tool-call conversion logic
|
|
324
|
+
* identical to OpenAI and avoids pulling in another transport library.
|
|
325
|
+
*
|
|
326
|
+
* Notes on Nemotron-specific quirks:
|
|
327
|
+
* - The endpoint accepts `temperature`, `top_p`, `max_tokens`, and `tools`
|
|
328
|
+
* in the standard OpenAI shape, so no schema translation is needed.
|
|
329
|
+
* - Image generation is not exposed for the text-only Nemotron models, so
|
|
330
|
+
* `generateImage` is intentionally not implemented (the unified `AIClient`
|
|
331
|
+
* surfaces a clear error for unsupported providers).
|
|
332
|
+
* - Self-hosted NIM deployments can be targeted by setting the
|
|
333
|
+
* `NEMOTRON_BASE_URL` env var (handled in `config.ts`). The API key can be
|
|
334
|
+
* any non-empty string for self-hosted NIM.
|
|
335
|
+
*/
|
|
336
|
+
class NemotronAdapter {
|
|
337
|
+
constructor(apiKey, baseURL) {
|
|
338
|
+
this.client = new openai_1.default({ apiKey, baseURL });
|
|
339
|
+
}
|
|
340
|
+
async complete(model, messages, options) {
|
|
341
|
+
const oaiMessages = toOpenAIMessages(messages);
|
|
342
|
+
const tools = options.tools?.length ? toOpenAITools(options.tools) : undefined;
|
|
343
|
+
const completion = await this.client.chat.completions.create({
|
|
344
|
+
model,
|
|
345
|
+
messages: oaiMessages,
|
|
346
|
+
tools: tools?.length ? tools : undefined,
|
|
347
|
+
...(modelSupportsTemperature(model) ? { temperature: options.temperature ?? 0.2 } : {}),
|
|
348
|
+
max_tokens: options.maxTokens,
|
|
349
|
+
});
|
|
350
|
+
const choice = completion.choices[0];
|
|
351
|
+
const msg = choice.message;
|
|
352
|
+
const content = (msg.content ?? '').toString().trim();
|
|
353
|
+
const tool_calls = msg.tool_calls
|
|
354
|
+
?.filter((tc) => tc.type === 'function' && 'function' in tc)
|
|
355
|
+
.map((tc) => ({
|
|
356
|
+
id: tc.id,
|
|
357
|
+
name: tc.function.name,
|
|
358
|
+
arguments: JSON.parse(tc.function.arguments || '{}'),
|
|
359
|
+
}));
|
|
360
|
+
const finishReason = choice.finish_reason === 'tool_calls'
|
|
361
|
+
? 'tool_calls'
|
|
362
|
+
: choice.finish_reason === 'length'
|
|
363
|
+
? 'length'
|
|
364
|
+
: 'stop';
|
|
365
|
+
const usage = completion.usage
|
|
366
|
+
? {
|
|
367
|
+
prompt_tokens: completion.usage.prompt_tokens,
|
|
368
|
+
completion_tokens: completion.usage.completion_tokens,
|
|
369
|
+
total_tokens: completion.usage.total_tokens,
|
|
370
|
+
}
|
|
371
|
+
: undefined;
|
|
372
|
+
const assistantMessage = {
|
|
373
|
+
role: 'assistant',
|
|
374
|
+
content,
|
|
375
|
+
...(tool_calls?.length ? { tool_calls } : {}),
|
|
376
|
+
};
|
|
377
|
+
return { content, finish_reason: finishReason, tool_calls, usage, model, assistantMessage };
|
|
378
|
+
}
|
|
379
|
+
async streamComplete(model, messages, options, onDelta) {
|
|
380
|
+
const oaiMessages = toOpenAIMessages(messages);
|
|
381
|
+
const stream = await this.client.chat.completions.create({
|
|
382
|
+
model,
|
|
383
|
+
messages: oaiMessages,
|
|
384
|
+
...(modelSupportsTemperature(model) ? { temperature: options.temperature ?? 0.3 } : {}),
|
|
385
|
+
stream: true,
|
|
386
|
+
stream_options: { include_usage: true },
|
|
387
|
+
});
|
|
388
|
+
let usage;
|
|
389
|
+
for await (const part of stream) {
|
|
390
|
+
const delta = part.choices?.[0]?.delta?.content ?? '';
|
|
391
|
+
if (delta) {
|
|
392
|
+
onDelta(delta);
|
|
393
|
+
}
|
|
394
|
+
if (part.usage) {
|
|
395
|
+
usage = {
|
|
396
|
+
prompt_tokens: part.usage.prompt_tokens ?? 0,
|
|
397
|
+
completion_tokens: part.usage.completion_tokens ?? 0,
|
|
398
|
+
total_tokens: part.usage.total_tokens ?? 0,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return { usage, model };
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
301
406
|
// Gemini adapter
|
|
302
407
|
// ---------------------------------------------------------------------------
|
|
303
408
|
class GeminiAdapter {
|
|
@@ -313,9 +418,7 @@ class GeminiAdapter {
|
|
|
313
418
|
config: {
|
|
314
419
|
...(systemInstruction ? { systemInstruction } : {}),
|
|
315
420
|
...(tools?.length ? { tools } : {}),
|
|
316
|
-
...(modelSupportsTemperature(model)
|
|
317
|
-
? { temperature: options.temperature ?? 0.2 }
|
|
318
|
-
: {}),
|
|
421
|
+
...(modelSupportsTemperature(model) ? { temperature: options.temperature ?? 0.2 } : {}),
|
|
319
422
|
},
|
|
320
423
|
});
|
|
321
424
|
const candidate = response.candidates?.[0];
|
|
@@ -366,9 +469,7 @@ class GeminiAdapter {
|
|
|
366
469
|
contents,
|
|
367
470
|
config: {
|
|
368
471
|
...(systemInstruction ? { systemInstruction } : {}),
|
|
369
|
-
...(modelSupportsTemperature(model)
|
|
370
|
-
? { temperature: options.temperature ?? 0.3 }
|
|
371
|
-
: {}),
|
|
472
|
+
...(modelSupportsTemperature(model) ? { temperature: options.temperature ?? 0.3 } : {}),
|
|
372
473
|
},
|
|
373
474
|
});
|
|
374
475
|
let usage;
|
|
@@ -427,7 +528,7 @@ class GeminiAdapter {
|
|
|
427
528
|
// Main AIClient
|
|
428
529
|
// ---------------------------------------------------------------------------
|
|
429
530
|
class AIClient {
|
|
430
|
-
constructor(provider, apiKey) {
|
|
531
|
+
constructor(provider, apiKey, options = {}) {
|
|
431
532
|
this.provider = provider;
|
|
432
533
|
if (provider === 'openai') {
|
|
433
534
|
this.openai = new OpenAIAdapter(apiKey);
|
|
@@ -438,6 +539,12 @@ class AIClient {
|
|
|
438
539
|
else if (provider === 'gemini') {
|
|
439
540
|
this.gemini = new GeminiAdapter(apiKey);
|
|
440
541
|
}
|
|
542
|
+
else if (provider === 'nemotron') {
|
|
543
|
+
// Default to the public NVIDIA NIM gateway. Self-hosted NIM deployments
|
|
544
|
+
// can override this via `NEMOTRON_BASE_URL` (see config.ts).
|
|
545
|
+
const baseURL = options.nemotronBaseURL || 'https://integrate.api.nvidia.com/v1';
|
|
546
|
+
this.nemotron = new NemotronAdapter(apiKey, baseURL);
|
|
547
|
+
}
|
|
441
548
|
}
|
|
442
549
|
getProvider() {
|
|
443
550
|
return this.provider;
|
|
@@ -452,6 +559,9 @@ class AIClient {
|
|
|
452
559
|
if (this.provider === 'gemini' && this.gemini) {
|
|
453
560
|
return this.gemini.complete(model, messages, options);
|
|
454
561
|
}
|
|
562
|
+
if (this.provider === 'nemotron' && this.nemotron) {
|
|
563
|
+
return this.nemotron.complete(model, messages, options);
|
|
564
|
+
}
|
|
455
565
|
throw new Error(`AI provider "${this.provider}" is not configured.`);
|
|
456
566
|
}
|
|
457
567
|
async streamComplete(model, messages, options = {}, onDelta) {
|
|
@@ -464,13 +574,27 @@ class AIClient {
|
|
|
464
574
|
if (this.provider === 'gemini' && this.gemini) {
|
|
465
575
|
return this.gemini.streamComplete(model, messages, options, onDelta);
|
|
466
576
|
}
|
|
577
|
+
if (this.provider === 'nemotron' && this.nemotron) {
|
|
578
|
+
return this.nemotron.streamComplete(model, messages, options, onDelta);
|
|
579
|
+
}
|
|
467
580
|
throw new Error(`AI provider "${this.provider}" is not configured.`);
|
|
468
581
|
}
|
|
582
|
+
/**
|
|
583
|
+
* Reports whether the configured provider can generate images.
|
|
584
|
+
*
|
|
585
|
+
* Centralising this check means agent tool registration and system-prompt
|
|
586
|
+
* builders no longer need to keep a hand-maintained allow/deny list of
|
|
587
|
+
* providers — they ask the client directly. Currently OpenAI and Gemini
|
|
588
|
+
* support image generation; Anthropic and Nemotron (text-only) do not.
|
|
589
|
+
*/
|
|
590
|
+
supportsImageGeneration() {
|
|
591
|
+
return this.provider === 'openai' || this.provider === 'gemini';
|
|
592
|
+
}
|
|
469
593
|
/**
|
|
470
594
|
* Generates an image with the currently configured provider.
|
|
471
595
|
*
|
|
472
|
-
* Supported providers are OpenAI and Gemini. Anthropic
|
|
473
|
-
* expose a text-to-image generation endpoint in this project.
|
|
596
|
+
* Supported providers are OpenAI and Gemini. Anthropic and Nemotron do not
|
|
597
|
+
* currently expose a text-to-image generation endpoint in this project.
|
|
474
598
|
*
|
|
475
599
|
* @param options - Unified image-generation options.
|
|
476
600
|
* @returns Provider-normalized image payload.
|
|
@@ -486,6 +610,15 @@ class AIClient {
|
|
|
486
610
|
}
|
|
487
611
|
}
|
|
488
612
|
exports.AIClient = AIClient;
|
|
613
|
+
/**
|
|
614
|
+
* Returns whether the given provider supports image generation.
|
|
615
|
+
*
|
|
616
|
+
* Module-level helper for callers that only have the provider id at hand
|
|
617
|
+
* (e.g. system-prompt builders) and don't want to construct an `AIClient`.
|
|
618
|
+
*/
|
|
619
|
+
function providerSupportsImageGeneration(provider) {
|
|
620
|
+
return provider === 'openai' || provider === 'gemini';
|
|
621
|
+
}
|
|
489
622
|
// ---------------------------------------------------------------------------
|
|
490
623
|
// Message format converters — OpenAI
|
|
491
624
|
// ---------------------------------------------------------------------------
|
|
@@ -646,4 +779,6 @@ function toGeminiTools(tools) {
|
|
|
646
779
|
// ---------------------------------------------------------------------------
|
|
647
780
|
// Shared singleton — import this instead of constructing a new AIClient
|
|
648
781
|
// ---------------------------------------------------------------------------
|
|
649
|
-
exports.aiClient = new AIClient(config_1.config.aiProvider, config_1.config.aiApiKey
|
|
782
|
+
exports.aiClient = new AIClient(config_1.config.aiProvider, config_1.config.aiApiKey, {
|
|
783
|
+
nemotronBaseURL: config_1.config.nemotronBaseUrl,
|
|
784
|
+
});
|