feat(core): Add NVIDIA Nemotron Models with cloud and self-hosted NIM support (#29618)

This commit is contained in:
Claire
2026-05-15 13:30:26 +01:00
committed by GitHub
parent 148bc89be9
commit e7df34ea74
14 changed files with 625 additions and 1 deletions
+12 -1
View File
@@ -32,6 +32,7 @@ export const chatHubLLMProviderSchema = z.enum([
'deepSeek',
'cohere',
'mistralCloud',
'nvidia',
]);
export type ChatHubLLMProvider = z.infer<typeof chatHubLLMProviderSchema>;
@@ -97,6 +98,7 @@ export const PROVIDER_CREDENTIAL_TYPE_MAP: Record<ChatHubLLMProvider, string> =
deepSeek: 'deepSeekApi',
cohere: 'cohereApi',
mistralCloud: 'mistralCloudApi',
nvidia: 'nvidiaApi',
};
export const VECTOR_STORE_PROVIDER_CREDENTIAL_TYPE_MAP: Record<ChatHubVectorStoreProvider, string> =
@@ -179,6 +181,11 @@ const mistralCloudModelSchema = z.object({
model: z.string(),
});
const nvidiaModelSchema = z.object({
provider: z.literal('nvidia'),
model: z.string(),
});
const n8nModelSchema = z.object({
provider: z.literal('n8n'),
workflowId: z.string(),
@@ -204,6 +211,7 @@ export const chatHubConversationModelSchema = z.discriminatedUnion('provider', [
deepSeekModelSchema,
cohereModelSchema,
mistralCloudModelSchema,
nvidiaModelSchema,
n8nModelSchema,
chatAgentSchema,
]);
@@ -222,6 +230,7 @@ export type ChatHubOpenRouterModel = z.infer<typeof openRouterModelSchema>;
export type ChatHubDeepSeekModel = z.infer<typeof deepSeekModelSchema>;
export type ChatHubCohereModel = z.infer<typeof cohereModelSchema>;
export type ChatHubMistralCloudModel = z.infer<typeof mistralCloudModelSchema>;
export type ChatHubNvidiaModel = z.infer<typeof nvidiaModelSchema>;
export type ChatHubBaseLLMModel =
| ChatHubOpenAIModel
| ChatHubAnthropicModel
@@ -236,7 +245,8 @@ export type ChatHubBaseLLMModel =
| ChatHubOpenRouterModel
| ChatHubDeepSeekModel
| ChatHubCohereModel
| ChatHubMistralCloudModel;
| ChatHubMistralCloudModel
| ChatHubNvidiaModel;
export type ChatHubN8nModel = z.infer<typeof n8nModelSchema>;
export type ChatHubCustomAgentModel = z.infer<typeof chatAgentSchema>;
@@ -302,6 +312,7 @@ export const emptyChatModelsResponse: ChatModelsResponse = {
deepSeek: { models: [] },
cohere: { models: [] },
mistralCloud: { models: [] },
nvidia: { models: [] },
n8n: { models: [] },
// eslint-disable-next-line @typescript-eslint/naming-convention
'custom-agent': { models: [] },
@@ -0,0 +1,60 @@
import type {
ICredentialDataDecryptedObject,
ICredentialTestRequest,
ICredentialType,
IHttpRequestOptions,
INodeProperties,
} from 'n8n-workflow';
export class NvidiaApi implements ICredentialType {
name = 'nvidiaApi';
displayName = 'NVIDIA Nemotron';
documentationUrl = 'nvidia';
properties: INodeProperties[] = [
{
displayName: 'Base URL',
name: 'url',
type: 'string',
required: true,
default: 'https://integrate.api.nvidia.com/v1',
description:
'Use the default for build.nvidia.com cloud, or change it to point at a self-hosted NIM container (e.g. http://localhost:8000/v1)',
},
{
displayName: 'API Key',
name: 'apiKey',
type: 'string',
typeOptions: { password: true },
required: false,
default: '',
description:
'Required for build.nvidia.com cloud. Leave blank for a self-hosted NIM that does not require authentication',
},
];
authenticate = async (
credentials: ICredentialDataDecryptedObject,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> => {
if (!credentials.apiKey) {
return requestOptions;
}
return {
...requestOptions,
headers: {
...requestOptions.headers,
Authorization: `Bearer ${credentials.apiKey as string}`,
},
};
};
test: ICredentialTestRequest = {
request: {
baseURL: '={{ $credentials.url }}',
url: '/models',
},
};
}
@@ -0,0 +1,273 @@
import { ChatOpenAI, type ClientOptions } from '@langchain/openai';
import {
getProxyAgent,
makeN8nLlmFailedAttemptHandler,
N8nLlmTracing,
getConnectionHintNoticeField,
} from '@n8n/ai-utilities';
import {
NodeConnectionTypes,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} from 'n8n-workflow';
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',
'nvidia/llama-3.1-nemotron-nano-8b-v1',
'nvidia/nemotron-4-340b-instruct',
'nvidia/nemotron-mini-4b-instruct',
];
export class LmChatNvidia implements INodeType {
description: INodeTypeDescription = {
displayName: 'NVIDIA Nemotron Chat Model',
name: 'lmChatNvidia',
icon: { light: 'file:nvidia.svg', dark: 'file:nvidia.dark.svg' },
group: ['transform'],
version: [1],
description: 'NVIDIA Nemotron models from build.nvidia.com or self-hosted NIM',
defaults: {
name: 'NVIDIA Nemotron Chat Model',
},
codex: {
categories: ['AI'],
subcategories: {
AI: ['Language Models', 'Root Nodes'],
'Language Models': ['Chat Models (Recommended)'],
},
resources: {
primaryDocumentation: [
{
url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatnvidia/',
},
],
},
},
inputs: [],
outputs: [NodeConnectionTypes.AiLanguageModel],
outputNames: ['Model'],
credentials: [
{
name: 'nvidiaApi',
required: true,
},
],
requestDefaults: {
ignoreHttpStatusErrors: true,
baseURL: '={{ $credentials?.url }}',
},
properties: [
getConnectionHintNoticeField([NodeConnectionTypes.AiChain, NodeConnectionTypes.AiAgent]),
{
displayName:
'If using JSON response format, you must include word "json" in the prompt in your chain or agent.',
name: 'notice',
type: 'notice',
default: '',
displayOptions: {
show: {
'/options.responseFormat': ['json_object'],
},
},
},
{
displayName: 'Model',
name: 'model',
type: 'options',
description:
'The Nemotron model which will generate the completion. <a href="https://build.nvidia.com/models">Learn more</a>.',
typeOptions: {
loadOptions: {
routing: {
request: {
method: 'GET',
url: '/models',
},
output: {
postReceive: [
{
type: 'rootProperty',
properties: {
property: 'data',
},
},
{
type: 'filter',
properties: {
pass: '={{ /nemotron/i.test($responseItem.id) }}',
},
},
{
type: 'setKeyValue',
properties: {
name: '={{$responseItem.id}}',
value: '={{$responseItem.id}}',
},
},
{
type: 'sort',
properties: {
key: 'name',
},
},
],
},
},
},
},
routing: {
send: {
type: 'body',
property: 'model',
},
},
default: 'nvidia/llama-3.3-nemotron-super-49b-v1',
options: NEMOTRON_FALLBACK_MODELS.map((id) => ({ name: id, value: id })),
},
{
displayName: 'Options',
name: 'options',
placeholder: 'Add Option',
description: 'Additional options to add',
type: 'collection',
default: {},
options: [
{
displayName: 'Frequency Penalty',
name: 'frequencyPenalty',
default: 0,
typeOptions: { maxValue: 2, minValue: -2, numberPrecision: 1 },
description:
"Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim",
type: 'number',
},
{
displayName: 'Maximum Number of Tokens',
name: 'maxTokens',
default: -1,
description:
'The maximum number of tokens to generate in the completion. Use -1 for the model default.',
type: 'number',
},
{
displayName: 'Response Format',
name: 'responseFormat',
default: 'text',
type: 'options',
options: [
{
name: 'Text',
value: 'text',
description: 'Regular text response',
},
{
name: 'JSON',
value: 'json_object',
description:
'Enables JSON mode, which should guarantee the message the model generates is valid JSON',
},
],
},
{
displayName: 'Presence Penalty',
name: 'presencePenalty',
default: 0,
typeOptions: { maxValue: 2, minValue: -2, numberPrecision: 1 },
description:
"Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics",
type: 'number',
},
{
displayName: 'Sampling Temperature',
name: 'temperature',
default: 0.7,
typeOptions: { maxValue: 2, minValue: 0, numberPrecision: 1 },
description:
'Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive.',
type: 'number',
},
{
displayName: 'Timeout',
name: 'timeout',
default: 360000,
description: 'Maximum amount of time a request is allowed to take in milliseconds',
type: 'number',
},
{
displayName: 'Max Retries',
name: 'maxRetries',
default: 2,
description: 'Maximum number of retries to attempt',
type: 'number',
},
{
displayName: 'Top P',
name: 'topP',
default: 1,
typeOptions: { maxValue: 1, minValue: 0, numberPrecision: 1 },
description:
'Controls diversity via nucleus sampling: 0.5 means half of all likelihood-weighted options are considered. We generally recommend altering this or temperature but not both.',
type: 'number',
},
],
},
],
};
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials<OpenAICompatibleCredential>('nvidiaApi');
const modelName = this.getNodeParameter('model', itemIndex) as string;
const options = this.getNodeParameter('options', itemIndex, {}) as {
frequencyPenalty?: number;
maxTokens?: number;
maxRetries: number;
timeout: number;
presencePenalty?: number;
temperature?: number;
topP?: number;
responseFormat?: 'text' | 'json_object';
};
const timeout = options.timeout;
const configuration: ClientOptions = {
baseURL: credentials.url,
fetchOptions: {
dispatcher: getProxyAgent(credentials.url, {
headersTimeout: timeout,
bodyTimeout: timeout,
}),
},
};
const model = new ChatOpenAI({
apiKey: credentials.apiKey || 'unused',
model: modelName,
...options,
timeout,
maxRetries: options.maxRetries ?? 2,
configuration,
callbacks: [new N8nLlmTracing(this)],
modelKwargs: options.responseFormat
? {
response_format: { type: options.responseFormat },
}
: undefined,
onFailedAttempt: makeN8nLlmFailedAttemptHandler(this, openAiFailedAttemptHandler),
});
return {
response: model,
};
}
}
@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>NVIDIA</title><path fill="#76B900" d="M8.948 8.798v-1.43a6.7 6.7 0 0 1 .424-.018c3.922-.124 6.493 3.374 6.493 3.374s-2.774 3.851-5.75 3.851c-.398 0-.787-.062-1.158-.185v-4.346c1.528.185 1.837.857 2.747 2.385l2.04-1.714s-1.492-1.952-4-1.952a6.016 6.016 0 0 0-.796.035m0-4.735v2.138l.424-.027c5.45-.185 9.01 4.47 9.01 4.47s-4.08 4.964-8.33 4.964c-.37 0-.733-.035-1.095-.097v1.325c.3.035.61.062.91.062 3.957 0 6.82-2.023 9.593-4.408.459.371 2.34 1.263 2.73 1.652-2.633 2.208-8.772 3.984-12.253 3.984-.335 0-.653-.018-.971-.053v1.864H24V4.063zm0 10.326v1.131c-3.657-.654-4.673-4.46-4.673-4.46s1.758-1.944 4.673-2.262v1.237H8.94c-1.528-.186-2.73 1.245-2.73 1.245s.68 2.412 2.739 3.11M2.456 10.9s2.164-3.197 6.5-3.533V6.201C4.153 6.59 0 10.653 0 10.653s2.35 6.802 8.948 7.42v-1.237c-4.84-.6-6.492-5.936-6.492-5.936z"/></svg>

After

Width:  |  Height:  |  Size: 897 B

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>NVIDIA</title><path fill="#76B900" d="M8.948 8.798v-1.43a6.7 6.7 0 0 1 .424-.018c3.922-.124 6.493 3.374 6.493 3.374s-2.774 3.851-5.75 3.851c-.398 0-.787-.062-1.158-.185v-4.346c1.528.185 1.837.857 2.747 2.385l2.04-1.714s-1.492-1.952-4-1.952a6.016 6.016 0 0 0-.796.035m0-4.735v2.138l.424-.027c5.45-.185 9.01 4.47 9.01 4.47s-4.08 4.964-8.33 4.964c-.37 0-.733-.035-1.095-.097v1.325c.3.035.61.062.91.062 3.957 0 6.82-2.023 9.593-4.408.459.371 2.34 1.263 2.73 1.652-2.633 2.208-8.772 3.984-12.253 3.984-.335 0-.653-.018-.971-.053v1.864H24V4.063zm0 10.326v1.131c-3.657-.654-4.673-4.46-4.673-4.46s1.758-1.944 4.673-2.262v1.237H8.94c-1.528-.186-2.73 1.245-2.73 1.245s.68 2.412 2.739 3.11M2.456 10.9s2.164-3.197 6.5-3.533V6.201C4.153 6.59 0 10.653 0 10.653s2.35 6.802 8.948 7.42v-1.237c-4.84-.6-6.492-5.936-6.492-5.936z"/></svg>

After

Width:  |  Height:  |  Size: 897 B

@@ -0,0 +1,199 @@
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/unbound-method */
import { ChatOpenAI } from '@langchain/openai';
import { makeN8nLlmFailedAttemptHandler, getProxyAgent } from '@n8n/ai-utilities';
import { createMockExecuteFunction } from 'n8n-nodes-base/test/nodes/Helpers';
import type { INode, ISupplyDataFunctions } from 'n8n-workflow';
import type { Mocked } from 'vitest';
import { LmChatNvidia } from '../LmChatNvidia.node';
vi.mock('@langchain/openai');
vi.mock('@n8n/ai-utilities');
const MockedChatOpenAI = vi.mocked(ChatOpenAI);
const mockedMakeN8nLlmFailedAttemptHandler = vi.mocked(makeN8nLlmFailedAttemptHandler);
const mockedGetProxyAgent = vi.mocked(getProxyAgent);
describe('LmChatNvidia', () => {
let node: LmChatNvidia;
const mockNodeDef: INode = {
id: '1',
name: 'NVIDIA Nemotron Chat Model',
typeVersion: 1,
type: 'n8n-nodes-langchain.lmChatNvidia',
position: [0, 0],
parameters: {},
};
const setupMockContext = (
credentialOverrides: Partial<{ apiKey: string; url: string }> = {},
nodeOverrides: Partial<INode> = {},
) => {
const nodeDef = { ...mockNodeDef, ...nodeOverrides };
const ctx = createMockExecuteFunction<ISupplyDataFunctions>(
{},
nodeDef,
) as Mocked<ISupplyDataFunctions>;
ctx.getCredentials = vi.fn().mockResolvedValue({
apiKey: 'test-key',
url: 'https://integrate.api.nvidia.com/v1',
...credentialOverrides,
});
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 === 'options') return {};
return undefined;
});
mockedMakeN8nLlmFailedAttemptHandler.mockReturnValue(vi.fn());
mockedGetProxyAgent.mockReturnValue({} as any);
return ctx;
};
beforeEach(() => {
node = new LmChatNvidia();
vi.clearAllMocks();
});
describe('node description', () => {
it('should have correct node properties', () => {
expect(node.description).toMatchObject({
displayName: 'NVIDIA Nemotron Chat Model',
name: 'lmChatNvidia',
group: ['transform'],
version: [1],
});
});
it('should require a single nvidiaApi credential', () => {
expect(node.description.credentials).toEqual([{ name: 'nvidiaApi', required: true }]);
});
it('should output ai_languageModel', () => {
expect(node.description.outputs).toEqual(['ai_languageModel']);
expect(node.description.outputNames).toEqual(['Model']);
});
it('should filter to Nemotron models 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);
});
});
describe('supplyData', () => {
it('should pass credential url to ChatOpenAI configuration', async () => {
const ctx = setupMockContext();
const result = await node.supplyData.call(ctx, 0);
expect(ctx.getCredentials).toHaveBeenCalledWith('nvidiaApi');
expect(MockedChatOpenAI).toHaveBeenCalledWith(
expect.objectContaining({
apiKey: 'test-key',
model: 'nvidia/llama-3.1-nemotron-70b-instruct',
configuration: expect.objectContaining({
baseURL: 'https://integrate.api.nvidia.com/v1',
}),
}),
);
expect(result).toEqual({ response: expect.any(Object) });
});
it('should accept a self-hosted base URL on the same credential', async () => {
const ctx = setupMockContext({ url: 'http://localhost:8000/v1', apiKey: '' });
await node.supplyData.call(ctx, 0);
expect(MockedChatOpenAI).toHaveBeenCalledWith(
expect.objectContaining({
configuration: expect.objectContaining({
baseURL: 'http://localhost:8000/v1',
}),
}),
);
});
it('should fall back to a placeholder apiKey when the credential has none', async () => {
const ctx = setupMockContext({ apiKey: '' });
await node.supplyData.call(ctx, 0);
expect(MockedChatOpenAI).toHaveBeenCalledWith(
expect.objectContaining({
apiKey: 'unused',
}),
);
});
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 === 'options')
return {
temperature: 0.5,
maxTokens: 2000,
topP: 0.9,
frequencyPenalty: 0.3,
presencePenalty: 0.2,
timeout: 60000,
maxRetries: 5,
};
return undefined;
});
await node.supplyData.call(ctx, 0);
expect(MockedChatOpenAI).toHaveBeenCalledWith(
expect.objectContaining({
temperature: 0.5,
maxTokens: 2000,
topP: 0.9,
frequencyPenalty: 0.3,
presencePenalty: 0.2,
timeout: 60000,
maxRetries: 5,
}),
);
});
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 === 'options') return { responseFormat: 'json_object' };
return undefined;
});
await node.supplyData.call(ctx, 0);
expect(MockedChatOpenAI).toHaveBeenCalledWith(
expect.objectContaining({
modelKwargs: { response_format: { type: 'json_object' } },
}),
);
});
it('should not set modelKwargs when no responseFormat', async () => {
const ctx = setupMockContext();
await node.supplyData.call(ctx, 0);
expect(MockedChatOpenAI).toHaveBeenCalledWith(
expect.objectContaining({
modelKwargs: undefined,
}),
);
});
});
});
@@ -66,6 +66,7 @@
"dist/credentials/MinimaxApi.credentials.js",
"dist/credentials/MoonshotApi.credentials.js",
"dist/credentials/LemonadeApi.credentials.js",
"dist/credentials/NvidiaApi.credentials.js",
"dist/credentials/OllamaApi.credentials.js",
"dist/credentials/OpenRouterApi.credentials.js",
"dist/credentials/PineconeApi.credentials.js",
@@ -128,6 +129,7 @@
"dist/nodes/llms/LmChatMinimax/LmChatMinimax.node.js",
"dist/nodes/llms/LmChatMoonshot/LmChatMoonshot.node.js",
"dist/nodes/llms/LMChatLemonade/LmChatLemonade.node.js",
"dist/nodes/llms/LmChatNvidia/LmChatNvidia.node.js",
"dist/nodes/llms/LMChatOllama/LmChatOllama.node.js",
"dist/nodes/llms/LmChatOpenRouter/LmChatOpenRouter.node.js",
"dist/nodes/llms/LmChatVercelAiGateway/LmChatVercelAiGateway.node.js",
@@ -46,6 +46,7 @@ const emptyCredentialIds = {
deepSeek: null,
cohere: null,
mistralCloud: null,
nvidia: null,
};
describe('ChatHubModelsService', () => {
@@ -895,6 +895,15 @@ ${this.getSystemMessageMetadata(timeZone) + artifactContext}`;
},
};
}
case 'nvidia': {
return {
...common,
parameters: {
model,
options: {},
},
};
}
default:
throw new OperationalError('Unsupported model provider');
}
@@ -93,6 +93,10 @@ export const PROVIDER_NODE_TYPE_MAP: Record<ChatHubLLMProvider, INodeTypeNameVer
name: '@n8n/n8n-nodes-langchain.lmChatMistralCloud',
version: 1,
},
nvidia: {
name: '@n8n/n8n-nodes-langchain.lmChatNvidia',
version: 1,
},
};
export const NODE_NAMES = {
@@ -158,6 +158,10 @@ export class ChatHubModelsService {
const rawModels = await this.fetchMistralCloudModels(credentials, additionalData);
return { models: this.transformAndFilterModels(rawModels, 'mistralCloud') };
}
case 'nvidia': {
const rawModels = await this.fetchNvidiaModels(credentials, additionalData);
return { models: this.transformAndFilterModels(rawModels, 'nvidia') };
}
case 'n8n':
return { models: await this.fetchAgentWorkflowsAsModels(user) };
case 'custom-agent':
@@ -405,6 +409,55 @@ export class ChatHubModelsService {
return foundationModels.concat(inferenceProfileModels);
}
private async fetchNvidiaModels(
credentials: INodeCredentials,
additionalData: IWorkflowExecuteAdditionalData,
): Promise<INodePropertyOptions[]> {
return await this.nodeParametersService.getOptionsViaLoadOptions(
{
routing: {
request: {
method: 'GET',
url: '/models',
},
output: {
postReceive: [
{
type: 'rootProperty',
properties: {
property: 'data',
},
},
{
type: 'filter',
properties: {
pass: '={{ /nemotron/i.test($responseItem.id) }}',
},
},
{
type: 'setKeyValue',
properties: {
name: '={{$responseItem.id}}',
value: '={{$responseItem.id}}',
},
},
{
type: 'sort',
properties: {
key: 'name',
},
},
],
},
},
},
additionalData,
PROVIDER_NODE_TYPE_MAP.nvidia,
{},
credentials,
);
}
private async fetchMistralCloudModels(
credentials: INodeCredentials,
additionalData: IWorkflowExecuteAdditionalData,
@@ -214,6 +214,7 @@ export const maxContextWindowTokens: Record<ChatHubLLMProvider, Record<string, n
'mistral-small-2506': 128000,
'mistral-small-latest': 128000,
},
nvidia: {},
};
const CONTEXT_WINDOW_SAFETY_FACTOR = 0.95;
@@ -306,6 +306,14 @@ export function createChatHubModuleSettings(
updatedAt: null,
enabled: true,
},
nvidia: {
provider: 'nvidia',
credentialId: null,
allowedModels: [],
createdAt: '2025-12-18T09:07:29.060Z',
updatedAt: null,
enabled: true,
},
},
...overrides,
};
@@ -32,6 +32,7 @@ export const providerDisplayNames: Record<ChatHubProvider, string> = {
deepSeek: 'DeepSeek',
cohere: 'Cohere',
mistralCloud: 'Mistral Cloud',
nvidia: 'NVIDIA Nemotron',
n8n: 'Workflow agent',
'custom-agent': 'Personal agent',
};