豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit da72a19

Browse files
author
octo-patch
committed
feat: add MiniMax provider support
- Add MiniMax chat model provider using OpenAI-compatible interface - Register 'minimax' in the provider enum (src/db/tables.ts) - Add createModel case for MiniMax in src/ai/fetch.ts (base URL: https://api.minimax.io/v1) - Add MiniMax to the provider selector in the new model form - Add MiniMax to the provider enum in the model detail form - Supports MiniMax-M2.7 and MiniMax-M2.7-highspeed models - Requires MINIMAX_API_KEY (stored per-model as apiKey) - Add unit tests for provider schema validation
1 parent 9d9c18b commit da72a19

File tree

5 files changed

+168
-3
lines changed

5 files changed

+168
-3
lines changed

src/ai/fetch.test.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { describe, expect, it } from 'bun:test'
2+
import { z } from 'zod'
3+
4+
/**
5+
* Tests for MiniMax provider support in the model creation form schemas.
6+
*
7+
* These tests verify the provider-level validation rules:
8+
* - MiniMax requires an API key (like openai / openrouter)
9+
* - MiniMax does not require a URL (unlike custom)
10+
*/
11+
12+
// Mirror the form schema from src/settings/models/new.tsx
13+
const newModelFormSchema = z
14+
.object({
15+
provider: z.enum(['thunderbolt', 'openai', 'custom', 'openrouter', 'minimax']),
16+
name: z.string().min(1),
17+
model: z.string().min(1),
18+
url: z.string().optional(),
19+
apiKey: z.string().optional(),
20+
})
21+
.refine(
22+
(data) => {
23+
if (data.provider === 'custom') return data.url !== undefined && data.url.length > 0
24+
return true
25+
},
26+
{ message: 'URL is required for Custom providers', path: ['url'] },
27+
)
28+
.refine(
29+
(data) => {
30+
if (data.provider === 'custom' || data.provider === 'thunderbolt') return true
31+
return data.apiKey !== undefined && data.apiKey.length > 0
32+
},
33+
{ message: 'API Key is required for this provider', path: ['apiKey'] },
34+
)
35+
36+
// Mirror the form schema from src/settings/models/detail.tsx
37+
const detailModelFormSchema = z
38+
.object({
39+
provider: z.enum(['thunderbolt', 'anthropic', 'openai', 'custom', 'openrouter', 'minimax']),
40+
name: z.string().min(1),
41+
model: z.string().min(1),
42+
url: z.string().optional(),
43+
apiKey: z.string().optional(),
44+
})
45+
.refine(
46+
(data) => {
47+
if (data.provider === 'custom') return data.url !== undefined && data.url.length > 0
48+
return true
49+
},
50+
{ message: 'URL is required for Custom providers', path: ['url'] },
51+
)
52+
.refine(
53+
(data) => {
54+
if (data.provider === 'custom' || data.provider === 'thunderbolt') return true
55+
return data.apiKey !== undefined && data.apiKey.length > 0
56+
},
57+
{ message: 'API Key is required for this provider', path: ['apiKey'] },
58+
)
59+
60+
const validBase = { name: 'MiniMax M2.7', model: 'MiniMax-M2.7', url: '', apiKey: '' }
61+
62+
describe('MiniMax provider - new model form schema', () => {
63+
it('accepts minimax as a valid provider', () => {
64+
const result = newModelFormSchema.safeParse({
65+
...validBase,
66+
provider: 'minimax',
67+
apiKey: 'mm-test-key',
68+
})
69+
expect(result.success).toBe(true)
70+
})
71+
72+
it('rejects minimax without an API key', () => {
73+
const result = newModelFormSchema.safeParse({
74+
...validBase,
75+
provider: 'minimax',
76+
apiKey: '',
77+
})
78+
expect(result.success).toBe(false)
79+
const errors = result.error?.flatten().fieldErrors
80+
expect(errors?.apiKey).toBeDefined()
81+
})
82+
83+
it('does not require a URL for minimax', () => {
84+
const result = newModelFormSchema.safeParse({
85+
...validBase,
86+
provider: 'minimax',
87+
apiKey: 'mm-test-key',
88+
url: '',
89+
})
90+
expect(result.success).toBe(true)
91+
})
92+
93+
it('includes minimax in the provider enum', () => {
94+
const result = newModelFormSchema.safeParse({
95+
...validBase,
96+
provider: 'unknown-provider',
97+
apiKey: 'key',
98+
})
99+
expect(result.success).toBe(false)
100+
})
101+
102+
it('accepts MiniMax-M2.7 as model name', () => {
103+
const result = newModelFormSchema.safeParse({
104+
provider: 'minimax',
105+
name: 'MiniMax M2.7',
106+
model: 'MiniMax-M2.7',
107+
apiKey: 'mm-key',
108+
})
109+
expect(result.success).toBe(true)
110+
})
111+
112+
it('accepts MiniMax-M2.7-highspeed as model name', () => {
113+
const result = newModelFormSchema.safeParse({
114+
provider: 'minimax',
115+
name: 'MiniMax M2.7 Highspeed',
116+
model: 'MiniMax-M2.7-highspeed',
117+
apiKey: 'mm-key',
118+
})
119+
expect(result.success).toBe(true)
120+
})
121+
})
122+
123+
describe('MiniMax provider - model detail form schema', () => {
124+
it('accepts minimax provider for editing', () => {
125+
const result = detailModelFormSchema.safeParse({
126+
...validBase,
127+
provider: 'minimax',
128+
apiKey: 'mm-test-key',
129+
})
130+
expect(result.success).toBe(true)
131+
})
132+
133+
it('rejects minimax without API key on detail form', () => {
134+
const result = detailModelFormSchema.safeParse({
135+
...validBase,
136+
provider: 'minimax',
137+
apiKey: '',
138+
})
139+
expect(result.success).toBe(false)
140+
const errors = result.error?.flatten().fieldErrors
141+
expect(errors?.apiKey).toBeDefined()
142+
})
143+
144+
it('includes anthropic in detail form provider enum (for direct Anthropic models)', () => {
145+
const result = detailModelFormSchema.safeParse({
146+
...validBase,
147+
provider: 'anthropic',
148+
apiKey: 'sk-ant-test',
149+
})
150+
expect(result.success).toBe(true)
151+
})
152+
})

src/ai/fetch.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,18 @@ export const createModel = async (modelConfig: Model) => {
124124
})
125125
return openrouter(modelConfig.model)
126126
}
127+
case 'minimax': {
128+
if (!modelConfig.apiKey) {
129+
throw new Error('No API key provided')
130+
}
131+
const minimax = createOpenAICompatible({
132+
name: 'minimax',
133+
baseURL: 'https://api.minimax.io/v1',
134+
apiKey: modelConfig.apiKey,
135+
fetch,
136+
})
137+
return minimax(modelConfig.model)
138+
}
127139
default:
128140
throw new Error(`Unsupported provider: ${modelConfig.provider}`)
129141
}

