Saltar al contenido principal

Usar la API desde código

InferNode es compatible con el SDK de OpenAI: cambia la URL base y usa tu llave de InferNode.

Antes de empezar​

  • Una llave de API de InferNode (empieza por sk-saptiva-). La obtienes en API > API Keys de Studio (si tu Cliente tiene la sección) o de tu administrador.
  • La URL base de tu instalación. InferNode se sirve bajo la ruta /infernode del dominio de la API: https://api.<tu-dominio>/infernode/v1. Sin llave, GET /v1/models responde 401 con "code": "identity.invalid_token"; con una llave mal escrita o revocada, identity.unauthorized. Es la forma de comprobar que la URL es correcta antes de pedir una llave.

Python​

from openai import OpenAI

client = OpenAI(
base_url="https://api.<tu-dominio>/infernode/v1",
api_key="sk-saptiva-...",
)

resp = client.chat.completions.create(
model="Saptiva Turbo",
messages=[{"role": "user", "content": "Resume este texto en tres líneas: ..."}],
temperature=0.2,
)
print(resp.choices[0].message.content)

Con streaming:

for chunk in client.chat.completions.create(model="Saptiva Turbo", messages=msgs, stream=True):
print(chunk.choices[0].delta.content or "", end="")

Herramientas (function calling)​

Los modelos con la capacidad tool-use aceptan tools con el mismo formato de OpenAI. El modelo devuelve tool_calls; tu código ejecuta la función y responde con un mensaje role: "tool".

tools = [{
"type": "function",
"function": {
"name": "buscar_alumno",
"description": "Busca un alumno por matrícula",
"parameters": {"type": "object", "properties": {"matricula": {"type": "string"}}, "required": ["matricula"]},
},
}]
resp = client.chat.completions.create(model="Saptiva Turbo", messages=msgs, tools=tools)
call = resp.choices[0].message.tool_calls[0]
# ejecuta buscar_alumno(**json.loads(call.function.arguments)) y añade el resultado:
msgs += [resp.choices[0].message, {"role": "tool", "tool_call_id": call.id, "content": "..."}]

Salida en JSON​

Con response_format={"type": "json_object"} y una instrucción que describa el esquema, un modelo con json-mode devuelve JSON válido. Es la forma de usar InferNode para extraer datos sin un agente.

Embeddings​

vec = client.embeddings.create(
model="Saptiva Embed",
input=["Explica la gravedad", "Qué es un agujero negro"], # varios textos en una llamada
dimensions=1024,
encoding_format="float",
)
print(len(vec.data), len(vec.data[0].embedding)) # 2 1024
  • input acepta una lista: varios textos en una sola llamada, un vector por texto y en el mismo orden.
  • dimensions recorta el vector. El modelo de embeddings de la plataforma entrega hasta 4096; con 1024 el índice ocupa la cuarta parte y para búsqueda semántica suele bastar. Fija el valor una vez y no lo cambies: vectores de distinto tamaño no se comparan.
  • encoding_format: float (lista de números, el habitual) o base64 (una cadena; 1024 dimensiones son 4096 bytes). Usa base64 solo si mueves muchos vectores y te importa el tamaño de la respuesta.

Usa el mismo modelo, y las mismas dimensiones, para indexar y para consultar.

curl​

curl https://api.<tu-dominio>/infernode/v1/chat/completions \
-H "Authorization: Bearer sk-saptiva-..." \
-H "Content-Type: application/json" \
-d '{"model":"Saptiva Turbo","messages":[{"role":"user","content":"Hola"}]}' -i

La opción -i muestra las cabeceras X-RateLimit-* y X-Request-Cost.

Qué modelo usar​

Los identificadores de los ejemplos son los de una instalación concreta y no valen en todas: cada Cliente tiene su propio catálogo.

El identificador es el nombre, tal cual

El campo model espera el nombre del modelo con sus espacios y paréntesis, por ejemplo Qwen3.6 35B A3B (FP8). No es la ruta del repositorio (Qwen/Qwen3.6-35B-A3B-FP8): esa forma responde 404 con "code": "model_not_found". Copia el valor tal como lo devuelve GET /v1/models.

GET /v1/models devuelve los nombres válidos para tu llave, y es lo primero que conviene correr. Son los mismos identificadores que ves en el Catálogo de modelos (administradores).

Elegir modelo: usa el alias​

