异步任务与结果
处理 202 响应:结果信封的字段与状态、三种查询方式、轮询策略、生成文件的下载与重新签名,以及保留期限。
视频、3D 等生成任务需要几十秒到几十分钟。平台会立即返回 202 和一个结果编号,在后台轮询厂商、结算费用并把生成文件转存到私有存储。你只需按编号查询,直到任务进入终态。
前提:已创建密钥并把它设置为环境变量 TOKENTOSEA_API_KEY。示例使用 curl、Python requests 或 Node.js 18+。
哪些请求会返回 202#
| 情况 | 说明 |
|---|---|
| 始终异步 | POST /v1/videos、POST /v1/3d/generations |
| 视型号而定 | 部分图像生成与编辑、文件转写类语音识别(*-filetrans、fun-asr、paraformer-*)、异步向量(text-embedding-async-*)、部分专用模型、MiniMax 异步语音合成 |
| 任何接口都可能 | 结果已生成但仍在结算或转存文件时,同步接口也会返回 202 |
因此所有调用方都应处理 202,而不只是视频接口。
结果信封#
202 响应体、失败时的 422 响应体,以及所有查询接口返回的都是同一个结构:
处理中
{
"id": "result_6f2c…(64 位十六进制)",
"run_id": "…",
"status": "upstream_pending"
}已完成
{
"id": "result_6f2c…",
"run_id": "…",
"status": "succeeded",
"billing_status": "succeeded",
"output": {
"id": "…",
"model": "cogvideox-3",
"data": [
{
"url": "https://…(短期签名链接)",
"asset_id": "…",
"mime_type": "video/mp4"
}
]
},
"status_code": 200,
"response_type": "application/json"
}| 字段 | 说明 |
|---|---|
id | 结果编号,格式 result_ + 64 位十六进制。用于 /v1/tasks/{id} 与 /v1/videos/{id}。 |
run_id | 平台调用记录编号,联系支持时请提供。 |
status | 处理状态,见下表。 |
billing_status | 结算状态:succeeded 已扣费,failed 未扣费,needs_review 待核对。尚未结算时不出现。 |
error.code | 失败或处理受阻时的原因。 |
output | 仅在 succeeded 时出现:原始响应。媒体结果的 data[] 中每项有 url、asset_id、mime_type。 |
status_code / response_type | 原始响应的状态码与内容类型。response_type 为 text/event-stream 时,output.stream 是完整的 SSE 文本。 |
状态#
status | 含义 | 是否终态 |
|---|---|---|
planned / sending | 已受理,正在发往厂商。planned 超过 5 分钟不变,说明请求没有发往厂商,见幂等与重试 | 否 |
streaming | 正在接收厂商的流式输出或实时会话 | 否 |
upstream_pending | 厂商已接收任务,正在生成;平台每 5 秒查询一次厂商 | 否 |
billing | 已拿到结果,正在结算 | 否 |
archiving | 已结算,正在把生成文件转存到私有存储 | 否 |
succeeded | 完成,output 可用 | 是 |
failed | 失败;billing_status 为 failed 时未扣费,为 succeeded 时已扣费但结果无法交付(paid_result_unavailable),请联系支持 | 是 |
needs_review | 结果或用量无法自动确认,预留金额暂不释放,等待人工核对 | 暂停自动处理,核对后会变为 succeeded 或 failed |
厂商任务超过 24 小时仍未完成时,调用转为 needs_review。
查询结果#
三种方式返回同一个信封,任选其一:
| 请求 | 说明 |
|---|---|
GET /v1/results/{Idempotency-Key} | 用你发起请求时的幂等键查询。适用于所有调用,包括同步调用和实时会话。 |
GET /v1/tasks/{id} | 用信封中的 result_… 编号查询。 |
GET /v1/videos/{id} | 同上,便于沿用视频接口的习惯写法。 |
必须使用创建该请求的同一把密钥;其他密钥查询返回 404。建议每 5–10 秒查询一次:平台本身每 5 秒查询一次厂商,更频繁的轮询不会更快拿到结果。
完整示例:生成并下载视频#
以 cogvideox-3 为例:提交任务 → 轮询 → 下载文件。
export REQUEST_KEY="$(uuidgen)"
# 1. 提交任务,返回 202 与结果信封
curl https://api.tokentosea.com/v1/videos \
-H "Authorization: Bearer $TOKENTOSEA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $REQUEST_KEY" \
-d '{
"model": "cogvideox-3",
"prompt": "A red cube slowly rotates on a white table. Steady camera.",
"duration": 5,
"quality": "speed",
"size": "1280x720",
"fps": 30,
"with_audio": false
}'
# 2. 每 5–10 秒查询一次,直到 status 为 succeeded / failed / needs_review
curl "https://api.tokentosea.com/v1/results/$REQUEST_KEY" \
-H "Authorization: Bearer $TOKENTOSEA_API_KEY"
# 3. 用 output.data[0].url 下载(链接最长 300 秒有效)
curl -L -o video.mp4 "粘贴 output.data[0].url"import os
import time
import uuid
import requests
API = "https://api.tokentosea.com/v1"
AUTH = {"Authorization": f"Bearer {os.environ['TOKENTOSEA_API_KEY']}"}
key = str(uuid.uuid4())
submit = requests.post(
f"{API}/videos",
headers={**AUTH, "Idempotency-Key": key},
json={
"model": "cogvideox-3",
"prompt": "A red cube slowly rotates on a white table. Steady camera.",
"duration": 5,
"quality": "speed",
"size": "1280x720",
"fps": 30,
"with_audio": False,
},
timeout=120,
)
if submit.status_code not in (200, 202):
raise SystemExit(f"提交失败:{submit.status_code} {submit.text}")
print("已受理:", submit.json().get("id"), "幂等键:", key)
deadline = time.time() + 30 * 60
while True:
envelope = requests.get(f"{API}/results/{key}", headers=AUTH, timeout=30).json()
status = envelope.get("status")
if status == "succeeded":
break
if status in ("failed", "needs_review"):
raise SystemExit(f"任务结束:{status} {envelope.get('error')} run_id={envelope.get('run_id')}")
if time.time() > deadline:
raise SystemExit(f"仍在处理,稍后用同一幂等键继续查询:{key}")
time.sleep(8)
for index, item in enumerate(envelope["output"]["data"]):
file = requests.get(item["url"], timeout=300)
if file.status_code == 403: # 签名已过期:重新获取下载链接
fresh = requests.get(f"{API}/assets/{item['asset_id']}/download-url", headers=AUTH, timeout=30).json()
file = requests.get(fresh["url"], timeout=300)
file.raise_for_status()
name = f"video-{index}.mp4"
with open(name, "wb") as output:
output.write(file.content)
print("已保存", name, item["mime_type"], item["asset_id"])import { randomUUID } from 'node:crypto';
import { writeFile } from 'node:fs/promises';
const API = 'https://api.tokentosea.com/v1';
const AUTH = { Authorization: `Bearer ${process.env.TOKENTOSEA_API_KEY}` };
const key = randomUUID();
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const submit = await fetch(`${API}/videos`, {
method: 'POST',
headers: { ...AUTH, 'Content-Type': 'application/json', 'Idempotency-Key': key },
body: JSON.stringify({
model: 'cogvideox-3',
prompt: 'A red cube slowly rotates on a white table. Steady camera.',
duration: 5,
quality: 'speed',
size: '1280x720',
fps: 30,
with_audio: false,
}),
});
if (![200, 202].includes(submit.status)) throw new Error(`提交失败:${submit.status} ${await submit.text()}`);
console.log('已受理:', (await submit.json()).id, '幂等键:', key);
const deadline = Date.now() + 30 * 60_000;
let envelope;
for (;;) {
envelope = await (await fetch(`${API}/results/${key}`, { headers: AUTH })).json();
if (envelope.status === 'succeeded') break;
if (['failed', 'needs_review'].includes(envelope.status)) {
throw new Error(`任务结束:${envelope.status} ${envelope.error?.code ?? ''} run_id=${envelope.run_id}`);
}
if (Date.now() > deadline) throw new Error(`仍在处理,稍后用同一幂等键继续查询:${key}`);
await sleep(8_000);
}
for (const [index, item] of envelope.output.data.entries()) {
let file = await fetch(item.url);
if (file.status === 403) {
// 签名已过期:重新获取下载链接
const fresh = await (await fetch(`${API}/assets/${item.asset_id}/download-url`, { headers: AUTH })).json();
file = await fetch(fresh.url);
}
if (!file.ok) throw new Error(`下载失败:HTTP ${file.status}`);
const name = `video-${index}.mp4`;
await writeFile(name, Buffer.from(await file.arrayBuffer()));
console.log('已保存', name, item.mime_type, item.asset_id);
}生成文件与下载链接#
- 生成的图片、视频、音频与 3D 文件由平台转存到私有对象存储,厂商的原始地址与元数据不会出现在结果中。
url是短期签名链接,最长 300 秒有效;每次查询结果都会重新签名。- 链接过期后,用
GET /v1/assets/{asset_id}/download-url获取新链接,返回{"url": "…", "expires_in": 300}(剩余保留期不足 300 秒时更短)。 - 资产只能用创建它的密钥获取;其他密钥返回
404 asset_not_found,匿名访问存储地址会被拒绝。 - 文件保留 30 天,请在此期间下载到你自己的存储。
保留期限#
| 数据 | 保留时间 |
|---|---|
结果记录(/v1/results 可查询) | 最后一次更新后约 31 天 |
| 生成文件(资产) | 30 天 |
| 下载签名链接 | 最长 300 秒 |
| 厂商任务的自动轮询 | 最长 24 小时,超时转为 needs_review |
常见错误#
| HTTP | error.code | 原因与处理 |
|---|---|---|
| 404 | result_not_found | 幂等键不对、换了密钥,或请求尚未到达平台。确认后可用原键原样重发。 |
| 404 | task_not_found | 任务编号不属于这把密钥,或不存在。 |
| 404 | asset_not_found | 资产不属于这把密钥,或已超过保留期。 |
| 422 | response_mode_not_verified | 媒体接口不支持 "stream": true。 |
| 502 | execution_result_pending | 结果暂未确定。继续用原键查询,不要重新提交。 |