src/db/tables.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export const modelsTable = sqliteTable(
7878
{
7979
id: text('id').primaryKey(),
8080
provider: text('provider', {
81-
enum: ['openai', 'custom', 'openrouter', 'thunderbolt', 'anthropic'],
81+
enum: ['openai', 'custom', 'openrouter', 'thunderbolt', 'anthropic', 'minimax'],
8282
}),
8383
name: text('name'),
8484
model: text('model'),

src/settings/models/detail.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { Trash2 } from 'lucide-react'
2828

2929
const formSchema = z
3030
.object({
31-
provider: z.enum(['thunderbolt', 'anthropic', 'openai', 'custom', 'openrouter']),
31+
provider: z.enum(['thunderbolt', 'anthropic', 'openai', 'custom', 'openrouter', 'minimax']),
3232
name: z.string().min(1, { message: 'Name is required.' }),
3333
model: z.string().min(1, { message: 'Model name is required.' }),
3434
url: z.string().optional(),

src/settings/models/new.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type { Model } from '@/types'
1616

1717
const formSchema = z
1818
.object({
19-
provider: z.enum(['thunderbolt', 'openai', 'custom', 'openrouter']),
19+
provider: z.enum(['thunderbolt', 'openai', 'custom', 'openrouter', 'minimax']),
2020
name: z.string().min(1, { message: 'Name is required.' }),
2121
model: z.string().min(1, { message: 'Model name is required.' }),
2222
url: z.string().optional(),
@@ -119,6 +119,7 @@ export default function NewModelPage() {
119119
<SelectItem value="thunderbolt">Thunderbolt</SelectItem>
120120
<SelectItem value="openai">OpenAI</SelectItem>
121121
<SelectItem value="openrouter">OpenRouter</SelectItem>
122+
<SelectItem value="minimax">MiniMax</SelectItem>
122123
<SelectItem value="custom">Custom</SelectItem>
123124
</SelectContent>
124125
</Select>

0 commit comments

Comments
 (0)