> ## Documentation Index
> Fetch the complete documentation index at: https://openrouter.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.
# Choose a Video Generation Model
> Select an OpenRouter video model by matching clip requirements and scoring priorities
export const CopyPromptButton = ({prompt, buttonLabel = "Copy prompt"}) => {
const [isCopied, setIsCopied] = useState(false);
const handleClick = () => {
navigator.clipboard.writeText(prompt).then(() => {
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
});
};
return
;
};
Use this guide when you need to add video model selection based on the clip
your app needs to generate.
By the end, your implementation should have a small model-selection helper that
filters models by capability and scores them by priority before submitting a
video job.
Not sure what model to use? Copy this prompt to run a model-selection process.
For reusable agent knowledge across projects, install the [openrouter-video skill](https://github.com/OpenRouterTeam/skills/tree/main/skills/openrouter-video).
## Before you start
You need:
* Node.js 20 or newer
* An OpenRouter API key available as `OPENROUTER_API_KEY` only if you submit
the optional generation request
* A stable, directly downloadable image URL if you test an image-to-video
request
Use the API reference pages as the source of truth for exact fields:
* [Create video generation request](/docs/api/api-reference/video-generation/submit-a-video-generation-request)
* [List video generation models](/docs/api/api-reference/video-generation/list-all-video-generation-models)
* [TypeScript SDK video generation reference](/docs/client-sdks/typescript/sdks/videogeneration/README)
Submitting `POST /api/v1/videos` starts a real video generation job and may
spend OpenRouter credits. Use the model-selection and request-preview steps
first, then submit only when the request is ready.
## Step 1: Fetch the video model list
Call the dedicated video model endpoint:
```js lines theme={null}
const response = await fetch("https://openrouter.ai/api/v1/videos/models");
if (!response.ok) {
throw new Error(await response.text());
}
const { data } = await response.json();
const models = data;
console.log(models.map((model) => model.id));
```
Actual output from the model-list call:
```text lines theme={null}
[
"kwaivgi/kling-v3.0-pro",
"kwaivgi/kling-v3.0-std",
"google/veo-3.1-fast",
"google/veo-3.1-lite",
"kwaivgi/kling-video-o1",
"minimax/hailuo-2.3",
"bytedance/seedance-2.0",
"bytedance/seedance-2.0-fast",
"alibaba/wan-2.7",
"alibaba/wan-2.6",
"bytedance/seedance-1-5-pro",
"openai/sora-2-pro",
"google/veo-3.1"
]
```
Each model includes the values you need for routing decisions. Use the
[List video generation models API
reference](/docs/api/api-reference/video-generation/list-all-video-generation-models) as the
source of truth for the endpoint response and model metadata fields. If your app
uses the TypeScript SDK, see the generated [`listVideosModels` SDK
reference](/docs/client-sdks/typescript/sdks/videogeneration/README#listvideosmodels)
for the SDK method shape.
## Step 2: Filter by the job you want to run
Start by translating the product request into model requirements: clip length,
output shape, generation mode, audio, deterministic retries, provider-specific
controls, and cost. Use the API reference above for the exact metadata fields to
inspect before filtering.
For example, this helper finds models that can generate a 720p, vertical,
image-to-video clip with first-frame support:
```js expandable lines theme={null}
function findVideoModels(models) {
return models.filter((model) => {
const supportsRequest =
model.supported_resolutions?.includes("720p") &&
model.supported_aspect_ratios?.includes("9:16") &&
model.supported_durations?.includes(5) &&
model.supported_frame_images?.includes("first_frame");
return supportsRequest;
});
}
function getLowestAdvertisedPrice(model) {
const prices = Object.values(model.pricing_skus ?? {})
.map((price) => Number(price))
.filter((price) => Number.isFinite(price));
return prices.length > 0 ? Math.min(...prices) : Number.POSITIVE_INFINITY;
}
const matchingModels = findVideoModels(models).sort((first, second) => {
return getLowestAdvertisedPrice(first) - getLowestAdvertisedPrice(second);
});
if (matchingModels.length === 0) {
throw new Error("No matching video model found.");
}
console.log(
JSON.stringify(
matchingModels.map((match) => ({
id: match.id,
lowest_advertised_price: getLowestAdvertisedPrice(match),
})),
null,
2,
),
);
```
Example output:
```json expandable lines theme={null}
[
{
"id": "bytedance/seedance-1-5-pro",
"lowest_advertised_price": 0.0000012
},
{
"id": "bytedance/seedance-2.0-fast",
"lowest_advertised_price": 0.0000056
},
{
"id": "bytedance/seedance-2.0",
"lowest_advertised_price": 0.000007
},
{
"id": "alibaba/wan-2.6",
"lowest_advertised_price": 0.04
},
{
"id": "kwaivgi/kling-v3.0-std",
"lowest_advertised_price": 0.084
},
{
"id": "alibaba/wan-2.7",
"lowest_advertised_price": 0.1
},
{
"id": "kwaivgi/kling-v3.0-pro",
"lowest_advertised_price": 0.112
},
{
"id": "kwaivgi/kling-video-o1",
"lowest_advertised_price": 0.112
}
]
```
At this point, you have models that satisfy the hard requirements. Score the
matching set before selecting one.
## Step 3: Score the matching models by priority
Use weighted priorities to make the final choice. For example, a draft workflow
might prioritize speed and cost, while a production render might prioritize
quality and cost:
```js expandable lines theme={null}
const priorityProfiles = {
fastAndCheap: {
speed: 0.55,
cost: 0.35,
quality: 0.1,
},
qualityAndCost: {
speed: 0.15,
cost: 0.3,
quality: 0.55,
},
balanced: {
speed: 0.33,
cost: 0.33,
quality: 0.34,
},
};
const resolutionRanks = new Map([
["480p", 1],
["720p", 2],
["1080p", 3],
["4K", 4],
]);
function getResolutionRank(model) {
return Math.max(
0,
...(model.supported_resolutions ?? []).map((resolution) => {
return resolutionRanks.get(resolution) ?? 0;
}),
);
}
function getSpeedScore(model) {
const id = model.id.toLowerCase();
if (id.includes("fast")) return 1;
if (id.includes("lite") || id.includes("std")) return 0.8;
if (id.includes("pro") || id.includes("o1")) return 0.35;
return 0.55;
}
function normalize(value, min, max, invert = false) {
if (!Number.isFinite(value) || max === min) {
return 0.5;
}
const score = (value - min) / (max - min);
return invert ? 1 - score : score;
}
function scoreVideoModels(models, weights) {
const prices = models.map(getLowestAdvertisedPrice).filter(Number.isFinite);
const minPrice = prices.length > 0 ? Math.min(...prices) : 0;
const maxPrice = prices.length > 0 ? Math.max(...prices) : 0;
const maxResolutionRank = Math.max(0, ...models.map(getResolutionRank));
return models
.map((model) => {
const price = getLowestAdvertisedPrice(model);
const speedScore = getSpeedScore(model);
const costScore = Number.isFinite(price)
? normalize(price, minPrice, maxPrice, true)
: 0;
const qualityScore =
maxResolutionRank === 0 ? 0.5 : getResolutionRank(model) / maxResolutionRank;
const score =
weights.speed * speedScore +
weights.cost * costScore +
weights.quality * qualityScore;
return {
model,
id: model.id,
score: Number(score.toFixed(3)),
lowest_advertised_price: price,
speed_score: Number(speedScore.toFixed(3)),
cost_score: Number(costScore.toFixed(3)),
quality_score: Number(qualityScore.toFixed(3)),
};
})
.sort((first, second) => second.score - first.score);
}
function summarizeScores(rankedModels) {
return rankedModels.slice(0, 4).map(({ model: _model, ...summary }) => {
return summary;
});
}
const fastAndCheapModels = scoreVideoModels(
matchingModels,
priorityProfiles.fastAndCheap,
);
const qualityAndCostModels = scoreVideoModels(
matchingModels,
priorityProfiles.qualityAndCost,
);
const model = fastAndCheapModels[0]?.model;
if (!model) {
throw new Error("No scored video model found.");
}
console.log(
JSON.stringify(
{
fast_and_cheap: summarizeScores(fastAndCheapModels),
quality_and_cost: summarizeScores(qualityAndCostModels),
},
null,
2,
),
);
console.log(`Use ${model.id}`);
```
Actual output from the scoring helper:
```json expandable lines theme={null}
{
"fast_and_cheap": [
{
"id": "bytedance/seedance-2.0-fast",
"score": 0.967,
"lowest_advertised_price": 0.0000056,
"speed_score": 1,
"cost_score": 1,
"quality_score": 0.667
},
{
"id": "bytedance/seedance-2.0",
"score": 0.752,
"lowest_advertised_price": 0.000007,
"speed_score": 0.55,
"cost_score": 1,
"quality_score": 1
},
{
"id": "bytedance/seedance-1-5-pro",
"score": 0.642,
"lowest_advertised_price": 0.0000012,
"speed_score": 0.35,
"cost_score": 1,
"quality_score": 1
},
{
"id": "alibaba/wan-2.6",
"score": 0.628,
"lowest_advertised_price": 0.04,
"speed_score": 0.55,
"cost_score": 0.643,
"quality_score": 1
}
],
"quality_and_cost": [
{
"id": "bytedance/seedance-2.0",
"score": 0.932,
"lowest_advertised_price": 0.000007,
"speed_score": 0.55,
"cost_score": 1,
"quality_score": 1
},
{
"id": "bytedance/seedance-1-5-pro",
"score": 0.903,
"lowest_advertised_price": 0.0000012,
"speed_score": 0.35,
"cost_score": 1,
"quality_score": 1
},
{
"id": "alibaba/wan-2.6",
"score": 0.825,
"lowest_advertised_price": 0.04,
"speed_score": 0.55,
"cost_score": 0.643,
"quality_score": 1
},
{
"id": "bytedance/seedance-2.0-fast",
"score": 0.817,
"lowest_advertised_price": 0.0000056,
"speed_score": 1,
"cost_score": 1,
"quality_score": 0.667
}
]
}
```
```text lines theme={null}
Use bytedance/seedance-2.0-fast
```
Pick the model that best fits your product needs after capability matching.
For example, you might prefer the lowest compatible price, audio support, seed
support, provider-specific controls, a specific provider, or a known latency
profile. The speed score is a slug-based heuristic, and the quality score uses
resolution support as a proxy. Pricing SKU units can differ by provider, so
treat the helper as a quick starting point and inspect the matching model's
`pricing_skus` before routing production traffic.
## Step 4: Preview the generation request
Before submitting, have the implementation build the exact request body it will
send. This makes capability mismatches visible before starting a paid job:
```js expandable lines theme={null}
const firstFrameUrl = process.env.FIRST_FRAME_URL;
if (!firstFrameUrl) {
throw new Error("Set FIRST_FRAME_URL to a directly downloadable image URL.");
}
const requestBody = {
model: model.id,
prompt:
"A handheld vertical product shot of a ceramic mug on a sunny kitchen counter",
duration: 5,
resolution: "720p",
aspect_ratio: "9:16",
frame_images: [
{
type: "image_url",
image_url: {
url: firstFrameUrl,
},
frame_type: "first_frame",
},
],
};
console.log(JSON.stringify(requestBody, null, 2));
```
Before submitting, check that your image URL returns `200` with an image
content type:
```bash lines theme={null}
curl -I "$FIRST_FRAME_URL"
```
Example output:
```text lines theme={null}
HTTP/2 200
content-type: image/jpeg
```
## Step 5: Submit when ready
```js lines theme={null}
const apiKey = process.env.OPENROUTER_API_KEY;
if (!apiKey) {
throw new Error("Set OPENROUTER_API_KEY before submitting a video job.");
}
const generation = await fetch("https://openrouter.ai/api/v1/videos", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
});
if (!generation.ok) {
throw new Error(await generation.text());
}
console.log(await generation.json());
```
The submission response contains the job `id`, `polling_url`, and an initial
status. In a completed run, that submitted job later reached this final state:
```json lines theme={null}
{
"id": "S2wge1oFOBzIj1PpFcFu",
"status": "completed",
"polling_url": "https://openrouter.ai/api/v1/videos/S2wge1oFOBzIj1PpFcFu",
"has_unsigned_urls": true
}
```
## Check your work
Before submission, you should see a request body whose model supports every
capability you filtered for. If you submit the request, you should see a
response with a video job `id`, a `polling_url`, and an initial status such as
`pending`. To wait for the playable MP4, use the polling and download helper
from [Generate and Download a Video from Text](/docs/cookbook/video-generation/text-to-video).