From 7d6345dee7ba2525fb9b712fa3ef5781bc8c3c3a Mon Sep 17 00:00:00 2001 From: FuturMix Date: Sat, 25 Apr 2026 12:58:18 +0800 Subject: [PATCH 1/2] feat: add FuturMix AI Gateway as chat model provider --- .../credentials/FuturMixApi.credential.ts | 25 +++ .../chatmodels/ChatFuturMix/ChatFuturMix.ts | 194 ++++++++++++++++++ .../ChatFuturMix/FlowiseChatFuturMix.ts | 20 ++ .../chatmodels/ChatFuturMix/futurmix.svg | 10 + 4 files changed, 249 insertions(+) create mode 100644 packages/components/credentials/FuturMixApi.credential.ts create mode 100644 packages/components/nodes/chatmodels/ChatFuturMix/ChatFuturMix.ts create mode 100644 packages/components/nodes/chatmodels/ChatFuturMix/FlowiseChatFuturMix.ts create mode 100644 packages/components/nodes/chatmodels/ChatFuturMix/futurmix.svg diff --git a/packages/components/credentials/FuturMixApi.credential.ts b/packages/components/credentials/FuturMixApi.credential.ts new file mode 100644 index 00000000000..a76b1a93ec7 --- /dev/null +++ b/packages/components/credentials/FuturMixApi.credential.ts @@ -0,0 +1,25 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class FuturMixAPIAuth implements INodeCredential { + label: string + name: string + version: number + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'FuturMix API Key' + this.name = 'futurmixApi' + this.version = 1.0 + this.inputs = [ + { + label: 'FuturMix API Key', + name: 'futurmixApiKey', + type: 'password', + description: 'API Key from https://futurmix.ai' + } + ] + } +} + +module.exports = { credClass: FuturMixAPIAuth } diff --git a/packages/components/nodes/chatmodels/ChatFuturMix/ChatFuturMix.ts b/packages/components/nodes/chatmodels/ChatFuturMix/ChatFuturMix.ts new file mode 100644 index 00000000000..fd9875e2c3f --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatFuturMix/ChatFuturMix.ts @@ -0,0 +1,194 @@ +import { ChatOpenAI as LangchainChatOpenAI, ChatOpenAIFields } from '@langchain/openai' +import { BaseCache } from '@langchain/core/caches' +import { ICommonObject, IMultiModalOption, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { ChatFuturMix } from './FlowiseChatFuturMix' + +class ChatFuturMix_ChatModels implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'FuturMix' + this.name = 'chatFuturMix' + this.version = 1.0 + this.type = 'ChatFuturMix' + this.icon = 'futurmix.svg' + this.category = 'Chat Models' + this.description = 'Wrapper around FuturMix AI Gateway - Access 22+ models with a single API key' + this.baseClasses = [this.type, ...getBaseClasses(LangchainChatOpenAI)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['futurmixApi'], + optional: true + } + this.inputs = [ + { + label: 'Cache', + name: 'cache', + type: 'BaseCache', + optional: true + }, + { + label: 'Model Name', + name: 'modelName', + type: 'string', + placeholder: 'claude-sonnet-4-20250514' + }, + { + label: 'Temperature', + name: 'temperature', + type: 'number', + step: 0.1, + default: 0.9, + optional: true + }, + { + label: 'Streaming', + name: 'streaming', + type: 'boolean', + default: true, + optional: true, + additionalParams: true + }, + { + label: 'Allow Image Uploads', + name: 'allowImageUploads', + type: 'boolean', + description: + 'Allow image input. Refer to the docs for more details.', + default: false, + optional: true + }, + { + label: 'Max Tokens', + name: 'maxTokens', + type: 'number', + step: 1, + optional: true, + additionalParams: true + }, + { + label: 'Top Probability', + name: 'topP', + type: 'number', + step: 0.1, + optional: true, + additionalParams: true + }, + { + label: 'Frequency Penalty', + name: 'frequencyPenalty', + type: 'number', + step: 0.1, + optional: true, + additionalParams: true + }, + { + label: 'Presence Penalty', + name: 'presencePenalty', + type: 'number', + step: 0.1, + optional: true, + additionalParams: true + }, + { + label: 'Timeout', + name: 'timeout', + type: 'number', + step: 1, + optional: true, + additionalParams: true + }, + { + label: 'Base Path', + name: 'basepath', + type: 'string', + optional: true, + default: 'https://futurmix.ai/v1', + description: 'Override the default base URL for the API.', + additionalParams: true + }, + { + label: 'Base Options', + name: 'baseOptions', + type: 'json', + optional: true, + description: 'Default headers to include with every request to the API.', + additionalParams: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const temperature = nodeData.inputs?.temperature as string + const modelName = nodeData.inputs?.modelName as string + const maxTokens = nodeData.inputs?.maxTokens as string + const topP = nodeData.inputs?.topP as string + const frequencyPenalty = nodeData.inputs?.frequencyPenalty as string + const presencePenalty = nodeData.inputs?.presencePenalty as string + const timeout = nodeData.inputs?.timeout as string + const streaming = nodeData.inputs?.streaming as boolean + const basePath = (nodeData.inputs?.basepath as string) || 'https://futurmix.ai/v1' + const baseOptions = nodeData.inputs?.baseOptions + const cache = nodeData.inputs?.cache as BaseCache + const allowImageUploads = nodeData.inputs?.allowImageUploads as boolean + + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const futurmixApiKey = getCredentialParam('futurmixApiKey', credentialData, nodeData) + + const obj: ChatOpenAIFields = { + temperature: parseFloat(temperature), + modelName, + openAIApiKey: futurmixApiKey, + apiKey: futurmixApiKey, + streaming: streaming ?? true + } + + if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10) + if (topP) obj.topP = parseFloat(topP) + if (frequencyPenalty) obj.frequencyPenalty = parseFloat(frequencyPenalty) + if (presencePenalty) obj.presencePenalty = parseFloat(presencePenalty) + if (timeout) obj.timeout = parseInt(timeout, 10) + if (cache) obj.cache = cache + + let parsedBaseOptions: any | undefined = undefined + + if (baseOptions) { + try { + parsedBaseOptions = typeof baseOptions === 'object' ? baseOptions : JSON.parse(baseOptions) + } catch (exception) { + throw new Error("Invalid JSON in the ChatFuturMix's BaseOptions: " + exception) + } + } + + if (basePath || parsedBaseOptions) { + obj.configuration = { + baseURL: basePath, + defaultHeaders: parsedBaseOptions + } + } + + const multiModalOption: IMultiModalOption = { + image: { + allowImageUploads: allowImageUploads ?? false + } + } + + const model = new ChatFuturMix(nodeData.id, obj) + model.setMultiModalOption(multiModalOption) + return model + } +} + +module.exports = { nodeClass: ChatFuturMix_ChatModels } diff --git a/packages/components/nodes/chatmodels/ChatFuturMix/FlowiseChatFuturMix.ts b/packages/components/nodes/chatmodels/ChatFuturMix/FlowiseChatFuturMix.ts new file mode 100644 index 00000000000..894eda07fee --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatFuturMix/FlowiseChatFuturMix.ts @@ -0,0 +1,20 @@ +import { ChatOpenAI as LangchainChatOpenAI, ChatOpenAIFields } from '@langchain/openai' +import { IMultiModalOption, IVisionChatModal } from '../../../src' + +export class ChatFuturMix extends LangchainChatOpenAI implements IVisionChatModal { + configuredModel: string + configuredMaxToken?: number + multiModalOption: IMultiModalOption + id: string + + constructor(id: string, fields?: ChatOpenAIFields) { + super(fields) + this.id = id + this.configuredModel = fields?.modelName ?? '' + this.configuredMaxToken = fields?.maxTokens + } + + setMultiModalOption(multiModalOption: IMultiModalOption): void { + this.multiModalOption = multiModalOption + } +} diff --git a/packages/components/nodes/chatmodels/ChatFuturMix/futurmix.svg b/packages/components/nodes/chatmodels/ChatFuturMix/futurmix.svg new file mode 100644 index 00000000000..5e39e9be48a --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatFuturMix/futurmix.svg @@ -0,0 +1,10 @@ + + + + + + + + + FM + From 132d9782c31d9cb5f542e2254eb68a5f31bd337a Mon Sep 17 00:00:00 2001 From: FuturMix Date: Sat, 25 Apr 2026 20:31:40 +0800 Subject: [PATCH 2/2] fix: guard temperature against NaN when undefined Move temperature out of the ChatOpenAIFields initializer and apply it conditionally, matching the pattern used by maxTokens, topP, frequencyPenalty and presencePenalty. This prevents parseFloat from returning NaN when the temperature input is undefined or empty. Co-Authored-By: Claude Opus 4.6 --- .../components/nodes/chatmodels/ChatFuturMix/ChatFuturMix.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/chatmodels/ChatFuturMix/ChatFuturMix.ts b/packages/components/nodes/chatmodels/ChatFuturMix/ChatFuturMix.ts index fd9875e2c3f..e5b7a629280 100644 --- a/packages/components/nodes/chatmodels/ChatFuturMix/ChatFuturMix.ts +++ b/packages/components/nodes/chatmodels/ChatFuturMix/ChatFuturMix.ts @@ -148,13 +148,13 @@ class ChatFuturMix_ChatModels implements INode { const futurmixApiKey = getCredentialParam('futurmixApiKey', credentialData, nodeData) const obj: ChatOpenAIFields = { - temperature: parseFloat(temperature), modelName, openAIApiKey: futurmixApiKey, apiKey: futurmixApiKey, streaming: streaming ?? true } + if (temperature) obj.temperature = parseFloat(temperature) if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10) if (topP) obj.topP = parseFloat(topP) if (frequencyPenalty) obj.frequencyPenalty = parseFloat(frequencyPenalty)