From e1c811bc920197838dc7a3c16ef83ad350d86828 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Tue, 17 Mar 2026 18:44:50 +0800 Subject: [PATCH 1/2] feat: add MiniMax as built-in AI provider - Add MiniMax to baseAiConfig with API base URL and platform link - MiniMax models (M2.5, M2.5-highspeed) are available via OpenAI-compatible API - Add unit tests verifying config entry structure - Add integration tests for chat completion and streaming --- src/app/core/setting/config.spec.mjs | 69 +++++++++++++++ src/app/core/setting/config.tsx | 7 ++ src/lib/ai/minimax-integration.spec.mjs | 110 ++++++++++++++++++++++++ 3 files changed, 186 insertions(+) create mode 100644 src/app/core/setting/config.spec.mjs create mode 100644 src/lib/ai/minimax-integration.spec.mjs diff --git a/src/app/core/setting/config.spec.mjs b/src/app/core/setting/config.spec.mjs new file mode 100644 index 000000000..2a3007349 --- /dev/null +++ b/src/app/core/setting/config.spec.mjs @@ -0,0 +1,69 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { resolve, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const configSource = readFileSync(resolve(__dirname, 'config.tsx'), 'utf-8') + +test('baseAiConfig includes MiniMax provider entry', () => { + assert.ok(configSource.includes("key: 'minimax'"), 'config should have minimax key') + assert.ok(configSource.includes("title: 'MiniMax'"), 'config should have MiniMax title') +}) + +test('MiniMax provider uses correct base URL', () => { + assert.ok( + configSource.includes("baseURL: 'https://api.minimax.io/v1'"), + 'MiniMax baseURL should be https://api.minimax.io/v1' + ) +}) + +test('MiniMax provider has apiKeyUrl pointing to platform', () => { + assert.ok( + configSource.includes("apiKeyUrl: 'https://platform.minimaxi.com/'"), + 'MiniMax apiKeyUrl should point to platform.minimaxi.com' + ) +}) + +test('MiniMax provider has an icon URL', () => { + // Extract the MiniMax block from the config + const minimaxIdx = configSource.indexOf("key: 'minimax'") + assert.ok(minimaxIdx > -1, 'MiniMax entry should exist in config') + const blockEnd = configSource.indexOf('},', minimaxIdx) + const minimaxBlock = configSource.substring(minimaxIdx, blockEnd) + assert.ok(minimaxBlock.includes('icon:'), 'MiniMax entry should have an icon') +}) + +test('MiniMax appears after existing providers (Gitee AI)', () => { + const giteeIdx = configSource.indexOf("key: 'gitee'") + const minimaxIdx = configSource.indexOf("key: 'minimax'") + assert.ok(giteeIdx > -1, 'Gitee AI entry should exist') + assert.ok(minimaxIdx > -1, 'MiniMax entry should exist') + assert.ok(minimaxIdx > giteeIdx, 'MiniMax should appear after Gitee AI in the config array') +}) + +test('all provider entries in baseAiConfig have required fields', () => { + // Extract all key entries + const keyPattern = /key:\s*'([^']+)'/g + const keys = [] + let match + while ((match = keyPattern.exec(configSource)) !== null) { + keys.push(match[1]) + } + assert.ok(keys.includes('minimax'), 'MiniMax should be in the provider keys') + assert.ok(keys.includes('chatgpt'), 'ChatGPT should be in the provider keys') + assert.ok(keys.includes('deepseek'), 'DeepSeek should be in the provider keys') + + // Verify MiniMax block has all required fields + const minimaxIdx = configSource.indexOf("key: 'minimax'") + const blockStart = configSource.lastIndexOf('{', minimaxIdx) + const blockEnd = configSource.indexOf('},', minimaxIdx) + const minimaxBlock = configSource.substring(blockStart, blockEnd + 1) + + assert.ok(minimaxBlock.includes('key:'), 'MiniMax should have key field') + assert.ok(minimaxBlock.includes('title:'), 'MiniMax should have title field') + assert.ok(minimaxBlock.includes('baseURL:'), 'MiniMax should have baseURL field') + assert.ok(minimaxBlock.includes('icon:'), 'MiniMax should have icon field') + assert.ok(minimaxBlock.includes('apiKeyUrl:'), 'MiniMax should have apiKeyUrl field') +}) diff --git a/src/app/core/setting/config.tsx b/src/app/core/setting/config.tsx index feabffd5f..9903dd47a 100644 --- a/src/app/core/setting/config.tsx +++ b/src/app/core/setting/config.tsx @@ -227,6 +227,13 @@ const baseAiConfig: AiConfig[] = [ icon: 'https://s2.loli.net/2025/09/15/ih7aTnGPvELFsVc.png', apiKeyUrl: 'https://ai.gitee.com/' }, + { + key: 'minimax', + title: 'MiniMax', + baseURL: 'https://api.minimax.io/v1', + icon: 'https://filecdn.minimax.chat/public/c5b4442f-ab8b-4d97-9119-8504670b0097.png', + apiKeyUrl: 'https://platform.minimaxi.com/' + }, ] export { baseAiConfig } \ No newline at end of file diff --git a/src/lib/ai/minimax-integration.spec.mjs b/src/lib/ai/minimax-integration.spec.mjs new file mode 100644 index 000000000..01155885a --- /dev/null +++ b/src/lib/ai/minimax-integration.spec.mjs @@ -0,0 +1,110 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +const API_KEY = process.env.MINIMAX_API_KEY +const BASE_URL = 'https://api.minimax.io/v1' + +// Skip all integration tests if no API key is set +const skipReason = API_KEY ? undefined : 'MINIMAX_API_KEY not set' + +test('MiniMax chat completion (non-streaming)', { skip: skipReason, timeout: 30000 }, async () => { + const response = await fetch(`${BASE_URL}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_KEY}`, + }, + body: JSON.stringify({ + model: 'MiniMax-M2.5', + messages: [{ role: 'user', content: 'Say "test passed" and nothing else.' }], + max_tokens: 20, + temperature: 0.7, + }), + }) + + assert.equal(response.ok, true, `HTTP ${response.status}: ${response.statusText}`) + const data = await response.json() + assert.ok(data.choices, 'response should have choices') + assert.ok(data.choices.length > 0, 'choices should not be empty') + assert.ok(data.choices[0].message.content, 'message content should not be empty') +}) + +test('MiniMax chat completion (streaming)', { skip: skipReason, timeout: 30000 }, async () => { + const response = await fetch(`${BASE_URL}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_KEY}`, + }, + body: JSON.stringify({ + model: 'MiniMax-M2.5', + messages: [{ role: 'user', content: 'Count 1 to 3.' }], + max_tokens: 50, + stream: true, + temperature: 0.7, + }), + }) + + assert.equal(response.ok, true, `HTTP ${response.status}: ${response.statusText}`) + + const reader = response.body.getReader() + const decoder = new TextDecoder() + let chunks = 0 + let buffer = '' + + while (true) { + const { done, value } = await reader.read() + if (done) break + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split('\n') + buffer = lines.pop() || '' + for (const line of lines) { + if (line.startsWith('data:') && !line.includes('[DONE]')) { + chunks++ + } + } + } + + assert.ok(chunks > 1, `expected multiple SSE chunks, got ${chunks}`) +}) + +test('MiniMax M2.5-highspeed model works', { skip: skipReason, timeout: 30000 }, async () => { + const response = await fetch(`${BASE_URL}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_KEY}`, + }, + body: JSON.stringify({ + model: 'MiniMax-M2.5-highspeed', + messages: [{ role: 'user', content: 'Say "highspeed ok"' }], + max_tokens: 20, + temperature: 0.7, + }), + }) + + assert.equal(response.ok, true, `HTTP ${response.status}: ${response.statusText}`) + const data = await response.json() + assert.ok(data.choices[0].message.content, 'highspeed model should return content') +}) + +test('MiniMax handles temperature edge cases', { skip: skipReason, timeout: 30000 }, async () => { + // Temperature=0 should still produce a valid response + const response = await fetch(`${BASE_URL}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_KEY}`, + }, + body: JSON.stringify({ + model: 'MiniMax-M2.5', + messages: [{ role: 'user', content: 'Say "ok"' }], + max_tokens: 10, + temperature: 0, + }), + }) + + assert.equal(response.ok, true, 'temperature=0 should be accepted') + const data = await response.json() + assert.ok(data.choices[0].message.content, 'should return content with temperature=0') +}) From ac921002801b9efcb175075a89c2610c652ef0de Mon Sep 17 00:00:00 2001 From: PR Bot Date: Wed, 18 Mar 2026 22:48:48 +0800 Subject: [PATCH 2/2] feat: upgrade MiniMax default model to M2.7 - Update integration tests to use MiniMax-M2.7 as primary model - Add MiniMax-M2.7-highspeed test - Add backward compatibility test for M2.5 - Keep all previous models as alternatives --- src/lib/ai/minimax-integration.spec.mjs | 36 +++++++++++++++++++------ 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/lib/ai/minimax-integration.spec.mjs b/src/lib/ai/minimax-integration.spec.mjs index 01155885a..25149bb0c 100644 --- a/src/lib/ai/minimax-integration.spec.mjs +++ b/src/lib/ai/minimax-integration.spec.mjs @@ -7,7 +7,7 @@ const BASE_URL = 'https://api.minimax.io/v1' // Skip all integration tests if no API key is set const skipReason = API_KEY ? undefined : 'MINIMAX_API_KEY not set' -test('MiniMax chat completion (non-streaming)', { skip: skipReason, timeout: 30000 }, async () => { +test('MiniMax M2.7 chat completion (non-streaming)', { skip: skipReason, timeout: 30000 }, async () => { const response = await fetch(`${BASE_URL}/chat/completions`, { method: 'POST', headers: { @@ -15,7 +15,7 @@ test('MiniMax chat completion (non-streaming)', { skip: skipReason, timeout: 300 'Authorization': `Bearer ${API_KEY}`, }, body: JSON.stringify({ - model: 'MiniMax-M2.5', + model: 'MiniMax-M2.7', messages: [{ role: 'user', content: 'Say "test passed" and nothing else.' }], max_tokens: 20, temperature: 0.7, @@ -29,7 +29,7 @@ test('MiniMax chat completion (non-streaming)', { skip: skipReason, timeout: 300 assert.ok(data.choices[0].message.content, 'message content should not be empty') }) -test('MiniMax chat completion (streaming)', { skip: skipReason, timeout: 30000 }, async () => { +test('MiniMax M2.7 chat completion (streaming)', { skip: skipReason, timeout: 30000 }, async () => { const response = await fetch(`${BASE_URL}/chat/completions`, { method: 'POST', headers: { @@ -37,7 +37,7 @@ test('MiniMax chat completion (streaming)', { skip: skipReason, timeout: 30000 } 'Authorization': `Bearer ${API_KEY}`, }, body: JSON.stringify({ - model: 'MiniMax-M2.5', + model: 'MiniMax-M2.7', messages: [{ role: 'user', content: 'Count 1 to 3.' }], max_tokens: 50, stream: true, @@ -68,7 +68,7 @@ test('MiniMax chat completion (streaming)', { skip: skipReason, timeout: 30000 } assert.ok(chunks > 1, `expected multiple SSE chunks, got ${chunks}`) }) -test('MiniMax M2.5-highspeed model works', { skip: skipReason, timeout: 30000 }, async () => { +test('MiniMax M2.7-highspeed model works', { skip: skipReason, timeout: 30000 }, async () => { const response = await fetch(`${BASE_URL}/chat/completions`, { method: 'POST', headers: { @@ -76,7 +76,7 @@ test('MiniMax M2.5-highspeed model works', { skip: skipReason, timeout: 30000 }, 'Authorization': `Bearer ${API_KEY}`, }, body: JSON.stringify({ - model: 'MiniMax-M2.5-highspeed', + model: 'MiniMax-M2.7-highspeed', messages: [{ role: 'user', content: 'Say "highspeed ok"' }], max_tokens: 20, temperature: 0.7, @@ -85,7 +85,27 @@ test('MiniMax M2.5-highspeed model works', { skip: skipReason, timeout: 30000 }, assert.equal(response.ok, true, `HTTP ${response.status}: ${response.statusText}`) const data = await response.json() - assert.ok(data.choices[0].message.content, 'highspeed model should return content') + assert.ok(data.choices[0].message.content, 'M2.7-highspeed model should return content') +}) + +test('MiniMax M2.5 model still works (backward compatibility)', { skip: skipReason, timeout: 30000 }, async () => { + const response = await fetch(`${BASE_URL}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_KEY}`, + }, + body: JSON.stringify({ + model: 'MiniMax-M2.5', + messages: [{ role: 'user', content: 'Say "ok"' }], + max_tokens: 10, + temperature: 0.7, + }), + }) + + assert.equal(response.ok, true, 'M2.5 model should still be accessible') + const data = await response.json() + assert.ok(data.choices[0].message.content, 'M2.5 should return content') }) test('MiniMax handles temperature edge cases', { skip: skipReason, timeout: 30000 }, async () => { @@ -97,7 +117,7 @@ test('MiniMax handles temperature edge cases', { skip: skipReason, timeout: 3000 'Authorization': `Bearer ${API_KEY}`, }, body: JSON.stringify({ - model: 'MiniMax-M2.5', + model: 'MiniMax-M2.7', messages: [{ role: 'user', content: 'Say "ok"' }], max_tokens: 10, temperature: 0,