Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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 @@ -227,7 +227,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 @@ -790,8 +790,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 @@ -816,6 +823,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 @@ -831,6 +839,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
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
24 changes: 24 additions & 0 deletions src/integrations/routeMetadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,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
37 changes: 31 additions & 6 deletions src/integrations/routeMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,22 @@ export function getMiniMaxBaseUrlOverride(
return undefined
}

function isMiniMaxModelName(value: string | undefined): boolean {
const normalized = value?.trim().toLowerCase()
return Boolean(
normalized &&
(normalized.startsWith('minimax-') || normalized.startsWith('minimax/')),
)
}

function hasMiniMaxRouteIntent(processEnv: NodeJS.ProcessEnv): boolean {
return (
getMiniMaxBaseUrlOverride(processEnv) !== undefined ||
isMiniMaxModelName(processEnv.OPENAI_MODEL) ||
isMiniMaxModelName(processEnv.ANTHROPIC_MODEL)
)
}

export function getXaiBaseUrlOverride(
processEnv: NodeJS.ProcessEnv = process.env,
): string | undefined {
Expand Down Expand Up @@ -288,12 +304,14 @@ export function hasXaiEnvOnlyProviderIntent(
export function hasMiniMaxEnvOnlyProviderIntent(
processEnv: NodeJS.ProcessEnv = process.env,
): boolean {
const hasExplicitMiniMaxIntent = hasMiniMaxRouteIntent(processEnv)
return (
hasNonEmptyEnvValue(processEnv.MINIMAX_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.OPENAI_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.XAI_API_KEY) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isMiniMaxBaseUrl) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)
(hasExplicitMiniMaxIntent ||
(!hasNonEmptyEnvValue(processEnv.OPENAI_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.XAI_API_KEY) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)))
)
}

