Skip to content
Open
Show file tree
Hide file tree
Changes from 15 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
5 changes: 2 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -282,10 +282,9 @@ ANTHROPIC_API_KEY=sk-ant-your-key-here
# MiniMax API provides text generation models.
# Get your API key from https://platform.minimax.io/
#
# CLAUDE_CODE_USE_OPENAI=1
# MINIMAX_API_KEY=your-minimax-key-here
# OPENAI_BASE_URL=https://api.minimax.io/v1
# OPENAI_MODEL=MiniMax-M2.5
# ANTHROPIC_BASE_URL=https://api.minimax.io/anthropic
# ANTHROPIC_MODEL=MiniMax-M2.7


# =============================================================================
Expand Down
4 changes: 2 additions & 2 deletions src/commands/benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ async function runBenchmark(
if (!isBenchmarkSupported()) {
context?.stdout?.write(
'Benchmark not supported for this provider.\n' +
'Supported: OpenAI-compatible endpoints (Ollama, NVIDIA NIM, MiniMax)\n',
'Supported: OpenAI-compatible endpoints (Ollama, NVIDIA NIM)\n',
)
return
}
Expand Down Expand Up @@ -53,4 +53,4 @@ export const benchmark: Command = {

await runBenchmark(model, context)
},
}
}
115 changes: 112 additions & 3 deletions src/components/ProviderManager.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ function mockProviderProfilesModule(options?: {
return {
provider: 'minimax',
name: 'MiniMax',
baseUrl: 'https://api.minimax.io/v1',
baseUrl: 'https://api.minimax.io/anthropic',
model: 'MiniMax-M2.7',
apiKey: '',
requiresApiKey: true,
Expand Down Expand Up @@ -792,8 +792,15 @@ test('ProviderManager saves OpenAI preset GPT-5 models with Responses API', asyn
}
})

test('ProviderManager skips advanced setup fields when adding MiniMax preset', async () => {
mockProviderManagerDependencies(() => undefined, async () => undefined)
test('ProviderManager saves MiniMax preset with Anthropic-compatible endpoint and type', async () => {
const addProviderProfile = mock((payload: any) => ({
id: 'minimax_profile',
...payload,
}))

mockProviderManagerDependencies(() => undefined, async () => undefined, {
addProviderProfile,
})

const nonce = `${Date.now()}-${Math.random()}`
const { ProviderManager } = await import(`./ProviderManager.js?ts=${nonce}`)
Expand All @@ -818,6 +825,7 @@ test('ProviderManager skips advanced setup fields when adding MiniMax preset', a

expect(modelOutput).toContain('MiniMax')
expect(modelOutput).toContain('MiniMax-M2.7')
expect(modelOutput).toContain('Provider type: Anthropic-compatible API')
expect(modelOutput).not.toContain('Provider name')
expect(modelOutput).not.toContain('Base URL')
expect(modelOutput).not.toContain('API mode')
Expand All @@ -833,6 +841,107 @@ test('ProviderManager skips advanced setup fields when adding MiniMax preset', a
expect(keyOutput).not.toContain('API mode')
expect(keyOutput).not.toContain('Auth header')
expect(keyOutput).not.toContain('Custom headers')

mounted.stdin.write('minimax-test-key')
await Bun.sleep(25)
mounted.stdin.write('\r')

await waitForCondition(() => addProviderProfile.mock.calls.length > 0)
expect(addProviderProfile).toHaveBeenCalledWith(
expect.objectContaining({
provider: 'minimax',
baseUrl: 'https://api.minimax.io/anthropic',
model: 'MiniMax-M2.7',
apiFormat: 'chat_completions',
}),
expect.objectContaining({ makeActive: true }),
)
} finally {
await mounted.dispose()
}
})

test('ProviderManager edit flow keeps MiniMax on Anthropic-compatible provider path', async () => {
const minimaxProfile = {
id: 'provider_minimax',
provider: 'minimax',
name: 'MiniMax',
baseUrl: 'https://api.minimax.io/anthropic',
model: 'MiniMax-M2.7',
apiKey: 'minimax-key',
}
const updateProviderProfile = mock((id: string, payload: any) => ({
...minimaxProfile,
id,
...payload,
}))

mockProviderManagerDependencies(
() => undefined,
async () => undefined,
{
getProviderProfiles: () => [minimaxProfile],
getActiveProviderProfile: () => minimaxProfile,
updateProviderProfile,
},
)

const nonce = `${Date.now()}-${Math.random()}`
const { ProviderManager } = await import(`./ProviderManager.js?ts=${nonce}`)
const mounted = await mountProviderManager(ProviderManager)

try {
await waitForFrameOutput(mounted.getOutput, frame =>
frame.includes('Provider manager') &&
frame.includes('Edit provider'),
)

mounted.stdin.write('j')
await Bun.sleep(25)
mounted.stdin.write('j')
await Bun.sleep(25)
mounted.stdin.write('\r')

await waitForFrameOutput(mounted.getOutput, frame =>
frame.includes('Edit provider') &&
frame.includes('MiniMax') &&
!frame.includes('Provider manager'),
)

mounted.stdin.write('\r')
const editOutput = await waitForFrameOutput(mounted.getOutput, frame =>
frame.includes('Edit provider profile') &&
frame.includes('Provider type: Anthropic-compatible API'),
)

expect(editOutput).toContain('Provider type: Anthropic-compatible API')
expect(editOutput).not.toContain('API mode')
expect(editOutput).not.toContain('Auth header')
expect(editOutput).not.toContain('Custom headers')

for (let step = 2; step <= 4; step++) {
mounted.stdin.write('\r')
await waitForFrameOutput(mounted.getOutput, frame =>
frame.includes(`Step ${step} of 4`),
)
}
mounted.stdin.write('\r')

await waitForCondition(() => updateProviderProfile.mock.calls.length > 0)
expect(updateProviderProfile).toHaveBeenCalledWith(
'provider_minimax',
expect.objectContaining({
provider: 'minimax',
baseUrl: 'https://api.minimax.io/anthropic',
model: 'MiniMax-M2.7',
}),
)
expect(updateProviderProfile.mock.calls[0]?.[1]).toMatchObject({
authHeader: undefined,
authScheme: undefined,
authHeaderValue: undefined,
customHeaders: undefined,
})
} finally {
await mounted.dispose()
}
Expand Down
10 changes: 10 additions & 0 deletions src/components/StartupScreen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const ENV_KEYS = [
'ANTHROPIC_DEFAULT_SONNET_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
'ANTHROPIC_BASE_URL',
'ANTHROPIC_API_KEY',
]

const originalEnv: Record<string, string | undefined> = {}
Expand Down Expand Up @@ -255,6 +256,15 @@ describe('detectProvider — explicit dedicated-provider env flags', () => {
process.env.MINIMAX_API_KEY = 'test-key'
expect(detectProvider().name).toBe('MiniMax')
})

test('Anthropic-compatible MiniMax profile is labeled MiniMax', () => {
process.env.ANTHROPIC_BASE_URL = 'https://api.minimax.io/anthropic'
process.env.ANTHROPIC_API_KEY = 'test-key'
process.env.ANTHROPIC_MODEL = 'MiniMax-M2.7'

expect(detectProvider().name).toBe('MiniMax')
expect(detectProvider().baseUrl).toBe('https://api.minimax.io/anthropic')
})
})

// --- modelOverride from --model flag ---
Expand Down
4 changes: 3 additions & 1 deletion src/components/StartupScreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { isLocalProviderUrl, resolveProviderRequest } from '../services/api/providerConfig.js'
import {
getRouteLabel,
isMiniMaxBaseUrl,
resolveRouteIdFromBaseUrl,
} from '../integrations/routeMetadata.js'
import { getLocalOpenAICompatibleProviderLabel } from '../utils/providerDiscovery.js'
Expand Down Expand Up @@ -161,7 +162,8 @@ export function detectProvider(modelOverride?: string): { name: string; model: s
const resolvedModel = parseUserSpecifiedModel(modelSetting)
const baseUrl = process.env.ANTHROPIC_BASE_URL ?? 'https://api.anthropic.com'
const isLocal = isLocalProviderUrl(baseUrl)
return { name: 'Anthropic', model: resolvedModel, baseUrl, isLocal }
const name = isMiniMaxBaseUrl(baseUrl) ? 'MiniMax' : 'Anthropic'
return { name, model: resolvedModel, baseUrl, isLocal }
}

// ─── Box drawing ──────────────────────────────────────────────────────────────
Expand Down
14 changes: 7 additions & 7 deletions src/integrations/models/minimax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default [
classification: ['chat', 'reasoning', 'coding'],
defaultModel: 'MiniMax-M2',
capabilities: minimaxM2Capabilities,
contextWindow: 204_800,
contextWindow: 196_608,
maxOutputTokens: 131_072,
}),
defineModel({
Expand All @@ -29,7 +29,7 @@ export default [
classification: ['chat', 'reasoning', 'vision', 'coding'],
defaultModel: 'MiniMax-M2.1',
capabilities: minimaxM2Capabilities,
contextWindow: 204_800,
contextWindow: 196_608,
maxOutputTokens: 131_072,
}),
defineModel({
Expand All @@ -40,7 +40,7 @@ export default [
classification: ['chat', 'reasoning', 'vision', 'coding'],
defaultModel: 'MiniMax-M2.1-highspeed',
capabilities: minimaxM2Capabilities,
contextWindow: 204_800,
contextWindow: 196_608,
maxOutputTokens: 131_072,
}),
defineModel({
Expand All @@ -51,7 +51,7 @@ export default [
classification: ['chat', 'reasoning', 'vision', 'coding'],
defaultModel: 'MiniMax-M2.5',
capabilities: minimaxM2Capabilities,
contextWindow: 204_800,
contextWindow: 196_608,
maxOutputTokens: 131_072,
}),
defineModel({
Expand All @@ -62,7 +62,7 @@ export default [
classification: ['chat', 'reasoning', 'vision', 'coding'],
defaultModel: 'MiniMax-M2.5-highspeed',
capabilities: minimaxM2Capabilities,
contextWindow: 204_800,
contextWindow: 196_608,
maxOutputTokens: 131_072,
}),
defineModel({
Expand All @@ -73,7 +73,7 @@ export default [
classification: ['chat', 'reasoning', 'vision', 'coding'],
defaultModel: 'MiniMax-M2.7',
capabilities: minimaxM2Capabilities,
contextWindow: 204_800,
contextWindow: 196_608,
maxOutputTokens: 131_072,
}),
defineModel({
Expand All @@ -84,7 +84,7 @@ export default [
classification: ['chat', 'reasoning', 'vision', 'coding'],
defaultModel: 'MiniMax-M2.7-highspeed',
capabilities: minimaxM2Capabilities,
contextWindow: 204_800,
contextWindow: 196_608,
maxOutputTokens: 131_072,
}),
defineModel({
Expand Down
34 changes: 34 additions & 0 deletions src/integrations/routeMetadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ test('resolveActiveRouteIdFromEnv treats MiniMax credential-only env as MiniMax'
).toBe('minimax')
})

test('resolveActiveRouteIdFromEnv treats Anthropic-compatible MiniMax profile env as MiniMax', () => {
expect(
resolveActiveRouteIdFromEnv({
ANTHROPIC_BASE_URL: 'https://api.minimax.io/anthropic',
ANTHROPIC_API_KEY: 'minimax-key',
ANTHROPIC_MODEL: 'MiniMax-M2.7',
}),
).toBe('minimax')
})

test('resolveActiveRouteIdFromEnv treats Venice credential-only env as Venice', () => {
expect(
resolveActiveRouteIdFromEnv({
Expand All @@ -122,6 +132,30 @@ test('resolveActiveRouteIdFromEnv prefers xAI when env-only keys compete', () =>
).toBe('xai')
})

test('resolveActiveRouteIdFromEnv lets explicit MiniMax model beat ambient OpenAI-compatible env', () => {
expect(
resolveActiveRouteIdFromEnv({
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_API_KEY: 'openai-key',
XAI_API_KEY: 'xai-key',
MINIMAX_API_KEY: 'minimax-key',
OPENAI_MODEL: 'MiniMax-M2.7',
}),
).toBe('minimax')
})

test('resolveActiveRouteIdFromEnv does not use MiniMax when OpenAI base conflicts', () => {
expect(
resolveActiveRouteIdFromEnv({
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_API_KEY: 'openai-key',
MINIMAX_API_KEY: 'minimax-key',
OPENAI_BASE_URL: 'https://api.openai.com/v1',
OPENAI_MODEL: 'MiniMax-M2.7',
}),
).toBe('openai')
})

test('resolveActiveRouteIdFromEnv keeps xAI primary base over stale API base', () => {
expect(
resolveActiveRouteIdFromEnv({
Expand Down
Loading