From 946fdaeb53a6e174adcc67b5f1aac2bf65b64cff Mon Sep 17 00:00:00 2001
From: aabao <602018325@qq.com>
Date: Wed, 8 Apr 2026 16:32:13 +0800
Subject: [PATCH 1/8] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=94=AF=E6=8C=81?=
=?UTF-8?q?=E6=96=87=E6=A1=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.kilocode/setup-script | 39 ++
docs/openapi/doubao-video-api.yaml | 623 +++++++++++++++++++++++++++++
2 files changed, 662 insertions(+)
create mode 100644 .kilocode/setup-script
create mode 100644 docs/openapi/doubao-video-api.yaml
diff --git a/.kilocode/setup-script b/.kilocode/setup-script
new file mode 100644
index 000000000000..24b867a49da4
--- /dev/null
+++ b/.kilocode/setup-script
@@ -0,0 +1,39 @@
+#!/bin/bash
+# Kilo Code Worktree Setup Script
+# This script runs before the agent starts in a worktree (new sessions only).
+#
+# Available environment variables:
+# WORKTREE_PATH - Absolute path to the worktree directory
+# REPO_PATH - Absolute path to the main repository
+#
+# Example tasks:
+# - Copy .env files from main repo
+# - Install dependencies
+# - Run database migrations
+# - Set up local configuration
+
+set -e # Exit on error
+
+echo "Setting up worktree: $WORKTREE_PATH"
+
+# Uncomment and modify as needed:
+
+# Copy environment files
+# if [ -f "$REPO_PATH/.env" ]; then
+# cp "$REPO_PATH/.env" "$WORKTREE_PATH/.env"
+# echo "Copied .env"
+# fi
+
+# Install dependencies (Node.js)
+# if [ -f "$WORKTREE_PATH/package.json" ]; then
+# cd "$WORKTREE_PATH"
+# npm install
+# fi
+
+# Install dependencies (Python)
+# if [ -f "$WORKTREE_PATH/requirements.txt" ]; then
+# cd "$WORKTREE_PATH"
+# pip install -r requirements.txt
+# fi
+
+echo "Setup complete!"
diff --git a/docs/openapi/doubao-video-api.yaml b/docs/openapi/doubao-video-api.yaml
new file mode 100644
index 000000000000..1fa205ed8174
--- /dev/null
+++ b/docs/openapi/doubao-video-api.yaml
@@ -0,0 +1,623 @@
+openapi: 3.1.0
+info:
+ title: 豆包视频生成 API
+ description: |
+ 基于 new-api 网关的豆包视频生成接口文档。
+
+ 支持的豆包视频模型(`DoubaoVideo` 渠道类型 54):
+ - `doubao-seedance-1-0-pro-250528`
+ - `doubao-seedance-1-0-lite-t2v`(仅文生视频)
+ - `doubao-seedance-1-0-lite-i2v`(仅图生视频)
+ - `doubao-seedance-1-5-pro-251215`
+ - `doubao-seedance-2-0-260128`
+ - `doubao-seedance-2-0-fast-260128`
+
+ **计费说明**:
+ - 计费单位为系统配额(Quota),由管理员为每个模型配置 `ModelRatio`(每次任务的基础价格)
+ - 当请求中的 `metadata.content` 包含 `video_url` 类型的内容(即图生视频/视频续拍)时,系统会自动应用视频输入折扣:
+ - `doubao-seedance-2-0-260128`:折扣比率 ≈ 0.6087(≈6折)
+ - `doubao-seedance-2-0-fast-260128`:折扣比率 ≈ 0.5946(≈6折)
+ - 视频生成为异步任务,提交时即预扣费,任务失败会自动退款
+
+ **认证**:所有接口均需在请求头中携带 API Token,格式:`Authorization: Bearer sk-xxxx`
+
+ version: "1.0.0"
+ contact:
+ name: API Support
+
+servers:
+ - url: https://api.example.com
+ description: new-api 网关地址(请替换为实际地址)
+
+tags:
+ - name: 视频生成
+ description: 豆包视频生成任务提交与查询
+ - name: 视频内容
+ description: 已完成视频的内容下载
+
+paths:
+ /v1/video/generations:
+ post:
+ tags:
+ - 视频生成
+ summary: 提交视频生成任务
+ description: |
+ 向豆包视频模型提交一个视频生成任务(异步)。
+
+ 提交后立即返回任务 ID,视频不会立刻生成完毕。
+ 请使用返回的 `id`(即 `task_id`)轮询 `GET /v1/video/generations/{task_id}` 查询结果。
+
+ **支持的场景**:
+ - 文生视频:仅提供 `prompt`,不传 `image`
+ - 图生视频:提供 `prompt` 和 `image`(图片 URL),或在 `metadata.content` 中传入图片
+ - 视频续拍/参考:在 `metadata.content` 中传入 `video_url` 类型内容(会触发视频输入折扣)
+
+ operationId: submitVideoGeneration
+ security:
+ - BearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/VideoGenerationRequest'
+ examples:
+ 文生视频:
+ summary: 基础文生视频
+ value:
+ model: doubao-seedance-2-0-260128
+ prompt: 宇航员在月球表面慢慢行走,地球悬挂在黑色天空中
+ seconds: "5"
+ metadata:
+ ratio: "16:9"
+ 图生视频:
+ summary: 图生视频(含参考图片)
+ value:
+ model: doubao-seedance-2-0-260128
+ prompt: 让图片中的人物微笑并转身
+ image: "https://example.com/reference.jpg"
+ seconds: "5"
+ metadata:
+ ratio: "16:9"
+ 视频续拍(含视频输入折扣):
+ summary: 基于已有视频片段续拍
+ value:
+ model: doubao-seedance-2-0-260128
+ prompt: 继续往前走,走向远处的山丘
+ metadata:
+ content:
+ - type: video_url
+ video_url:
+ url: "https://example.com/input.mp4"
+ seconds: "5"
+ 高级参数:
+ summary: 完整参数示例
+ value:
+ model: doubao-seedance-2-0-260128
+ prompt: 一朵玫瑰花在阳光下缓缓绽放,微风吹拂
+ seconds: "8"
+ metadata:
+ ratio: "16:9"
+ resolution: "720p"
+ seed: 42
+ watermark: false
+ camera_fixed: false
+ responses:
+ "200":
+ description: 任务提交成功,返回任务信息(此时任务仍在生成中,status 为 queued)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/VideoTaskResponse'
+ example:
+ id: task_abc123def456
+ task_id: task_abc123def456
+ object: video
+ model: doubao-seedance-2-0-260128
+ status: queued
+ progress: 0
+ created_at: 1712345678
+ "400":
+ description: 请求参数错误
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ "401":
+ description: 未授权(Token 无效或缺失)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ "402":
+ description: 账户余额不足
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ "500":
+ description: 服务器内部错误
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+
+ /v1/video/generations/{task_id}:
+ get:
+ tags:
+ - 视频生成
+ summary: 查询视频生成任务状态
+ description: |
+ 根据任务 ID 查询视频生成任务的当前状态和结果。
+
+ **任务状态说明**:
+ - `queued`:任务已提交,等待处理
+ - `in_progress`:任务处理中
+ - `completed`:任务已完成,可从 `metadata.url` 获取视频地址
+ - `failed`:任务失败,查看 `error` 字段了解失败原因
+
+ **获取视频**:
+ - 任务 `completed` 后,`metadata.url` 字段包含视频的直接下载链接
+ - 也可调用 `GET /v1/videos/{task_id}/content` 通过代理下载视频文件
+
+ **轮询建议**:每 5 秒查询一次,超时时间建议设置为 10 分钟。
+
+ operationId: getVideoGenerationTask
+ security:
+ - BearerAuth: []
+ parameters:
+ - name: task_id
+ in: path
+ required: true
+ description: 提交任务时返回的任务 ID(`id` 字段)
+ schema:
+ type: string
+ example: task_abc123def456
+ responses:
+ "200":
+ description: 查询成功(可能处于任意状态)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/VideoTaskResponse'
+ examples:
+ 处理中:
+ summary: 任务进行中
+ value:
+ id: task_abc123def456
+ task_id: task_abc123def456
+ object: video
+ model: doubao-seedance-2-0-260128
+ status: in_progress
+ progress: 50
+ created_at: 1712345678
+ 已完成:
+ summary: 任务完成,视频可下载
+ value:
+ id: task_abc123def456
+ task_id: task_abc123def456
+ object: video
+ model: doubao-seedance-2-0-260128
+ status: completed
+ progress: 100
+ created_at: 1712345678
+ completed_at: 1712345900
+ metadata:
+ url: "https://cdn.example.com/videos/result.mp4"
+ 失败:
+ summary: 任务失败
+ value:
+ id: task_abc123def456
+ task_id: task_abc123def456
+ object: video
+ model: doubao-seedance-2-0-260128
+ status: failed
+ progress: 100
+ created_at: 1712345678
+ completed_at: 1712345700
+ error:
+ code: content_policy_violation
+ message: 请求内容违反了内容安全策略
+ "404":
+ description: 任务不存在
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ "401":
+ description: 未授权
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+
+ /v1/videos:
+ post:
+ tags:
+ - 视频生成
+ summary: 提交视频生成任务(OpenAI 兼容格式)
+ description: |
+ 与 `POST /v1/video/generations` 功能完全相同,为兼容 OpenAI Video API 格式而提供。
+ 请求和响应格式与 `/v1/video/generations` 一致。
+ operationId: submitVideoOpenAI
+ security:
+ - BearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/VideoGenerationRequest'
+ responses:
+ "200":
+ description: 任务提交成功
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/VideoTaskResponse'
+ "400":
+ description: 请求参数错误
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ "401":
+ description: 未授权
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+
+ /v1/videos/{task_id}:
+ get:
+ tags:
+ - 视频生成
+ summary: 查询任务状态(OpenAI 兼容格式)
+ description: |
+ 与 `GET /v1/video/generations/{task_id}` 功能相同,兼容 OpenAI Video API 格式。
+ operationId: getVideoTaskOpenAI
+ security:
+ - BearerAuth: []
+ parameters:
+ - name: task_id
+ in: path
+ required: true
+ description: 任务 ID
+ schema:
+ type: string
+ example: task_abc123def456
+ responses:
+ "200":
+ description: 查询成功
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/VideoTaskResponse'
+ "404":
+ description: 任务不存在
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+
+ /v1/videos/{task_id}/content:
+ get:
+ tags:
+ - 视频内容
+ summary: 下载视频文件内容
+ description: |
+ 通过代理方式下载已完成的视频文件内容(二进制流)。
+
+ **前置条件**:任务状态必须为 `completed`,否则返回 400 错误。
+
+ 此接口会:
+ 1. 查询数据库获取任务关联的视频 URL
+ 2. 通过服务器代理请求并转发视频内容给客户端
+ 3. 响应头包含 `Cache-Control: public, max-age=86400`(24小时缓存)
+
+ **支持认证方式**:Bearer Token 或 Session Cookie(Dashboard 登录用户均可访问)
+
+ operationId: downloadVideoContent
+ security:
+ - BearerAuth: []
+ - SessionAuth: []
+ parameters:
+ - name: task_id
+ in: path
+ required: true
+ description: 任务 ID(`completed` 状态才可下载)
+ schema:
+ type: string
+ example: task_abc123def456
+ responses:
+ "200":
+ description: 视频文件内容(二进制流)
+ content:
+ video/mp4:
+ schema:
+ type: string
+ format: binary
+ video/*:
+ schema:
+ type: string
+ format: binary
+ "400":
+ description: 任务未完成
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ example:
+ error:
+ message: "Task is not completed yet, current status: in_progress"
+ type: invalid_request_error
+ "404":
+ description: 任务不存在
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ "502":
+ description: 上游视频 URL 不可用
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+
+components:
+ securitySchemes:
+ BearerAuth:
+ type: http
+ scheme: bearer
+ bearerFormat: JWT
+ description: "API Token,格式:Bearer sk-xxxx"
+ SessionAuth:
+ type: apiKey
+ in: cookie
+ name: session
+ description: Dashboard 登录后的 Session Cookie
+
+ schemas:
+ VideoGenerationRequest:
+ type: object
+ required:
+ - model
+ - prompt
+ properties:
+ model:
+ type: string
+ description: |
+ 视频生成模型名称。支持的模型:
+ - `doubao-seedance-1-0-pro-250528`
+ - `doubao-seedance-1-0-lite-t2v`(仅文生视频)
+ - `doubao-seedance-1-0-lite-i2v`(仅图生视频)
+ - `doubao-seedance-1-5-pro-251215`
+ - `doubao-seedance-2-0-260128`(推荐)
+ - `doubao-seedance-2-0-fast-260128`(更快速)
+ example: doubao-seedance-2-0-260128
+ enum:
+ - doubao-seedance-1-0-pro-250528
+ - doubao-seedance-1-0-lite-t2v
+ - doubao-seedance-1-0-lite-i2v
+ - doubao-seedance-1-5-pro-251215
+ - doubao-seedance-2-0-260128
+ - doubao-seedance-2-0-fast-260128
+ prompt:
+ type: string
+ description: 视频内容描述(文本提示词)。建议使用详细、具体的描述以获得更好的生成效果。
+ example: 一朵玫瑰花在阳光下缓缓绽放,微风轻抚花瓣,背景是模糊的绿色草坪
+ image:
+ type: string
+ description: |
+ 参考图片(图生视频时使用)。支持 HTTP/HTTPS URL 或 Base64 编码的图片数据。
+ 当同时使用 `metadata.content` 中的图片时,此字段的图片会被添加到 `content` 数组前端。
+ example: "https://example.com/reference-image.jpg"
+ seconds:
+ type: string
+ description: 视频时长(秒),以字符串形式传入。常用值为 "5" 或 "8"。
+ example: "5"
+ metadata:
+ $ref: '#/components/schemas/VideoMetadata'
+
+ VideoMetadata:
+ type: object
+ description: |
+ 豆包视频特定的扩展参数,通过 `metadata` 字段传入。
+ 所有字段均为可选,未传入时使用模型默认值。
+ properties:
+ ratio:
+ type: string
+ description: 视频宽高比
+ example: "16:9"
+ enum:
+ - "16:9"
+ - "9:16"
+ - "1:1"
+ - "4:3"
+ - "3:4"
+ resolution:
+ type: string
+ description: 输出视频分辨率
+ example: "720p"
+ enum:
+ - "480p"
+ - "720p"
+ - "1080p"
+ seed:
+ type: integer
+ description: 随机种子,固定相同种子可复现相似结果。范围:0 ~ 2147483647
+ example: 42
+ watermark:
+ type: boolean
+ description: 是否在视频上添加水印。默认为 false(不加水印)。
+ example: false
+ camera_fixed:
+ type: boolean
+ description: 是否固定摄像机(镜头不移动)。默认为 false(允许镜头运动)。
+ example: false
+ content:
+ type: array
+ description: |
+ 多模态内容数组,支持图片或视频输入。
+ **注意**:当包含 `video_url` 类型条目时,系统会自动应用视频输入折扣计费:
+ - `doubao-seedance-2-0-260128`:约 6.1 折
+ - `doubao-seedance-2-0-fast-260128`:约 5.9 折
+ items:
+ $ref: '#/components/schemas/ContentItem'
+ return_last_frame:
+ type: boolean
+ description: 是否返回视频最后一帧图片(用于续拍场景)
+ example: false
+ generate_audio:
+ type: boolean
+ description: 是否生成配套音频
+ example: false
+ draft:
+ type: boolean
+ description: 是否生成草稿(快速预览,质量较低)
+ example: false
+ service_tier:
+ type: string
+ description: 服务等级(影响优先级和价格)
+ example: default
+ callback_url:
+ type: string
+ description: 任务完成后的回调通知 URL(需配合豆包官方回调机制使用)
+ example: "https://your-server.com/webhook/video-callback"
+
+ ContentItem:
+ type: object
+ required:
+ - type
+ properties:
+ type:
+ type: string
+ description: 内容类型
+ enum:
+ - text
+ - image_url
+ - video_url
+ example: image_url
+ text:
+ type: string
+ description: 文本内容(type 为 text 时使用)
+ example: 背景是黄昏时分的海滩
+ image_url:
+ type: object
+ description: 图片 URL(type 为 image_url 时使用)
+ properties:
+ url:
+ type: string
+ description: 图片的 HTTP/HTTPS URL 或 base64 数据
+ example: "https://example.com/photo.jpg"
+ video_url:
+ type: object
+ description: 视频 URL(type 为 video_url 时使用,**会触发视频输入折扣**)
+ properties:
+ url:
+ type: string
+ description: 视频的 HTTP/HTTPS URL
+ example: "https://example.com/input-video.mp4"
+
+ VideoTaskResponse:
+ type: object
+ description: 视频任务响应(提交后和查询时返回相同结构)
+ properties:
+ id:
+ type: string
+ description: 任务唯一标识符(由 new-api 系统生成,非豆包原始 ID)
+ example: task_abc123def456
+ task_id:
+ type: string
+ description: 与 `id` 相同,兼容旧版接口(已废弃,请使用 `id`)
+ example: task_abc123def456
+ deprecated: true
+ object:
+ type: string
+ description: 对象类型,固定为 "video"
+ example: video
+ model:
+ type: string
+ description: 使用的模型名称
+ example: doubao-seedance-2-0-260128
+ status:
+ type: string
+ description: |
+ 任务当前状态:
+ - `queued`:已提交,排队等待
+ - `in_progress`:生成中
+ - `completed`:已完成,可下载视频
+ - `failed`:生成失败
+ enum:
+ - queued
+ - in_progress
+ - completed
+ - failed
+ example: completed
+ progress:
+ type: integer
+ description: 任务进度(0-100 的整数)
+ minimum: 0
+ maximum: 100
+ example: 100
+ created_at:
+ type: integer
+ format: int64
+ description: 任务创建时间(Unix 时间戳,秒)
+ example: 1712345678
+ completed_at:
+ type: integer
+ format: int64
+ description: 任务完成时间(Unix 时间戳,秒)。仅在 status 为 completed 或 failed 时返回。
+ example: 1712345900
+ expires_at:
+ type: integer
+ format: int64
+ description: 任务结果过期时间(Unix 时间戳)
+ example: 1712432300
+ metadata:
+ type: object
+ description: 任务结果元数据
+ properties:
+ url:
+ type: string
+ description: 视频文件的直接访问 URL(仅在 status 为 completed 时存在)
+ example: "https://cdn.example.com/videos/output-video.mp4"
+ error:
+ $ref: '#/components/schemas/VideoTaskError'
+ remixed_from_video_id:
+ type: string
+ description: 如果是 remix 任务,此字段为原始视频的任务 ID
+ example: task_origin789ghi012
+
+ VideoTaskError:
+ type: object
+ description: 视频任务失败时的错误详情
+ properties:
+ code:
+ type: string
+ description: 错误码
+ example: content_policy_violation
+ message:
+ type: string
+ description: 人类可读的错误描述
+ example: 请求内容违反了内容安全策略,请修改 prompt 后重试
+
+ ErrorResponse:
+ type: object
+ properties:
+ error:
+ type: object
+ properties:
+ message:
+ type: string
+ description: 错误描述信息
+ example: Invalid authentication token
+ type:
+ type: string
+ description: 错误类型
+ example: invalid_request_error
+ code:
+ type: string
+ description: 错误码(部分场景下包含)
+ example: invalid_api_key
From 4ab4aba89a2d1b49403068cf44074720418a25a0 Mon Sep 17 00:00:00 2001
From: aabao <602018325@qq.com>
Date: Wed, 8 Apr 2026 16:52:09 +0800
Subject: [PATCH 2/8] =?UTF-8?q?=E4=BC=98=E5=8C=96sora-2-pro=20=E8=AE=A1?=
=?UTF-8?q?=E8=B4=B9=EF=BC=8C=E6=94=AF=E6=8C=81=201080P?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
i18n/locales/zh-CN.yaml | 2 +-
model/user.go | 2 +-
relay/channel/task/sora/adaptor.go | 32 ++++++++++++++++++++++++++----
relay/common/relay_utils.go | 6 +++---
4 files changed, 33 insertions(+), 9 deletions(-)
diff --git a/i18n/locales/zh-CN.yaml b/i18n/locales/zh-CN.yaml
index 1f3b5a7b4bc5..d0fc9ea6b8b8 100644
--- a/i18n/locales/zh-CN.yaml
+++ b/i18n/locales/zh-CN.yaml
@@ -56,7 +56,7 @@ user.password_login_disabled: "管理员关闭了密码登录"
user.register_disabled: "管理员关闭了新用户注册"
user.password_register_disabled: "管理员关闭了通过密码进行注册,请使用第三方账户验证的形式进行注册"
user.username_or_password_empty: "用户名或密码为空"
-user.username_or_password_error: "用户名或密码错误,或用户已被封禁"
+user.username_or_password_error: "用户名或密码错误,请检查后重试"
user.email_or_password_empty: "邮箱地址或密码为空!"
user.exists: "用户名已存在,或已注销"
user.not_exists: "用户不存在"
diff --git a/model/user.go b/model/user.go
index 1210b5435d04..402023dfb84b 100644
--- a/model/user.go
+++ b/model/user.go
@@ -604,7 +604,7 @@ func (user *User) ValidateAndFill() (err error) {
DB.Where("username = ? OR email = ?", username, username).First(user)
okay := common.ValidatePasswordAndHash(password, user.Password)
if !okay || user.Status != common.UserStatusEnabled {
- return errors.New("用户名或密码错误,或用户已被封禁")
+ return errors.New("用户名或密码错误,请检查后重试")
}
return nil
}
diff --git a/relay/channel/task/sora/adaptor.go b/relay/channel/task/sora/adaptor.go
index e9029aa20d46..5c71d381a4f1 100644
--- a/relay/channel/task/sora/adaptor.go
+++ b/relay/channel/task/sora/adaptor.go
@@ -95,6 +95,16 @@ func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycom
}
// EstimateBilling 根据用户请求的 seconds 和 size 计算 OtherRatios。
+//
+// Sora 官方定价(按秒计费):
+//
+// sora-2 720p (720x1280 / 1280x720) $0.10/s
+// sora-2-pro 720p (720x1280 / 1280x720) $0.30/s → size ratio = 1.0 (sora-2-pro 基准)
+// sora-2-pro 1024p (1024x1792 / 1792x1024) $0.50/s → size ratio = 5/3 ≈ 1.6667
+// sora-2-pro 1080p (1080x1920 / 1920x1080) $0.70/s → size ratio = 7/3 ≈ 2.3333
+//
+// 管理员应分别为 sora-2 设置 $0.10/s 的基准价,为 sora-2-pro 设置 $0.30/s 的基准价(720p)。
+// 系统会自动乘以 seconds 和 size ratio 得出最终费用。
func (a *TaskAdaptor) EstimateBilling(c *gin.Context, info *relaycommon.RelayInfo) map[string]float64 {
// remix 路径的 OtherRatios 已在 ResolveOriginTask 中设置
if info.Action == constant.TaskActionRemix {
@@ -119,12 +129,26 @@ func (a *TaskAdaptor) EstimateBilling(c *gin.Context, info *relaycommon.RelayInf
size = "720x1280"
}
+ model := info.OriginModelName
+
+ // size ratio 相对于各模型自身的 720p 基准价
+ // sora-2 只支持 720p → ratio = 1.0
+ // sora-2-pro 720p → ratio = 1.0, 1024p → ratio = 5/3 ≈ 1.6667, 1080p → ratio = 7/3 ≈ 2.3333
+ sizeRatio := 1.0
+ if strings.HasPrefix(model, "sora-2-pro") {
+ switch size {
+ case "1920x1080", "1080x1920":
+ sizeRatio = 7.0 / 3.0 // $0.70 / $0.30
+ case "1792x1024", "1024x1792":
+ sizeRatio = 5.0 / 3.0 // $0.50 / $0.30
+ default: // 720x1280, 1280x720
+ sizeRatio = 1.0
+ }
+ }
+
ratios := map[string]float64{
"seconds": float64(seconds),
- "size": 1,
- }
- if size == "1792x1024" || size == "1024x1792" {
- ratios["size"] = 1.666667
+ "size": sizeRatio,
}
return ratios
}
diff --git a/relay/common/relay_utils.go b/relay/common/relay_utils.go
index 18df77a645d6..1f4f3572f3b8 100644
--- a/relay/common/relay_utils.go
+++ b/relay/common/relay_utils.go
@@ -168,10 +168,10 @@ func ValidateMultipartDirect(c *gin.Context, info *RelayInfo) *dto.TaskError {
}
if model == "sora-2" && !lo.Contains([]string{"720x1280", "1280x720"}, size) {
- return createTaskError(fmt.Errorf("sora-2 size is invalid"), "invalid_size", http.StatusBadRequest, true)
+ return createTaskError(fmt.Errorf("sora-2 only supports 720p (720x1280 or 1280x720)"), "invalid_size", http.StatusBadRequest, true)
}
- if model == "sora-2-pro" && !lo.Contains([]string{"720x1280", "1280x720", "1792x1024", "1024x1792"}, size) {
- return createTaskError(fmt.Errorf("sora-2 size is invalid"), "invalid_size", http.StatusBadRequest, true)
+ if model == "sora-2-pro" && !lo.Contains([]string{"720x1280", "1280x720", "1792x1024", "1024x1792", "1920x1080", "1080x1920"}, size) {
+ return createTaskError(fmt.Errorf("sora-2-pro only supports 720p (720x1280/1280x720), 1024p (1024x1792/1792x1024) or 1080p (1080x1920/1920x1080)"), "invalid_size", http.StatusBadRequest, true)
}
// OtherRatios 已移到 Sora adaptor 的 EstimateBilling 中设置
}
From 4d56fc9918abed40d2638808f7f19aed79b5d3e1 Mon Sep 17 00:00:00 2001
From: aabao <602018325@qq.com>
Date: Wed, 8 Apr 2026 18:44:19 +0800
Subject: [PATCH 3/8] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=B1=86=E5=8C=85?=
=?UTF-8?q?=E6=96=87=E6=A1=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/openapi/doubao-video-api.yaml | 486 ++++++++++++++++++++---------
1 file changed, 335 insertions(+), 151 deletions(-)
diff --git a/docs/openapi/doubao-video-api.yaml b/docs/openapi/doubao-video-api.yaml
index 1fa205ed8174..76ff186d53fb 100644
--- a/docs/openapi/doubao-video-api.yaml
+++ b/docs/openapi/doubao-video-api.yaml
@@ -2,38 +2,103 @@ openapi: 3.1.0
info:
title: 豆包视频生成 API
description: |
- 基于 new-api 网关的豆包视频生成接口文档。
-
- 支持的豆包视频模型(`DoubaoVideo` 渠道类型 54):
- - `doubao-seedance-1-0-pro-250528`
- - `doubao-seedance-1-0-lite-t2v`(仅文生视频)
- - `doubao-seedance-1-0-lite-i2v`(仅图生视频)
- - `doubao-seedance-1-5-pro-251215`
- - `doubao-seedance-2-0-260128`
- - `doubao-seedance-2-0-fast-260128`
-
- **计费说明**:
- - 计费单位为系统配额(Quota),由管理员为每个模型配置 `ModelRatio`(每次任务的基础价格)
- - 当请求中的 `metadata.content` 包含 `video_url` 类型的内容(即图生视频/视频续拍)时,系统会自动应用视频输入折扣:
- - `doubao-seedance-2-0-260128`:折扣比率 ≈ 0.6087(≈6折)
- - `doubao-seedance-2-0-fast-260128`:折扣比率 ≈ 0.5946(≈6折)
- - 视频生成为异步任务,提交时即预扣费,任务失败会自动退款
-
- **认证**:所有接口均需在请求头中携带 API Token,格式:`Authorization: Bearer sk-xxxx`
-
- version: "1.0.0"
+ # 豆包 Seedance 2.0 视频生成 API
+
+ 基于 new-api 网关统一接入豆包视频模型,提供标准化的视频生成与查询接口。
+
+ > 📖 **官方参数文档**:[豆包视频生成 API 参数说明](https://www.volcengine.com/docs/82379/1520758?lang=zh)
+
+ ---
+
+ ## 支持的模型
+
+ 本网关当前支持豆包 Seedance 2.0 系列视频生成模型:
+
+ | 模型名称 | 简介 | 适用场景 |
+ |---------|------|---------|
+ | `doubao-seedance-2-0-260128` | **Seedance 2.0 旗舰版**(推荐)
高质量、强动态表现、精准遵循提示词 | 商业级视频创作、高质量内容生成 |
+ | `doubao-seedance-2-0-fast-260128` | **Seedance 2.0 快速版**
生成速度更快,适合对延迟敏感的场景 | 快速预览、实时应用、批量生成 |
+
+ ---
+
+ ## 使用流程
+
+ 视频生成为**异步任务**,完整调用流程:
+
+ ```
+ 1. POST /v1/video/generations → 获得 task_id
+ 2. GET /v1/video/generations/{task_id} → 轮询状态(建议每 5 秒查询一次)
+ 3. 状态变为 completed → 从 metadata.url 获取视频地址
+ 4. GET /v1/videos/{task_id}/content → 可选:通过代理下载视频文件
+ ```
+
+ ---
+
+ ## 计费说明
+
+ - **预扣费机制**:提交任务时预扣费,任务失败自动退款
+ - **视频时长与费用相关**:时长设置越长,消耗配额越多,请谨慎设置
+ - **视频输入折扣**:当请求 `metadata.content` 中包含 `video_url` 类型内容(即视频续拍场景)时,系统自动应用折扣:
+ - `doubao-seedance-2-0-260128`:折扣约 **60.87%**(含视频 28元/千帧 vs 不含视频 46元/千帧)
+ - `doubao-seedance-2-0-fast-260128`:折扣约 **59.46%**(含视频 22元/千帧 vs 不含视频 37元/千帧)
+
+ ---
+
+ ## 认证
+
+ 所有接口均需在请求头携带 API Token:
+
+ ```
+ Authorization: Bearer sk-xxxx
+ ```
+
+ version: "2.0.0"
contact:
name: API Support
+externalDocs:
+ description: 豆包视频生成官方参数文档
+ url: https://www.volcengine.com/docs/82379/1520758?lang=zh
+
servers:
- - url: https://api.example.com
+ - url: https://your-new-api-host.com
description: new-api 网关地址(请替换为实际地址)
tags:
+ - name: 模型介绍
+ description: |
+ ## Seedance 2.0 模型系列
+
+ 豆包 Seedance 2.0 是字节跳动火山引擎推出的最新一代视频生成大模型,具备以下核心能力:
+
+ ### 核心特性
+
+ - **高质量输出**:支持 480p / 720p 分辨率输出
+ - **多模态输入**:支持纯文本(文生视频)、图片+文本(图生视频)、视频+文本(视频续拍)三种输入方式
+ - **灵活时长**:支持 4~15 秒范围内的任意整数时长,或设为 -1 由模型智能选择
+ - **多尺寸支持**:支持 16:9、9:16、1:1、4:3、3:4 等多种宽高比
+ - **可控生成**:支持固定镜头、随机种子、是否添加水印等控制参数
+
+ ### 模型对比
+
+ | 特性 | Seedance 2.0 | Seedance 2.0 Fast |
+ |------|-------------|-----------------|
+ | 生成质量 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
+ | 生成速度 | 中等 | 快速 |
+ | 支持分辨率 | 480p、720p | 480p、720p |
+ | 推荐使用场景 | 高质量商业内容 | 实时预览、批量任务 |
+
+ ### 参数限制说明
+
+ - **时长(duration)**:支持 4~15 秒整数,或 -1(模型智能选择)
+ - **分辨率(resolution)**:仅支持 `480p` 和 `720p`,**不支持 1080p**
+ - **宽高比(ratio)**:`16:9`、`9:16`、`1:1`、`4:3`、`3:4`
+
+ > 📖 更多参数细节请参考:[官方文档](https://www.volcengine.com/docs/82379/1520758?lang=zh)
- name: 视频生成
- description: 豆包视频生成任务提交与查询
+ description: 豆包视频生成任务提交与状态查询
- name: 视频内容
- description: 已完成视频的内容下载
+ description: 已完成视频的内容代理下载
paths:
/v1/video/generations:
@@ -42,15 +107,20 @@ paths:
- 视频生成
summary: 提交视频生成任务
description: |
- 向豆包视频模型提交一个视频生成任务(异步)。
+ 向豆包 Seedance 2.0 视频模型提交一个视频生成任务(**异步**)。
- 提交后立即返回任务 ID,视频不会立刻生成完毕。
- 请使用返回的 `id`(即 `task_id`)轮询 `GET /v1/video/generations/{task_id}` 查询结果。
+ 提交成功后立即返回任务 ID,视频不会即时生成完毕。
+ 请使用返回的 `id` 轮询 `GET /v1/video/generations/{task_id}` 查询任务状态。
- **支持的场景**:
- - 文生视频:仅提供 `prompt`,不传 `image`
- - 图生视频:提供 `prompt` 和 `image`(图片 URL),或在 `metadata.content` 中传入图片
- - 视频续拍/参考:在 `metadata.content` 中传入 `video_url` 类型内容(会触发视频输入折扣)
+ **支持的生成场景**:
+
+ | 场景 | 所需字段 |
+ |------|---------|
+ | 文生视频 | `model` + `prompt` |
+ | 图生视频 | `model` + `prompt` + `image`(图片URL)|
+ | 视频续拍 | `model` + `prompt` + `metadata.content`(含 `video_url`)|
+
+ > ⚠️ **计费提示**:视频时长影响费用,`duration=-1` 时由模型自动选择,实际时长可通过查询 API 的 `duration` 字段获取。
operationId: submitVideoGeneration
security:
@@ -62,49 +132,58 @@ paths:
schema:
$ref: '#/components/schemas/VideoGenerationRequest'
examples:
- 文生视频:
- summary: 基础文生视频
+ 文生视频(基础):
+ summary: 文生视频 - 智能选择时长
value:
model: doubao-seedance-2-0-260128
- prompt: 宇航员在月球表面慢慢行走,地球悬挂在黑色天空中
- seconds: "5"
+ prompt: 宇航员在月球表面慢慢行走,地球悬挂在黑色天空中,画面宏大震撼
+ seconds: "-1"
metadata:
ratio: "16:9"
+ resolution: "720p"
+ 文生视频(指定时长):
+ summary: 文生视频 - 指定 8 秒时长
+ value:
+ model: doubao-seedance-2-0-260128
+ prompt: 一朵玫瑰花在阳光下缓缓绽放,微风轻抚花瓣,背景是模糊的绿色草坪
+ seconds: "8"
+ metadata:
+ ratio: "9:16"
+ resolution: "720p"
+ watermark: false
+ seed: 42
图生视频:
summary: 图生视频(含参考图片)
value:
model: doubao-seedance-2-0-260128
- prompt: 让图片中的人物微笑并转身
- image: "https://example.com/reference.jpg"
+ prompt: 让图片中的人物微笑并缓缓转身,眼神温柔
+ image: "https://example.com/portrait.jpg"
seconds: "5"
metadata:
ratio: "16:9"
视频续拍(含视频输入折扣):
- summary: 基于已有视频片段续拍
+ summary: 视频续拍 - 基于已有视频片段续拍(约6折计费)
value:
model: doubao-seedance-2-0-260128
- prompt: 继续往前走,走向远处的山丘
+ prompt: 继续往前走,走向远处连绵起伏的山丘,镜头缓缓推进
+ seconds: "5"
metadata:
content:
- type: video_url
video_url:
- url: "https://example.com/input.mp4"
- seconds: "5"
- 高级参数:
- summary: 完整参数示例
+ url: "https://example.com/input-clip.mp4"
+ 快速版示例:
+ summary: 使用快速版模型(速度更快)
value:
- model: doubao-seedance-2-0-260128
- prompt: 一朵玫瑰花在阳光下缓缓绽放,微风吹拂
- seconds: "8"
+ model: doubao-seedance-2-0-fast-260128
+ prompt: 城市夜晚的延时摄影,车流如光河,霓虹闪烁
+ seconds: "-1"
metadata:
ratio: "16:9"
- resolution: "720p"
- seed: 42
- watermark: false
- camera_fixed: false
+ resolution: "480p"
responses:
"200":
- description: 任务提交成功,返回任务信息(此时任务仍在生成中,status 为 queued)
+ description: 任务提交成功(此时任务仍在排队,`status` 为 `queued`)
content:
application/json:
schema:
@@ -118,11 +197,15 @@ paths:
progress: 0
created_at: 1712345678
"400":
- description: 请求参数错误
+ description: 请求参数错误(如模型名称不支持、参数超出范围等)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
+ example:
+ error:
+ message: "invalid model: doubao-seedance-2-0-xxx"
+ type: invalid_request_error
"401":
description: 未授权(Token 无效或缺失)
content:
@@ -130,7 +213,7 @@ paths:
schema:
$ref: '#/components/schemas/ErrorResponse'
"402":
- description: 账户余额不足
+ description: 账户余额不足,无法完成预扣费
content:
application/json:
schema:
@@ -150,17 +233,21 @@ paths:
description: |
根据任务 ID 查询视频生成任务的当前状态和结果。
- **任务状态说明**:
- - `queued`:任务已提交,等待处理
- - `in_progress`:任务处理中
- - `completed`:任务已完成,可从 `metadata.url` 获取视频地址
- - `failed`:任务失败,查看 `error` 字段了解失败原因
+ **任务状态流转**:
+
+ ```
+ queued → in_progress → completed ✅ 可下载视频
+ ↘ failed ❌ 查看 error 字段
+ ```
- **获取视频**:
- - 任务 `completed` 后,`metadata.url` 字段包含视频的直接下载链接
- - 也可调用 `GET /v1/videos/{task_id}/content` 通过代理下载视频文件
+ **视频获取方式**(任务 `completed` 后):
+ 1. 从响应体 `metadata.url` 字段获取视频直链
+ 2. 调用 `GET /v1/videos/{task_id}/content` 通过代理下载视频文件
- **轮询建议**:每 5 秒查询一次,超时时间建议设置为 10 分钟。
+ **轮询建议**:
+ - 每 **5 秒**查询一次
+ - 总超时建议设置为 **10 分钟**
+ - Seedance 2.0 通常在 **1~3 分钟**内完成生成
operationId: getVideoGenerationTask
security:
@@ -169,20 +256,30 @@ paths:
- name: task_id
in: path
required: true
- description: 提交任务时返回的任务 ID(`id` 字段)
+ description: 提交任务时返回的任务 ID(响应体 `id` 字段)
schema:
type: string
- example: task_abc123def456
+ example: task_abc123def456
responses:
"200":
- description: 查询成功(可能处于任意状态)
+ description: 查询成功(任务可能处于任意状态)
content:
application/json:
schema:
$ref: '#/components/schemas/VideoTaskResponse'
examples:
- 处理中:
- summary: 任务进行中
+ 排队等待:
+ summary: 任务排队中
+ value:
+ id: task_abc123def456
+ task_id: task_abc123def456
+ object: video
+ model: doubao-seedance-2-0-260128
+ status: queued
+ progress: 0
+ created_at: 1712345678
+ 生成中:
+ summary: 视频生成中
value:
id: task_abc123def456
task_id: task_abc123def456
@@ -192,7 +289,7 @@ paths:
progress: 50
created_at: 1712345678
已完成:
- summary: 任务完成,视频可下载
+ summary: 生成完成,视频可下载
value:
id: task_abc123def456
task_id: task_abc123def456
@@ -203,9 +300,9 @@ paths:
created_at: 1712345678
completed_at: 1712345900
metadata:
- url: "https://cdn.example.com/videos/result.mp4"
- 失败:
- summary: 任务失败
+ url: "https://cdn.volcengineapi.com/videos/output-abc123.mp4"
+ 生成失败:
+ summary: 生成失败
value:
id: task_abc123def456
task_id: task_abc123def456
@@ -217,15 +314,15 @@ paths:
completed_at: 1712345700
error:
code: content_policy_violation
- message: 请求内容违反了内容安全策略
- "404":
- description: 任务不存在
+ message: 请求内容违反了内容安全策略,请修改 prompt 后重试
+ "401":
+ description: 未授权
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
- "401":
- description: 未授权
+ "404":
+ description: 任务不存在(task_id 无效或不属于当前用户)
content:
application/json:
schema:
@@ -237,8 +334,8 @@ paths:
- 视频生成
summary: 提交视频生成任务(OpenAI 兼容格式)
description: |
- 与 `POST /v1/video/generations` 功能完全相同,为兼容 OpenAI Video API 格式而提供。
- 请求和响应格式与 `/v1/video/generations` 一致。
+ 与 `POST /v1/video/generations` 功能完全相同,为兼容 OpenAI Video API 格式而提供的别名路由。
+ 请求和响应格式与 `/v1/video/generations` 完全一致。
operationId: submitVideoOpenAI
security:
- BearerAuth: []
@@ -274,7 +371,7 @@ paths:
- 视频生成
summary: 查询任务状态(OpenAI 兼容格式)
description: |
- 与 `GET /v1/video/generations/{task_id}` 功能相同,兼容 OpenAI Video API 格式。
+ 与 `GET /v1/video/generations/{task_id}` 功能相同,兼容 OpenAI Video API 格式的别名路由。
operationId: getVideoTaskOpenAI
security:
- BearerAuth: []
@@ -285,7 +382,7 @@ paths:
description: 任务 ID
schema:
type: string
- example: task_abc123def456
+ example: task_abc123def456
responses:
"200":
description: 查询成功
@@ -304,18 +401,18 @@ paths:
get:
tags:
- 视频内容
- summary: 下载视频文件内容
+ summary: 代理下载视频文件
description: |
- 通过代理方式下载已完成的视频文件内容(二进制流)。
+ 通过 new-api 服务器代理下载已完成的视频文件(二进制流)。
**前置条件**:任务状态必须为 `completed`,否则返回 400 错误。
- 此接口会:
- 1. 查询数据库获取任务关联的视频 URL
- 2. 通过服务器代理请求并转发视频内容给客户端
- 3. 响应头包含 `Cache-Control: public, max-age=86400`(24小时缓存)
+ **工作原理**:
+ 1. 根据 `task_id` 查询数据库获取豆包存储的视频 URL
+ 2. 通过服务器代理请求并将视频内容流式转发给客户端
+ 3. 响应头包含 `Cache-Control: public, max-age=86400`(24 小时缓存)
- **支持认证方式**:Bearer Token 或 Session Cookie(Dashboard 登录用户均可访问)
+ **认证支持**:Bearer Token 或 Dashboard Session Cookie 均可访问。
operationId: downloadVideoContent
security:
@@ -325,24 +422,21 @@ paths:
- name: task_id
in: path
required: true
- description: 任务 ID(`completed` 状态才可下载)
+ description: 任务 ID(任务必须处于 `completed` 状态)
schema:
type: string
- example: task_abc123def456
+ example: task_abc123def456
responses:
"200":
- description: 视频文件内容(二进制流)
+ description: 视频文件二进制流
content:
video/mp4:
schema:
type: string
format: binary
- video/*:
- schema:
- type: string
- format: binary
+ example: "[视频二进制数据]"
"400":
- description: 任务未完成
+ description: 任务未完成,无法下载
content:
application/json:
schema:
@@ -351,6 +445,12 @@ paths:
error:
message: "Task is not completed yet, current status: in_progress"
type: invalid_request_error
+ "401":
+ description: 未授权
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
"404":
description: 任务不存在
content:
@@ -358,7 +458,7 @@ paths:
schema:
$ref: '#/components/schemas/ErrorResponse'
"502":
- description: 上游视频 URL 不可用
+ description: 上游视频 URL 不可访问
content:
application/json:
schema:
@@ -375,7 +475,7 @@ components:
type: apiKey
in: cookie
name: session
- description: Dashboard 登录后的 Session Cookie
+ description: Dashboard 登录后的 Session Cookie(仅视频下载接口支持)
schemas:
VideoGenerationRequest:
@@ -383,51 +483,76 @@ components:
required:
- model
- prompt
+ description: 视频生成请求参数
properties:
model:
type: string
description: |
- 视频生成模型名称。支持的模型:
- - `doubao-seedance-1-0-pro-250528`
- - `doubao-seedance-1-0-lite-t2v`(仅文生视频)
- - `doubao-seedance-1-0-lite-i2v`(仅图生视频)
- - `doubao-seedance-1-5-pro-251215`
- - `doubao-seedance-2-0-260128`(推荐)
- - `doubao-seedance-2-0-fast-260128`(更快速)
+ 视频生成模型名称。
+
+ 支持的模型:
+ - `doubao-seedance-2-0-260128`:旗舰版,高质量(**推荐**)
+ - `doubao-seedance-2-0-fast-260128`:快速版,速度优先
example: doubao-seedance-2-0-260128
enum:
- - doubao-seedance-1-0-pro-250528
- - doubao-seedance-1-0-lite-t2v
- - doubao-seedance-1-0-lite-i2v
- - doubao-seedance-1-5-pro-251215
- doubao-seedance-2-0-260128
- doubao-seedance-2-0-fast-260128
prompt:
type: string
- description: 视频内容描述(文本提示词)。建议使用详细、具体的描述以获得更好的生成效果。
- example: 一朵玫瑰花在阳光下缓缓绽放,微风轻抚花瓣,背景是模糊的绿色草坪
+ description: |
+ 视频内容的文字描述(提示词)。
+
+ 建议:
+ - 使用详细、具体的描述
+ - 可包含场景、动作、风格、镜头语言等
+ - 中英文均支持
+ example: 宇航员在月球表面漫步,地球悬挂在黑色星空中,宏大震撼的画面
image:
type: string
description: |
- 参考图片(图生视频时使用)。支持 HTTP/HTTPS URL 或 Base64 编码的图片数据。
- 当同时使用 `metadata.content` 中的图片时,此字段的图片会被添加到 `content` 数组前端。
+ 参考图片(**图生视频**场景使用)。
+
+ 支持格式:
+ - HTTP/HTTPS 图片 URL
+ - Base64 编码的图片数据(格式:`data:image/jpeg;base64,...`)
+
+ 当同时在 `metadata.content` 中也传入了图片时,此字段的图片会附加在 content 数组前端。
example: "https://example.com/reference-image.jpg"
seconds:
type: string
- description: 视频时长(秒),以字符串形式传入。常用值为 "5" 或 "8"。
- example: "5"
+ description: |
+ 视频时长设置(字符串格式)。
+
+ **Seedance 2.0 & 2.0 Fast 支持两种配置方式**:
+
+ 1. **指定具体时长**:填写 4~15 范围内的任意整数(单位:秒),如 `"5"`、`"8"`、`"15"`
+ 2. **智能选择**:设置为 `"-1"`,由模型在有效范围内自主选择合适的视频长度
+
+ > ⚠️ 视频时长与计费相关,时长越长消耗配额越多,请谨慎设置。
+ > 实际生成视频的时长可通过查询任务 API 返回的 `duration` 字段获取。
+ example: "-1"
+ default: "-1"
metadata:
$ref: '#/components/schemas/VideoMetadata'
VideoMetadata:
type: object
description: |
- 豆包视频特定的扩展参数,通过 `metadata` 字段传入。
+ 豆包视频的扩展参数,通过 `metadata` 字段传入。
+
所有字段均为可选,未传入时使用模型默认值。
+
+ > 📖 详细参数说明请参考 [官方文档](https://www.volcengine.com/docs/82379/1520758?lang=zh)
properties:
ratio:
type: string
- description: 视频宽高比
+ description: |
+ 视频宽高比(输出画面比例)。
+
+ 常用场景参考:
+ - `16:9`:横屏,适合普通视频、电影感
+ - `9:16`:竖屏,适合短视频、手机端
+ - `1:1`:方形,适合社交媒体
example: "16:9"
enum:
- "16:9"
@@ -437,58 +562,91 @@ components:
- "3:4"
resolution:
type: string
- description: 输出视频分辨率
+ description: |
+ 输出视频分辨率。
+
+ **Seedance 2.0 & 2.0 Fast 分辨率限制**:
+ - ✅ `480p`:支持
+ - ✅ `720p`:支持(推荐)
+ - ❌ `1080p`:**不支持**(Seedance 2.0 系列均不支持 1080p)
+
+ > 注:1080p 仅在 Seedance 1.0 lite 的参考图(i2v)场景下支持。
example: "720p"
enum:
- "480p"
- "720p"
- - "1080p"
seed:
type: integer
- description: 随机种子,固定相同种子可复现相似结果。范围:0 ~ 2147483647
+ description: |
+ 随机种子,用于控制生成结果的随机性。
+
+ - 相同的 `seed` + 相同的 `prompt` 会产生相似(但不完全相同)的视频
+ - 有效范围:0 ~ 2,147,483,647
+ - 不传时随机生成
example: 42
+ minimum: 0
+ maximum: 2147483647
watermark:
type: boolean
- description: 是否在视频上添加水印。默认为 false(不加水印)。
+ description: |
+ 是否在视频上添加水印。
+
+ - `false`(默认):不添加水印
+ - `true`:添加豆包水印
example: false
+ default: false
camera_fixed:
type: boolean
- description: 是否固定摄像机(镜头不移动)。默认为 false(允许镜头运动)。
+ description: |
+ 是否固定摄像机(禁止镜头运动)。
+
+ - `false`(默认):允许镜头自然运动
+ - `true`:固定镜头,适合需要静态构图的场景
example: false
+ default: false
content:
type: array
description: |
多模态内容数组,支持图片或视频输入。
- **注意**:当包含 `video_url` 类型条目时,系统会自动应用视频输入折扣计费:
- - `doubao-seedance-2-0-260128`:约 6.1 折
- - `doubao-seedance-2-0-fast-260128`:约 5.9 折
+
+ **视频续拍场景**:传入 `video_url` 类型条目,系统会自动识别并应用视频输入折扣计费:
+ - `doubao-seedance-2-0-260128`:折扣约 **60.87%**
+ - `doubao-seedance-2-0-fast-260128`:折扣约 **59.46%**
+
+ **图片输入场景**:传入 `image_url` 类型条目。
+
+ 数组中同时支持 `text` 类型(覆盖顶层 `prompt`)。
items:
$ref: '#/components/schemas/ContentItem'
return_last_frame:
type: boolean
- description: 是否返回视频最后一帧图片(用于续拍场景)
+ description: 是否返回视频最后一帧图片(可用于下一段视频续拍的参考帧)
example: false
generate_audio:
type: boolean
- description: 是否生成配套音频
+ description: 是否为视频生成配套音频(实验性功能)
example: false
draft:
type: boolean
- description: 是否生成草稿(快速预览,质量较低)
+ description: 是否生成草稿版本(生成速度快,质量较低,适合快速预览)
example: false
service_tier:
type: string
- description: 服务等级(影响优先级和价格)
+ description: 服务等级(影响任务优先级)
example: default
callback_url:
type: string
- description: 任务完成后的回调通知 URL(需配合豆包官方回调机制使用)
+ format: uri
+ description: |
+ 任务完成/失败时的回调通知 URL(HTTP POST 方式推送)。
+ 需配合豆包官方回调格式,建议通过业务服务器接收。
example: "https://your-server.com/webhook/video-callback"
ContentItem:
type: object
required:
- type
+ description: 多模态内容条目(用于 `metadata.content` 数组)
properties:
type:
type: string
@@ -500,54 +658,66 @@ components:
example: image_url
text:
type: string
- description: 文本内容(type 为 text 时使用)
- example: 背景是黄昏时分的海滩
+ description: 文本内容(`type` 为 `text` 时使用,会覆盖顶层 `prompt` 字段)
+ example: 宇航员站起身,回望地球
image_url:
type: object
- description: 图片 URL(type 为 image_url 时使用)
+ description: 图片引用(`type` 为 `image_url` 时使用)
+ required:
+ - url
properties:
url:
type: string
- description: 图片的 HTTP/HTTPS URL 或 base64 数据
- example: "https://example.com/photo.jpg"
+ description: 图片的 HTTP/HTTPS URL 或 Base64 数据
+ example: "https://example.com/reference-photo.jpg"
video_url:
type: object
- description: 视频 URL(type 为 video_url 时使用,**会触发视频输入折扣**)
+ description: |
+ 视频引用(`type` 为 `video_url` 时使用)。
+
+ > ⚠️ 包含此类型条目时,系统自动应用视频输入折扣计费(约6折)。
+ required:
+ - url
properties:
url:
type: string
description: 视频的 HTTP/HTTPS URL
- example: "https://example.com/input-video.mp4"
+ example: "https://example.com/input-video-clip.mp4"
VideoTaskResponse:
type: object
- description: 视频任务响应(提交后和查询时返回相同结构)
+ description: 视频任务响应体(任务提交后和状态查询时均返回此结构)
properties:
id:
type: string
- description: 任务唯一标识符(由 new-api 系统生成,非豆包原始 ID)
+ description: 任务唯一标识符(由 new-api 系统生成)
example: task_abc123def456
task_id:
type: string
- description: 与 `id` 相同,兼容旧版接口(已废弃,请使用 `id`)
+ description: 同 `id` 字段,保持向后兼容(**已废弃**,请使用 `id`)
example: task_abc123def456
deprecated: true
object:
type: string
- description: 对象类型,固定为 "video"
+ description: 对象类型,固定返回 `"video"`
example: video
+ enum:
+ - video
model:
type: string
- description: 使用的模型名称
+ description: 实际使用的模型名称
example: doubao-seedance-2-0-260128
status:
type: string
description: |
任务当前状态:
- - `queued`:已提交,排队等待
- - `in_progress`:生成中
- - `completed`:已完成,可下载视频
- - `failed`:生成失败
+
+ | 状态 | 说明 |
+ |------|------|
+ | `queued` | 已提交,排队等待处理 |
+ | `in_progress` | 视频生成中 |
+ | `completed` | 生成完成,可从 `metadata.url` 下载视频 |
+ | `failed` | 生成失败,查看 `error` 字段了解原因 |
enum:
- queued
- in_progress
@@ -556,7 +726,7 @@ components:
example: completed
progress:
type: integer
- description: 任务进度(0-100 的整数)
+ description: 任务进度(0~100 的整数,仅供参考)
minimum: 0
maximum: 100
example: 100
@@ -568,12 +738,12 @@ components:
completed_at:
type: integer
format: int64
- description: 任务完成时间(Unix 时间戳,秒)。仅在 status 为 completed 或 failed 时返回。
+ description: 任务完成/失败时间(Unix 时间戳,秒)。仅在任务结束后返回。
example: 1712345900
expires_at:
type: integer
format: int64
- description: 任务结果过期时间(Unix 时间戳)
+ description: 任务结果链接过期时间(Unix 时间戳,秒)
example: 1712432300
metadata:
type: object
@@ -581,13 +751,18 @@ components:
properties:
url:
type: string
- description: 视频文件的直接访问 URL(仅在 status 为 completed 时存在)
- example: "https://cdn.example.com/videos/output-video.mp4"
+ description: |
+ 视频文件的直接访问 URL。
+
+ - 仅在 `status` 为 `completed` 时存在
+ - URL 有效期有限,建议及时下载或缓存
+ - 也可通过 `GET /v1/videos/{task_id}/content` 代理下载
+ example: "https://cdn.volcengineapi.com/videos/output-abc123.mp4"
error:
$ref: '#/components/schemas/VideoTaskError'
remixed_from_video_id:
type: string
- description: 如果是 remix 任务,此字段为原始视频的任务 ID
+ description: 如果是 remix(续拍)任务,此字段为原始视频的任务 ID
example: task_origin789ghi012
VideoTaskError:
@@ -596,7 +771,7 @@ components:
properties:
code:
type: string
- description: 错误码
+ description: 错误码(由豆包上游返回)
example: content_policy_violation
message:
type: string
@@ -605,9 +780,13 @@ components:
ErrorResponse:
type: object
+ description: API 错误响应(HTTP 4xx/5xx 时返回)
properties:
error:
type: object
+ required:
+ - message
+ - type
properties:
message:
type: string
@@ -617,6 +796,11 @@ components:
type: string
description: 错误类型
example: invalid_request_error
+ enum:
+ - invalid_request_error
+ - authentication_error
+ - permission_error
+ - server_error
code:
type: string
description: 错误码(部分场景下包含)
From cbe1d1d99b919d9921b5f7a601c476ea33dc1bcf Mon Sep 17 00:00:00 2001
From: aabao <602018325@qq.com>
Date: Fri, 10 Apr 2026 17:05:11 +0800
Subject: [PATCH 4/8] =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=B1=86=E5=8C=85?=
=?UTF-8?q?=E5=8E=9F=E7=94=9F=E8=B7=AF=E7=94=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
middleware/doubao_adapter.go | 237 +++++++++++++++++++++++++++++++++++
router/video-router.go | 12 ++
2 files changed, 249 insertions(+)
create mode 100644 middleware/doubao_adapter.go
diff --git a/middleware/doubao_adapter.go b/middleware/doubao_adapter.go
new file mode 100644
index 000000000000..a3d320652151
--- /dev/null
+++ b/middleware/doubao_adapter.go
@@ -0,0 +1,237 @@
+package middleware
+
+import (
+ "bytes"
+ "io"
+ "net/http"
+ "strconv"
+
+ "github.com/QuantumNous/new-api/common"
+ relayconstant "github.com/QuantumNous/new-api/relay/constant"
+ "github.com/gin-gonic/gin"
+)
+
+// doubaoContentItem mirrors the "content" array items from the Doubao native API.
+type doubaoContentItem struct {
+ Type string `json:"type,omitempty"`
+ Text string `json:"text,omitempty"`
+ ImageURL *doubaoMediaURL `json:"image_url,omitempty"`
+ VideoURL *doubaoMediaURL `json:"video_url,omitempty"`
+ AudioURL *doubaoMediaURL `json:"audio_url,omitempty"`
+}
+
+type doubaoMediaURL struct {
+ URL string `json:"url,omitempty"`
+}
+
+// doubaoNativeRequest is the native Doubao video creation request body.
+// POST /api/v3/contents/generations/tasks
+type doubaoNativeRequest struct {
+ Model string `json:"model"`
+ Content []doubaoContentItem `json:"content,omitempty"`
+ Resolution string `json:"resolution,omitempty"`
+ Ratio string `json:"ratio,omitempty"`
+ // Duration and Seed can be int or object {"value": N} from upstream.
+ // We accept any JSON here and re-pack them into metadata.
+ Duration interface{} `json:"duration,omitempty"`
+ Seed interface{} `json:"seed,omitempty"`
+ CameraFixed interface{} `json:"camera_fixed,omitempty"`
+ Watermark interface{} `json:"watermark,omitempty"`
+ // Pass-through extra fields
+ CallbackURL string `json:"callback_url,omitempty"`
+ ReturnLastFrame interface{} `json:"return_last_frame,omitempty"`
+ ServiceTier string `json:"service_tier,omitempty"`
+ ExecutionExpiresAfter interface{} `json:"execution_expires_after,omitempty"`
+ GenerateAudio interface{} `json:"generate_audio,omitempty"`
+ Draft interface{} `json:"draft,omitempty"`
+ Frames interface{} `json:"frames,omitempty"`
+ Tools interface{} `json:"tools,omitempty"`
+}
+
+// DoubaoRequestConvert converts native Doubao video API requests into new-api's
+// internal task format so they can be handled by the existing task relay pipeline.
+//
+// Supported paths:
+// POST /api/v3/contents/generations/tasks → create task
+// GET /api/v3/contents/generations/tasks/:task_id → fetch task
+func DoubaoRequestConvert() func(c *gin.Context) {
+ return func(c *gin.Context) {
+ // Determine if this is a fetch request (GET with task_id)
+ if c.Request.Method == http.MethodGet {
+ taskID := c.Param("task_id")
+ if taskID == "" {
+ abortWithOpenAiMessage(c, http.StatusBadRequest, "task_id is required")
+ return
+ }
+ // Route to internal fetch endpoint
+ c.Request.URL.Path = "/v1/video/generations/" + taskID
+ c.Set("task_id", taskID)
+ c.Set("relay_mode", relayconstant.RelayModeVideoFetchByID)
+ c.Next()
+ return
+ }
+
+ // POST: parse native Doubao request and convert to internal format
+ var nativeReq doubaoNativeRequest
+ if err := common.UnmarshalBodyReusable(c, &nativeReq); err != nil {
+ abortWithOpenAiMessage(c, http.StatusBadRequest, "invalid request body: "+err.Error())
+ return
+ }
+
+ // Extract prompt and images from the content array
+ var prompt string
+ var images []string
+
+ for _, item := range nativeReq.Content {
+ switch item.Type {
+ case "text":
+ if item.Text != "" {
+ prompt = item.Text
+ }
+ case "image_url":
+ if item.ImageURL != nil && item.ImageURL.URL != "" {
+ images = append(images, item.ImageURL.URL)
+ }
+ }
+ }
+
+ // Build metadata — carry all non-standard fields so the doubao adaptor
+ // can pick them up via taskcommon.UnmarshalMetadata.
+ metadata := make(map[string]interface{})
+
+ // Re-pack the full original content array so hasVideoInMetadata() works
+ // correctly (doubao adaptor checks metadata["content"]).
+ if len(nativeReq.Content) > 0 {
+ rawContent := make([]interface{}, len(nativeReq.Content))
+ for i, item := range nativeReq.Content {
+ m := make(map[string]interface{})
+ if item.Type != "" {
+ m["type"] = item.Type
+ }
+ if item.Text != "" {
+ m["text"] = item.Text
+ }
+ if item.ImageURL != nil {
+ m["image_url"] = map[string]interface{}{"url": item.ImageURL.URL}
+ }
+ if item.VideoURL != nil {
+ m["video_url"] = map[string]interface{}{"url": item.VideoURL.URL}
+ }
+ if item.AudioURL != nil {
+ m["audio_url"] = map[string]interface{}{"url": item.AudioURL.URL}
+ }
+ rawContent[i] = m
+ }
+ metadata["content"] = rawContent
+ }
+
+ if nativeReq.Resolution != "" {
+ metadata["resolution"] = nativeReq.Resolution
+ }
+ if nativeReq.Ratio != "" {
+ metadata["ratio"] = nativeReq.Ratio
+ }
+ if nativeReq.Duration != nil {
+ metadata["duration"] = normalizeDuration(nativeReq.Duration)
+ }
+ if nativeReq.Seed != nil {
+ metadata["seed"] = nativeReq.Seed
+ }
+ if nativeReq.CameraFixed != nil {
+ metadata["camera_fixed"] = nativeReq.CameraFixed
+ }
+ if nativeReq.Watermark != nil {
+ metadata["watermark"] = nativeReq.Watermark
+ }
+ if nativeReq.CallbackURL != "" {
+ metadata["callback_url"] = nativeReq.CallbackURL
+ }
+ if nativeReq.ReturnLastFrame != nil {
+ metadata["return_last_frame"] = nativeReq.ReturnLastFrame
+ }
+ if nativeReq.ServiceTier != "" {
+ metadata["service_tier"] = nativeReq.ServiceTier
+ }
+ if nativeReq.ExecutionExpiresAfter != nil {
+ metadata["execution_expires_after"] = nativeReq.ExecutionExpiresAfter
+ }
+ if nativeReq.GenerateAudio != nil {
+ metadata["generate_audio"] = nativeReq.GenerateAudio
+ }
+ if nativeReq.Draft != nil {
+ metadata["draft"] = nativeReq.Draft
+ }
+ if nativeReq.Frames != nil {
+ metadata["frames"] = nativeReq.Frames
+ }
+ if nativeReq.Tools != nil {
+ metadata["tools"] = nativeReq.Tools
+ }
+
+ // Build unified internal request
+ unifiedReq := map[string]interface{}{
+ "model": nativeReq.Model,
+ "prompt": prompt,
+ "metadata": metadata,
+ }
+
+ if len(images) == 1 {
+ unifiedReq["image"] = images[0]
+ } else if len(images) > 1 {
+ unifiedReq["images"] = images
+ }
+
+ // Extract seconds/duration for billing estimation
+ if nativeReq.Duration != nil {
+ if secs := extractDurationSeconds(nativeReq.Duration); secs > 0 {
+ unifiedReq["seconds"] = strconv.Itoa(secs)
+ }
+ }
+
+ jsonData, err := common.Marshal(unifiedReq)
+ if err != nil {
+ abortWithOpenAiMessage(c, http.StatusInternalServerError, "failed to marshal request: "+err.Error())
+ return
+ }
+
+ // Replace request body
+ c.Request.Body = io.NopCloser(bytes.NewBuffer(jsonData))
+ c.Set(common.KeyRequestBody, jsonData)
+
+ // Redirect to the internal video generation endpoint
+ c.Request.URL.Path = "/v1/video/generations"
+
+ c.Next()
+ }
+}
+
+// normalizeDuration converts various duration representations to a plain int.
+// Doubao native API accepts both {"value": N} and plain N.
+func normalizeDuration(v interface{}) interface{} {
+ switch d := v.(type) {
+ case float64:
+ return int(d)
+ case int:
+ return d
+ case map[string]interface{}:
+ if val, ok := d["value"]; ok {
+ return normalizeDuration(val)
+ }
+ }
+ return v
+}
+
+// extractDurationSeconds returns the integer second value from a duration field.
+func extractDurationSeconds(v interface{}) int {
+ switch d := v.(type) {
+ case float64:
+ return int(d)
+ case int:
+ return d
+ case map[string]interface{}:
+ if val, ok := d["value"]; ok {
+ return extractDurationSeconds(val)
+ }
+ }
+ return 0
+}
diff --git a/router/video-router.go b/router/video-router.go
index 461451104520..f838443e81e6 100644
--- a/router/video-router.go
+++ b/router/video-router.go
@@ -49,4 +49,16 @@ func SetVideoRouter(router *gin.Engine) {
// Maps to: /?Action=CVSync2AsyncSubmitTask&Version=2022-08-31 and /?Action=CVSync2AsyncGetResult&Version=2022-08-31
jimengOfficialGroup.POST("/", controller.RelayTask)
}
+
+ // Doubao video native API routes — allows downstream to call us using the
+ // official Doubao video API path format directly.
+ // POST /api/v3/contents/generations/tasks → create video task
+ // GET /api/v3/contents/generations/tasks/:task_id → fetch task status
+ doubaoVideoGroup := router.Group("/api/v3/contents/generations")
+ doubaoVideoGroup.Use(middleware.RouteTag("relay"))
+ doubaoVideoGroup.Use(middleware.DoubaoRequestConvert(), middleware.TokenAuth(), middleware.Distribute())
+ {
+ doubaoVideoGroup.POST("/tasks", controller.RelayTask)
+ doubaoVideoGroup.GET("/tasks/:task_id", controller.RelayTaskFetch)
+ }
}
From 0ceb0d5e46a4030f03cad659fbcb51554ad4b944 Mon Sep 17 00:00:00 2001
From: aabao <602018325@qq.com>
Date: Fri, 10 Apr 2026 17:18:44 +0800
Subject: [PATCH 5/8] =?UTF-8?q?=E6=81=A2=E5=A4=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.kilocode/setup-script | 39 ---------------------------------------
model/user.go | 2 +-
2 files changed, 1 insertion(+), 40 deletions(-)
delete mode 100644 .kilocode/setup-script
diff --git a/.kilocode/setup-script b/.kilocode/setup-script
deleted file mode 100644
index 24b867a49da4..000000000000
--- a/.kilocode/setup-script
+++ /dev/null
@@ -1,39 +0,0 @@
-#!/bin/bash
-# Kilo Code Worktree Setup Script
-# This script runs before the agent starts in a worktree (new sessions only).
-#
-# Available environment variables:
-# WORKTREE_PATH - Absolute path to the worktree directory
-# REPO_PATH - Absolute path to the main repository
-#
-# Example tasks:
-# - Copy .env files from main repo
-# - Install dependencies
-# - Run database migrations
-# - Set up local configuration
-
-set -e # Exit on error
-
-echo "Setting up worktree: $WORKTREE_PATH"
-
-# Uncomment and modify as needed:
-
-# Copy environment files
-# if [ -f "$REPO_PATH/.env" ]; then
-# cp "$REPO_PATH/.env" "$WORKTREE_PATH/.env"
-# echo "Copied .env"
-# fi
-
-# Install dependencies (Node.js)
-# if [ -f "$WORKTREE_PATH/package.json" ]; then
-# cd "$WORKTREE_PATH"
-# npm install
-# fi
-
-# Install dependencies (Python)
-# if [ -f "$WORKTREE_PATH/requirements.txt" ]; then
-# cd "$WORKTREE_PATH"
-# pip install -r requirements.txt
-# fi
-
-echo "Setup complete!"
diff --git a/model/user.go b/model/user.go
index 402023dfb84b..1210b5435d04 100644
--- a/model/user.go
+++ b/model/user.go
@@ -604,7 +604,7 @@ func (user *User) ValidateAndFill() (err error) {
DB.Where("username = ? OR email = ?", username, username).First(user)
okay := common.ValidatePasswordAndHash(password, user.Password)
if !okay || user.Status != common.UserStatusEnabled {
- return errors.New("用户名或密码错误,请检查后重试")
+ return errors.New("用户名或密码错误,或用户已被封禁")
}
return nil
}
From eb72bd29e093c5807aa85d1846c57aa18578737c Mon Sep 17 00:00:00 2001
From: aabao <602018325@qq.com>
Date: Fri, 10 Apr 2026 17:20:10 +0800
Subject: [PATCH 6/8] Update zh-CN.yaml
---
i18n/locales/zh-CN.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/i18n/locales/zh-CN.yaml b/i18n/locales/zh-CN.yaml
index d0fc9ea6b8b8..1f3b5a7b4bc5 100644
--- a/i18n/locales/zh-CN.yaml
+++ b/i18n/locales/zh-CN.yaml
@@ -56,7 +56,7 @@ user.password_login_disabled: "管理员关闭了密码登录"
user.register_disabled: "管理员关闭了新用户注册"
user.password_register_disabled: "管理员关闭了通过密码进行注册,请使用第三方账户验证的形式进行注册"
user.username_or_password_empty: "用户名或密码为空"
-user.username_or_password_error: "用户名或密码错误,请检查后重试"
+user.username_or_password_error: "用户名或密码错误,或用户已被封禁"
user.email_or_password_empty: "邮箱地址或密码为空!"
user.exists: "用户名已存在,或已注销"
user.not_exists: "用户不存在"
From f84ac1846554be9677d5394befa1e4fb7afb932f Mon Sep 17 00:00:00 2001
From: aabao <602018325@qq.com>
Date: Fri, 10 Apr 2026 17:20:55 +0800
Subject: [PATCH 7/8] Delete doubao-video-api.yaml
---
docs/openapi/doubao-video-api.yaml | 807 -----------------------------
1 file changed, 807 deletions(-)
delete mode 100644 docs/openapi/doubao-video-api.yaml
diff --git a/docs/openapi/doubao-video-api.yaml b/docs/openapi/doubao-video-api.yaml
deleted file mode 100644
index 76ff186d53fb..000000000000
--- a/docs/openapi/doubao-video-api.yaml
+++ /dev/null
@@ -1,807 +0,0 @@
-openapi: 3.1.0
-info:
- title: 豆包视频生成 API
- description: |
- # 豆包 Seedance 2.0 视频生成 API
-
- 基于 new-api 网关统一接入豆包视频模型,提供标准化的视频生成与查询接口。
-
- > 📖 **官方参数文档**:[豆包视频生成 API 参数说明](https://www.volcengine.com/docs/82379/1520758?lang=zh)
-
- ---
-
- ## 支持的模型
-
- 本网关当前支持豆包 Seedance 2.0 系列视频生成模型:
-
- | 模型名称 | 简介 | 适用场景 |
- |---------|------|---------|
- | `doubao-seedance-2-0-260128` | **Seedance 2.0 旗舰版**(推荐)
高质量、强动态表现、精准遵循提示词 | 商业级视频创作、高质量内容生成 |
- | `doubao-seedance-2-0-fast-260128` | **Seedance 2.0 快速版**
生成速度更快,适合对延迟敏感的场景 | 快速预览、实时应用、批量生成 |
-
- ---
-
- ## 使用流程
-
- 视频生成为**异步任务**,完整调用流程:
-
- ```
- 1. POST /v1/video/generations → 获得 task_id
- 2. GET /v1/video/generations/{task_id} → 轮询状态(建议每 5 秒查询一次)
- 3. 状态变为 completed → 从 metadata.url 获取视频地址
- 4. GET /v1/videos/{task_id}/content → 可选:通过代理下载视频文件
- ```
-
- ---
-
- ## 计费说明
-
- - **预扣费机制**:提交任务时预扣费,任务失败自动退款
- - **视频时长与费用相关**:时长设置越长,消耗配额越多,请谨慎设置
- - **视频输入折扣**:当请求 `metadata.content` 中包含 `video_url` 类型内容(即视频续拍场景)时,系统自动应用折扣:
- - `doubao-seedance-2-0-260128`:折扣约 **60.87%**(含视频 28元/千帧 vs 不含视频 46元/千帧)
- - `doubao-seedance-2-0-fast-260128`:折扣约 **59.46%**(含视频 22元/千帧 vs 不含视频 37元/千帧)
-
- ---
-
- ## 认证
-
- 所有接口均需在请求头携带 API Token:
-
- ```
- Authorization: Bearer sk-xxxx
- ```
-
- version: "2.0.0"
- contact:
- name: API Support
-
-externalDocs:
- description: 豆包视频生成官方参数文档
- url: https://www.volcengine.com/docs/82379/1520758?lang=zh
-
-servers:
- - url: https://your-new-api-host.com
- description: new-api 网关地址(请替换为实际地址)
-
-tags:
- - name: 模型介绍
- description: |
- ## Seedance 2.0 模型系列
-
- 豆包 Seedance 2.0 是字节跳动火山引擎推出的最新一代视频生成大模型,具备以下核心能力:
-
- ### 核心特性
-
- - **高质量输出**:支持 480p / 720p 分辨率输出
- - **多模态输入**:支持纯文本(文生视频)、图片+文本(图生视频)、视频+文本(视频续拍)三种输入方式
- - **灵活时长**:支持 4~15 秒范围内的任意整数时长,或设为 -1 由模型智能选择
- - **多尺寸支持**:支持 16:9、9:16、1:1、4:3、3:4 等多种宽高比
- - **可控生成**:支持固定镜头、随机种子、是否添加水印等控制参数
-
- ### 模型对比
-
- | 特性 | Seedance 2.0 | Seedance 2.0 Fast |
- |------|-------------|-----------------|
- | 生成质量 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
- | 生成速度 | 中等 | 快速 |
- | 支持分辨率 | 480p、720p | 480p、720p |
- | 推荐使用场景 | 高质量商业内容 | 实时预览、批量任务 |
-
- ### 参数限制说明
-
- - **时长(duration)**:支持 4~15 秒整数,或 -1(模型智能选择)
- - **分辨率(resolution)**:仅支持 `480p` 和 `720p`,**不支持 1080p**
- - **宽高比(ratio)**:`16:9`、`9:16`、`1:1`、`4:3`、`3:4`
-
- > 📖 更多参数细节请参考:[官方文档](https://www.volcengine.com/docs/82379/1520758?lang=zh)
- - name: 视频生成
- description: 豆包视频生成任务提交与状态查询
- - name: 视频内容
- description: 已完成视频的内容代理下载
-
-paths:
- /v1/video/generations:
- post:
- tags:
- - 视频生成
- summary: 提交视频生成任务
- description: |
- 向豆包 Seedance 2.0 视频模型提交一个视频生成任务(**异步**)。
-
- 提交成功后立即返回任务 ID,视频不会即时生成完毕。
- 请使用返回的 `id` 轮询 `GET /v1/video/generations/{task_id}` 查询任务状态。
-
- **支持的生成场景**:
-
- | 场景 | 所需字段 |
- |------|---------|
- | 文生视频 | `model` + `prompt` |
- | 图生视频 | `model` + `prompt` + `image`(图片URL)|
- | 视频续拍 | `model` + `prompt` + `metadata.content`(含 `video_url`)|
-
- > ⚠️ **计费提示**:视频时长影响费用,`duration=-1` 时由模型自动选择,实际时长可通过查询 API 的 `duration` 字段获取。
-
- operationId: submitVideoGeneration
- security:
- - BearerAuth: []
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/VideoGenerationRequest'
- examples:
- 文生视频(基础):
- summary: 文生视频 - 智能选择时长
- value:
- model: doubao-seedance-2-0-260128
- prompt: 宇航员在月球表面慢慢行走,地球悬挂在黑色天空中,画面宏大震撼
- seconds: "-1"
- metadata:
- ratio: "16:9"
- resolution: "720p"
- 文生视频(指定时长):
- summary: 文生视频 - 指定 8 秒时长
- value:
- model: doubao-seedance-2-0-260128
- prompt: 一朵玫瑰花在阳光下缓缓绽放,微风轻抚花瓣,背景是模糊的绿色草坪
- seconds: "8"
- metadata:
- ratio: "9:16"
- resolution: "720p"
- watermark: false
- seed: 42
- 图生视频:
- summary: 图生视频(含参考图片)
- value:
- model: doubao-seedance-2-0-260128
- prompt: 让图片中的人物微笑并缓缓转身,眼神温柔
- image: "https://example.com/portrait.jpg"
- seconds: "5"
- metadata:
- ratio: "16:9"
- 视频续拍(含视频输入折扣):
- summary: 视频续拍 - 基于已有视频片段续拍(约6折计费)
- value:
- model: doubao-seedance-2-0-260128
- prompt: 继续往前走,走向远处连绵起伏的山丘,镜头缓缓推进
- seconds: "5"
- metadata:
- content:
- - type: video_url
- video_url:
- url: "https://example.com/input-clip.mp4"
- 快速版示例:
- summary: 使用快速版模型(速度更快)
- value:
- model: doubao-seedance-2-0-fast-260128
- prompt: 城市夜晚的延时摄影,车流如光河,霓虹闪烁
- seconds: "-1"
- metadata:
- ratio: "16:9"
- resolution: "480p"
- responses:
- "200":
- description: 任务提交成功(此时任务仍在排队,`status` 为 `queued`)
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/VideoTaskResponse'
- example:
- id: task_abc123def456
- task_id: task_abc123def456
- object: video
- model: doubao-seedance-2-0-260128
- status: queued
- progress: 0
- created_at: 1712345678
- "400":
- description: 请求参数错误(如模型名称不支持、参数超出范围等)
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ErrorResponse'
- example:
- error:
- message: "invalid model: doubao-seedance-2-0-xxx"
- type: invalid_request_error
- "401":
- description: 未授权(Token 无效或缺失)
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ErrorResponse'
- "402":
- description: 账户余额不足,无法完成预扣费
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ErrorResponse'
- "500":
- description: 服务器内部错误
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ErrorResponse'
-
- /v1/video/generations/{task_id}:
- get:
- tags:
- - 视频生成
- summary: 查询视频生成任务状态
- description: |
- 根据任务 ID 查询视频生成任务的当前状态和结果。
-
- **任务状态流转**:
-
- ```
- queued → in_progress → completed ✅ 可下载视频
- ↘ failed ❌ 查看 error 字段
- ```
-
- **视频获取方式**(任务 `completed` 后):
- 1. 从响应体 `metadata.url` 字段获取视频直链
- 2. 调用 `GET /v1/videos/{task_id}/content` 通过代理下载视频文件
-
- **轮询建议**:
- - 每 **5 秒**查询一次
- - 总超时建议设置为 **10 分钟**
- - Seedance 2.0 通常在 **1~3 分钟**内完成生成
-
- operationId: getVideoGenerationTask
- security:
- - BearerAuth: []
- parameters:
- - name: task_id
- in: path
- required: true
- description: 提交任务时返回的任务 ID(响应体 `id` 字段)
- schema:
- type: string
- example: task_abc123def456
- responses:
- "200":
- description: 查询成功(任务可能处于任意状态)
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/VideoTaskResponse'
- examples:
- 排队等待:
- summary: 任务排队中
- value:
- id: task_abc123def456
- task_id: task_abc123def456
- object: video
- model: doubao-seedance-2-0-260128
- status: queued
- progress: 0
- created_at: 1712345678
- 生成中:
- summary: 视频生成中
- value:
- id: task_abc123def456
- task_id: task_abc123def456
- object: video
- model: doubao-seedance-2-0-260128
- status: in_progress
- progress: 50
- created_at: 1712345678
- 已完成:
- summary: 生成完成,视频可下载
- value:
- id: task_abc123def456
- task_id: task_abc123def456
- object: video
- model: doubao-seedance-2-0-260128
- status: completed
- progress: 100
- created_at: 1712345678
- completed_at: 1712345900
- metadata:
- url: "https://cdn.volcengineapi.com/videos/output-abc123.mp4"
- 生成失败:
- summary: 生成失败
- value:
- id: task_abc123def456
- task_id: task_abc123def456
- object: video
- model: doubao-seedance-2-0-260128
- status: failed
- progress: 100
- created_at: 1712345678
- completed_at: 1712345700
- error:
- code: content_policy_violation
- message: 请求内容违反了内容安全策略,请修改 prompt 后重试
- "401":
- description: 未授权
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ErrorResponse'
- "404":
- description: 任务不存在(task_id 无效或不属于当前用户)
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ErrorResponse'
-
- /v1/videos:
- post:
- tags:
- - 视频生成
- summary: 提交视频生成任务(OpenAI 兼容格式)
- description: |
- 与 `POST /v1/video/generations` 功能完全相同,为兼容 OpenAI Video API 格式而提供的别名路由。
- 请求和响应格式与 `/v1/video/generations` 完全一致。
- operationId: submitVideoOpenAI
- security:
- - BearerAuth: []
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/VideoGenerationRequest'
- responses:
- "200":
- description: 任务提交成功
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/VideoTaskResponse'
- "400":
- description: 请求参数错误
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ErrorResponse'
- "401":
- description: 未授权
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ErrorResponse'
-
- /v1/videos/{task_id}:
- get:
- tags:
- - 视频生成
- summary: 查询任务状态(OpenAI 兼容格式)
- description: |
- 与 `GET /v1/video/generations/{task_id}` 功能相同,兼容 OpenAI Video API 格式的别名路由。
- operationId: getVideoTaskOpenAI
- security:
- - BearerAuth: []
- parameters:
- - name: task_id
- in: path
- required: true
- description: 任务 ID
- schema:
- type: string
- example: task_abc123def456
- responses:
- "200":
- description: 查询成功
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/VideoTaskResponse'
- "404":
- description: 任务不存在
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ErrorResponse'
-
- /v1/videos/{task_id}/content:
- get:
- tags:
- - 视频内容
- summary: 代理下载视频文件
- description: |
- 通过 new-api 服务器代理下载已完成的视频文件(二进制流)。
-
- **前置条件**:任务状态必须为 `completed`,否则返回 400 错误。
-
- **工作原理**:
- 1. 根据 `task_id` 查询数据库获取豆包存储的视频 URL
- 2. 通过服务器代理请求并将视频内容流式转发给客户端
- 3. 响应头包含 `Cache-Control: public, max-age=86400`(24 小时缓存)
-
- **认证支持**:Bearer Token 或 Dashboard Session Cookie 均可访问。
-
- operationId: downloadVideoContent
- security:
- - BearerAuth: []
- - SessionAuth: []
- parameters:
- - name: task_id
- in: path
- required: true
- description: 任务 ID(任务必须处于 `completed` 状态)
- schema:
- type: string
- example: task_abc123def456
- responses:
- "200":
- description: 视频文件二进制流
- content:
- video/mp4:
- schema:
- type: string
- format: binary
- example: "[视频二进制数据]"
- "400":
- description: 任务未完成,无法下载
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ErrorResponse'
- example:
- error:
- message: "Task is not completed yet, current status: in_progress"
- type: invalid_request_error
- "401":
- description: 未授权
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ErrorResponse'
- "404":
- description: 任务不存在
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ErrorResponse'
- "502":
- description: 上游视频 URL 不可访问
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ErrorResponse'
-
-components:
- securitySchemes:
- BearerAuth:
- type: http
- scheme: bearer
- bearerFormat: JWT
- description: "API Token,格式:Bearer sk-xxxx"
- SessionAuth:
- type: apiKey
- in: cookie
- name: session
- description: Dashboard 登录后的 Session Cookie(仅视频下载接口支持)
-
- schemas:
- VideoGenerationRequest:
- type: object
- required:
- - model
- - prompt
- description: 视频生成请求参数
- properties:
- model:
- type: string
- description: |
- 视频生成模型名称。
-
- 支持的模型:
- - `doubao-seedance-2-0-260128`:旗舰版,高质量(**推荐**)
- - `doubao-seedance-2-0-fast-260128`:快速版,速度优先
- example: doubao-seedance-2-0-260128
- enum:
- - doubao-seedance-2-0-260128
- - doubao-seedance-2-0-fast-260128
- prompt:
- type: string
- description: |
- 视频内容的文字描述(提示词)。
-
- 建议:
- - 使用详细、具体的描述
- - 可包含场景、动作、风格、镜头语言等
- - 中英文均支持
- example: 宇航员在月球表面漫步,地球悬挂在黑色星空中,宏大震撼的画面
- image:
- type: string
- description: |
- 参考图片(**图生视频**场景使用)。
-
- 支持格式:
- - HTTP/HTTPS 图片 URL
- - Base64 编码的图片数据(格式:`data:image/jpeg;base64,...`)
-
- 当同时在 `metadata.content` 中也传入了图片时,此字段的图片会附加在 content 数组前端。
- example: "https://example.com/reference-image.jpg"
- seconds:
- type: string
- description: |
- 视频时长设置(字符串格式)。
-
- **Seedance 2.0 & 2.0 Fast 支持两种配置方式**:
-
- 1. **指定具体时长**:填写 4~15 范围内的任意整数(单位:秒),如 `"5"`、`"8"`、`"15"`
- 2. **智能选择**:设置为 `"-1"`,由模型在有效范围内自主选择合适的视频长度
-
- > ⚠️ 视频时长与计费相关,时长越长消耗配额越多,请谨慎设置。
- > 实际生成视频的时长可通过查询任务 API 返回的 `duration` 字段获取。
- example: "-1"
- default: "-1"
- metadata:
- $ref: '#/components/schemas/VideoMetadata'
-
- VideoMetadata:
- type: object
- description: |
- 豆包视频的扩展参数,通过 `metadata` 字段传入。
-
- 所有字段均为可选,未传入时使用模型默认值。
-
- > 📖 详细参数说明请参考 [官方文档](https://www.volcengine.com/docs/82379/1520758?lang=zh)
- properties:
- ratio:
- type: string
- description: |
- 视频宽高比(输出画面比例)。
-
- 常用场景参考:
- - `16:9`:横屏,适合普通视频、电影感
- - `9:16`:竖屏,适合短视频、手机端
- - `1:1`:方形,适合社交媒体
- example: "16:9"
- enum:
- - "16:9"
- - "9:16"
- - "1:1"
- - "4:3"
- - "3:4"
- resolution:
- type: string
- description: |
- 输出视频分辨率。
-
- **Seedance 2.0 & 2.0 Fast 分辨率限制**:
- - ✅ `480p`:支持
- - ✅ `720p`:支持(推荐)
- - ❌ `1080p`:**不支持**(Seedance 2.0 系列均不支持 1080p)
-
- > 注:1080p 仅在 Seedance 1.0 lite 的参考图(i2v)场景下支持。
- example: "720p"
- enum:
- - "480p"
- - "720p"
- seed:
- type: integer
- description: |
- 随机种子,用于控制生成结果的随机性。
-
- - 相同的 `seed` + 相同的 `prompt` 会产生相似(但不完全相同)的视频
- - 有效范围:0 ~ 2,147,483,647
- - 不传时随机生成
- example: 42
- minimum: 0
- maximum: 2147483647
- watermark:
- type: boolean
- description: |
- 是否在视频上添加水印。
-
- - `false`(默认):不添加水印
- - `true`:添加豆包水印
- example: false
- default: false
- camera_fixed:
- type: boolean
- description: |
- 是否固定摄像机(禁止镜头运动)。
-
- - `false`(默认):允许镜头自然运动
- - `true`:固定镜头,适合需要静态构图的场景
- example: false
- default: false
- content:
- type: array
- description: |
- 多模态内容数组,支持图片或视频输入。
-
- **视频续拍场景**:传入 `video_url` 类型条目,系统会自动识别并应用视频输入折扣计费:
- - `doubao-seedance-2-0-260128`:折扣约 **60.87%**
- - `doubao-seedance-2-0-fast-260128`:折扣约 **59.46%**
-
- **图片输入场景**:传入 `image_url` 类型条目。
-
- 数组中同时支持 `text` 类型(覆盖顶层 `prompt`)。
- items:
- $ref: '#/components/schemas/ContentItem'
- return_last_frame:
- type: boolean
- description: 是否返回视频最后一帧图片(可用于下一段视频续拍的参考帧)
- example: false
- generate_audio:
- type: boolean
- description: 是否为视频生成配套音频(实验性功能)
- example: false
- draft:
- type: boolean
- description: 是否生成草稿版本(生成速度快,质量较低,适合快速预览)
- example: false
- service_tier:
- type: string
- description: 服务等级(影响任务优先级)
- example: default
- callback_url:
- type: string
- format: uri
- description: |
- 任务完成/失败时的回调通知 URL(HTTP POST 方式推送)。
- 需配合豆包官方回调格式,建议通过业务服务器接收。
- example: "https://your-server.com/webhook/video-callback"
-
- ContentItem:
- type: object
- required:
- - type
- description: 多模态内容条目(用于 `metadata.content` 数组)
- properties:
- type:
- type: string
- description: 内容类型
- enum:
- - text
- - image_url
- - video_url
- example: image_url
- text:
- type: string
- description: 文本内容(`type` 为 `text` 时使用,会覆盖顶层 `prompt` 字段)
- example: 宇航员站起身,回望地球
- image_url:
- type: object
- description: 图片引用(`type` 为 `image_url` 时使用)
- required:
- - url
- properties:
- url:
- type: string
- description: 图片的 HTTP/HTTPS URL 或 Base64 数据
- example: "https://example.com/reference-photo.jpg"
- video_url:
- type: object
- description: |
- 视频引用(`type` 为 `video_url` 时使用)。
-
- > ⚠️ 包含此类型条目时,系统自动应用视频输入折扣计费(约6折)。
- required:
- - url
- properties:
- url:
- type: string
- description: 视频的 HTTP/HTTPS URL
- example: "https://example.com/input-video-clip.mp4"
-
- VideoTaskResponse:
- type: object
- description: 视频任务响应体(任务提交后和状态查询时均返回此结构)
- properties:
- id:
- type: string
- description: 任务唯一标识符(由 new-api 系统生成)
- example: task_abc123def456
- task_id:
- type: string
- description: 同 `id` 字段,保持向后兼容(**已废弃**,请使用 `id`)
- example: task_abc123def456
- deprecated: true
- object:
- type: string
- description: 对象类型,固定返回 `"video"`
- example: video
- enum:
- - video
- model:
- type: string
- description: 实际使用的模型名称
- example: doubao-seedance-2-0-260128
- status:
- type: string
- description: |
- 任务当前状态:
-
- | 状态 | 说明 |
- |------|------|
- | `queued` | 已提交,排队等待处理 |
- | `in_progress` | 视频生成中 |
- | `completed` | 生成完成,可从 `metadata.url` 下载视频 |
- | `failed` | 生成失败,查看 `error` 字段了解原因 |
- enum:
- - queued
- - in_progress
- - completed
- - failed
- example: completed
- progress:
- type: integer
- description: 任务进度(0~100 的整数,仅供参考)
- minimum: 0
- maximum: 100
- example: 100
- created_at:
- type: integer
- format: int64
- description: 任务创建时间(Unix 时间戳,秒)
- example: 1712345678
- completed_at:
- type: integer
- format: int64
- description: 任务完成/失败时间(Unix 时间戳,秒)。仅在任务结束后返回。
- example: 1712345900
- expires_at:
- type: integer
- format: int64
- description: 任务结果链接过期时间(Unix 时间戳,秒)
- example: 1712432300
- metadata:
- type: object
- description: 任务结果元数据
- properties:
- url:
- type: string
- description: |
- 视频文件的直接访问 URL。
-
- - 仅在 `status` 为 `completed` 时存在
- - URL 有效期有限,建议及时下载或缓存
- - 也可通过 `GET /v1/videos/{task_id}/content` 代理下载
- example: "https://cdn.volcengineapi.com/videos/output-abc123.mp4"
- error:
- $ref: '#/components/schemas/VideoTaskError'
- remixed_from_video_id:
- type: string
- description: 如果是 remix(续拍)任务,此字段为原始视频的任务 ID
- example: task_origin789ghi012
-
- VideoTaskError:
- type: object
- description: 视频任务失败时的错误详情
- properties:
- code:
- type: string
- description: 错误码(由豆包上游返回)
- example: content_policy_violation
- message:
- type: string
- description: 人类可读的错误描述
- example: 请求内容违反了内容安全策略,请修改 prompt 后重试
-
- ErrorResponse:
- type: object
- description: API 错误响应(HTTP 4xx/5xx 时返回)
- properties:
- error:
- type: object
- required:
- - message
- - type
- properties:
- message:
- type: string
- description: 错误描述信息
- example: Invalid authentication token
- type:
- type: string
- description: 错误类型
- example: invalid_request_error
- enum:
- - invalid_request_error
- - authentication_error
- - permission_error
- - server_error
- code:
- type: string
- description: 错误码(部分场景下包含)
- example: invalid_api_key
From fa42bcc32be621155052a601826514b41fdae5c0 Mon Sep 17 00:00:00 2001
From: aabao <602018325@qq.com>
Date: Fri, 10 Apr 2026 20:36:19 +0800
Subject: [PATCH 8/8] =?UTF-8?q?=E5=85=BC=E5=AE=B9=E8=B1=86=E5=8C=85/api/v3?=
=?UTF-8?q?/=E8=B7=AF=E7=94=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
middleware/doubao_adapter.go | 18 +++++--
relay/channel/adapter.go | 8 +++
relay/channel/task/doubao/adaptor.go | 74 ++++++++++++++++++++++++++++
relay/relay_task.go | 27 ++++++++++
4 files changed, 122 insertions(+), 5 deletions(-)
diff --git a/middleware/doubao_adapter.go b/middleware/doubao_adapter.go
index a3d320652151..05a000287084 100644
--- a/middleware/doubao_adapter.go
+++ b/middleware/doubao_adapter.go
@@ -5,6 +5,7 @@ import (
"io"
"net/http"
"strconv"
+ "strings"
"github.com/QuantumNous/new-api/common"
relayconstant "github.com/QuantumNous/new-api/relay/constant"
@@ -63,10 +64,13 @@ func DoubaoRequestConvert() func(c *gin.Context) {
abortWithOpenAiMessage(c, http.StatusBadRequest, "task_id is required")
return
}
- // Route to internal fetch endpoint
+ // Route to internal fetch endpoint.
+ // Set doubao_native_route so that videoFetchByIDRespBodyBuilder
+ // returns Doubao-native format, which downstream ParseTaskResult can parse.
c.Request.URL.Path = "/v1/video/generations/" + taskID
c.Set("task_id", taskID)
c.Set("relay_mode", relayconstant.RelayModeVideoFetchByID)
+ c.Set("doubao_native_route", true)
c.Next()
return
}
@@ -79,14 +83,14 @@ func DoubaoRequestConvert() func(c *gin.Context) {
}
// Extract prompt and images from the content array
- var prompt string
+ var promptParts []string
var images []string
for _, item := range nativeReq.Content {
switch item.Type {
case "text":
- if item.Text != "" {
- prompt = item.Text
+ if t := strings.TrimSpace(item.Text); t != "" {
+ promptParts = append(promptParts, t)
}
case "image_url":
if item.ImageURL != nil && item.ImageURL.URL != "" {
@@ -94,6 +98,7 @@ func DoubaoRequestConvert() func(c *gin.Context) {
}
}
}
+ prompt := strings.Join(promptParts, "\n")
// Build metadata — carry all non-standard fields so the doubao adaptor
// can pick them up via taskcommon.UnmarshalMetadata.
@@ -194,9 +199,12 @@ func DoubaoRequestConvert() func(c *gin.Context) {
return
}
- // Replace request body
+ // Replace request body — also clear the BodyStorage cache so that
+ // downstream handlers (e.g. ValidateBasicTaskRequest) read the new body
+ // instead of the cached original Doubao native payload.
c.Request.Body = io.NopCloser(bytes.NewBuffer(jsonData))
c.Set(common.KeyRequestBody, jsonData)
+ c.Set(common.KeyBodyStorage, nil)
// Redirect to the internal video generation endpoint
c.Request.URL.Path = "/v1/video/generations"
diff --git a/relay/channel/adapter.go b/relay/channel/adapter.go
index d2f7c6bb6d5a..1158c4925f77 100644
--- a/relay/channel/adapter.go
+++ b/relay/channel/adapter.go
@@ -81,3 +81,11 @@ type TaskAdaptor interface {
type OpenAIVideoConverter interface {
ConvertToOpenAIVideo(originTask *model.Task) ([]byte, error)
}
+
+// DoubaoNativeResponseConverter is implemented by adaptors that can convert an
+// internal Task record back into the Doubao-native API response format, so that
+// a downstream new-api instance using the "doubao-video" channel type can parse
+// the polling response with its own ParseTaskResult method.
+type DoubaoNativeResponseConverter interface {
+ ConvertToDoubaoNativeResponse(originTask *model.Task) ([]byte, error)
+}
diff --git a/relay/channel/task/doubao/adaptor.go b/relay/channel/task/doubao/adaptor.go
index a6dabb5f1086..c51d2a57e073 100644
--- a/relay/channel/task/doubao/adaptor.go
+++ b/relay/channel/task/doubao/adaptor.go
@@ -366,3 +366,77 @@ func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) ([]byte, erro
return common.Marshal(openAIVideo)
}
+
+// ConvertToDoubaoNativeResponse converts an internal Task record back to the
+// Doubao-native API response format. This is used when a downstream new-api
+// instance (configured with the "doubao-video" channel type pointing to us)
+// polls for task status via GET /api/v3/contents/generations/tasks/:task_id.
+// The downstream's ParseTaskResult expects this exact format.
+func (a *TaskAdaptor) ConvertToDoubaoNativeResponse(originTask *model.Task) ([]byte, error) {
+ // Try to reconstruct from stored upstream response first
+ var dResp responseTask
+ if len(originTask.Data) > 0 {
+ _ = common.Unmarshal(originTask.Data, &dResp)
+ }
+
+ // If the upstream task_id is stored, use it; otherwise fall back to public ID
+ upstreamID := originTask.GetUpstreamTaskID()
+ if upstreamID == "" {
+ upstreamID = originTask.TaskID
+ }
+
+ // Map internal status back to Doubao native status strings
+ doubaoStatus := internalStatusToDoubaoStatus(originTask.Status)
+
+ // Build a Doubao-native response payload
+ native := map[string]interface{}{
+ "id": upstreamID,
+ "model": originTask.Properties.OriginModelName,
+ "status": doubaoStatus,
+ }
+
+ if doubaoStatus == "succeeded" {
+ videoURL := originTask.GetResultURL()
+ if videoURL == "" {
+ videoURL = dResp.Content.VideoURL
+ }
+ native["content"] = map[string]interface{}{
+ "video_url": videoURL,
+ }
+ }
+
+ if doubaoStatus == "failed" {
+ native["error"] = map[string]interface{}{
+ "code": dResp.Error.Code,
+ "message": dResp.Error.Message,
+ }
+ }
+
+ if dResp.Usage.TotalTokens > 0 {
+ native["usage"] = dResp.Usage
+ }
+ if originTask.CreatedAt > 0 {
+ native["created_at"] = originTask.CreatedAt
+ }
+ if originTask.UpdatedAt > 0 {
+ native["updated_at"] = originTask.UpdatedAt
+ }
+
+ return common.Marshal(native)
+}
+
+// internalStatusToDoubaoStatus maps internal task status to Doubao native status strings.
+func internalStatusToDoubaoStatus(status model.TaskStatus) string {
+ switch status {
+ case model.TaskStatusQueued, model.TaskStatusSubmitted, model.TaskStatusNotStart:
+ return "pending"
+ case model.TaskStatusInProgress:
+ return "processing"
+ case model.TaskStatusSuccess:
+ return "succeeded"
+ case model.TaskStatusFailure:
+ return "failed"
+ default:
+ return "pending"
+ }
+}
diff --git a/relay/relay_task.go b/relay/relay_task.go
index 098e23828b6c..4f422c619cbb 100644
--- a/relay/relay_task.go
+++ b/relay/relay_task.go
@@ -384,6 +384,33 @@ func videoFetchByIDRespBodyBuilder(c *gin.Context) (respBody []byte, taskResp *d
return
}
+ // Doubao native route: return Doubao-native format so downstream new-api
+ // instances (using the "doubao-video" channel type) can parse the polling
+ // response with their own ParseTaskResult method.
+ if c.GetBool("doubao_native_route") {
+ adaptor := GetTaskAdaptor(originTask.Platform)
+ if adaptor != nil {
+ if converter, ok := adaptor.(channel.DoubaoNativeResponseConverter); ok {
+ nativeData, convertErr := converter.ConvertToDoubaoNativeResponse(originTask)
+ if convertErr != nil {
+ taskResp = service.TaskErrorWrapper(convertErr, "convert_to_doubao_native_failed", http.StatusInternalServerError)
+ return
+ }
+ respBody = nativeData
+ return
+ }
+ // Fallback: if the platform doesn't implement DoubaoNativeResponseConverter,
+ // return OpenAI Video API format as the next best option.
+ if converter, ok := adaptor.(channel.OpenAIVideoConverter); ok {
+ openAIVideoData, convertErr := converter.ConvertToOpenAIVideo(originTask)
+ if convertErr == nil {
+ respBody = openAIVideoData
+ return
+ }
+ }
+ }
+ }
+
// OpenAI Video API 格式: 走各 adaptor 的 ConvertToOpenAIVideo
if isOpenAIVideoAPI {
adaptor := GetTaskAdaptor(originTask.Platform)