Llama siempre por el alias. Un alias es un nombre de producto estable (Saptiva Turbo) que apunta al modelo vigente; el nombre exacto del Catálogo (Qwen3.6 35B A3B (FP8)) lleva la versión dentro, así que deja de existir en cuanto el modelo se actualiza y tu código empieza a recibir model_not_found. Con el alias no cambias nada: la respuesta trae en model el modelo que contestó, y GET /v1/models/{alias} te dice a cuál apunta con la cabecera X-Alias-Of.

GET /v1/models/aliases los lista con sus capacidades, ventana de contexto y para qué está pensado cada uno. Estos cinco vienen de fábrica; una instalación puede añadir otros.

AliasPara quéCapacidadesRazona antes de responder
Saptiva TurboTareas rápidas del día a día: chats, resúmenes, asistentes con muchos usuarios a la vezchat, código, json-mode, herramientas, visiónPuede, pero conviene apagarlo (abajo)
Saptiva OmniEl modelo principal de los agentes: herramientas, documentos, imágenes, tareas de varios pasoschat, json-mode, razonamiento, herramientas, visión, audio, videoSí
Saptiva CortexProblemas que piden razonamiento profundo: análisis, planeación, código complejochat, código, json-mode, razonamiento, herramientas, visiónSí
Saptiva EmbedIndexar, buscar por significado, RAGembeddings, multilingüeNo aplica
Saptiva GuardModerar entradas y salidas contra las políticas de comunicaciónmoderaciónNo aplica

Reglas rápidas: para clasificar, extraer o reformatear, Saptiva Turbo con temperature: 0 y el razonamiento apagado; para responder con documentos o encadenar herramientas, Saptiva Omni; cuando el problema de verdad requiere pensar, Saptiva Cortex con max_tokens amplio. Si necesitas una versión concreta y no la vigente, el nombre exacto del Catálogo sigue siendo válido: es la excepción, no la norma. GET /v1/models/aliases siempre devuelve los alias de tu instalación.

Controlar el razonamiento​

Los modelos que razonan gastan tokens en pensar antes de escribir, y ese gasto sale de tu max_tokens. Tres parámetros lo controlan. InferNode no los interpreta: los reenvía al modelo tal cual (aparecen en la cabecera X-Saptiva-Passthrough).

{
"model": "Saptiva Turbo",
"messages": [
{"role": "system", "content": "Responde en una sola palabra."},
{"role": "user", "content": "Di: listo"}
],
"max_tokens": 4096,
"temperature": 0.7,
"thinking_token_budget": 4096,
"chat_template_kwargs": {"enable_thinking": false, "reasoning_budget": 4096},
"reasoning": {"effort": "none"},
"stream": false,
"stream_options": {"include_usage": true}
}

La diferencia, medida con la misma pregunta:

Con el razonamiento apagadoSin apagarlo (max_tokens: 512)
content"listo"vacío
Tokens de salida3512, todos gastados en pensar
  • Para chats, resúmenes y clasificación, usa Saptiva Turbo con enable_thinking: false y "effort": "none", como arriba. Respuesta directa y barata.
  • Para que el modelo piense (Omni o Cortex), quita esos tres parámetros o pon enable_thinking: true con un reasoning_budget acorde, y sube max_tokens para que quede margen después de razonar. El razonamiento llega en el campo reasoning del mensaje; la respuesta, en content.
  • Si tu aplicación corta por tiempo de espera, cambia a "stream": true: los tokens llegan según se generan y ningún proxy cierra la conexión por silencio.

Parámetros que acepta chat/completions​

Además de los de OpenAI (messages, max_tokens o max_completion_tokens, temperature 0-2, top_p, n hasta 8, stream y stream_options.include_usage, stop, presence_penalty, frequency_penalty, logit_bias, seed, response_format, tools, tool_choice, parallel_tool_calls, logprobs, top_logprobs hasta 20, user), InferNode acepta:

ParámetroQué hace
top_k, min_p, repetition_penaltyControles de muestreo de los modelos locales.
guard: trueEvalúa la conversación con las guardas de contenido después de responder; el resultado queda en GET /v1/guard/events.
metadataEtiquetas libres que se guardan con el evento de consumo (por ejemplo {"proyecto": "becas"}), para desglosar el gasto después. No se envía al modelo.

