# Seedance 视频生成

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

用火山方舟的 Seedance 系列模型生成视频，支持文生视频、首帧 / 首尾帧生视频、参考图或参考视频生视频，以及视频编辑与延长。视频生成以异步任务的方式提供：提交后立即拿到任务 ID，轮询到完成后再下载视频。接口形状参照 OpenAI 的 Videos API，网关把统一的参数转换成火山方舟的格式；OpenAI 官方 SDK 的 `videos.create()` 可以直接提交任务，查询和下载建议用 HTTP 直接调用。任务生命周期的通用说明见[异步任务](https://platform.lyrouter.com/ai_gateway/docs/async)。

## 支持的模型

| 模型 ID | 特点 | 时长与分辨率 | 计费口径 |
| --- | --- | --- | --- |
| `bytedance/doubao-seedance-2.5` | 能力最全：参考图最多 30 张，可输出 1080p（10bit）和 `mov`，支持用 `omni_reference_task_type` 声明任务类型 | 4～30 秒；480p、720p、1080p | 按输出 Token；按分辨率（480p / 720p 一档、1080p 一档）和有无视频输入分档 |
| `bytedance/doubao-seedance-2.0-fast` | 2.0 系列，参考图最多 9 张，只输出 `mp4` | 4～15 秒；480p、720p | 按输出 Token；按有无视频输入分档 |
| `bytedance/doubao-seedance-2.0-mini` | 与 2.0-fast 能力相同 | 4～15 秒；480p、720p | 按输出 Token；按有无视频输入分档 |

三个模型都支持文生视频、首帧 / 首尾帧生视频、图片参考、视频参考、视频编辑与延长、生成有声视频，输出宽高比可选 21:9、16:9、4:3、1:1、3:4、9:16 或自适应。模型需要在工作空间的模型广场开通后才能调用；具体单价以模型广场显示的为准。

## 端点

| 方法 | 路径 | 用途 |
| --- | --- | --- |
| POST | `/v1/videos` | 提交视频生成任务，立即返回任务 ID |
| GET | `/v1/videos/{id}` | 查询任务状态 |
| GET | `/v1/videos/{id}/content` | 下载视频（302 跳转到视频地址） |
| DELETE | `/v1/videos/{id}` | 删除已结束的任务；不能取消进行中的任务 |

完整地址为 `https://api.lyrouter.com` 加上表中的路径。

## 鉴权

在请求头中携带 API Key：

```text
Authorization: Bearer <API Key>
```

`Bearer` 区分大小写。也可以改用 `x-api-key: <API Key>`，两者同时存在时只读 `Authorization`。API Key 在工作空间的 **API 密钥** 页面创建，只用于 LYRouter 鉴权，不会转发给上游服务商。完整说明见[接入地址与鉴权](https://platform.lyrouter.com/ai_gateway/docs/call-gateway)。

同一工作空间内的任意 API Key 都能查询、下载该工作空间的任务；其他工作空间的 Key 查询时返回 404。

## 请求头

| 请求头 | 必填 | 说明 |
| --- | --- | --- |
| `Content-Type` | 提交时必填 | `application/json`。`POST /v1/videos` 也接受 `multipart/form-data`，见下文「用表单提交」 |
| `X-Gateway-Strict-Parameters` | 否 | 设为 `true` 时，请求中有会被丢弃的参数就直接返回 400，不提交任务、不计费，见[网关扩展请求头与响应头](https://platform.lyrouter.com/ai_gateway/docs/gateway-headers) |

## 提交任务

`POST /v1/videos`

提交一个视频生成任务。网关校验参数、转换成火山方舟的格式后提交给服务商，立即返回 `status` 为 `queued` 的任务对象，视频在后台生成。提交请求本身不产生费用。

### 请求体

| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `model` | string | 是 | 上表中的模型 ID。必须是具体的模型 ID：虚拟模型 `@vmodel/<名称>` 和 `lyrouter/auto` 返回 403，失败时也不会改用工作空间的默认模型 |
| `prompt` | string | 是 | 提示词。用 `@图像1`、`@视频1` 指代参考素材；视频编辑、延长任务靠提示词表达意图，见下文「任务类型与限制」 |
| `seconds` | integer | 否 | 视频时长（秒），必须是正整数。2.5 为 4～30，2.0 系列为 4～15；不写时用服务商的默认值。**视频编辑任务不要传** |
| `size` | string | 否 | 分辨率，写成 `宽x高`（如 `1280x720`）或档位名（`480p`、`720p`、`1080p`），见下文「`size` 的两种写法」。`1080p` 仅 2.5 支持 |
| `first_frame` | string | 否 | 首帧图片的 URL（首帧生视频），1 张。输出宽高比自动跟随首帧图片 |
| `last_frame` | string | 否 | 尾帧图片的 URL，与 `first_frame` 一起使用（首尾帧生视频） |
| `reference_image` | string \| string[] | 否 | 参考图的 URL，单张写字符串，多张写数组。2.5 最多 30 张，2.0 系列最多 9 张 |
| `input_reference` | string | 否 | 参考**视频**的 URL，用于参考生视频、视频编辑和视频延长。只能传一个地址，写成数组返回 400 |
| `generate_audio` | boolean | 否 | 是否生成有声视频，三个模型都支持。不传时默认开启，生成的视频带音轨 |
| `watermark` | boolean | 否 | `true` 时在视频右下角加「AI生成」标识，默认不加 |
| `output_format` | string | 否 | 输出格式：`mp4`（默认）或 `mov`。`mov` 仅 2.5 支持 |
| `omni_reference_task_type` | string | 否 | 为 2.5 设计；2.0 系列传入目前不会报错，任务照常生成并计费。显式声明任务类型：`auto`（默认）、`reference`、`edit`、`extend`，见下文「任务类型与限制」 |

- 表中没有的字段会被丢弃，响应头 `X-Gateway-Unsupported-Parameters` 列出它们的名字；带 `X-Gateway-Strict-Parameters: true` 时改为返回 400。火山方舟原生的 `duration`、`resolution`、`ratio`、`content` 也属于这一类，请改用 `seconds`、`size` 和上面的素材字段。
- 本接口不支持 `n`，每次请求生成一个视频。
- `reference_audio`（参考音频）是例外：目前不会转发给 Seedance，传了不报错，也不会出现在 `X-Gateway-Unsupported-Parameters` 中，参考音频被直接忽略；需要参考音频时请改用 [MiniMax H3 视频生成](https://platform.lyrouter.com/ai_gateway/docs/video-minimax)。`source_video` 只用于 MiniMax H3 的视频升清，传给 Seedance 返回 400 `source_video is not supported for this model`。
- 素材地址要能被服务商访问。大文件请用公网 URL，不要用 Base64，请求体总大小不超过 64 MB。

### 用表单提交

`POST /v1/videos` 也接受 `multipart/form-data`，OpenAI 官方 SDK 的 `videos.create()` 就用这种格式。表单中网关读取 `model`、`prompt`、`seconds`、`size`、`input_reference`、`watermark`、`generate_audio`、`output_format`、`omni_reference_task_type` 这几个字段：

- **`input_reference` 只能传 URL，传文件不会报错但会被忽略**：请填可公开访问的 `https://` 地址；目前上传文件时，任务按没有参考视频的方式生成，并照常计费。
- `watermark`、`generate_audio` 只接受字面量 `true` 或 `false`。
- `first_frame`、`last_frame`、`reference_image` 请用 JSON 请求体提交。目前表单里的这些字段会被换成占位值转发给服务商，例如带 `first_frame` 时服务商返回 400 `The parameter content[1].image_url specified in the request is not valid`，报错不指向真实原因。

### 请求示例

示例从环境变量 `LYROUTER_API_KEY` 读取 API Key，设置方法见[完成第一次模型调用](https://platform.lyrouter.com/ai_gateway/docs/quickstart)。

**请求示例**

cURL:
```bash
curl https://api.lyrouter.com/v1/videos \
  -H "Authorization: Bearer $LYROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "bytedance/doubao-seedance-2.0-mini",
    "prompt": "一只柯基在草地上奔跑，阳光穿过树叶",
    "size": "1280x720",
    "seconds": 5,
    "generate_audio": true
  }'
```

Python:
```python
import os
import httpx

resp = httpx.post(
    "https://api.lyrouter.com/v1/videos",
    headers={"Authorization": f"Bearer {os.environ['LYROUTER_API_KEY']}"},
    json={
        "model": "bytedance/doubao-seedance-2.0-mini",
        "prompt": "一只柯基在草地上奔跑，阳光穿过树叶",
        "size": "1280x720",
        "seconds": 5,
        "generate_audio": True,
    },
    timeout=60,
)
resp.raise_for_status()
dropped = resp.headers.get("X-Gateway-Unsupported-Parameters")
if dropped:
    print("这些参数没有生效：", dropped)
task = resp.json()
print(task["id"], task["status"])
```

Node.js:
```js
const resp = await fetch("https://api.lyrouter.com/v1/videos", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.LYROUTER_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "bytedance/doubao-seedance-2.0-mini",
    prompt: "一只柯基在草地上奔跑，阳光穿过树叶",
    size: "1280x720",
    seconds: 5,
    generate_audio: true,
  }),
});
if (!resp.ok) throw new Error(await resp.text());
const dropped = resp.headers.get("X-Gateway-Unsupported-Parameters");
if (dropped) console.log("这些参数没有生效：", dropped);
const task = await resp.json();
console.log(task.id, task.status);
```

### 响应

提交成功返回 HTTP 200 和任务对象，`status` 为 `queued`，字段含义见下文「查询任务」。提交响应里没有 `usage`，费用在任务完成时结算。下面的 400 是 `seconds` 不是正整数，403 是模型没有在本工作空间开通（或用了虚拟模型、`lyrouter/auto`），429 是服务商账户的并发任务已满。

**响应示例**

200:
```json
{
  "id": "aigwt_volcengine_videos_MXgno8VlzuzZtJl0kq83jA",
  "object": "video",
  "model": "doubao-seedance-2-0-mini-260615",
  "status": "queued",
  "progress": 0,
  "created_at": 1791950400,
  "expires_at": 1792555200,
  "seconds": 5,
  "size": "1280x720"
}
```

400:
```json
{
  "error": {
    "message": "seconds must be a positive whole number of seconds",
    "type": "invalid_request_error",
    "code": "invalid_parameter"
  }
}
```

403:
```json
{
  "error": {
    "message": "Model is not available in the model plaza",
    "type": "access_denied",
    "code": "model_not_available"
  }
}
```

429:
```json
{
  "error": {
    "message": "upstream concurrency full",
    "type": "rate_limit_error",
    "code": "upstream_concurrency_full"
  }
}
```

- 保存好返回的 `id`。查询时网络出错或超时，用同一个 ID 继续查询，不要重新提交：重新提交会生成新任务并再次计费。
- 429 `upstream_concurrency_full` 的并发限制按服务商账户共享，换一个 API Key 也一样，请稍后重新提交。

## 查询任务

`GET /v1/videos/{id}`

读取任务的当前状态。网关在后台替你向服务商轮询进度，这个接口只返回网关记录的状态，查得更频繁不会更早拿到结果。

### 路径参数

| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | string | 是 | 提交时返回的任务 ID，以 `aigwt_` 开头。服务商自己的任务 ID 查不到，返回 404 |

### 请求示例

**请求示例**

cURL:
```bash
curl https://api.lyrouter.com/v1/videos/$TASK_ID \
  -H "Authorization: Bearer $LYROUTER_API_KEY"
```

Python:
```python
import os
import httpx

task_id = "aigwt_volcengine_videos_MXgno8VlzuzZtJl0kq83jA"
resp = httpx.get(
    f"https://api.lyrouter.com/v1/videos/{task_id}",
    headers={"Authorization": f"Bearer {os.environ['LYROUTER_API_KEY']}"},
    timeout=60,
)
resp.raise_for_status()
task = resp.json()
print(task["status"], task.get("usage"), task.get("error"))
```

Node.js:
```js
const taskId = "aigwt_volcengine_videos_MXgno8VlzuzZtJl0kq83jA";
const resp = await fetch(`https://api.lyrouter.com/v1/videos/${taskId}`, {
  headers: { Authorization: `Bearer ${process.env.LYROUTER_API_KEY}` },
});
if (!resp.ok) throw new Error(await resp.text());
const task = await resp.json();
console.log(task.status, task.usage, task.error);
```

### 响应

任务存在时总是返回 HTTP 200，任务本身失败也一样，以 `status` 判断结果。任务 ID 不存在、已超过 7 天或属于其他工作空间时返回 404。

**响应示例**

200 进行中:
```json
{
  "id": "aigwt_volcengine_videos_MXgno8VlzuzZtJl0kq83jA",
  "object": "video",
  "model": "doubao-seedance-2-0-mini-260615",
  "status": "in_progress",
  "progress": 0,
  "created_at": 1791950400,
  "expires_at": 1792555200,
  "seconds": 5,
  "size": "1280x720"
}
```

200 已完成:
```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 }
}
```

200 已失败:
```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
  }
}
```

404:
```json
{
  "error": {
    "message": "video task not found",
    "type": "invalid_request_error",
    "code": "not_found"
  }
}
```

| `status` | 含义 | 你需要做的 |
| --- | --- | --- |
| `queued` | 已受理，尚未开始生成 | 继续轮询 |
| `in_progress` | 服务商正在生成 | 继续轮询 |
| `completed` | 已完成 | 调用 `/content` 下载视频 |
| `failed` | 已失败 | 读取 `error`，不收费 |

| 字段 | 说明 |
| --- | --- |
| `id` | 网关的任务 ID，格式为 `aigwt_<服务商>_<任务类型>_<随机串>`。它不是服务商的任务 ID，向服务商报障时请用 `error.provider_request_id` |
| `object` | 固定为 `video` |
| `model` | 实际调用的上游模型名，例如 `doubao-seedance-2-0-mini-260615`，与请求中的模型 ID 写法不同 |
| `status` | `queued`、`in_progress`、`completed`、`failed` 之一 |
| `progress` | 完成前为 `0`，完成后为 `100`，不表示中间进度，请以 `status` 判断 |
| `created_at` / `completed_at` | 提交时间、结束时间（Unix 秒）。`completed_at` 只在任务结束后出现 |
| `expires_at` | 任务记录的过期时间，提交后 7 天。过期后查询返回 404 |
| `seconds` / `size` | 原样回显提交时的值 |
| `error` | 仅 `failed` 时出现：`code`、`message`，以及有值时才出现的 `provider_request_id`（服务商的请求标识）和 `retryable` |
| `usage` | 任务完成后出现：`duration_seconds` 为视频秒数，`completion_tokens` 为服务商上报的输出 Token 数，Seedance 按它计费 |

失败任务的 `error.code` 多为服务商的错误码，原样返回：

| `error.code` | 含义与处理 |
| --- | --- |
| `InvalidParameter.TaskTypeConstraint` | 任务类型与参数冲突，例如视频编辑任务传了 `seconds`。对照下文「任务类型与限制」修改参数 |
| `InvalidParameter.TaskTypeMismatch` | 用 `omni_reference_task_type` 声明的类型与模型的判定不一致。按任务类型表中的关键词写提示词，或改用 `auto` |
| `OutputVideoSensitiveContentDetected` 等 | 内容审核未通过，修改提示词或素材后重新提交 |
| `upstream_error` | 服务商报告失败，但没有给出可识别的错误码 |

- `error.retryable` 为 `false` 表示原样重试不会成功，需要修改请求；为 `true` 表示可以重试；没有这个字段表示网关无法判断，请不要把缺失当成 `false`。完整的取值见[异步任务](https://platform.lyrouter.com/ai_gateway/docs/async)。
- **轮询间隔**：提交后约 10 秒开始查询，之后逐步放宽到每 30 秒一次。
- **等待上限**：提交 24 小时后仍未完成的任务不会再有结果，也不收费，可以停止轮询。这时查询结果可能仍显示 `queued` 或 `in_progress`，任务的实际状态请在控制台工作空间的 **多媒体生成** 页面按任务 ID 搜索查看（「状态」显示「已过期」）。

## 下载视频

`GET /v1/videos/{id}/content`

任务变为 `completed` 后调用，返回 302，`Location` 响应头是服务商提供的带签名的视频地址。让客户端跟随跳转即可下载：cURL 加 `-L`，Python `httpx` 设 `follow_redirects=True`，Node.js 的 `fetch` 默认跟随。

### 请求示例

**请求示例**

cURL:
```bash
# 下载到本地
curl -L https://api.lyrouter.com/v1/videos/$TASK_ID/content \
  -H "Authorization: Bearer $LYROUTER_API_KEY" \
  -o video.mp4

