From ac4778bb5c956e6d8e98d347d1e13bcb6ea6d4cd Mon Sep 17 00:00:00 2001 From: Stephen Wright Date: Thu, 4 Jun 2026 15:30:22 +0100 Subject: [PATCH] feat(NVIDIA Nemotron Chat Model Node): Restrict model selector to supported models (#31698) --- .../llms/LmChatNvidia/LmChatNvidia.node.ts | 20 +++++++---- .../LmChatNvidia/test/LmChatNvidia.test.ts | 35 +++++++++++++++---- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatNvidia/LmChatNvidia.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatNvidia/LmChatNvidia.node.ts index c4c8d674b25..760a060e2a4 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatNvidia/LmChatNvidia.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatNvidia/LmChatNvidia.node.ts @@ -16,12 +16,18 @@ import { import type { OpenAICompatibleCredential } from '../../../types/types'; import { openAiFailedAttemptHandler } from '../../vendors/OpenAi/helpers/error-handling'; -const NEMOTRON_FALLBACK_MODELS = [ - 'nvidia/llama-3.3-nemotron-super-49b-v1', - 'nvidia/llama-3.1-nemotron-70b-instruct', +// Allow-list of supported Nemotron chat models. The selector fetches the live +// catalog from the API and reduces it to these ids; if the fetch fails, this +// same list is shown as the static fallback. +const NEMOTRON_SUPPORTED_MODELS = [ 'nvidia/llama-3.1-nemotron-nano-8b-v1', - 'nvidia/nemotron-4-340b-instruct', - 'nvidia/nemotron-mini-4b-instruct', + 'nvidia/llama-3.3-nemotron-super-49b-v1', + 'nvidia/llama-3.3-nemotron-super-49b-v1.5', + 'nvidia/nemotron-3-nano-30b-a3b', + 'nvidia/nemotron-3-nano-omni-30b-a3b-reasoning', + 'nvidia/nemotron-3-super-120b-a12b', + 'nvidia/nemotron-nano-12b-v2-vl', + 'nvidia/nvidia-nemotron-nano-9b-v2', ]; export class LmChatNvidia implements INodeType { @@ -103,7 +109,7 @@ export class LmChatNvidia implements INodeType { { type: 'filter', properties: { - pass: '={{ /nemotron/i.test($responseItem.id) }}', + pass: `={{ ${JSON.stringify(NEMOTRON_SUPPORTED_MODELS)}.includes($responseItem.id) }}`, }, }, { @@ -131,7 +137,7 @@ export class LmChatNvidia implements INodeType { }, }, default: 'nvidia/llama-3.3-nemotron-super-49b-v1', - options: NEMOTRON_FALLBACK_MODELS.map((id) => ({ name: id, value: id })), + options: NEMOTRON_SUPPORTED_MODELS.map((id) => ({ name: id, value: id })), }, { displayName: 'Options', diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatNvidia/test/LmChatNvidia.test.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatNvidia/test/LmChatNvidia.test.ts index 0705d67588a..6764515d0e2 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatNvidia/test/LmChatNvidia.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatNvidia/test/LmChatNvidia.test.ts @@ -46,7 +46,7 @@ describe('LmChatNvidia', () => { }); ctx.getNode = vi.fn().mockReturnValue(nodeDef); ctx.getNodeParameter = vi.fn().mockImplementation((paramName: string) => { - if (paramName === 'model') return 'nvidia/llama-3.1-nemotron-70b-instruct'; + if (paramName === 'model') return 'nvidia/llama-3.3-nemotron-super-49b-v1'; if (paramName === 'options') return {}; return undefined; }); @@ -80,13 +80,36 @@ describe('LmChatNvidia', () => { expect(node.description.outputNames).toEqual(['Model']); }); - it('should filter to Nemotron models in loadOptions', () => { + it('should reduce the fetched catalog to the supported allow-list in loadOptions', () => { const modelProp = node.description.properties.find((p) => p?.name === 'model'); expect(modelProp).toBeDefined(); const postReceive = (modelProp?.typeOptions as any)?.loadOptions?.routing?.output ?.postReceive as Array<{ type: string; properties: { pass?: string } }>; const filterStep = postReceive.find((step) => step.type === 'filter'); - expect(filterStep?.properties.pass).toMatch(/nemotron/i); + const pass = filterStep?.properties.pass ?? ''; + + // The filter keeps only ids present in the allow-list (built from the same + // constant that backs the static options), excluding e.g. reward models. + const supported = ((modelProp as any)?.options as Array<{ value: string }>).map( + (o) => o.value, + ); + expect(pass).toContain('.includes($responseItem.id)'); + for (const id of supported) { + expect(pass).toContain(id); + } + expect(pass).not.toContain('nemotron-70b-reward'); + }); + + it('should expose the supported models as static fallback options', () => { + const modelProp = node.description.properties.find((p) => p?.name === 'model'); + const options = (modelProp as any)?.options as Array<{ name: string; value: string }>; + const values = options.map((o) => o.value); + + expect(values).toContain('nvidia/llama-3.3-nemotron-super-49b-v1'); + expect(values).toContain('nvidia/nemotron-3-super-120b-a12b'); + expect(values).not.toContain('nvidia/llama-3.1-nemotron-70b-reward'); + // The default must be one of the offered options. + expect(values).toContain((modelProp as any)?.default); }); }); @@ -100,7 +123,7 @@ describe('LmChatNvidia', () => { expect(MockedChatOpenAI).toHaveBeenCalledWith( expect.objectContaining({ apiKey: 'test-key', - model: 'nvidia/llama-3.1-nemotron-70b-instruct', + model: 'nvidia/llama-3.3-nemotron-super-49b-v1', configuration: expect.objectContaining({ baseURL: 'https://integrate.api.nvidia.com/v1', }), @@ -138,7 +161,7 @@ describe('LmChatNvidia', () => { it('should pass options through to ChatOpenAI', async () => { const ctx = setupMockContext(); ctx.getNodeParameter = vi.fn().mockImplementation((paramName: string) => { - if (paramName === 'model') return 'nvidia/llama-3.1-nemotron-70b-instruct'; + if (paramName === 'model') return 'nvidia/llama-3.3-nemotron-super-49b-v1'; if (paramName === 'options') return { temperature: 0.5, @@ -170,7 +193,7 @@ describe('LmChatNvidia', () => { it('should set response_format in modelKwargs when responseFormat is provided', async () => { const ctx = setupMockContext(); ctx.getNodeParameter = vi.fn().mockImplementation((paramName: string) => { - if (paramName === 'model') return 'nvidia/llama-3.1-nemotron-70b-instruct'; + if (paramName === 'model') return 'nvidia/llama-3.3-nemotron-super-49b-v1'; if (paramName === 'options') return { responseFormat: 'json_object' }; return undefined; });