'frame_images is only accepted on auto': use the flat frame fields

Video Router refuses frame_images and input_references on a pinned model. Send image_url, end_image_url or reference_*_urls there, or move to POST /v1/videos.

4 min readSume
All posts

The 400 "frame_images is only accepted on auto- / sume/auto / auto." means you sent the OpenRouter-style frame_images array to POST /v1/video-router/generate with a pinned model such as seedance-2.5. On that route, pinned models take flat fields: image_url for the first frame and end_image_url for the last. The same refusal exists for input_references and for task.

Two routes, two vocabularies

Sume has two ways to create a video job, and the docs recommend POST /v1/videos for new work. That route is shaped like the OpenRouter video API and accepts frame_images with a frame_type of first_frame or last_frame, plus input_references. The older Video Router keeps its flat vocabulary for pinned models and only lets auto-, sume/auto and auto use the array forms. Both create the same jobs with the same model ids, so moving a call is a path-and-body change.

How the same intent is spelled on each route, from the Sume docs and schema on main (read 2026-10-05)
IntentPOST /v1/videosPOST /v1/video-router/generate (pinned model)
First frameframe_images entry, frame_type first_frameimage_url (alias first_frame_url)
Last frameframe_images entry, frame_type last_frameend_image_url (alias last_frame_url)
Style or content reference imageinput_references, type image_urlreference_image_urls
Reference videoinput_references, type video_urlreference_video_urls
Reference audioinput_references, type audio_urlreference_audio_urls

Convert the array in a few lines

If you already have an OpenRouter-style body, this converter produces the flat form for a pinned model. It only builds the dictionary and prints it, so it runs without a key and creates no job.

def to_flat(body: dict) -> dict:
    flat = {k: v for k, v in body.items() if k not in ("frame_images", "input_references")}
    for f in body.get("frame_images", []):
        key = "image_url" if f["frame_type"] == "first_frame" else "end_image_url"
        flat[key] = f["image_url"]["url"]
    names = {"image_url": "reference_image_urls", "video_url": "reference_video_urls",
             "audio_url": "reference_audio_urls"}
    for ref in body.get("input_references", []):
        flat.setdefault(names[ref["type"]], []).append(ref[ref["type"]]["url"])
    return flat

print(to_flat({
    "model": "seedance-2.5",
    "prompt": "Slow push-in on a ceramic mug",
    "frame_images": [{"type": "image_url", "frame_type": "first_frame",
                      "image_url": {"url": "https://example.com/a.png"}}],
}))

What the conversion cannot fix

Flat fields obey their own pairing rules. A frame and a reference list cannot travel together on the Video Router, and a last frame needs a first frame. Those are covered in the neighbouring posts on the "not both" and "requires image_url" errors. The /v1/videos route has a different rule when both arrays are present: frame_images controls the mode and the request runs as image-to-video, as the related post on which field wins explains.

When to stay on the Video Router

Keep the flat route if your integration already uses it and has no reason to change; the docs say it stays available and does not change. Pick /v1/videos when you want the OpenRouter-shaped wire or the Idempotency-Key replay behaviour described on the Sume differences table. Do not mix the two vocabularies inside one request: an unknown key on the Video Router is a validation error, not an ignored field. That strictness is useful: a typo in a field name fails fast instead of silently dropping your last frame and billing a clip that ignored it. Treat the 400 as the cheapest debugging tool in the pipeline.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume