💄 style: support Minimax T2I models (#8583)

This commit is contained in:
Zhijie He
2025-07-29 21:54:56 +08:00
committed by GitHub
parent c29559cb2e
commit f8a01aacb0
4 changed files with 703 additions and 2 deletions
+43 -2
View File
@@ -1,4 +1,4 @@
import { AIChatModelCard } from '@/types/aiModel';
import { AIChatModelCard, AIImageModelCard } from '@/types/aiModel';
const minimaxChatModels: AIChatModelCard[] = [
{
@@ -51,6 +51,47 @@ const minimaxChatModels: AIChatModelCard[] = [
},
];
export const allModels = [...minimaxChatModels];
const minimaxImageModels: AIImageModelCard[] = [
{
description:
'全新图像生成模型,画面表现细腻,支持文生图、图生图',
displayName: 'Image 01',
enabled: true,
id: 'image-01',
parameters: {
aspectRatio: {
default: '1:1',
enum: ['1:1', '16:9', '4:3', '3:2', '2:3', '3:4', '9:16', '21:9'],
},
prompt: {
default: '',
},
seed: { default: null },
},
releasedAt: '2025-02-28',
type: 'image',
},
{
description:
'图像生成模型,画面表现细腻,支持文生图并进行画风设置',
displayName: 'Image 01 Live',
enabled: true,
id: 'image-01-live',
parameters: {
aspectRatio: {
default: '1:1',
enum: ['1:1', '16:9', '4:3', '3:2', '2:3', '3:4', '9:16', '21:9'],
},
prompt: {
default: '',
},
seed: { default: null },
},
releasedAt: '2025-02-28',
type: 'image',
},
];
export const allModels = [...minimaxChatModels, ...minimaxImageModels];
export default allModels;
@@ -0,0 +1,553 @@
// @vitest-environment edge-runtime
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { CreateImagePayload } from '../types/image';
import { CreateImageOptions } from '../utils/openaiCompatibleFactory';
import { createMiniMaxImage } from './createImage';
// Mock the console.error to avoid polluting test output
vi.spyOn(console, 'error').mockImplementation(() => {});
const mockOptions: CreateImageOptions = {
apiKey: 'test-api-key',
baseURL: 'https://api.minimaxi.com/v1',
provider: 'minimax',
};
beforeEach(() => {
// Reset all mocks before each test
vi.clearAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
describe('createMiniMaxImage', () => {
describe('Success scenarios', () => {
it('should successfully generate image with basic prompt', async () => {
const mockImageUrl = 'https://minimax-cdn.com/images/generated/test-image.jpg';
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
json: async () => ({
base_resp: {
status_code: 0,
status_msg: 'success',
},
data: {
image_urls: [mockImageUrl],
},
id: 'img-123456',
metadata: {
failed_count: '0',
success_count: '1',
},
}),
});
const payload: CreateImagePayload = {
model: 'image-01',
params: {
prompt: 'A beautiful sunset over the mountains',
},
};
const result = await createMiniMaxImage(payload, mockOptions);
expect(fetch).toHaveBeenCalledWith(
'https://api.minimaxi.com/v1/image_generation',
{
method: 'POST',
headers: {
'Authorization': 'Bearer test-api-key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
aspect_ratio: undefined,
model: 'image-01',
n: 1,
prompt: 'A beautiful sunset over the mountains',
}),
},
);
expect(result).toEqual({
imageUrl: mockImageUrl,
});
});
it('should handle custom aspect ratio', async () => {
const mockImageUrl = 'https://minimax-cdn.com/images/generated/custom-ratio.jpg';
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
json: async () => ({
base_resp: {
status_code: 0,
status_msg: 'success',
},
data: {
image_urls: [mockImageUrl],
},
id: 'img-custom-ratio',
metadata: {
failed_count: '0',
success_count: '1',
},
}),
});
const payload: CreateImagePayload = {
model: 'image-01',
params: {
prompt: 'Abstract digital art',
aspectRatio: '16:9',
},
};
const result = await createMiniMaxImage(payload, mockOptions);
expect(fetch).toHaveBeenCalledWith(
'https://api.minimaxi.com/v1/image_generation',
expect.objectContaining({
body: JSON.stringify({
aspect_ratio: '16:9',
model: 'image-01',
n: 1,
prompt: 'Abstract digital art',
}),
}),
);
expect(result).toEqual({
imageUrl: mockImageUrl,
});
});
it('should handle seed value correctly', async () => {
const mockImageUrl = 'https://minimax-cdn.com/images/generated/seeded-image.jpg';
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
json: async () => ({
base_resp: {
status_code: 0,
status_msg: 'success',
},
data: {
image_urls: [mockImageUrl],
},
id: 'img-seeded',
metadata: {
failed_count: '0',
success_count: '1',
},
}),
});
const payload: CreateImagePayload = {
model: 'image-01',
params: {
prompt: 'Reproducible image with seed',
seed: 42,
},
};
const result = await createMiniMaxImage(payload, mockOptions);
expect(fetch).toHaveBeenCalledWith(
'https://api.minimaxi.com/v1/image_generation',
expect.objectContaining({
body: JSON.stringify({
aspect_ratio: undefined,
model: 'image-01',
n: 1,
prompt: 'Reproducible image with seed',
seed: 42,
}),
}),
);
expect(result).toEqual({
imageUrl: mockImageUrl,
});
});
it('should handle seed value of 0 correctly', async () => {
const mockImageUrl = 'https://minimax-cdn.com/images/generated/zero-seed.jpg';
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
json: async () => ({
base_resp: {
status_code: 0,
status_msg: 'success',
},
data: {
image_urls: [mockImageUrl],
},
id: 'img-zero-seed',
metadata: {
failed_count: '0',
success_count: '1',
},
}),
});
const payload: CreateImagePayload = {
model: 'image-01',
params: {
prompt: 'Image with seed 0',
seed: 0,
},
};
await createMiniMaxImage(payload, mockOptions);
// Verify that seed: 0 is included in the request
expect(fetch).toHaveBeenCalledWith(
'https://api.minimaxi.com/v1/image_generation',
expect.objectContaining({
body: JSON.stringify({
aspect_ratio: undefined,
model: 'image-01',
n: 1,
prompt: 'Image with seed 0',
seed: 0,
}),
}),
);
});
it('should handle multiple generated images and return the first one', async () => {
const mockImageUrls = [
'https://minimax-cdn.com/images/generated/image-1.jpg',
'https://minimax-cdn.com/images/generated/image-2.jpg',
];
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
json: async () => ({
base_resp: {
status_code: 0,
status_msg: 'success',
},
data: {
image_urls: mockImageUrls,
},
id: 'img-multiple',
metadata: {
failed_count: '0',
success_count: '2',
},
}),
});
const payload: CreateImagePayload = {
model: 'image-01',
params: {
prompt: 'Multiple images test',
},
};
const result = await createMiniMaxImage(payload, mockOptions);
expect(result).toEqual({
imageUrl: mockImageUrls[0], // Should return the first image
});
});
it('should handle partial failures gracefully', async () => {
const mockImageUrl = 'https://minimax-cdn.com/images/generated/partial-success.jpg';
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
json: async () => ({
base_resp: {
status_code: 0,
status_msg: 'success',
},
data: {
image_urls: [mockImageUrl],
},
id: 'img-partial',
metadata: {
failed_count: '2',
success_count: '1',
},
}),
});
const payload: CreateImagePayload = {
model: 'image-01',
params: {
prompt: 'Test partial failure',
},
};
const result = await createMiniMaxImage(payload, mockOptions);
expect(result).toEqual({
imageUrl: mockImageUrl,
});
});
});
describe('Error scenarios', () => {
it('should handle HTTP error responses', async () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: false,
status: 400,
statusText: 'Bad Request',
json: async () => ({
base_resp: {
status_code: 1001,
status_msg: 'Invalid prompt format',
},
}),
});
const payload: CreateImagePayload = {
model: 'image-01',
params: {
prompt: 'Invalid prompt',
},
};
await expect(createMiniMaxImage(payload, mockOptions)).rejects.toEqual(
expect.objectContaining({
errorType: 'ProviderBizError',
provider: 'minimax',
}),
);
});
it('should handle non-JSON error responses', async () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: false,
status: 500,
statusText: 'Internal Server Error',
json: async () => {
throw new Error('Failed to parse JSON');
},
});
const payload: CreateImagePayload = {
model: 'image-01',
params: {
prompt: 'Test prompt',
},
};
await expect(createMiniMaxImage(payload, mockOptions)).rejects.toEqual(
expect.objectContaining({
errorType: 'ProviderBizError',
provider: 'minimax',
}),
);
});
it('should handle API error status codes', async () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
json: async () => ({
base_resp: {
status_code: 1002,
status_msg: 'Content policy violation',
},
data: {
image_urls: [],
},
id: 'img-error',
metadata: {
failed_count: '1',
success_count: '0',
},
}),
});
const payload: CreateImagePayload = {
model: 'image-01',
params: {
prompt: 'Inappropriate content',
},
};
await expect(createMiniMaxImage(payload, mockOptions)).rejects.toEqual(
expect.objectContaining({
errorType: 'ProviderBizError',
provider: 'minimax',
}),
);
});
it('should handle empty image URLs array', async () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
json: async () => ({
base_resp: {
status_code: 0,
status_msg: 'success',
},
data: {
image_urls: [],
},
id: 'img-empty',
metadata: {
failed_count: '1',
success_count: '0',
},
}),
});
const payload: CreateImagePayload = {
model: 'image-01',
params: {
prompt: 'Empty result test',
},
};
await expect(createMiniMaxImage(payload, mockOptions)).rejects.toEqual(
expect.objectContaining({
errorType: 'ProviderBizError',
provider: 'minimax',
}),
);
});
it('should handle missing data field', async () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
json: async () => ({
base_resp: {
status_code: 0,
status_msg: 'success',
},
id: 'img-no-data',
metadata: {
failed_count: '0',
success_count: '1',
},
}),
});
const payload: CreateImagePayload = {
model: 'image-01',
params: {
prompt: 'Missing data test',
},
};
await expect(createMiniMaxImage(payload, mockOptions)).rejects.toEqual(
expect.objectContaining({
errorType: 'ProviderBizError',
provider: 'minimax',
}),
);
});
it('should handle null/empty image URL', async () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
json: async () => ({
base_resp: {
status_code: 0,
status_msg: 'success',
},
data: {
image_urls: [''], // Empty string URL
},
id: 'img-empty-url',
metadata: {
failed_count: '0',
success_count: '1',
},
}),
});
const payload: CreateImagePayload = {
model: 'image-01',
params: {
prompt: 'Empty URL test',
},
};
await expect(createMiniMaxImage(payload, mockOptions)).rejects.toEqual(
expect.objectContaining({
errorType: 'ProviderBizError',
provider: 'minimax',
}),
);
});
it('should handle network errors', async () => {
global.fetch = vi.fn().mockRejectedValueOnce(new Error('Network connection failed'));
const payload: CreateImagePayload = {
model: 'image-01',
params: {
prompt: 'Network error test',
},
};
await expect(createMiniMaxImage(payload, mockOptions)).rejects.toEqual(
expect.objectContaining({
errorType: 'ProviderBizError',
provider: 'minimax',
}),
);
});
it('should handle unauthorized access', async () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: false,
status: 401,
statusText: 'Unauthorized',
json: async () => ({
base_resp: {
status_code: 1003,
status_msg: 'Invalid API key',
},
}),
});
const payload: CreateImagePayload = {
model: 'image-01',
params: {
prompt: 'Unauthorized test',
},
};
await expect(createMiniMaxImage(payload, mockOptions)).rejects.toEqual(
expect.objectContaining({
errorType: 'ProviderBizError',
provider: 'minimax',
}),
);
});
it('should handle malformed JSON response', async () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
json: async () => {
throw new Error('Unexpected token in JSON');
},
});
const payload: CreateImagePayload = {
model: 'image-01',
params: {
prompt: 'JSON error test',
},
};
await expect(createMiniMaxImage(payload, mockOptions)).rejects.toEqual(
expect.objectContaining({
errorType: 'ProviderBizError',
provider: 'minimax',
}),
);
});
});
});
@@ -0,0 +1,105 @@
import createDebug from 'debug';
import { CreateImagePayload, CreateImageResponse } from '../types/image';
import { AgentRuntimeError } from '../utils/createError';
import { CreateImageOptions } from '../utils/openaiCompatibleFactory';
const log = createDebug('lobe-image:minimax');
interface MiniMaxImageResponse {
base_resp: {
status_code: number;
status_msg: string;
};
data: {
image_urls: string[];
};
id: string;
metadata: {
failed_count: string;
success_count: string;
};
}
/**
* Create image using MiniMax API
*/
export async function createMiniMaxImage(
payload: CreateImagePayload,
options: CreateImageOptions,
): Promise<CreateImageResponse> {
const { apiKey, baseURL, provider } = options;
const { model, params } = payload;
try {
const endpoint = `${baseURL}/image_generation`;
const response = await fetch(endpoint, {
body: JSON.stringify({
aspect_ratio: params.aspectRatio,
model,
n: 1,
prompt: params.prompt,
//prompt_optimizer: true, // 开启 prompt 自动优化
...(typeof params.seed === 'number' ? { seed: params.seed } : {}),
}),
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
method: 'POST',
});
if (!response.ok) {
let errorData;
try {
errorData = await response.json();
} catch {
// Failed to parse JSON error response
}
throw new Error(
`MiniMax API error (${response.status}): ${errorData?.base_resp || response.statusText}`
);
}
const data: MiniMaxImageResponse = await response.json();
log('Image generation response: %O', data);
// Check API response status
if (data.base_resp.status_code !== 0) {
throw new Error(`MiniMax API error: ${data.base_resp.status_msg}`);
}
// Check if we have valid image data
if (!data.data?.image_urls || data.data.image_urls.length === 0) {
throw new Error('No images generated in response');
}
// Log generation statistics
const successCount = parseInt(data.metadata.success_count);
const failedCount = parseInt(data.metadata.failed_count);
log('Image generation completed: %d successful, %d failed', successCount, failedCount);
// Return the first generated image URL
const imageUrl = data.data.image_urls[0];
if (!imageUrl) {
throw new Error('No valid image URL in response');
}
log('Image generated successfully: %s', imageUrl);
return { imageUrl };
} catch (error) {
log('Error in createMiniMaxImage: %O', error);
throw AgentRuntimeError.createImage({
error: error as any,
errorType: 'ProviderBizError',
provider,
});
}
}
+2
View File
@@ -2,6 +2,7 @@ import minimaxChatModels from '@/config/aiModels/minimax';
import { ModelProvider } from '../types';
import { createOpenAICompatibleRuntime } from '../utils/openaiCompatibleFactory';
import { createMiniMaxImage } from './createImage';
export const getMinimaxMaxOutputs = (modelId: string): number | undefined => {
const model = minimaxChatModels.find((model) => model.id === modelId);
@@ -34,6 +35,7 @@ export const LobeMinimaxAI = createOpenAICompatibleRuntime({
} as any;
},
},
createImage: createMiniMaxImage,
debug: {
chatCompletion: () => process.env.DEBUG_MINIMAX_CHAT_COMPLETION === '1',
},