Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
9 changes: 9 additions & 0 deletions .changeset/add-minimax-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@open-codesign/shared": minor
"@open-codesign/providers": patch
"@open-codesign/desktop": patch
---

Add MiniMax as a built-in onboarding provider.

MiniMax is now available as a first-class provider option alongside Anthropic, OpenAI, OpenRouter, and Ollama. It uses the OpenAI-compatible wire (`openai-chat`) with a default base URL of `https://api.minimax.io/v1` and ships with a static model hint for `MiniMax-M3` (default), `MiniMax-M2.7`, and `MiniMax-M2.7-highspeed`. Credentials can be supplied via the `MINIMAX_API_KEY` environment variable or entered during onboarding.
1 change: 1 addition & 0 deletions apps/desktop/src/main/imports/codex-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const ALLOWED_IMPORT_ENV_KEYS: ReadonlySet<string> = new Set([
'GEMINI_API_KEY',
'GOOGLE_API_KEY',
'GROQ_API_KEY',
'MINIMAX_API_KEY',
'MISTRAL_API_KEY',
'OPENAI_API_KEY',
'OPENROUTER_API_KEY',
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/onboarding-ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ function parseValidateKey(raw: unknown): ValidateKeyInput {
}
if (!isSupportedOnboardingProvider(provider)) {
throw new CodesignError(
`Provider "${provider}" is not supported in v0.1. Only anthropic, openai, openrouter.`,
`Provider "${provider}" is not supported in v0.1. Only anthropic, openai, openrouter, minimax.`,
ERROR_CODES.PROVIDER_NOT_SUPPORTED,
);
}
Expand Down
36 changes: 36 additions & 0 deletions packages/providers/src/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,40 @@ describe('pingProvider', () => {
});
await pingProvider('anthropic', 'sk-ant-oat-xyz', 'https://sub2api.example.com');
});

it('validates MiniMax with Bearer auth and returns model count', async () => {
mockFetch(async (url, init) => {
expect(url).toBe('https://api.minimax.io/v1/models');
const headers = (init?.headers ?? {}) as Record<string, string>;
expect(headers['authorization']).toBe('Bearer mm-test-key');
return new Response(
JSON.stringify({
data: [
{ id: 'MiniMax-M3' },
{ id: 'MiniMax-M2.7' },
{ id: 'MiniMax-M2.7-highspeed' },
],
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
});
const result = await pingProvider('minimax', 'mm-test-key');
expect(result).toEqual({ ok: true, modelCount: 3 });
});

it('returns 401 code for MiniMax invalid key', async () => {
mockFetch(async () => new Response('unauthorized', { status: 401 }));
const result = await pingProvider('minimax', 'bad-key');
expect(result.ok).toBe(false);
if (!result.ok) expect(result.code).toBe('401');
});

it('respects custom baseUrl for MiniMax', async () => {
mockFetch(async (url) => {
expect(url).toBe('https://api.minimaxi.com/v1/models');
return new Response(JSON.stringify({ data: [{ id: 'MiniMax-M3' }] }), { status: 200 });
});
const result = await pingProvider('minimax', 'mm-test-key', 'https://api.minimaxi.com/v1');
expect(result).toEqual({ ok: true, modelCount: 1 });
});
});
9 changes: 8 additions & 1 deletion packages/providers/src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ function endpoint(provider: SupportedOnboardingProvider, baseUrl?: string): Prov
headers: () => ({}),
};
}
case 'minimax': {
const root = baseUrl ? normalizeValidateBaseUrl(baseUrl) : 'https://api.minimax.io';
return {
url: `${root}/v1/models`,
headers: (apiKey) => ({ authorization: `Bearer ${apiKey}` }),
};
}
}
}

