异步图像与视频:respond-async、轮询与 Webhook

· Hyperouter · 阅读约 4 分钟

视频需要一段时间才能生成,图像有时也是如此。Hyperouter 提供三种方式,让你无需长时间占用连接就能等到结果:为图像发送 Prefer: respond-async、轮询记录,或者在任务结束时由 Webhook 通知你。本指南逐一介绍。

各模型的应答方式

GET /v1/models 中的每个模型都会列出 response_modes:

仅列出 async 的模型必须带上 Prefer 请求头;缺少时,响应为 400,code 为 async_required。视频通过 Videos API 始终是异步的,无论是否带该请求头。

图像:Prefer: respond-async

对列出 async 的模型,加上该请求头即可。下面的模型 id 只是示例:请查看你所用模型的 response_modes。这是 RFC 7240,因此可以重复出现,也可以携带其他偏好,例如 Prefer: wait=10, respond-async。

curl -i https://api.hyperouter.app/v1/images/generations \
  -H "Authorization: Bearer $HYPEROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Prefer: respond-async" \
  -d '{"model": "x-ai/grok-imagine-image", "prompt": "A lighthouse at dusk", "n": 1}'

你会立即收到 202,响应头为 Preference-Applied: respond-async:

{ "id": "0194f2c4-7a1e-7b3c-9d02-5f6a1c0e8b21", "status": "queued" }

对于应答方式为 sync 的模型,如果发送时没有带该请求头,请求最多会等待图像 90 秒。到时图像仍未就绪,就会返回 503,code 为 timeout,带有响应头 x-should-retry: false,并以 error.generation_id 给出任务的 id。任务会继续运行,只有成功时才会计费。请按下文所述轮询该 id。

轮询图像

GET /v1/generations/{id} 返回记录:status 为 queued、submitting、running、done 或 failed。对于图像,outputs 为 [{"url", "expires_at"}],即有效期 24 小时的签名链接。结果可获取 30 天。

import os
import time

import requests

headers = {"Authorization": f"Bearer {os.environ['HYPEROUTER_API_KEY']}"}

def wait_for_image(generation_id: str, every: float = 3.0, limit: float = 600.0) -> dict:
    deadline = time.monotonic() + limit
    while time.monotonic() < deadline:
        r = requests.get(
            f"https://api.hyperouter.app/v1/generations/{generation_id}",
            headers=headers,
            timeout=30,
        )
        r.raise_for_status()
        rec = r.json()["data"]
        if rec["status"] in ("done", "failed"):
            return rec
        time.sleep(every)
    raise TimeoutError(generation_id)

状态为 failed 时,记录中的 error 包含 code 和 message。失败的任务不收费。读取结果不计入你的速率限制。

视频:始终是任务

调用 POST /v1/videos 会立即返回 200,响应是状态为 queued 的 Video 对象。Prefer 在这里不起任何作用。

curl https://api.hyperouter.app/v1/videos \
  -H "Authorization: Bearer $HYPEROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "x-ai/grok-imagine-video", "prompt": "A lighthouse at dusk", "seconds": 8}'

轮询 GET /v1/videos/{id},直到 status 为 completed(也可能是 queued、in_progress 或 failed)。progress 从 0 到 100。然后下载文件:

async function waitForVideo(id: string): Promise<void> {
  const headers = { Authorization: `Bearer ${process.env.HYPEROUTER_API_KEY}` };
  for (;;) {
    const res = await fetch(`https://api.hyperouter.app/v1/videos/${id}`, { headers });
    const video = await res.json();
    if (video.status === "completed") break;
    if (video.status === "failed") throw new Error(video.error?.code ?? "failed");
    await new Promise((r) => setTimeout(r, 5000));
  }
  const file = await fetch(`https://api.hyperouter.app/v1/videos/${id}/content`, { headers });
  // file is video/mp4; ?variant=thumbnail returns the thumbnail
}

视频会保留到其 expires_at,即创建后 30 天。之后请求会返回 404 not_found,因此想保留的内容请及时下载。费用在任务开始前预扣,任务失败时会全额释放。

用 Webhook 代替轮询

在控制台中添加一个端点,任务结束时就会有请求发送到该端点:

事件发送时机
video.completed, video.failed视频任务结束。
image.completed, image.failed你以 Prefer: respond-async 发送的图像任务结束。

载荷只携带 id,不含结果的其他内容,因此请使用你的 API 密钥获取对象:data.id 就是 GET /v1/videos/{id} 和 GET /v1/generations/{id} 所接受的 id。每次投递都采用 Standard Webhooks 方案签名,因此 OpenAI SDK 可以直接校验。请在解析之前,对原始请求体进行校验:

const event = client.webhooks.unwrap(rawBody, req.headers, process.env.HYPEROUTER_WEBHOOK_SECRET);
if (event.type === "video.completed") {
  const video = await client.videos.retrieve(event.data.id);
}

请在 10 秒内返回 2xx。其他情况会重试,最多再重试八次,每次重试的 webhook-id 都相同,可用它去重。Webhook 参考给出了端点规则、请求头和完整的重试时间表。

如果某个端点的所有投递连续 3 天失败,该端点会被停用,控制台会说明原因。在你重新启用之前,请改用轮询。

下一步