Skip to content

feat(video): first/last frame for Seedance 2.0 - #2699

Merged
steebchen merged 5 commits into
mainfrom
seedance-frame-url-support
Jun 16, 2026
Merged

steebchen merged 5 commits into
mainfrom
seedance-frame-url-support

Conversation

@steebchen

@steebchen steebchen commented Jun 16, 2026 •

Copy link
Copy Markdown
Member

Summary

Two related changes, both surfaced while testing ByteDance Seedance 2.0 video generation in the playground.

1. First/last frame inputs for Seedance 2.0

ByteDance Seedance 2.0 supports first/last frame conditioning for image-to-video, but the gateway and playground didn't expose it:

  • The playground hid the First/Last frame pickers behind supportsVideoFrameInput(), which only returned true for Veo / MiniMax / Grok models.
  • The gateway's createBytedanceVideoJob() only forwarded the first frame — a last_frame was silently dropped, and frame inputs were rejected for ByteDance by the constraint checker.

Changes:

  • Gateway (videos.ts): forward last_frame as a role: "last_frame" content entry; allow inputMode === "frames" for Seedance 2.0 mappings (non-2.0 ByteDance models still rejected with a clear message); renamed isBytedanceReferenceModel → isBytedanceSeedance2Model.
  • Playground (video-gen.ts): supportsVideoFrameInput() returns true for Seedance 2.0.
  • Docs (video-generation.mdx): new First/last frame inputs section with a provider table (Seedance 2.0 called out), rules, and example.
  • Tests: ByteDance video mock handler + forwarding/rejection tests; playground capability test.

2. Fix "Video unavailable" for cross-project video playback

After generating a video, the playground could show "Video unavailable" even though the job completed successfully with a valid content URL. Root cause: the playground polls video status via the session/org-authorized API app, but loaded the content through the project-scoped gateway API key. When the active playground key's project differed from the project that owns the job (e.g. after an org/project switch, or a rotated auto-generated key), the gateway returned 404 "Video not found".

Verified against a real completed Seedance 2.0 job: the gateway returned the full video to the owning key (200) but 404 to a different project's key — exactly the playground's symptom.

Changes:

  • API (routes/video.ts): the video status route now returns a keyless signed content URL (buildSignedGatewayVideoLogContentUrl), scoped by the same org-access check it already performs.
  • Playground ([videoId]/content/route.ts): the content proxy resolves that signed URL via the session-authorized API instead of the project-scoped gateway key. Playback now works for any video the user can access, and stored history URLs stay durable (each load re-resolves a fresh signed URL).

Verification

  • pnpm format ✅ · pnpm build ✅ · docs build ✅
  • Gateway video spec ✅ 30/30 · playground video-gen spec ✅ 13/13
  • End-to-end against a real completed job: API-route-produced signed URL streams the full 8.9 MB video keyless (HTTP 200); confirmed the gateway 404s for a wrong-project key (the original bug) and 200s for the owning key.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added support for Seedance 2.0 image/last_frame inputs using the Bytedance provider.
    • Video job status responses for completed videos can now include content (signed playback URLs).
  • Bug Fixes
    • Enforced frame/reference validation rules and improved rejection messaging for incompatible model/provider combinations.
  • Tests
    • Added/extended test coverage for Seedance 2.0 Bytedance frame requests, compatibility detection, and OpenAI mock task generation.
  • Documentation
    • Updated POST /v1/videos supported fields and added first/last frame interpolation guidance and constraints.

Enable image-to-video first/last frame conditioning for ByteDance
Seedance 2.0 (and 2.0 Fast). The playground hid the frame inputs
behind supportsVideoFrameInput, and the gateway only forwarded the
first frame to ByteDance.

- gateway: forward last_frame as a role:"last_frame" content entry
  and allow inputMode "frames" for Seedance 2.0 mappings
- playground: expose First/Last frame pickers for Seedance 2.0
- add a ByteDance video mock handler plus forwarding/rejection tests

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 16, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@steebchen, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 6 minutes and 8 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: fc6aa1a5-0f37-4378-8864-6c55643264d6

📥 Commits

Reviewing files that changed from the base of the PR and between 411c81a and 035ff69.

📒 Files selected for processing (4)
  • apps/docs/content/features/video-generation.mdx
  • apps/playground/src/app/api/video/[videoId]/content/route.ts
  • apps/playground/src/lib/video-gen.spec.ts
  • apps/playground/src/lib/video-gen.ts

Walkthrough

