Skip to content

Commit 7bc194e

Browse files
author
PR Bot
committed
feat: add MiniMax as first-class LLM provider
Add MiniMax AI (https://www.minimax.io/) as a configurable LLM provider alongside Anthropic. MiniMax provides an OpenAI-compatible API with models like MiniMax-M2.7 and MiniMax-M2.5-highspeed. Changes: - Add /api/user/config/minimax GET/POST endpoints for config management - Add MiniMax tab to Settings dialog with API key, base URL, and model - Update loadEnvVarsForSandbox to inject MINIMAX_API_KEY, MINIMAX_BASE_URL, and MINIMAX_MODEL environment variables into sandboxes - Add MiniMax to .env.template with default base URL - Add vitest config and unit/integration tests for MiniMax provider - Update README acknowledgments
1 parent 5cfc59a commit 7bc194e

9 files changed

Lines changed: 965 additions & 6 deletions

File tree

.env.template

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ RUNTIME_IMAGE=""
3434
AIPROXY_ENDPOINT=""
3535
ANTHROPIC_BASE_URL=""
3636

37+
# MiniMax (OpenAI-compatible API)
38+
MINIMAX_BASE_URL="https://api.minimax.io/v1"
39+
MINIMAX_MODEL=""
40+
3741
# Log
3842
LOG_LEVEL="info"
3943

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ See [CHANGELOG.md](CHANGELOG.md) for release history.
208208
## Acknowledgments
209209

210210
- [Anthropic](https://www.anthropic.com/) for Claude Code
211+
- [MiniMax](https://www.minimax.io/) for MiniMax AI models (M2.7, M2.5-highspeed)
211212
- [Sealos](https://sealos.io/) for Kubernetes platform
212213
- [ttyd](https://github.com/tsl0922/ttyd) for web terminal
213214
- [FileBrowser](https://github.com/filebrowser/filebrowser) for file management
@@ -225,5 +226,5 @@ See [CHANGELOG.md](CHANGELOG.md) for release history.
225226

226227
<div align="center">
227228
<strong>100% AI-generated code.</strong> Prompted by [@fanux](https://github.com/fanux).
228-
<br>Powered by Claude Code, with models from Anthropic (Sonnet, Opus), Google (Gemini), Zhipu AI (GLM), and Moonshot (Kimi).
229+
<br>Powered by Claude Code, with models from Anthropic (Sonnet, Opus), Google (Gemini), MiniMax (M2.7), Zhipu AI (GLM), and Moonshot (Kimi).
229230
</div>
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/**
2+
* Unit tests for MiniMax Configuration API
3+
*
4+
* Tests the GET and POST endpoints at /api/user/config/minimax
5+
*/
6+
7+
import { describe, expect, it, vi, beforeEach } from 'vitest'
8+
9+
// Mock dependencies
10+
vi.mock('@/lib/db', () => ({
11+
prisma: {
12+
userConfig: {
13+
findMany: vi.fn(),
14+
upsert: vi.fn(),
15+
deleteMany: vi.fn(),
16+
},
17+
$transaction: vi.fn((fn: (tx: unknown) => Promise<void>) =>
18+
fn({
19+
userConfig: {
20+
upsert: vi.fn(),
21+
deleteMany: vi.fn(),
22+
},
23+
})
24+
),
25+
},
26+
}))
27+
28+
vi.mock('@/lib/auth', () => ({
29+
auth: vi.fn(),
30+
}))
31+
32+
vi.mock('@/lib/logger', () => ({
33+
logger: {
34+
child: () => ({
35+
info: vi.fn(),
36+
error: vi.fn(),
37+
warn: vi.fn(),
38+
debug: vi.fn(),
39+
}),
40+
},
41+
}))
42+
43+
// Mock withAuth to pass through session
44+
vi.mock('@/lib/api-auth', () => ({
45+
withAuth: (handler: Function) => {
46+
return async (req: Request) => {
47+
const session = { user: { id: 'test-user-id' } }
48+
return handler(req, { params: Promise.resolve({}) }, session)
49+
}
50+
},
51+
}))
52+
53+
import { prisma } from '@/lib/db'
54+
55+
describe('GET /api/user/config/minimax', () => {
56+
beforeEach(() => {
57+
vi.clearAllMocks()
58+
})
59+
60+
it('should return MiniMax configuration when configs exist', async () => {
61+
const mockConfigs = [
62+
{ key: 'MINIMAX_API_KEY', value: 'test-api-key' },
63+
{ key: 'MINIMAX_API', value: 'https://api.minimax.io/v1' },
64+
{ key: 'MINIMAX_MODEL', value: 'MiniMax-M2.7' },
65+
]
66+
vi.mocked(prisma.userConfig.findMany).mockResolvedValue(mockConfigs as never)
67+
68+
const { GET } = await import('@/app/api/user/config/minimax/route')
69+
const req = new Request('http://localhost/api/user/config/minimax')
70+
const response = await GET(req as never, { params: Promise.resolve({}) })
71+
const data = await response.json()
72+
73+
expect(data.apiKey).toBe('test-api-key')
74+
expect(data.apiBaseUrl).toBe('https://api.minimax.io/v1')
75+
expect(data.model).toBe('MiniMax-M2.7')
76+
})
77+
78+
it('should return nulls when no configs exist', async () => {
79+
vi.mocked(prisma.userConfig.findMany).mockResolvedValue([] as never)
80+
81+
const { GET } = await import('@/app/api/user/config/minimax/route')
82+
const req = new Request('http://localhost/api/user/config/minimax')
83+
const response = await GET(req as never, { params: Promise.resolve({}) })
84+
const data = await response.json()
85+
86+
expect(data.apiKey).toBeNull()
87+
expect(data.apiBaseUrl).toBeNull()
88+
expect(data.model).toBeNull()
89+
})
90+
91+
it('should return 500 on database error', async () => {
92+
vi.mocked(prisma.userConfig.findMany).mockRejectedValue(new Error('DB error') as never)
93+
94+
const { GET } = await import('@/app/api/user/config/minimax/route')
95+
const req = new Request('http://localhost/api/user/config/minimax')
96+
const response = await GET(req as never, { params: Promise.resolve({}) })
97+
98+
expect(response.status).toBe(500)
99+
const data = await response.json()
100+
expect(data.error).toBe('Failed to fetch MiniMax configuration')
101+
})
102+
})
103+
104+
describe('POST /api/user/config/minimax', () => {
105+
beforeEach(() => {
106+
vi.clearAllMocks()
107+
})
108+
109+
it('should save MiniMax configuration successfully', async () => {
110+
const { POST } = await import('@/app/api/user/config/minimax/route')
111+
const req = new Request('http://localhost/api/user/config/minimax', {
112+
method: 'POST',
113+
body: JSON.stringify({
114+
apiBaseUrl: 'https://api.minimax.io/v1',
115+
apiKey: 'test-api-key',
116+
model: 'MiniMax-M2.7',
117+
}),
118+
headers: { 'Content-Type': 'application/json' },
119+
})
120+
const response = await POST(req as never, { params: Promise.resolve({}) })
121+
const data = await response.json()
122+
123+
expect(data.success).toBe(true)
124+
expect(data.message).toBe('MiniMax configuration saved successfully')
125+
})
126+
127+
it('should reject missing API base URL', async () => {
128+
const { POST } = await import('@/app/api/user/config/minimax/route')
129+
const req = new Request('http://localhost/api/user/config/minimax', {
130+
method: 'POST',
131+
body: JSON.stringify({
132+
apiBaseUrl: '',
133+
apiKey: 'test-api-key',
134+
}),
135+
headers: { 'Content-Type': 'application/json' },
136+
})
137+
const response = await POST(req as never, { params: Promise.resolve({}) })
138+
139+
expect(response.status).toBe(400)
140+
const data = await response.json()
141+
expect(data.error).toBe('API base URL is required')
142+
})
143+
144+
it('should reject missing API key', async () => {
145+
const { POST } = await import('@/app/api/user/config/minimax/route')
146+
const req = new Request('http://localhost/api/user/config/minimax', {
147+
method: 'POST',
148+
body: JSON.stringify({
149+
apiBaseUrl: 'https://api.minimax.io/v1',
150+
apiKey: '',
151+
}),
152+
headers: { 'Content-Type': 'application/json' },
153+
})
154+
const response = await POST(req as never, { params: Promise.resolve({}) })
155+
156+
expect(response.status).toBe(400)
157+
const data = await response.json()
158+
expect(data.error).toBe('API key is required')
159+
})
160+
161+
it('should reject invalid URL format', async () => {
162+
const { POST } = await import('@/app/api/user/config/minimax/route')
163+
const req = new Request('http://localhost/api/user/config/minimax', {
164+
method: 'POST',
165+
body: JSON.stringify({
166+
apiBaseUrl: 'not-a-url',
167+
apiKey: 'test-api-key',
168+
}),
169+
headers: { 'Content-Type': 'application/json' },
170+
})
171+
const response = await POST(req as never, { params: Promise.resolve({}) })
172+
173+
expect(response.status).toBe(400)
174+
const data = await response.json()
175+
expect(data.error).toBe('Invalid API base URL format')
176+
})
177+
178+
it('should handle optional model being empty (delete config)', async () => {
179+
const { POST } = await import('@/app/api/user/config/minimax/route')
180+
const req = new Request('http://localhost/api/user/config/minimax', {
181+
method: 'POST',
182+
body: JSON.stringify({
183+
apiBaseUrl: 'https://api.minimax.io/v1',
184+
apiKey: 'test-api-key',
185+
model: '',
186+
}),
187+
headers: { 'Content-Type': 'application/json' },
188+
})
189+
const response = await POST(req as never, { params: Promise.resolve({}) })
190+
const data = await response.json()
191+
192+
expect(data.success).toBe(true)
193+
})
194+
195+
it('should save without optional model field', async () => {
196+
const { POST } = await import('@/app/api/user/config/minimax/route')
197+
const req = new Request('http://localhost/api/user/config/minimax', {
198+
method: 'POST',
199+
body: JSON.stringify({
200+
apiBaseUrl: 'https://api.minimax.io/v1',
201+
apiKey: 'test-api-key',
202+
}),
203+
headers: { 'Content-Type': 'application/json' },
204+
})
205+
const response = await POST(req as never, { params: Promise.resolve({}) })
206+
const data = await response.json()
207+
208+
expect(data.success).toBe(true)
209+
})
210+
})

0 commit comments

Comments
 (0)