Saltar al contenido principal

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/batches responde 404. 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, con model_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": true falla con stream_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​

EstadoQué significa
validatingRecién creado. Se está revisando el archivo. Ninguna línea ha corrido
in_progressLas líneas se están ejecutando
finalizingTerminaron; se están armando los archivos de salida. Ya no se puede cancelar
completedTerminado. Están el archivo de resultados y, si hubo fallos, el de errores
failedUn 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
cancellingPediste cancelarlo. Las líneas en curso terminan y se cobran
cancelledCancelado. Se conservan los resultados de lo que sí corrió
expiredSe 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_credits y quedan registradas en el archivo de errores.

Límites del archivo​

LímiteValor por defectoSi se supera
Tamaño del archivo100 MiB413 file_too_large al subirlo
Tamaño de una línea1 MiBEl archivo falla con invalid_json_line
Líneas por archivo50 000El archivo falla con too_many_tasks
Entradas de vectores, sumando todas las líneas50 000El archivo falla con too_many_tasks
Almacenamiento del Cliente10 GiB400 storage_quota_exceeded al subir
Lotes activos a la vezSegún el plan429 too_many_active_batches
Líneas en esperaSegún el planEl 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 vesCausaQué hacer
El lote queda en failed sin que corra nadaUn problema del archivo entero. errors.data lista hasta 100 causas con su líneaCorrige el archivo y vuelve a subirlo
model_not_batchableEse modelo no tiene capacidad asignada para lotesPide que se le asigne, o usa otro modelo
model_not_foundEl modelo no existe para tu llave, o se retiró a mitad del loteRevisa GET /v1/models
duplicate_custom_idDos líneas comparten identificadorHaz únicos los custom_id
413 file_too_largeEl archivo pasa de 100 MiBPártelo en varios lotes
429 too_many_active_batchesYa tienes en marcha los lotes que permite tu planEspera a que terminen
authorization_revoked en las líneasLa llave, la membresía o la organización dejó de ser válida mientras corríaRevisa la llave y vuelve a crear el lote
Sigue en validating un buen ratoEl arranque se reintenta solo. Puede tardar hasta cerca de media hora en empezarEspera; no vuelvas a crearlo, porque devolvería el mismo lote
in_progress sin avanzarLa capacidad del modelo está ocupada con otro trabajo del ClienteEs normal en lotes grandes. Si no avanza en horas, repórtalo con el identificador del lote

Siguiente paso​