Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions apps/gateway/src/videos/videos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,15 @@ function getAvalancheSoraTaskModelName(
return `${baseModelName}-${inputMode === "reference" ? "image" : "text"}-to-video`;
}

function getBytedanceVideoUpstreamModelName(baseModelName: string): string {
const modelNameMap: Record<string, string> = {
"seedance-2-0": "dreamina-seedance-2-0-260128",
"seedance-2-0-fast": "dreamina-seedance-2-0-fast-260128",
"seedance-1-5-pro": "seedance-1-5-pro-251215",
};
return modelNameMap[baseModelName] ?? baseModelName;
}

function getVideoUpstreamModelName(
providerId: Provider,
baseModelName: string,
Expand All @@ -920,6 +929,8 @@ function getVideoUpstreamModelName(
switch (providerId) {
case "avalanche":
return getAvalancheVideoModelName(baseModelName);
case "bytedance":
return getBytedanceVideoUpstreamModelName(baseModelName);
case "google-vertex":
return baseModelName;
default:
Expand Down Expand Up @@ -969,6 +980,8 @@ function getDefaultVideoProviderBaseUrl(providerId: Provider): string | null {
switch (providerId) {
case "openai":
return "https://api.openai.com";
case "bytedance":
return "https://ark.ap-southeast.bytepluses.com/api/v3";
case "google-vertex":
return "https://aiplatform.googleapis.com";
default:
Expand Down Expand Up @@ -2804,6 +2817,108 @@ async function createGoogleVertexVideoJob(
};
}

function getBytedanceVideoAspectRatio(videoSize: VideoSizeConfig): string {
if (videoSize.orientation === "portrait") {
return "9:16";
}
return "16:9";
}

async function createBytedanceVideoJob(
providerContext: ProviderContext,
providerMapping: ProviderModelMapping,
videoSize: VideoSizeConfig,
prompt: string,
durationSeconds: number,
includeAudio: boolean,
firstFrameInput: VideoImageInput | undefined,
processedFirstFrame: ProcessedVideoImageInput | null,
processedReferenceImages: ProcessedVideoImageInput[],
): Promise<{
upstreamId: string;
upstreamRequest: Record<string, unknown>;
upstreamResponse: Record<string, unknown>;
}> {
const upstreamModelName = getBytedanceVideoUpstreamModelName(
providerMapping.modelName,
);
const content: Array<Record<string, unknown>> = [
{
type: "text",
text: prompt,
},
];

if (processedFirstFrame) {
content.push({
type: "image_url",
image_url: {
url: `data:${processedFirstFrame.mimeType};base64,${processedFirstFrame.bytesBase64Encoded}`,
},
});
}

if (processedReferenceImages.length > 0) {
for (const image of processedReferenceImages) {
content.push({
type: "image_url",
image_url: {
url: `data:${image.mimeType};base64,${image.bytesBase64Encoded}`,
},
});
}
}

const isDreaminaModel = upstreamModelName.startsWith("dreamina-");

const upstreamRequest: Record<string, unknown> = {
model: upstreamModelName,
content,
duration: isDreaminaModel ? `${durationSeconds}s` : durationSeconds,
aspect_ratio: getBytedanceVideoAspectRatio(videoSize),
generate_audio: includeAudio,
};

if (isDreaminaModel) {
upstreamRequest.resolution =
videoSize.resolution === "1080p" ? "1080p" : "720p";
}

const upstreamUrl = joinUrl(
providerContext.baseUrl,
"/contents/generations/tasks",
);
const rawResponse = await fetchUpstreamJson(upstreamUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
...getProviderHeaders("bytedance", providerContext.token, {
requestId: providerContext.requestId,
}),
},
body: JSON.stringify(upstreamRequest),
});

const upstreamResponse = addRequestedVideoMetadata(
{
...rawResponse,
status: rawResponse.status ?? "queued",
duration: durationSeconds,
aspect_ratio: upstreamRequest.aspect_ratio,
},
videoSize,
);

const upstreamId = extractUpstreamVideoId(upstreamResponse);
if (!upstreamId) {
throw new HTTPException(502, {
message: "ByteDance video response did not include a task id",
});
}

return { upstreamId, upstreamRequest, upstreamResponse };
}

async function createUpstreamVideoJob(
providerContext: ProviderContext,
providerMapping: ProviderModelMapping,
Expand Down Expand Up @@ -2856,6 +2971,18 @@ async function createUpstreamVideoJob(
lastFrameInput,
referenceImageInputs,
);
case "bytedance":
return await createBytedanceVideoJob(
providerContext,
providerMapping,
videoSize,
prompt,
durationSeconds,
includeAudio,
firstFrameInput,
processedFirstFrame,
processedReferenceImages,
);
case "google-vertex":
return await createGoogleVertexVideoJob(
providerContext,
Expand Down
72 changes: 72 additions & 0 deletions apps/worker/src/services/video-jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ function getDefaultVideoProviderBaseUrl(providerId: Provider): string | null {
switch (providerId) {
case "openai":
return "https://api.openai.com";
case "bytedance":
return "https://ark.ap-southeast.bytepluses.com/api/v3";
case "google-vertex":
return "https://aiplatform.googleapis.com";
default:
Expand Down Expand Up @@ -1791,6 +1793,72 @@ async function finalizeVideoJob(job: VideoJobRecord): Promise<void> {
}
}

function isBytedanceVideoProvider(providerId: string): boolean {
return providerId === "bytedance";
}

async function fetchBytedanceStatus(
job: VideoJobRecord,
providerContext: ResolvedVideoProviderContext,
): Promise<Record<string, unknown>> {
const url = joinUrl(
providerContext.baseUrl,
`/contents/generations/tasks/${job.upstreamId}`,
);
const { body, response } = await fetchJsonResponse(url, {
method: "GET",
headers: getVideoProviderHeaders(job, providerContext),
});

if (!response.ok) {
throw new Error(
typeof body.error === "object" &&
body.error &&
"message" in body.error &&
typeof body.error.message === "string"
? body.error.message
: `ByteDance status request failed with status ${response.status}`,
);
}

const data =
body.data && typeof body.data === "object"
? (body.data as Record<string, unknown>)
: body;

const rawStatus = typeof data.status === "string" ? data.status : "queued";
const content =
data.content && typeof data.content === "object"
? (data.content as Record<string, unknown>)
: null;
const videoUrl =
content && typeof content.video_url === "string" ? content.video_url : null;

return addRequestedVideoMetadata(job, {
...body,
status: rawStatus,
progress:
rawStatus === "succeeded" || rawStatus === "completed"
? 100
: rawStatus === "failed"
? 100
: rawStatus === "running"
? 50
: 0,
url: videoUrl,
video_url: videoUrl,
output_url: videoUrl,
mime_type: videoUrl ? "video/mp4" : undefined,
error:
rawStatus === "failed"
? (extractError(data) ?? {
message: "ByteDance video generation failed",
})
: null,
bytedance_raw_response: body,
});
}

async function fetchUpstreamStatus(
job: VideoJobRecord,
): Promise<Record<string, unknown>> {
Expand All @@ -1806,6 +1874,10 @@ async function fetchUpstreamStatus(
return await fetchGoogleVertexStatus(job, providerContext);
}

if (isBytedanceVideoProvider(job.usedProvider)) {
return await fetchBytedanceStatus(job, providerContext);
}

return await fetchGenericVideoStatus(job, providerContext);
}

Expand Down
108 changes: 108 additions & 0 deletions packages/models/src/models/bytedance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,114 @@ export const bytedanceModels = [
},
],
},
{
id: "seedance-2-0",
name: "Seedance 2.0",
description:
"ByteDance Seedance 2.0 high-quality video generation model with text-to-video and image-to-video support",
family: "bytedance",
output: ["video"],
releasedAt: new Date("2025-12-18"),
providers: [
{
test: "skip",
providerId: "bytedance",
modelName: "seedance-2-0",
Comment thread
analogpvt marked this conversation as resolved.
Resolved
inputPrice: undefined,
outputPrice: undefined,
requestPrice: undefined,
perSecondPrice: {
default_audio: 0.1512,
default_video: 0.1512,
"1080p_audio": 0.3402,
"1080p_video": 0.3402,
},
contextSize: 2000,
maxOutput: 1,
streaming: false,
vision: false,
tools: false,
jsonOutput: false,
videoGenerations: true,
supportedVideoSizes: ["1280x720", "720x1280", "1920x1080", "1080x1920"],
supportedVideoDurationsSeconds: [5, 10],
supportsVideoAudio: true,
supportsVideoWithoutAudio: true,
},
],
},
{
id: "seedance-2-0-fast",
name: "Seedance 2.0 Fast",
description:
"ByteDance Seedance 2.0 Fast - accelerated video generation model for quicker outputs",
family: "bytedance",
output: ["video"],
releasedAt: new Date("2025-12-18"),
providers: [
{
test: "skip",
providerId: "bytedance",
modelName: "seedance-2-0-fast",
inputPrice: undefined,
outputPrice: undefined,
requestPrice: undefined,
perSecondPrice: {
default_audio: 0.05184,
default_video: 0.02592,
"1080p_audio": 0.1166,
"1080p_video": 0.05832,
},
contextSize: 2000,
maxOutput: 1,
streaming: false,
vision: false,
tools: false,
jsonOutput: false,
videoGenerations: true,
supportedVideoSizes: ["1280x720", "720x1280", "1920x1080", "1080x1920"],
supportedVideoDurationsSeconds: [5, 10],
supportsVideoAudio: true,
supportsVideoWithoutAudio: true,
},
],
},
{
id: "seedance-1-5-pro",
name: "Seedance 1.5 Pro",
description:
"ByteDance Seedance 1.5 Pro - professional video generation model with complex instruction following and native audio",
family: "bytedance",
output: ["video"],
releasedAt: new Date("2025-09-15"),
providers: [
{
test: "skip",
providerId: "bytedance",
modelName: "seedance-1-5-pro",
inputPrice: undefined,
outputPrice: undefined,
requestPrice: undefined,
perSecondPrice: {
default_audio: 0.05184,
default_video: 0.02592,
"1080p_audio": 0.1166,
"1080p_video": 0.05832,
},
contextSize: 2000,
maxOutput: 1,
streaming: false,
vision: false,
tools: false,
jsonOutput: false,
videoGenerations: true,
supportedVideoSizes: ["1280x720", "720x1280", "1920x1080", "1080x1920"],
supportedVideoDurationsSeconds: [5, 10],
supportsVideoAudio: true,
supportsVideoWithoutAudio: true,
},
],
},
{
id: "seedream-4-5",
name: "Seedream 4.5",
Expand Down
Loading