- SDK
- Stateless Video API
SDK
Stateless Video API
Text, frames, source videos, and image/audio/video reference combinations.
Use operation="generate_video", video_model_provider="openrouter", video_model_name, and optional video_config. All recipes require a text content prompt. Use the shared client setup and a long timeout.
Choose an input pattern
There are three distinct media-input paths. Choose the one matching your intent; the API does not convert between them.
| Pattern | Fields | Meaning |
|---|---|---|
| Text only | No input files | Generate a scene from the prompt. |
| First frame | input_image | Animate starting from that image. |
| First + last frame | input_image + input_last_frame | Guide the opening and closing frames. |
| Source video | input_video | Edit, extend, upscale, or condition on a source clip, depending on the model. |
| General references | input_references | Guide content, identity, motion, or sound without assigning first/last-frame roles. |
| Source video + references | input_video + input_references | Source clip plus additional guidance, only if the provider supports the combination. |
Combination rules:
input_last_framerequiresinput_image; last-frame-only requests are rejected.- First/last frames must be images.
- Do not combine frame fields with
input_videoorinput_references. input_videomust be a video; internally it is placed first in the provider reference list, followed byinput_references.- Reference files may be images, audio, or video only when the selected model/provider accepts those modalities and their combination.
- Filenames and upload order do not choose first/last-frame roles. Use the explicit fields.
1. Text to video
result = await client.send_message(
"A blue ball gently rolling across a white tabletop.",
operation="generate_video",
video_model_provider="openrouter",
video_model_name="google/veo-3.1-lite",
video_config={"duration": 4, "resolution": "720p", "generate_audio": False},
)
print(result.generated_media)
No files are needed. This path is implemented; stateless text-only video was not separately included in the live E2E matrix.
2. Image to video: first frame
result = await client.send_message(
"Animate the scene with a gentle camera push-in.",
operation="generate_video",
video_model_provider="openrouter",
video_model_name="google/veo-3.1-lite",
video_config={"duration": 4, "resolution": "720p", "generate_audio": False},
input_image="start.png",
)
Use a model advertising first_frame in supported_frame_images. This is not the same as supplying an image as a general reference. The stateless first-frame-only variant was not separately live-tested; first+last-frame generation was.
3. First and last frame to video
result = await client.send_message(
"Create a smooth transition.",
operation="generate_video",
video_model_provider="openrouter",
video_model_name="google/veo-3.1-lite",
video_config={"duration": 4, "resolution": "720p", "generate_audio": False},
input_image="start.png",
input_last_frame="end.png",
)
The prompt needs no document IDs or tool instructions. This flow passed live tests in streaming and non-streaming modes in both environments. Models must support both frame roles; supported durations/resolutions may depend on the chosen mode.
4. Video to video
Choose a video-input-capable editing model, not a text-to-video model that merely produces videos. This example deliberately leaves model selection explicit:
import os
result = await client.send_message(
"Change the blue background to red while preserving the subject's motion.",
operation="generate_video",
video_model_provider="openrouter",
video_model_name=os.environ["VIDEO_EDIT_MODEL_ID"],
input_video="source.mp4",
)
Start with no video_config, then add controls the editing model supports. Do not copy Veo’s duration/resolution settings to an editor or upscaler. For upscaling, select an upscaling model and use its supported upscale_factor / creativity controls.
To add a reference image, supply input_references=["appearance.png"] alongside input_video. The provider must support source video plus image references; video input alone does not prove this. Video-to-video, upscaling, and source-plus-reference variants are implemented paths, not yet live E2E-verified.
Use a generated clip as the next input
Stateless upload fields accept local files, not the returned URL or document ID. Download a prior result without forwarding your API key to storage, then submit it as the source:
import httpx
import os
async with httpx.AsyncClient(timeout=120) as downloader:
async with downloader.stream("GET", result.generated_media[0]["url"]) as response:
response.raise_for_status()
with open("source.mp4", "wb") as output:
async for chunk in response.aiter_bytes():
output.write(chunk)
edited = await client.send_message(
"Change the background to a snowy landscape.",
operation="generate_video",
video_model_provider="openrouter",
video_model_name=os.environ["VIDEO_EDIT_MODEL_ID"],
input_video="source.mp4",
)
Here result is a previous video-generation response. Keeping its thread_id on the next call is optional and does not automatically supply the previous clip.
5. Image references to video
References guide the video without pinning its first or last frame:
result = await client.send_message(
"Animate the blue ball from the reference on a white tabletop.",
operation="generate_video",
video_model_provider="openrouter",
video_model_name="bytedance/seedance-2.0-mini",
video_config={"duration": 4, "resolution": "480p", "generate_audio": False},
input_references=["subject.png"],
)
This single-image reference flow passed live tests in both response modes and environments. For multiple image references, use input_references=["subject.png", "style.png"]; provider count/combination limits still apply, and that variant was not separately live-tested.
Veo 3.1 Lite does not accept general image references. Use its frame inputs instead when you intend to control the first/last frame, or explicitly choose a reference-capable model. The API does not silently substitute a model.
6. Every reference-modality combination
All seven nonempty combinations use one repeated field, input_references. Every request still includes a text prompt. These rows describe request shapes, not a promise that one model accepts all combinations.
| Reference modalities | SDK file list | Required provider support |
|---|---|---|
| Image | ["subject.png"] | General image references, not just frame images. |
| Audio | ["soundtrack.wav"] | Audio references; some models may also require an image/video. |
| Video | ["motion.mp4"] | Video references; use input_video for an explicit source clip instead. |
| Image + audio | ["subject.png", "soundtrack.wav"] | Both modalities together. |
| Image + video | ["subject.png", "motion.mp4"] | Both modalities together. |
| Audio + video | ["soundtrack.wav", "motion.mp4"] | Both modalities together. |
| Image + audio + video | ["subject.png", "soundtrack.wav", "motion.mp4"] | All three together. |
For source-video-plus-reference workflows, keep input_video="source.mp4" and choose the appropriate reference list from this table. The combined source and reference set must be supported; this does not override provider restrictions or allow frame fields in the same request.
Use this recipe for any row, replacing the list and prompt. Choose the model explicitly after checking provider support:
import os
result = await client.send_message(
"Use the pictured subject, the movement from the clip, and the audio as guidance.",
operation="generate_video",
video_model_provider="openrouter",
video_model_name=os.environ["REFERENCE_VIDEO_MODEL_ID"],
input_references=["subject.png", "soundtrack.wav", "motion.mp4"],
)
Audio/video and mixed-modality reference variants have not been live E2E-verified. Architecture metadata is a necessary check but does not fully describe reference support, combinations, minimum counts, codecs, or duration limits. Provider restrictions can still reject a request that passes basic modality validation.
Audio input and generated audio output are different capabilities. Supplying soundtrack.wav does not guarantee it will be copied or synchronized into the result. video_config.generate_audio controls output audio only on models supporting that option.
JavaScript and raw HTTP
The field names and config are unchanged across SDKs. This is the first+last-frame recipe:
const result = await client.sendMessage({
content: "Create a smooth transition.",
operation: "generate_video",
video_model_provider: "openrouter",
video_model_name: "google/veo-3.1-lite",
video_config: { duration: 4, resolution: "720p", generate_audio: false },
input_image: "start.png",
input_last_frame: "end.png",
});
For the other recipes, replace the file fields rather than mixing modes:
| Workflow | JavaScript option | HTTP multipart field |
|---|---|---|
| Source video | input_video: "source.mp4" | -F 'input_video=@source.mp4;type=video/mp4' |
| Image reference | input_references: ["subject.png"] | -F 'input_references=@subject.png;type=image/png' |
| Audio reference | input_references: ["soundtrack.wav"] | -F 'input_references=@soundtrack.wav;type=audio/wav' |
| Video reference | input_references: ["motion.mp4"] | -F 'input_references=@motion.mp4;type=video/mp4' |
For mixed references, combine the list entries in JavaScript or repeat the HTTP field for each file. Remove input_image / input_last_frame and choose a compatible reference model. Text-only calls can use JSON instead of multipart.
Video configuration
video_config accepts only supported fields and values. These are available controls, not a universal config to copy wholesale:
| Field | Purpose / constraint |
|---|---|
duration | Clip length in seconds; check supported_durations. |
resolution | Named resolution; check supported_resolutions. |
aspect_ratio | Output ratio; check supported_aspect_ratios. |
size | Exact size; check supported_sizes. Prefer this or resolution/aspect ratio unless the provider documents their combination. |
generate_audio | Boolean output-audio control, if supported. Omit if unsupported—even false need not be accepted by the config schema. |
seed | Integer seed, if supported. |
provider | Routing object: only, order, ignore, sort, allow_fallbacks. No arbitrary video provider-options passthrough. |
upscale_factor | Upscaling factor within the advertised range; requires an appropriate model/workflow. |
creativity | One of the advertised values; requires an appropriate model/workflow. |
For example, add "provider": {"sort": "price"} inside video_config to request price-based routing. Provider selection never changes the requested model.
Discover candidates with GET /models/video/all or client.list_video_models(). Inspect a selected model with GET /models/video/{model_name} or client.get_video_model("google/veo-3.1-lite"); JavaScript uses listVideoModels() / getVideoModel(). Check frame capabilities and provider documentation as well as architecture. Supported values can differ between text, frame, and reference modes.
Results, streaming, and limits
Every recipe returns the same generated_media shape. Add stream=True / stream: true to any recipe for media events and keepalives; see shared streaming and error handling.
Do not expect an immediate job ID/polling interface: these requests wait for completion. Input count/size safeguards and provider limits still apply. Generation can take minutes and is billed; avoid automatic retries and do not treat a disconnected client as cancellation.