OpenRouter 影片生成 API:程式碼優先指引
OpenRouter Video Generation API: A Code-First Guide
OpenRouter 透過統一的非同步影片生成 API,讓開發者一次提交、輪詢、下載即可支援 Seedance、Veo、Wan 等多款模型。
- 工作流程:POST /api/v1/videos 提交工作 → 監控 job‑status → 下載 MP4。
統一的非同步 API 讓多模型影片生成變簡單,利於快速測試與成本掌控。
將影片生成加入應用程式時,若只測試單一模型,流程相當簡單。但若想嘗試其他模型,複雜度就會顯現。每個供應商可能擁有不同的端點、請求引數、工作狀態、輪詢邏輯以及輸出格式。這使得簡單的模型切換變成另一個需要建置與維護的整合。
我們把這個工作流程放在 一個非同步影片 API 後面。你向 POST /api/v1/videos 提交提示,收到工作 ID,持續輪詢直到生成完成,然後下載完成的影片。
在本指南中,我們將從頭到尾構建這個流程。先使用 Seedance 提交工作,安全輪詢,儲存 MP4,接著以相同方式整合 Veo 和 Wan。
簡而言之
- 一個端點,支援多個影片模型。透過
POST /api/v1/videos使用 Seedance、Veo、Wan 以及其他支援的模型進行生成。 - 工作流程為非同步。提交工作,輪詢其狀態,然後下載完成的影片。
- 透過更改模型識別碼來切換模型。部分模型特定設定,例如持續時間與長寬比,仍需依模型調整,詳見第 4 步,但端點、授權、輪詢迴圈與下載邏輯不會改變。
為何非同步 API 比其他方案更佳
影片生成所需時間長於一般 API 回應。模型必須產生並協調多個畫格,維持畫格間的視覺一致性,並且有時會產生相配的音訊。根據模型與請求設定,整個過程可能從數秒到數分鐘不等。
整段時間保持原始 HTTP 請求開啟非常脆弱。瀏覽器會話可能關閉,無伺服器函式可能達到執行限制,或代理可能在影片尚未完成前逾時。
非同步 API 將提交與完成分離:
- 提交生成請求。
- 立即收到工作 ID。
- 單獨檢查工作狀態。
- 生成完成後下載影片。
你的應用程式可以在模型在背景執行時繼續運作。亦可於重新啟動後恢復工作,因為生成是附著於持久工作 ID,而非長時間連線。
直接與單一供應商整合
若你已知想使用的模型且不預期會改變,直接與供應商整合可行。你會使用供應商的授權、請求格式、工作狀態、輪詢端點以及輸出回應。
當你想比較另一個模型時,額外工作才會顯現。新的供應商可能使用不同的欄位名稱來表示持續時間與解析度,或回傳不同的工作物件及不同的終端狀態。它也可能需要另一種下載完成資產的方法。此時你的應用程式就需要第二個客戶端、另一組環境變數,以及更多供應商特定的錯誤處理。
這種做法本身並無錯誤。只是表示切換模型是整合變更,而非設定變更,這使實驗變慢並隨著模型清單增長而提高維護成本。
在本機執行影片模型
本機生成可讓你擁有最高控制權。你可以選擇模型權重、客製化工作流程、將資產保留在自身環境內,並避免為每一次生成支付託管供應商費用。
這種控制伴隨著基礎設施的責任。您需要適當的 GPU 容量以及正確的 Python 與 CUDA 相依套件。您還需要足夠的儲存空間和每個模型族群的執行環境。更高解析度和更長影片會增加記憶體與處理需求,另外加入另一模型可能需要下載更多權重或維護另一工作流程。
對於已經擁有 GPU 基礎設施或需要本地處理的團隊而言,這可能值得。當您目標是快速加入影片生成並測試多個模型時,這是一個較重的起點。託管的 OpenRouter 路徑移除了大部分設定,這正是本指南其餘部分所涵蓋的。
透過 OpenRouter 使用單一託管 API
我們保持支援影片模型的生成生命週期一致。無論選擇的模型是 Seedance、Veo、Wan 或目錄中的其他模型,應用程式都使用相同的 API 金鑰、POST /api/v1/videos 端點、工作狀態流程與輸出檢索流程。
模型仍有不同的功能。某些模型可能支援更長的時長,而另一些則提供額外的長寬比、更高解析度、音訊生成或供應商特定控制。我們透過影片模型端點呈現這些差異,而非將所有模型強行套用相同功能集。
這樣即可得到穩定的整合,同時不隱藏各模型的差異。您的應用程式可以查詢目前的功能、構建有效請求,並在不替換周邊工作基礎設施的情況下切換模型。
先決條件與設定
您只需要 OpenRouter API 金鑰以及能夠傳送 HTTP 請求的工具。此處範例使用 Python 搭配 requests 與 TypeScript 搭配內建 fetch API,但此工作流程可在任何能傳送 HTTP 請求的語言中使用。
先從您的 OpenRouter 帳號建立 API 金鑰,然後將其存入環境變數,而非直接寫入原始碼:
export OPENROUTER_API_KEY="sk-or-..."對於 Python 範例,如果尚未安裝 requests,請安裝:
pip install requestsOpenRouter 以 bearer token 驗證 API 請求。在 Python 中,我們將一次定義共用值,並在整個指南中重複使用:
import os
import requests
API_KEY = os.environ["OPENROUTER_API_KEY"]
BASE_URL = "https://openrouter.ai/api/v1"
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}提交工作前,您也可以 查詢影片模型端點 以檢視目前可用的模型及其支援專案:
curl "https://openrouter.ai/api/v1/videos/models" \
-H "Authorization: Bearer $OPENROUTER_API_KEY"回應包含各模型支援的時長、解析度、長寬比、幀影像支援、音訊功能、定價 SKU 與供應商特定引數。這比假設一個影片模型接受的設定同樣適用於另一個模型更可靠。
步驟 1:提交影片生成工作
向 /api/v1/videos 傳送 POST 請求,並指定影片模型。請包含描述您想要生成內容的 prompt。
model 在每一次請求中必填,prompt 在文字轉影片時必填。僅支援以影像輸入生成影片的模型可省略。若選擇的模型支援,您亦可提供可選設定,如時長、解析度、長寬比、音訊生成、參考影像及種子。
我們將在整個指南中使用相同的 prompt:
PROMPT = (
"A paper boat drifting down a rain-slicked gutter at night, "
"neon reflections, slow tracking shot, cinematic lighting"
)以下函式使用 Seedance 2.0 提交工作:
def submit_video(model: str, prompt: str) -> dict:
response = requests.post(
f"{BASE_URL}/videos",
headers=HEADERS,
json={
"model": model,
"prompt": prompt,
"duration": 4,
"resolution": "720p",
"aspect_ratio": "16:9",
"generate_audio": False,
},
timeout=60,
)
response.raise_for_status()
return response.json()
job = submit_video(
model="bytedance/seedance-2.0",
prompt=PROMPT,
)
print("Job ID:", job["id"])
print("Status:", job["status"])
print("Polling URL:", job["polling_url"])對應的 cURL 請求為:
curl "https://openrouter.ai/api/v1/videos" \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "bytedance/seedance-2.0",
"prompt": "A paper boat drifting down a rain-slicked gutter at night, neon reflections, slow tracking shot, cinematic lighting",
"duration": 4,
"resolution": "720p",
"aspect_ratio": "16:9",
"generate_audio": false
}'成功的請求回傳 HTTP 202 Accepted。回應代表背景工作,而非完成影片:
{
"id": "job-abc123",
"status": "pending",
"polling_url": "https://openrouter.ai/api/v1/videos/job-abc123"
}在繼續之前先儲存回傳的工作 ID。若程式重新啟動,您應能繼續追蹤現有工作,而非重新提交並支付額外生成費用。
步驟 2:輪詢工作直到完成
Step 1 中回傳的 polling_url 指向與 GET /api/v1/videos/{id} 相同的工作資源,它們是同一個端點。影片工作可經歷以下狀態:
| 狀態 | 含義 |
|---|---|
pending | 工作已被接受,正在等待執行 |
in_progress | 供應商正在產生影片 |
completed | 影片已準備好可下載 |
failed | 產生失敗 |
cancelled | 工作已被取消 |
expired | 工作超過允許的存活時間 |
你的輪詢迴圈應在 completed 時返回,並在 failed、cancelled 或 expired 時以錯誤停止。否則,應用程式可能會持續檢查永遠不會產生影片的工作。
檔案化回應會將 polling_url 回傳為完整 URL。以下的 urljoin 呼叫是防禦式程式碼,也能處理相對路徑,因此迴圈無論哪種情況都能運作:
import time
from urllib.parse import urljoin
TERMINAL_ERROR_STATES = {
"failed",
"cancelled",
"expired",
}
def poll_video(
initial_job: dict,
interval: float = 30.0,
timeout: float = 3600.0,
) -> dict:
"""Poll until the video completes or reaches an error state."""
polling_url = urljoin(
"https://openrouter.ai",
initial_job["polling_url"],
)
deadline = time.monotonic() + timeout
job = initial_job
while True:
status = job["status"]
print("Status:", status)
if status == "completed":
return job
if status in TERMINAL_ERROR_STATES:
error = job.get("error") or "No error details were returned."
raise RuntimeError(
f"Video generation ended with status '{status}': {error}"
)
if status not in {"pending", "in_progress"}:
raise RuntimeError(
f"Received unexpected job status: {status}"
)
if time.monotonic() >= deadline:
raise TimeoutError(
f"Job {job['id']} did not complete within "
f"{timeout} seconds."
)
time.sleep(interval)
response = requests.get(
polling_url,
headers={
"Authorization": f"Bearer {API_KEY}",
},
timeout=30,
)
response.raise_for_status()
job = response.json()
completed_job = poll_video(job)此迴圈包含兩項保護措施,快速範例常忽略。首先,它處理所有檔案化的終端狀態,而不僅僅等待 completed。其次,它設定一小時的逾時,避免工作無限期留在程序中。值得注意的一個邊緣案例:因為逾時檢查在每次休眠前進行,而非休眠後,工作在最壞情況下可能會超過一次輪詢間隔的名義逾時,才被迴圈捕捉。對於背景工作而言,這是一個不錯的折衷。如果你需要硬性上限,請在從休眠喚醒後立即再次檢查逾時。
我們目前的指引使用 30 秒輪詢間隔。影片工作通常需要約 30 秒至數分鐘,且每秒檢查不會讓供應商更快完成。上述間隔與逾時上限皆為操作指引,而非端點本身的檔案化合約,請依照自己的工作負載調整。
相同的輪詢流程,使用 TypeScript:
type VideoJobStatus =
| "pending"
| "in_progress"
| "completed"
| "failed"
| "cancelled"
| "expired";
type VideoJob = {
id: string;
polling_url: string;
status: VideoJobStatus;
error?: string;
unsigned_urls?: string[];
};
const apiKey = process.env.OPENROUTER_API_KEY;
if (!apiKey) {
throw new Error("OPENROUTER_API_KEY is not set");
}
const terminalErrorStates = new Set<VideoJobStatus>([
"failed",
"cancelled",
"expired",
]);
async function pollVideo(
initialJob: VideoJob,
intervalMs = 30_000,
timeoutMs = 3_600_000,
): Promise<VideoJob> {
const pollingUrl = new URL(
initialJob.polling_url,
"https://openrouter.ai",
);
const deadline = Date.now() + timeoutMs;
let job = initialJob;
while (true) {
console.log(`Status: ${job.status}`);
if (job.status === "completed") {
return job;
}
if (terminalErrorStates.has(job.status)) {
throw new Error(
job.error ?? `Video generation ${job.status}`,
);
}
if (Date.now() >= deadline) {
throw new Error(
`Video job ${job.id} did not complete before the timeout`,
);
}
await new Promise((resolve) =>
setTimeout(resolve, intervalMs),
);
const response = await fetch(pollingUrl, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
if (!response.ok) {
throw new Error(
`Polling failed: ${response.status} ${await response.text()}`,
);
}
job = (await response.json()) as VideoJob;
}
}將失敗的狀態 請求 與失敗的影片 工作 分開處理。輪詢時的臨時逾時並不表示產生本身失敗。請重試同一工作 ID 的狀態請求,而非提交新工作。
步驟 3:檢索並儲存影片
當狀態變為 completed 時,工作回應包含已填充的 unsigned_urls 陣列。每個條目指向工作已驗證的內容端點:
GET /api/v1/videos/{jobId}/content?index=0索引預設為 0。只有當模型返回多個影片輸出時才需要更改。儘管欄位名稱如此,這些 URL 並非預簽名,請像輪詢時一樣在 Authorization 標頭中傳送 API 金鑰。
以下輔助程式在存在未簽名 URL 時使用第一個,若極少情況下不存在,則從工作 ID 重新構造內容 URL。
def download_video(
job: dict,
output_path: str = "out.mp4",
index: int = 0,
) -> None:
unsigned_urls = job.get("unsigned_urls") or []
download_url = (
unsigned_urls[index]
if len(unsigned_urls) > index
else (
f"{BASE_URL}/videos/"
f"{job['id']}/content?index={index}"
)
)
with requests.get(
download_url,
headers={
"Authorization": f"Bearer {API_KEY}",
},
stream=True,
timeout=180,
) as response:
response.raise_for_status()
with open(output_path, "wb") as output_file:
for chunk in response.iter_content(
chunk_size=1024 * 1024
):
if chunk:
output_file.write(chunk)
print(f"Saved {output_path}")
download_video(completed_job)以分塊方式串流回應可避免在寫入磁碟前將整個 MP4 載入記憶體。
以下是 TypeScript 等效程式。請注意,此版本將下載緩衝到記憶體,而非直接串流到磁碟,對於短片來說沒問題,但若經常下載長或高解析度影片,建議改為使用管道串流:
import { writeFile } from "node:fs/promises";
async function downloadVideo(
job: VideoJob,
outputPath = "out.mp4",
index = 0,
): Promise<void> {
const downloadUrl =
job.unsigned_urls?.[index] ??
`https://openrouter.ai/api/v1/videos/` +
`${job.id}/content?index=${index}`;
const response = await fetch(downloadUrl, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
if (!response.ok) {
throw new Error(
`Download failed: ${response.status} ` +
`${await response.text()}`,
);
}
const videoBuffer = Buffer.from(
await response.arrayBuffer(),
);
await writeFile(outputPath, videoBuffer);
console.log(`Saved ${outputPath}`);
}此時,你已在磁碟上擁有生成的 MP4。請將完成的影片移至你控制的儲存位置,而非將產生端點視為永久檔案主機。完成的工作也可能包含一個 usage 物件,內含最終成本,這是回應主體的一部分,無論你使用哪種語言:
usage = completed_job.get("usage") or {}
print("Generation cost:", usage.get("cost"))
print("Used BYOK:", usage.get("is_byok"))將該值與內部工作記錄一起儲存,以便追蹤每次產生的實際成本。
步驟 4:一行切換模型
提交、輪詢與下載函式並未繫結於 Seedance。若要使用其他支援的影片模型,只需更改模型識別碼:
# Seedance
MODEL = "bytedance/seedance-2.0"
# Veo
# MODEL = "google/veo-3.1"
# Wan
# MODEL = "alibaba/wan-2.7"
job = submit_video(
model=MODEL,
prompt=PROMPT,
)
completed_job = poll_video(job)
download_video(completed_job)端點、驗證、回應結構、狀態處理以及下載邏輯在三者之間保持相同。自動遷移不到的是每個可選設定。切換模型只需改一行程式碼,但並不保證任何特定的時長、解析度或畫面比例組合在新模型上皆能驗證。此設定恰好在本指南所涵蓋的三個模型之間可攜帶:
{
"duration": 4,
"resolution": "720p",
"aspect_ratio": "16:9",
"generate_audio": false
}撰寫時,即時模型端點顯示 Seedance 2.0、Veo 3.1 與 Wan 2.7 均支援該特定組合:四秒、720p、16:9。這是三個範例之間共用的設定,並非聲稱每個設定在所有模型上都相同。超出此範圍時差異會迅速顯現:
- Veo 3.1 目前支援四秒、六秒及八秒的時長。
- Seedance 2.0 目前支援四至十五秒的時長以及額外的畫面比例。
- Wan 2.7 目前支援兩至十秒的時長以及 720p 或 1080p 的解析度。
五秒的請求在 Seedance 與 Wan 上會驗證通過,但在 Veo 上會失敗。這也是為什麼您的應用程式應在提交請求前查詢 /api/v1/videos/models,而不是假設一個模型接受的設定在另一個模型上也能使用。上述數字在依賴前值得再次與即時端點核對,因為模型功能會變化。
同一端點也會暴露 allowed_passthrough_parameters 供模型特定功能使用。這些是您允許在請求的 provider.options 物件內傳送的鍵,該物件以供應商 slug 為鍵,例如 provider.options["google-vertex"].parameters。僅轉發提供者支援的選項,未識別的鍵則會被丟棄。以 Veo 為例,目前列出了 negativePrompt 與 enhancePrompt 等控制項,而 Wan 則暴露 negative_prompt 與 prompt_extend 等選項。
進入正式環境前值得了解的幾點
上述程式碼足以產生並下載一段影片。當此流程投入正式環境後,問題就會改變:您需要控制成本、區分工作失敗與網路失敗、避免重複處理,並在提交程式結束後持續追蹤工作。
擴充前先檢查成本
影片生成價格因模型與設定而異。時長、解析度、音訊生成以及供應商的計費方式皆會影響最終成本。本地生成完全改變了成本結構,沒有每段影片費用,但需要實際的前期硬體與維護費用。託管 API 讓成本保持變動且與使用量掛鉤,根據您的使用量以及是否已擁有硬體,成本會較便宜或較昂貴。
不要在應用程式中建立單一的通用成本公式。查詢 /api/v1/videos/models 並閱讀所選模型的 pricing_skus,再顯示估算或提交大型批次。工作完成時,回應可包含 usage 物件,內含該生成的實際成本:
{
"usage": {
"cost": 0.5,
"is_byok": false
}
}在執行大型批次前,先使用目前模型資料估算成本,然後將估算值與已完成工作的實際 usage.cost 值進行比較。這也能協助您發現因較高解析度、較長時長、生成音訊或不同模型所造成的意外變化。
處理失敗而不產生重複工作
失敗的輪詢 請求 與失敗的影片生成 工作 並不相同。即使供應商仍在生成影片,您的應用程式在檢查狀態時仍可能失去連線。如果您立即再次提交提示,兩個工作都可能完成,結果您會得到兩支影片並被收取兩筆費用,雖然只是一個使用者請求。
在提交成功後立即持久化 OpenRouter 工作 ID。一個有用的工作紀錄可能包含以下欄位:
{
"internal_request_id": "req_9f21",
"openrouter_job_id": "job-abc123",
"model": "bytedance/seedance-2.0",
"status": "pending",
"attempt_number": 1,
"submitted_at": "2026-07-27T12:00:00Z",
"output_location": null,
"cost": null,
"error": null
}當狀態請求因超時、連線錯誤或暫時性伺服器回應失敗時,使用現有的工作 ID 重試狀態請求。僅在工作本身達到 failed、cancelled 或 expired 並且您的應用程式重試策略允許再次嘗試時,才建立新的生成。
將工作重試與輪詢重試分開。輪詢重試會再次檢查相同的工作,而生成重試則會產生一個新的付費工作。限制生成重試次數,並保留同一內部請求所建立的每個工作 ID,這樣在需要調查重複輸出、供應商失敗或意外成本時,您就能擁有完整紀錄。
在輪詢停止擴充套件時使用 Webhook
輪詢 是指令碼、原型與少量工作的良好預設方式。當您的應用程式可能同時執行數百個生成時,它的效率會降低。
若要自動接收結果,提交工作時請包含一個 HTTPS callback_url:
{
"model": "bytedance/seedance-2.0",
"prompt": "A paper boat drifting through neon reflections",
"duration": 4,
"resolution": "720p",
"aspect_ratio": "16:9",
"callback_url": "https://example.com/webhooks/openrouter-video"
}您可以為單一請求設定回呼,或為工作區設定預設回呼。請求層級的值會優先於工作區預設。
當工作達到終端狀態時,我們會傳送 Webhook。每次傳送都包含一個 X-OpenRouter-Idempotency-Key,例如:
job-abc123-completed在處理事件前先儲存該值。若 Webhook 再次傳送,您的處理程式即可辨識該工作已被處理,避免重複下載影片或重複啟動下一個工作流程。
當設定了 Webhook 簽名機密時,請求還會包含一個 X-OpenRouter-Signature。在解析或重新序列化之前,請先使用原始請求主體驗證簽名。生產環境的處理程式應該儲存新的工作狀態,快速回傳成功回應,並將下載、轉碼或儲存工作交由背景工作者處理。
在耐久儲存中追蹤併發工作
提交與等待是分離的操作,因此您的應用程式可同時執行多個影片工作。不要為每個工作啟動無限輪詢迴圈。使用受限的工作者池或工作佇列,並控制可同時執行的狀態請求與下載數量。
在 Python 中,您可以使用執行緒池或非同步工作佇列處理有限數量的工作。在 TypeScript 中,受限併發佇列比直接將數千個輪詢承諾傳遞給 Promise.all() 更安全。
實際實作不如以下規則重要:
- 在開始輪詢前儲存每個工作 ID。
- 限制活躍的輪詢與下載運算元量。
- 工作者重啟後恢復未完成的工作。
- 不要僅因應用程式重啟就重新提交工作。
- 完成後即時將影片移至您自己的儲存。
工作 ID 是您應用程式與已進行中的生成之間的耐久連結。將其視為應用程式狀態的一部分,而非僅存在於單一執行程序中的值。
將上述全部整合
我們已經說明瞭四個步驟,無論你指向哪一個模型,這些步驟都不會改變。你只需要使用 POST /api/v1/videos 提交請求,輪詢 GET /api/v1/videos/{id},同時監控所有四個終端狀態,下載結果,並在想切換不同模型時改變一個字串。
一旦非同步生命週期設定正確,模型就變成一個設定,而非架構決策,無論你使用本文提到的三個模型還是之後新增的任何模型。
如果你想先做一個起點,瀏覽影片模型目錄,可以在決定之前並排檢視價格與功能。
常見問題
OpenRouter 支援影片生成嗎?
是的,透過專用的非同步 API。你將提示送至 POST /api/v1/videos,輪詢 GET /api/v1/videos/{id} 直到狀態為 completed,然後下載結果。支援的模型包括 Seedance、Veo、Wan 等,均透過相同的端點。
如何使用 API 從文字產生影片?
傳送 POST 請求至 /api/v1/videos,包含模型與提示。你會收到一個工作 ID 以及一個 polling_url,而非影片本身。輪詢直到狀態達到 completed,然後從 unsigned_urls 或 /content 端點下載。
如何輪詢非同步影片生成工作?
以約 30 秒為間隔呼叫 GET /api/v1/videos/{id},直到狀態達到終端狀態:completed、failed、cancelled 或 expired。設定一個逾時上限,避免卡住的工作永遠掛起你的流程。
OpenRouter 支援哪些影片模型?
目錄包含 Seedance、Veo、Wan 等模型,且持續擴充。查詢 GET /api/v1/videos/models 可取得目前列表,以及各模型支援的解析度、時長、寬高比與傳遞引數。
我可以在不重寫程式碼的情況下切換影片模型嗎?
可以。請求格式、驗證與輪詢迴圈在所有模型中相同,僅 model 欄位不同。模型特定引數仍透過 provider.options 傳遞給供應商。
在本地生成 AI 影片還是透過 API 更划算?
本地生成在支付硬體費用後,每個片段不再收費,但需要具備強大的 GPU、相依套件管理,以及每個模型族別獨立設定。託管 API 以每次生成收費,且完全省去 GPU 與設定。哪種更便宜取決於你的使用量以及你是否已擁有硬體。
AI 影片生成需要多久?
通常介於三十秒到數分鐘之間,取決於模型、解析度與片長。這也是 API 以非同步方式而非同步呼叫的原因。
影片生成是否符合零資料保留?
不行。非同步檢索步驟需要暫時保留生成的輸出以便下載,因此啟用零資料保留的請求不會被路由至影片生成。
來源:openrouter blog · openrouter.ai