# 只取视频地址，不下载
curl -s -o /dev/null -w '%{redirect_url}\n' \
  https://api.lyrouter.com/v1/videos/$TASK_ID/content \
  -H "Authorization: Bearer $LYROUTER_API_KEY"
```

Python:
```python
import os
import httpx

task_id = "aigwt_volcengine_videos_MXgno8VlzuzZtJl0kq83jA"
with httpx.stream(
    "GET",
    f"https://api.lyrouter.com/v1/videos/{task_id}/content",
    headers={"Authorization": f"Bearer {os.environ['LYROUTER_API_KEY']}"},
    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)
```

Node.js:
```js
import { writeFile } from "node:fs/promises";

const taskId = "aigwt_volcengine_videos_MXgno8VlzuzZtJl0kq83jA";
const video = await fetch(`https://api.lyrouter.com/v1/videos/${taskId}/content`, {
  headers: { Authorization: `Bearer ${process.env.LYROUTER_API_KEY}` },
});
if (!video.ok) throw new Error(`download failed: ${video.status}`);
await writeFile("video.mp4", Buffer.from(await video.arrayBuffer()));
```

### 响应

**响应示例**

302:
```text
状态码：302
Location: <服务商提供的带签名的视频地址>
```

409:
```json
{
  "error": {
    "message": "video is not ready yet",
    "type": "invalid_request_error",
    "code": "not_ready"
  }
}
```

404:
```json
{
  "error": {
    "message": "video task not found",
    "type": "invalid_request_error",
    "code": "not_found"
  }
}
```

- 视频地址在任务完成时确定，之后每次调用都跳转到同一个地址。地址的有效期由服务商决定，通常为 24 小时，过期后无法通过网关重新获取，请拿到后立即下载或转存到自己的存储。
- 任务还没完成（或已失败）时返回 409。这类 4xx 也会计入 API Key 的错误率，频繁调用可能触发[熔断](https://platform.lyrouter.com/ai_gateway/docs/rate-errors)，请先查询到 `completed` 再下载。

## 删除任务

`DELETE /v1/videos/{id}`

对已结束（`completed` 或 `failed`）的任务返回 HTTP 200 和任务对象，并带 `"deleted": true`；任务记录不会因此删除，在 `expires_at` 之前仍可查询和下载。对进行中的任务调用返回 409 `cancel_not_supported`：服务商不提供取消接口，请等待任务结束。OpenAI 官方 SDK 的 `videos.delete()` 读取的就是这个 `deleted` 字段。

## Seedance 特有参数与限制

### `size` 的两种写法

| 写法 | 网关怎么处理 |
| --- | --- |
| `宽x高`（如 `1280x720`） | 取**短边**作为分辨率档（`1280x720` → `720p`）；宽高比与服务商支持的某个比例相差在 2% 以内时取该比例，否则按约分后的比例原样发出，由服务商返回 400（如 `1000x480` 报 `the parameter ratio specified in the request is not valid ...`） |
| 档位名（如 `720p`、`1080p`） | 原样送出，宽高比由服务商按提示词自适应 |

- 首帧 / 首尾帧生视频时，输出宽高比跟随首帧图片。
- Seedance 2.5 的 1080p 采用 10bit 位深 + H.265/HEVC 编码，少数播放器不兼容；`mov` 采用 H.264 + yuv444p + PCM。建议用 VLC、mpv、ffplay 或 IINA 播放。

### 任务类型与限制

Seedance 会根据输入素材和提示词**自动判断任务类型**，不同类型有不同的限制：

| 任务类型 | 怎么触发 | 限制 |
| --- | --- | --- |
| 文生视频 | 只传 `prompt` | 无特殊限制 |
| 首帧 / 首尾帧生视频 | 传 `first_frame`（+ `last_frame`） | 输出宽高比自动跟随首帧图片 |
| 参考生视频 | 传 `reference_image` / `input_reference` | 无特殊限制 |
| **视频编辑** | 传 `input_reference`，并在提示词中表达编辑意图 | **不要传 `seconds`**；输出宽高比与时长自动跟随源视频；源视频时长须为 4～30 秒 |
| **视频延长** | 传 `input_reference`，并在提示词中表达延长意图 | 输出宽高比自动跟随源视频 |

视频编辑任务的时长固定跟随源视频，而网关的 `seconds` 只接受正整数，所以编辑任务请直接不传 `seconds`；传了会被服务商拒绝（`InvalidParameter.TaskTypeConstraint`）。

使用 Seedance 2.5 时，可以用 `omni_reference_task_type` 显式声明任务类型，把服务商的异步失败提前成提交时的同步报错：

| 值 | 含义 | 提示词需包含的关键词 |
| --- | --- | --- |
| `auto`（默认） | 由模型判断 | — |
| `reference` | 参考生视频 | — |
| `edit` | 视频编辑 | 编辑视频 / 增加 / 加上 / 删除 / 去掉 / 修改 / 替换 / 改成 |
| `extend` | 视频延长 | 向前延长 / 向后延长 / 延续 / 续写 |

`edit`、`extend` 只适用于带参考素材的场景：用在文生视频（不带参考素材）上，提交时直接返回 400，错误码为 `InvalidParameter.TaskTypeConstraint`，报错信息包含 `This parameter is only supported in the multimodal reference scenario, and is not applicable to text-to-video generation or first-frame / first-last-frame generation`。

声明的类型与模型实际判定的不一致时，任务可能异步失败（`InvalidParameter.TaskTypeMismatch`）。

### 输入素材限制（Seedance 2.5）

| 素材 | 限制 |
| --- | --- |
| 图片 | jpeg、png、webp、bmp、tiff、gif、heic、heif；宽高 [300, 6000] px；宽高比 [0.4, 2.5]；单张 ≤ 30 MB；首帧 1 张 / 首尾帧 2 张 / 参考图 1～30 张 |
| 视频 | mp4、mov（H.264 或 H.265，音频 AAC / MP3 / PCM）；分辨率 480p 或 720p；时长 [2, 30] 秒（编辑任务 [4, 30] 秒）；宽高 [300, 6000] px；宽高比 [0.4, 2.5]；单个 ≤ 200 MB；帧率 [24, 60] |
| 请求体 | 总大小 ≤ 64 MB，大文件请用公网 URL，不要用 Base64 |

Seedance **不接受含真人人脸的参考图和参考视频**，会被输入审核拦截。

服务商的联网搜索、返回尾帧等能力没有对应的网关参数，目前无法通过 LYRouter 使用。

### 计费

Seedance 按服务商上报的**输出 Token** 计费（任务对象中的 `usage.completion_tokens`），分档维度是分辨率和有无视频输入：

| 模型 | 分档 |
| --- | --- |
| `bytedance/doubao-seedance-2.5` | 480p / 720p 与 1080p 两档，每档再按有无视频输入分 |
| `bytedance/doubao-seedance-2.0-fast` | 只按有无视频输入分两档 |
| `bytedance/doubao-seedance-2.0-mini` | 只按有无视频输入分两档 |

- **有视频输入时按另一档单价计费，单价更低，但总价更高**：传了 `input_reference` 的任务（参考视频、视频编辑、视频延长）计量的 Token 大约是不带参考视频时的两倍（见下文「等效秒」），单价的下降抵不过用量的增加。只传首帧 / 首尾帧的图生视频不算视频输入。
- Token 数 = 固定开销 + 每秒 Token × 等效秒，已经体现了分辨率与时长的差异（实测 480p 4 秒约 40,594 Token，8 秒约 80,770，720p 4 秒约 87,300），不再按时长分档。不带参考视频时，等效秒就是输出秒数。
- **带参考视频时，等效秒不等于输出时长**：输出不超过 6 秒时，等效秒 = 输出秒数 + 参考视频秒数；超过 6 秒时，等效秒 = 1.5 × 输出秒数 + 2，与参考视频时长无关。6 秒是拐点，例如参考视频 5 秒时，输出 5 秒按 10 秒计量，输出 10 秒按 17 秒计量。视频编辑、视频延长同样按这条规则计量（实测）。
- 价格在提交时锁定；任务变为 `completed` 时按实际上报的 Token 数结算；`failed` 的任务（包括超时）不收费。在日志中查看任务费用的方法见[异步任务](https://platform.lyrouter.com/ai_gateway/docs/async)。

## 完整调用示例

提交一个首帧生视频任务，轮询到结束后下载。把 `first_frame` 换成你自己的公网图片地址。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": "bytedance/doubao-seedance-2.0-mini",
    "prompt": "镜头缓慢推近，小猫转头看向窗外",
    "first_frame": "https://example.com/cat.jpg",
    "size": "720p",
    "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": "bytedance/doubao-seedance-2.0-mini",
        "prompt": "镜头缓慢推近，小猫转头看向窗外",
        "first_frame": "https://example.com/cat.jpg",
        "size": "720p",
        "seconds": 5,
    })
    resp.raise_for_status()
    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: "bytedance/doubao-seedance-2.0-mini",
    prompt: "镜头缓慢推近，小猫转头看向窗外",
    first_frame: "https://example.com/cat.jpg",
    size: "720p",
    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` | 原因与处理 |