Expand All @@ -313,6 +331,13 @@ export function hasVeniceEnvOnlyProviderIntent(
export function resolveEnvOnlyProviderRouteId(
processEnv: NodeJS.ProcessEnv = process.env,
): 'xai' | 'minimax' | 'venice' | null {
if (
hasMiniMaxRouteIntent(processEnv) &&
hasMiniMaxEnvOnlyProviderIntent(processEnv)
) {
return 'minimax'
}

if (hasXaiEnvOnlyProviderIntent(processEnv)) {
return 'xai'
}
Expand Down Expand Up @@ -529,6 +554,9 @@ export function resolveActiveRouteIdFromEnv(
return 'vertex'
}

const envOnlyRouteId = resolveEnvOnlyProviderRouteId(processEnv)
if (envOnlyRouteId) return envOnlyRouteId

if (isEnvTruthy(processEnv.CLAUDE_CODE_USE_OPENAI)) {
const baseUrl =
processEnv.OPENAI_BASE_URL ?? processEnv.OPENAI_API_BASE
Expand Down Expand Up @@ -564,9 +592,6 @@ export function resolveActiveRouteIdFromEnv(
return 'custom'
}

const envOnlyRouteId = resolveEnvOnlyProviderRouteId(processEnv)
if (envOnlyRouteId) return envOnlyRouteId

return 'anthropic'
}

Expand Down
28 changes: 12 additions & 16 deletions src/integrations/vendors/minimax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { defineVendor } from '../define.js'
export default defineVendor({
id: 'minimax',
label: 'MiniMax',
classification: 'openai-compatible',
defaultBaseUrl: 'https://api.minimax.io/v1',
classification: 'native',
defaultBaseUrl: 'https://api.minimax.io/anthropic',
defaultModel: 'MiniMax-M2.7',
requiredEnvVars: ['MINIMAX_API_KEY'],
setup: {
Expand All @@ -13,11 +13,7 @@ export default defineVendor({
credentialEnvVars: ['MINIMAX_API_KEY'],
},
transportConfig: {
kind: 'openai-compatible',
openaiShim: {
supportsApiFormatSelection: false,
supportsAuthHeaders: false,
},
kind: 'anthropic-proxy',
},
preset: {
id: 'minimax',
Expand All @@ -30,20 +26,20 @@ export default defineVendor({
matchDefaultBaseUrl: true,
matchBaseUrlHosts: ['api.minimax.io', 'api.minimax.chat'],
},
credentialEnvVars: ['MINIMAX_API_KEY', 'OPENAI_API_KEY'],
credentialEnvVars: ['MINIMAX_API_KEY'],
missingCredentialMessage:
'MiniMax auth is required. Set MINIMAX_API_KEY or OPENAI_API_KEY.',
'MiniMax auth is required. Set MINIMAX_API_KEY.',
},
catalog: {
source: 'static',
models: [
{ id: 'minimax-m2', apiName: 'MiniMax-M2', label: 'MiniMax M2', modelDescriptorId: 'minimax-m2' },
{ id: 'minimax-m2.1', apiName: 'MiniMax-M2.1', label: 'MiniMax M2.1', modelDescriptorId: 'minimax-m2.1' },
{ id: 'minimax-m2.1-highspeed', apiName: 'MiniMax-M2.1-highspeed', label: 'MiniMax M2.1 Highspeed', modelDescriptorId: 'minimax-m2.1-highspeed' },
{ id: 'minimax-m2.5', apiName: 'MiniMax-M2.5', label: 'MiniMax M2.5', modelDescriptorId: 'minimax-m2.5' },
{ id: 'minimax-m2.5-highspeed', apiName: 'MiniMax-M2.5-highspeed', label: 'MiniMax M2.5 Highspeed', modelDescriptorId: 'minimax-m2.5-highspeed' },
{ id: 'minimax-m2.7', apiName: 'MiniMax-M2.7', label: 'MiniMax M2.7', modelDescriptorId: 'minimax-m2.7' },
{ id: 'minimax-m2.7-highspeed', apiName: 'MiniMax-M2.7-highspeed', label: 'MiniMax M2.7 Highspeed', modelDescriptorId: 'minimax-m2.7-highspeed' },
{ id: 'minimax-m2', apiName: 'MiniMax-M2', label: 'MiniMax M2', modelDescriptorId: 'minimax-m2', contextWindow: 204_800 },
{ id: 'minimax-m2.1', apiName: 'MiniMax-M2.1', label: 'MiniMax M2.1', modelDescriptorId: 'minimax-m2.1', contextWindow: 204_800 },
{ id: 'minimax-m2.1-highspeed', apiName: 'MiniMax-M2.1-highspeed', label: 'MiniMax M2.1 Highspeed', modelDescriptorId: 'minimax-m2.1-highspeed', contextWindow: 204_800 },
{ id: 'minimax-m2.5', apiName: 'MiniMax-M2.5', label: 'MiniMax M2.5', modelDescriptorId: 'minimax-m2.5', contextWindow: 204_800 },
{ id: 'minimax-m2.5-highspeed', apiName: 'MiniMax-M2.5-highspeed', label: 'MiniMax M2.5 Highspeed', modelDescriptorId: 'minimax-m2.5-highspeed', contextWindow: 204_800 },
{ id: 'minimax-m2.7', apiName: 'MiniMax-M2.7', label: 'MiniMax M2.7', modelDescriptorId: 'minimax-m2.7', contextWindow: 204_800 },
{ id: 'minimax-m2.7-highspeed', apiName: 'MiniMax-M2.7-highspeed', label: 'MiniMax M2.7 Highspeed', modelDescriptorId: 'minimax-m2.7-highspeed', contextWindow: 204_800 },
{ id: 'minimax-text-01', apiName: 'MiniMax-Text-01', label: 'MiniMax Text 01', modelDescriptorId: 'minimax-text-01' },
{ id: 'minimax-text-01-preview', apiName: 'MiniMax-Text-01-Preview', label: 'MiniMax Text 01 Preview', modelDescriptorId: 'minimax-text-01-preview' },
{ id: 'minimax-vision-01', apiName: 'MiniMax-Vision-01', label: 'MiniMax Vision 01', modelDescriptorId: 'minimax-vision-01' },
Expand Down
Loading
Loading