# 多模态 API 参考

图像、视频、音频生成接口。机器可读规格见 [`multimodal-openapi.yaml`](multimodal-openapi.yaml)。

| 项 | 值 |
|---|---|
| 基址 | `https://{gateway_host}/v1` |
| 认证 | Bearer Token |
| 请求编码 | UTF-8 |
| 文档版本 | 2026-09-15 |

---

## 目录

1. [快速开始](#1-快速开始)
2. [模型标识](#2-模型标识)
3. [请求处理模式](#3-请求处理模式)
4. [参数参考](#4-参数参考)
5. [图像接口](#5-图像接口)
6. [视频接口](#6-视频接口)
7. [音频接口](#7-音频接口)
8. [计量与计费](#8-计量与计费)
9. [错误处理](#9-错误处理)
10. [SDK 支持](#10-sdk-支持)
11. [模型目录（全量）](#11-模型目录全量)

---

## 1. 快速开始

本 API 与 OpenAI API 的请求/响应形状兼容：产物数组字段名为 `data` 且类型为数组，
`usage` 位于响应顶层，错误结构为 `{"error": {...}}`。可直接使用 OpenAI 官方 SDK，
仅需替换 `base_url`。

```python
from openai import OpenAI

client = OpenAI(
    api_key="sk-xxxxxxxx",
    base_url="https://api.lyrouter.com/v1",
)

image = client.images.generate(
    model="bytedance/doubao-seedream-5.0-lite",
    prompt="a single green leaf, flat design",
    size="1920x1920",
)
print(image.data[0].url)
```

```bash
curl https://api.lyrouter.com/v1/images/generations \
  -H "Authorization: Bearer sk-xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"model":"bytedance/doubao-seedream-5.0-lite","prompt":"a single green leaf","size":"1920x1920"}'
```

**认证**：所有请求在 `Authorization` 头中携带 API Key。认证失败返回 `401`；
Key 有效但无权访问目标模型返回 `403`。

---

## 2. 模型标识

模型标识格式为 `<供应商>/<模型名>`，例如：

| 标识 | 模态 |
|---|---|
| `bytedance/doubao-seedream-5.0-lite` | 图像生成 |
| `openai/gpt-image-2.5-flare` | 图像生成 + 编辑 |
| `openai/gpt-image-2.5-sunburst` | 图像生成 + 编辑 |
| `google/gemini-3.1-flash-image` | 图像生成 + 编辑 |
| `google/gemini-3-pro-image` | 图像生成 + 编辑 |
| `bytedance/doubao-seedance-2.0-mini` | 视频生成 |
| `minimax/minimax-h3` | 视频生成 |

可用模型列表通过 `GET /v1/models` 获取。该接口返回当前 API Key 有权访问的全部模型。

> `GET /v1/models` 返回的是**上架列表**，不等同于权限清单。个别模型可能在列表中
> 但调用时返回 `403`。以实际调用结果为准。

---

## 3. 请求处理模式

网关对请求体有两种处理方式。**模式由端点决定，调用方无需也无法选择。**
了解当前端点属于哪种模式，决定了你能传哪些参数。

| 模式 | 适用范围 | 行为 | 未知参数的结果 |
|---|---|---|---|
| **透传** | GPT-Image 系列的图像编辑、语音合成、语音转写、语音翻译，以及**大多数**图像生成模型 | 请求体原样转发给模型服务商 | 转发给服务商，由服务商决定接受或拒绝 |
| **契约转换** | 视频生成（全部模型）· 图像生成与编辑中的 **Gemini 系模型** | 请求体转换为服务商格式 | 被网关丢弃，并在响应头中列出 |

🔴 **模式是按「端点 + 模型」定的，不是只按端点。**
同一个 `/v1/images/generations`，打 `bytedance/doubao-seedream-5.0-lite` 是透传，
打 Gemini 系模型是契约转换 —— 详见 [§4.2 的例外说明](#例外gemini-系图像模型走契约转换不是透传)。
**严格模式与 `X-Gateway-Unsupported-Parameters` 响应头只在契约转换下有意义**，
所以它们在 Gemini 图像请求上同样生效。唯一的例外是 GPT-Image 系列在**生成接口**上
声明丢弃的 `response_format`（2.5 系列另有 `stream`、`stream_options`，见
[§4.2](#产物以什么形式返回)）：它们同样进响应头，严格模式下同样返回 `400`。

### 3.1 透传模式

请求体不做修改。这意味着：

- 本文档 [§4](#4-参数参考) 列出的参数可以使用；例外是上文列出的、在透传模型上被有意丢弃的少数参数（如 GPT-Image 生成接口的 `response_format`）。
- **服务商特有的参数也可以传**，网关不拦截。是否生效取决于服务商。
  可用参数请查阅对应服务商的官方文档。
- 参数不被服务商接受时，返回的是**服务商自身的错误**，例如：

```json
{
  "error": {
    "code": "InvalidParameter",
    "message": "The parameter `guidance_scale` specified in the request is not valid."
  }
}
```

- 部分模型的约束（如 `quality` 必填、最小像素数）由服务商校验，网关不预先拦截。
  这样做是为了避免网关的校验规则滞后于服务商而误拒合法请求。

### 3.2 契约转换模式

网关将请求体转换为服务商格式。**只有本文档定义的参数会被保留**，
其余参数被丢弃，并通过响应头列出：

```http
X-Gateway-Unsupported-Parameters: guidance_scale,duration
```

被丢弃的参数不会导致请求失败，但服务商将使用其默认值。

如需在出现不支持的参数时直接失败，在请求中加入：

```http
X-Gateway-Strict-Parameters: true
```

此时返回 `400`，并在消息中列出参数名，请求不会发送给服务商，**不产生费用**：

```json
{
  "error": { "message": "unsupported parameters for this model: guidance_scale" }
}
```

> **建议**：生产环境中检查 `X-Gateway-Unsupported-Parameters` 响应头，
> 或启用严格模式。未启用时，被丢弃的参数不产生任何错误信号。

### 3.3 网关扩展头汇总

| 头 | 方向 | 取值 | 适用模式 |
|---|---|---|---|
| `X-Gateway-Strict-Parameters` | 请求 | `true` | 契约转换；以及 GPT-Image 生成接口上网关声明丢弃的参数 |
| `X-Gateway-Unsupported-Parameters` | 响应 | 逗号分隔的参数名 | 同上 |

---

## 4. 参数参考

### 4.1 通用参数

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `model` | string | 是 | 模型标识，格式 `<供应商>/<模型名>` |

### 4.2 图像参数

适用于 `/v1/images/generations`。**处理模式逐模型不同**（见 [§3](#3-请求处理模式)）：
服务商原生协议与 OpenAI 形状一致的走透传，未列出的参数一并转发；
需要契约转换的（如 Gemini 系图像模型）只翻译下表中的参数，其余丢弃并在
响应头 `X-Gateway-Unsupported-Parameters` 中列出。

| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
| `prompt` | string | 是 | — | 生成提示词 |
| `n` | integer | 否 | `1` | 生成数量。GPT-Image 系列**最大 10**；Gemini 系丢弃；Seedream 目前不支持用 `n` 出多张：传 `n=2` 也只出 1 张，不报错，也不进 `X-Gateway-Unsupported-Parameters` |
| `size` | string | 否 | 模型默认 | 分辨率，格式 `<宽>x<高>`；Seedream 也接受 `2K` 等档位名，Gemini 接受 `1K`/`2K`/`4K`。**存在模型相关的最小像素限制**；GPT-Image 系列只接受 `宽x高`（传档位名如 `4K` 返回 `Invalid size '4K'. Expected WIDTHxHEIGHT, for example '1824x1024'.`），且须满足四条硬约束：宽高都能被 16 整除、最长边 ≤ 3840、总像素约 100 万～830 万（报错原文与正反例见 [§11.1](#111-图像模型) GPT-Image 小节）；Gemini 会把它翻译成比例 + 档位 |
| `quality` | string | 否 | 模型默认 | 生成质量，**取值域逐模型不同**：`gpt-image-2` 为 `low`/`medium`/`high`/`auto`，**必填**（不传会间歇性 `400`；`auto` 实测等同 `low`）；GPT-Image-2.5 为 `low`/`medium`/`high`/`xhigh`/`max`/`auto`，默认 `low`（`auto` 实测等同 `low`），`xhigh`/`max` 由上游账号能力决定、可能被拒（收到 `Invalid value: 'xhigh'` 时改用 `high`）；Gemini 系丢弃 |
| `background` | string | 否 | 模型默认 | `transparent`/`opaque`/`auto`。仅 GPT-image 系列 |
| `output_format` | string | 否 | 模型默认 | **`gpt-image-2` 只接受 `png`/`jpeg`，不支持 `webp`**；GPT-Image-2.5 系列另支持 `webp`；Seedream 为 `png`/`jpeg`。实测不传时 `gpt-image-2` 返回 `png`，Seedream 返回 `jpeg` |
| `moderation` | string | 否 | 模型默认 | `auto`/`low`。仅 GPT-image 系列 |
| `response_format` | string | 否 | 服务商默认 | `url` 或 `b64_json`。**并非所有模型都支持，见下表** |
| `user` | string | 否 | — | 调用方自定义标识。Gemini 系丢弃 |

#### 产物以什么形式返回

**这不是网关的选择，取决于服务商协议。** 逐个实测（2026-09-17）：

| 模型 | 产物形式 | 传 `response_format` 会怎样 |
|---|---|---|
| `bytedance/doubao-seedream-5.0-lite` | **URL**（`data[].url`） | 转发给服务商 |
| `openai/gpt-image-2` | base64（`data[].b64_json`） | 被网关丢弃，响应头列出 |
| `openai/gpt-image-2.5-flare`<br>`openai/gpt-image-2.5-sunburst` | base64（`data[].b64_json`） | 同上 |
| `google/gemini-3-pro-image`<br>`google/gemini-3.1-flash-image` | **只能 base64** | 被丢弃，响应头列出 |

> 🔴 **`response_format` 对整个 GPT-image 系列都是无效参数，网关在生成接口上会替你丢掉。**
> 这个参数只对 DALL·E 2/3 有效，**GPT-image 系列移除了它**，固定返回 base64。
> 直接转发给上游会拿到：
> ```json
> {"error":{"message":"Unknown parameter: 'response_format'.",
>           "type":"invalid_request_error","code":"unknown_parameter"}}
> ```
> 所以网关在 spec 里声明丢弃它（`drop_params`）：它永远改变不了结果，转发它唯一的
> 效果就是制造 400。被丢弃的事实会出现在 `X-Gateway-Unsupported-Parameters` 响应头里，
> 严格模式下返回 400 —— 也就是说你**察觉得到**，这正是它比"随机 400"好的地方。
>
> ⚠️ 本条更正了本文档 2026-09-17 版的说法（当时写的是「上游报错 400」）。那是
> `drop_params` 加入之前的行为；现在三个 GPT-image 模型在生成接口上一律先丢弃再转发。
>
> ⚠️ **编辑接口（`/v1/images/edits`）不做这种丢弃**：spec 只在生成接口上声明了
> `drop_params`，编辑请求里的 `response_format` 会原样转发给上游；传 `stream: true` 时网关目前还会
> 自行补上 `stream_options`，上游返回 400 `Unknown parameter: 'stream_options'.` ——
> 编辑接口上不要传这几个参数。

> 🔴 **Gemini 系图像模型无法返回 URL。** 它的 `:generateContent` 只有 `inlineData`
> （内联 base64）一种产物形式，官方文档中不存在 URL/URI 返回方式。传
> `response_format` 不会报错（网关在翻译时丢弃它），但也不会生效 —— 响应头
> `X-Gateway-Unsupported-Parameters` 会列出它；启用严格模式时返回 `400`。

> **所以想要 URL 形式的产物，当前只有 Seedream 5.0（pro / lite）。** GPT-Image 与 Gemini 系的
> 上游协议都只给 base64，网关无法把它变成 URL —— 那需要网关自己落盘并托管产物，
> 是另一套设施（视频接口那种 `/v1/videos/{id}/content` 的模式），图像接口目前没有。

> **这会影响可观测性，而原因不是体积上限。** 一张 Gemini 生成的图内联后约 **2.5 MB**，
> 而**内联的 base64 媒体在落库前一律被替换成摘要** ——
> `[base64 <字节数> #<crc32>]`，这是刻意设计（否则几 MB 的图会把日志库撑爆）。
>
> 所以**图永远不会出现在日志里，调大任何上限都不会改变这一点**，
> 控制台的日志详情页对这类请求就是空的。**这不代表请求失败：图在响应体里，客户端收得到。**
>
> 那个摘要保留了**字节数与 CRC32**，所以"出没出图、多大、是不是同一张"
> 这三个问题仍然可以从日志回答。返回 URL 的模型响应只有几百字节，URL 本身会留在日志里。

> **图像尺寸有下限，且下限因模型而异。** 例如 `bytedance/doubao-seedream-5.0-lite`
> 要求不低于 **3,686,400 像素**（宽 × 高），因此业界常见的 `1024x1024`
> （1,048,576 像素）会被服务商拒绝：
>
> ```json
> {
>   "error": {
>     "code": "InvalidParameter",
>     "message": "The parameter `size` specified in the request is not valid: image size must be at least 3686400 pixels"
>   }
> }
> ```
>
> 该模型下 `1920x1920` 恰好等于这一下限，是可用的最小正方形尺寸。
> 本接口为透传模式，尺寸由服务商校验，网关不代为改写。

#### 例外：Gemini 系图像模型走契约转换，不是透传

上面说的"透传"对**多数**图像模型成立。少数服务商的图像端点契约与 OpenAI 差异过大，
网关会按该模型的契约转换请求 —— 目前 **Gemini 系图像模型**属于这一类。
对它们，`size` 的行为与上面不同：

| `size` 的写法 | 网关怎么处理 | 效果 |
|---|---|---|
| **宽 × 高**（`1376x768`） | 换算成宽高比，匹配最接近的比例（**容差 2%**），再按长边**向上**取分辨率档 | **同时控制比例与分辨率** |
| **档位名**（`1K` / `2K` / `4K`） | 原样传给服务商 | 只控制分辨率，比例由服务商默认 |

🔴 **不要使用 `512` 档**：它在生产环境被服务商拒绝（两个模型都是）。

合法的宽高比档位：
`1:1` `2:3` `3:2` `3:4` `4:3` `4:5` `5:4` `9:16` `16:9` `21:9` `1:4` `4:1` `1:8` `8:1`。

**比例超出 2% 容差时（例如 `1000x700`，比 3:2 差 4.8%）：**

| 模式 | 行为 |
|---|---|
| 普通 | **不替你挑最近的档** —— 整个 `size` 都不下发（宽高比和分辨率档一起丢弃），由服务商用默认比例和分辨率出图；`size` 被计入丢弃参数，在 `X-Gateway-Unsupported-Parameters` 响应头中列出。**请求返回 `200`、有产物、照常计费**，但比例和分辨率都不是你要的 |
| [严格](#32-契约转换模式) | 返回 `400`，点名 `size` |

🔴 **普通模式下这是一次"成功但不是你要的"** —— 唯一的信号是响应头 `X-Gateway-Unsupported-Parameters: size`。
生产环境建议带 `X-Gateway-Strict-Parameters: true`，把它变成 `400`。
常见的踩法是照抄服务商的出图尺寸：要 16:9 + 4K 请写 `4096x2304`，**不要写 `5632x3072`**
（比例 1.833，偏离 16:9 超过 2%，整个 `size` 会被丢弃）。

⚠️ **`size` 不接受比例字符串。** 写 `16:9` 不会被理解成宽高比 ——
它既不是"宽x高"也不是已知档位名，会被原样当作分辨率档位传给服务商，
而服务商不认识它。**要 16:9 请写一个该比例的宽高**（如 `1376x768`）。

🔴 **宽高写法按长边向上取分辨率档，封顶 4K。** 两个模型同一档位的输出 token 不同（2026-10-14 生产实测）：

| 长边 | 档位 | `gemini-3-pro-image` 输出 token | `gemini-3.1-flash-image` 输出 token |
|---|---|---|---|
| ≤ 1376 | `1K` | 1120 | 1120 |
| ≤ 2816 | `2K` | 1120（2K 档不生效，与 1K 相同） | 1680 |
| 更大（封顶） | `4K` | 2000 | 2520 |

长边不超过 704 时网关会映射到 `512` 档（`apisix/plugins/ai-drivers/request_shape.lua:203`），因此同样可能被拒绝，请让长边大于 704。

出图尺寸由服务商按比例档和分辨率档决定，**不等于请求的宽高**：例如 `gemini-3-pro-image`
请求 `4096x2304` 实际出图 `5504x3072`。请以返回图片的实际宽高为准。

例如 `1024x1024` → 1:1 + `1K`，`2048x2048` → 1:1 + `2K`，`3840x2160` → 16:9 + `4K`。
方向是刻意的：给小了你拿不到要的分辨率、请求照样返回 `200`；给大了多付一些 token 费，
但图不比所求差。长边略超过某一档就会落到下一档（`1500x1500` 取 `2K` 档），
想精确控制分辨率就直接写档位名。

**这些模型上只有 `prompt`、`size`、`image` 会被翻译（`model` 用于选路），其余参数一律丢弃**，
包括 `n`、`quality`、`response_format`、`user`：Gemini 的候选数参数属于文本生成，
在图像端点上是否生效没有证据；`quality` 在其契约里没有对应概念；产物只能是 base64。
丢弃会通过 `X-Gateway-Unsupported-Parameters` 响应头明示，
启用[严格模式](#32-契约转换模式)时直接返回 `400`。

### 4.3 视频参数

适用于 `/v1/videos`。**契约转换模式**，下表是全部受支持的参数，
其余参数将被丢弃。

| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
| `prompt` | string | 是 | — | 生成提示词 |
| `seconds` | number | 见说明 | Seedance：服务商默认 | 视频时长（秒），正整数。**MiniMax 必填**：服务商要求提供，网关不补默认值 |
| `size` | string | 见说明 | Seedance：服务商默认 | 分辨率。见下方《分辨率的两种写法》。**MiniMax 必填**：服务商要求提供，网关不补默认值 |
| `input_reference` | string \| string[] | 否 | — | **参考视频**的 URL（视频生视频） |
| `first_frame` | string | 否 | — | 首帧图片 URL（图生视频） |
| `last_frame` | string | 否 | — | 尾帧图片 URL |
| `reference_image` | string \| string[] | 否 | — | 参考图片，单个地址或地址数组均可 |
| `reference_audio` | string \| string[] | 否 | — | 参考音频（**只有 MiniMax 收**；火山 Seedance 目前不转发，传了会被直接忽略，见下方矩阵） |
| `generate_audio` | boolean | 否 | Seedance：开启 | 是否生成配音（**只有火山 Seedance 收**；不传时生成有声视频） |
| `watermark` | boolean | 否 | 模型默认 | 是否加水印 |
| `output_format` | string | 否 | 模型默认 | 容器格式，如 `mov`（**只有火山 Seedance 2.5 收**） |
| `omni_reference_task_type` | string | 否 | — | 全模态参考任务类型（**为火山 Seedance 2.5 设计**；2.0 系列传入目前不会报错，任务照常生成并计费） |
| `source_video` | string | 否 | — | **只用于 `/v1/videos/regenerations`**（见 [§6.8](#68-再生成与上下文增强)），在 `/v1/videos` 上传会返回 `400` |

> **本接口不支持 `n`。** 每次请求生成一个视频。传入 `n` 会被丢弃；
> 启用严格模式时返回 `400`。需要多个视频请发起多次请求。

#### 逐模型的参数支持矩阵

**不支持的参数会被显式拒绝（`400`），不会被静默丢弃** —— 这是刻意的：
悄悄丢掉一个参数，你拿到的是按默认值生成的结果，而账单一模一样。
目前唯一的例外是火山 Seedance 上的 `reference_audio`：既不报错，也不进
`X-Gateway-Unsupported-Parameters`，参考音频被直接忽略。需要参考音频请用 MiniMax H3。

| 参数 | 火山 Seedance | MiniMax H3（生成） | MiniMax H3（再生成） |
|---|---|---|---|
| `prompt` / `size` | ✅ | ✅ | ✅ |
| `seconds` | ✅ | ✅ | ❌ **400**：时长由源视频决定 |
| `input_reference`（参考视频） | ✅ 单个 | ✅ 数组 | ✅ 数组 |
| `first_frame` / `last_frame` | ✅ | ✅ | ✅ |
| `reference_image` | ✅ | ✅ | ✅ |
| `reference_audio` | ⚠️ 不转发（不报错，被直接忽略） | ✅ | ✅ |
| `generate_audio` | ✅ | ❌ **400** | ❌ **400** |
| `output_format` | ✅（2.5） | ❌ **400** | ❌ **400** |
| `omni_reference_task_type` | ✅（为 2.5 设计；2.0 系列目前也不报错） | ❌ **400** | ❌ **400** |
| `watermark` | ✅ | ✅ | ✅ |
| `source_video` | ❌ 400 | ❌ **400** | ✅ **必填** |

⚠️ MiniMax 对参考输入还有两条**服务商侧**的硬约束（由服务商拒绝，网关不预先校验）：
每边 256–5760 像素、宽高比 0.4–2.5。违反时返回服务商的 `code 2013` 并点名
`content[N].image_url`，**失败终态不计费**。

⚠️ 图生与多模态参考**互斥**：出现 `reference_*` 就不能再有 `first_frame` / `last_frame`。
这一条同样由服务商裁定。

#### 参考图片是**计费项**

MiniMax H3 对参考图片按张计费：**前 5 张免费，超出部分按张计价**（单价见控制台）。
终态的 `usage` 里不会单列张数，但它确实进了账单
（响应里能看到的是总额；逐项可在控制台的用量明细里查）。传 6 张时第 6 张开始计费。

> **时长参数名为 `seconds`。** `duration` 不属于本契约，将被丢弃，
> 服务商使用其默认时长。这会导致生成的视频长度与预期不符，而请求返回 `200`。

#### 分辨率的两种写法

`size` 接受两种写法，任选其一：

| 写法 | 示例 | 说明 |
|---|---|---|
| 宽 × 高 | `1366x768` | 短边必须**精确**落在该模型的档位上 |
| 档位名 | `768P`、`2K`、`1080p` | 原样转发给服务商，由服务商裁定 |

**视频模型的分辨率是离散档位，不是任意尺寸。** 例如 MiniMax-H3 只接受
`480P` / `768P` / `2K` —— 短边 720 不在其中，因此 `1280x720` 会返回 `400`：

```json
{
  "error": {
    "message": "this model takes discrete resolution tiers, not arbitrary sizes: 1280x720 (short side 720). Use a size whose short side is 480 or 768, or pass the tier name directly as size (e.g. \"768P\", \"2K\")"
  }
}
```

档位与短边的对应关系逐模型不同，且并非每个档位都能用宽高表达
（`2K` 没有固定短边）。**不确定时直接写档位名。**

用宽高写法时，宽高比同样必须落在模型支持的比例内（容差 2%），
否则返回 `400`：MiniMax 由网关返回并指明是尺寸引起的；火山 Seedance 由服务商返回
`the parameter ratio specified in the request is not valid ...`（如 `1000x480`），不指明是尺寸引起的。

### 4.4 音频参数

**透传模式**，未列出的参数一并转发给服务商。

**语音合成**（`/v1/audio/speech`）

| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
| `input` | string | 是 | — | 待合成文本 |
| `voice` | string | 是 | — | 音色标识 |
| `response_format` | string | 否 | `mp3` | `mp3`、`opus`、`aac`、`flac`、`wav`、`pcm` |
| `speed` | number | 否 | `1.0` | 语速 |

**语音转写 / 翻译**（`/v1/audio/transcriptions`、`/v1/audio/translations`）

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `file` | file | 是 | 音频文件 |
| `language` | string | 否 | 源语言，ISO-639-1。仅转写接口 |
| `response_format` | string | 否 | `json`、`text`、`srt`、`verbose_json`、`vtt` |

---

## 5. 图像接口

### 5.1 创建图像

```http
POST /v1/images/generations
Content-Type: application/json
```

请求参数见 [§4.2](#42-图像参数)。

**响应字段**

| 字段 | 类型 | 说明 |
|---|---|---|
| `created` | integer | Unix 时间戳（秒）。Gemini 系不返回这个字段 |
| `data` | array | 产物数组。即使 `n=1` 也为数组 |
| `data[].url` | string | 产物地址。**只有服务商原生返回 URL 的模型才有**，见 [§4.2 产物以什么形式返回](#产物以什么形式返回) |
| `data[].b64_json` | string | Base64 编码内容。其余模型返回这个 |
| `data[].size` | string | 实际生成的分辨率 |
| `usage` | object | 计量信息，位于响应顶层。字段随模型而异，见 [§8](#8-计量与计费) |

**响应示例**

```json
{
  "created": 1789454321,
  "data": [
    {
      "url": "https://ark-acg-cn-beijing.tos-cn-beijing.volces.com/...",
      "size": "1920x1920"
    }
  ],
  "usage": {
    "generated_images": 1,
    "output_tokens": 14400,
    "total_tokens": 14400
  }
}
```

### 5.2 编辑图像

```http
POST /v1/images/edits
```

**接受两种请求体。** 选哪一种取决于模型（见下面的兼容性一节）。

#### multipart/form-data

官方 OpenAI SDK 发的形态。GPT-Image 系列（透传）上网关**原样转发字节**，仅定位并改写
`model` 字段，不重新编码文件内容；Gemini 系会解析 multipart、把文件段转成 base64，
再按该模型的契约翻译（见下面的兼容性一节）。

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `model` | string | 是 | 模型标识 |
| `image` | file | 是 | 待编辑图像 |
| `prompt` | string | 是 | 编辑指令 |
| `mask` | file | 否 | 遮罩图像。Gemini 系丢弃 |
| `quality` | string | **`gpt-image-2` 必填** | 取值域逐模型不同，同 [§4.2](#42-图像参数)：`gpt-image-2` 为 `low` / `medium` / `high` / `auto`，漏传会间歇性 400，`auto` 实测等同 `low`；2.5 系列为 `low` / `medium` / `high` / `xhigh` / `max` / `auto`，默认 `low`；Gemini 系丢弃 |
| `n` | integer | 否 | 生成数量。GPT-Image 系列最大 10；Gemini 系丢弃 |
| `size` | string | 否 | 输出分辨率，规则同 [§4.2](#42-图像参数)（GPT-Image 的四条硬约束、Gemini 的比例 + 档位） |

#### application/json

本网关在 OpenAI 契约之上的**扩展**（官方的 `/images/edits` 只收 multipart）。
原有 multipart 调用一个字节都没变。

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `model` | string | 是 | 模型标识 |
| `prompt` | string | 是 | 编辑指令。空串会被拒 |
| `image` | string \| string[] | 是 | 一张或多张输入图。data URI；Gemini 系也接受裸 base64 |
| 其余 | | 否 | 同 [§4.2 图像参数](#42-图像参数)。但 GPT-Image 系列在编辑接口上**不丢弃** `response_format`、`stream`，会原样转发给上游，不要传 |

```json
{
  "model": "google/gemini-3.1-flash-image",
  "prompt": "change the background to solid blue",
  "image": "data:image/png;base64,iVBORw0KGgo..."
}
```

**图片格式怎么确定（仅 Gemini 系）**：优先取 data URI 里声明的 MIME；传裸 base64 时按
PNG / JPEG / WEBP / GIF 的字节特征识别。两者都不成立时返回 `400`，消息里说明第几张图、
并要求用 data URI 显式声明 —— 网关**不会**替你挑一个默认格式。猜错的表现是上游回一句
"invalid image"，那会把排查引向客户的图片文件，离真因很远。
GPT-Image 系列的 JSON 请求体原样转发给上游，网关不解析图片，格式由上游校验。

响应结构同 [§5.1](#51-创建图像)。

#### 模型兼容性

判定取决于该模型的 `api_types`（见 [§2](#2-模型标识) 的模型列表）：

- 含 `images_edits` → 支持，正常转发；
- 含同族的别的操作（如 `images_generations`）但不含 `images_edits`
  → 网关**直接拒绝** `400`，`error.code = unsupported_model_operation`，
  不消耗额度、不产生上游调用；
- 只有粗粒度的 `images` → 网关不预判，请求转给服务商由它判定。

**各模型接受哪种请求体**（2026-10-09 生产实测）：

| 模型 | 请求处理 | multipart | JSON |
|---|---|---|---|
| `openai/gpt-image-2` | 透传 | ✅ | ✅ |
| `openai/gpt-image-2.5-flare` | 透传 | ✅ | ✅ |
| `openai/gpt-image-2.5-sunburst` | 透传 | ✅ | ✅ |
| `google/gemini-3.1-flash-image` | **契约转换** | ✅ | ✅ |
| `google/gemini-3-pro-image` | **契约转换** | ✅ | ✅ |

三个 GPT-Image 模型在网关侧走的是**逐字节相同的透传路径**（spec 里都没有声明
`body_shape`，`/images/edits` 不做任何形状翻译），唯一差别是发给上游的模型 ID。
spec 在编辑接口上也没有声明 `drop_params`，所以生成接口上会被丢弃的 `response_format`、
`stream` 在这里不会被丢弃（传 `stream: true` 时网关目前还会补上 `stream_options`，上游返回
400 `Unknown parameter: 'stream_options'.`）—— 编辑接口上不要传这几个参数。

> 🔴 **两种形态在网关侧开销相同，耗时全在上游，而上游的波动极大。**
> 2026-10-09 对 `openai/gpt-image-2.5-sunburst` 用同一张 1024×1024 输入图、
> 同一句 prompt、同样 `quality=low` 各打一次，网关记录的拆分：
>
> | 形态 | 结果 | 总耗时 | 其中上游 | 其中网关 |
> |---|---|---|---|---|
> | `multipart/form-data` | 200 | 252.8s | 252.6s | **0.13s** |
> | `application/json` | 200 | 37.3s | 37.2s | **0.12s** |
>
> **不要把这次的 252s / 37s 读成「multipart 慢 7 倍」** —— 网关侧两者只差 10 毫秒，
> 而这个模型族自身的波动就远大于此。生产历史（仅成功调用）：
>
> | 模型 / 接口 | 次数 | 最快 | 中位 | 最慢 |
> |---|---|---|---|---|
> | `gpt-image-2.5-flare` 编辑（JSON） | 2 | 19.7s | — | **167.0s** |
> | `gpt-image-2.5-sunburst` 编辑（JSON） | 2 | 37.3s | — | **100.6s** |
> | `gpt-image-2` 编辑（multipart） | 5 | 19.2s | 27.9s | 57.9s |
> | `gpt-image-2.5-sunburst` 生成 | 7 | 24.0s | 53.1s | **281.7s** |
>
> **同一形态内部就已经有 8 倍以上的差距**，所以单次配对测不出形态差异。
> 对接时的实际结论只有一条：**客户端超时按最坏情况设，不低于 300 秒**，
> 两种形态都一样。

> ✅ **Gemini 系模型两种形态都收（2026-10-09 起）。** 它的上游是原生 `generateContent`，
> 只认 JSON（图片要落成 `inlineData`）。收到 multipart 时，网关先把请求体归一成字段表、
> 文件段转成 base64，再按该模型的契约翻译过去 —— 所以官方 OpenAI SDK 的 `images.edit()`
> （无条件走 multipart）现在也能驱动 Gemini。归一路径有 **30 MB** 的内联上限，超限返回 400。
>
> 实测依据：`gemini-3-pro-image` 走 multipart 生产返回 200（上游 200）；
> `gemini-3.1-flash-image` 走 multipart 返回 200，`usage.input_tokens=1135`
> （纯生成只有个位数），证明输入图确实到达了上游。
>
> ⚠️ 本节两次推翻过早前的结论：先是「这两个模型不支持编辑」（写于它们接入编辑能力之前），
> 后是「它们只能用 JSON」（写于网关支持 multipart 归一之前）。两条都已作废。

**编辑接口的输出尺寸**：GPT-Image 在不传 `size` 时，输出尺寸既不一定是生成接口默认的
`1024x1024`，也不一定跟随输入图（实测 `gpt-image-2` 输入 1024×1024 得到 `1254x1254`；
`gpt-image-2.5-flare` 输入 1536×1024 得到 `1024x1024`；2.5 系列传一张 8×8 的图得到 `1254x1254`）。
需要确定的输出尺寸时请显式传 `size`。

**编辑比生成多一项费用**：输入图片。GPT-Image-2.5 记在 `input_image`，Gemini 并入
`input`。同样的 `quality` 与 `size` 下，编辑与生成的输出 token 相同。见 [§8](#8-计量与计费)。

---

## 6. 视频接口

视频生成为异步接口。流程：创建任务 → 轮询状态 → 获取产物。

**一个模型可能提供多个接口。** 例如 `minimax/minimax-h3` 有三个：

| 接口 | 路径 | 产物 | 计量 |
|---|---|---|---|
| 生成 | `POST /v1/videos` | 视频 | 秒（按分辨率档位） |
| 再生成（768P → 2K） | `POST /v1/videos/regenerations` | 视频 | 秒（另一个费率） |
| 上下文增强 | `POST /v1/videos/context` | **文本**（增强后的提示词） | **token**（输入/输出两档） |

三者共用**同一个模型名、同一套任务 id 空间、同一个查询接口** ——
拿到的句柄一律用 `GET /v1/videos/{id}` 查，不必关心它是哪个接口发起的。
详见 [§6.8](#68-再生成与上下文增强)。

### 6.1 创建任务

> 下例中的 `1280x720` 对 `bytedance/doubao-seedance-2.0-mini` 有效。
> **分辨率档位逐模型不同，同一个尺寸换个模型可能被拒** ——
> 换模型时请先看 [§4.3 分辨率的两种写法](#分辨率的两种写法)。

```http
POST /v1/videos
Content-Type: application/json          # 或 multipart/form-data
```

请求参数见 [§4.3](#43-视频参数)。响应为[视频任务对象](#64-视频任务对象)，
创建成功时 `status` 恒为 `queued`。

> **两种 `Content-Type` 都收。** 官方 OpenAI Node SDK v7 的
> `videos.create()` **无条件**发 `multipart/form-data`（哪怕一个文件都不传），
> 所以这条对用官方 SDK 的人是默认路径。
>
> 非契约字段在两种格式下都会被丢弃、都由
> `X-Gateway-Unsupported-Parameters` 点名、严格模式下都返回 `400`。
>
> ⚠️ 但 multipart 下网关只读取 `model`、`prompt`、`seconds`、`size`、`input_reference`、
> `watermark`、`generate_audio`、`output_format`、`omni_reference_task_type` 这几个字段的值。
>
> - `input_reference` 只能填可公开访问的 `https://` 地址。填了非 URL 的文本返回 `400`
>   （纯文本 `input_reference must be a URL; …`）；**上传文件目前不会报错，文件被忽略**，
>   任务按没有参考视频生成并照常计费。
> - `first_frame`、`last_frame`、`reference_image`、`reference_audio` 在表单里目前会被换成
>   占位值转发给服务商：火山 Seedance 带 `first_frame` 时服务商返回 400
>   `The parameter content[1].image_url specified in the request is not valid`；MiniMax 带
>   `reference_audio` 时提交返回 200，任务随后失败（`audio url must be http(s):// or data:...;base64`）。
>   需要它们时请用 JSON 提交。
> - 网关生成的纯文本 `400`（如上面的 `input_reference`、表单布尔值不是 `true`/`false`），
>   响应头目前仍是 `Content-Type: application/json`。
>
> ⚠️ **2026-09-17 之前不是这样**：那时 multipart 下的非契约字段**被丢弃但不点名**，
> 严格模式也拦不住。**如果你为此写过绕行逻辑（改用 JSON、或自行校验字段），现在可以去掉。**

**请求示例**

```bash
curl https://api.lyrouter.com/v1/videos \
  -H "Authorization: Bearer sk-xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "bytedance/doubao-seedance-2.0-mini",
    "prompt": "a paper boat drifting on still water, top view",
    "seconds": 5,
    "size": "1280x720"
  }'
```

**响应示例**

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

### 6.2 查询任务状态

```http
GET /v1/videos/{video_id}
```

响应为[视频任务对象](#64-视频任务对象)。

建议提交后约 10 秒开始查询，之后逐步放宽到每 30 秒一次。网关按固定周期轮询服务商，
提高客户端轮询频率不会更早获得结果。

**终态响应示例**

```json
{
  "id": "aigwt_volcengine_videos_MXgno8VlzuzZtJl0kq83jA",
  "object": "video",
  "model": "doubao-seedance-2-0-mini-260615",
  "status": "completed",
  "progress": 100,
  "created_at": 1789454983,
  "completed_at": 1789455121,
  "expires_at": 1790059783,
  "seconds": 5,
  "size": "1280x720",
  "usage": { "duration_seconds": 5, "completion_tokens": 108900 }
}
```

### 6.3 获取产物

```http
GET /v1/videos/{video_id}/content
```

返回 `302`，产物地址位于 `Location` 响应头。**视频任务对象中不含产物地址字段**，
产物只能通过本接口获取。

| 状态码 | 说明 |
|---|---|
| `302` | 重定向至产物地址 |
| `409` | 任务尚未完成（`code: not_ready`），应继续轮询 |
| `404` | 任务不存在，或不属于当前 API Key 所在工作区 |

采用 `302` 而非 `301`，因产物地址带签名且会过期，不应被客户端或中间代理永久缓存。

```bash
# 直接下载
curl -L https://api.lyrouter.com/v1/videos/{video_id}/content \
  -H "Authorization: Bearer sk-xxxxxxxx" -o output.mp4

# 仅获取地址
curl -i https://api.lyrouter.com/v1/videos/{video_id}/content \
  -H "Authorization: Bearer sk-xxxxxxxx" | grep -i '^location:'
```

### 6.4 视频任务对象

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string | 网关任务标识。非服务商的任务 ID |
| `object` | string | 固定值 `video` |
| `model` | string | 实际使用的模型名 |
| `status` | string | 任务状态，见 [§6.5](#65-任务状态) |
| `progress` | integer | 进度百分比，0–100 |
| `created_at` | integer | 创建时间，Unix 时间戳（秒） |
| `completed_at` | integer | 完成时间。仅终态存在 |
| `expires_at` | integer | 任务记录的过期时间，提交后 7 天。创建响应里就有；过期后查询与取产物均返回 `404` |
| `seconds` | number | 回显请求提交的时长 |
| `size` | string | 回显请求提交的分辨率 |
| `output_text` | string | **文本产物**。只有上下文增强任务有它（见 [§6.8](#68-再生成与上下文增强)）；视频任务不带这个键 |
| `error` | object | 错误信息。仅 `status=failed` 时存在，字段见 [§9.3](#93-视频任务错误码) |
| `usage` | object | 计量信息。仅 `status=completed` 时存在：视频任务有 `duration_seconds`（视频秒数），按 token 计费的 Seedance 另有 `completion_tokens`；上下文增强任务只有 `completion_tokens` |

### 6.5 任务状态

| `status` | 含义 | 客户端动作 |
|---|---|---|
| `queued` | 已受理，尚未开始 | 继续轮询 |
| `in_progress` | 生成中 | 继续轮询 |
| `completed` | 已完成 | 调用 [§6.3](#63-获取产物) 获取产物 |
| `failed` | 已失败 | 读取 `error.code` |

失败时 `error.code` 的取值见 [§9.3](#93-视频任务错误码)。

### 6.6 删除任务

```http
DELETE /v1/videos/{video_id}
```

对**仍在进行中**的任务调用会返回 409，`error.code` 为 `cancel_not_supported`：目前接入的服务商都不提供取消接口，任务会在上游继续运行并计费，网关因此不做"假取消"。请等待任务进入终态。
对**已结束**（`completed` 或 `failed`）的任务调用返回 `200` 和任务对象，并带 `"deleted": true`；
任务记录不会因此删除，在 `expires_at` 之前仍可查询和取产物。

### 6.7 完整示例

```python
import time
import requests

BASE = "https://api.lyrouter.com/v1"
HEADERS = {"Authorization": "Bearer sk-xxxxxxxx"}


def generate_video(prompt: str, seconds: int = 5, timeout: int = 900) -> tuple[str, dict]:
    """提交视频生成任务并等待完成，返回 (产物地址, 计量信息)。"""
    resp = requests.post(f"{BASE}/videos", headers=HEADERS, json={
        "model": "bytedance/doubao-seedance-2.0-mini",
        "prompt": prompt,
        "seconds": seconds,
    })
    resp.raise_for_status()

    dropped = resp.headers.get("X-Gateway-Unsupported-Parameters")
    if dropped:
        raise ValueError(f"以下参数不受支持，已被丢弃: {dropped}")

    task_id = resp.json().get("id")
    if not task_id:  # 没拿到任务 ID 就停下，不要去轮询一个不存在的任务
        raise RuntimeError(f"没有拿到任务 ID: {resp.text}")
    delay, deadline = 10, time.time() + timeout

    while time.time() < deadline:
        time.sleep(delay)
        delay = min(delay * 1.5, 30)

        task = requests.get(f"{BASE}/videos/{task_id}", headers=HEADERS).json()
        if task["status"] == "completed":
            content = requests.get(
                f"{BASE}/videos/{task_id}/content",
                headers=HEADERS, allow_redirects=False,
            )
            return content.headers["Location"], task.get("usage", {})
        if task["status"] == "failed":
            raise RuntimeError(task.get("error", {}).get("code", "unknown"))

    raise TimeoutError(f"任务 {task_id} 在 {timeout} 秒内未完成")
```

### 6.8 再生成与上下文增强

这两个接口与 [§6.1](#61-创建任务) 是**同一个模型的另外两个接口**：模型名相同、
任务 id 空间相同，拿到的句柄照旧用 `GET /v1/videos/{id}` 查。
差别只在提交路径、请求体和计费口径。

支持情况**逐模型不同**，目前只有 `minimax/minimax-h3`。⚠️ 目前网关并不会拒绝所有
不支持的模型，请只用 `minimax/minimax-h3` 调用这两个接口：

| 模型 | 再生成 | 上下文增强 |
|---|---|---|
| 火山 Seedance | 2.5 实测返回 `400` `source_video is not supported for this model`（`invalid_parameter`） | **被受理**，生成一条普通视频（没有 `output_text`），按视频计费 |
| `minimax/minimax-h3-max`（已下线） | **被受理**并建出任务 | 服务商透传的 `400`：`invalid params, model MiniMax-H3-Max does not support resolution 2K, …`，没有 `error.code` |

#### 再生成（768P → 2K）

```http
POST /v1/videos/regenerations
Content-Type: application/json

{
  "model": "minimax/minimax-h3",
  "prompt": "<生成源视频时真正送进模型的那份提示词>",
  "source_video": "https://example.com/source-768p.mp4",
  "size": "2K"
}
```

| 字段 | 必填 | 说明 |
|---|---|---|
| `source_video` | ✅ | 要放大的 768P 视频地址 |
| `prompt` | ✅ | 生成源视频时的**最终**提示词（若当时用过上下文增强，填增强**后**那份） |
| `size` | | 目标档位，默认 `2K` |
| `input_reference` | | 生成源视频时用过的**参考视频**，按原样附上 |
| `first_frame` / `last_frame` / `reference_image` / `reference_audio` | | 生成源视频时用过的其他参考素材，按原样附上 |
| `watermark` | | 是否加水印 |
| `seconds` | ❌ | **传了会 400**：时长由源视频决定 |

⚠️ 服务商要求把生成源视频时的全部输入原样附上。给的不是那一份时**不会报错**，
只是效果不达预期 —— 网关无从校验这一点。

#### 上下文增强（产物是文本）

```http
POST /v1/videos/context
Content-Type: application/json

{
  "model": "minimax/minimax-h3",
  "prompt": "一只猫在雨里走过霓虹街道",
  "seconds": 5
}
```

`seconds` **必填**（4～15）：不传返回 `400`，正文是纯文本
`binding: expr_path=duration, cause=missing required parameter`（响应头目前仍是
`Content-Type: application/json`）。不传 `size` 时网关按 `2K` 发给服务商。

终态对象里多一个 `output_text`（增强后的提示词），**没有** `/content` 可取（目前对它调
`/content` 返回 HTTP `500`，正文是 HTML）：

```json
{
  "id": "aigwt_...",
  "object": "video",
  "status": "completed",
  "output_text": "…（增强后的提示词）…",
  "usage": { "completion_tokens": 1820 }
}
```

典型用法是两步：先调本接口拿 `output_text`，再把它当 `prompt` 交给 `POST /v1/videos`。

🔴 计量口径与视频不同：按 **input / output token 两档独立**计价，不按秒。
`usage` 里不会有 `duration_seconds`。

---

## 7. 音频接口

> **本章示例中的 `<model-id>` 为占位符。** 音频模型的可用性取决于您的账户，
> 调用前请用 `GET /v1/models` 确认。返回 `403 model_not_available` 表示该模型
> 未对您的账户开放，返回 `404` 表示该模型所在通道未提供本接口 ——
> 两者都不是请求格式问题，改参数无法解决。

### 7.1 语音合成

```http
POST /v1/audio/speech
Content-Type: application/json
```

请求参数见 [§4.4](#44-音频参数)。响应为音频二进制流，
`Content-Type` 随 `response_format` 变化。

```bash
curl https://api.lyrouter.com/v1/audio/speech \
  -H "Authorization: Bearer sk-xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"model":"<model-id>","input":"你好","voice":"Cherry"}' \
  --output speech.mp3
```

### 7.2 语音转写

```http
POST /v1/audio/transcriptions
Content-Type: multipart/form-data
```

**响应字段**

| 字段 | 类型 | 说明 |
|---|---|---|
| `text` | string | 转写文本 |
| `usage` | object | 计量信息。部分模型不返回 |

`usage` 是否返回取决于模型：按 token 计费的转写模型返回完整 `usage`；
按时长计费的模型其响应中不含时长信息，`usage` 缺失。

### 7.3 语音翻译

```http
POST /v1/audio/translations
Content-Type: multipart/form-data
```

参数与响应同 [§7.2](#72-语音转写)，输出固定为英文。

---

## 8. 计量与计费

### 8.1 `usage` 字段

**`usage` 的字段随模态与模型而异，不应假设某个字段一定存在。**
不同模型可能按 token、按张数、按秒数计量：

| 字段 | 出现场景 |
|---|---|
| `total_tokens` / `output_tokens` | 按 token 计量的模型（部分图像模型） |
| `input_tokens_details.text_tokens`<br>`input_tokens_details.image_tokens` | GPT-Image 系列：输入的文字与图片**分开报** |
| `output_tokens_details.reasoning_tokens` | `google/gemini-3-pro-image` 的思考 token（已计入 `total_tokens`）；Flash 不产生，不出现 |
| `generated_images` / `input_images` | Seedream：出图张数 / 输入的参考图张数（`input_images` 只在 pro 上出现） |
| `duration_seconds` | 视频生成、升清任务终态 |
| `completion_tokens` | 视频任务终态：Seedance（按它计费，同时也有 `duration_seconds`）与上下文增强任务。图像接口不返回 |
| `input_tokens` / `output_tokens` | GPT-Image 与 Gemini 图像模型，以及按 token 计量的语音转写模型 |

### 8.1b 同步图像模型的计费项

同样是"图像生成"，三个模型族的计费项并不一样 —— 账单上看到的条目因此不同：

| 模型 | 计费项 | 来源字段 |
|---|---|---|
| `openai/gpt-image-2`、`openai/gpt-image-2.5-*` | `input_text` · `input_image` · `output` | `input_tokens_details.{text,image}_tokens`、`output_tokens` |
| `google/gemini-3.1-flash-image` | `input` · `output` | `promptTokenCount`、`candidatesTokenCount` |
| `google/gemini-3-pro-image` | `input` · `output` · `thinking` | 同上 + `thoughtsTokenCount` |

**编辑接口比生成接口多一项输入图片的费用。** GPT-Image 按 32×32 像素一块折算：长边不足 1024 时
先放大到长边的 2 倍（最多 1024），像素超过约 152 万时先等比缩小，再按 `ceil(宽/32) × ceil(高/32)` 计，
实测 1024×1024 的图为 1024 token、8×8 的图为 1 token。Gemini 与尺寸无关：`gemini-3-pro-image` 每张 560、
`gemini-3.1-flash-image` 每张 1120。GPT-Image 系列记在 `input_image`，
Gemini 并入 `input`（它的 usage 不区分文字与图片）。

**输出 token 的量级由哪个参数决定**（2026-10-14 生产实测）：

- **GPT-Image：主要由 `quality` 决定。** `1024x1024` 时 2.5 系列 `low` 输出 196 token、`max`
  输出 7,024 token，最高档约为最低档的 **36 倍**。同名档位在两代模型上不是同一个量：2.5 的
  `high` 等于 `gpt-image-2` 的 `medium` —— 跨模型比较请比 token，不要比档位名。
- **GPT-Image 不是像素越多越贵**（只适用于 GPT-Image）：输出 token 不随像素增加，例如
  `1024x1536`（约 157 万像素）的输出 token 比 `1024x1024`（约 105 万像素）还少。
- **Gemini：由分辨率档决定，两个模型各档不同**（`3-pro` 的 2K 档不生效），见
  [§4.2 例外](#例外gemini-系图像模型走契约转换不是透传)。
- **Seedream：按张计费。** `doubao-seedream-5.0-pro` 的参考图（**输入图片**，不是出图张数）
  每次请求第 1 张不收费，超出部分按张计费。

> 🔴 **厂商价目表上的"前缀缓存"两档价没有录，因为量取不到。** GPT-Image-2.5 的 usage
> 原文里**没有任何缓存字段** —— 录进去也是永远收不到钱的惰性条目。将来上游开始报缓存
> 用量，再录不迟。

### 8.2 计费时点

| 接口类型 | 计费时点 |
|---|---|
| 同步 | 响应返回时 |
| 异步（视频） | 任务到达终态时。提交响应中**不含** `usage` |

未产生结果的任务不计费，包括 `deadline_exceeded`、`canceled`、`upstream_error`
三种终态。

### 8.3 产物地址有效期

图像与视频产物的 URL 为服务商签发的**预签名地址，有效期通常为 24 小时**。
过期后访问返回 `403` 或 `404`（取决于服务商）。需长期保留的产物应在获取后自行下载存储。

---

## 9. 错误处理

### 9.1 错误响应结构

```json
{
  "error": {
    "message": "错误描述",
    "type": "invalid_request_error",
    "code": "model_not_available"
  }
}
```

透传模式下，服务商拒绝请求时返回的是**服务商自身的错误结构与错误码**，
字段名可能与上述不同。

**错误响应一定有响应体。** 服务商偶尔会回一个状态码但不给任何内容
（实测：某图像服务商对不存在的编辑接口回 `404` + `Content-Length: 0`）。
这种情况下网关补一个上述结构的错误体，并带上三个附加字段：

| 字段 | 说明 |
|---|---|
| `error.upstream_status` | 服务商返回的 HTTP 状态码 |
| `error.upstream_request_id` | 服务商自己的请求标识。**报障时请提供这个值**，网关自己的 `x-request-id` 服务商查不到。服务商未提供时该字段不出现 |
| `error.generated_by` | 固定值 `ai-gateway`，表示这条错误由网关生成而非服务商返回 |

**服务商给了响应体时网关一字不改地透传**，不会覆盖它的 `message`。
注意网关自己校验请求时生成的错误（如参数不合法、`unsupported parameters …`、`not_found`）
也**不带** `error.generated_by`，所以不能靠这个字段的有无区分错误来自网关还是服务商。

#### 🔴 有的服务商用 `200` 包着错误返回 —— 网关会改判成 `4xx`

少数服务商（Gemini 系图像模型）**拒绝请求时不回错误状态码**，而是回
`200` + 一个装着拒绝说明的响应体、且没有任何产物。

**这对客户端是最坏的一种**：官方 SDK 看到 `200` 不会抛异常，
于是你拿到的是"调用成功，但 `data` 是空的"，**分不清成功和失败**。

**网关的处理**（2026-09-17 起）：识别出来，对外改判成服务商自己声明的那个状态码。

```json
HTTP/1.1 400 Bad Request

{
  "error": {
    "code": "upstream_rejected",
    "type": "invalid_request_error",
    "message": "Request contains an invalid argument.",
    "upstream_status": 200,
    "upstream_error_code": 400,
    "upstream_error_reason": "INVALID_ARGUMENT",
    "generated_by": "ai-gateway"
  }
}
```

| 字段 | 说明 |
|---|---|
| `error.code` | 固定 `upstream_rejected` —— 这是"服务商拒绝了你的请求"，不是网关的判断 |
| `error.message` | **服务商原话，不改写** |
| `error.upstream_status` | 服务商实际给的状态码（这里就是那个 `200`） |
| `error.upstream_error_code` | 服务商在响应体里自己声明的错误码 |

> **`upstream_status: 200` 和本响应的 `400` 不是矛盾，是事实**：
> 服务商确实说了 `200`，我们确实认为这是一次失败。抹掉它只会让报障时查不清。

**判定很窄，四条同时满足才会改判**：响应是 `2xx` · 体里有顶层 `error` 对象 ·
`error.code` 是 `400`–`599` 的数字 · 该响应里**一个产物都没有**。
**缺任何一条都原样透传，一个字节不改。**

另一种情形是服务商**没有声明错误、但也没有产出图片**：Gemini 回 `200`、没有任何产物，
且候选的 `finishReason` 不是 `STOP`（如 `NO_IMAGE`、`SAFETY`）。网关同样改判成 `400`，
`error.code` 为 `upstream_no_artifact`，并用 `error.upstream_finish_reason` 带出服务商给的原因：

```json
{
  "error": {
    "code": "upstream_no_artifact",
    "type": "invalid_request_error",
    "message": "upstream produced no image (finishReason: NO_IMAGE)",
    "upstream_status": 200,
    "upstream_finish_reason": "NO_IMAGE",
    "generated_by": "ai-gateway"
  }
}
```

`finishReason` 缺失或为 `STOP` 时不改判、原样透传 —— 那可能是一次网关没认出产物的成功调用。

🔴 **这类错误是终态，网关不会换一个服务商重试** —— 同样的参数打给谁都会同样失败，
重试只会让你多等。请按 `error.message` 修正请求后重发。

### 9.2 HTTP 状态码

| 状态码 | 含义 | 处理建议 |
|---|---|---|
| `400` | 请求参数不合法；严格模式下存在不受支持的参数；或服务商用 `2xx` 包着错误返回而被网关改判（见 [§9.1](#91-错误响应结构)，此时 `error.upstream_status` 是那个 `2xx`） | 按 `error.message` 修正请求 |
| `401` | API Key 缺失、格式错误或已停用 | 检查 `Authorization` 头 |
| `402` | 余额不足 | 充值后重试 |
| `403` | 模型不可用或无访问权限 | 确认模型已上架且当前 Key 有权访问 |
| `404` | 资源不存在，或不属于当前 API Key 所在工作区 | 检查资源标识 |
| `409` | 状态冲突（产物尚未就绪、任务状态不允许取消） | 按 `error.code` 处理 |
| `429` | 触发限流，或服务商账号级并发已满 | 退避后重试。**更换 API Key 无效**，并发配额为服务商账号级共享 |
| `5xx` | 网关或服务商内部错误 | 重试。已提交的异步任务不受影响，网关将继续轮询 |

### 9.3 视频任务错误码

`status=failed` 时 `error.code` 的取值：

| `error.code` | 含义 | 计费 |
|---|---|---|
| `deadline_exceeded` | 提交后 24 小时仍未完成。⚠️ 目前这个状态不会同步到查询接口，`GET /v1/videos/{id}` 可能一直显示 `queued` 或 `in_progress`；提交 24 小时后仍未完成的任务可以停止轮询，实际状态请在控制台工作空间的 **多媒体生成** 页面按任务 ID 查看 | 不计费 |
| `canceled` | 任务被取消。目前不支持取消进行中的任务（见 [§6.6](#66-删除任务)），正常使用不会出现 | 不计费 |
| `upstream_error` | 服务商侧失败，且未给出可识别的错误码 | 不计费 |
| 服务商自有错误码 | 原样透出（如 `InvalidParameter`、`OutputVideoSensitiveContentDetected.PolicyViolation`） | 不计费 |

失败的任务对象上还可能带两个附加字段：

| 字段 | 说明 |
|---|---|
| `error.provider_request_id` | 服务商自己的请求标识。**报障时请提供这个值** |
| `error.retryable` | `false` = 重发同样的请求不会成功（参数不合法、内容审核未通过）；`true` = 值得重试。**字段不存在表示网关无法判断**，请自行决定 |

🔴 `error.retryable` 只在网关对该服务商的该错误码有确切结论时才出现。
不要把"字段不存在"读成 `false`。

---

## 10. SDK 支持

### 10.1 OpenAI 官方 SDK

图像与音频接口可直接使用 OpenAI 官方 SDK，仅需替换 `base_url`。

```python
from openai import OpenAI

client = OpenAI(api_key="sk-xxxxxxxx",
                base_url="https://api.lyrouter.com/v1")

# 图像
image = client.images.generate(
    model="bytedance/doubao-seedream-5.0-lite",
    prompt="a single green leaf",
    size="1920x1920",
)

# 模型列表
models = client.models.list()
```

```javascript
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: "sk-xxxxxxxx",
  baseURL: "https://api.lyrouter.com/v1",
});

const image = await client.images.generate({
  model: "bytedance/doubao-seedream-5.0-lite",
  prompt: "a single green leaf",
  size: "1920x1920",
});
```

**传递服务商特有参数**（透传模式下可用）：

```python
client.images.generate(
    model="bytedance/doubao-seedream-5.0-lite",
    prompt="...",
    extra_body={"watermark": False},      # 转发给服务商
)
```

**传递网关扩展头**：

```python
client.images.generate(
    model="bytedance/doubao-seedream-5.0-lite",
    prompt="...",
    size="1920x1920",
    extra_headers={"X-Gateway-Strict-Parameters": "true"},
)
```

> ⚠️ **严格模式只在网关会丢弃参数的地方有意义，而丢不丢取决于「端点 + 模型」，
> 不是只看端点。** 见 [§3 请求处理模式](#3-请求处理模式)的那张表。
>
> | 你在调 | 严格模式 |
> |---|---|
> | `/v1/videos` | ✅ 有效 |
> | `/v1/images/*` + **Gemini 系模型**（走契约转换，见 [§4.2 例外](#例外gemini-系图像模型走契约转换不是透传)） | ✅ 有效 |
> | `/v1/images/generations` + **GPT-Image 系列**（透传） | ⚠️ 只对网关声明丢弃的参数有效：`response_format`，2.5 系列另有 `stream`、`stream_options`。带了它们返回 `400`，其余参数原样转发 |
> | `/v1/images/*` + 其它模型（透传，含 Seedream 与 GPT-Image 的编辑接口） | ❌ **加了也没有任何效果** |
> | `/v1/audio/*`（透传） | ❌ 同上 |
>
> 透传模式下，除了 GPT-Image 生成接口声明丢弃的那几个参数，不存在"被丢弃的参数"这个概念 ——
> 你传什么就原样发给服务商，由它自己判定。所以那个头在这些接口上不拦截任何东西。

### 10.2 视频接口

OpenAI Node SDK 自 v7 起提供 `client.videos` 资源，可直接用于本网关；Python SDK 需要 2.x
（实测 1.58.1 没有 `client.videos`，2.48.0 可用）：

```python
video = client.videos.create(
    model="bytedance/doubao-seedance-2.0-mini",
    prompt="a paper boat drifting on still water",
    seconds=5,
    size="1280x720",
)
while video.status in ("queued", "in_progress"):
    time.sleep(10)
    video = client.videos.retrieve(video.id)

content = client.videos.download_content(video.id)   # 自动跟随 302
```

**逐方法支持情况**（基于 `openai` v7.15.0 核对）：

| SDK 方法 | 支持 | 说明 |
|---|---|---|
| `videos.create()` | ✅ | SDK 以 `multipart/form-data` 提交（无论是否含文件），本网关已支持。⚠️ 表单里只读取契约字段中的 `model`、`prompt`、`seconds`、`size`、`input_reference`、`watermark`、`generate_audio`、`output_format`、`omni_reference_task_type`；`first_frame`、`last_frame`、`reference_image`、`reference_audio` 请用 JSON 提交（见 [§6.1](#61-创建任务)） |
| `videos.retrieve()` | ✅ | |
| `videos.download_content()` | ✅ | 本网关返回 `302`，SDK 自动跟随，取回产物字节 |
| `videos.delete()` | ⚠️ 语义不同 | 只对**已结束**（`completed` / `failed`）的任务有效：返回 video 对象附带 `deleted: true`，而非 OpenAI 的 `{object: "video.deleted"}`。🔴 `deleted: true` **不表示数据已删除** —— 任务记录在 `expires_at` 之前仍可 `retrieve`，产物地址也仍可取。对进行中的任务返回 `409`（`cancel_not_supported`），见 [§6.6](#66-删除任务) |
| `videos.list()` | ❌ | 未提供列表接口 |
| `videos.remix()` | ❌ | 返回 `404` |

**参数差异**：

| 参数 | OpenAI | 本网关 |
|---|---|---|
| `size` | 四个固定值（`720x1280` / `1280x720` / `1024x1792` / `1792x1024`） | 取决于模型，见 [§4.3](#分辨率的两种写法)。🔴 **`1280x720` 并非对所有模型有效**；档位名（`768P`）能用但不在 SDK 的类型声明里，TypeScript 下需绕过类型检查 |
| `seconds` | `"4"` / `"8"` / `"12"` | 正整数，取值范围随模型 |
| `input_reference` | 文件上传或引用对象 | **参考视频的 URL 字符串**。不兼容：Python SDK 直接传字符串会在本地抛 `RuntimeError: Expected entry at input_reference to be bytes, an io.IOBase instance, PathLike or a tuple …`，请求不会发出，需用 `extra_body={"input_reference": url}` 传；Node SDK 运行时接受字符串，但 TypeScript 类型不允许。上传的文件目前会被忽略（见 [§6.1](#61-创建任务)） |
| `model` | `sora-2` 系列 | 本网关的模型标识 |

**响应字段差异**：`prompt`、`remixed_from_video_id` 不返回；
`expires_at` 返回的是**句柄过期时刻**（届时 `retrieve` 与取产物均返回 `404`）。
这些字段缺失不会导致 SDK 报错，读取时为 `undefined` / `None`。

---

## 11. 模型目录（全量）

本节逐个列出网关上当前可调用的全部多模态模型：接口、请求处理模式、**全部参数**（含取值
范围与默认值）、计费口径。

**读参数表之前先看处理模式**：

| 模式 | 网关做什么 | 照谁的文档写参数 |
|------|-----------|-----------------|
| 透传 | 请求体原样转发，只改写模型名 | 厂商原生参数 |
| 契约转换 | 翻译成厂商私有形状 | 网关契约（表里列全了） |

被丢弃的参数一律进响应头 `X-Gateway-Unsupported-Parameters`；带
`X-Gateway-Strict-Parameters: true` 则直接 400（`unsupported_parameters`）。

### 11.1 图像模型

### Seedream 5.0 pro / lite（火山方舟）

两个模型都是**透传**——下表的参数原样送到上游，厂商支持什么就能用什么。

#### 能力对比

| 能力 | `doubao-seedream-5.0-pro` | `doubao-seedream-5.0-lite` |
|------|---------------------------|----------------------------|
| 文生图 | ✓ | ✓ |
| 文生组图（一次出多张） | 暂不支持 | ✓ |
| 单图 / 多图生图 | ✓ | ✓ |
| 单图 / 多图生组图 | 暂不支持 | ✓ |
| **交互编辑**（坐标定位编辑） | ✓ | ✗ |
| **图层拆分**（拆成底图 + 最多 16 图层） | ✓ | ✗ |
| 流式输出 | 暂不支持 | ✓ |
| 联网搜索 | 暂不支持 | ✓ |
| 可选分辨率档位 | `1K`、`1.5K`、`2K` | `2K`、`3K`、`4K` |
| 输出格式 | `png`、`jpeg` | `png`、`jpeg` |
| 提示词优化模式 | 标准、极速 | 标准 |
| 生成数量限制 | 单图，或 1 底图 + 最多 16 图层 | 参考图数 + 生成图数 ≤ 15 |
| 限流 | 500 张/分钟 | 500 张/分钟 |

#### 参数

| 参数 | 类型 | 默认 | 取值 | 说明 |
|------|------|------|------|------|
| `model` | string | — | 见上表 | 必填 |
| `prompt` | string | — | — | 提示词。图层拆分时可省略（不传则模型自动识别要拆的元素） |
| `image` | string / string[] | — | 图片 URL 或 `data:image/<格式>;base64,<编码>` | 参考图。生图场景建议不超过 10 张；图层拆分仅支持 1 张 |
| `size` | string | `2K`（生图）/ `auto`（图层拆分） | 见下 | 两种写法不可混用 |
| `response_format` | string | `url` | `url`、`b64_json` | |
| `output_format` | string | `jpeg`（实测） | `png`、`jpeg` | 图层拆分时只控制底图格式，**图层恒为 png** |
| `background` | string | `opaque` | `transparent`、`opaque` | 透明通道。仅图生图、且只支持输入 1 张带透明通道的图 |
| `watermark` | boolean | `true`（实测不传也加水印） | `true`、`false` | `true` 在右下角加「AI生成」水印；不想要水印请显式传 `false` |
| `layer_decomposition` | boolean | `false` | `true`、`false` | **仅 pro**。开启后 `image` 变为必填且只能 1 张 |
| `optimize_prompt_options.mode` | string | `standard` | `standard`、`fast` | **仅 pro**。`fast` 更快、质量略低 |

**`size` 的两种写法**

方式 1，**指定档位（推荐）**：传 `1K` / `1.5K` / `2K`（pro）或 `2K` / `3K` / `4K`（lite），宽高比在 `prompt` 里用自然语言描述，由模型决定具体像素。pro 的档位映射（常见值，不限于此）：

| 档位 | 1:1 | 4:3 | 16:9 | 9:16 | 3:2 | 21:9 |
|------|-----|-----|------|------|-----|------|
| 1K | 1024×1024 | 1152×864 | 1424×800 | 800×1424 | 1248×832 | 1568×672 |
| 1.5K | 1536×1536 | 1792×1344 | 2048×1152 | 1152×2048 | 1872×1248 | 2352×1008 |
| 2K | 2048×2048 | 2368×1776 | 2816×1584 | 1584×2816 | 2496×1664 | 3136×1344 |

> [!tip]
> `1.5K` 与 `1K` **价格相同**，但出图效果更好。

方式 2，**指定宽高像素**（`宽x高`，如 `2048x1024`）：

- 总像素（宽 × 高）：pro 范围 **[921600, 4624220]**；lite 不少于 **3686400**（最小正方形是 `1920x1920`，常见的 `1024x1024` 会被拒绝，见 [§4.2](#42-图像参数)）
- 宽高比（宽/高）范围 **[1/16, 16]**
- 两条必须同时满足。`512x512` 总像素 262144，低于两款的下限，无效

图层拆分场景只支持档位写法，默认 `auto`：输入图在 [921600, 4624220] 之间按原尺寸输出；小于 1K 按 1K；大于 2K 按 2K。

#### 交互编辑（仅 pro）

在 `prompt` 里用坐标标签指定编辑位置，坐标**归一化到 0~999**（左上角 0,0，右下角 999,999）：

- 点选：`<point>x y</point>`，由模型判断影响范围
- 框选：`<bbox>x1 y1 x2 y2</bbox>`，精确控制区域

换算：`x = round(x_px / 展示宽 * 1000)`，`y` 同理。

| 场景 | 提示词写法 |
|------|-----------|
| 编辑指定点附近的对象 | `把图1<point>520 460</point>位置换成皇冠` |
| 编辑指定区域的对象 | `把图1<bbox>120 180 640 760</bbox>区域替换成花园` |
| 跨图片编辑 | `将图1<bbox>179 283 796 986</bbox>的主体放到图2<bbox>118 331 933 871</bbox>位置` |

> [!tip]
> 框选区域内有多个主体时，在提示词里补一句说明要编辑哪个（「左侧的人」「戴帽子的猫」）。需要保持不变的物体也可以一并框出来并写明「保持不变」。

也可以不用坐标，直接在参考图上手绘草图 / 涂鸦 / 圈选，再用自然语言描述标记位置。

#### 图层拆分（仅 pro）

`layer_decomposition: true` 把单张图拆成 1 张底图和最多 16 个带透明通道的 PNG 图层。

```bash
curl https://api.lyrouter.com/v1/images/generations \
  -H "Authorization: Bearer sk-你的密钥" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "bytedance/doubao-seedream-5.0-pro",
    "image": "https://example.com/poster.png",
    "size": "2K",
    "layer_decomposition": true,
    "watermark": false
  }'
```

响应的 `data[]` 同时包含底图与图层：

| 字段 | 出现在 | 含义 |
|------|--------|------|
| `url` | 底图、图层 | 下载链接，**仅保留 24 小时** |
| `z_index` | 底图、图层 | 叠放顺序。底图固定 `0`，图层递增 |
| `bounding_box.absolute` | 图层 | `[left, top, right, bottom]`，输出底图坐标系中的绝对像素位置 |
| `bounding_box.normalized` | 图层 | 同上但归一化（0~1000），用于还原到任意尺寸画布 |
| `name`、`description` | 图层 | 图层名称与内容描述 |

还原到底图：`x = left`，`y = top`，`w = right - left`，`h = bottom - top`，按 `z_index` 从小到大叠放。
还原到 `W × H` 的画布：`x = left / 1000 × W`，其余同理（归一化坐标是整数，换算有取整误差）。

拆出的图层可以作为输入图再次送进模型单独编辑；想保留透明背景就同时传 `background: transparent` 和 `output_format: png`。

> [!note]
> 图层拆分每次请求**预扣 17 张的限流额度**（按最大输出张数预留），生成完成后按实际张数返还。

#### 输入图限制

| 约束 | 生图场景 | 图层拆分场景 |
|------|----------|--------------|
| 格式 | jpeg、png、webp、bmp、tiff、gif、heic、heif | png、jpeg |
| 总像素（宽×高） | [196, 36,000,000] | [262144, 36,000,000] |
| 宽高长度 | ≥ 14 px（实测 14×14 可用） | — |
| 宽高比（宽/高） | [1/16, 16] | [1/16, 16] |
| 单张大小 | ≤ 30 MB | ≤ 30 MB |
| 张数 | 建议不超过 10 张（实测传 11 张未被拒绝，但输出尺寸异常） | 仅 1 张 |

base64 需遵循 `data:image/<格式>;base64,<编码>`，`<格式>` 必须小写。

#### 计费

按**张**计，分档维度是**分辨率档位**与**是否图层拆分**：

| 模型 | 分档 |
|------|------|
| `doubao-seedream-5.0-pro` | 1K / 1.5K（≤ 261 万像素）一档，2K 一档 |
| `doubao-seedream-5.0-pro` | 图层拆分时另有两档（同样按 1K/1.5K 与 2K 分） |
| `doubao-seedream-5.0-lite` | 不分档 |

`seedream-5.0-pro` 的**参考图**单独计一项：每次请求的第 1 张参考图不收费，超出部分按张计费。这里数的是输入的参考图，不是出图张数。图层拆分按实际产出的图片张数（底图 + 各图层）计费。

> [!tip]
> `1.5K` 与 `1K` **同档同价**，出图更好——没有特殊理由时优先用 `1.5K`。

### GPT-Image-2 / 2.5-flare / 2.5-sunburst

**透传**，参数与 OpenAI 图像 API 一致。

| 参数 | 类型 | 必填 | 取值 | 说明 |
|------|------|------|------|------|
| `model` | string | 是 | `openai/gpt-image-2`、`openai/gpt-image-2.5-flare`、`openai/gpt-image-2.5-sunburst` | |
| `prompt` | string | 是 | — | |
| `quality` | string | 见说明 | 逐模型不同，见下表 | `gpt-image-2` **必填**；建议所有 GPT-Image 模型都显式传入 |
| `size` | string | 否 | `宽x高` | 只接受 `宽x高`，不接受档位名；须满足下文四条硬约束，否则 400 |
| `n` | integer | 否 | 最大 10 | 出图张数 |
| `user` | string | 否 | — | 终端用户标识 |

**`quality` 逐模型取值**：

| 模型 | 取值 | 默认 |
|------|------|------|
| `gpt-image-2` | `low`、`medium`、`high`、`auto` | 无，**必填**：不传会间歇性返回 400（`INVALID_REQUEST_BODY`）；`auto` 实测等同 `low` |
| `gpt-image-2.5-flare`、`gpt-image-2.5-sunburst` | `low`、`medium`、`high`、`xhigh`、`max`、`auto` | `low`（`auto` 实测等同 `low`） |

`xhigh`、`max` 两档是否可用由上游账号能力决定，可能被拒；收到 `Invalid value: 'xhigh'` 时改用 `high`。

**`size` 的四条硬约束**（由服务商校验，网关不预先拦截，任一不满足返回 400）：

| 约束 | 服务商报错原文 | 示例 |
|------|---------------|------|
| 宽和高都能被 16 整除 | `Width and height must both be divisible by 16.` | `1008x1008` ✓；`1254x1254`、`1312x1199` ✗ |
| 最长边 ≤ 3840 | `The longest edge must be less than or equal to 3840.` | `3840x2160` ✓；`4096x4096`、`1024x4096` ✗ |
| 总像素不少于约 100 万 | `Requested resolution is below the current minimum pixel budget.` | `1008x1008`（约 102 万）✓；`512x1024`（约 52 万）✗ |
| 总像素不超过约 830 万 | `Requested resolution exceeds the current pixel budget.` | `3840x2160`（约 829 万）✓；`3840x2176`（约 836 万）✗ |

上表的报错原文前面都带 `Invalid size '<宽>x<高>'. ` 前缀，例如 `Invalid size '4096x4096'. The longest edge must be less than or equal to 3840.`

GPT-Image 不接受档位名：传 `4K` 返回 `Invalid size '4K'. Expected WIDTHxHEIGHT, for example '1824x1024'.`

> [!warning]
> **生成接口上 `response_format` 会被丢弃。** 这个系列固定返回 base64，而上游对该参数的处理不稳定（实测同一模型、同一套参数，21 分钟内在 400 与 200 之间来回翻）。网关统一丢弃它并在 `X-Gateway-Unsupported-Parameters` 里说明，把「随机失败」换成「确定行为」。2.5 系列同时丢弃 `stream` / `stream_options`——传了上游会开启流式，导致请求失败（500）。
>
> **编辑接口（`/v1/images/edits`）上网关不丢弃这几个参数**，会原样转发给上游，请不要传。

> [!warning]
> **耗时按分钟算。** 2026-10-16 实测一次生成约 20~60 秒（最长一次约 83 秒），参数错误导致的上游 400 在 2 秒内返回；更早的测量中出现过 2~4 分钟、最长 213 秒的生成。客户端超时请按分钟设置。

#### 计费

按 token 计：**输入文本**、**输入图片**、**输出**各有独立单价。**缓存命中**另有一项单价，但 `usage` 不含缓存字段（实测 2.5 系列与 `gpt-image-2` 都没有），不产生这一项（见 [§8.1b](#81b-同步图像模型的计费项)）。

上游在 `usage.input_tokens_details` 里分开报文字与图片 token，网关按两个单价分别计费。

输出 token 主要由 `quality` 决定：`1024x1024` 时 2.5 系列 `low` 输出 196 token、`max` 输出 7024 token，最高档约为最低档的 36 倍。同名档位在两代模型上不是同一个量 —— 2.5 系列的 `high` 与 `gpt-image-2` 的 `medium` 输出 token 相同，跨模型比较请比 token，不要比档位名。

> [!note]
> **GPT-Image 不是像素越多越贵**（只适用于 GPT-Image）：输出 token 不随像素增加，例如 `1024x1536`（约 157 万像素）的输出 token 比 `1024x1024`（约 105 万像素）还少。

---

### Gemini 3 Pro Image / 3.1 Flash Image

这两个走**契约转换**——网关把统一参数翻译成 Gemini 原生的 `generateContent` 形状。因此**只有下表这些参数会被送出去**：

| 参数 | 是否生效 | 说明 |
|------|---------|------|
| `model` | ✓ | `google/gemini-3-pro-image`、`google/gemini-3.1-flash-image` |
| `prompt` | ✓ | 翻译成 `contents[].parts[].text`。必填，不写或为空返回 400 |
| `size` | ✓ | 见下，两种写法行为不同 |
| `image` | ✓ | 参考图 / 待编辑图，一张或多张，翻译成 `inlineData`。data URI 取其中声明的格式；裸 base64 按 PNG / JPEG / WEBP / GIF 的字节特征识别，认不出时返回 400 并指明第几张图。不接受图片 URL |
| `n` | **丢弃** | Gemini 的候选数参数属于文本生成，图像端点上是否生效没有证据。宁可明示丢弃，也不编一个「请求 200、设置静默不生效、账单照出」的映射 |
| `quality` | **丢弃** | Gemini 契约里没有这个概念 |
| `response_format` | **丢弃** | 只能返回 base64（`b64_json`） |
| `user` 及其他参数 | **丢弃** | — |

被丢弃的参数会出现在响应头 `X-Gateway-Unsupported-Parameters` 里；带 `X-Gateway-Strict-Parameters: true` 则直接 400。

#### `size`：档位名或宽高

| 写法 | 映射到 | 实际效果 |
|------|--------|---------|
| 档位名（`1K`、`2K`、`4K`） | `imageConfig.imageSize` | **只控制分辨率**，宽高比由上游默认。各档输出 token 两个模型不同，见 [§4.2 例外](#例外gemini-系图像模型走契约转换不是透传)；`512` 档在生产环境被服务商拒绝，不要使用 |
| 宽高（`1376x768`） | `imageConfig.aspectRatio` + `imageConfig.imageSize` | 先匹配最接近的支持比例（误差 2% 以内），再按长边**向上**取分辨率档位，**比例与分辨率都生效** |
| 宽高比超出 2% 误差（如 `1000x700`） | 整个 `size` 不下发 | 比例和分辨率档都被丢弃，按上游默认的比例和分辨率出图。请求仍返回 200、仍出图、仍计费，唯一的信号是响应头 `X-Gateway-Unsupported-Parameters: size`；严格模式下返回 400 |

长边与档位的对应、两个模型各档的输出 token、长边 ≤ 704 落入 `512` 档的问题见 [§4.2 例外](#例外gemini-系图像模型走契约转换不是透传)。出图尺寸由服务商按比例档和分辨率档决定，不等于请求的宽高（例如 `gemini-3-pro-image` 请求 `4096x2304` 实际出图 `5504x3072`），请以返回图片的实际宽高为准。例如 `1024x1024` 映射为 1:1 + `1K`，`2048x2048` 映射为 1:1 + `2K`。

> [!warning]
> 向上取档意味着长边略超过某一档时会落到下一档：`1500x1500` 取的是 `2K` 档，账单按实际输出的 token 收。想精确控制分辨率就直接传档位名。
>
> 比例偏差超过 2% 时整个 `size` 被丢弃、请求照常成功，生产环境建议带 `X-Gateway-Strict-Parameters: true`，让这种请求改为返回 400。要 16:9 + 4K 请写 `4096x2304`，不要照抄 `5632x3072`（比例 1.833，偏离 16:9 超过 2%，会被丢弃）。
>
> `size` 不接受比例字符串：`16:9` 会被当作档位名原样传给上游，上游不认识，返回 400（`upstream_rejected`）。要 16:9 请写该比例的宽高，如 `1376x768`。

支持的宽高比：`1:1`、`2:3`、`3:2`、`3:4`、`4:3`、`4:5`、`5:4`、`9:16`、`16:9`、`21:9`、`1:4`、`4:1`、`1:8`、`8:1`。传宽高时匹配误差 2% 以内的最接近比例，超出误差则比例和分辨率档都不下发，`size` 计入丢弃参数。

#### 计费

按 token 计：**输入**、**输出**各有独立单价；`gemini-3-pro-image` 另有**思考**一项（`usage.output_tokens_details.reasoning_tokens`，`usage.total_tokens` 包含它），`gemini-3.1-flash-image` 不产生思考 token。带参考图时，输入图片并入「输入」。

> [!note]
> **思考 token（Pro）单独计费且不可预测。** 只有 `gemini-3-pro-image` 产生思考 token。生产实测每次 115～316 个、中位约 220，由模型自行决定，不随输出分辨率档位或提示词长度变化；它约占单次费用的 1.5%，预算时可按每次约 220 个估算。


### 11.2 视频模型

### Seedance 2.5 / 2.0-fast / 2.0-mini（火山方舟）

#### 能力对比

| 能力 | `doubao-seedance-2.5` | `doubao-seedance-2.0-fast` | `doubao-seedance-2.0-mini` |
|------|----------------------|---------------------------|---------------------------|
| 文生视频 | ✓ | ✓ | ✓ |
| 首帧 / 首尾帧生视频 | ✓ | ✓ | ✓ |
| 图片参考 / 视频参考 | ✓ | ✓ | ✓ |
| **音频参考** | ✗ 网关目前不转发 | ✗ 同左 | ✗ 同左 |
| 编辑视频 / 延长视频 | ✓ | ✓ | ✓ |
| 生成有声视频 | ✓ | ✓ | ✓ |
| **参考素材上限** | **30 图 + 1 视频** | 9 图 + 1 视频 | 9 图 + 1 视频 |
| **输出分辨率** | 480p、720p、1080p(10bit) | 480p、720p | 480p、720p |
| 输出宽高比 | 21:9、16:9、4:3、1:1、3:4、9:16、自适应 | 同左 | 同左 |
| **输出时长** | **4~30 秒** | 4~15 秒 | 4~15 秒 |
| **输出格式** | mp4、**mov** | mp4 | mp4 |

- `reference_audio`（参考音频）目前不会转发给 Seedance：传了不报错，也不会出现在 `X-Gateway-Unsupported-Parameters` 中。需要参考音频时请改用 MiniMax H3。
- 参考视频 `input_reference` 每次只能传一个地址，写成数组返回 400。
- 服务商的联网搜索、返回尾帧等能力没有对应的网关参数，目前无法通过 LYRouter 使用。

> [!note]
> Seedance 2.5 的 1080p 采用 **10bit 位深 + H.265/HEVC** 编码，少数播放器不兼容；`mov` 采用 H.264 + yuv444p + PCM，建议用 VLC / mpv / ffplay / IINA 播放。

#### `size` 的两种写法

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

#### 任务类型与特殊限制

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

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

> [!warning]
> **视频编辑任务不能指定时长。** 厂商要求该场景时长固定跟随源视频，网关的 `seconds` 只接受正整数，所以**编辑任务请直接不传 `seconds`**，让厂商用默认值。传了会被厂商拒绝（`InvalidParameter.TaskTypeConstraint`）。

用 `omni_reference_task_type` 显式声明任务类型，可以把厂商的**异步报错提前成同步报错**（提交时就失败，而不是任务跑到一半失败）：

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

`omni_reference_task_type` 为 2.5 设计；2.0 系列传入目前不会报错，任务照常生成并计费。`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`）。提示词里用 `@视频1`、`@图像1` 指代参考素材。

#### 输入素材限制（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 |

> [!warning]
> Seedance **不接受含真人人脸的参考图 / 参考视频**，会被输入审核拦截。

#### 计费

按输出 token 计（任务对象中的 `usage.completion_tokens`），分档维度是**分辨率**与**有无视频输入**：

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

「有视频输入」指请求带了参考视频 `input_reference`（参考视频、视频编辑、视频延长）；只传首帧 / 首尾帧的图生视频不算。**有视频输入时按另一档单价计费，单价更低，但总价更高**：带参考视频时计量的 token 大约翻倍（见下文「等效秒」）。

> [!note]
> 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 秒计量。

### MiniMax H3 / H3-Max

> [!warning]
> `minimax/minimax-h3-max` **已被服务商下线，目前不可用，请改用 `minimax/minimax-h3`**。现在提交 H3-Max 任务会返回 HTTP 200，但任务对象是空的（`id` 为空），附带服务商的 `base_resp.status_code: 2013`、`status_msg: "invalid params, POST /v1/videos is no longer supported"`，没有任务 ID 可轮询，也不扣费。本节保留 H3-Max 的差异说明，仅供对照。

#### 能力对比

| | `minimax/minimax-h3` | `minimax/minimax-h3-max`（已下线） |
|---|---|---|
| 定位 | 标准版 | **极速版** |
| 分辨率 | `480P`、`768P`、`2K` | `480P`、`768P`（**不支持 2K**） |
| 时长 | 4~15 秒 | 5~15 秒（**不支持 4 秒**） |
| 文生视频 / 图生视频（首尾帧） | ✓ | ✓ |
| 多模态参考（图 / 视频 / 音频） | ✓ | ✓，但**不支持多张参考图**（服务商会拒绝并建议改用 H3） |
| 视频升清 `/v1/videos/regenerations` | ✓ | ✗ |
| 上下文增强 `/v1/videos/context` | ✓ | ✗ |

#### `size`：短边必须正好落在档位上

| 写法 | 网关怎么处理 |
|------|-------------|
| 档位名（`480P`、`768P`、`2K`） | 原样送出 |
| `宽x高` | 取**短边**查档位表：短边必须**精确**是 `480` 或 `768`，否则网关直接拒绝并告诉你合法值；宽高比按就近匹配 |

> [!note]
> 网关在这里替你拒，而不是把一个拼出来的档位发给厂商——厂商只会回一句「ratio 非法」，看不出是尺寸引起的。

`size` 和 `seconds` 在 MiniMax 上都要显式传：服务商要求提供分辨率和时长，网关不补默认值。

档位决定的是分辨率档，产物的实际像素由服务商决定。实测：文生视频和多模态参考用 `480P` 或 `854x480` 时产物为 1038×576（短边 576，不是 480）；`768P` 为 1344×768；首尾帧图生视频用 1:1 的图片 + `854x480` 时为 768×768；升清到 `2K` 为 2560×1440。

#### 三种输入场景与 `ratio` 规则

| 场景 | 怎么传 | 宽高比 |
|------|--------|--------|
| 文生视频 | 只传 `prompt` | **必须显式指定**，不能自适应。网关由 `size` 推导 |
| 图生视频 | `first_frame`（+ `last_frame`） | 由输入图决定，传了也会被忽略 |
| 多模态参考生视频 | `reference_image` / `input_reference` / `reference_audio` | 可选，默认自适应。**传参考视频时不能自适应**：`size` 写成档位名时任务会失败（`h3submit: aspect ratio "adaptive" is not supported for task ref2va`），请写成 `宽x高`（如 `854x480`） |

> [!warning]
> **图生视频与多模态参考互斥**：传了 `reference_*` 就不能再传 `first_frame` / `last_frame`，反之亦然。

#### 这个模型明确不支持的参数

以下参数在网关契约里存在（别的模型用得上），但 MiniMax 没有对应概念，传了会被**显式拒绝**并说明原因，不会悄悄忽略：

`generate_audio`（有声由参考音频承载）、`output_format`（没有容器格式选项）、`omni_reference_task_type`（没有全模态任务类型）、`source_video`（只属于升清接口）。

#### 输入素材限制

| | 限制 |
|---|---|
| **图片** | JPG、JPEG、PNG、WEBP、HEIC、HEIF；宽高 [256, 5760] px；宽高比 [0.4, 2.5]；≤ 30 MB；首帧 ≤ 1、尾帧 ≤ 1、参考图 ≤ 9 |
| **视频** | MP4、MOV（视频 H.264/H.265，音频 AAC/MP3）；≤ 50 MB；≤ 3 个；单段 [2,15] 秒且总时长 ≤ 15 秒；宽高 [256,5760] px；宽高比 [0.4,2.5]；帧率 [23.976, 60] |
| **音频** | WAV、MP3；≤ 15 MB；≤ 3 个；单段 [2,15] 秒且总时长 ≤ 15 秒 |
| **请求体** | 总大小 ≤ 64 MB，大文件请用公网 URL |

#### 视频升清（`/v1/videos/regenerations`）

把一条 **H3 768P** 产物重新生成为 2K。只支持 H3 自己产出的、符合 768P 规格的视频，不是通用的视频放大。

```bash
curl https://api.lyrouter.com/v1/videos/regenerations \
  -H "Authorization: Bearer sk-你的密钥" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "minimax/minimax-h3",
    "prompt": "生成那条 768P 时实际送入模型的最终提示词",
    "source_video": "https://example.com/h3-768p.mp4",
    "size": "2K"
  }'
```

源视频必须满足：含音轨、24 fps、宽和高都能被 32 整除、面积在 589,824 ~ 1,032,192 像素之间、总帧数 107~362（每档递增 17 帧，约 4~15 秒）。

> [!warning]
> **`prompt` 必须是生成 768P 时实际送入模型的最终提示词**，参考素材也要与当时一致。任何输入不一致都可能拿不到预期效果。如果当时用过上下文增强，要用增强**之后**的提示词。

#### 上下文增强（`/v1/videos/context`）

把多模态上下文理解成一段结构化的、语义更丰富的提示词。**这个接口只返回文本，不生成视频**。

```bash
curl https://api.lyrouter.com/v1/videos/context \
  -H "Authorization: Bearer sk-你的密钥" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "minimax/minimax-h3",
    "prompt": "史诗级太空歌剧预告：女舰长独自站在观景窗前",
    "seconds": 5
  }'
```

同样是异步任务，轮询到终态后产物是**文本**（增强后的提示词），拿去喂给 `/v1/videos` 即可。这个接口的 `seconds` 是**必填**的（4~15），不传返回 400 纯文本 `binding: expr_path=duration, cause=missing required parameter`；不传 `size` 时网关按 `2K` 发给服务商。只用 `minimax/minimax-h3` 调用：目前传 Seedance 模型不会被拒，而是生成一条普通视频并按视频计费（见 [§6.8](#68-再生成与上下文增强)）。

#### 计费

视频按**秒**计，按**分辨率**分档：

| 模型 | 分档 |
|------|------|
| `minimax-h3` | 480P / 768P / 2K 三档；升清（`/videos/regenerations`）单独一档 |
| `minimax-h3-max`（已被服务商下线，目前不可用，请改用 `minimax-h3`） | 480P / 768P 两档 |

**参考图单独计一项**（两个模型的参考图单价不同）：`minimax-h3` 的参考图前 5 张免费，超出部分按张计费。

**上下文增强**（`/videos/context`）按 token 计，输入与输出两项分开 —— 它产出的是文本，不是视频。

> [!note]
> 计费秒数是**输入 + 输出**的总和（`usage.duration_seconds`）。传了参考视频时，参考视频的秒数同样计入（实测 4 秒参考视频 + 4 秒输出计 8）。视频升清不相加，只计源视频的秒数（实测源视频 4 秒计 4）。


### 11.3 语音模型

#### qwen-audio-3.0-tts-plus

`qwen/qwen-audio-3.0-tts-plus` 走标准的 `/v1/chat/completions`，把文本放进 `messages`，响应里带回音频。

```bash
curl https://api.lyrouter.com/v1/chat/completions \
  -H "Authorization: Bearer sk-你的密钥" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen/qwen-audio-3.0-tts-plus",
    "messages": [{ "role": "user", "content": "今天天气不错，适合出门散步。" }]
  }'
```

**计费按字符**，按**输入文本的字符数**计（上游不返回 token 用量）。中文按字计，不是按字节。

> [!warning]
> **该模型当前在生产上并未真正合成语音。** 全部 660 次「成功」调用都落到了 `model_real=qwen3.7-plus`（一个文本模型），
> 没有一次响应里带 `audio` 字段；真正打到 `qwen-audio-3.0-tts-plus` 的 3 次全部返回 500。
> 在重新验证之前，请不要按这一节接入。

---

## 附录：文档维护

本文档的以下内容由代码决定，修改代码时需同步更新：

| 内容 | 来源 |
|---|---|
| 端点路径 | `gatewayansible` 的 `roles/wiring/defaults` |
| 视频状态与错误码 | `apisix/plugins/ai-drivers/videos.lua` |
| 图像响应结构 | `apisix/plugins/ai-drivers/image_envelope.lua` |
| 视频受支持参数 | `apisix/plugins/ai-proxy/async_body.lua` 的 `KNOWN` |
| 处理模式（透传/转换） | 厂商 spec 的 `sync.<endpoint_type>.body_shape` 是否声明 |

契约测试 `gatewayansible/tests/test_openapi_matches_routes.py` 校验 OpenAPI 规格
与实际下发路由的路径集合一致性。字段名、状态取值与错误码无自动校验，需人工同步。