Adds first/last frame input support for ByteDance Seedance 2.0 by introducing a model helper, gating constraints, and extending request construction with last_frame content. Updates the API video response schema to include content fields with signed URLs. Refactors the playground video content route to use session-authorized backend flow instead of playground API keys. Includes mock endpoint, integration tests, and feature documentation.

Changes

Seedance 2.0 Frame Input Support

Layer / File(s) Summary
Seedance 2.0 model helper and frame/reference constraint gating
apps/gateway/src/videos/videos.ts, apps/playground/src/lib/video-gen.ts, apps/playground/src/lib/video-gen.spec.ts
Introduces isBytedanceSeedance2Model helper and applies it to gate inputMode === "frames" (allowing bytedance only for Seedance 2.0) and inputMode === "reference" constraints. Updates supportsVideoFrameInput in the playground to return true for Seedance 2.0 model IDs when provider is absent or "bytedance", with tests asserting correct outcomes.
ByteDance last_frame content construction
apps/gateway/src/videos/videos.ts
Extends createBytedanceVideoJob signature to accept processedLastFrame parameter. Conditionally appends a last_frame image_url entry (base64 data URL) to the ByteDance request content array. Updates the call site in createUpstreamVideoJob to pass processedLastFrame.
ByteDance mock endpoint and integration tests
apps/gateway/src/test-utils/mock-openai-server.ts, apps/gateway/src/videos/videos.spec.ts
Adds POST /contents/generations/tasks mock route that parses content for text prompt and first/last frame image data URLs, stores a queued MockVideoJobState, and returns { id, status: "queued" }. Two integration tests cover: happy-path frame forwarding for seedance-2-0, and HTTP 400 rejection for non-Seedance-2.0 Bytedance models with frame inputs.
Feature documentation and examples
apps/docs/content/features/video-generation.mdx
Updated "Supported fields" table clarifies image and last_frame are base64 data URL frame inputs. New "First/last frame inputs" section documents frame interpolation, field availability by provider/model, and rules (last_frame requires image, Seedance scope, no reference input mixing), with a Seedance 2.0 example request.

Video Content Delivery

Layer / File(s) Summary
API video response content field
apps/api/src/routes/video.ts
Extends videoJobResponseSchema to include optional content array for completed videos, containing signed URLs and optional MIME type. Route handler computes content by querying the log record by job requestId when status is completed, generating a signed gateway video log content URL, and including it in the JSON response.
Playground content route backend flow
apps/playground/src/app/api/video/[videoId]/content/route.ts
Refactors the content route to use session-authorized backend flow instead of playground API keys. Reads session cookies (regular and __Secure- variants), fetches video status from apiBackendUrl to extract the first content URL, returns 404 when unavailable, and forwards incoming Range header to the source content fetch with normalized video response headers.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

  • theopenco/llmgateway#2270: Adds the core createBytedanceVideoJob and /contents/generations/tasks upstream flow that this PR extends with Seedance 2.0 model detection and last_frame parameter passing.
  • theopenco/llmgateway#2538: Modifies the same ByteDance Seedance 2.0 constraint gating and upstream content construction logic in videos.ts, handling reference_videos/reference_audios in parallel patterns.

Suggested reviewers

  • smakosh
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(video): first/last frame for Seedance 2.0' directly summarizes the main change: adding first/last frame support for ByteDance Seedance 2.0 models in video generation, which is the core objective of this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch seedance-frame-url-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/playground/src/lib/video-gen.ts`:
- Around line 105-107: The code at line 105-107 in the isSeedance2ReferenceModel
check enables frame support for ByteDance, but the mappingSupportsVideoRequest
function (around line 230 onwards) still rejects ByteDance in frame mode,
creating an inconsistency. Update the mappingSupportsVideoRequest function to
remove or modify the logic that currently filters out ByteDance when frame mode
is enabled, ensuring that ByteDance models can pass frame option validation just
as the capability detection at line 105-107 indicates they should.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 99a854ee-687e-42fb-a968-4c4d01493aba

📥 Commits

Reviewing files that changed from the base of the PR and between b9e8c97 and 4fe3384.

📒 Files selected for processing (5)
  • apps/gateway/src/test-utils/mock-openai-server.ts
  • apps/gateway/src/videos/videos.spec.ts
  • apps/gateway/src/videos/videos.ts
  • apps/playground/src/lib/video-gen.spec.ts
  • apps/playground/src/lib/video-gen.ts

