Fix Seedance runtime model handling
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
resolveSeedanceActualModel,
|
||||
seedanceVideoAdapter,
|
||||
} from '../model-adapters/seedance-adapter';
|
||||
import type { AdapterContext } from '../model-adapters/types';
|
||||
|
||||
describe('seedance video adapter', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('keeps runtime Doubao Seedance model ids unchanged', () => {
|
||||
expect(
|
||||
resolveSeedanceActualModel('doubao-seedance-2-0-fast-260128', '720p')
|
||||
).toBe('doubao-seedance-2-0-fast-260128');
|
||||
});
|
||||
|
||||
it('maps built-in Seedance aliases to concrete API model ids', () => {
|
||||
expect(resolveSeedanceActualModel('seedance-1.5-pro', '720p')).toBe(
|
||||
'doubao-seedance-1-5-pro_720p'
|
||||
);
|
||||
});
|
||||
|
||||
it('submits runtime Doubao Seedance model ids without adding a second prefix', async () => {
|
||||
const requests: Array<{ url: string; init: RequestInit }> = [];
|
||||
const fetcher = vi.fn(
|
||||
async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
requests.push({ url: String(input), init: init || {} });
|
||||
if ((init?.method || 'GET') === 'POST') {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: 'task-1',
|
||||
status: 'queued',
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: 'task-1',
|
||||
status: 'succeeded',
|
||||
progress: 100,
|
||||
url: 'https://cdn.example.com/seedance.mp4',
|
||||
seconds: '5',
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const context: AdapterContext = {
|
||||
baseUrl: 'https://truemodel.benchu.cloud/v1',
|
||||
apiKey: 'sk-test',
|
||||
authType: 'bearer',
|
||||
operation: 'video',
|
||||
fetcher,
|
||||
};
|
||||
|
||||
const resultPromise = seedanceVideoAdapter.generateVideo(context, {
|
||||
model: 'doubao-seedance-2-0-fast-260128',
|
||||
prompt: 'tiny city at night',
|
||||
size: '720p@16:9',
|
||||
duration: 5,
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
const result = await resultPromise;
|
||||
|
||||
const body = requests[0]?.init.body as FormData;
|
||||
expect(result.url).toBe('https://cdn.example.com/seedance.mp4');
|
||||
expect(requests[0]?.url).toBe('/api/truemodel/v1/videos');
|
||||
expect(body.get('model')).toBe('doubao-seedance-2-0-fast-260128');
|
||||
expect(body.get('model')).not.toBe(
|
||||
'doubao-doubao-seedance-2-0-fast-260128_720p'
|
||||
);
|
||||
expect(body.get('size')).toBe('16:9');
|
||||
expect(requests[1]?.url).toBe('/api/truemodel/v1/videos/task-1');
|
||||
});
|
||||
});
|
||||
@@ -15,14 +15,28 @@ const SEEDANCE_MODELS = [
|
||||
|
||||
type SeedanceSubmitResponse = {
|
||||
id: string;
|
||||
status: 'queued' | 'processing' | 'completed' | 'failed';
|
||||
status:
|
||||
| 'queued'
|
||||
| 'processing'
|
||||
| 'in_progress'
|
||||
| 'completed'
|
||||
| 'succeeded'
|
||||
| 'failed'
|
||||
| 'error';
|
||||
created_at?: number;
|
||||
error?: string | { code: string; message: string };
|
||||
};
|
||||
|
||||
type SeedanceQueryResponse = {
|
||||
id: string;
|
||||
status: 'queued' | 'processing' | 'completed' | 'failed';
|
||||
status:
|
||||
| 'queued'
|
||||
| 'processing'
|
||||
| 'in_progress'
|
||||
| 'completed'
|
||||
| 'succeeded'
|
||||
| 'failed'
|
||||
| 'error';
|
||||
progress?: number;
|
||||
video_url?: string;
|
||||
url?: string;
|
||||
@@ -36,10 +50,20 @@ const DEFAULT_POLL_MAX_ATTEMPTS = 1080; // ~90 min
|
||||
/**
|
||||
* 将逻辑模型 ID + 分辨率拼接为 API 实际模型名
|
||||
* seedance-1.5-pro + 720p → doubao-seedance-1-5-pro_720p
|
||||
* 运行时发现的 doubao-seedance-* 已经是真实供应商模型名,必须原样发送。
|
||||
*/
|
||||
const resolveActualModel = (logicalId: string, resolution: string): string => {
|
||||
export const resolveSeedanceActualModel = (
|
||||
logicalId: string,
|
||||
resolution: string
|
||||
): string => {
|
||||
const normalizedLogicalId = logicalId.trim();
|
||||
const lowerId = normalizedLogicalId.toLowerCase();
|
||||
if (lowerId.startsWith('doubao-seedance-')) {
|
||||
return normalizedLogicalId;
|
||||
}
|
||||
|
||||
// 将 "." 替换为 "-":seedance-1.5-pro → seedance-1-5-pro
|
||||
const normalized = logicalId.replace(/\./g, '-');
|
||||
const normalized = normalizedLogicalId.replace(/\./g, '-');
|
||||
return `doubao-${normalized}_${resolution}`;
|
||||
};
|
||||
|
||||
@@ -208,12 +232,14 @@ export const seedanceVideoAdapter: VideoModelAdapter = {
|
||||
async generateVideo(context, request: VideoGenerationRequest) {
|
||||
const logicalModel = request.model || 'seedance-1.5-pro';
|
||||
const resolution = extractResolution(request.size);
|
||||
const actualModel = resolveActualModel(logicalModel, resolution);
|
||||
const actualModel = resolveSeedanceActualModel(logicalModel, resolution);
|
||||
const parsedSize = parseSeedanceSize(request.size);
|
||||
|
||||
// 宽高比优先取显式参数,其次回退到 size 中的组合值
|
||||
const aspectRatio =
|
||||
normalizeAspectRatio(request.params?.aspect_ratio as string | undefined) ||
|
||||
normalizeAspectRatio(
|
||||
request.params?.aspect_ratio as string | undefined
|
||||
) ||
|
||||
normalizeAspectRatio(request.params?.aspectRatio as string | undefined) ||
|
||||
parsedSize.aspectRatio ||
|
||||
'16:9';
|
||||
@@ -253,7 +279,7 @@ export const seedanceVideoAdapter: VideoModelAdapter = {
|
||||
onSubmitted?.(taskId);
|
||||
|
||||
// 提交时已失败
|
||||
if (submitResult.status === 'failed') {
|
||||
if (submitResult.status === 'failed' || submitResult.status === 'error') {
|
||||
throw new Error(extractErrorMessage(submitResult.error));
|
||||
}
|
||||
|
||||
@@ -281,11 +307,11 @@ export const seedanceVideoAdapter: VideoModelAdapter = {
|
||||
(status.status === 'failed'
|
||||
? 100
|
||||
: status.status === 'completed'
|
||||
? 100
|
||||
: 0);
|
||||
? 100
|
||||
: 0);
|
||||
onProgress?.(progress, status.status);
|
||||
|
||||
if (status.status === 'completed') {
|
||||
if (status.status === 'completed' || status.status === 'succeeded') {
|
||||
const url = status.video_url || status.url;
|
||||
if (!url) {
|
||||
throw new Error('Seedance 结果缺少视频 URL');
|
||||
@@ -298,7 +324,7 @@ export const seedanceVideoAdapter: VideoModelAdapter = {
|
||||
};
|
||||
}
|
||||
|
||||
if (status.status === 'failed') {
|
||||
if (status.status === 'failed' || status.status === 'error') {
|
||||
isBusinessFailure = true;
|
||||
throw new Error(extractErrorMessage(status.error));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user