Lanzar peticiones de GPT Image 2 por lotes sin perder tareas
Envía varios prompts de GPT Image 2, guarda los identificadores de tarea, reanuda el sondeo tras una interrupción y descarga las imágenes sin reenviarlas.
Leer en MarkdownLa generación por lotes con GPT Image 2 en SeedRouter consiste en enviar peticiones de imagen normales y seguir cada identificador de tarea devuelto. Esta guía usa un script pequeño del lado del cliente, no una API Batch aparte ni un producto con descuento por lotes. Cada prompt es su propia tarea; el script guarda el progreso para que puedas volver a consultar esas tareas tras una interrupción.
Hay dos trabajos distintos: generar una imagen y descargarla. Una descarga fallida no obliga a generar de nuevo. Que venza el plazo del sondeo no significa que la tarea de imagen haya fallado. Mantener esas distinciones en el script ahorra trabajo y evita confusiones.
¿Qué necesitas antes de empezar?
Usa Node.js 22 o posterior, una variable de entorno SEEDROUTER_API_KEY en el servidor y saldo suficiente en la cuenta para tu conjunto de pruebas. Empieza con dos prompts. Consulta los precios actuales antes de aumentar el número de imágenes.
Guarda el código siguiente como images.mjs en un directorio de trabajo privado. Allí crea image-tasks.json y descarga los archivos PNG en images/. Conserva el archivo de estado: es lo que conecta tus etiquetas locales con las tareas aceptadas. No añadas secretos ni prompts con datos sensibles de clientes a un repositorio público.
Los ejemplos piden una imagen por prompt. Usa el parámetro n cuando quieras varias salidas del mismo prompt; no lo confundas con enviar prompts distintos. SeedRouter documenta el rango admitido y otras restricciones en la referencia del modelo.
Envía una vez y guarda los identificadores
El script escribe una entrada unconfirmed antes de enviar una petición. Si el proceso se detiene antes de guardar la respuesta, la siguiente ejecución no volverá a enviar ese elemento en silencio. Revisa la tarea en tu cuenta antes de decidir qué hacer con una entrada sin confirmar.
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
const mode = process.argv[2];
if (!['submit', 'poll'].includes(mode)) {
throw new Error('Use: node images.mjs submit | poll');
}
const key = process.env.SEEDROUTER_API_KEY;
if (!key) throw new Error('Set SEEDROUTER_API_KEY on your server.');
const base = 'https://api.seedrouter.ai/v1';
const headers = { Authorization: `Bearer ${key}` };
const file = 'image-tasks.json';
let state;
try {
state = JSON.parse(await readFile(file, 'utf8'));
} catch (error) {
if (error.code !== 'ENOENT') throw error;
state = {};
}
const save = async () => {
await writeFile(`${file}.tmp`, JSON.stringify(state, null, 2), { mode: 0o600 });
await rename(`${file}.tmp`, file);
};
const pause = () => new Promise((resolve) => setTimeout(resolve, 3000));
const jobs = [
['blue-mug', 'A cobalt-blue ceramic mug on a pale gray tabletop.'],
['green-bowl', 'A green ceramic bowl on a pale gray tabletop.'],
];
if (mode === 'submit') {
for (const [name, prompt] of jobs) {
if (state[name]) continue;
state[name] = { status: 'unconfirmed' };
await save();
try {
const response = await fetch(`${base}/images/generations`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'gpt-image-2', prompt,
size: '1024x1024', quality: 'low', output_format: 'png', n: 1,
}),
signal: AbortSignal.timeout(60000),
});
const task = await response.json();
if (typeof task.id === 'string' && task.id) {
state[name] = { id: task.id, status: 'processing' };
await save();
}
if (!response.ok || !state[name].id) {
throw new Error('Submission needs review.');
}
} catch {
console.error(`${name}: stopped; inspect the saved state before continuing.`);
process.exitCode = 1;
break;
}
}
} else {
await mkdir('images', { recursive: true });
const deadline = Date.now() + 600000;
do {
for (const [name, entry] of Object.entries(state)) {
if (!entry.id || entry.status === 'failed' || entry.downloaded) continue;
try {
const response = await fetch(`${base}/tasks/${encodeURIComponent(entry.id)}`, {
headers, signal: AbortSignal.timeout(30000),
});
if (!response.ok) throw new Error('Task check failed.');
const task = await response.json();
if (!['processing', 'completed', 'failed'].includes(task.status)) {
throw new Error('Unexpected task status.');
}
entry.status = task.status;
await save();
if (task.status !== 'completed') continue;
const images = task.output?.data;
if (!Array.isArray(images) || !images.length) {
throw new Error('Completed task has no image URLs.');
}
for (const [index, image] of images.entries()) {
const url = new URL(image.url);
if (url.protocol !== 'https:') throw new Error('Expected an HTTPS image URL.');
// The download is a separate request: never attach the API key.
const result = await fetch(url, { signal: AbortSignal.timeout(60000) });
if (!result.ok || !result.headers.get('content-type')?.startsWith('image/png')) {
throw new Error('PNG download failed.');
}
await writeFile(`images/${name}-${index}.png`, Buffer.from(await result.arrayBuffer()));
}
entry.downloaded = true;
await save();
} catch {
console.error(`${name}: check or download incomplete; task ID retained.`);
}
}
if (!Object.values(state).some((entry) => entry.id && entry.status !== 'failed' && !entry.downloaded)) break;
await pause();
} while (Date.now() < deadline);
}
console.log(JSON.stringify(state, null, 2));Ejecuta node images.mjs submit y después node images.mjs poll. Ejecuta solo una copia del script a la vez y no renombres etiquetas en el archivo de estado. Este es un ejemplo local pequeño, no un sistema de almacenamiento multiproceso. Para un servicio con varias instancias, usa almacenamiento duradero con propiedad exclusiva de cada envío en lugar de compartir este archivo JSON.
Reanuda el sondeo sin generar de nuevo
Vuelve a ejecutar node images.mjs poll tras un plazo local vencido o una interrupción de red. Lee los identificadores guardados y los comprueba con peticiones GET. No envía otra petición de generación. Una tarea completada con la descarga a medias se vuelve a comprobar y sus archivos PNG se descargan con los mismos nombres deterministas.
El plazo de diez minutos pertenece a este script de ejemplo. No es un tiempo de finalización prometido ni una cancelación del servidor. Una petición de red en curso puede terminar después de ese plazo del bucle. La referencia del ciclo de vida de las tareas define los estados reales: processing, completed y failed.
Una entrada unconfirmed necesita revisión manual porque el script no tiene un identificador guardado. No borres esa entrada para reenviar solo por ver si funciona a la segunda. Consulta primero el historial de tareas de tu cuenta. Si recuperas su identificador, ponlo en la entrada guardada y usa el sondeo; si no, resuelve el envío incierto antes de decidir generar otra vez.
¿Cuánta concurrencia conviene usar?
Empieza con envío secuencial y un conjunto de pruebas pequeño, como hace este ejemplo. Cada tarea aceptada sigue su curso mientras el script envía la siguiente, pero las peticiones no salen en una ráfaga ilimitada. El código no implica ningún número de concurrencia universalmente seguro.
En una aplicación mayor, convierte el número máximo de tareas sin terminar en un ajuste explícito. Deja de admitir trabajo nuevo al alcanzar ese límite y sigue comprobando las tareas ya aceptadas. Gestiona los límites de la cuenta con las indicaciones de error documentadas, no repitiendo peticiones POST a ciegas.
Lleva un registro aparte de los resultados que necesiten una revisión creativa. Una imagen terminada que no te gusta no es una tarea de API fallida. Es una nueva decisión editorial y, si envías otra petición, una generación más que incluir en el presupuesto.
Revisa el conjunto antes de darlo por terminado
Compara tus etiquetas de entrada con el archivo de estado y los archivos descargados. Cada etiqueta debería tener una explicación: descargada, aún en proceso, fallida o pendiente de revisar el envío. Contar solo archivos puede ocultar una tarea sin terminar o una descarga interrumpida a medias.
Abre también las imágenes. El script comprueba el estado HTTP y el tipo de contenido PNG, pero no puede decidir si una taza tiene el color pedido ni si una etiqueta se lee. Revisa el encargo visual por separado del éxito del transporte.
Si cambias output_format, actualiza a la vez el tipo de contenido esperado y la extensión del archivo. Renombrar a .png un JPEG descargado no lo convierte. El ejemplo fija PNG a propósito para que este asunto añadido de formatos no tape la recuperación de tareas.
Preguntas frecuentes
¿Esto es la Batch API de OpenAI?
No. Envía tareas de imagen normales de SeedRouter desde un script. No usa un endpoint de Batch API ni implica precios especiales por lotes.
¿Puedo reanudar solo las descargas?
Sí. Ejecuta el comando de sondeo con el estado guardado. Vuelve a recuperar la salida de las tareas completadas y descarga los resultados que falten sin crear otra tarea de imagen.
¿Una tarea fallida detiene a las demás?
No. El bucle de sondeo registra esa tarea como fallida y sigue comprobando las demás. Revisa el elemento fallido por separado. Una tarea que falla no se cobra; un tiempo agotado local por sí solo no demuestra un fallo.
Guarda el registro de tareas junto a las imágenes
Una generación por lotes fiable con GPT Image 2 depende de mantener el vínculo entre cada prompt y su identificador de tarea. Conserva ese registro, reanuda las tareas existentes y reintenta las descargas al margen de la generación. Cuando un conjunto pequeño se comporte como esperas, aumenta la carga dentro de un presupuesto que puedas vigilar con la guía de precios.