Comment thread apps/playground/src/lib/video-gen.ts
Add a "First/last frame inputs" subsection to the video generation
docs covering the image/last_frame fields, provider support
(Seedance 2.0 specifically), rules, and a Seedance 2.0 example.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/docs/content/features/video-generation.mdx`:
- Line 72: The documentation in the "Not combinable with references" section on
line 72 of video-generation.mdx lists the non-combinable reference inputs as
`reference_images`, `reference_videos`, and `reference_audios`, but omits
`input_reference` which the API validation also rejects when combined with frame
mode inputs. Add `input_reference` to the list of reference input parameters
that cannot be combined with frame inputs (`image`, `last_frame`) to align the
documentation with the actual API behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c2c14190-809a-4f23-883d-78c2689df2ab

📥 Commits

Reviewing files that changed from the base of the PR and between 4fe3384 and 815000a.

📒 Files selected for processing (1)
  • apps/docs/content/features/video-generation.mdx

Comment thread apps/docs/content/features/video-generation.mdx Outdated
The playground polled video status via the session/org-authorized API
app but loaded the actual content through the project-scoped gateway
API key. When the active playground key's project differed from the
project that owns the job (e.g. after an org/project switch), the
gateway returned 404 "Video not found" and the player showed
"Video unavailable" even though the job was completed with a valid
content URL.

- api: video status route now returns a keyless signed content URL
  (buildSignedGatewayVideoLogContentUrl) scoped by the same org-access
  check it already performs
- playground: content proxy resolves that signed URL via the
  session-authorized API instead of the project-scoped gateway key, so
  playback works for any video the user can access and history URLs
  stay durable

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/playground/src/app/api/video/`[videoId]/content/route.ts:
- Around line 40-48: Both external fetch calls in the route handler lack
timeouts which could cause requests to hang indefinitely. At the statusResponse
fetch call (line 40-48) to apiBackendUrl and the fetch call to sourceUrl (line
70-75), add timeout protection using AbortController. Create an AbortController
for each fetch, set a timeout (e.g., 30 seconds) using setTimeout to call
controller.abort(), pass the signal property from the controller in the fetch
options, and handle potential AbortError exceptions to provide graceful timeout
failures instead of indefinite hangs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 661b4b61-8292-47dc-af2f-ce21c6853a8b

📥 Commits

Reviewing files that changed from the base of the PR and between 815000a and 411c81a.

📒 Files selected for processing (2)
  • apps/api/src/routes/video.ts
  • apps/playground/src/app/api/video/[videoId]/content/route.ts

