Run an OpenRouter video script on Sume: four changes (Python)

Sume's /v1/videos copies OpenRouter's video wire. Port a polling script by changing the base URL and model id, then dropping seed, size and provider options.

4 min readSume
All posts

OpenRouter's video guide describes an asynchronous flow: POST /api/v1/videos returns an id, a polling_url and a pending status, you poll until the status is completed or failed, and finished jobs expose unsigned_urls that you download with your key. Sume's /v1/videos route was written to match that wire, so a script built for OpenRouter ports with four small edits rather than a rewrite.

The edits matter because Sume deliberately differs in a few places. Where the Sume contract and OpenRouter's guide disagree, the difference is listed in Sume's own contract document, and the items below are the ones a ported script usually trips on.

The four changes

OpenRouter video script edits needed for Sume, from the Sume videos contract and the OpenRouter guide (read 2026-10-03)
ChangeOpenRouterSume
Base URLhttps://openrouter.ai/api/v1https://api.sume.com/v1 (no /api segment)
Model idvendor/model, such as google/veo-3.1Bare catalog id, such as h3-max-recast
Unsupported fieldsseed and size are fields in the request schemaseed and size return 400 in v1
Provider optionsprovider.options is a field in the request schemaMust be empty or omitted

Response shapes need no change. The submit response (202) and the poll response are bare OpenRouter-style objects and are not wrapped in Sume's usual data envelope. Statuses are pending, in_progress, completed, failed and cancelled, and a finished job adds unsigned_urls and a usage.cost.

A ported script

This script uses only the standard library. It submits a Recast job with one source video and one host photo in input_references, then follows polling_url until the job leaves pending and in_progress. Sume accepts Authorization: Bearer or x-api-key, and the sample uses the first, which is the form an OpenRouter client already sends. Send only one of them, since both together is a 401.

import json, os, time, urllib.request

BASE = "https://api.sume.com/v1"  # OpenRouter: https://openrouter.ai/api/v1
HEAD = {"Authorization": "Bearer " + os.environ["SUME_API_KEY"], "Content-Type": "application/json"}


def call(url: str, body: dict | None = None) -> dict:
    data = json.dumps(body).encode() if body else None
    req = urllib.request.Request(url, data, HEAD)
    with urllib.request.urlopen(req, timeout=60) as r:
        return json.load(r)


job = call(BASE + "/videos", {
    "model": "h3-max-recast",  # bare id, not "vendor/model"
    "resolution": "768p",
    "input_references": [
        {"type": "video_url", "video_url": {"url": "https://example.com/source.mp4"}},
        {"type": "image_url", "image_url": {"url": "https://example.com/host.jpg"}},
    ],
})
print(job["id"], job["status"])
while job["status"] in ("pending", "in_progress"):
    time.sleep(float(os.environ.get("POLL", "10")))
    job = call(job["polling_url"])
print(job["status"], job.get("unsigned_urls"))

After it completes

  • unsigned_urls[0] is a Sume content URL that redirects to the file. Fetch it with your key, as OpenRouter's guide also says to.
  • The job id doubles as a Sume job id, so GET /v1/jobs/{id}/status works too, and it adds terminal and next_poll_after_seconds.
  • Add an Idempotency-Key header to the submit call before you put the script on a retry loop.
  • Poll every 10 to 30 seconds. OpenRouter suggests around 30 seconds for video, and Sume's status endpoint states its own preferred interval.
  • Check the model's own limits first. GET /v1/videos/models lists supported resolutions, durations and reference types for each id.

The OpenRouter wire is described in their video guide. The Sume model list and rules are in the videos docs, and header rules are in authentication.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume