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
/infernodedel dominio de la API:https://api.<tu-dominio>/infernode/v1. Sin llave,GET /v1/modelsresponde401con"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
inputacepta una lista: varios textos en una sola llamada, un vector por texto y en el mismo orden.dimensionsrecorta 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) obase64(una cadena; 1024 dimensiones son 4096 bytes). Usabase64solo 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 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.
| Alias | Para qué | Capacidades | Razona antes de responder |
|---|---|---|---|
| Saptiva Turbo | Tareas rápidas del día a día: chats, resúmenes, asistentes con muchos usuarios a la vez | chat, código, json-mode, herramientas, visión | Puede, pero conviene apagarlo (abajo) |
| Saptiva Omni | El modelo principal de los agentes: herramientas, documentos, imágenes, tareas de varios pasos | chat, json-mode, razonamiento, herramientas, visión, audio, video | Sí |
| Saptiva Cortex | Problemas que piden razonamiento profundo: análisis, planeación, código complejo | chat, código, json-mode, razonamiento, herramientas, visión | Sí |
| Saptiva Embed | Indexar, buscar por significado, RAG | embeddings, multilingüe | No aplica |
| Saptiva Guard | Moderar entradas y salidas contra las políticas de comunicación | moderación | No 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 apagado | Sin apagarlo (max_tokens: 512) | |
|---|---|---|
content | "listo" | vacío |
| Tokens de salida | 3 | 512, todos gastados en pensar |
- Para chats, resúmenes y clasificación, usa Saptiva Turbo con
enable_thinking: falsey"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: truecon unreasoning_budgetacorde, y subemax_tokenspara que quede margen después de razonar. El razonamiento llega en el camporeasoningdel mensaje; la respuesta, encontent. - 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ámetro | Qué hace |
|---|---|
top_k, min_p, repetition_penalty | Controles de muestreo de los modelos locales. |
guard: true | Evalúa la conversación con las guardas de contenido después de responder; el resultado queda en GET /v1/guard/events. |
metadata | Etiquetas 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, leeRetry-After(segundos) oX-RateLimit-Reset-Requests/-Tokensy espera hasta entonces; el cuerpo indica endetails.dimensionqué límite se superó (rpm,rpd,tpm,tpdoipmpara imágenes). No reintentes en bucle. - Ante
502, reintenta con espera exponencial (1 s, 2 s, 4 s) hasta tres veces. - Ante
504conprovider_timeout, el modelo no respondió; reintentar en segundos no ayuda. Cambia de modelo o espera (InferNode no responde). - Guarda
X-Request-Iden 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-Attrae la fecha. Si usaste un alias,X-Alias-Ofindica 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_tokensacotado y pausa al recibir429.
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
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.