Video Router body to /v1/videos: frame_images, input_references
Map Video Router image_url, end_image_url and reference_image_urls to /v1/videos frame_images and input_references, with a Node converter.

To move a request from POST /v1/video-router/generate to POST /v1/videos, rename the image fields. image_url becomes a frame_images entry with frame_type first_frame, end_image_url becomes one with last_frame, and reference_image_urls becomes input_references. The model ids are the same, so no id mapping is needed. mode is dropped; callback_url takes over from webhook mode. Edits that use video_url stay on the Video Router.
The field map
Both routes create the same jobs. The difference is the wire format: Video Router takes flat URL fields and returns a data envelope, while /v1/videos follows the OpenRouter shape, with image objects and a flat job response. Sume recommends /v1/videos for new integrations.
| Video Router | /v1/videos | Note |
|---|---|---|
| image_url | frame_images[] with frame_type first_frame | Entry is {type: image_url, image_url: {url}} |
| end_image_url | frame_images[] with frame_type last_frame | Only for models that list last_frame |
| reference_image_urls (up to 10) | input_references[] | Same {type, image_url} entry |
| reference_video_urls (up to 3, each up to 3 s) | input_references[] video entries | Not converted by the sample; map by hand |
| video_url (edit) | not available | Keep on Video Router; cannot combine with image or reference fields |
| mode: async / webhook | callback_url | HTTPS only |
The converter
The function destructures the Video Router names, builds the image objects once, and leaves every other key (model, prompt, duration, resolution) untouched. It throws for video_url and for video or audio references, because those need a decision from a person about the entry format for the chosen model. It warns when both frames and references are present: if you send both fields, frame_images controls the mode and Sume runs image-to-video.
export function toVideosBody({ image_url, end_image_url, reference_image_urls, video_url, mode, ...rest }) {
if (video_url) throw new Error("video_url edits stay on /v1/video-router/generate");
if (rest.reference_video_urls || rest.reference_audio_urls) throw new Error("map video/audio references by hand");
const img = (url) => ({ type: "image_url", image_url: { url } });
const body = { ...rest };
if (image_url) body.frame_images = [{ ...img(image_url), frame_type: "first_frame" }];
if (end_image_url) {
body.frame_images = [...(body.frame_images ?? []), { ...img(end_image_url), frame_type: "last_frame" }];
}
if (reference_image_urls?.length) body.input_references = reference_image_urls.map(img);
if (body.frame_images && body.input_references) {
console.warn("both sent: /v1/videos runs this as image-to-video; references are not used as frames");
}
return body;
}
console.log(
JSON.stringify(
toVideosBody({
model: "gemini-omni-flash-1.1",
prompt: "Product spins on a turntable",
image_url: "https://example.com/first.png",
end_image_url: "https://example.com/last.png",
duration: 6,
mode: "async",
}),
null,
2,
),
);Check the model before you convert
A model accepts a frame_type only if it is listed in supported_frame_images, and a reference type only if supported_input_references includes it. For example, Gemini Omni Flash 1.1 accepts image and video references but no audio, while the Seedance 2.x models, Wan 3.0 and the MiniMax H3 models accept audio and video references. Read those arrays from GET /v1/videos/models before sending.
The response also changes. A client that unwrapped data.job from Video Router must now read id, polling_url and status from the top level, and poll GET /v1/videos/{id}. The same job is still visible at GET /v1/jobs/{id}/status if you want the extra job fields.
Worked example: the body in the sample asks gemini-omni-flash-1.1 for a 6-second clip with a first and a last frame. After conversion it has two frame_images entries and no mode key, and the price is the same as before the move, 6 x 0.125 = $0.75 at 720p. Nothing about billing changes with the route; Sume bills the provider list price times 1.25 on both.
Because the two routes share one model vocabulary, a good migration test is to submit the same prompt through both with different Idempotency-Key values on a cheap model and compare usage.cost on the two completed jobs. They should match, since both routes create the same kind of job. If they do not, stop and read the request ids before you move traffic.
- Test the converter on one body per model you use.
- Do not send image_url or frame_images with video_url.
- Keep Idempotency-Key; it is accepted on both routes.
Sources
Related posts
More in Developers
- Count queued and processing jobs with GET /v1/jobs before a wave
Page GET /v1/jobs with status=queued and status=processing, subtract from concurrency_limit, and submit only that many Seedance 2.5 or Omni clips.
- createSumeClient sends x-api-key only: an extra header gives 401
The Sume API accepts Bearer or x-api-key but rejects both at once with 401. A fetch wrapper for createSumeClient that drops the extra header, tested offline.
- curl -w http_code: branch a Sume video submit in bash on 202, 402, 429
A bash submit that saves the body, reads the HTTP code with curl -w and branches on 202, 402, 429 and 5xx. Three Wan 3.0 payloads cost $1.875, $3.75 and $7.50.
- Cut dead air before the first word: STT start time, $0.01 split
Read words[0].start from a Sume STT job, then split the recording from that second for $0.01. For a 3-minute take the whole fix is $0.04. A copyable request.
Written by Sume