diff --git a/packages/@n8n/api-types/src/chat-hub.ts b/packages/@n8n/api-types/src/chat-hub.ts index 4be63dfdedd..09998c0aa53 100644 --- a/packages/@n8n/api-types/src/chat-hub.ts +++ b/packages/@n8n/api-types/src/chat-hub.ts @@ -11,7 +11,13 @@ import { Z } from 'zod-class'; /** * Supported AI model providers */ -export const chatHubLLMProviderSchema = z.enum(['openai', 'anthropic', 'google']); +export const chatHubLLMProviderSchema = z.enum([ + 'openai', + 'anthropic', + 'google', + 'azureOpenAi', + 'ollama', +]); export type ChatHubLLMProvider = z.infer; export const chatHubProviderSchema = z.enum([ @@ -32,6 +38,8 @@ export const PROVIDER_CREDENTIAL_TYPE_MAP: Record< openai: 'openAiApi', anthropic: 'anthropicApi', google: 'googlePalmApi', + ollama: 'ollamaApi', + azureOpenAi: 'azureOpenAiApi', }; export type ChatHubAgentTool = typeof JINA_AI_TOOL_NODE_TYPE | typeof SEAR_XNG_TOOL_NODE_TYPE; @@ -54,6 +62,16 @@ const googleModelSchema = z.object({ model: z.string(), }); +const azureOpenAIModelSchema = z.object({ + provider: z.literal('azureOpenAi'), + model: z.string(), +}); + +const ollamaModelSchema = z.object({ + provider: z.literal('ollama'), + model: z.string(), +}); + const n8nModelSchema = z.object({ provider: z.literal('n8n'), workflowId: z.string(), @@ -68,6 +86,8 @@ export const chatHubConversationModelSchema = z.discriminatedUnion('provider', [ openAIModelSchema, anthropicModelSchema, googleModelSchema, + azureOpenAIModelSchema, + ollamaModelSchema, n8nModelSchema, chatAgentSchema, ]); @@ -75,7 +95,14 @@ export const chatHubConversationModelSchema = z.discriminatedUnion('provider', [ export type ChatHubOpenAIModel = z.infer; export type ChatHubAnthropicModel = z.infer; export type ChatHubGoogleModel = z.infer; -export type ChatHubBaseLLMModel = ChatHubOpenAIModel | ChatHubAnthropicModel | ChatHubGoogleModel; +export type ChatHubAzureOpenAIModel = z.infer; +export type ChatHubOllamaModel = z.infer; +export type ChatHubBaseLLMModel = + | ChatHubOpenAIModel + | ChatHubAnthropicModel + | ChatHubGoogleModel + | ChatHubAzureOpenAIModel + | ChatHubOllamaModel; export type ChatHubN8nModel = z.infer; export type ChatHubCustomAgentModel = z.infer; @@ -114,6 +141,8 @@ export const emptyChatModelsResponse: ChatModelsResponse = { openai: { models: [] }, anthropic: { models: [] }, google: { models: [] }, + azureOpenAi: { models: [] }, + ollama: { models: [] }, n8n: { models: [] }, // eslint-disable-next-line @typescript-eslint/naming-convention 'custom-agent': { models: [] }, diff --git a/packages/cli/src/modules/chat-hub/chat-hub-workflow.service.ts b/packages/cli/src/modules/chat-hub/chat-hub-workflow.service.ts index cc6f9a4d024..41a65870f64 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub-workflow.service.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub-workflow.service.ts @@ -448,6 +448,25 @@ export class ChatHubWorkflowService { options: {}, }, }; + case 'azureOpenAi': + return { + ...common, + parameters: { + model: { __rl: true, mode: 'id', value: model }, + options: {}, + }, + }; + case 'ollama': { + return { + ...common, + parameters: { + model: { __rl: true, mode: 'id', value: model }, + options: {}, + }, + }; + } + default: + throw new OperationalError('Unsupported model provider'); } } diff --git a/packages/cli/src/modules/chat-hub/chat-hub.constants.ts b/packages/cli/src/modules/chat-hub/chat-hub.constants.ts index d27e581b6f3..51a362d4847 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub.constants.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub.constants.ts @@ -24,6 +24,14 @@ export const PROVIDER_NODE_TYPE_MAP: Record { + const results = await this.nodeParametersService.getOptionsViaLoadOptions( + { + // From Ollama Model node + // https://github.com/n8n-io/n8n/blob/master/packages/%40n8n/nodes-langchain/nodes/llms/LMOllama/description.ts#L24 + routing: { + request: { + method: 'GET', + url: '/api/tags', + }, + output: { + postReceive: [ + { + type: 'rootProperty', + properties: { + property: 'models', + }, + }, + { + type: 'setKeyValue', + properties: { + name: '={{$responseItem.name}}', + value: '={{$responseItem.name}}', + }, + }, + { + type: 'sort', + properties: { + key: 'name', + }, + }, + ], + }, + }, + }, + additionalData, + PROVIDER_NODE_TYPE_MAP.ollama, + {}, + credentials, + ); + + return { + models: results.map((result) => ({ + name: String(result.value), + description: result.description ?? null, + model: { + provider: 'ollama', + model: String(result.value), + }, + createdAt: null, + updatedAt: null, + })), + }; + } + + private async fetchAzureOpenAiModels( + _credentials: INodeCredentials, + _additionalData: IWorkflowExecuteAdditionalData, + ): Promise { + // Azure doesn't appear to offer a way to list available models via API. + // If we add support for this in the future on the Azure OpenAI node we should copy that + // implementation here too. + return { + models: [], + }; + } + private async fetchAgentWorkflowsAsModels(user: User): Promise { const nodeTypes = [CHAT_TRIGGER_NODE_TYPE]; const workflows = await this.workflowService.getWorkflowsWithNodesIncluded( diff --git a/packages/cli/src/modules/chat-hub/context-limits.ts b/packages/cli/src/modules/chat-hub/context-limits.ts index 7dc5fa6831c..ca3fde834c4 100644 --- a/packages/cli/src/modules/chat-hub/context-limits.ts +++ b/packages/cli/src/modules/chat-hub/context-limits.ts @@ -137,6 +137,8 @@ export const maxContextWindowTokens: Record { function getAgent(model: ChatHubConversationModel) { if (!agents.value) return; - return agents.value[model.provider].models.find((agent) => isMatchedAgent(agent, model)); + const agent = agents.value[model.provider].models.find((agent) => isMatchedAgent(agent, model)); + + if (!agent) { + if (model.provider === 'custom-agent' || model.provider === 'n8n') { + return; + } + + // Allow custom models chosen by ID even if they are not in the fetched list + return { + model: { + provider: model.provider, + model: model.model, + }, + name: model.model, + description: null, + createdAt: null, + updatedAt: null, + }; + } + + return agent; } return { diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/components/CredentialSelectorModal.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/components/CredentialSelectorModal.vue index 6bf1333132d..65b3ae2962a 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/components/CredentialSelectorModal.vue +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/components/CredentialSelectorModal.vue @@ -1,6 +1,6 @@ + + + + diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/components/ModelSelector.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/components/ModelSelector.vue index 18459379823..77713e6af63 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/components/ModelSelector.vue +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/components/ModelSelector.vue @@ -13,13 +13,16 @@ import type { ChatModelDto, ChatModelsResponse, } from '@n8n/api-types'; -import { providerDisplayNames } from '@/features/ai/chatHub/constants'; +import { + CHAT_CREDENTIAL_SELECTOR_MODAL_KEY, + CHAT_MODEL_BY_ID_SELECTOR_MODAL_KEY, + providerDisplayNames, +} from '@/features/ai/chatHub/constants'; import CredentialIcon from '@/features/credentials/components/CredentialIcon.vue'; import { onClickOutside } from '@vueuse/core'; import { useI18n } from '@n8n/i18n'; import type { CredentialsMap } from '../chat.types'; -import CredentialSelectorModal from './CredentialSelectorModal.vue'; import { useUIStore } from '@/app/stores/ui.store'; import { useCredentialsStore } from '@/features/credentials/credentials.store'; import ChatAgentAvatar from '@/features/ai/chatHub/components/ChatAgentAvatar.vue'; @@ -55,10 +58,22 @@ function handleSelectCredentials(provider: ChatHubProvider, id: string) { emit('selectCredential', provider, id); } +function handleSelectModelById(provider: ChatHubLLMProvider, modelId: string) { + emit('change', { + model: { + provider, + model: modelId, + }, + name: modelId, + description: null, + updatedAt: null, + createdAt: null, + }); +} + const i18n = useI18n(); const agents = ref(emptyChatModelsResponse); const dropdownRef = useTemplateRef('dropdownRef'); -const credentialSelectorProvider = ref(null); const uiStore = useUIStore(); const credentialsStore = useCredentialsStore(); const telemetry = useTelemetry(); @@ -119,10 +134,16 @@ const menu = computed(() => { const submenu = agentOptions.concat([ ...(agentOptions.length > 0 ? [{ isDivider: true as const, id: 'divider' }] : []), + { + id: `${provider}::add-model`, + icon: 'plus', + title: i18n.baseText('chatHub.agent.addModel'), + disabled: false, + }, { id: `${provider}::configure`, icon: 'settings', - title: 'Configure credentials...', + title: i18n.baseText('chatHub.agent.configureCredentials'), disabled: false, }, ]); @@ -148,8 +169,26 @@ function openCredentialsSelectorOrCreate(provider: ChatHubLLMProvider) { return; } - credentialSelectorProvider.value = provider; - uiStore.openModal('chatCredentialSelector'); + uiStore.openModalWithData({ + name: CHAT_CREDENTIAL_SELECTOR_MODAL_KEY, + data: { + provider, + initialValue: credentials?.[provider] ?? null, + onSelect: handleSelectCredentials, + onCreateNew: handleCreateNewCredential, + }, + }); +} + +function openModelByIdSelector(provider: ChatHubLLMProvider) { + uiStore.openModalWithData({ + name: CHAT_MODEL_BY_ID_SELECTOR_MODAL_KEY, + data: { + provider, + initialValue: null, + onSelect: handleSelectModelById, + }, + }); } function onSelect(id: string) { @@ -174,6 +213,15 @@ function onSelect(id: string) { return; } + if ( + identifier === 'add-model' && + parsedModel.provider !== 'n8n' && + parsedModel.provider !== 'custom-agent' + ) { + openModelByIdSelector(parsedModel.provider); + return; + } + const selected = agents.value[parsedModel.provider].models.find((a) => isMatchedAgent(a, parsedModel), ); @@ -242,15 +290,6 @@ defineExpose({ - - = { openai: 'OpenAI', anthropic: 'Anthropic', google: 'Google', + azureOpenAi: 'Azure OpenAI', + ollama: 'Ollama', n8n: 'n8n', 'custom-agent': 'Custom Agent', }; @@ -19,3 +21,5 @@ export const MOBILE_MEDIA_QUERY = '(max-width: 768px)'; export const TOOLS_SELECTOR_MODAL_KEY = 'toolsSelectorModal'; export const AGENT_EDITOR_MODAL_KEY = 'agentEditorModal'; +export const CHAT_CREDENTIAL_SELECTOR_MODAL_KEY = 'chatCredentialSelectorModal'; +export const CHAT_MODEL_BY_ID_SELECTOR_MODAL_KEY = 'chatModelByIdSelectorModal'; diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/module.descriptor.ts b/packages/frontend/editor-ui/src/features/ai/chatHub/module.descriptor.ts index ce28a952138..7dc03e0ca9d 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/module.descriptor.ts +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/module.descriptor.ts @@ -5,6 +5,8 @@ import { CHAT_AGENTS_VIEW, TOOLS_SELECTOR_MODAL_KEY, AGENT_EDITOR_MODAL_KEY, + CHAT_CREDENTIAL_SELECTOR_MODAL_KEY, + CHAT_MODEL_BY_ID_SELECTOR_MODAL_KEY, } from '@/features/ai/chatHub/constants'; const ChatSidebar = async () => await import('@/features/ai/chatHub/components/ChatSidebar.vue'); @@ -40,6 +42,31 @@ export const ChatModule: FrontendModuleDescription = { }, }, }, + { + key: CHAT_CREDENTIAL_SELECTOR_MODAL_KEY, + component: async () => await import('./components/CredentialSelectorModal.vue'), + initialState: { + open: false, + data: { + provider: null, + initialValue: null, + onSelect: () => {}, + onCreateNew: () => {}, + }, + }, + }, + { + key: CHAT_MODEL_BY_ID_SELECTOR_MODAL_KEY, + component: async () => await import('./components/ModelByIdSelectorModal.vue'), + initialState: { + open: false, + data: { + provider: null, + initialValue: null, + onSelect: () => {}, + }, + }, + }, ], routes: [ {