Use the API from code
InferNode is compatible with the OpenAI SDK: change the base URL and use your InferNode key.
Before you start
- An InferNode API key (it starts with
sk-saptiva-). You get it in API > API Keys in Studio (if your Client has the section) or from your administrator. - The base URL of your installation. InferNode is served under the
/infernodepath of the API domain:https://api.<tu-dominio>/infernode/v1. Without a key,GET /v1/modelsresponds401with"code": "identity.invalid_token"; with a mistyped or revoked key,identity.unauthorized. This is how you check that the URL is correct before you request a key.
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)
With streaming:
for chunk in client.chat.completions.create(model="Saptiva Turbo", messages=msgs, stream=True):
print(chunk.choices[0].delta.content or "", end="")
Tools (function calling)
Models with the tool-use capability accept tools in the same format as OpenAI. The model returns tool_calls; your code runs the function and replies with a role: "tool" message.
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]
# run buscar_alumno(**json.loads(call.function.arguments)) and append the result:
msgs += [resp.choices[0].message, {"role": "tool", "tool_call_id": call.id, "content": "..."}]
JSON output
With response_format={"type": "json_object"} and an instruction that describes the schema, a model with json-mode returns valid JSON. This is how you use InferNode to extract data without an agent.
Embeddings
vec = client.embeddings.create(
model="Saptiva Embed",
input=["Explica la gravedad", "Qué es un agujero negro"], # several texts in one call
dimensions=1024,
encoding_format="float",
)
print(len(vec.data), len(vec.data[0].embedding)) # 2 1024
inputaccepts a list: several texts in a single call, one vector per text and in the same order.dimensionstruncates the vector. The platform's embeddings model delivers up to 4096; with 1024 the index takes up a quarter of the space, and for semantic search it is usually enough. Set the value once and do not change it: vectors of different sizes cannot be compared.encoding_format:float(a list of numbers, the usual choice) orbase64(a string; 1024 dimensions are 4096 bytes). Usebase64only if you move many vectors and the response size matters to you.
Use the same model, and the same dimensions, to index and to query.
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
The -i option shows the X-RateLimit-* and X-Request-Cost headers.
Which model to use
The identifiers in the examples belong to a specific installation and do not work in all of them: each Client has its own catalog.
The model field expects the model name with its spaces and parentheses, for example Qwen3.6 35B A3B (FP8). It is not the repository path (Qwen/Qwen3.6-35B-A3B-FP8): that form responds 404 with "code": "model_not_found". Copy the value exactly as GET /v1/models returns it.
GET /v1/models returns the valid names for your key, and it is the first thing you should run. They are the same identifiers you see in the Model catalog (administrators).
Choose a model: use the alias
Always call by the alias. An alias is a stable product name (Saptiva Turbo) pointing at the current model; the exact Catalog name (Qwen3.6 35B A3B (FP8)) carries the version inside it, so it stops existing as soon as the model is updated and your code starts getting model_not_found. With the alias you change nothing: the response carries in model the model that answered, and GET /v1/models/{alias} tells you which one it points at through the X-Alias-Of header.
GET /v1/models/aliases lists them with their capabilities, context window and what each one is intended for. These five ship by default; an installation can add others.
| Alias | What for | Capabilities | Reasons before responding |
|---|---|---|---|
| Saptiva Turbo | Quick everyday tasks: chats, summaries, assistants with many users at once | chat, code, json-mode, tools, vision | It can, but it is better to turn it off (below) |
| Saptiva Omni | The main model for agents: tools, documents, images, multi-step tasks | chat, json-mode, reasoning, tools, vision, audio, video | Yes |
| Saptiva Cortex | Problems that call for deep reasoning: analysis, planning, complex code | chat, code, json-mode, reasoning, tools, vision | Yes |
| Saptiva Embed | Indexing, searching by meaning, RAG | embeddings, multilingual | Not applicable |
| Saptiva Guard | Moderating inputs and outputs against communication policies | moderation | Not applicable |
Quick rules: to classify, extract or reformat, Saptiva Turbo with temperature: 0 and reasoning turned off; to answer with documents or chain tools, Saptiva Omni; when the problem truly requires thinking, Saptiva Cortex with a generous max_tokens. If you need one specific version instead of the current one, the exact Catalog name is still valid: that is the exception, not the rule. GET /v1/models/aliases always returns the aliases in your installation.
Control reasoning
Reasoning models spend tokens thinking before they write, and that spending comes out of your max_tokens. Three parameters control it. InferNode does not interpret them: it forwards them to the model as is (they appear in the X-Saptiva-Passthrough header).
{
"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}
}
The difference, measured with the same question:
| With reasoning turned off | Without turning it off (max_tokens: 512) | |
|---|---|---|
content | "listo" | empty |
| Output tokens | 3 | 512, all spent thinking |
- For chats, summaries and classification, use Saptiva Turbo with
enable_thinking: falseand"effort": "none", as above. A direct and inexpensive response. - For the model to think (Omni or Cortex), remove those three parameters or set
enable_thinking: truewith a matchingreasoning_budget, and raisemax_tokensso there is room left after reasoning. The reasoning arrives in the message'sreasoningfield; the response, incontent. - If your application cuts off on timeout, switch to
"stream": true: tokens arrive as they are generated and no proxy closes the connection for being silent.
Parameters that chat/completions accepts
In addition to the OpenAI ones (messages, max_tokens or max_completion_tokens, temperature 0-2, top_p, n up to 8, stream and stream_options.include_usage, stop, presence_penalty, frequency_penalty, logit_bias, seed, response_format, tools, tool_choice, parallel_tool_calls, logprobs, top_logprobs up to 20, user), InferNode accepts:
| Parameter | What it does |
|---|---|
top_k, min_p, repetition_penalty | Sampling controls for local models. |
guard: true | Evaluates the conversation with the content guards after responding; the result is stored in GET /v1/guard/events. |
metadata | Free-form tags stored with the usage event (for example {"proyecto": "becas"}), so you can break down spending later. It is not sent to the model. |
Keys that InferNode does not recognize are forwarded to the model as is and listed in the X-Saptiva-Passthrough header; the default values the administrator set for that model appear in X-Saptiva-Model-Defaults. With reasoning models, the response includes reasoning in the assistant message, in addition to content.
Handling limits
- On a
429, readRetry-After(seconds) orX-RateLimit-Reset-Requests/-Tokensand wait until then; the body indicates indetails.dimensionwhich limit was exceeded (rpm,rpd,tpm,tpdoripmfor images). Do not retry in a loop. - On a
502, retry with exponential backoff (1 s, 2 s, 4 s) up to three times. - On a
504withprovider_timeout, the model did not respond; retrying within seconds does not help. Switch models or wait (InferNode is not responding). - Save
X-Request-Idin your logs: it is what Saptiva AI needs to investigate a call. - If
X-Model-Deprecated: trueappears, the model is being retired;X-Model-Retires-Atcarries the date. If you used an alias,X-Alias-Ofindicates the actual model that responded. Plan the switch before that date.
Long inference processes
For a large batch or long generations, the enemy is not the model: it is dropped connections, per-minute limits and not knowing how much you have spent so far. Four practices:
Request the response in streaming. A long generation without stream keeps the connection silent until the end, and any proxy or client with a short timeout cuts it off early. With stream: true tokens arrive as they are generated:
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}
}'
The -N turns off curl's buffer so you see the chunks right away. With stream_options.include_usage, the last chunk carries the token count; in streaming the X-RateLimit-*-Tokens headers do not arrive, because the count happens after the header is sent.
Watch out for reasoning models. Models that reason before responding spend tokens thinking, and that spending comes out of your max_tokens. With a low cap, the response arrives with an empty content and a length finish_reason: the model ran out of room before writing. In one test, asking for a single word consumed 192 output tokens. Leave at least a few hundred tokens, and read the reasoning in the message's reasoning field if you are interested.
Bound each call. Always set max_tokens: without it, an ambiguous instruction can consume the model's maximum in each iteration of the batch. Check the available context beforehand with GET /v1/models/{name} (context_window, max_output_tokens). The window is shared between the prompt and max_tokens, so the sum of the two is what has to fit: requesting max_tokens equal to the context_window always fails (Context exceeded). And a request cannot last longer than about 300 seconds, which bounds the response to what the model manages to generate in that time (Measured performance).
Iterate within the limits. A sequential batch that reads X-RateLimit-Remaining-Requests and X-RateLimit-Remaining-Tokens in each response knows when to slow down before the 429; if the 429 arrives, wait for as long as Retry-After says and continue where you left off (Handling limits). Do not launch the batch in parallel without knowing your rpm and tpm: you only turn the limit into errors.
Tag and measure spending. Send metadata with a tag for the process ({"proyecto": "lote-agosto"}): each call is marked and GET /v1/billing/usage?period=AAAA-MM later breaks down tokens and cost for you. In real time, X-Request-Cost carries the cost of the call and X-Credits-Remaining the balance; if X-Credits-Warning appears, the process is about to run out of credits halfway through the batch.
And save the X-Request-Id of each call in your logs. When a response in the batch looks odd, that identifier is all you need to trace it.
From a lab notebook
Inside a notebook you call InferNode the same way as from your machine: it is the same URL and the same key. The only thing that changes is where you store the secret.
Do not write the key in a cell. The notebook is saved with its output and often ends up in a repository. Put it in a session environment variable and read it from the code:
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)
To define the variable without it staying in the notebook, use the lab terminal (Notebooks, terminal button) before you open it:
export INFERNODE_API_KEY='sk-saptiva-...'
If you prefer to check the connection from the terminal before writing code:
curl https://api.<tu-dominio>/infernode/v1/me \
-H "Authorization: Bearer $INFERNODE_API_KEY" | jq
It responds with your organization, your plan and the key in use.
If it returns 401 with "code": "identity.invalid_token" and the message Missing or malformed bearer token, the variable is empty: the typical case is having written the file with the key and not having loaded it with source, or being in another terminal. The message is identical to the one for a nonexistent key, so before you doubt the key, check the variable with echo ${INFERNODE_API_KEY:+definida}. If it returns 401 with identity.unauthorized, the key did arrive but it has already been rotated or revoked. And if nothing responds, the lab has no outbound access to that domain and you need to request it from whoever operates the installation.
If the server answers, even with a 401, that already tells you the network is fine: the request_id in the response was issued by InferNode.
Two habits that save trouble:
- The notebook shares the lab's quota. A call to InferNode does not consume CPU or GPU from your lab (the model runs elsewhere), but it does count against the limits of your key, which are per person.
- If you are going to process a batch, apply what is in Long inference processes: streaming, a bounded
max_tokensand a pause when you receive a429.
Manage your key
Your key is personal: you can only list, rotate and revoke your own, and nobody else can rotate them for you. Everything is done against /v1/me/api-keys with the key you have now.
View your keys. The secret is never shown again, so to identify a key you go by its name and by key_suffix_4 (the last four characters):
curl https://api.<tu-dominio>/infernode/v1/me/api-keys \
-H "Authorization: Bearer $INFERNODE_API_KEY" | jq
The response carries items with each key (without the secret), active_count and quota, which is the maximum number of keys your plan allows.
Rotate. Take the key's id from the previous step and call rotate. The response is a 201 with the new secret, and rotated_from_id points to the key you just left behind:
curl -X POST https://api.<tu-dominio>/infernode/v1/me/api-keys/<key_id>/rotate \
-H "Authorization: Bearer $INFERNODE_API_KEY" | jq
Rotating revokes the old key in the same operation, and the new secret is shown only once. Save it before you close the terminal and update your scripts and environment variables right away: any process that keeps using the previous key will start receiving 401 with identity.unauthorized.
Revoke a key you no longer use, without creating another:
curl -X DELETE https://api.<tu-dominio>/infernode/v1/me/api-keys/<key_id> \
-H "Authorization: Bearer $INFERNODE_API_KEY"
It responds 204 with no body.
Check which key you are calling with. GET /v1/me returns your organization, your membership, the plan (with its limits) and the key in use, including key_suffix_4 and last_used_at:
curl https://api.<tu-dominio>/infernode/v1/me \
-H "Authorization: Bearer $INFERNODE_API_KEY" | jq
It is the fastest way to confirm that a new key is active.
If a key of yours leaked (it ended up in a chat, a repository or a log), rotate it as soon as you notice: it is a single call and you do not need an administrator.