diff --git a/.changeset/minimax-image-generation.md b/.changeset/minimax-image-generation.md
new file mode 100644
index 00000000..29557290
--- /dev/null
+++ b/.changeset/minimax-image-generation.md
@@ -0,0 +1,7 @@
+---
+'@open-codesign/shared': minor
+'@open-codesign/providers': minor
+'@open-codesign/desktop': minor
+---
+
+Add MiniMax image generation with regional endpoints, custom image credentials, and validated image data for workspace assets.
diff --git a/apps/desktop/src/main/image-generation-settings.test.ts b/apps/desktop/src/main/image-generation-settings.test.ts
index 8532e8de..b958de0f 100644
--- a/apps/desktop/src/main/image-generation-settings.test.ts
+++ b/apps/desktop/src/main/image-generation-settings.test.ts
@@ -4,6 +4,8 @@ import {
ERROR_CODES,
hydrateConfig,
IMAGE_GENERATION_SCHEMA_VERSION,
+ MINIMAX_IMAGE_BASE_URLS,
+ MINIMAX_IMAGE_MODEL,
type ProviderEntry,
} from '@open-codesign/shared';
import { afterEach, describe, expect, it, vi } from 'vitest';
@@ -13,6 +15,7 @@ import {
isGenerateImageAssetEnabled,
parseImageGenerationUpdate,
resolveImageGenerationConfig,
+ toGenerateImageOptions,
updateImageGenerationSettings,
} from './image-generation-settings';
@@ -121,6 +124,68 @@ describe('image generation enablement', () => {
mocks.writeConfig.mockReset();
});
+ it('switches image generation to MiniMax with isolated custom credentials', async () => {
+ mocks.cachedConfig = makeConfig(true);
+ const view = await updateImageGenerationSettings({ provider: 'minimax' });
+ expect(view).toMatchObject({
+ provider: 'minimax',
+ model: MINIMAX_IMAGE_MODEL,
+ baseUrl: MINIMAX_IMAGE_BASE_URLS.global_en,
+ credentialMode: 'custom',
+ hasCustomKey: false,
+ });
+ await expect(resolveImageGenerationConfig(mocks.cachedConfig)).rejects.toMatchObject({
+ code: ERROR_CODES.PROVIDER_KEY_MISSING,
+ });
+ await updateImageGenerationSettings({
+ apiKey: 'test-key',
+ baseUrl: MINIMAX_IMAGE_BASE_URLS.cn_zh,
+ });
+ const resolved = await resolveImageGenerationConfig(mocks.cachedConfig);
+ expect(resolved).toMatchObject({
+ provider: 'minimax',
+ apiKey: 'test-key',
+ baseUrl: MINIMAX_IMAGE_BASE_URLS.cn_zh,
+ });
+ expect(resolved).not.toBeNull();
+ if (resolved) {
+ expect(toGenerateImageOptions(resolved, 'poster', undefined, '16:9')).toMatchObject({
+ provider: 'minimax',
+ aspectRatio: '16:9',
+ prompt: 'poster',
+ });
+ }
+ });
+
+ it('keeps the MiniMax image endpoint independent of inherited text configuration', async () => {
+ const cfg = makeConfig(true);
+ const provider = Object.values(cfg.providers)[0];
+ const imageSettings = cfg.imageGeneration;
+ if (!provider || !imageSettings) throw new Error('Missing test configuration');
+ cfg.providers['minimax'] = {
+ ...provider,
+ id: 'minimax',
+ baseUrl: 'https://text.example/text-api',
+ };
+ cfg.imageGeneration = {
+ ...imageSettings,
+ provider: 'minimax',
+ model: MINIMAX_IMAGE_MODEL,
+ };
+ getApiKeyForProviderMock.mockReturnValue('test-key');
+ await expect(resolveImageGenerationConfig(cfg)).resolves.toMatchObject({
+ baseUrl: MINIMAX_IMAGE_BASE_URLS.global_en,
+ apiKey: 'test-key',
+ });
+ expect(getApiKeyForProviderMock).toHaveBeenCalledWith('minimax');
+ expect(
+ parseImageGenerationUpdate({
+ provider: 'minimax',
+ baseUrl: MINIMAX_IMAGE_BASE_URLS.cn_zh,
+ }),
+ ).toMatchObject({ provider: 'minimax', baseUrl: MINIMAX_IMAGE_BASE_URLS.cn_zh });
+ });
+
it('disables generate_image_asset when image generation is turned off', async () => {
const cfg = makeConfig(false);
await expect(isGenerateImageAssetEnabled(cfg)).resolves.toBe(false);
diff --git a/apps/desktop/src/main/image-generation-settings.ts b/apps/desktop/src/main/image-generation-settings.ts
index a55e47e2..e5824130 100644
--- a/apps/desktop/src/main/image-generation-settings.ts
+++ b/apps/desktop/src/main/image-generation-settings.ts
@@ -229,7 +229,9 @@ export async function resolveImageGenerationConfig(
apiKey = getApiKeyForProvider(parsed.provider);
}
const inheritedBaseUrl =
- parsed.credentialMode === 'inherit' ? cfg.providers[parsed.provider]?.baseUrl : undefined;
+ parsed.credentialMode === 'inherit' && parsed.provider !== 'minimax'
+ ? cfg.providers[parsed.provider]?.baseUrl
+ : undefined;
log.info('resolve.ok', {
provider: parsed.provider,
model: parsed.model,
@@ -363,7 +365,8 @@ export async function updateImageGenerationSettings(
const credentialMode =
provider === CHATGPT_CODEX_PROVIDER_ID
? 'inherit'
- : (patch.credentialMode ?? current.credentialMode);
+ : (patch.credentialMode ??
+ (providerChanged && provider === 'minimax' ? 'custom' : current.credentialMode));
let next: ImageGenerationSettings = {
...current,
...safePatch,
diff --git a/apps/desktop/src/renderer/src/components/settings/ImageGenerationTab.tsx b/apps/desktop/src/renderer/src/components/settings/ImageGenerationTab.tsx
index 10956b9a..665b6a3f 100644
--- a/apps/desktop/src/renderer/src/components/settings/ImageGenerationTab.tsx
+++ b/apps/desktop/src/renderer/src/components/settings/ImageGenerationTab.tsx
@@ -1,4 +1,5 @@
import { useT } from '@open-codesign/i18n';
+import { MINIMAX_IMAGE_BASE_URLS, MINIMAX_IMAGE_MODEL } from '@open-codesign/shared';
import { Image as ImageIcon } from 'lucide-react';
import { useEffect, useState } from 'react';
import type { ImageGenerationSettingsView } from '../../../../preload/index';
@@ -6,12 +7,14 @@ import { useCodesignStore } from '../../store';
import { Label, NativeSelect, Row, SectionTitle, SegmentedControl } from './primitives';
function defaultImageModelFor(provider: ImageGenerationSettingsView['provider']): string {
+ if (provider === 'minimax') return MINIMAX_IMAGE_MODEL;
if (provider === 'openrouter') return 'openai/gpt-5.4-image-2';
if (provider === 'chatgpt-codex') return 'gpt-5.5';
return 'gpt-image-2';
}
function defaultImageBaseUrlFor(provider: ImageGenerationSettingsView['provider']): string {
+ if (provider === 'minimax') return MINIMAX_IMAGE_BASE_URLS.global_en;
if (provider === 'openrouter') return 'https://openrouter.ai/api/v1';
if (provider === 'chatgpt-codex') return 'https://chatgpt.com/backend-api';
return 'https://api.openai.com/v1';
@@ -140,6 +143,7 @@ function ImageGenerationPanel() {
value={settings.provider}
disabled={saving}
options={[
+ { value: 'minimax', label: 'MiniMax' },
{ value: 'openai', label: 'OpenAI' },
{
value: 'chatgpt-codex',
@@ -153,7 +157,12 @@ function ImageGenerationPanel() {
const provider = value as ImageGenerationSettingsView['provider'];
void save({
provider,
- credentialMode: provider === 'chatgpt-codex' ? 'inherit' : settings.credentialMode,
+ credentialMode:
+ provider === 'chatgpt-codex'
+ ? 'inherit'
+ : provider === 'minimax'
+ ? 'custom'
+ : settings.credentialMode,
model: defaultImageModelFor(provider),
baseUrl: defaultImageBaseUrlFor(provider),
});
@@ -173,6 +182,20 @@ function ImageGenerationPanel() {
+ {settings.provider === 'minimax' ? (
+