Las claves que InferNode no reconoce se reenvían al modelo tal cual y se listan en la cabecera X-Saptiva-Passthrough; los valores por defecto que el administrador fijó para ese modelo aparecen en X-Saptiva-Model-Defaults. Con modelos de razonamiento, la respuesta incluye reasoning en el mensaje del asistente, además de content.

Manejo de límites​

  • Ante 429, lee Retry-After (segundos) o X-RateLimit-Reset-Requests / -Tokens y espera hasta entonces; el cuerpo indica en details.dimension qué límite se superó (rpm, rpd, tpm, tpd o ipm para imágenes). No reintentes en bucle.
  • Ante 502, reintenta con espera exponencial (1 s, 2 s, 4 s) hasta tres veces.
  • Ante 504 con provider_timeout, el modelo no respondió; reintentar en segundos no ayuda. Cambia de modelo o espera (InferNode no responde).
  • Guarda X-Request-Id en tus logs: es lo que Saptiva AI necesita para investigar una llamada.
  • Si aparece X-Model-Deprecated: true, el modelo está en retiro; X-Model-Retires-At trae la fecha. Si usaste un alias, X-Alias-Of indica el modelo real que respondió. Planifica el cambio antes de la fecha.

Procesos largos de inferencia​

Para un lote grande o generaciones largas, el enemigo no es el modelo: son los cortes de conexión, los límites por minuto y no saber cuánto llevas gastado. Cuatro prácticas:

Pide la respuesta en streaming. Una generación larga sin stream mantiene la conexión callada hasta el final, y cualquier proxy o cliente con timeout corto la corta antes de tiempo. Con stream: true los tokens llegan conforme se generan:

curl -N https://api.<tu-dominio>/infernode/v1/chat/completions \
-H "Authorization: Bearer $INFERNODE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "nombre del modelo",
"messages": [{"role": "user", "content": "..."}],
"max_tokens": 2048,
"stream": true,
"stream_options": {"include_usage": true}
}'

El -N desactiva el buffer de curl para ver los chunks al momento. Con stream_options.include_usage, el último chunk trae el conteo de tokens; en streaming las cabeceras X-RateLimit-*-Tokens no llegan, porque el conteo ocurre después de enviar la cabecera.

Cuidado con los modelos de razonamiento. Los que razonan antes de responder gastan tokens en pensar, y ese gasto sale de tu max_tokens. Con un tope bajo, la respuesta llega con content vacío y finish_reason de longitud: el modelo se quedó sin margen antes de escribir. En una prueba, pedir una sola palabra consumió 192 tokens de salida. Deja al menos unos cientos de tokens, y lee el razonamiento en el campo reasoning del mensaje si te interesa.

Acota cada llamada. Fija siempre max_tokens: sin él, una instrucción ambigua puede consumir el máximo del modelo en cada iteración del lote. Verifica antes el contexto disponible con GET /v1/models/{name} (context_window, max_output_tokens). La ventana se reparte entre el prompt y max_tokens, así que la suma de los dos es la que tiene que caber: pedir max_tokens igual al context_window falla siempre (Contexto excedido). Y una petición no puede durar más de unos 300 segundos, lo que acota la respuesta a lo que el modelo alcance a generar en ese tiempo (Rendimiento medido).

Itera respetando los límites. Un lote secuencial que lee X-RateLimit-Remaining-Requests y X-RateLimit-Remaining-Tokens en cada respuesta sabe cuándo frenar antes del 429; si el 429 llega, espera lo que diga Retry-After y continúa donde ibas (Manejo de límites). No lances el lote en paralelo sin saber tu rpm y tpm: solo conviertes el límite en errores.

Etiqueta y mide el gasto. Manda metadata con una etiqueta del proceso ({"proyecto": "lote-agosto"}): cada llamada queda marcada y GET /v1/billing/usage?period=AAAA-MM te desglosa después tokens y costo. En caliente, X-Request-Cost trae el costo de la llamada y X-Credits-Remaining el saldo; si aparece X-Credits-Warning, el proceso está por quedarse sin créditos a medio lote.

Y guarda el X-Request-Id de cada llamada en tus registros. Cuando una respuesta del lote salga rara, ese identificador es lo único que hace falta para rastrearla.

Desde un notebook del laboratorio​

Dentro de un notebook llamas a InferNode igual que desde tu máquina: es la misma URL y la misma llave. Lo único que cambia es dónde guardas el secreto.

