diff --git a/.changeset/add-minimax-provider.md b/.changeset/add-minimax-provider.md new file mode 100644 index 00000000..55f36ad5 --- /dev/null +++ b/.changeset/add-minimax-provider.md @@ -0,0 +1,6 @@ +--- +'@open-codesign/shared': minor +'@open-codesign/providers': patch +--- + +Add MiniMax as a built-in provider with static M3 and M2.7 model choices, regional endpoint guidance, and Codex environment-key import support. diff --git a/MINIMAX.md b/MINIMAX.md new file mode 100644 index 00000000..ee21f305 --- /dev/null +++ b/MINIMAX.md @@ -0,0 +1,43 @@ +# MiniMax Provider + +Open CoDesign includes MiniMax as a built-in provider. The default entry uses the global English OpenAI-compatible endpoint and exposes `MiniMax-M3` and `MiniMax-M2.7` as static model choices. + +## Configuration + +- Provider ID: `minimax` +- API key environment variable: `MINIMAX_API_KEY` +- Default model: `MiniMax-M3` +- Model choices: `MiniMax-M3`, `MiniMax-M2.7` +- Official documentation: `https://platform.minimax.io/docs/api-reference/api-overview` +- China documentation: `https://platform.minimaxi.com/docs/api-reference/api-overview` + +The built-in provider uses `https://api.minimax.io/v1`. To use another region or wire format, add a custom provider in Settings and use one of these public bases: + +| Region | Wire | Base URL | Documentation | +| --- | --- | --- | --- | +| `global_en` | OpenAI-compatible | `https://api.minimax.io/v1` | `https://platform.minimax.io/docs` | +| `global_en` | Anthropic-compatible | `https://api.minimax.io/anthropic` | `https://platform.minimax.io/docs` | +| `cn_zh` | OpenAI-compatible | `https://api.minimaxi.com/v1` | `https://platform.minimaxi.com/docs` | +| `cn_zh` | Anthropic-compatible | `https://api.minimaxi.com/anthropic` | `https://platform.minimaxi.com/docs` | + +For an Anthropic-compatible entry, enter the base directly without adding `/v1`. Open CoDesign derives the versioned `/v1/messages` request path internally. For an OpenAI-compatible entry, keep `/v1` in the public base; the client appends the chat-completions path. + +## Model catalog + +Prices are USD per million tokens. The model metadata below mirrors the shared provider registry. + +| Model | Context | Input | Output | Cache read | Cache write | Input modalities | Thinking | +| --- | ---: | ---: | ---: | ---: | ---: | --- | --- | +| `MiniMax-M3` | 1,000,000 | 0.30 | 1.20 | 0.06 | null | Text, image, video | Adaptive, disabled | +| `MiniMax-M2.7` | 204,800 | 0.30 | 1.20 | 0.06 | 0.375 | Text | Always on | + +`MiniMax-M3` has tiered pricing based on input length and service tier: + +| Service tier | Input length | Input | Output | Cache read | +| --- | --- | ---: | ---: | ---: | +| Standard | Up to 512,000 | 0.30 | 1.20 | 0.06 | +| Standard | Above 512,000 | 0.60 | 2.40 | 0.12 | +| Priority | Up to 512,000 | 0.45 | 1.80 | 0.09 | +| Priority | Above 512,000 | 0.90 | 3.60 | 0.18 | + +The desktop provider adapter keeps MiniMax OpenAI-compatible models on the chat-completions path and does not enable the OpenAI developer-role reasoning flag for these model IDs. diff --git a/README.md b/README.md index b32bdca0..72f9f827 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,7 @@ Add a `SKILL.md` to any project to teach the model your own taste. - **Unified provider model** — Anthropic, OpenAI, Gemini, DeepSeek, OpenRouter, SiliconFlow, local Ollama, or any OpenAI-compatible relay; keyless (IP-allowlisted) proxies supported - **One-click import and sign-in** — bring Claude Code / Codex API-key provider configs across, or sign in with ChatGPT subscription for Codex models - **Dynamic model picker** — every provider exposes its real model catalogue, not a hardcoded shortlist +- **MiniMax support** - built-in MiniMax access with regional OpenAI-compatible and Anthropic-compatible endpoint guidance ([provider guide](./MINIMAX.md)) ### Generation and editing - **Prompt → HTML or JSX/React component** prototype, rendered in a sandboxed iframe (vendored React 18 + Babel on-device) diff --git a/apps/desktop/src/main/imports/codex-config.test.ts b/apps/desktop/src/main/imports/codex-config.test.ts index c1330aaf..9b68d565 100644 --- a/apps/desktop/src/main/imports/codex-config.test.ts +++ b/apps/desktop/src/main/imports/codex-config.test.ts @@ -262,6 +262,7 @@ env_key = "${envKey}" 'DEEPSEEK_API_KEY', 'XAI_API_KEY', 'GROQ_API_KEY', + 'MINIMAX_API_KEY', ])('accepts known provider env_key=%s', async (envKey) => { const toml = ` [model_providers.p] diff --git a/apps/desktop/src/main/imports/codex-config.ts b/apps/desktop/src/main/imports/codex-config.ts index 330ef428..cee61400 100644 --- a/apps/desktop/src/main/imports/codex-config.ts +++ b/apps/desktop/src/main/imports/codex-config.ts @@ -49,6 +49,7 @@ export const ALLOWED_IMPORT_ENV_KEYS: ReadonlySet = new Set([ 'GOOGLE_API_KEY', 'GROQ_API_KEY', 'MISTRAL_API_KEY', + 'MINIMAX_API_KEY', 'OPENAI_API_KEY', 'OPENROUTER_API_KEY', 'PERPLEXITY_API_KEY', diff --git a/apps/desktop/src/main/onboarding/provider-parsers.ts b/apps/desktop/src/main/onboarding/provider-parsers.ts index 65b2dfa2..f5b85d45 100644 --- a/apps/desktop/src/main/onboarding/provider-parsers.ts +++ b/apps/desktop/src/main/onboarding/provider-parsers.ts @@ -218,7 +218,7 @@ export function parseValidateKey(raw: unknown): ValidateKeyInput { const providerId = provider.trim(); if (!isSupportedOnboardingProvider(providerId)) { throw new CodesignError( - `Provider "${providerId}" is not supported. Only anthropic, openai, openrouter, ollama.`, + `Provider "${providerId}" is not supported. Only anthropic, openai, openrouter, minimax, ollama.`, ERROR_CODES.PROVIDER_NOT_SUPPORTED, ); } diff --git a/packages/providers/src/validate.test.ts b/packages/providers/src/validate.test.ts index d693fc2c..8105a8b1 100644 --- a/packages/providers/src/validate.test.ts +++ b/packages/providers/src/validate.test.ts @@ -1,4 +1,4 @@ -import { CodesignError } from '@open-codesign/shared'; +import { CodesignError, MINIMAX_ENDPOINTS } from '@open-codesign/shared'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { pingProvider } from './validate'; @@ -127,6 +127,23 @@ describe('pingProvider', () => { expect(result).toEqual({ ok: true, modelCount: 0 }); }); + it('uses the configured global MiniMax OpenAI endpoint and Bearer auth', async () => { + mockFetch(async (url, init) => { + expect(url).toBe(`${MINIMAX_ENDPOINTS.global_en.openaiBaseUrl}/models`); + const headers = (init?.headers ?? {}) as Record; + expect(headers['authorization']).toBe('Bearer minimax-test-key'); + return new Response( + JSON.stringify({ data: [{ id: 'MiniMax-M3' }, { id: 'MiniMax-M2.7' }] }), + { + status: 200, + headers: { 'content-type': 'application/json' }, + }, + ); + }); + const result = await pingProvider('minimax', 'minimax-test-key'); + expect(result).toEqual({ ok: true, modelCount: 2 }); + }); + it('respects custom baseUrl without /v1 suffix', async () => { mockFetch(async (url) => { expect(url).toBe('https://proxy.example/v1/models'); diff --git a/packages/providers/src/validate.ts b/packages/providers/src/validate.ts index 0336b283..e91f2153 100644 --- a/packages/providers/src/validate.ts +++ b/packages/providers/src/validate.ts @@ -3,6 +3,7 @@ import { CodesignError, ERROR_CODES, isSupportedOnboardingProvider, + MINIMAX_ENDPOINTS, type SupportedOnboardingProvider, stripInferenceEndpointSuffix, } from '@open-codesign/shared'; @@ -57,6 +58,15 @@ function endpoint(provider: SupportedOnboardingProvider, baseUrl?: string): Prov headers: (apiKey) => ({ authorization: `Bearer ${apiKey}` }), }; } + case 'minimax': { + const root = baseUrl + ? normalizeValidateBaseUrl(baseUrl) + : normalizeValidateBaseUrl(MINIMAX_ENDPOINTS.global_en.openaiBaseUrl); + return { + url: `${root}/v1/models`, + headers: (apiKey) => ({ authorization: `Bearer ${apiKey}` }), + }; + } case 'ollama': { // Local Ollama — OpenAI-compat endpoint at /v1. No auth header; the // caller (renderer) may still pass a non-empty apiKey as a sentinel @@ -97,7 +107,7 @@ export async function pingProvider( ): Promise { if (!isSupportedOnboardingProvider(provider)) { throw new CodesignError( - `Provider "${provider}" is not supported by the first-run provider shortcut. Supported: anthropic, openai, openrouter, ollama. Add custom providers in Settings, or use ChatGPT subscription sign-in for chatgpt-codex.`, + `Provider "${provider}" is not supported by the first-run provider shortcut. Supported: anthropic, openai, openrouter, minimax, ollama. Add custom providers in Settings, or use ChatGPT subscription sign-in for chatgpt-codex.`, ERROR_CODES.PROVIDER_NOT_SUPPORTED, ); } diff --git a/packages/shared/src/config.test.ts b/packages/shared/src/config.test.ts index 129396be..12b6ec7e 100644 --- a/packages/shared/src/config.test.ts +++ b/packages/shared/src/config.test.ts @@ -5,6 +5,9 @@ import { defaultProviderCapabilities, detectWireFromBaseUrl, hydrateConfig, + MINIMAX_ENDPOINTS, + MINIMAX_MODEL_CATALOG, + MINIMAX_MODEL_IDS, migrateLegacyToV3, parseConfigFlexible, resolveProviderCapabilities, @@ -13,6 +16,50 @@ import { } from './config'; describe('config v3 schema', () => { + it('registers MiniMax models and all configured endpoint variants', () => { + expect(MINIMAX_MODEL_IDS).toEqual(['MiniMax-M3', 'MiniMax-M2.7']); + expect(BUILTIN_PROVIDERS.minimax).toMatchObject({ + id: 'minimax', + wire: 'openai-chat', + baseUrl: MINIMAX_ENDPOINTS.global_en.openaiBaseUrl, + envKey: 'MINIMAX_API_KEY', + defaultModel: 'MiniMax-M3', + modelsHint: ['MiniMax-M3', 'MiniMax-M2.7'], + }); + expect( + Object.values(MINIMAX_ENDPOINTS).flatMap((endpoint) => [ + endpoint.openaiBaseUrl, + endpoint.anthropicBaseUrl, + ]), + ).toEqual([ + 'https://api.minimax.io/v1', + 'https://api.minimax.io/anthropic', + 'https://api.minimaxi.com/v1', + 'https://api.minimaxi.com/anthropic', + ]); + expect( + Object.values(MINIMAX_ENDPOINTS).every((endpoint) => + endpoint.anthropicBaseUrl.endsWith('/anthropic'), + ), + ).toBe(true); + expect(MINIMAX_MODEL_CATALOG).toMatchObject([ + { + modelId: 'MiniMax-M3', + contextWindow: 1_000_000, + inputModalities: ['text', 'image', 'video'], + thinking: ['adaptive', 'disabled'], + }, + { + modelId: 'MiniMax-M2.7', + contextWindow: 204_800, + inputModalities: ['text'], + thinking: ['always_on'], + }, + ]); + expect(MINIMAX_MODEL_CATALOG[0]?.pricingTiersUsdPerMillionTokens).toHaveLength(4); + expect(MINIMAX_MODEL_CATALOG[1]?.pricingUsdPerMillionTokens.cacheWrite).toBe(0.375); + }); + it('parses a minimal v3 config', () => { const raw = { version: 3, @@ -302,7 +349,7 @@ describe('config v3 schema', () => { }); describe('migrateLegacyToV3', () => { - it('seeds three builtin providers from an empty v2', () => { + it('seeds all supported builtin providers from an empty v2', () => { const legacy = { version: 2 as const, provider: 'anthropic' as const, diff --git a/packages/shared/src/config.ts b/packages/shared/src/config.ts index 12aac774..f36b5865 100644 --- a/packages/shared/src/config.ts +++ b/packages/shared/src/config.ts @@ -7,6 +7,7 @@ const ProviderIdEnum = z.enum([ 'openai', 'google', 'openrouter', + 'minimax', 'groq', 'cerebras', 'xai', @@ -20,6 +21,7 @@ export const SUPPORTED_ONBOARDING_PROVIDERS = [ 'anthropic', 'openai', 'openrouter', + 'minimax', 'ollama', ] as const; export type SupportedOnboardingProvider = (typeof SUPPORTED_ONBOARDING_PROVIDERS)[number]; @@ -29,6 +31,84 @@ export type SupportedOnboardingProvider = (typeof SUPPORTED_ONBOARDING_PROVIDERS export const OLLAMA_DEFAULT_BASE_URL = 'http://localhost:11434/v1'; export const OLLAMA_DEFAULT_MODEL = 'llama3.2'; +export const MINIMAX_ENDPOINTS = { + global_en: { + region: 'global_en', + openaiBaseUrl: 'https://api.minimax.io/v1', + anthropicBaseUrl: 'https://api.minimax.io/anthropic', + docsRoot: 'https://platform.minimax.io/docs', + }, + cn_zh: { + region: 'cn_zh', + openaiBaseUrl: 'https://api.minimaxi.com/v1', + anthropicBaseUrl: 'https://api.minimaxi.com/anthropic', + docsRoot: 'https://platform.minimaxi.com/docs', + }, +} as const; + +export const MINIMAX_MODEL_CATALOG = [ + { + modelId: 'MiniMax-M3', + contextWindow: 1_000_000, + pricingUsdPerMillionTokens: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: null, + }, + pricingTiersUsdPerMillionTokens: [ + { + serviceTier: 'standard', + inputTokensLte: 512_000, + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: null, + }, + { + serviceTier: 'standard', + inputTokensGt: 512_000, + input: 0.6, + output: 2.4, + cacheRead: 0.12, + cacheWrite: null, + }, + { + serviceTier: 'priority', + inputTokensLte: 512_000, + input: 0.45, + output: 1.8, + cacheRead: 0.09, + cacheWrite: null, + }, + { + serviceTier: 'priority', + inputTokensGt: 512_000, + input: 0.9, + output: 3.6, + cacheRead: 0.18, + cacheWrite: null, + }, + ], + inputModalities: ['text', 'image', 'video'], + thinking: ['adaptive', 'disabled'], + }, + { + modelId: 'MiniMax-M2.7', + contextWindow: 204_800, + pricingUsdPerMillionTokens: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0.375, + }, + inputModalities: ['text'], + thinking: ['always_on'], + }, +] as const; + +export const MINIMAX_MODEL_IDS = MINIMAX_MODEL_CATALOG.map((model) => model.modelId); + // ── Wire types (v3) ────────────────────────────────────────────────────────── export const WireApiSchema = z.enum([ @@ -278,6 +358,23 @@ export const BUILTIN_PROVIDERS: Readonly