API reference / Session settings
Session settings
These defaults are separate from named Personas. Use GET /auth/settings and POST /auth/settings with a signed-in account or full API key for settings without an active stream. The compatible /api/settings routes below are available during a running avatar session. A streaming token cannot save settings.
Read the record
Takes no parameters. Whose settings these are comes from the credential, never from the request.
curl "https://zeli-avatar.zelibot.xyz/api/settings" -H "X-Api-Key: zsk_live_..."const { settings, models, effort_levels, tts_providers, persisted } =
await admin.management.getSettings();{
"settings": {
"provider": "anthropic",
"model": "au.anthropic.claude-sonnet-5",
"live_model": "au.anthropic.claude-haiku-4-5",
"system_prompt": "You are a friendly front desk host.",
"max_tokens": 1024,
"top_p": null,
"temperature": null,
"effort": "medium",
"reasoning": "low",
"tts_provider": "cartesia",
"voice_id": "a0e99841-...",
"voice_id_chatterbox": null,
"avatar": "01-presenter-male",
"emotion_responsive": true
},
"models": [],
"effort_levels": ["low", "medium", "high", "xhigh", "max"],
"reasoning_levels": ["off", "low", "medium", "high"],
"tts_providers": [],
"persisted": true
}| Field | Meaning |
|---|---|
settings | This caller's effective record. The whole of it |
models | The model catalogue, documented under LLMs |
effort_levels | Reasoning effort values the models accept, low to high |
reasoning_levels | How long the model may think before it speaks, off to high |
tts_providers | The voice engines this deployment can run, with their capability flags |
persisted | Whether the settings store is reachable. false means a save will not survive the avatar server |
The payload includes system_prompt, which is why it is scoped to
the caller rather than to the box. It used to answer everybody with whoever had
configured the machine last, which on a shared avatar server meant handing out another
tenant's persona.
Write the record
Requires a full key. Send only the fields you want to change; the rest are left alone. Writes nothing to process state, so a save landing during somebody else's reply cannot change it, and cannot change a reply of your own already in flight.
The settable fields are an allowlist, not "whatever was posted":
The quality model, used for a one shot turn.
The low latency model, used for the always on live conversation.
The persona's instructions. This is the entire knowledge surface; see Knowledge.
Reply length ceiling.
Nucleus sampling. Only accepted by models whose
supports_sampling flag is true.
Sampling temperature. Same capability gate as top_p.
One of effort_levels. max is accepted only by
models flagged effort_max. Used only while
reasoning is unset; once a thinking level is chosen, that
level sets the effort.
How long the model thinks before it speaks: one of
reasoning_levels (off, low,
medium, high). Higher levels think longer before
speaking, so replies start later. An empty string or null is
automatic: each model's own default, shown as
default_reasoning in the model
catalogue (low on Claude Opus 5 and Sonnet 5, which think
unless told otherwise; off elsewhere). A value that is not a
level is treated as automatic rather than refused. The level is chosen for
the live_model; the other model takes it only when it takes a
level the same way, and otherwise runs at its own default. A model with no
thinking control ignores it.
What the avatar says before a tool runs when the model said nothing first,
in this persona's own words: {tool name: {language: line}},
for example {"end_call": {"en": "Bye for now."}}.
Languages are en es fr de it pt hi ja; a line is one sentence
of at most 160 characters, with no angle or square brackets; at most 32
tools. The line is chosen in the
call's current language, then the built in line for that language, and
English only as a last resort. skip_turn cannot have one: it is
silent on purpose. A refused value answers 400.
Which voice engine to speak with.
The default avatar for this caller's sessions.
Whether to infer a delivery tone rather than always speaking neutrally.
The Cartesia voice slot, kept under this name for compatibility. Every other
provider gets voice_id_<provider>, matched by prefix.
This persona's custom tools, at most ten. Written whole: send the full list each time, and an empty list clears it. The definition fields and their limits are on the Tools page.
curl -X POST "https://zeli-avatar.zelibot.xyz/api/settings" \
-H "X-Api-Key: zsk_live_..." \
-H "Content-Type: application/json" \
-d '{"system_prompt": "You are a friendly front desk host.", "effort": "medium"}'await admin.management.updateSettings({
system_prompt: "You are a friendly front desk host.",
effort: "medium",
});Response 200:
{ "ok": true, "saved": true, "settings": {} }saved is whether the record reached durable storage. settings is the whole
record after the write, so one round trip is enough.
A refusal is a 400 with ok: false and a reason, not a 500. There are two
reasons, and the message names which one it is:
{
"ok": false,
"error": {
"code": "setting_refused",
"message": "..."
}
}A model that does not exist, or one this deployment will not send data to. The
second is the residency rule: see the selectable flag under
LLMs.
Give the persona tools
A persona's custom tools live on the same record, under tools. Defining one is
a settings write, so it needs a full key and belongs on your server. Answering
one happens wherever the session runs, through the SDK.
// Server side, full key: define the tool.
await admin.management.updateSettings({
tools: [
{
name: "book_a_tour",
description: "Book a tour of the building. Call it once the person has named a day.",
parameters: {
type: "object",
properties: { day: { type: "string" } },
required: ["day"],
},
mode: "await_result",
timeout_seconds: 8,
},
],
});
// Where the session runs: answer it. Register before connect.
client.registerToolCallHandler("book_a_tour", async ({ day }) => {
const booking = await tours.book(String(day));
return { confirmed: true, at: booking.time };
});# Server side, full key: define the tool.
await admin.management.update_settings({
"tools": [
{
"name": "book_a_tour",
"description": "Book a tour of the building. Call it once the person has named a day.",
"parameters": {
"type": "object",
"properties": {"day": {"type": "string"}},
"required": ["day"],
},
"mode": "await_result",
"timeout_seconds": 8,
}
]
})
# Where the session runs: answer it. Register before connect.
@client.tool_call_handler("book_a_tour")
async def book_a_tour(args, call):
booking = await tours.book(args["day"])
return {"confirmed": True, "at": booking.time}The read returns the list under settings.tools. The persona only ever sees the
entries that are enabled and valid. A refused definition answers
400 setting_refused and saves nothing from that request.
Whatever a handler returns is read by a language model, and so is the message
of a ToolCallError it throws. Any other error is kept off the wire
and sent as "The tool failed." Return the smallest answer that does the job, never put a secret in a
tool's arguments, description or result, and authorise inside the handler
rather than trusting the arguments. The full list is under
Tools.