Skip to content

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-legacy and birla-agent-vaani, then have the deployment's SIP integration select the destination with LiveKit's upstream create_dispatch API. 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:

  1. stop routing new calls to the new workers;
  2. route new calls to the previous version;
  3. let existing calls finish on their original runtime;
  4. 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.