异步任务
视频生成通常需要几分钟,LYRouter 以异步任务的方式提供:POST /v1/videos 提交后立即返回一个以 aigwt_ 开头的任务 ID,之后用 GET /v1/videos/{id} 查询状态,完成后用 GET /v1/videos/{id}/content 下载视频。网关会替你向服务商轮询任务进度,你的查询只读取网关记录的状态;费用在任务完成时结算,失败或超时的任务不收费。各模型的参数、尺寸和计费单位见多模态能力,本页只讲任务的生命周期。
生命周期
| 步骤 | 方法与路径 | 返回 |
|---|---|---|
| 1. 提交 | POST https://api.lyrouter.com/v1/videos |
任务对象,status 为 queued |
| 2. 查询 | GET https://api.lyrouter.com/v1/videos/{id} |
任务对象,status 随进度变化 |
| 3. 下载 | GET https://api.lyrouter.com/v1/videos/{id}/content |
302 跳转到视频文件地址 |
| 删除 | DELETE https://api.lyrouter.com/v1/videos/{id} |
任务已结束时返回任务对象并带 "deleted": true;任务记录不会因此删除,进行中的任务返回 409 |
- 提交:鉴权与其他接口相同。请求体可以是 JSON,也可以是
multipart/form-data(OpenAI 官方 SDK 的videos.create()使用这种格式);用表单提交时,input_reference只能填可公开访问的https://地址:目前上传文件不会报错,但文件会被忽略,任务按没有参考视频的方式生成并照常计费;first_frame、last_frame、reference_image、reference_audio请用 JSON 提交,目前它们在表单里会被换成占位值转发给服务商,导致提交被服务商拒绝或任务失败。参数见多模态能力中的「视频通用参数」和各模型章节。 - 同一模型的其他任务:部分模型还提供
POST /v1/videos/regenerations(视频升清)和POST /v1/videos/context(上下文增强,产出的是文本),它们返回同样的任务对象,用同样的方式查询,见多模态能力。 - 谁能查询:同一工作空间内的任意 API Key 都能查询、下载该工作空间的任务;其他工作空间的 Key 查询时返回 404。
- 只接受具体模型:虚拟模型和
lyrouter/auto不能用于视频接口,失败时也不会改用默认模型。
任务对象
提交、查询返回的都是同一种任务对象。下面是一个已完成任务的示例:
{
"id": "aigwt_volcengine_videos_MXgno8VlzuzZtJl0kq83jA",
"object": "video",
"model": "doubao-seedance-2-0-mini-260615",
"status": "completed",
"progress": 100,
"created_at": 1791950400,
"completed_at": 1791950538,
"expires_at": 1792555200,
"seconds": 5,
"size": "1280x720",
"usage": { "duration_seconds": 5, "completion_tokens": 108900 }
}
| 字段 | 说明 |
|---|---|
id |
网关的任务 ID,格式为 aigwt_<服务商>_<任务类型>_<随机串>。它不是服务商的任务 ID,向服务商报障时请用 error.provider_request_id |
object |
固定为 video |
model |
实际调用的上游模型名,可能与请求中的模型 ID 写法不同 |
status |
queued、in_progress、completed、failed,见下表 |
progress |
完成前为 0,完成后为 100,不表示中间进度,请以 status 判断 |
created_at / completed_at |
提交时间、结束时间(Unix 秒)。completed_at 只在任务结束后出现 |
expires_at |
任务记录的过期时间,提交后 7 天。过期后查询返回 404 |
seconds / size |
原样回显提交时的值 |
output_text |
仅上下文增强任务有,为增强后的提示词 |
error |
仅 failed 时出现,见下文「失败的任务」 |
usage |
任务完成后的计量:视频任务为 duration_seconds(视频秒数),按 Token 计费的模型(如 Seedance)另有 completion_tokens;上下文增强任务为 completion_tokens |
status |
含义 | 你需要做的 |
|---|---|---|
queued |
已受理,尚未开始生成 | 继续轮询 |
in_progress |
服务商正在生成 | 继续轮询 |
completed |
已完成 | 调用 /content 下载,或读取 output_text |
failed |
已失败 | 读取 error,不收费 |
失败的任务
失败的任务查询时仍返回 HTTP 200,status 为 failed,原因在 error 中:
{
"id": "aigwt_volcengine_videos_Qw3hTz0bVn1sLk8pYc4dRe",
"object": "video",
"model": "doubao-seedance-2-0-mini-260615",
"status": "failed",
"progress": 0,
"created_at": 1791950400,
"completed_at": 1791950482,
"expires_at": 1792555200,
"seconds": 5,
"error": {
"code": "OutputVideoSensitiveContentDetected",
"message": "The request failed because the output video may contain sensitive information.",
"provider_request_id": "021791950538123abcdef",
"retryable": false
}
}
error.code |
含义 |
|---|---|
deadline_exceeded |
提交后 24 小时仍未完成。目前这个状态不会同步到查询接口,GET /v1/videos/{id} 可能一直显示 queued 或 in_progress,请按下文「设置等待上限」处理 |
canceled |
任务被取消。目前不支持取消进行中的任务,正常使用不会出现 |
upstream_error |
服务商报告失败,但没有给出可识别的错误码 |
| 服务商的错误码 | 原样返回,例如 InvalidParameter、OutputVideoSensitiveContentDetected 等 |
error.provider_request_id:服务商自己的请求标识,联系服务商或 LYRouter 支持时提供它。没有时不出现。error.retryable:false表示原样重试也不会成功(例如参数不合法、内容审核未通过),需要修改请求;true表示可以重试。网关无法判断时不出现这个字段,请不要把缺失当成false。- 以上失败都不收费。
轮询建议
- 间隔:提交后约 10 秒开始查询,之后逐步放宽到每 30 秒一次。网关在后台按自己的节奏向服务商轮询,查询得更频繁不会更早拿到结果。
- 只在完成后下载:任务未完成时调用
/content返回 409。这类 4xx 也会计入 API Key 的错误率,频繁调用可能触发熔断,请先查询到completed再下载。 - 查询失败不等于任务失败:查询超时或网络错误时,保留任务 ID 继续查询,不要重新提交。重新提交会生成一个新任务并再次计费。
- 设置等待上限:提交 24 小时后仍未完成的任务不会再有结果,也不收费,可以停止轮询。在控制台工作空间的 多媒体生成 页面按任务 ID 搜索,可以看到任务的「状态」(如「生成中」「已过期」)、「失败原因」和产物。
- 服务商并发已满:提交时返回 429
upstream_concurrency_full表示该服务商账户的并发任务已满,这个限制按服务商账户共享,换一个 API Key 也一样,请稍后重新提交。
下载产物
GET /v1/videos/{id}/content 返回 302,Location 头是服务商提供的带签名的视频地址。让客户端跟随跳转即可下载(cURL 加 -L,Python httpx 设 follow_redirects=True,Node.js 的 fetch 默认跟随)。只想拿到地址时不跟随跳转,读取 Location 头:
curl -s -o /dev/null -w '%{redirect_url}\n' \
https://api.lyrouter.com/v1/videos/$TASK_ID/content \
-H "Authorization: Bearer $LYROUTER_API_KEY"
- 视频地址在任务完成时确定,之后每次调用
/content都跳转到同一个地址。地址的有效期由服务商决定,通常为 24 小时,过期后无法通过网关重新获取,请拿到后立即下载或转存到自己的存储。 - 上下文增强任务的结果是
output_text中的文本,没有/content可下载。
计费与结算
- 价格在提交时锁定:提交之后再调价,不影响这个任务的费用。
- 数量按实际结果计算:任务完成时,按服务商实际返回的计量(如视频秒数、Token 数)计费,而不是按请求中填写的值。各模型的计费单位和分档见多模态能力。
- 完成时扣费:费用在任务变为
completed时结算;failed的任务(包括超时)不收费。提交请求本身不产生费用。 - 在日志中查看:在日志列表的 自定义列 中打开「任务类型」「费用」两列。提交请求的「任务类型」显示「异步」,任务结算前「费用」显示「结算中」,结算后显示该任务的费用。打开日志详情会看到「创建任务的这次请求本身不产生费用,钱记在它拉起的任务上」以及任务 ID,切换到「整个任务」可查看任务的「结算状态」和「任务费用」。失败的任务不会结算,日志列表可能一直显示「结算中」,请以 多媒体生成 页面中的任务状态为准。
- 配额:任务的费用在结算时计入适用的信用点配额。
完整示例:提交、轮询、下载
把 MODEL_ID 换成模型广场中的视频模型 ID,并按该模型支持的取值填写 seconds、size(见多模态能力)。cURL 示例需要 jq,请保存为脚本文件后用 bash 运行(出错时用 exit 退出)。
BASE=https://api.lyrouter.com/v1
AUTH="Authorization: Bearer $LYROUTER_API_KEY"
# 1. 提交任务;没拿到任务 ID 就打印响应并退出,不要继续轮询
RESP=$(curl -s "$BASE/videos" -H "$AUTH" -H "Content-Type: application/json" \
-d '{"model":"MODEL_ID","prompt":"一只柯基在草地上奔跑,阳光穿过树叶","seconds":5}')
TASK_ID=$(echo "$RESP" | jq -r '.id // empty')
if [ -z "$TASK_ID" ]; then echo "提交失败:$RESP"; exit 1; fi
echo "task: $TASK_ID"
# 2. 轮询:从 10 秒开始,逐步放宽到 30 秒,直到结束;最多等 2 小时
DELAY=10
GIVE_UP_AT=$(( $(date +%s) + 2 * 3600 ))
while true; do
if [ "$(date +%s)" -gt "$GIVE_UP_AT" ]; then
echo "$TASK_ID 仍在生成,可稍后用同一个 ID 继续查询"; exit 1
fi
sleep $DELAY
DELAY=$(( DELAY * 3 / 2 > 30 ? 30 : DELAY * 3 / 2 ))
TASK=$(curl -s "$BASE/videos/$TASK_ID" -H "$AUTH")
STATUS=$(echo "$TASK" | jq -r '.status')
echo "status: $STATUS"
case "$STATUS" in
completed|failed) break ;;
queued|in_progress|"") ;; # 空值多为网络抖动:保留任务 ID 继续查
*) echo "查询出错:$TASK"; exit 1 ;;
esac
done
# 3. 完成则下载,失败则打印原因
if [ "$STATUS" = "completed" ]; then
curl -sL "$BASE/videos/$TASK_ID/content" -H "$AUTH" -o video.mp4
else
echo "$TASK" | jq '.error'
fi
import os
import time
import httpx
BASE = "https://api.lyrouter.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['LYROUTER_API_KEY']}"}
with httpx.Client(headers=HEADERS, timeout=60) as client:
# 1. 提交任务
resp = client.post(f"{BASE}/videos", json={
"model": "MODEL_ID",
"prompt": "一只柯基在草地上奔跑,阳光穿过树叶",
"seconds": 5,
})
resp.raise_for_status()
dropped = resp.headers.get("X-Gateway-Unsupported-Parameters")
if dropped:
print("这些参数没有生效:", dropped)
task = resp.json()
task_id = task.get("id")
if not task_id:
raise RuntimeError(f"没有拿到任务 ID:{task}")
print("task:", task_id)
# 2. 轮询:从 10 秒开始,逐步放宽到 30 秒;最多等 2 小时
delay, give_up_at = 10, time.time() + 2 * 3600
while task["status"] in ("queued", "in_progress"):
if time.time() > give_up_at:
raise TimeoutError(f"{task_id} 仍在生成,可稍后用同一个 ID 继续查询")
time.sleep(delay)
delay = min(delay * 1.5, 30)
try:
resp = client.get(f"{BASE}/videos/{task_id}")
resp.raise_for_status()
except httpx.TransportError:
continue # 网络抖动:保留任务 ID 继续查,不要重新提交
task = resp.json()
if task["status"] == "failed":
err = task.get("error", {})
raise RuntimeError(f"{err.get('code')}: {err.get('message')}")
# 3. 下载:接口返回 302,跟随跳转即可
with client.stream("GET", f"{BASE}/videos/{task_id}/content",
follow_redirects=True, timeout=600) as video:
video.raise_for_status()
with open("video.mp4", "wb") as f:
for chunk in video.iter_bytes():
f.write(chunk)
print("已保存 video.mp4,用量:", task.get("usage"))
import { writeFile } from "node:fs/promises";
const BASE = "https://api.lyrouter.com/v1";
const headers = { Authorization: `Bearer ${process.env.LYROUTER_API_KEY}` };
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// 1. 提交任务
const submit = await fetch(`${BASE}/videos`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({
model: "MODEL_ID",
prompt: "一只柯基在草地上奔跑,阳光穿过树叶",
seconds: 5,
}),
});
if (!submit.ok) throw new Error(await submit.text());
let task = await submit.json();
if (!task.id) throw new Error(`没有拿到任务 ID:${JSON.stringify(task)}`);
console.log("task:", task.id);
// 2. 轮询:从 10 秒开始,逐步放宽到 30 秒
let delay = 10_000;
while (task.status === "queued" || task.status === "in_progress") {
await sleep(delay);
delay = Math.min(delay * 1.5, 30_000);
let resp;
try {
resp = await fetch(`${BASE}/videos/${task.id}`, { headers });
} catch {
continue; // 网络抖动:保留任务 ID 继续查,不要重新提交
}
if (!resp.ok) throw new Error(await resp.text());
task = await resp.json();
}
if (task.status === "failed") {
throw new Error(`${task.error?.code}: ${task.error?.message}`);
}
// 3. 下载:fetch 默认跟随 302 跳转
const video = await fetch(`${BASE}/videos/${task.id}/content`, { headers });
if (!video.ok) throw new Error(`download failed: ${video.status}`);
await writeFile("video.mp4", Buffer.from(await video.arrayBuffer()));
console.log("saved video.mp4", task.usage);
常见错误
错误响应为 {"error":{"message":"...","type":"...","code":"..."}}。
| 状态码 | error.message |
error.code |
原因与处理 |
|---|---|---|---|
| 404 | video task not found |
not_found |
任务 ID 不存在、已超过 7 天、属于其他工作空间,或传入的是服务商的任务 ID。确认使用提交时返回的 aigwt_ ID |
| 409 | video is not ready yet |
not_ready |
任务还没完成(或已失败)就调用了 /content。先查询到 completed |
| 409 | cancelling an in-flight video task is not supported: ... |
cancel_not_supported |
对进行中的任务调用了 DELETE。服务商不提供取消接口,请等待任务结束 |
| 429 | upstream concurrency full |
upstream_concurrency_full |
服务商账户的并发任务已满,换 API Key 无效,稍后重新提交 |
| 400 | 具体参数的校验原文 | invalid_parameter |
视频参数不合法,例如尺寸、时长不在模型支持的范围内。按报错修改,见多模态能力 |
| 400 | unsupported parameters for this model: <参数名> |
unsupported_parameters |
带了 X-Gateway-Strict-Parameters: true 且有模型不支持的参数,见网关扩展请求头与响应头 |
| 403 | Model is not available in the model plaza |
model_not_available |
模型没有在本工作空间开通,或使用了虚拟模型、lyrouter/auto。换用已开通的具体模型 ID |
- 用表单提交时
input_reference只能传 URL。目前上传文件不会报错,文件会被忽略,任务按没有参考视频的方式生成并照常计费;只有填了非 URL 的文本(如video.mp4)时才返回 400 纯文本input_reference must be a URL; uploading a file is not supported by this gateway (pass a publicly reachable https:// URL instead)。这类由网关返回的纯文本报错,响应头目前仍是Content-Type: application/json,解析前请先判断正文是不是 JSON。 - 提交时服务商拒绝请求的,状态码和报错原文由服务商决定。完整列表见错误码与请求标识。