Imágenes y vídeo asíncronos: respond-async, consulta periódica y webhooks
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:
sync: una respuesta retenida.async: envíaPrefer: respond-async, recibe202y consulta periódicamente.stream:"stream": true.
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:
| Evento | Se envía cuando |
|---|---|
video.completed, video.failed | Termina un trabajo de vídeo. |
image.completed, image.failed | Termina 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
- Reintenta con seguridad con Idempotency-Key hace que la propia llamada de creación sea segura de repetir.
- Imágenes y Vídeos son las referencias completas.