非同期の画像と動画:respond-async、ポーリング、Webhook

· Hyperouter · 読了目安 5 分

動画の生成には時間がかかり、画像も同様のことがあります。Hyperouter には、長時間の接続を保持せずに結果を待つ方法が 3 つあります。画像には Prefer: respond-async を送信する、記録をポーリングする、ジョブの終了を Webhook で受け取る、の 3 つです。このガイドでは、それぞれを説明します。

モデルごとの応答方法

GET /v1/models の各モデルは、response_modes を列挙しています。

async のみを列挙するモデルには Prefer ヘッダーが必要です。ヘッダーがない場合は、コード async_required の 400 が返ります。動画は、ヘッダーの有無にかかわらず、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 秒待ちます。その時間内に画像が完成しない場合は、コード timeout、ヘッダー x-should-retry: false、そしてジョブの ID を error.generation_id に含む 503 が返ります。ジョブは継続し、成功した場合にのみ課金されます。その 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 の呼び出しは、ステータス queued の Video オブジェクトとともに、すぐに 200 で応答します。ここでは 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}'

status が completed になるまで GET /v1/videos/{id} をポーリングします(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
}

動画は、作成から 30 日後の expires_at まで保持されます。それを過ぎると 404 not_found が返るため、残したいものはダウンロードしてください。金額は事前に確保され、ジョブが失敗した場合は全額が解放されます。

ポーリングの代わりに Webhook を使う

ダッシュボードでエンドポイントを追加すると、ジョブが終了したときにそのエンドポイントへリクエストが届きます。

Event送信されるタイミング
video.completed、video.failed動画のジョブが終了したとき。
image.completed、image.failedPrefer: respond-async を付けて送信した画像のジョブが終了したとき。

ペイロードには ID だけが含まれ、結果そのものは含まれません。オブジェクトは API キーで取得してください。data.id は、GET /v1/videos/{id} と GET /v1/generations/{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 で応答してください。それ以外の応答は、最大でさらに 8 回再試行されます。webhook-id は再試行のたびに同じ値になるため、重複の排除に使えます。エンドポイントのルール、ヘッダー、再試行のスケジュールの全体は、Webhook リファレンスに掲載しています。

3 日間すべての配信が失敗したエンドポイントは無効になり、ダッシュボードにその理由が表示されます。再度有効にするまでは、ポーリングを使ってください。

次のステップ