Expand Down Expand Up @@ -97,7 +104,7 @@ export async function pingProvider(
): Promise<ValidateResult> {
if (!isSupportedOnboardingProvider(provider)) {
throw new CodesignError(
`Provider "${provider}" is not supported in v0.1. Supported: anthropic, openai, openrouter, ollama.`,
`Provider "${provider}" is not supported in v0.1. Supported: anthropic, openai, openrouter, ollama, minimax.`,
ERROR_CODES.PROVIDER_NOT_SUPPORTED,
);
}
Expand Down
39 changes: 38 additions & 1 deletion packages/shared/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ describe('config v3 schema', () => {
});

describe('migrateLegacyToV3', () => {
it('seeds three builtin providers from an empty v2', () => {
it('seeds all builtin providers from an empty v2', () => {
const legacy = {
version: 2 as const,
provider: 'anthropic' as const,
Expand Down Expand Up @@ -347,6 +347,43 @@ describe('provider capability helpers', () => {
});
});

describe('MiniMax builtin provider', () => {
it('is included in SUPPORTED_ONBOARDING_PROVIDERS', () => {
expect(SUPPORTED_ONBOARDING_PROVIDERS).toContain('minimax');
});

it('has correct wire and baseUrl', () => {
expect(BUILTIN_PROVIDERS.minimax.wire).toBe('openai-chat');
expect(BUILTIN_PROVIDERS.minimax.baseUrl).toBe('https://api.minimax.io/v1');
});

it('uses static-hint model discovery with M3 + M2.7 models', () => {
expect(BUILTIN_PROVIDERS.minimax.capabilities?.modelDiscoveryMode).toBe('static-hint');
expect(BUILTIN_PROVIDERS.minimax.modelsHint).toEqual([
'MiniMax-M3',
'MiniMax-M2.7',
'MiniMax-M2.7-highspeed',
]);
expect(BUILTIN_PROVIDERS.minimax.defaultModel).toBe('MiniMax-M3');
});

it('reads MINIMAX_API_KEY env var', () => {
expect(BUILTIN_PROVIDERS.minimax.envKey).toBe('MINIMAX_API_KEY');
});

it('resolves capabilities correctly via resolveProviderCapabilities', () => {
const caps = resolveProviderCapabilities('minimax', {
wire: 'openai-chat',
baseUrl: 'https://api.minimax.io/v1',
});
expect(caps.supportsChatCompletions).toBe(true);
expect(caps.supportsSystemRole).toBe(true);
expect(caps.supportsToolCalling).toBe(true);
expect(caps.requiresClaudeCodeIdentity).toBe(false);
expect(caps.supportsReasoning).toBe(false);
});
});

describe('requiresClaudeCodeIdentity — host-based detection', () => {
it('official api.anthropic.com → requiresClaudeCodeIdentity: false', () => {
const caps = resolveProviderCapabilities('anthropic', {
Expand Down
30 changes: 30 additions & 0 deletions packages/shared/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const SUPPORTED_ONBOARDING_PROVIDERS = [
'openai',
'openrouter',
'ollama',
'minimax',
] as const;
export type SupportedOnboardingProvider = (typeof SUPPORTED_ONBOARDING_PROVIDERS)[number];

Expand Down Expand Up @@ -327,6 +328,28 @@ export const BUILTIN_PROVIDERS: Readonly<Record<SupportedOnboardingProvider, Pro
modelDiscoveryMode: 'models',
},
},
minimax: {
id: 'minimax',
name: 'MiniMax',
builtin: true,
wire: 'openai-chat',
baseUrl: 'https://api.minimax.io/v1',
envKey: 'MINIMAX_API_KEY',
defaultModel: 'MiniMax-M3',
modelsHint: ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.7-highspeed'],
capabilities: {
supportsKeyless: false,
supportsModelsEndpoint: false,
supportsChatCompletions: true,
supportsResponsesApi: false,
supportsSystemRole: true,
supportsDeveloperRole: false,
supportsReasoning: false,
supportsToolCalling: true,
requiresClaudeCodeIdentity: false,
modelDiscoveryMode: 'static-hint',
},
},
} as const;

// ── ConfigSchema v3 — canonical on-disk shape ────────────────────────────────
Expand Down Expand Up @@ -520,6 +543,13 @@ export const PROVIDER_SHORTLIST: Record<SupportedOnboardingProvider, ProviderSho
primary: [OLLAMA_DEFAULT_MODEL, 'llama3.1', 'qwen2.5'],
defaultPrimary: OLLAMA_DEFAULT_MODEL,
},
minimax: {
provider: 'minimax',
label: 'MiniMax',
keyHelpUrl: 'https://platform.minimax.io/user-center/basic-information/interface-key',
primary: ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.7-highspeed'],
defaultPrimary: 'MiniMax-M3',
},
};

export function isSupportedOnboardingProvider(p: string): p is SupportedOnboardingProvider {
Expand Down
Loading