mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
💄 style: add Tencent Hunyuan 3.0 ImageGen support (#13166)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { AIChatModelCard } from '../types/aiModel';
|
||||
import type { AIChatModelCard, AIImageModelCard } from '../types/aiModel';
|
||||
|
||||
// https://cloud.tencent.com/document/product/1729/104753
|
||||
const hunyuanChatModels: AIChatModelCard[] = [
|
||||
@@ -513,6 +513,31 @@ const hunyuanChatModels: AIChatModelCard[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...hunyuanChatModels];
|
||||
const hunyuanImageModels: AIImageModelCard[] = [
|
||||
{
|
||||
description:
|
||||
'Powerful original-image feature extraction and detail preservation capabilities, delivering richer visual texture and producing high-accuracy, well-composed, production-grade visuals.',
|
||||
displayName: 'HY-Image-V3.0',
|
||||
enabled: true,
|
||||
id: 'HY-Image-V3.0',
|
||||
parameters: {
|
||||
height: { default: 1024, max: 2048, min: 512, step: 1 },
|
||||
imageUrls: { default: [], maxCount: 3 },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 1024, max: 2048, min: 512, step: 1 },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [{ name: 'imageGeneration', rate: 0.2, strategy: 'fixed', unit: 'image' }],
|
||||
},
|
||||
releasedAt: '2026-01-26',
|
||||
type: 'image',
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...hunyuanChatModels, ...hunyuanImageModels];
|
||||
|
||||
export default allModels;
|
||||
|
||||
543
packages/model-runtime/src/providers/hunyuan/createImage.test.ts
Normal file
543
packages/model-runtime/src/providers/hunyuan/createImage.test.ts
Normal file
@@ -0,0 +1,543 @@
|
||||
// @vitest-environment node
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { CreateImageOptions } from '../../core/openaiCompatibleFactory';
|
||||
import type { CreateImagePayload } from '../../types/image';
|
||||
import { createHunyuanImage } from './createImage';
|
||||
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
const mockOptions: CreateImageOptions = {
|
||||
apiKey: 'sk-test-api-key',
|
||||
baseURL: 'https://api.cloudai.tencent.com/v1',
|
||||
provider: 'hunyuan',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('createHunyuanImage', () => {
|
||||
describe('Success scenarios', () => {
|
||||
it('should successfully generate image with basic prompt', async () => {
|
||||
const mockJobId = '1301052320-1774048771-3ff52e2c-24b3-11f1-aca3-525400cc0b9a-0';
|
||||
const mockImageUrl = 'https://aiart-1258344699.cos.ap-guangzhou.myqcloud.com/test/image.png';
|
||||
|
||||
global.fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
request_id: 'req-123',
|
||||
job_id: mockJobId,
|
||||
}),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
request_id: 'req-456',
|
||||
status: '5',
|
||||
data: [{ url: mockImageUrl }],
|
||||
}),
|
||||
});
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'HY-Image-V3.0',
|
||||
params: {
|
||||
prompt: '生成一个可爱猫猫',
|
||||
},
|
||||
};
|
||||
|
||||
const resultPromise = createHunyuanImage(payload, mockOptions);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
const result = await resultPromise;
|
||||
|
||||
const submitCall = (fetch as any).mock.calls[0];
|
||||
expect(submitCall[0]).toBe('https://api.cloudai.tencent.com/v1/aiart/submit');
|
||||
const submitBody = JSON.parse(submitCall[1].body);
|
||||
expect(submitBody).toEqual({
|
||||
model: 'HY-Image-V3.0',
|
||||
prompt: '生成一个可爱猫猫',
|
||||
size: '1024:1024',
|
||||
extra_body: {
|
||||
logo_add: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const queryCall = (fetch as any).mock.calls[1];
|
||||
expect(queryCall[0]).toBe('https://api.cloudai.tencent.com/v1/aiart/query');
|
||||
const queryBody = JSON.parse(queryCall[1].body);
|
||||
expect(queryBody).toEqual({ job_id: mockJobId });
|
||||
|
||||
expect(result).toEqual({
|
||||
imageUrl: mockImageUrl,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle custom size parameter', async () => {
|
||||
const mockJobId = 'job-custom-size';
|
||||
const mockImageUrl = 'https://aiart.tencent.com/test/custom.png';
|
||||
|
||||
global.fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ job_id: mockJobId }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
status: '5',
|
||||
data: [{ url: mockImageUrl }],
|
||||
}),
|
||||
});
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'HY-Image-V3.0',
|
||||
params: {
|
||||
prompt: 'Custom size test',
|
||||
size: '1024x1024',
|
||||
},
|
||||
};
|
||||
|
||||
const resultPromise = createHunyuanImage(payload, mockOptions);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
const result = await resultPromise;
|
||||
|
||||
const submitBody = JSON.parse((fetch as any).mock.calls[0][1].body);
|
||||
expect(submitBody.size).toBe('1024:1024');
|
||||
expect(result.imageUrl).toBe(mockImageUrl);
|
||||
});
|
||||
|
||||
it('should handle width and height parameters', async () => {
|
||||
const mockJobId = 'job-dims';
|
||||
const mockImageUrl = 'https://aiart.tencent.com/test/dims.png';
|
||||
|
||||
global.fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ job_id: mockJobId }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
status: '5',
|
||||
data: [{ url: mockImageUrl }],
|
||||
}),
|
||||
});
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'HY-Image-V3.0',
|
||||
params: {
|
||||
prompt: 'Custom dimensions',
|
||||
width: 1024,
|
||||
height: 768,
|
||||
},
|
||||
};
|
||||
|
||||
const resultPromise = createHunyuanImage(payload, mockOptions);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
const result = await resultPromise;
|
||||
|
||||
const submitBody = JSON.parse((fetch as any).mock.calls[0][1].body);
|
||||
expect(submitBody.size).toBe('1024:768');
|
||||
expect(result.imageUrl).toBe(mockImageUrl);
|
||||
});
|
||||
|
||||
it('should handle seed parameter', async () => {
|
||||
const mockJobId = 'job-seed';
|
||||
const mockImageUrl = 'https://aiart.tencent.com/test/seed.png';
|
||||
|
||||
global.fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ job_id: mockJobId }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
status: '5',
|
||||
data: [{ url: mockImageUrl }],
|
||||
}),
|
||||
});
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'HY-Image-V3.0',
|
||||
params: {
|
||||
prompt: 'With seed',
|
||||
seed: 84445,
|
||||
},
|
||||
};
|
||||
|
||||
const resultPromise = createHunyuanImage(payload, mockOptions);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
await resultPromise;
|
||||
|
||||
const submitBody = JSON.parse((fetch as any).mock.calls[0][1].body);
|
||||
expect(submitBody.extra_body.seed).toBe(84445);
|
||||
});
|
||||
|
||||
it('should handle imageUrls for image-to-image', async () => {
|
||||
const mockJobId = 'job-img2img';
|
||||
const mockImageUrl = 'https://aiart.tencent.com/test/edited.png';
|
||||
|
||||
global.fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ job_id: mockJobId }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
status: '5',
|
||||
data: [{ url: mockImageUrl }],
|
||||
}),
|
||||
});
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'HY-Image-V3.0',
|
||||
params: {
|
||||
prompt: 'Add a cat',
|
||||
imageUrls: ['https://example.com/source.png'],
|
||||
},
|
||||
};
|
||||
|
||||
const resultPromise = createHunyuanImage(payload, mockOptions);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
const result = await resultPromise;
|
||||
|
||||
const submitBody = JSON.parse((fetch as any).mock.calls[0][1].body);
|
||||
expect(submitBody.images).toEqual(['https://example.com/source.png']);
|
||||
expect(result.imageUrl).toBe(mockImageUrl);
|
||||
});
|
||||
|
||||
it('should poll multiple times until completion', async () => {
|
||||
const mockJobId = 'job-polling';
|
||||
const mockImageUrl = 'https://aiart.tencent.com/test/final.png';
|
||||
|
||||
global.fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ job_id: mockJobId }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ status: '1' }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ status: '2' }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
status: '5',
|
||||
data: [{ url: mockImageUrl }],
|
||||
}),
|
||||
});
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'HY-Image-V3.0',
|
||||
params: {
|
||||
prompt: 'Polling test',
|
||||
},
|
||||
};
|
||||
|
||||
const resultPromise = createHunyuanImage(payload, mockOptions);
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
const result = await resultPromise;
|
||||
|
||||
expect(result.imageUrl).toBe(mockImageUrl);
|
||||
expect((fetch as any).mock.calls.length).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error scenarios - Submit endpoint', () => {
|
||||
it('should handle 401 unauthorized error', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 401,
|
||||
json: async () => ({
|
||||
error: {
|
||||
message: 'Incorrect API key provided',
|
||||
type: 'invalid_request_error',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'HY-Image-V3.0',
|
||||
params: { prompt: 'Test' },
|
||||
};
|
||||
|
||||
await expect(createHunyuanImage(payload, mockOptions)).rejects.toEqual(
|
||||
expect.objectContaining({
|
||||
errorType: 'ProviderBizError',
|
||||
provider: 'hunyuan',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle image download error', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
request_id: 'req-123',
|
||||
job_id: '',
|
||||
error: {
|
||||
message: '图片下载错误。',
|
||||
type: 'api_error',
|
||||
code: 'FailedOperation.ImageDownloadError',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'HY-Image-V3.0',
|
||||
params: { prompt: 'Test' },
|
||||
};
|
||||
|
||||
await expect(createHunyuanImage(payload, mockOptions)).rejects.toEqual(
|
||||
expect.objectContaining({
|
||||
errorType: 'ProviderBizError',
|
||||
provider: 'hunyuan',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle missing job_id', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ request_id: 'req-123' }),
|
||||
});
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'HY-Image-V3.0',
|
||||
params: { prompt: 'Test' },
|
||||
};
|
||||
|
||||
await expect(createHunyuanImage(payload, mockOptions)).rejects.toEqual(
|
||||
expect.objectContaining({
|
||||
errorType: 'ProviderBizError',
|
||||
provider: 'hunyuan',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle HTTP error with error.message format', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 400,
|
||||
json: async () => ({
|
||||
message: 'Invalid prompt',
|
||||
}),
|
||||
});
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'HY-Image-V3.0',
|
||||
params: { prompt: 'Invalid' },
|
||||
};
|
||||
|
||||
await expect(createHunyuanImage(payload, mockOptions)).rejects.toEqual(
|
||||
expect.objectContaining({
|
||||
errorType: 'ProviderBizError',
|
||||
provider: 'hunyuan',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error scenarios - Query endpoint', () => {
|
||||
it('should handle API error in query response', async () => {
|
||||
const mockJobId = 'job-query-error';
|
||||
|
||||
global.fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ job_id: mockJobId }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
status: '',
|
||||
error: {
|
||||
message: 'Unknown job status: ',
|
||||
type: 'api_error',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'HY-Image-V3.0',
|
||||
params: { prompt: 'Test' },
|
||||
};
|
||||
|
||||
try {
|
||||
await createHunyuanImage(payload, mockOptions);
|
||||
expect.fail('Expected error to be thrown');
|
||||
} catch (error) {
|
||||
expect(error).toEqual(
|
||||
expect.objectContaining({
|
||||
errorType: 'ProviderBizError',
|
||||
provider: 'hunyuan',
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle missing status in query response', async () => {
|
||||
const mockJobId = 'job-no-status';
|
||||
|
||||
global.fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ job_id: mockJobId }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({}),
|
||||
});
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'HY-Image-V3.0',
|
||||
params: { prompt: 'Test' },
|
||||
};
|
||||
|
||||
try {
|
||||
await createHunyuanImage(payload, mockOptions);
|
||||
expect.fail('Expected error to be thrown');
|
||||
} catch (error) {
|
||||
expect(error).toEqual(
|
||||
expect.objectContaining({
|
||||
errorType: 'ProviderBizError',
|
||||
provider: 'hunyuan',
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle failed status', async () => {
|
||||
const mockJobId = 'job-failed';
|
||||
|
||||
global.fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ job_id: mockJobId }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ status: '4' }),
|
||||
});
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'HY-Image-V3.0',
|
||||
params: { prompt: 'Test' },
|
||||
};
|
||||
|
||||
try {
|
||||
await createHunyuanImage(payload, mockOptions);
|
||||
expect.fail('Expected error to be thrown');
|
||||
} catch (error) {
|
||||
expect(error).toEqual(
|
||||
expect.objectContaining({
|
||||
errorType: 'ProviderBizError',
|
||||
provider: 'hunyuan',
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle completed status with empty data', async () => {
|
||||
const mockJobId = 'job-empty-data';
|
||||
|
||||
global.fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ job_id: mockJobId }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ status: '5', data: null }),
|
||||
});
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'HY-Image-V3.0',
|
||||
params: { prompt: 'Test' },
|
||||
};
|
||||
|
||||
try {
|
||||
await createHunyuanImage(payload, mockOptions);
|
||||
expect.fail('Expected error to be thrown');
|
||||
} catch (error) {
|
||||
expect(error).toEqual(
|
||||
expect.objectContaining({
|
||||
errorType: 'ProviderBizError',
|
||||
provider: 'hunyuan',
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle completed status with empty images array', async () => {
|
||||
const mockJobId = 'job-empty-images';
|
||||
|
||||
global.fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ job_id: mockJobId }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ status: '5', data: [] }),
|
||||
});
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'HY-Image-V3.0',
|
||||
params: { prompt: 'Test' },
|
||||
};
|
||||
|
||||
try {
|
||||
await createHunyuanImage(payload, mockOptions);
|
||||
expect.fail('Expected error to be thrown');
|
||||
} catch (error) {
|
||||
expect(error).toEqual(
|
||||
expect.objectContaining({
|
||||
errorType: 'ProviderBizError',
|
||||
provider: 'hunyuan',
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle network error', async () => {
|
||||
global.fetch = vi.fn().mockRejectedValueOnce(new Error('Network error'));
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'HY-Image-V3.0',
|
||||
params: { prompt: 'Test' },
|
||||
};
|
||||
|
||||
await expect(createHunyuanImage(payload, mockOptions)).rejects.toEqual(
|
||||
expect.objectContaining({
|
||||
errorType: 'ProviderBizError',
|
||||
provider: 'hunyuan',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
230
packages/model-runtime/src/providers/hunyuan/createImage.ts
Normal file
230
packages/model-runtime/src/providers/hunyuan/createImage.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import createDebug from 'debug';
|
||||
|
||||
import type { CreateImageOptions } from '../../core/openaiCompatibleFactory';
|
||||
import type { CreateImagePayload, CreateImageResponse } from '../../types/image';
|
||||
import { asyncifyPolling } from '../../utils/asyncifyPolling';
|
||||
import { AgentRuntimeError } from '../../utils/createError';
|
||||
|
||||
const log = createDebug('lobe-image:hunyuan');
|
||||
|
||||
interface HunyuanImageSubmitResponse {
|
||||
error?: {
|
||||
code?: string;
|
||||
message?: string;
|
||||
type?: string;
|
||||
};
|
||||
job_id?: string;
|
||||
request_id?: string;
|
||||
}
|
||||
|
||||
interface HunyuanImageQueryResponse {
|
||||
data?: Array<{
|
||||
url: string;
|
||||
}> | null;
|
||||
error?: {
|
||||
code?: string;
|
||||
message?: string;
|
||||
type?: string;
|
||||
};
|
||||
request_id?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
// Hunyuan3.0 ImageGen Status Code
|
||||
// https://cloud.tencent.com/document/product/1668/124633
|
||||
const getStatusName = (status: string): string => {
|
||||
const statusMap: Record<string, string> = {
|
||||
'1': 'PENDING',
|
||||
'2': 'PROCESSING',
|
||||
'4': 'FAILED',
|
||||
'5': 'COMPLETED',
|
||||
};
|
||||
return statusMap[status] || `UNKNOWN(${status})`;
|
||||
};
|
||||
|
||||
export async function createHunyuanImage(
|
||||
payload: CreateImagePayload,
|
||||
options: CreateImageOptions,
|
||||
): Promise<CreateImageResponse> {
|
||||
const { apiKey, provider } = options;
|
||||
const { model, params } = payload;
|
||||
|
||||
// Hunyuan3.0 ImageGen BaseURL
|
||||
// https://cloud.tencent.com/document/product/1668/129429
|
||||
const baseURL = options.baseURL || 'https://api.cloudai.tencent.com/v1';
|
||||
|
||||
try {
|
||||
log('Starting Hunyuan image generation with model: %s and params: %O', model, params);
|
||||
|
||||
const submitUrl = `${baseURL}/aiart/submit`;
|
||||
const submitBody: Record<string, any> = {
|
||||
model,
|
||||
prompt: params.prompt,
|
||||
...(params.width && params.height
|
||||
? { size: `${params.width}:${params.height}` }
|
||||
: params.size
|
||||
? { size: params.size.replace('x', ':') }
|
||||
: { size: '1024:1024' }),
|
||||
...(params.imageUrls && params.imageUrls.length > 0
|
||||
? { images: params.imageUrls }
|
||||
: params.imageUrl
|
||||
? { images: [params.imageUrl] }
|
||||
: {}),
|
||||
extra_body: {
|
||||
logo_add: 0, // Add Watermark: 0 disabled, 1 enabled
|
||||
...(typeof params.seed === 'number' ? { seed: params.seed } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
log('Submitting task to: %s', submitUrl);
|
||||
log('Submit body: %O', submitBody);
|
||||
|
||||
const submitResponse = await fetch(submitUrl, {
|
||||
body: JSON.stringify(submitBody),
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!submitResponse.ok) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await submitResponse.json();
|
||||
} catch {}
|
||||
|
||||
const errorMessage =
|
||||
typeof errorData?.error?.message === 'string'
|
||||
? errorData.error.message
|
||||
: typeof errorData?.message === 'string'
|
||||
? errorData.message
|
||||
: JSON.stringify(errorData || submitResponse.statusText);
|
||||
|
||||
throw new Error(`Hunyuan API submit error (${submitResponse.status}): ${errorMessage}`);
|
||||
}
|
||||
|
||||
const submitData: HunyuanImageSubmitResponse = await submitResponse.json();
|
||||
log('Submit response: %O', submitData);
|
||||
|
||||
if (submitData.error?.message) {
|
||||
throw new Error(`Hunyuan API error: ${submitData.error.message}`);
|
||||
}
|
||||
|
||||
if (!submitData.job_id) {
|
||||
throw new Error(
|
||||
`No job_id returned from submit endpoint. Response: ${JSON.stringify(submitData)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const jobId = submitData.job_id;
|
||||
log('Task submitted successfully, job_id: %s', jobId);
|
||||
|
||||
const queryUrl = `${baseURL}/aiart/query`;
|
||||
|
||||
const result = await asyncifyPolling<HunyuanImageQueryResponse, CreateImageResponse>({
|
||||
checkStatus: (taskStatus: HunyuanImageQueryResponse): any => {
|
||||
log('Checking task status: %O', taskStatus);
|
||||
|
||||
if (taskStatus.error?.message) {
|
||||
log('API error response: %s', taskStatus.error.message);
|
||||
return {
|
||||
error: new Error(`Hunyuan API error: ${taskStatus.error.message}`),
|
||||
status: 'failed',
|
||||
};
|
||||
}
|
||||
|
||||
const status = taskStatus.status;
|
||||
|
||||
// Status return an empty string if query got an error
|
||||
if (!status) {
|
||||
return {
|
||||
error: new Error('Invalid query response: missing status'),
|
||||
status: 'failed',
|
||||
};
|
||||
}
|
||||
|
||||
log('Task status: %s', getStatusName(status));
|
||||
|
||||
// Task completed
|
||||
if (status === '5') {
|
||||
if (!taskStatus.data || !Array.isArray(taskStatus.data) || taskStatus.data.length === 0) {
|
||||
return {
|
||||
error: new Error('Task completed but no images generated'),
|
||||
status: 'failed',
|
||||
};
|
||||
}
|
||||
|
||||
const imageUrl = taskStatus.data[0].url;
|
||||
if (!imageUrl) {
|
||||
return {
|
||||
error: new Error('No valid image URL in response'),
|
||||
status: 'failed',
|
||||
};
|
||||
}
|
||||
|
||||
log('Image generation completed successfully: %s', imageUrl);
|
||||
return {
|
||||
data: { imageUrl },
|
||||
status: 'success',
|
||||
};
|
||||
}
|
||||
|
||||
// Task failed
|
||||
if (status === '4') {
|
||||
return {
|
||||
error: new Error('Task failed'),
|
||||
status: 'failed',
|
||||
};
|
||||
}
|
||||
|
||||
return { status: 'pending' };
|
||||
},
|
||||
logger: {
|
||||
debug: (message: any, ...args: any[]) => log(message, ...args),
|
||||
error: (message: any, ...args: any[]) => log(message, ...args),
|
||||
},
|
||||
maxConsecutiveFailures: 5,
|
||||
maxRetries: 60,
|
||||
pollingQuery: async () => {
|
||||
log('Polling task status for job_id: %s', jobId);
|
||||
|
||||
const queryResponse = await fetch(queryUrl, {
|
||||
body: JSON.stringify({ job_id: jobId }),
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!queryResponse.ok) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await queryResponse.json();
|
||||
} catch {}
|
||||
|
||||
const errorMessage =
|
||||
typeof errorData?.message === 'string'
|
||||
? errorData.message
|
||||
: JSON.stringify(errorData || queryResponse.statusText);
|
||||
|
||||
throw new Error(`Hunyuan API query error (${queryResponse.status}): ${errorMessage}`);
|
||||
}
|
||||
|
||||
return await queryResponse.json();
|
||||
},
|
||||
});
|
||||
|
||||
log('Image generation completed: %O', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
log('Error in createHunyuanImage: %O', error);
|
||||
|
||||
throw AgentRuntimeError.createImage({
|
||||
error: error as any,
|
||||
errorType: 'ProviderBizError',
|
||||
provider,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { ModelProvider } from 'model-bank';
|
||||
|
||||
import type { OpenAICompatibleFactoryOptions } from '../../core/openaiCompatibleFactory';
|
||||
import { createOpenAICompatibleRuntime } from '../../core/openaiCompatibleFactory';
|
||||
import { createHunyuanImage } from './createImage';
|
||||
|
||||
export interface HunyuanModelCard {
|
||||
id: string;
|
||||
@@ -38,6 +39,7 @@ export const params = {
|
||||
} as any;
|
||||
},
|
||||
},
|
||||
createImage: createHunyuanImage,
|
||||
debug: {
|
||||
chatCompletion: () => process.env.DEBUG_HUNYUAN_CHAT_COMPLETION === '1',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user