Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
28 changes: 28 additions & 0 deletions apps/docs/content/features/video-generation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ LLMGateway currently supports a focused subset of the OpenAI video API.
| `last_frame` | object | no | Optional ending frame when `image` is provided |
| `reference_images` | array | no | One to three provider-specific image inputs |
| `input_reference` | object | no | Alias for one or more `reference_images` |
| `reference_videos` | array | no | One to three reference video HTTPS URLs (Seedance 2.0 only, see below) |
| `callback_url` | string | no | LLMGateway extension for completion webhooks |
| `callback_secret` | string | no | LLMGateway extension used to sign webhook deliveries |

Expand All @@ -54,6 +55,33 @@ LLMGateway currently supports a focused subset of the OpenAI video API.

Requests return `400` when the selected provider cannot serve the requested `size` or `seconds`. Seedance derives `aspect_ratio` from the requested `size` (16:9 for landscape, 9:16 for portrait).

### Seedance 2.0 omni-reference

Seedance 2.0 (`seedance-2-0`, `seedance-2-0-fast`) supports reference-guided video generation. You can supply reference images and reference videos in the same request:

- `reference_images` (or `input_reference`) — one to three image inputs (HTTPS URLs or base64 data URLs)
- `reference_videos` — one to three reference video **HTTPS URLs** (base64 data URLs are not supported for videos)

Reference inputs cannot be combined with the first/last frame inputs (`image`, `last_frame`). Reference videos are only supported on Seedance 2.0 models.

```bash
curl -X POST "https://api.llmgateway.io/v1/videos" \
-H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "seedance-2-0",
"prompt": "The subject performs the choreography from the reference video",
"seconds": 5,
"size": "1280x720",
"reference_images": [
{ "image_url": "https://example.com/subject.png" }
],
"reference_videos": [
"https://example.com/reference-motion.mp4"
]
}'
```

### Not supported yet

- multipart uploads
Expand Down
127 changes: 127 additions & 0 deletions apps/gateway/src/videos/videos.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,133 @@ describe("videos", () => {
);
});

test("/v1/videos rejects non-https reference videos", async () => {
await db.insert(tables.apiKey).values({
id: "token-id",
token: "real-token",
projectId: "project-id",
description: "Test API Key",
createdBy: "user-id",
});

const res = await app.request("/v1/videos", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer real-token",
},
body: JSON.stringify({
model: "seedance-2-0",
prompt: "Reproduce the camera move from the reference clip",
size: "1280x720",
seconds: 5,
reference_videos: ["http://example.com/reference-motion.mp4"],
}),
});

expect(res.status).toBe(400);
const json = await res.json();
expect(JSON.stringify(json)).toContain("reference_videos");
});

test("/v1/videos rejects combining frames with reference videos", async () => {
await db.insert(tables.apiKey).values({
id: "token-id",
token: "real-token",
projectId: "project-id",
description: "Test API Key",
createdBy: "user-id",
});

const res = await app.request("/v1/videos", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer real-token",
},
body: JSON.stringify({
model: "seedance-2-0",
prompt: "Blend these inputs",
size: "1280x720",
seconds: 5,
image: { image_url: "data:image/png;base64,aGVsbG8=" },
reference_videos: ["https://example.com/reference-motion.mp4"],
}),
});

expect(res.status).toBe(400);
const json = await res.json();
expect(JSON.stringify(json)).toContain("Frame inputs");
});

test("/v1/videos rejects reference videos on non-bytedance models", async () => {
await db.insert(tables.apiKey).values({
id: "token-id",
token: "real-token",
projectId: "project-id",
description: "Test API Key",
createdBy: "user-id",
});

await db.insert(tables.providerKey).values({
id: "provider-key-id",
token: "sk-test-key",
provider: "avalanche",
organizationId: "org-id",
baseUrl: mockServerUrl,
});

const res = await app.request("/v1/videos", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer real-token",
},
body: JSON.stringify({
model: "avalanche/veo-3.1-fast-generate-preview",
prompt: "Reproduce this motion",
size: "1920x1080",
seconds: 8,
reference_videos: ["https://example.com/reference-motion.mp4"],
}),
});

expect(res.status).toBe(400);
const json = await res.json();
expect(JSON.stringify(json)).toContain(
"reference videos are currently only supported on bytedance",
);
});

test("/v1/videos restricts reference inputs to Seedance 2.0 models", async () => {
await db.insert(tables.apiKey).values({
id: "token-id",
token: "real-token",
projectId: "project-id",
description: "Test API Key",
createdBy: "user-id",
});

const res = await app.request("/v1/videos", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer real-token",
},
body: JSON.stringify({
model: "bytedance/seedance-1-5-pro",
prompt: "Reproduce this motion",
size: "1280x720",
seconds: 5,
reference_videos: ["https://example.com/reference-motion.mp4"],
}),
});

expect(res.status).toBe(400);
const json = await res.json();
expect(JSON.stringify(json)).toContain("Seedance 2.0");
});

test("/v1/videos uses routing metrics to pick the best eligible provider", async () => {
const originalGoogleCloudProject = process.env.LLM_GOOGLE_CLOUD_PROJECT;
const originalRuntimeGoogleCloudProject = process.env.GOOGLE_CLOUD_PROJECT;
Expand Down
Loading
Loading