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.
@@ -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 !== 'anthropic' ? '\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
+ - **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 === 'anthropic'
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 === 'anthropic' ? 'A `web_search` or `web_fetch`' : 'A `web_search`, `web_fetch`, or `generate_image`'} **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
+ 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 the Anthropic provider because the
20
- * underlying `aiClient.generateImage()` only supports OpenAI and Gemini
21
- * registering an unsupported tool would invite the model to call it and
22
- * fail at execution time. The system prompt for Anthropic is built without
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 !== 'anthropic') {
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 does not currently
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
+ });