Comment on lines 40 to 48
const statusResponse = await fetch(
`${apiBackendUrl}/video/${encodeURIComponent(videoId)}`,
{
headers: {
Authorization: `Bearer ${apiKey}`,
"x-source": "chat.llmgateway.io",
...(rangeHeader ? { Range: rangeHeader } : {}),
Cookie: cookieHeader,
},
cache: "no-store",
},
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add timeouts to external fetch calls to prevent hanging requests.

Both fetch calls to apiBackendUrl (line 40) and sourceUrl (line 70) lack timeouts. If either backend is slow or unresponsive, the request could hang indefinitely, degrading availability.

🛡️ Proposed fix using AbortController
+const FETCH_TIMEOUT_MS = 30_000;
+
 // Resolve the signed, keyless content URL through the session-authorized API
 // (org-scoped access) rather than the project-scoped gateway API key, so
 // playback works for any video the user can access regardless of which
 // playground API key is currently active.
 const { apiBackendUrl } = getConfig();
+const statusAbort = AbortSignal.timeout(FETCH_TIMEOUT_MS);
 const statusResponse = await fetch(
   `${apiBackendUrl}/video/${encodeURIComponent(videoId)}`,
   {
     headers: {
       Cookie: cookieHeader,
     },
     cache: "no-store",
+    signal: statusAbort,
   },
 );
 const rangeHeader = req.headers.get("Range");
+const contentAbort = AbortSignal.timeout(FETCH_TIMEOUT_MS);
 const response = await fetch(sourceUrl, {
   headers: {
     ...(rangeHeader ? { Range: rangeHeader } : {}),
   },
   cache: "no-store",
+  signal: contentAbort,
 });

Also applies to: 70-75

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/app/api/video/`[videoId]/content/route.ts around lines 40
- 48, Both external fetch calls in the route handler lack timeouts which could
cause requests to hang indefinitely. At the statusResponse fetch call (line
40-48) to apiBackendUrl and the fetch call to sourceUrl (line 70-75), add
timeout protection using AbortController. Create an AbortController for each
fetch, set a timeout (e.g., 30 seconds) using setTimeout to call
controller.abort(), pass the signal property from the controller in the fetch
options, and handle potential AbortError exceptions to provide graceful timeout
failures instead of indefinite hangs.

steebchen and others added 2 commits June 16, 2026 17:38
- video-gen: allow Seedance 2.0 (and 2.0 Fast) through frame-mode
  size/duration filtering in mappingSupportsVideoRequest, matching
  supportsVideoFrameInput so the selectors aren't emptied once a frame
  is added; non-2.0 bytedance models stay rejected
- content proxy: add a 10s timeout to the status fetch (504 on
  timeout); leave the streaming fetch untimed so large downloads aren't
  cut off
- docs: include input_reference in the frame/reference non-combinable list
- tests: cover frame-mode size/duration selection for Seedance 2.0

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@steebchen
steebchen enabled auto-merge June 16, 2026 17:07
@steebchen
steebchen added this pull request to the merge queue Jun 16, 2026
Merged via the queue into main with commit 4939af6 Jun 16, 2026
16 checks passed
@steebchen
steebchen deleted the seedance-frame-url-support branch June 16, 2026 17:23
steebchen added a commit that referenced this pull request Jun 16, 2026
## Summary

Two related changes, both surfaced while testing ByteDance Seedance 2.0
video generation in the playground.

### 1. First/last frame inputs for Seedance 2.0

ByteDance Seedance 2.0 supports first/last frame conditioning for
image-to-video, but the gateway and playground didn't expose it:

- The playground hid the **First/Last frame** pickers behind
`supportsVideoFrameInput()`, which only returned `true` for Veo /
MiniMax / Grok models.
- The gateway's `createBytedanceVideoJob()` only forwarded the **first**
frame — a `last_frame` was silently dropped, and frame inputs were
rejected for ByteDance by the constraint checker.

Changes:
- **Gateway** (`videos.ts`): forward `last_frame` as a `role:
"last_frame"` content entry; allow `inputMode === "frames"` for Seedance
2.0 mappings (non-2.0 ByteDance models still rejected with a clear
message); renamed `isBytedanceReferenceModel` →
`isBytedanceSeedance2Model`.
- **Playground** (`video-gen.ts`): `supportsVideoFrameInput()` returns
`true` for Seedance 2.0.
- **Docs** (`video-generation.mdx`): new **First/last frame inputs**
section with a provider table (Seedance 2.0 called out), rules, and
example.
- **Tests**: ByteDance video mock handler + forwarding/rejection tests;
playground capability test.

### 2. Fix "Video unavailable" for cross-project video playback

After generating a video, the playground could show **"Video
unavailable"** even though the job completed successfully with a valid
content URL. Root cause: the playground polls video **status** via the
session/org-authorized API app, but loaded the **content** through the
project-scoped gateway API key. When the active playground key's project
differed from the project that owns the job (e.g. after an org/project
switch, or a rotated auto-generated key), the gateway returned `404
"Video not found"`.

Verified against a real completed Seedance 2.0 job: the gateway returned
the full video to the owning key (200) but 404 to a different project's
key — exactly the playground's symptom.

Changes:
- **API** (`routes/video.ts`): the video status route now returns a
keyless signed content URL (`buildSignedGatewayVideoLogContentUrl`),
scoped by the same org-access check it already performs.
- **Playground** (`[videoId]/content/route.ts`): the content proxy
resolves that signed URL via the session-authorized API instead of the
project-scoped gateway key. Playback now works for any video the user
can access, and stored history URLs stay durable (each load re-resolves
a fresh signed URL).

## Verification
- `pnpm format` ✅ · `pnpm build` ✅ · docs build ✅
- Gateway video spec ✅ 30/30 · playground video-gen spec ✅ 13/13
- End-to-end against a real completed job: API-route-produced signed URL
streams the full 8.9 MB video keyless (HTTP 200); confirmed the gateway
404s for a wrong-project key (the original bug) and 200s for the owning
key.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Added support for Seedance 2.0 `image`/`last_frame` inputs using the
Bytedance provider.
* Video job status responses for completed videos can now include
`content` (signed playback URLs).
* **Bug Fixes**
* Enforced frame/reference validation rules and improved rejection
messaging for incompatible model/provider combinations.
* **Tests**
* Added/extended test coverage for Seedance 2.0 Bytedance frame
requests, compatibility detection, and OpenAI mock task generation.
* **Documentation**
* Updated `POST /v1/videos` supported fields and added first/last frame
interpolation guidance and constraints.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant