Imágenes y vídeo asíncronos: respond-async, consulta periódica y webhooks

· Hyperouter · 4 min de lectura

Un vídeo tarda un rato, y una imagen también puede tardar. Hyperouter te da tres maneras de esperar un resultado sin una conexión retenida durante mucho tiempo: enviar Prefer: respond-async para una imagen, consultar periódicamente el registro, o dejar que un webhook te avise cuando termine el trabajo. Esta guía cubre cada una.

Qué modelos responden de qué manera

Cada modelo de GET /v1/models lista response_modes:

Un modelo que lista solo async necesita el encabezado Prefer; sin él la respuesta es 400 con el código async_required. El vídeo es siempre asíncrono a través de la API de vídeos, con o sin el encabezado.

Imágenes: Prefer: respond-async

En un modelo que lista async, añade el encabezado. El id de modelo de abajo es un ejemplo: comprueba response_modes para el tuyo. Es RFC 7240, así que puede repetirse y puede llevar otras preferencias, por ejemplo 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}'

Recibes 202 de inmediato, con el encabezado de respuesta Preference-Applied: respond-async:

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

Un modelo que responde sync y recibe la solicitud sin el encabezado espera la imagen hasta 90 segundos. Si la imagen no está lista para entonces, la respuesta es 503 con el código timeout, el encabezado x-should-retry: false y el id del trabajo como error.generation_id. El trabajo continúa y solo se te factura si tiene éxito. Consulta periódicamente ese id como se explica a continuación.

Consulta periódica de una imagen

GET /v1/generations/{id} devuelve el registro: status es queued, submitting, running, done o failed. Para una imagen, outputs contiene [{"url", "expires_at"}], enlaces firmados válidos durante 24 horas. Los resultados están disponibles durante 30 días.

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)

En failed, el error del registro contiene un code y un message. Un trabajo fallido no cuesta nada. Leer un resultado no cuenta para tus límites de velocidad.

Vídeos: siempre un trabajo

Una llamada a POST /v1/videos responde 200 de inmediato con el objeto Video en estado queued. Prefer no cambia nada aquí.

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}'

Consulta periódicamente GET /v1/videos/{id} hasta que status sea completed (también puede ser queued, in_progress o failed). progress va de 0 a 100. Después descarga el archivo:

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
}

El vídeo se conserva hasta su expires_at, 30 días después de su creación. Pasado ese plazo responde 404 not_found, así que descarga lo que quieras conservar. El importe se retiene por adelantado y se libera por completo si el trabajo falla.

Webhooks en lugar de consulta periódica

Añade un endpoint en el panel y una solicitud lo alcanza cuando termina un trabajo:

EventoSe envía cuando
video.completed, video.failedTermina un trabajo de vídeo.
image.completed, image.failedTermina un trabajo de imagen que enviaste con Prefer: respond-async.

La carga lleva un id y nada más del resultado, así que obtén el objeto con tu clave de API: data.id es lo que aceptan GET /v1/videos/{id} y GET /v1/generations/{id}. Cada entrega se firma con el esquema Standard Webhooks, así que los SDK de OpenAI la verifican. Verifica el cuerpo sin procesar antes de analizarlo:

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);
}

Responde con un 2xx en menos de 10 segundos. Cualquier otra cosa se reintenta, hasta ocho veces más, y webhook-id es el mismo en cada reintento, así que úsalo para eliminar duplicados. La referencia de webhooks tiene las reglas de los endpoints, los encabezados y el calendario completo de reintentos.

Un endpoint cuyas entregas fallan todas durante 3 días se desactiva, y el panel indica por qué. Hasta que lo vuelvas a activar, consulta periódicamente.

Qué sigue