| --- | --- | --- | --- |
| 400 | `seconds must be a positive whole number of seconds` | `invalid_parameter` | `seconds` 不是正整数，例如写成了 `0`、负数或小数 |
| 400 | `input_reference must be a non-empty URL string` | `invalid_parameter` | `input_reference` 为空或写成了数组。Seedance 只收一个参考视频地址 |
| 400 | `generate_audio must be a boolean` | `invalid_parameter` | JSON 中 `generate_audio`（或 `watermark`）不是布尔值 |
| 400 | `source_video is not supported for this model` | `invalid_parameter` | `source_video` 只用于 MiniMax H3 的视频升清，从请求中删掉 |
| 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 |
| 404 | `video task not found` | `not_found` | 任务 ID 不存在、已超过 7 天、属于其他工作空间，或传入的是服务商的任务 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 无效，稍后重新提交 |

- 用表单提交时 `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)。

## 常见问题

### 视频编辑任务失败并返回 `InvalidParameter.TaskTypeConstraint`，怎么办？

**答：** 视频编辑任务的时长固定跟随源视频，请从请求中删掉 `seconds` 再提交。其他任务类型的限制见上文「任务类型与限制」。

### 任务失败并返回 `InvalidParameter.TaskTypeMismatch`，怎么办？

**答：** 用 `omni_reference_task_type` 声明的任务类型与模型的判定不一致。按任务类型表中的关键词改写提示词（例如编辑任务写明「替换」「去掉」），或改用 `auto` 让模型自行判断。

### 请求 1080p 或 `mov` 被服务商拒绝，为什么？

**答：** 1080p 和 `mov` 只有 `bytedance/doubao-seedance-2.5` 支持，2.0 系列只能输出 480p、720p 的 `mp4`。换用 2.5，或把 `size` 改为 `720p` 并去掉 `output_format`。

### 下载的 1080p 视频打不开，怎么办？

**答：** Seedance 2.5 的 1080p 是 10bit H.265/HEVC 编码，部分播放器不支持。用 VLC、mpv、ffplay 或 IINA 播放，或者自行转码。

### 可以用 OpenAI 官方 SDK 调用吗？

**答：** 可以用 `client.videos.create()` 提交任务，SDK 以表单格式提交，网关会转换成服务商需要的格式。Python SDK 需要 `openai` 2.x：实测 1.58.1 没有 `client.videos`，2.48.0 可用。表单中只能传 URL 形式的 `input_reference`：Python SDK 直接写 `input_reference="https://…"` 会在本地抛出 `RuntimeError: Expected entry at input_reference to be bytes, an io.IOBase instance, PathLike or a tuple …`，请求不会发出，请改用 `extra_body={"input_reference": "https://…"}`；Node.js SDK 运行时接受字符串，但 TypeScript 类型不允许，需要绕过类型检查。`size` 的档位名（如 `720p`）同样不在 SDK 的类型声明里，TypeScript 中需要绕过类型检查；首帧、尾帧、参考图请用 HTTP 发送 JSON 请求体。查询和下载建议按本页的 HTTP 写法调用。

### 视频地址过期了，还能重新下载吗？

**答：** 不能。视频地址在任务完成时确定，再次调用 `/content` 拿到的仍是同一个地址，过期后需要重新提交任务。请在任务完成后立即下载或转存。

### 查询结果一直是 `in_progress`，怎么办？

**答：** 视频生成通常需要几分钟。提交 24 小时后仍未完成的任务不会再有结果，也不收费，可以停止轮询，并在控制台 **多媒体生成** 页面按任务 ID 查看任务状态。不要在等待期间重复提交同样的请求，每次提交都会生成新任务并单独计费。
