diff --git a/README.md b/README.md index 9fd3f14758..996b5b6abf 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Typical users of ChatALL are: | [iFLYTEK SPARK](http://xinghuo.xfyun.cn/) | Yes | Coming soon | | | [Kimi](https://kimi.moonshot.cn/ ) | Yes | No API | | | [Llama 2 13B & 70B](https://ai.meta.com/llama/) | Yes | No API | | +| [MiniMax](https://www.minimax.io/) | No | Yes | | | [MOSS](https://moss.fastnlp.top/) | Yes | No API | | | [Perplexity](https://www.perplexity.ai/) | Yes | No API | | | [Phind](https://www.phind.com/) | Yes | No API | | diff --git a/README_ZH-CN.md b/README_ZH-CN.md index 981730859d..6806972970 100644 --- a/README_ZH-CN.md +++ b/README_ZH-CN.md @@ -48,6 +48,7 @@ ChatALL 的典型用户是: | [讯飞星火](http://xinghuo.xfyun.cn/) | 支持 | 即将推出 | | | [Kimi](https://kimi.moonshot.cn/) | 支持 | 无 API | | | [Llama 2 13B 和 70B](https://ai.meta.com/llama/) | 支持 | 无 API | | +| [MiniMax](https://www.minimax.io/) | 不支持 | 支持 | | | [MOSS](https://moss.fastnlp.top/) | 支持 | 无 API | | | [Perplexity](https://www.perplexity.ai/) | 支持 | 无 | | | [Phind](https://www.phind.com/) | 支持 | 无 API | | diff --git a/public/bots/minimax-logo.svg b/public/bots/minimax-logo.svg new file mode 100644 index 0000000000..243dfc8785 --- /dev/null +++ b/public/bots/minimax-logo.svg @@ -0,0 +1,5 @@ + + + MM + MiniMax + diff --git a/public/bots/minimax-m25-highspeed-logo.png b/public/bots/minimax-m25-highspeed-logo.png new file mode 100644 index 0000000000..17d0c5f6d9 Binary files /dev/null and b/public/bots/minimax-m25-highspeed-logo.png differ diff --git a/public/bots/minimax-m25-logo.png b/public/bots/minimax-m25-logo.png new file mode 100644 index 0000000000..cfdc2c15c7 Binary files /dev/null and b/public/bots/minimax-m25-logo.png differ diff --git a/src/bots/index.js b/src/bots/index.js index 3f989ff94c..7bfdb34a23 100644 --- a/src/bots/index.js +++ b/src/bots/index.js @@ -66,6 +66,9 @@ import ClaudeAPIHaikuBot from "./anthropic/ClaudeAPIHaikuBot"; import Grok2APIBot from "./xai/Grok2APIBot"; import Grok3APIBot from "./xai/Grok3APIBot"; import Grok3MiniAPIBot from "./xai/Grok3MiniAPIBot"; +import MiniMaxM27Bot from "./minimax/MiniMaxM27Bot"; +import MiniMaxM27HighspeedBot from "./minimax/MiniMaxM27HighspeedBot"; +import MiniMaxM3Bot from "./minimax/MiniMaxM3Bot"; const all = [ Qihoo360AIBrainBot.getInstance(), @@ -134,6 +137,9 @@ const all = [ Grok2APIBot.getInstance(), Grok3APIBot.getInstance(), Grok3MiniAPIBot.getInstance(), + MiniMaxM27Bot.getInstance(), + MiniMaxM27HighspeedBot.getInstance(), + MiniMaxM3Bot.getInstance(), ]; const disabled = ["HuggingChatBot"]; @@ -230,6 +236,9 @@ export const botTags = { bots.getBotByClassName("Grok2APIBot"), bots.getBotByClassName("Grok3APIBot"), bots.getBotByClassName("Grok3MiniAPIBot"), + bots.getBotByClassName("MiniMaxM27Bot"), + bots.getBotByClassName("MiniMaxM27HighspeedBot"), + bots.getBotByClassName("MiniMaxM3Bot"), ], madeInChina: [ bots.getBotByClassName("Qihoo360AIBrainBot"), @@ -243,6 +252,9 @@ export const botTags = { bots.getBotByClassName("ChatGLMBot"), bots.getBotByClassName("ChatGLM4Bot"), bots.getBotByClassName("KimiBot"), + bots.getBotByClassName("MiniMaxM27Bot"), + bots.getBotByClassName("MiniMaxM27HighspeedBot"), + bots.getBotByClassName("MiniMaxM3Bot"), ], }; export default bots; diff --git a/src/bots/minimax/MiniMaxAPIBot.js b/src/bots/minimax/MiniMaxAPIBot.js new file mode 100644 index 0000000000..e992c5621c --- /dev/null +++ b/src/bots/minimax/MiniMaxAPIBot.js @@ -0,0 +1,41 @@ +import LangChainBot from "../LangChainBot"; +import store from "@/store"; +import { ChatOpenAI } from "@langchain/openai"; + +export default class MiniMaxAPIBot extends LangChainBot { + static _brandId = "minimaxApi"; + static _className = "MiniMaxAPIBot"; + + constructor() { + super(); + } + + async _checkAvailability() { + let available = false; + + if (store.state.minimaxApi.apiKey) { + this.setupModel(); + available = true; + } + return available; + } + + _setupModel() { + const chatModel = new ChatOpenAI({ + configuration: { + basePath: store.state.minimaxApi.alterUrl + ? store.state.minimaxApi.alterUrl + : "https://api.minimax.io/v1", + }, + openAIApiKey: store.state.minimaxApi.apiKey, + modelName: this.constructor._model ? this.constructor._model : "", + temperature: store.state.minimaxApi.temperature, + streaming: true, + }); + return chatModel; + } + + getPastRounds() { + return store.state.minimaxApi.pastRounds; + } +} diff --git a/src/bots/minimax/MiniMaxM27Bot.js b/src/bots/minimax/MiniMaxM27Bot.js new file mode 100644 index 0000000000..3b92e54763 --- /dev/null +++ b/src/bots/minimax/MiniMaxM27Bot.js @@ -0,0 +1,10 @@ +import MiniMaxAPIBot from "./MiniMaxAPIBot"; + +export default class MiniMaxM27Bot extends MiniMaxAPIBot { + static _className = "MiniMaxM27Bot"; + static _logoFilename = "minimax-logo.svg"; + static _model = "MiniMax-M2.7"; + constructor() { + super(); + } +} diff --git a/src/bots/minimax/MiniMaxM27HighspeedBot.js b/src/bots/minimax/MiniMaxM27HighspeedBot.js new file mode 100644 index 0000000000..86ea57e84a --- /dev/null +++ b/src/bots/minimax/MiniMaxM27HighspeedBot.js @@ -0,0 +1,10 @@ +import MiniMaxAPIBot from "./MiniMaxAPIBot"; + +export default class MiniMaxM27HighspeedBot extends MiniMaxAPIBot { + static _className = "MiniMaxM27HighspeedBot"; + static _logoFilename = "minimax-logo.svg"; + static _model = "MiniMax-M2.7-highspeed"; + constructor() { + super(); + } +} diff --git a/src/bots/minimax/MiniMaxM3Bot.js b/src/bots/minimax/MiniMaxM3Bot.js new file mode 100644 index 0000000000..d8cfe5b366 --- /dev/null +++ b/src/bots/minimax/MiniMaxM3Bot.js @@ -0,0 +1,10 @@ +import MiniMaxAPIBot from "./MiniMaxAPIBot"; + +export default class MiniMaxM3Bot extends MiniMaxAPIBot { + static _className = "MiniMaxM3Bot"; + static _logoFilename = "minimax-logo.svg"; + static _model = "MiniMax-M3"; + constructor() { + super(); + } +} diff --git a/src/components/BotSettings/MiniMaxAPIBotSettings.vue b/src/components/BotSettings/MiniMaxAPIBotSettings.vue new file mode 100644 index 0000000000..66206bc46b --- /dev/null +++ b/src/components/BotSettings/MiniMaxAPIBotSettings.vue @@ -0,0 +1,73 @@ + + + diff --git a/src/components/SettingsModal.vue b/src/components/SettingsModal.vue index b639dd4315..fb33dc7665 100644 --- a/src/components/SettingsModal.vue +++ b/src/components/SettingsModal.vue @@ -128,6 +128,7 @@ import { resolveTheme, applyTheme, Mode } from "../theme"; import ClaudeAPIBotSettings from "./BotSettings/ClaudeAPIBotSettings.vue"; import GroqAPIBotSettings from "./BotSettings/GroqAPIBotSettings.vue"; import xAIAPIBotSettings from "./BotSettings/xAIAPIBotSettings.vue"; +import MiniMaxAPIBotSettings from "./BotSettings/MiniMaxAPIBotSettings.vue"; const { ipcRenderer } = window.require("electron"); const { t: $t, locale } = useI18n(); @@ -156,6 +157,7 @@ const botSettings = [ { brand: "huggingChat", component: HuggingChatBotSettings }, { brand: "kimi", component: KimiBotSettings }, { brand: "lmsys", component: LMSYSBotSettings }, + { brand: "minimaxApi", component: MiniMaxAPIBotSettings }, { brand: "mistral", component: MistralBotSettings }, { brand: "moss", component: MOSSBotSettings }, { brand: "openaiApi", component: OpenAIAPIBotSettings }, diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index a2a895cfa3..c21d56f5ac 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -393,5 +393,17 @@ "10": "10", "25": "25", "50": "50", - "100": "100" + "100": "100", + "minimaxApi": { + "name": "MiniMax API", + "MiniMax-M27": "MiniMax-M2.7", + "MiniMax-M27-highspeed": "MiniMax-M2.7-highspeed", + "MiniMax-M3": "MiniMax-M3", + "alterUrl": "API URL", + "alterUrlPrompt": "Custom API endpoint URL (default: https://api.minimax.io/v1)", + "temperature": "Temperature", + "temperaturePrompt": "What sampling temperature to use, between 0.1 and 1", + "temperature01": "Precise", + "temperature1": "Creative" + } } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index b2483a03e6..4dd53915aa 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -317,6 +317,18 @@ "gemma-7b-it": "Gemma 7b", "gemma2-9b-it": "Gemma2 9b" }, + "minimaxApi": { + "name": "MiniMax API", + "MiniMax-M27": "MiniMax-M2.7", + "MiniMax-M27-highspeed": "MiniMax-M2.7-highspeed", + "MiniMax-M3": "MiniMax-M3", + "alterUrl": "API URL", + "alterUrlPrompt": "Custom API endpoint URL (default: https://api.minimax.io/v1)", + "temperature": "Temperature", + "temperaturePrompt": "What sampling temperature to use, between 0.1 and 1", + "temperature01": "Precise", + "temperature1": "Creative" + }, "xaiApi": { "name": "xAI API", "grok-2-latest": "Grok-2", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 2238dac17b..2ea8bdbe88 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -393,5 +393,17 @@ "10": "10", "25": "25", "50": "50", - "100": "100" + "100": "100", + "minimaxApi": { + "name": "MiniMax API", + "MiniMax-M27": "MiniMax-M2.7", + "MiniMax-M27-highspeed": "MiniMax-M2.7-highspeed", + "MiniMax-M3": "MiniMax-M3", + "alterUrl": "API URL", + "alterUrlPrompt": "Custom API endpoint URL (default: https://api.minimax.io/v1)", + "temperature": "Temperature", + "temperaturePrompt": "What sampling temperature to use, between 0.1 and 1", + "temperature01": "Precise", + "temperature1": "Creative" + } } diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index e3327bb793..b98d42fc82 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -393,5 +393,17 @@ "10": "10", "25": "25", "50": "50", - "100": "100" + "100": "100", + "minimaxApi": { + "name": "MiniMax API", + "MiniMax-M27": "MiniMax-M2.7", + "MiniMax-M27-highspeed": "MiniMax-M2.7-highspeed", + "MiniMax-M3": "MiniMax-M3", + "alterUrl": "API URL", + "alterUrlPrompt": "Custom API endpoint URL (default: https://api.minimax.io/v1)", + "temperature": "Temperature", + "temperaturePrompt": "What sampling temperature to use, between 0.1 and 1", + "temperature01": "Precise", + "temperature1": "Creative" + } } diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index ad0f563897..32c58f8ab4 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -395,5 +395,17 @@ "10": "10", "25": "25", "50": "50", - "100": "100" + "100": "100", + "minimaxApi": { + "name": "MiniMax API", + "MiniMax-M27": "MiniMax-M2.7", + "MiniMax-M27-highspeed": "MiniMax-M2.7-highspeed", + "MiniMax-M3": "MiniMax-M3", + "alterUrl": "API URL", + "alterUrlPrompt": "Custom API endpoint URL (default: https://api.minimax.io/v1)", + "temperature": "Temperature", + "temperaturePrompt": "What sampling temperature to use, between 0.1 and 1", + "temperature01": "Precise", + "temperature1": "Creative" + } } diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index dedb86e53d..87a9aa3742 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -393,5 +393,17 @@ "10": "10", "25": "25", "50": "50", - "100": "100" + "100": "100", + "minimaxApi": { + "name": "MiniMax API", + "MiniMax-M27": "MiniMax-M2.7", + "MiniMax-M27-highspeed": "MiniMax-M2.7-highspeed", + "MiniMax-M3": "MiniMax-M3", + "alterUrl": "API URL", + "alterUrlPrompt": "Custom API endpoint URL (default: https://api.minimax.io/v1)", + "temperature": "Temperature", + "temperaturePrompt": "What sampling temperature to use, between 0.1 and 1", + "temperature01": "Precise", + "temperature1": "Creative" + } } diff --git a/src/i18n/locales/ko.json b/src/i18n/locales/ko.json index a92bf0a5aa..4d7a797491 100644 --- a/src/i18n/locales/ko.json +++ b/src/i18n/locales/ko.json @@ -396,5 +396,17 @@ "10": "10", "25": "25", "50": "50", - "100": "100" + "100": "100", + "minimaxApi": { + "name": "MiniMax API", + "MiniMax-M27": "MiniMax-M2.7", + "MiniMax-M27-highspeed": "MiniMax-M2.7-highspeed", + "MiniMax-M3": "MiniMax-M3", + "alterUrl": "API URL", + "alterUrlPrompt": "Custom API endpoint URL (default: https://api.minimax.io/v1)", + "temperature": "Temperature", + "temperaturePrompt": "What sampling temperature to use, between 0.1 and 1", + "temperature01": "Precise", + "temperature1": "Creative" + } } diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 0f1e4adb0e..8c38a3e86a 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -393,5 +393,17 @@ "10": "10", "25": "25", "50": "50", - "100": "100" + "100": "100", + "minimaxApi": { + "name": "MiniMax API", + "MiniMax-M27": "MiniMax-M2.7", + "MiniMax-M27-highspeed": "MiniMax-M2.7-highspeed", + "MiniMax-M3": "MiniMax-M3", + "alterUrl": "API URL", + "alterUrlPrompt": "Custom API endpoint URL (default: https://api.minimax.io/v1)", + "temperature": "Temperature", + "temperaturePrompt": "What sampling temperature to use, between 0.1 and 1", + "temperature01": "Precise", + "temperature1": "Creative" + } } diff --git a/src/i18n/locales/vi.json b/src/i18n/locales/vi.json index a68bbca440..79b8158e94 100644 --- a/src/i18n/locales/vi.json +++ b/src/i18n/locales/vi.json @@ -372,5 +372,17 @@ "10": "10", "25": "25", "50": "50", - "100": "100" + "100": "100", + "minimaxApi": { + "name": "MiniMax API", + "MiniMax-M27": "MiniMax-M2.7", + "MiniMax-M27-highspeed": "MiniMax-M2.7-highspeed", + "MiniMax-M3": "MiniMax-M3", + "alterUrl": "API URL", + "alterUrlPrompt": "Custom API endpoint URL (default: https://api.minimax.io/v1)", + "temperature": "Temperature", + "temperaturePrompt": "What sampling temperature to use, between 0.1 and 1", + "temperature01": "Precise", + "temperature1": "Creative" + } } diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index e7f3ddf1bf..ce0560c693 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -393,5 +393,17 @@ "10": "10", "25": "25", "50": "50", - "100": "100" + "100": "100", + "minimaxApi": { + "name": "MiniMax API", + "MiniMax-M27": "MiniMax-M2.7", + "MiniMax-M27-highspeed": "MiniMax-M2.7-highspeed", + "MiniMax-M3": "MiniMax-M3", + "alterUrl": "API 地址", + "alterUrlPrompt": "自定义 API 端点地址(默认:https://api.minimax.io/v1)", + "temperature": "温度", + "temperaturePrompt": "采样温度,范围 0.1 到 1", + "temperature01": "精确", + "temperature1": "创意" + } } diff --git a/src/i18n/locales/zhtw.json b/src/i18n/locales/zhtw.json index d727742046..61344f8aa4 100644 --- a/src/i18n/locales/zhtw.json +++ b/src/i18n/locales/zhtw.json @@ -393,5 +393,17 @@ "10": "10", "25": "25", "50": "50", - "100": "100" + "100": "100", + "minimaxApi": { + "name": "MiniMax API", + "MiniMax-M27": "MiniMax-M2.7", + "MiniMax-M27-highspeed": "MiniMax-M2.7-highspeed", + "MiniMax-M3": "MiniMax-M3", + "alterUrl": "API 地址", + "alterUrlPrompt": "自定义 API 端点地址(默认:https://api.minimax.io/v1)", + "temperature": "温度", + "temperaturePrompt": "采样温度,范围 0.1 到 1", + "temperature01": "精确", + "temperature1": "创意" + } } diff --git a/src/store/index.js b/src/store/index.js index c48e1b5b1f..5be2184224 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -126,6 +126,12 @@ export default createStore({ apiKey: "", pastRounds: 5, }, + minimaxApi: { + apiKey: "", + temperature: 1, + pastRounds: 5, + alterUrl: "", + }, currentChatIndex: 0, updateCounter: 0, theme: undefined, @@ -277,6 +283,9 @@ export default createStore({ setXaiApi(state, values) { state.xaiApi = { ...state.xaiApi, ...values }; }, + setMiniMaxApi(state, values) { + state.minimaxApi = { ...state.minimaxApi, ...values }; + }, setLatestPromptIndex(state, promptIndex) { Chats.table.update(state.currentChatIndex, { latestPromptIndex: promptIndex, diff --git a/tests/minimax-integration.test.js b/tests/minimax-integration.test.js new file mode 100644 index 0000000000..0ab032820d --- /dev/null +++ b/tests/minimax-integration.test.js @@ -0,0 +1,282 @@ +/** + * Integration tests for MiniMax API via OpenAI-compatible endpoint. + * + * Requires MINIMAX_API_KEY environment variable to be set. + * + * Run: MINIMAX_API_KEY= node tests/minimax-integration.test.js + */ + +const https = require("https"); + +const API_KEY = process.env.MINIMAX_API_KEY; +const BASE_URL = "https://api.minimax.io/v1"; + +let passed = 0; +let failed = 0; + +function assert(condition, message) { + if (condition) { + console.log(` ✓ ${message}`); + passed++; + } else { + console.error(` ✗ ${message}`); + failed++; + } +} + +function describe(name, fn) { + console.log(`\n${name}`); + return fn(); +} + +function chatCompletion(model, messages, options = {}) { + return new Promise((resolve, reject) => { + const url = new URL(`${BASE_URL}/chat/completions`); + + const body = JSON.stringify({ + model, + messages, + temperature: options.temperature || 1.0, + max_tokens: options.max_tokens || 100, + stream: false, + }); + + const req = https.request( + url, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${API_KEY}`, + }, + }, + (res) => { + let data = ""; + res.on("data", (chunk) => (data += chunk)); + res.on("end", () => { + try { + resolve({ status: res.statusCode, body: JSON.parse(data) }); + } catch (e) { + resolve({ status: res.statusCode, body: data }); + } + }); + }, + ); + + req.on("error", reject); + req.setTimeout(60000, () => { + req.destroy(); + reject(new Error("Request timed out")); + }); + req.write(body); + req.end(); + }); +} + +async function run() { + if (!API_KEY) { + console.error("ERROR: MINIMAX_API_KEY environment variable is not set"); + process.exit(1); + } + + // ─── Test 1: MiniMax-M3 basic completion ───────────────────── + await describe("MiniMax-M3 basic chat completion", async () => { + try { + const result = await chatCompletion("MiniMax-M3", [ + { role: "user", content: "Say hello in exactly one word." }, + ]); + + assert(result.status === 200, `Status code is 200 (got ${result.status})`); + assert(result.body.choices, "Response has choices array"); + assert( + result.body.choices.length > 0, + "Response has at least one choice", + ); + assert( + result.body.choices[0].message, + "First choice has a message", + ); + assert( + typeof result.body.choices[0].message.content === "string", + "Message content is a string", + ); + assert( + result.body.choices[0].message.content.length > 0, + `Got response: "${result.body.choices[0].message.content.substring(0, 50)}"`, + ); + assert( + result.body.model, + `Model in response: ${result.body.model}`, + ); + } catch (err) { + assert(false, `Request failed: ${err.message}`); + } + }); + + // ─── Test 2: MiniMax-M2.7 basic completion ──────────────────── + await describe("MiniMax-M2.7 basic chat completion", async () => { + try { + const result = await chatCompletion("MiniMax-M2.7", [ + { role: "user", content: "Say hello in exactly one word." }, + ]); + + assert(result.status === 200, `Status code is 200 (got ${result.status})`); + assert(result.body.choices, "Response has choices array"); + assert( + result.body.choices.length > 0, + "Response has at least one choice", + ); + assert( + result.body.choices[0].message, + "First choice has a message", + ); + assert( + typeof result.body.choices[0].message.content === "string", + "Message content is a string", + ); + assert( + result.body.choices[0].message.content.length > 0, + `Got response: "${result.body.choices[0].message.content.substring(0, 50)}"`, + ); + assert( + result.body.model, + `Model in response: ${result.body.model}`, + ); + } catch (err) { + assert(false, `Request failed: ${err.message}`); + } + }); + + // ─── Test 3: MiniMax-M2.7-highspeed basic completion ───────── + await describe("MiniMax-M2.7-highspeed basic chat completion", async () => { + try { + const result = await chatCompletion("MiniMax-M2.7-highspeed", [ + { role: "user", content: "What is 3+3? Answer with just the number." }, + ]); + + assert(result.status === 200, `Status code is 200 (got ${result.status})`); + assert(result.body.choices, "Response has choices array"); + assert( + result.body.choices.length > 0, + "Response has at least one choice", + ); + assert( + result.body.choices[0].message.content.length > 0, + `Got response: "${result.body.choices[0].message.content.substring(0, 50)}"`, + ); + } catch (err) { + assert(false, `Request failed: ${err.message}`); + } + }); + + // ─── Test 4: MiniMax-M2.7-highspeed alternate completion ────────── + await describe("MiniMax-M2.7-highspeed alternate basic chat completion", async () => { + try { + const result = await chatCompletion("MiniMax-M2.7-highspeed", [ + { role: "user", content: "What is 2+2? Answer with just the number." }, + ]); + + assert(result.status === 200, `Status code is 200 (got ${result.status})`); + assert(result.body.choices, "Response has choices array"); + assert( + result.body.choices.length > 0, + "Response has at least one choice", + ); + assert( + result.body.choices[0].message.content.length > 0, + `Got response: "${result.body.choices[0].message.content.substring(0, 50)}"`, + ); + } catch (err) { + assert(false, `Request failed: ${err.message}`); + } + }); + + // ─── Test 5: Temperature handling ────────────────────────────── + await describe("Temperature handling", async () => { + try { + // Test with valid temperature (1.0) + const result = await chatCompletion( + "MiniMax-M2.7-highspeed", + [{ role: "user", content: "Hi" }], + { temperature: 1.0, max_tokens: 10 }, + ); + assert( + result.status === 200, + `Temperature 1.0 works (status ${result.status})`, + ); + + // Test with valid low temperature (0.1) + const result2 = await chatCompletion( + "MiniMax-M2.7-highspeed", + [{ role: "user", content: "Hi" }], + { temperature: 0.1, max_tokens: 10 }, + ); + assert( + result2.status === 200, + `Temperature 0.1 works (status ${result2.status})`, + ); + } catch (err) { + assert(false, `Request failed: ${err.message}`); + } + }); + + // ─── Test 6: Multi-turn conversation ─────────────────────────── + await describe("Multi-turn conversation", async () => { + try { + const result = await chatCompletion("MiniMax-M2.7-highspeed", [ + { role: "user", content: "Remember the number 42." }, + { + role: "assistant", + content: "I'll remember the number 42.", + }, + { + role: "user", + content: "What number did I ask you to remember? Reply with just the number.", + }, + ]); + + assert(result.status === 200, `Status code is 200 (got ${result.status})`); + assert( + result.body.choices[0].message.content.includes("42"), + `Response includes "42": "${result.body.choices[0].message.content.substring(0, 100)}"`, + ); + } catch (err) { + assert(false, `Request failed: ${err.message}`); + } + }); + + // ─── Test 7: System message ──────────────────────────────────── + await describe("System message support", async () => { + try { + const result = await chatCompletion("MiniMax-M2.7-highspeed", [ + { + role: "system", + content: "You are a helpful assistant. Always end your response with the word PINEAPPLE.", + }, + { role: "user", content: "Say hi." }, + ]); + + assert(result.status === 200, `Status code is 200 (got ${result.status})`); + assert( + result.body.choices[0].message.content.length > 0, + `Got response with system message: "${result.body.choices[0].message.content.substring(0, 100)}"`, + ); + } catch (err) { + assert(false, `Request failed: ${err.message}`); + } + }); + + // ─── Summary ─────────────────────────────────────────────────── + console.log(`\n${"─".repeat(50)}`); + console.log(`Results: ${passed} passed, ${failed} failed`); + if (failed > 0) { + process.exit(1); + } else { + console.log("All integration tests passed!"); + } +} + +run().catch((err) => { + console.error("Fatal error:", err); + process.exit(1); +}); diff --git a/tests/minimax-unit.test.js b/tests/minimax-unit.test.js new file mode 100644 index 0000000000..bd79776361 --- /dev/null +++ b/tests/minimax-unit.test.js @@ -0,0 +1,349 @@ +/** + * Unit tests for MiniMax provider integration in ChatALL. + * + * Run: node tests/minimax-unit.test.js + */ + +const fs = require("fs"); +const path = require("path"); + +let passed = 0; +let failed = 0; + +function assert(condition, message) { + if (condition) { + console.log(` ✓ ${message}`); + passed++; + } else { + console.error(` ✗ ${message}`); + failed++; + } +} + +function describe(name, fn) { + console.log(`\n${name}`); + fn(); +} + +// ─── Bot files exist ─────────────────────────────────────────────── +describe("Bot files exist", () => { + const botsDir = path.join(__dirname, "..", "src", "bots", "minimax"); + + assert( + fs.existsSync(path.join(botsDir, "MiniMaxAPIBot.js")), + "MiniMaxAPIBot.js exists", + ); + assert( + fs.existsSync(path.join(botsDir, "MiniMaxM27Bot.js")), + "MiniMaxM27Bot.js exists", + ); + assert( + fs.existsSync(path.join(botsDir, "MiniMaxM27HighspeedBot.js")), + "MiniMaxM27HighspeedBot.js exists", + ); + assert( + fs.existsSync(path.join(botsDir, "MiniMaxM3Bot.js")), + "MiniMaxM3Bot.js exists", + ); +}); + +// ─── Bot file contents ──────────────────────────────────────────── +describe("MiniMaxAPIBot.js contents", () => { + const content = fs.readFileSync( + path.join(__dirname, "..", "src", "bots", "minimax", "MiniMaxAPIBot.js"), + "utf-8", + ); + + assert( + content.includes('import { ChatOpenAI } from "@langchain/openai"'), + "Uses ChatOpenAI from @langchain/openai", + ); + assert( + content.includes("https://api.minimax.io/v1"), + "Default base URL is https://api.minimax.io/v1", + ); + assert( + content.includes("minimaxApi.apiKey"), + "Uses minimaxApi.apiKey from store", + ); + assert( + content.includes("minimaxApi.temperature"), + "Uses minimaxApi.temperature from store", + ); + assert( + content.includes("minimaxApi.alterUrl"), + "Supports custom API URL via alterUrl", + ); + assert(content.includes("streaming: true"), "Streaming is enabled"); + assert( + content.includes('static _brandId = "minimaxApi"'), + 'Brand ID is "minimaxApi"', + ); +}); + +describe("MiniMaxM3Bot.js contents", () => { + const content = fs.readFileSync( + path.join(__dirname, "..", "src", "bots", "minimax", "MiniMaxM3Bot.js"), + "utf-8", + ); + + assert( + content.includes('static _model = "MiniMax-M3"'), + 'Model is "MiniMax-M3"', + ); + assert( + content.includes("extends MiniMaxAPIBot"), + "Extends MiniMaxAPIBot", + ); + assert( + content.includes('static _className = "MiniMaxM3Bot"'), + 'Class name is "MiniMaxM3Bot"', + ); +}); + +describe("MiniMaxM27Bot.js contents", () => { + const content = fs.readFileSync( + path.join(__dirname, "..", "src", "bots", "minimax", "MiniMaxM27Bot.js"), + "utf-8", + ); + + assert( + content.includes('static _model = "MiniMax-M2.7"'), + 'Model is "MiniMax-M2.7"', + ); + assert( + content.includes("extends MiniMaxAPIBot"), + "Extends MiniMaxAPIBot", + ); + assert( + content.includes('static _className = "MiniMaxM27Bot"'), + 'Class name is "MiniMaxM27Bot"', + ); +}); + +describe("MiniMaxM27HighspeedBot.js contents", () => { + const content = fs.readFileSync( + path.join( + __dirname, + "..", + "src", + "bots", + "minimax", + "MiniMaxM27HighspeedBot.js", + ), + "utf-8", + ); + + assert( + content.includes('static _model = "MiniMax-M2.7-highspeed"'), + 'Model is "MiniMax-M2.7-highspeed"', + ); + assert( + content.includes("extends MiniMaxAPIBot"), + "Extends MiniMaxAPIBot", + ); + assert( + content.includes('static _className = "MiniMaxM27HighspeedBot"'), + 'Class name is "MiniMaxM27HighspeedBot"', + ); +}); + +// ─── Settings component ─────────────────────────────────────────── +describe("Settings component", () => { + const settingsPath = path.join( + __dirname, + "..", + "src", + "components", + "BotSettings", + "MiniMaxAPIBotSettings.vue", + ); + assert(fs.existsSync(settingsPath), "MiniMaxAPIBotSettings.vue exists"); + + const content = fs.readFileSync(settingsPath, "utf-8"); + assert( + content.includes("CommonBotSettings"), + "Uses CommonBotSettings component", + ); + assert( + content.includes("apiKey"), + "Has API key setting", + ); + assert( + content.includes("temperature"), + "Has temperature setting", + ); + assert( + content.includes("pastRounds"), + "Has pastRounds setting", + ); + assert( + content.includes("alterUrl"), + "Has alterUrl setting for custom API endpoint", + ); +}); + +// ─── Store configuration ────────────────────────────────────────── +describe("Store configuration", () => { + const storeContent = fs.readFileSync( + path.join(__dirname, "..", "src", "store", "index.js"), + "utf-8", + ); + + assert( + storeContent.includes("minimaxApi:"), + "minimaxApi state exists in store", + ); + assert( + storeContent.includes("setMiniMaxApi"), + "setMiniMaxApi mutation exists", + ); + + // Check default values + const stateMatch = storeContent.match( + /minimaxApi:\s*\{[^}]+\}/s, + ); + if (stateMatch) { + const stateBlock = stateMatch[0]; + assert( + stateBlock.includes('apiKey: ""'), + "Default apiKey is empty string", + ); + assert( + stateBlock.includes("temperature: 1"), + "Default temperature is 1", + ); + assert( + stateBlock.includes("pastRounds: 5"), + "Default pastRounds is 5", + ); + assert( + stateBlock.includes('alterUrl: ""'), + "Default alterUrl is empty string", + ); + } else { + assert(false, "Could not parse minimaxApi state block"); + } +}); + +// ─── Bot registration ───────────────────────────────────────────── +describe("Bot registration in index.js", () => { + const indexContent = fs.readFileSync( + path.join(__dirname, "..", "src", "bots", "index.js"), + "utf-8", + ); + + assert( + indexContent.includes( + 'import MiniMaxM27Bot from "./minimax/MiniMaxM27Bot"', + ), + "MiniMaxM27Bot is imported", + ); + assert( + indexContent.includes( + 'import MiniMaxM27HighspeedBot from "./minimax/MiniMaxM27HighspeedBot"', + ), + "MiniMaxM27HighspeedBot is imported", + ); + assert( + indexContent.includes( + 'import MiniMaxM3Bot from "./minimax/MiniMaxM3Bot"', + ), + "MiniMaxM3Bot is imported", + ); + assert( + indexContent.includes("MiniMaxM27Bot.getInstance()"), + "MiniMaxM27Bot is registered in all array", + ); + assert( + indexContent.includes("MiniMaxM27HighspeedBot.getInstance()"), + "MiniMaxM27HighspeedBot is registered in all array", + ); + assert( + indexContent.includes("MiniMaxM3Bot.getInstance()"), + "MiniMaxM3Bot is registered in all array", + ); + + // Check API tag + assert( + indexContent.includes('bots.getBotByClassName("MiniMaxM27Bot")'), + "MiniMaxM27Bot is in botTags", + ); + assert( + indexContent.includes('bots.getBotByClassName("MiniMaxM27HighspeedBot")'), + "MiniMaxM27HighspeedBot is in botTags", + ); + assert( + indexContent.includes('bots.getBotByClassName("MiniMaxM3Bot")'), + "MiniMaxM3Bot is in botTags", + ); +}); + +// ─── i18n entries ───────────────────────────────────────────────── +describe("i18n entries", () => { + const enLocale = JSON.parse( + fs.readFileSync( + path.join(__dirname, "..", "src", "i18n", "locales", "en.json"), + "utf-8", + ), + ); + + assert(enLocale.minimaxApi, "minimaxApi key exists in en.json"); + assert( + enLocale.minimaxApi.name === "MiniMax API", + 'name is "MiniMax API"', + ); + assert( + enLocale.minimaxApi["MiniMax-M27"] === "MiniMax-M2.7", + "MiniMax-M2.7 model name is correct", + ); + assert( + enLocale.minimaxApi["MiniMax-M27-highspeed"] === "MiniMax-M2.7-highspeed", + "MiniMax-M2.7-highspeed model name is correct", + ); + assert( + enLocale.minimaxApi["MiniMax-M3"] === "MiniMax-M3", + "MiniMax-M3 model name is correct", + ); + + // Check Chinese locale + const zhLocale = JSON.parse( + fs.readFileSync( + path.join(__dirname, "..", "src", "i18n", "locales", "zh.json"), + "utf-8", + ), + ); + assert(zhLocale.minimaxApi, "minimaxApi key exists in zh.json"); +}); + +// ─── Logo files ─────────────────────────────────────────────────── +describe("Logo files", () => { + const logosDir = path.join(__dirname, "..", "public", "bots"); + + assert( + fs.existsSync(path.join(logosDir, "minimax-logo.svg")), + "minimax-logo.svg exists", + ); +}); + +// ─── README entries ─────────────────────────────────────────────── +describe("README entries", () => { + const readme = fs.readFileSync( + path.join(__dirname, "..", "README.md"), + "utf-8", + ); + assert(readme.includes("MiniMax"), "MiniMax is mentioned in README.md"); + assert( + readme.includes("https://www.minimax.io/"), + "MiniMax link is in README.md", + ); +}); + +// ─── Summary ────────────────────────────────────────────────────── +console.log(`\n${"─".repeat(50)}`); +console.log(`Results: ${passed} passed, ${failed} failed`); +if (failed > 0) { + process.exit(1); +} else { + console.log("All unit tests passed!"); +}