Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions src/app/core/setting/config.spec.mjs
Original file line number Diff line number Diff line change
@@ -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')
})
7 changes: 7 additions & 0 deletions src/app/core/setting/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
130 changes: 130 additions & 0 deletions src/lib/ai/minimax-integration.spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
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 M2.7 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.7',
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 M2.7 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.7',
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.7-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.7-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, '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 () => {
// 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.7',
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')
})