# 异步任务

来源: https://platform.lyrouter.com/ai_gateway/docs/async
更新日期: 2026-10-19

视频生成通常需要几分钟，LYRouter 以异步任务的方式提供：`POST /v1/videos` 提交后立即返回一个以 `aigwt_` 开头的任务 ID，之后用 `GET /v1/videos/{id}` 查询状态，完成后用 `GET /v1/videos/{id}/content` 下载视频。网关会替你向服务商轮询任务进度，你的查询只读取网关记录的状态；费用在任务完成时结算，失败或超时的任务不收费。各模型的参数、尺寸和计费单位见[多模态能力](https://platform.lyrouter.com/ai_gateway/docs/multimodal)，本页只讲任务的生命周期。

## 生命周期

| 步骤 | 方法与路径 | 返回 |
| --- | --- | --- |
| 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 提交，目前它们在表单里会被换成占位值转发给服务商，导致提交被服务商拒绝或任务失败。参数见[多模态能力](https://platform.lyrouter.com/ai_gateway/docs/multimodal)中的「视频通用参数」和各模型章节。
- **同一模型的其他任务**：部分模型还提供 `POST /v1/videos/regenerations`（视频升清）和 `POST /v1/videos/context`（上下文增强，产出的是文本），它们返回同样的任务对象，用同样的方式查询，见[多模态能力](https://platform.lyrouter.com/ai_gateway/docs/multimodal)。
- **谁能查询**：同一工作空间内的任意 API Key 都能查询、下载该工作空间的任务；其他工作空间的 Key 查询时返回 404。
- **只接受具体模型**：虚拟模型和 `lyrouter/auto` 不能用于视频接口，失败时也不会改用默认模型。

## 任务对象

提交、查询返回的都是同一种任务对象。下面是一个已完成任务的示例：

```json
{
  "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` 中：

```json
{
  "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 的错误率，频繁调用可能触发[熔断](https://platform.lyrouter.com/ai_gateway/docs/rate-errors)，请先查询到 `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` 头：

```bash
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 数）计费，而不是按请求中填写的值。各模型的计费单位和分档见[多模态能力](https://platform.lyrouter.com/ai_gateway/docs/multimodal)。
- **完成时扣费**：费用在任务变为 `completed` 时结算；`failed` 的任务（包括超时）不收费。提交请求本身不产生费用。
- **在日志中查看**：在[日志](https://platform.lyrouter.com/ai_gateway/docs/logs)列表的 **自定义列** 中打开「任务类型」「费用」两列。提交请求的「任务类型」显示「异步」，任务结算前「费用」显示「结算中」，结算后显示该任务的费用。打开日志详情会看到「创建任务的这次请求本身不产生费用，钱记在它拉起的任务上」以及任务 ID，切换到「整个任务」可查看任务的「结算状态」和「任务费用」。失败的任务不会结算，日志列表可能一直显示「结算中」，请以 **多媒体生成** 页面中的任务状态为准。
- **配额**：任务的费用在结算时计入适用的信用点[配额](https://platform.lyrouter.com/ai_gateway/docs/quota)。

## 完整示例：提交、轮询、下载

把 `MODEL_ID` 换成模型广场中的视频模型 ID，并按该模型支持的取值填写 `seconds`、`size`（见[多模态能力](https://platform.lyrouter.com/ai_gateway/docs/multimodal)）。cURL 示例需要 `jq`，请保存为脚本文件后用 `bash` 运行（出错时用 `exit` 退出）。

**视频任务示例**

cURL:
```bash
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
```

Python:
```python
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"))
```

Node.js:
```js
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` | 视频参数不合法，例如尺寸、时长不在模型支持的范围内。按报错修改，见[多模态能力](https://platform.lyrouter.com/ai_gateway/docs/multimodal) |
| 400 | `unsupported parameters for this model: <参数名>` | `unsupported_parameters` | 带了 `X-Gateway-Strict-Parameters: true` 且有模型不支持的参数，见[网关扩展请求头与响应头](https://platform.lyrouter.com/ai_gateway/docs/gateway-headers) |
| 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。
- 提交时服务商拒绝请求的，状态码和报错原文由服务商决定。完整列表见[错误码与请求标识](https://platform.lyrouter.com/ai_gateway/docs/errors)。
