diff --git a/README.en.md b/README.en.md index c1cf0ed..92ba79c 100644 --- a/README.en.md +++ b/README.en.md @@ -146,7 +146,7 @@ docker run -d --name clawpanel --restart unless-stopped \ ## Quick Start 1. **Initial Setup** — First launch auto-detects Node.js, Git, OpenClaw. One-click install if missing. -2. **Configure Models** — Add AI providers (DeepSeek, OpenAI, Ollama, etc.) with API keys. Test connectivity. +2. **Configure Models** — Add AI providers (DeepSeek, MiniMax, OpenAI, Ollama, etc.) with API keys. Test connectivity. 3. **Start Gateway** — Go to Service Management, click Start. Green status = ready. 4. **Start Chatting** — Go to Live Chat, select model, start conversation with streaming & Markdown. diff --git a/README.md b/README.md index 27137a7..500aea0 100644 --- a/README.md +++ b/README.md @@ -489,6 +489,7 @@ Web 版功能与桌面版一致,后端通过 `scripts/dev-api.js` 调用本机 | 服务商 | 获取 API Key | |--------|-------------| | DeepSeek | [platform.deepseek.com](https://platform.deepseek.com/) | +| MiniMax | [platform.minimaxi.com](https://platform.minimaxi.com/) | | OpenAI | [platform.openai.com](https://platform.openai.com/) | | 阿里通义 | [dashscope.console.aliyun.com](https://dashscope.console.aliyun.com/) | | Ollama(本地) | 免费,无需 Key,安装后自动检测 | diff --git a/src/lib/model-presets.js b/src/lib/model-presets.js index 55c2205..95d6de6 100644 --- a/src/lib/model-presets.js +++ b/src/lib/model-presets.js @@ -20,7 +20,7 @@ export const PROVIDER_PRESETS = [ { key: 'volcengine', label: '火山引擎', baseUrl: 'https://ark.cn-beijing.volces.com/api/v3', api: 'openai-completions', site: 'https://volcengine.com/L/Ph1OP5I3_GY', desc: '字节跳动旗下云平台,支持豆包等模型' }, { key: 'aliyun', label: '阿里云百炼', baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', api: 'openai-completions', site: 'https://www.aliyun.com/benefit/ai/aistar?userCode=keahn2zr&clubBiz=subTask..12435175..10263..', desc: '阿里云 AI 大模型平台,支持通义千问全系列' }, { key: 'zhipu', label: '智谱 AI', baseUrl: 'https://open.bigmodel.cn/api/paas/v4', api: 'openai-completions', site: 'https://www.bigmodel.cn/glm-coding?ic=3F6F9XYKTS', desc: '国产大模型领军企业,支持 GLM-4 全系列' }, - { key: 'minimax', label: 'MiniMax', baseUrl: 'https://api.minimax.chat/v1', api: 'openai-completions', site: 'https://platform.minimaxi.com/subscribe/coding-plan?code=7pUc5oLo4K&source=link', desc: '国产多模态大模型,支持 MiniMax-Text 系列' }, + { key: 'minimax', label: 'MiniMax', baseUrl: 'https://api.minimax.io/v1', api: 'openai-completions', site: 'https://platform.minimaxi.com/', desc: '国产多模态大模型,支持 MiniMax-M2.7 / M2.5 系列,兼容 OpenAI 接口' }, { key: 'openai', label: 'OpenAI 官方', baseUrl: 'https://api.openai.com/v1', api: 'openai-completions' }, { key: 'anthropic', label: 'Anthropic 官方', baseUrl: 'https://api.anthropic.com', api: 'anthropic-messages' }, { key: 'deepseek', label: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1', api: 'openai-completions' }, @@ -70,6 +70,12 @@ export const MODEL_PRESETS = { { id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', contextWindow: 1000000, reasoning: true }, { id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash', contextWindow: 1000000 }, ], + minimax: [ + { id: 'MiniMax-M2.7', name: 'MiniMax M2.7', contextWindow: 1000000 }, + { id: 'MiniMax-M2.7-highspeed', name: 'MiniMax M2.7 Highspeed', contextWindow: 1000000 }, + { id: 'MiniMax-M2.5', name: 'MiniMax M2.5', contextWindow: 204000 }, + { id: 'MiniMax-M2.5-highspeed', name: 'MiniMax M2.5 Highspeed', contextWindow: 204000 }, + ], ollama: [ { id: 'qwen2.5:7b', name: 'Qwen 2.5 7B', contextWindow: 32768 }, { id: 'llama3.2', name: 'Llama 3.2', contextWindow: 8192 }, diff --git a/tests/model-presets.test.js b/tests/model-presets.test.js new file mode 100644 index 0000000..ea8cdd3 --- /dev/null +++ b/tests/model-presets.test.js @@ -0,0 +1,104 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { + API_TYPES, + PROVIDER_PRESETS, + MODEL_PRESETS, +} from '../src/lib/model-presets.js' + +// ===== Provider Presets ===== + +test('PROVIDER_PRESETS contains MiniMax entry', () => { + const minimax = PROVIDER_PRESETS.find(p => p.key === 'minimax') + assert.ok(minimax, 'MiniMax provider preset should exist') + assert.equal(minimax.label, 'MiniMax') + assert.equal(minimax.api, 'openai-completions') +}) + +test('MiniMax provider preset uses correct API base URL', () => { + const minimax = PROVIDER_PRESETS.find(p => p.key === 'minimax') + assert.equal(minimax.baseUrl, 'https://api.minimax.io/v1') +}) + +test('MiniMax provider preset has site and description', () => { + const minimax = PROVIDER_PRESETS.find(p => p.key === 'minimax') + assert.ok(minimax.site, 'MiniMax should have a site URL') + assert.ok(minimax.desc, 'MiniMax should have a description') +}) + +test('all provider presets have required fields', () => { + for (const p of PROVIDER_PRESETS) { + assert.ok(p.key, `preset missing key`) + assert.ok(p.label, `preset ${p.key} missing label`) + assert.ok(p.baseUrl, `preset ${p.key} missing baseUrl`) + assert.ok(p.api, `preset ${p.key} missing api type`) + const valid = API_TYPES.map(t => t.value) + assert.ok(valid.includes(p.api), `preset ${p.key} has invalid api type: ${p.api}`) + } +}) + +test('no duplicate provider preset keys', () => { + const keys = PROVIDER_PRESETS.map(p => p.key) + const unique = new Set(keys) + assert.equal(keys.length, unique.size, 'provider preset keys must be unique') +}) + +// ===== Model Presets ===== + +test('MODEL_PRESETS contains MiniMax models', () => { + assert.ok(MODEL_PRESETS.minimax, 'MODEL_PRESETS should have a minimax key') + assert.ok(Array.isArray(MODEL_PRESETS.minimax), 'minimax presets should be an array') + assert.ok(MODEL_PRESETS.minimax.length >= 2, 'should have at least 2 MiniMax models') +}) + +test('MiniMax model presets include M2.7 and M2.5 variants', () => { + const ids = MODEL_PRESETS.minimax.map(m => m.id) + assert.ok(ids.includes('MiniMax-M2.7'), 'should include MiniMax-M2.7') + assert.ok(ids.includes('MiniMax-M2.7-highspeed'), 'should include MiniMax-M2.7-highspeed') + assert.ok(ids.includes('MiniMax-M2.5'), 'should include MiniMax-M2.5') + assert.ok(ids.includes('MiniMax-M2.5-highspeed'), 'should include MiniMax-M2.5-highspeed') +}) + +test('MiniMax model presets have required fields', () => { + for (const m of MODEL_PRESETS.minimax) { + assert.ok(m.id, `model missing id`) + assert.ok(m.name, `model ${m.id} missing name`) + assert.ok(typeof m.contextWindow === 'number' && m.contextWindow > 0, + `model ${m.id} should have a positive contextWindow`) + } +}) + +test('MiniMax M2.7 models have 1M context window', () => { + const m27 = MODEL_PRESETS.minimax.find(m => m.id === 'MiniMax-M2.7') + assert.equal(m27.contextWindow, 1000000) + const m27hs = MODEL_PRESETS.minimax.find(m => m.id === 'MiniMax-M2.7-highspeed') + assert.equal(m27hs.contextWindow, 1000000) +}) + +test('MiniMax M2.5 models have 204K context window', () => { + const m25 = MODEL_PRESETS.minimax.find(m => m.id === 'MiniMax-M2.5') + assert.equal(m25.contextWindow, 204000) + const m25hs = MODEL_PRESETS.minimax.find(m => m.id === 'MiniMax-M2.5-highspeed') + assert.equal(m25hs.contextWindow, 204000) +}) + +test('all model preset groups have valid structure', () => { + for (const [group, models] of Object.entries(MODEL_PRESETS)) { + assert.ok(Array.isArray(models), `${group} should be an array`) + for (const m of models) { + assert.ok(m.id, `model in ${group} missing id`) + assert.ok(m.name, `model ${m.id} in ${group} missing name`) + } + } +}) + +// ===== Integration: Provider ↔ Model Presets alignment ===== + +test('each MODEL_PRESETS group has a matching PROVIDER_PRESETS entry', () => { + const providerKeys = new Set(PROVIDER_PRESETS.map(p => p.key)) + for (const group of Object.keys(MODEL_PRESETS)) { + assert.ok(providerKeys.has(group), + `MODEL_PRESETS group "${group}" has no matching PROVIDER_PRESETS entry`) + } +})