Procesar peticiones en lote
Un lote es un archivo con muchas peticiones completas que InferNode ejecuta por su cuenta. Subes el archivo, creas el lote, consultas su estado de vez en cuando y descargas los resultados. Si alguna línea falló, hay además un archivo aparte con esas líneas.
Conviene cuando tienes trabajo que no necesitas ahora mismo: clasificar un histórico, resumir miles de documentos, generar los vectores de un corpus. Las líneas del lote no gastan tus peticiones por minuto, así que el trabajo pesado no deja sin servicio a tus agentes y flujos.
Antes de empezar
- Una llave de InferNode (Usar la API desde código).
- Un modelo del Catálogo que admita lotes. Quien administra la plataforma decide cuáles y con cuánta capacidad; si el modelo no los admite, el archivo falla con
model_not_batchable. - Que tu instalación tenga los lotes habilitados. Si no los tiene,
/v1/batchesresponde404. Compruébalo con una petición cualquiera a esa ruta.
El archivo de entrada
Un archivo de texto con una petición por línea, en formato JSONL. Cada línea lleva un identificador tuyo (custom_id), el método, la ruta y el cuerpo que enviarías al endpoint normal.
{"custom_id": "doc-1", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "Saptiva Turbo", "messages": [{"role": "user", "content": "Resume este expediente en una frase."}], "max_tokens": 64}}
{"custom_id": "doc-2", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "Saptiva Turbo", "messages": [{"role": "user", "content": "¿Qué área firma este documento?"}], "max_tokens": 64}}
Para vectores, las líneas apuntan a /v1/embeddings:
{"custom_id": "parrafo-1", "method": "POST", "url": "/v1/embeddings", "body": {"model": "<modelo-de-vectores>", "input": ["primer párrafo", "segundo párrafo"]}}
Nombra el modelo por su alias (Saptiva Turbo), no por el nombre exacto del Catálogo: un lote puede tardar, y si el modelo se actualiza mientras tanto, el nombre exacto deja de resolver y las líneas que falten terminan en model_not_found. GET /v1/models/aliases lista los de tu instalación.
Reglas del archivo:
- Un solo endpoint y un solo modelo. Una línea que nombre otra ruta falla el archivo con
url_mismatch, y una que nombre otro modelo, conmodel_mismatch. custom_idúnico, no vacío y de 512 caracteres como máximo. Es lo que te permite casar cada resultado con su entrada, porque el orden de salida no está garantizado.- Sin
stream. Una línea con"stream": truefalla constream_not_supported. Un lote nunca responde en streaming. - El cuerpo es el mismo que aceptaría el endpoint normal y se valida igual.
Los cuatro pasos
BASE=https://api.<tu-dominio>/infernode/v1
# 1. Subir el archivo. Devuelve un id file-...
curl -sS $BASE/files \
-H "Authorization: Bearer sk-saptiva-..." \
-F purpose=batch -F file=@entrada.jsonl
# 2. Crear el lote. Devuelve un id batch_... en estado validating
curl -sS $BASE/batches \
-H "Authorization: Bearer sk-saptiva-..." -H "Content-Type: application/json" \
-d '{"input_file_id":"file-...","endpoint":"/v1/chat/completions","completion_window":"24h"}'
# 3. Consultar hasta que el estado sea completed, failed, cancelled o expired
curl -sS $BASE/batches/batch_... -H "Authorization: Bearer sk-saptiva-..."
# 4. Descargar los resultados, con el output_file_id que trae la consulta
curl -sS $BASE/files/file-.../content \
-H "Authorization: Bearer sk-saptiva-..." -o resultados.jsonl
completion_window solo acepta "24h" y se guarda por compatibilidad con el SDK de OpenAI: no es una promesa de entrega. Un lote avanza al ritmo que deja libre la capacidad del modelo, que se comparte con el resto del Cliente.
Si vuelves a subir un archivo con el mismo contenido, InferNode devuelve el que ya tenía en lugar de duplicarlo, con la cabecera X-Saptiva-Deduplicated-From. Lo mismo al crear dos veces el mismo lote mientras el primero sigue vivo.
Con el SDK de OpenAI
El flujo es idéntico al de OpenAI; solo cambia base_url.
import os, time
from openai import OpenAI
cliente = OpenAI(
base_url="https://api.<tu-dominio>/infernode/v1",
api_key=os.environ["SAPTIVA_API_KEY"],
)
archivo = cliente.files.create(file=open("entrada.jsonl", "rb"), purpose="batch")
lote = cliente.batches.create(
input_file_id=archivo.id,
endpoint="/v1/chat/completions",
completion_window="24h",
)
terminales = {"completed", "failed", "expired", "cancelled"}
while (lote := cliente.batches.retrieve(lote.id)).status not in terminales:
time.sleep(30)
print(lote.status, lote.request_counts)
if lote.output_file_id:
print(cliente.files.content(lote.output_file_id).text)
if lote.error_file_id:
print(cliente.files.content(lote.error_file_id).text)
Consulta cada 30 segundos o más: cada consulta es una petición normal y sí gasta tus límites por minuto.
Estados
| Estado | Qué significa |
|---|---|
validating | Recién creado. Se está revisando el archivo. Ninguna línea ha corrido |
in_progress | Las líneas se están ejecutando |
finalizing | Terminaron; se están armando los archivos de salida. Ya no se puede cancelar |
completed | Terminado. Están el archivo de resultados y, si hubo fallos, el de errores |
failed | Un problema del archivo completo, por ejemplo una línea mal formada o un modelo que no admite lotes. Ninguna línea corrió, y errors lista hasta 100 causas |
cancelling | Pediste cancelarlo. Las líneas en curso terminan y se cobran |
cancelled | Cancelado. Se conservan los resultados de lo que sí corrió |
expired | Se alcanzó la fecha de expires_at. Se conserva lo que alcanzó a correr |
Mientras avanza, request_counts lleva la cuenta de total, completed y failed. En failed entran también las líneas canceladas y las expiradas, así que un lote que cancelaste muestra ahí las que no llegaron a correr.
Los resultados
El archivo de salida trae una línea por cada línea que terminó bien, con tu custom_id y la respuesta que habría devuelto el endpoint normal:
{"id": "batch_req_...", "custom_id": "doc-1", "response": {"status_code": 200, "request_id": "req_...", "body": {"choices": [{"message": {"role": "assistant", "content": "..."}}]}}, "error": null}
Todo lo demás va al archivo de errores, que solo existe si falló alguna línea: error_file_id viene vacío cuando todas funcionaron. Ahí caben dos formas. Una línea rechazada por el modelo o por la validación conserva el código y el cuerpo que habría devuelto la llamada normal, así que puedes leer su causa igual que en una llamada suelta. Una línea detenida por el propio lote, por ejemplo al cancelarlo, trae response vacío y un error con su causa.
Entre los dos archivos está siempre el total de líneas de entrada, así que ninguna se pierde sin explicación.
Los archivos no se guardan para siempre. Tanto el de entrada como los de salida caducan a los 30 días por defecto, y cada archivo lleva su fecha en expires_at. Descarga los resultados que quieras conservar.
Los archivos se descargan solo con GET /v1/files/{id}/content, que responde con la cabecera X-Saptiva-Content-SHA256 para que verifiques lo que bajaste. No hay enlaces temporales de descarga.
Cancelar
curl -sS -X POST $BASE/batches/batch_.../cancel -H "Authorization: Bearer sk-saptiva-..."
El lote pasa a cancelling. Lo que ya estaba corriendo termina y se cobra; lo que no había empezado aparece en el archivo de errores como batch_cancelled y no se cobra. Así toda línea queda explicada. Un lote en finalizing o ya terminado responde 400 con batch_not_cancellable.
Un administrador de tu organización, o quien opera la plataforma, también puede cancelar un lote tuyo. Cuando pasa, el lote trae en errors.data una entrada cancelled_by_org_admin (nombra a la persona) o cancelled_by_platform_admin. Si cancelas tú, no aparece ninguna entrada.
Qué cuesta y qué consume
- Cada línea cuesta lo mismo que la llamada equivalente al mismo modelo. No hay descuento por lote.
- El consumo se registra aparte del interactivo: en
GET /v1/billing/balance, el último movimiento (last_event.endpoint) lleva el endpoint marcado, por ejemplo/v1/chat/completions:batch. - Las líneas no gastan tus peticiones por minuto. Sí lo hacen las llamadas de control: subir, crear, consultar, listar y cancelar.
- Si el saldo se agota a mitad del lote, las líneas restantes fallan con
insufficient_creditsy quedan registradas en el archivo de errores.
Límites del archivo
| Límite | Valor por defecto | Si se supera |
|---|---|---|
| Tamaño del archivo | 100 MiB | 413 file_too_large al subirlo |
| Tamaño de una línea | 1 MiB | El archivo falla con invalid_json_line |
| Líneas por archivo | 50 000 | El archivo falla con too_many_tasks |
| Entradas de vectores, sumando todas las líneas | 50 000 | El archivo falla con too_many_tasks |
| Almacenamiento del Cliente | 10 GiB | 400 storage_quota_exceeded al subir |
| Lotes activos a la vez | Según el plan | 429 too_many_active_batches |
| Líneas en espera | Según el plan | El archivo falla con enqueued_request_limit_exceeded |
Los archivos de salida cuentan para el almacenamiento pero nunca se rechazan. Si te quedas sin espacio, borra archivos que ya no uses con DELETE /v1/files/{id}. Un archivo de entrada usado por un lote vivo no se deja borrar, y responde file_in_use.
Cuando algo falla
| Lo que ves | Causa | Qué hacer |
|---|---|---|
El lote queda en failed sin que corra nada | Un problema del archivo entero. errors.data lista hasta 100 causas con su línea | Corrige el archivo y vuelve a subirlo |
model_not_batchable | Ese modelo no tiene capacidad asignada para lotes | Pide que se le asigne, o usa otro modelo |
model_not_found | El modelo no existe para tu llave, o se retiró a mitad del lote | Revisa GET /v1/models |
duplicate_custom_id | Dos líneas comparten identificador | Haz únicos los custom_id |
413 file_too_large | El archivo pasa de 100 MiB | Pártelo en varios lotes |
429 too_many_active_batches | Ya tienes en marcha los lotes que permite tu plan | Espera a que terminen |
authorization_revoked en las líneas | La llave, la membresía o la organización dejó de ser válida mientras corría | Revisa la llave y vuelve a crear el lote |
Sigue en validating un buen rato | El arranque se reintenta solo. Puede tardar hasta cerca de media hora en empezar | Espera; no vuelvas a crearlo, porque devolvería el mismo lote |
in_progress sin avanzar | La capacidad del modelo está ocupada con otro trabajo del Cliente | Es normal en lotes grandes. Si no avanza en horas, repórtalo con el identificador del lote |