Deploy and operate¶
The generated application has three processes:
VAANI_ENVIRONMENT=production voicebot check
VAANI_ENVIRONMENT=production voicebot serve --host 0.0.0.0
VAANI_ENVIRONMENT=production voicebot worker start
Worker startup resolves one immutable adapter registry and runs the same
VoiceRuntime adapter preflight used at call start. This registry includes the
bound recording-gcs adapter when the selected preset enables recording; a
missing or mismatched adapter fails startup before the worker can accept a
LiveKit job. Declare noise_cancellation_mode = "cloud" in the environment
document before using a Cloud-only preset noise profile. self-host and
none modes fail closed for that profile rather than silently claiming that
noise filtering is active.
The registry closes a prior composition split: live execution carried the bound recorder, while preset-manifest construction and scripted tests assembled their own adapter tuples. The regression is covered by the live manifest and VoiceRuntime preflight tests.
Deploy the API, worker, and frontend independently when your platform requires it. Keep their pinned backend/frontend versions compatible.
Ownership boundary¶
VAANI owns the accepted LiveKit call: media, configured providers, turns,
standard tools, transcripts, lifecycle, and one final CallResult.
Your deployment owns:
- LiveKit project, agent dispatch, capacity, region, and credentials;
- domains, TLS, authentication, token-endpoint authorization, and CORS;
- phone numbers, carrier, trunks, SIP routing, and transfer destinations;
- business APIs, consent, recording/storage policy, and secret values;
- monitoring, rollout, rollback, and business acceptance.
VAANI does not provision SIP or prove carrier routing.
Admit LiveKit jobs before Assistant execution¶
run_livekit_worker() exposes LiveKit Agents 1.5.17's request admission hook:
from vaani.assistant.worker import run_livekit_worker
async def admit_birla_canary(request) -> None:
# This allocator, its atomic decision, and its counters belong to the
# deployment. VAANI only forwards the provider request.
if await deployment_allocator.assign_to_vaani(request):
await request.accept()
else:
# Keep the call eligible for another worker or a later dispatch try.
await request.reject(terminate=False)
run_livekit_worker(
assistant,
agent_name="birla-shared",
on_request=admit_birla_canary,
)
The callback receives a LiveKit JobRequest before VAANI calls
assistant.run(). It must explicitly await request.accept() or
await request.reject(...); returning without an answer is not acceptance.
When on_request is omitted, VAANI passes None and LiveKit keeps its
automatic accept-all behavior. VAANI does not log the request or inspect its
secrets.
There are two supported SIP canary architectures:
- Shared-name request admission: register legacy and VAANI workers under
the same LiveKit agent name, and make a deployment-owned allocator perform
one atomic choice for each offered request. The non-selected worker must use
reject(terminate=False), allowing LiveKit to offer or assign the call to another eligible worker. This is the suitable pattern when both worker generations share an upstream SIP dispatch rule. - Distinct-name upstream dispatch: register distinct names such as
birla-agent-legacyandbirla-agent-vaani, then have the deployment's SIP integration select the destination with LiveKit's upstreamcreate_dispatchAPI. The selected worker can accept normally; no VAANI allocation policy or shared counter is required.
Do not use the browser token endpoint as evidence for inbound SIP routing. A SIP dispatch rule must target the shared name, or the upstream integration must create a dispatch for the intended distinct name. In a shared-name pool, make the allocator decision idempotent so two workers do not both accept the same offer. A callback failure or no answer is rejected non-terminally by LiveKit.
reject(terminate=True) is terminal: LiveKit will not reassign that job to
another worker, so it can drop a call. Reserve terminal rejection for an
intentional end-of-call decision or a dispatch that cannot be retried; use the
non-terminal form for canary non-selection and capacity declines. Test zero
percent, the chosen canary percentage, and rollback with a redacted real SIP
call before production. Browser-room and token checks are not substitutes.
DID persona routing¶
Persona selection belongs at the deployment's existing upstream dispatch
boundary. Keep one fixed Assistant per persona worker and register each with
the normal run_livekit_worker() call; on_request remains the admission and
capacity boundary immediately before assistant.run(). The deployment may use
an immutable mapping such as the Birla-owned example below:
from types import MappingProxyType
PERSONA_BY_DID_PREFIX = MappingProxyType(
{
"VQ_PAINTER_CONTRACTOR_": "birla-painter-contractor",
"VQ_OPUS_CARE_": "birla-opus-care",
}
)
def agent_name_for_did(did: object) -> str | None:
if not isinstance(did, str):
return None
normalized = did.strip().upper()
return next(
(
agent_name
for prefix, agent_name in PERSONA_BY_DID_PREFIX.items()
if normalized.startswith(prefix)
),
None,
)
agent_name = agent_name_for_did(approved_inbound_did)
if agent_name is None:
# Missing or unknown DID: reject before creating a LiveKit dispatch.
await reject_dispatch()
else:
await create_dispatch(agent_name=agent_name)
This snippet is self-contained in a clean VAANI installation. The repository
checkout also contains examples/birla_loyalty/routing.py as a source-only
Birla deployment example; that examples module is intentionally not packaged
in the VAANI wheel.
The Painter/Contractor and Opus Care workers then each pass their already-built
Assistant to run_livekit_worker(assistant, agent_name=..., on_request=...).
This composition needs no vendor-specific VAANI API or Assistant factory, and
selection happens before preparation/greeting, so a wrong-persona prompt or
business tool cannot run. Unknown and missing DIDs fail closed; they must not
fall through to an Opus Care or Painter default. Normalize only for lookup,
keep the raw DID out of routine logs/evidence, and do not retain it in shared
mutable state. Concurrent dispatches must use the pure per-request lookup so
one caller cannot select another caller's persona.
Credentialed external LiveKit/SIP validation remains pending. VAANI rc43 publishes the hook, but its publication evidence does not certify consumer SIP allocation.
Secrets and safety¶
Inject LiveKit, provider, business API, database, and storage credentials from a
secret manager. Do not bake .env into an image or place values in TOML,
locks, prompts, frontend variables, or logs.
Authorize every business tool server-side. Restrict transfer to reviewed named routes. Use idempotency for side effects. Record only with approved consent, access control, retention, and deletion policies.
Pre-production calls¶
Run VAANI_ENVIRONMENT=production ./scripts/check, then test:
- browser rooms through every production route and language;
- inbound calls through the target number, trunk, and dispatch;
- real test-environment business APIs;
- greeting, interruption, silence, tools, fallback, transfer, and ending;
- participant and assistant transcripts, Judge, recording if enabled, and result publication;
- expected load, worker drain, and failure recovery.
Measure end-of-speech → first-audio latency against the current bot. A passing configuration check is not a provider, browser, or SIP acceptance result.
Monitor¶
Alert on call outcome and lifecycle phase, end-of-speech → first-audio latency, STT/LLM/TTS route and fallback counts, provider/tool/transfer errors, interruption and silence results, transcript/Judge/publication failure, resource cleanup, worker health, and usage/cost completeness.
Correlate with safe call IDs. Keep unknown usage unknown rather than recording
zero. The final CallResult is immutable; sink delivery status remains
separate.
Route Health is worker-local and starts empty after restart. It suppresses a failing target only under the locked Resilience Policy, probes it after the policy delay, and affects new operations—not already completed work. See configuration and defaults for exact fields.
Roll back¶
Keep the previous pinned deployment available:
- stop routing new calls to the new workers;
- route new calls to the previous version;
- let existing calls finish on their original runtime;
- verify worker drain and terminal results from both versions.
Do not move an active call between runtime versions or mutate an already published result.
Release checklist¶
- [ ] Exact private runtime, environment documents, and locks are reviewed.
- [ ] Secrets are injected and logs are redacted.
- [ ] Recording consent, access, retention, deletion, required-failure behavior, and lock identity passed when enabled.
- [ ] Browser, provider, business API, and inbound SIP calls passed.
- [ ] Languages, tools, silence, interruption, fallback, transfer, and ending passed.
- [ ] Metrics, alerts, result sinks, capacity, drain, and rollback passed.
- [ ] Product, security, privacy, telephony, and operations owners approved.
Use Troubleshooting for failures.