From a331603fedd901adfe5999abd954415c35172756 Mon Sep 17 00:00:00 2001 From: Octopus Date: Tue, 2 Jun 2026 23:27:00 +0800 Subject: [PATCH] feat: upgrade MiniMax default model to M3 - Add ChatMiniMax node with FlowiseChatMiniMax integration - Add MiniMaxApi credential definition - Update models.json with MiniMax model list (M3, M2.7, M2.7-highspeed) - Set MiniMax-M3 as the default model - Add MiniMax TTS support in textToSpeech.ts --- .../credentials/MiniMaxApi.credential.ts | 23 ++++ packages/components/models.json | 24 ++++ .../chatmodels/ChatMiniMax/ChatMiniMax.ts | 118 ++++++++++++++++ .../ChatMiniMax/FlowiseChatMiniMax.ts | 41 ++++++ .../nodes/chatmodels/ChatMiniMax/minimax.svg | 1 + packages/components/src/textToSpeech.ts | 127 +++++++++++++++++- 6 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 packages/components/credentials/MiniMaxApi.credential.ts create mode 100644 packages/components/nodes/chatmodels/ChatMiniMax/ChatMiniMax.ts create mode 100644 packages/components/nodes/chatmodels/ChatMiniMax/FlowiseChatMiniMax.ts create mode 100644 packages/components/nodes/chatmodels/ChatMiniMax/minimax.svg diff --git a/packages/components/credentials/MiniMaxApi.credential.ts b/packages/components/credentials/MiniMaxApi.credential.ts new file mode 100644 index 00000000000..b4f8301e2b6 --- /dev/null +++ b/packages/components/credentials/MiniMaxApi.credential.ts @@ -0,0 +1,23 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class MiniMaxApi implements INodeCredential { + label: string + name: string + version: number + inputs: INodeParams[] + + constructor() { + this.label = 'MiniMax API' + this.name = 'miniMaxApi' + this.version = 1.0 + this.inputs = [ + { + label: 'MiniMax API Key', + name: 'miniMaxApiKey', + type: 'password' + } + ] + } +} + +module.exports = { credClass: MiniMaxApi } diff --git a/packages/components/models.json b/packages/components/models.json index 6b8ce72d5b8..ead7cb577d0 100644 --- a/packages/components/models.json +++ b/packages/components/models.json @@ -2193,6 +2193,30 @@ } ] }, + { + "name": "chatMiniMax", + "models": [ + { + "label": "MiniMax-M3", + "name": "MiniMax-M3", + "description": "MiniMax M3 - 512K context, 128K max output, image input", + "input_cost": 6e-7, + "output_cost": 2.4e-6 + }, + { + "label": "MiniMax-M2.7", + "name": "MiniMax-M2.7", + "input_cost": 3e-7, + "output_cost": 1.2e-6 + }, + { + "label": "MiniMax-M2.7-highspeed", + "name": "MiniMax-M2.7-highspeed", + "input_cost": 6e-7, + "output_cost": 2.4e-6 + } + ] + }, { "name": "chatMistralAI", "models": [ diff --git a/packages/components/nodes/chatmodels/ChatMiniMax/ChatMiniMax.ts b/packages/components/nodes/chatmodels/ChatMiniMax/ChatMiniMax.ts new file mode 100644 index 00000000000..7dda1e1de2c --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatMiniMax/ChatMiniMax.ts @@ -0,0 +1,118 @@ +import { ChatAnthropic as LangchainChatAnthropic } from '@langchain/anthropic' +import { BaseCache } from '@langchain/core/caches' +import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { ChatMiniMax, ChatMiniMaxInput } from './FlowiseChatMiniMax' +import { getModels, MODEL_TYPE } from '../../../src/modelLoader' + +class ChatMiniMax_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 = 'ChatMiniMax' + this.name = 'chatMiniMax' + this.version = 1.0 + this.type = 'ChatMiniMax' + this.icon = 'minimax.svg' + this.category = 'Chat Models' + this.description = 'Wrapper around MiniMax large language models using Anthropic-compatible API' + this.baseClasses = [this.type, ...getBaseClasses(LangchainChatAnthropic)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['miniMaxApi'] + } + this.inputs = [ + { + label: 'Cache', + name: 'cache', + type: 'BaseCache', + optional: true + }, + { + label: 'Model Name', + name: 'modelName', + type: 'asyncOptions', + loadMethod: 'listModels', + default: 'MiniMax-M3' + }, + { + label: 'Temperature', + name: 'temperature', + type: 'number', + step: 0.1, + default: 1.0, + optional: true + }, + { + label: 'Streaming', + name: 'streaming', + type: 'boolean', + default: true, + optional: true, + additionalParams: true + }, + { + label: 'Max Tokens', + name: 'maxTokens', + type: 'number', + step: 1, + optional: true, + additionalParams: true + }, + { + label: 'Top P', + name: 'topP', + type: 'number', + step: 0.1, + optional: true, + additionalParams: true + } + ] + } + + //@ts-ignore + loadMethods = { + async listModels(): Promise { + return await getModels(MODEL_TYPE.CHAT, 'chatMiniMax') + } + } + + 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 streaming = nodeData.inputs?.streaming as boolean + const cache = nodeData.inputs?.cache as BaseCache + + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const miniMaxApiKey = getCredentialParam('miniMaxApiKey', credentialData, nodeData) + + const obj: ChatMiniMaxInput = { + modelName, + miniMaxApiKey, + streaming: streaming ?? true + } + + if (temperature) obj.temperature = parseFloat(temperature) + if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10) + if (topP) obj.topP = parseFloat(topP) + if (cache) obj.cache = cache + + const model = new ChatMiniMax(nodeData.id, obj) + return model + } +} + +module.exports = { nodeClass: ChatMiniMax_ChatModels } diff --git a/packages/components/nodes/chatmodels/ChatMiniMax/FlowiseChatMiniMax.ts b/packages/components/nodes/chatmodels/ChatMiniMax/FlowiseChatMiniMax.ts new file mode 100644 index 00000000000..0d4304cf35d --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatMiniMax/FlowiseChatMiniMax.ts @@ -0,0 +1,41 @@ +import { ChatAnthropic as LangchainChatAnthropic, AnthropicInput } from '@langchain/anthropic' +import { type BaseChatModelParams } from '@langchain/core/language_models/chat_models' + +export interface ChatMiniMaxInput extends Partial, BaseChatModelParams { + miniMaxApiKey?: string +} + +export class ChatMiniMax extends LangchainChatAnthropic { + configuredModel: string + configuredMaxToken?: number + id: string + + constructor(id: string, fields?: ChatMiniMaxInput) { + const miniMaxApiKey = fields?.miniMaxApiKey || fields?.anthropicApiKey + + super({ + ...fields, + anthropicApiKey: miniMaxApiKey, + clientOptions: { + baseURL: 'https://api.minimax.io/anthropic' + } + }) + + this.id = id + this.configuredModel = fields?.modelName || 'MiniMax-M3' + this.configuredMaxToken = fields?.maxTokens + + // @langchain/anthropic defaults topP and topK to -1 as an "unset" sentinel and + // always serialises them into the request body. The real Anthropic API accepts + // -1 silently, but MiniMax's Anthropic-compatible endpoint requires top_p/top_k + // to be in (0, 1]. Setting them to undefined causes JSON.stringify to omit the + // fields entirely so MiniMax applies its own defaults. + if (fields?.topP === undefined) this.topP = undefined as unknown as number + if (fields?.topK === undefined) this.topK = undefined as unknown as number + } + + revertToOriginalModel(): void { + this.modelName = this.configuredModel + this.maxTokens = this.configuredMaxToken + } +} diff --git a/packages/components/nodes/chatmodels/ChatMiniMax/minimax.svg b/packages/components/nodes/chatmodels/ChatMiniMax/minimax.svg new file mode 100644 index 00000000000..2a60bd4731e --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatMiniMax/minimax.svg @@ -0,0 +1 @@ +Minimax \ No newline at end of file diff --git a/packages/components/src/textToSpeech.ts b/packages/components/src/textToSpeech.ts index c4611806406..1597192e8b0 100644 --- a/packages/components/src/textToSpeech.ts +++ b/packages/components/src/textToSpeech.ts @@ -7,7 +7,8 @@ import type { ReadableStream } from 'node:stream/web' const TextToSpeechType = { OPENAI_TTS: 'openai', - ELEVEN_LABS_TTS: 'elevenlabs' + ELEVEN_LABS_TTS: 'elevenlabs', + MINIMAX_TTS: 'minimax' } export const convertTextToSpeechStream = async ( @@ -100,6 +101,118 @@ export const convertTextToSpeechStream = async ( }) break } + + case TextToSpeechType.MINIMAX_TTS: { + onStart('mp3') + + const apiKey = credentialData.miniMaxApiKey + if (!apiKey) { + throw new Error('MiniMax API Key is required') + } + + const voiceId = textToSpeechConfig.voice || 'English_expressive_narrator' + const model = textToSpeechConfig.model || 'speech-2.8-hd' + + const requestBody: Record = { + model: model, + text: text, + stream: true, + language_boost: 'auto', + output_format: 'hex', + voice_setting: { + voice_id: voiceId, + speed: textToSpeechConfig.speed ?? 1.0, + vol: textToSpeechConfig.vol ?? 1.0, + pitch: textToSpeechConfig.pitch ?? 0 + }, + audio_setting: { + format: 'mp3', + sample_rate: 32000, + bitrate: 128000, + channel: 1 + } + } + + const response = await fetch('https://api.minimax.io/v1/t2a_v2', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}` + }, + body: JSON.stringify(requestBody), + signal: abortController.signal + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`MiniMax TTS API error: ${response.status} - ${errorText}`) + } + + if (!response.body) { + throw new Error('Failed to get response stream from MiniMax') + } + + const reader = response.body.getReader() + const decoder = new TextDecoder() + let sseBuffer = '' + + const processMinimaxStream = async () => { + for (;;) { + if (abortController.signal.aborted) { + reader.cancel() + streamDestroyed = true + reject(new Error('TTS generation aborted')) + return + } + + const { done, value } = await reader.read() + if (done) break + + sseBuffer += decoder.decode(value, { stream: true }) + const lines = sseBuffer.split('\n') + sseBuffer = lines.pop() || '' + + for (const line of lines) { + const trimmedLine = line.trim() + if (!trimmedLine || trimmedLine.startsWith(':')) { + continue + } + + if (trimmedLine.startsWith('data:')) { + const jsonStr = trimmedLine.slice(5).trim() + if (!jsonStr) continue + + try { + const eventData = JSON.parse(jsonStr) + + if (eventData.base_resp?.status_code !== 0) { + const errorMsg = eventData.base_resp?.status_msg || 'Unknown error' + reject(new Error(`MiniMax TTS error: ${errorMsg}`)) + return + } + + if (eventData.data?.audio) { + const audioChunk = Buffer.from(eventData.data.audio, 'hex') + onChunk(audioChunk) + } + + if (eventData.data?.status === 2) { + break + } + } catch { + // Skip malformed JSON + } + } + } + } + + onEnd() + resolve() + } + + await processMinimaxStream() + break + } } } else { reject(new Error('Text to speech is not selected. Please configure TTS in the chatflow.')) @@ -234,6 +347,18 @@ export const getVoices = async (provider: string, credentialId: string, options: })) } + case TextToSpeechType.MINIMAX_TTS: { + return [ + // English voices (official recommended) + { id: 'English_expressive_narrator', name: 'Expressive Narrator', category: 'English' }, + { id: 'English_Graceful_Lady', name: 'Graceful Lady', category: 'English' }, + { id: 'English_Insightful_Speaker', name: 'Insightful Speaker', category: 'English' }, + { id: 'English_radiant_girl', name: 'Radiant Girl', category: 'English' }, + { id: 'English_Persuasive_Man', name: 'Persuasive Man', category: 'English' }, + { id: 'English_Lucky_Robot', name: 'Lucky Robot', category: 'English' } + ] + } + default: throw new Error(`Unsupported TTS provider: ${provider}`) }