Video-router image_url to /v1/videos frame_images, in Python
Map image_url, end_image_url and reference_image_urls from /v1/video-router/generate to frame_images and input_references on /v1/videos, with Python.

To move a request from /v1/video-router/generate to /v1/videos, turn image_url into a frame_images entry with frame_type: "first_frame", end_image_url into one with "last_frame", and reference_image_urls into input_references. The legacy routes stay registered as an alias over the same jobs, so there is no deadline, but new integrations should use /v1/videos.
The mapping
Model ids are the same on both surfaces, and are bare ids such as wan-3.0. See the video generation docs and the legacy Video Router docs.
| Legacy field | /v1/videos field | Mode it selects |
|---|---|---|
image_url | frame_images[] with frame_type: first_frame | Image-to-video |
end_image_url | frame_images[] with frame_type: last_frame | Image-to-video |
reference_image_urls | input_references[] with type: image_url | Reference-to-video |
video_url (edit, Recast, Genjutsu source) | Stays a Video Router field; not in the OpenRouter-shaped body | Edit or swap |
model, prompt, duration, resolution, aspect_ratio | Same names | No change |
A converter
This runs as written with Python 3 and prints the new body. It drops the legacy image fields and, because frame_images wins on /v1/videos, leaves references out when a frame is present.
def convert(old):
new = {k: v for k, v in old.items()
if k not in ("image_url", "end_image_url", "reference_image_urls")}
frames = []
if old.get("image_url"):
frames.append(frame(old["image_url"], "first_frame"))
if old.get("end_image_url"):
frames.append(frame(old["end_image_url"], "last_frame"))
if frames:
new["frame_images"] = frames
refs = old.get("reference_image_urls") or []
if refs and not frames:
new["input_references"] = [
{"type": "image_url", "image_url": {"url": u}} for u in refs]
return new
def frame(url, kind):
return {"type": "image_url", "image_url": {"url": url}, "frame_type": kind}
legacy = {"model": "wan-3.0", "prompt": "Pan across the shelf",
"duration": 6, "image_url": "https://example.com/a.png",
"end_image_url": "https://example.com/b.png"}
print(convert(legacy))Behaviors to expect
- The mode is inferred from the body, so there is no separate mode field to carry over.
size,seedand a non-emptyprovider.optionsreturn 400unsupported_parameteron/v1/videos.generate_audio: falseis a 400 on rows where audio is always on.- Status values map to
pending,in_progress,completed,failedandcancelled.
Keep the legacy path for
A source video_url request (Omni edit, Recast, Genjutsu) is only on the Video Router surface, so leave those calls on /v1/video-router/generate. Move everything else, and test one job per model first; the reference rules post explains the one place behavior differs from OpenRouter.
Sources
Related posts
More in Developers
- LinkedIn API sunsets: 202510, 202511 and 202601 dates, and the upgrade
LinkedIn lists 202510 sunsetting October 15, 2026, 202511 on November 16, 2026 and 202601 on January 15, 2027. Pick one target and test with a Sume clip.
- LinkedIn missing or deprecated version header errors: fail in CI
LinkedIn answers a missing or deprecated Linkedin-Version header with an error response. Check the header in CI, before a video post fails at runtime.
- LinkedIn-Version has no default: pin YYYYMM in your video poster
LinkedIn does not apply the latest API version when the header is missing. Pin a YYYYMM value in config and fail loudly if it is unset.
- Verify a Sume job webhook in Python, and refuse an empty secret
A Python verifier for Sume job webhooks: HMAC SHA 256 over timestamp.body, a 5-minute window, rotation-safe, and a hard refusal when the secret is empty.
Written by Sume