Python SDK / Configuration
Configuration
Three dataclasses shape a session: AvatarConfig (the persona),
ClientOptions (where the server is), and
SessionOptions (per-session media settings).
from zeli import AvatarConfig, ClientOptions, SessionOptionsAvatarConfig
Describes the avatar persona to bring to life.
| Field | Type | Default | Description |
|---|---|---|---|
avatar_id | str | None | None | Prepared avatar to render (uploaded files are keyed by filename stem). If omitted, the server default is used. |
name | str | None | None | Human-friendly persona label. |
voice_id | str | None | None | Voice to speak with. |
llm_id | str | None | None | Model to answer this session's turns with. Sent on the connection body; a model the server will not run raises ModelRefusedError rather than falling back. |
system_prompt | str | None | None | System prompt priming the model. |
language_code | str | None | None | BCP-47 language, e.g. "en". |
emotion_responsive | bool | False | Reserved. Never transmitted; the server matches tone clips on its own. See Emotions. |
enhance | bool | False | Region-limited mouth restoration for sharper lips/teeth. |
enhance_strength | float | None | None | Blend factor for enhance in [0, 1]. |
loop_mode | "boomerang" | "forward" | None | None | How the idle loop repeats while speaking. |
max_session_length_seconds | int | None | None | Seconds this session may run before the server ends it. Sent on the control gateway's upgrade URL. Omit for the server's own ceiling; pass 0 to ask for none. |
Four of these fields reach the server and the other seven do not.
On the wire: avatar_id and llm_id travel on the
connection body, voice_id on every control gateway command, and
max_session_length_seconds on the control gateway's upgrade URL.
The remaining seven are reserved: they are never transmitted, so
setting one has no effect and no error, only a warning on the zeli
logger naming it. Configure those on the server instead, through its
/api/settings.
Two fields have moved off that list since it was written, because the server grew
the thing each was asking for: max_session_length_seconds once a
session ceiling existed, and llm_id once a session could choose its
own model. A list of dead fields that outlives the death is worse than no list,
because it teaches you not to use something that works.
ClientOptions
Client-wide settings: where the server is and how to reach it.
| Field | Type | Default | Description |
|---|---|---|---|
server_url | str | "http://localhost:8080" | Base URL of the avatar server. |
connect_path | str | "/connect" | Persistent WebRTC offer/answer path. |
ice_servers | list[dict] | None | Google STUN | ICE servers for NAT traversal. |
connect_timeout | float | 30.0 | Seconds to wait for the media connection. |
client_label | str | None | None | Optional label attached to requests for observability. |
SessionOptions
Per-session settings passed to connect().
| Field | Type | Default | Description |
|---|---|---|---|
video_quality | "high" | "auto" | "high" | "auto" lets the server adapt to bandwidth. |
receive_video | bool | True | Subscribe to the inbound video track. |
receive_audio | bool | True | Subscribe to the inbound audio track. |
Example
client = ZeliClient(
avatar_config=AvatarConfig(
avatar_id="01-presenter-male__confident",
voice_id="your-voice-id",
llm_id="au.anthropic.claude-opus-5-5",
max_session_length_seconds=900,
),
options=ClientOptions(
server_url="https://avatar.zelibot.xyz",
connect_timeout=20.0,
),
)
async with client.connect(SessionOptions(video_quality="auto")) as session:
...