diff --git a/.changeset/add-minimax-provider.md b/.changeset/add-minimax-provider.md new file mode 100644 index 00000000..56777ecb --- /dev/null +++ b/.changeset/add-minimax-provider.md @@ -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-M2.7` and `MiniMax-M2.7-highspeed`. Credentials can be supplied via the `MINIMAX_API_KEY` environment variable or entered during onboarding. diff --git a/apps/desktop/src/main/imports/codex-config.ts b/apps/desktop/src/main/imports/codex-config.ts index 7c271fb1..0998b297 100644 --- a/apps/desktop/src/main/imports/codex-config.ts +++ b/apps/desktop/src/main/imports/codex-config.ts @@ -48,6 +48,7 @@ export const ALLOWED_IMPORT_ENV_KEYS: ReadonlySet = new Set([ 'GEMINI_API_KEY', 'GOOGLE_API_KEY', 'GROQ_API_KEY', + 'MINIMAX_API_KEY', 'MISTRAL_API_KEY', 'OPENAI_API_KEY', 'OPENROUTER_API_KEY', diff --git a/apps/desktop/src/main/onboarding-ipc.ts b/apps/desktop/src/main/onboarding-ipc.ts index c1803768..298fc33b 100644 --- a/apps/desktop/src/main/onboarding-ipc.ts +++ b/apps/desktop/src/main/onboarding-ipc.ts @@ -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, ); } diff --git a/packages/providers/src/validate.test.ts b/packages/providers/src/validate.test.ts index 8dc6afac..eab6f944 100644 --- a/packages/providers/src/validate.test.ts +++ b/packages/providers/src/validate.test.ts @@ -205,4 +205,34 @@ 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; + expect(headers['authorization']).toBe('Bearer mm-test-key'); + return new Response( + JSON.stringify({ data: [{ 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: 2 }); + }); + + 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-M2.7' }] }), { status: 200 }); + }); + const result = await pingProvider('minimax', 'mm-test-key', 'https://api.minimaxi.com/v1'); + expect(result).toEqual({ ok: true, modelCount: 1 }); + }); }); diff --git a/packages/providers/src/validate.ts b/packages/providers/src/validate.ts index 5af53a66..df82231c 100644 --- a/packages/providers/src/validate.ts +++ b/packages/providers/src/validate.ts @@ -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}` }), + }; + } } } @@ -97,7 +104,7 @@ export async function pingProvider( ): Promise { 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, ); } diff --git a/packages/shared/src/config.test.ts b/packages/shared/src/config.test.ts index b7bf2a10..fdc6d804 100644 --- a/packages/shared/src/config.test.ts +++ b/packages/shared/src/config.test.ts @@ -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, @@ -347,6 +347,42 @@ 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 M2.7 models', () => { + expect(BUILTIN_PROVIDERS.minimax.capabilities?.modelDiscoveryMode).toBe('static-hint'); + expect(BUILTIN_PROVIDERS.minimax.modelsHint).toEqual([ + 'MiniMax-M2.7', + 'MiniMax-M2.7-highspeed', + ]); + expect(BUILTIN_PROVIDERS.minimax.defaultModel).toBe('MiniMax-M2.7'); + }); + + 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', { diff --git a/packages/shared/src/config.ts b/packages/shared/src/config.ts index 68d61881..9a093e9b 100644 --- a/packages/shared/src/config.ts +++ b/packages/shared/src/config.ts @@ -21,6 +21,7 @@ export const SUPPORTED_ONBOARDING_PROVIDERS = [ 'openai', 'openrouter', 'ollama', + 'minimax', ] as const; export type SupportedOnboardingProvider = (typeof SUPPORTED_ONBOARDING_PROVIDERS)[number]; @@ -327,6 +328,28 @@ export const BUILTIN_PROVIDERS: Readonly