No escribas la llave en una celda. El notebook se guarda con su salida y suele acabar en un repositorio. Ponla en una variable de entorno de la sesión y léela desde el código:

import os
from openai import OpenAI

client = OpenAI(
api_key=os.environ["INFERNODE_API_KEY"],
base_url="https://api.<tu-dominio>/infernode/v1",
)

resp = client.chat.completions.create(
model="<nombre del modelo>",
messages=[{"role": "user", "content": "Resume este párrafo en una línea."}],
max_tokens=512,
)
print(resp.choices[0].message.content)

Para definir la variable sin que quede en el notebook, usa la terminal del laboratorio (Notebooks, botón de terminal) antes de abrirlo:

export INFERNODE_API_KEY='sk-saptiva-...'

Si prefieres comprobar la conexión desde la terminal antes de escribir código:

curl https://api.<tu-dominio>/infernode/v1/me \
-H "Authorization: Bearer $INFERNODE_API_KEY" | jq

Responde con tu organización, tu plan y la llave en uso.

Si devuelve 401 con "code": "identity.invalid_token" y el mensaje Missing or malformed bearer token, la variable está vacía: el caso típico es haber escrito el archivo con la llave y no haberlo cargado con source, o estar en otra terminal. El mensaje es idéntico al de una llave inexistente, así que antes de dudar de la llave comprueba la variable con echo ${INFERNODE_API_KEY:+definida}. Si devuelve 401 con identity.unauthorized, la llave sí llegó pero ya se rotó o se revocó. Y si no responde nada, el laboratorio no tiene salida a ese dominio y hay que pedirlo a quien opera la instalación.

Que el servidor conteste, aunque sea con un 401, ya te dice que la red está bien: el request_id de la respuesta lo emitió InferNode.

Dos costumbres que ahorran problemas:

  • El notebook comparte la cuota del laboratorio. Una llamada a InferNode no consume CPU ni GPU de tu laboratorio (el modelo corre fuera), pero sí cuenta contra los límites de tu llave, que son por persona.
  • Si vas a procesar un lote, aplica lo de Procesos largos de inferencia: streaming, max_tokens acotado y pausa al recibir 429.

Administrar tu llave​

Tu llave es personal: solo puedes listar, rotar y revocar las tuyas, y nadie más puede rotarlas por ti. Todo se hace contra /v1/me/api-keys con la llave que tienes ahora.

Ver tus llaves. El secreto no se vuelve a mostrar nunca, así que para identificar una te guías por su name y por key_suffix_4 (los cuatro últimos caracteres):

curl https://api.<tu-dominio>/infernode/v1/me/api-keys \
-H "Authorization: Bearer $INFERNODE_API_KEY" | jq

La respuesta trae items con cada llave (sin el secreto), active_count y quota, que es el máximo de llaves que permite tu plan.

Rotar. Toma el id de la llave del paso anterior y llama a rotate. La respuesta es un 201 con el nuevo secreto, y rotated_from_id apunta a la que acabas de dejar atrás:

curl -X POST https://api.<tu-dominio>/infernode/v1/me/api-keys/<key_id>/rotate \
-H "Authorization: Bearer $INFERNODE_API_KEY" | jq
La anterior deja de valer al instante

Rotar revoca la llave vieja en la misma operación, y el secreto nuevo se muestra una sola vez. Guárdalo antes de cerrar la terminal y actualiza tus scripts y variables de entorno enseguida: cualquier proceso que siga usando la anterior empezará a recibir 401 con identity.unauthorized.

Revocar una llave que ya no usas, sin crear otra:

curl -X DELETE https://api.<tu-dominio>/infernode/v1/me/api-keys/<key_id> \
-H "Authorization: Bearer $INFERNODE_API_KEY"

Responde 204 sin cuerpo.

Comprobar con qué llave estás llamando. GET /v1/me devuelve tu organización, tu membresía, el plan (con sus límites) y la llave en uso, incluidos key_suffix_4 y last_used_at:

curl https://api.<tu-dominio>/infernode/v1/me \
-H "Authorization: Bearer $INFERNODE_API_KEY" | jq

Es la forma más rápida de confirmar que una llave nueva quedó activa.

Si una llave se te filtró (quedó en un chat, en un repositorio o en un registro), rótala en cuanto lo notes: es una sola llamada y no necesitas a un administrador.

Siguiente paso​