# Bridge a LiveKit Agent

> Use the vendor-bridge front-proxy to connect a call to an unchanged LiveKit agent when your LiveKit room must stay in operation.

You have a working LiveKit agent. You want a phone call or a browser leg to reach
it over FirstByte's QUIC transport. You do not want to edit the agent, and you
do not want to stop your LiveKit server. This page shows the **vendor-bridge
front-proxy**. The voice gateway terminates the caller edge. It then relays the
audio to a small bridge process. The bridge process joins your LiveKit room as an
ordinary participant. Your agent code does not change.

> **NOTE:**
> **Most agents do not need this page.** If you can remove the LiveKit server,
>   install the **transport plugin** — `livekit-plugins-clutchcall` / `@clutchcall/livekit-transport` —
>   into your agent process instead. Then you do not need the sidecar, the room, or
>   the credentials setup below. This is the recommended path:
>   [Run a LiveKit Agent on FirstByte Transport](/modalities/voice/recipes/livekit-transport).
> 
>   Use the front-proxy on this page when the **room itself must stay** — for
>   example, other participants join it, or another part of your stack depends on it.

## 1. Configure a vendor-bridge agent

Give the agent a single `VENDOR_BRIDGE` entry node instead of an ASR→LLM→TTS
cascade. With this configuration, the runtime runs **no** turn detection and
**no** model. It sends the caller's audio to your LiveKit room. It streams the
room's audio back.

```json agent-config.json
{
  "entry_node": "bridge",
  "pipeline": {
    "bridge": {
      "node_type": "VENDOR_BRIDGE",
      "vendor_provider": "livekit",
      "vendor_room": "support",
      "vendor_creds_ref": "livekit_prod"
    }
  }
}
```

> **WARNING:**
> The node map key is **`pipeline`**, not `nodes`. An agent config written with
>   `nodes` parses to an empty pipeline and the call falls back to the built-in
>   cascade instead of reaching your room.

- **``** (`string`, default `livekit`) — The external vendor to route to. The **sidecar** implements `livekit` and   `vapi` today (Twilio Media Streams is the same PCM-over-WebSocket proxy shape   and is a natural follow-on). Setting it to `clutchcall` instead means   the opposite arrangement — your own agent process on our transport, no   sidecar and no room. See   [LiveKit Agent over FirstByte Transport](/modalities/voice/integrations/livekit).

- **``** (`string`) — The LiveKit room that the bridge sidecar joins. For the other vendors, this is   the Vapi assistant id or the Twilio app.

- **``** (`string`) — The key into your tenant's sealed vendor credentials: your `LIVEKIT_URL`,   `LIVEKIT_API_KEY`, and `LIVEKIT_API_SECRET`. The system stores these values at   rest and never inlines them.

## 2. Place a call at that agent

Attach the agent on `originate`. The gateway connects the audio automatically on
answer. You do not need your own `AudioBridge`.

  
```ts
import { Voice } from "clutchcall_connect/voice";

const v = new Voice({
  baseUrl: "https://engine.firstbyte.fast",
  apiKey:  process.env.CLUTCHCALL_CREDENTIALS!,
  orgId:   "org_abc",
});

const call = await v.calls.originate({
  to:      "+15551234567",
  from:    "+15558675309",
  trunkId: "trunk_main",
  agent:   "livekit-front-proxy",   // the vendor-bridge agent above
});
console.log("dialing", call.sid);
```
  
  
```python
import os
from clutchcall.voice import Voice

v = Voice(
    base_url="https://engine.firstbyte.fast",
    api_key=os.environ["CLUTCHCALL_CREDENTIALS"],
    org_id="org_abc",
)

call = v.calls.originate(
    to="+15551234567",
    from_="+15558675309",
    trunk_id="trunk_main",
    agent="livekit-front-proxy",
)
print("dialing", call.sid)
```
  

For **inbound** calls, attach the same agent to a number or trunk instead of
dialing. See [SIP Trunking](/modalities/voice/transport-telephony/sip-trunking)
and [Number Provisioning](/modalities/voice/transport-telephony/number-provisioning).

## 3. Promote a live call to the LiveKit agent

If a call is already in progress, attach the vendor-bridge agent to the running
`sid`. This moves the conversation to your LiveKit room in the middle of the call.

  
```ts
await v.agents.attach(call.sid, "livekit-front-proxy");
```
  
  
```python
v.agents.attach(call.sid, "livekit-front-proxy")
```
  

## 4. Run the bridge sidecar

The gateway side of the bridge — the vendor-bridge node plus the media handler —
is part of the runtime. The process that **joins your LiveKit room** is a small
Go sidecar (`clutchcall-livekit-bridge`) that you run next to your
LiveKit deployment, because it holds your LiveKit credentials.

It is a WebSocket server, not a client of yours. The engine connects **to it**:

```bash
# One listener, two routes: /bridge (the media socket) and /healthz.
clutchcall-livekit-bridge \
  --addr :7071 \
  --tls-cert /etc/ssl/bridge.crt \
  --tls-key  /etc/ssl/bridge.key
```

> **WARNING:**
> **TLS is not optional in production.** The engine's WebSocket client is
>   TLS-only, so without `--tls-cert` / `--tls-key` the sidecar serves plaintext
>   and the engine will not connect. Plaintext is for local development and
>   health checks.

Per session the engine opens one connection to `/bridge` and sends a JSON
handshake as the first text message; every message after that is one audio
frame, in either direction. The sidecar is codec-agnostic — the engine sends the
vendor-appropriate codec (Opus for LiveKit, PCM16 for Vapi) and converts on the
return, so the sidecar only forwards bytes.

```json handshake
{
  "vendor":      "livekit",
  "url":         "wss://your-livekit-host.example.com",
  "api_key":     "APIxxxx",
  "api_secret":  "secretxxxx",
  "room":        "support",
  "identity":    "clutchcall-caller"
}
```

The credentials in that handshake come from your tenant's **sealed vendor
credentials**, looked up by `vendor_creds_ref` — they are stored at rest and
never inlined into an agent config.

For `"vendor": "vapi"` the same handshake carries `assistant_id`, and `api_key`
is reused as the Vapi private key; the sidecar POSTs `/call`, gets the call
WebSocket, and proxies PCM both ways.

> **NOTE:**
> **What is shipped and what you deploy.** The vendor-bridge routing is part of
>   the runtime. The sidecar is a separate binary you operate, because it is the
>   only component that needs your LiveKit URL, key, and secret.

## Browser leg? The compat shim (preview)

If your LiveKit surface is a **browser app** (`livekit-client`) and not a
server-side agent, you can skip the edge bridge. Change one import line to run
the client's audio directly over the transport.

```diff
- import { Room, RoomEvent, createLocalAudioTrack } from "livekit-client";
+ import { Room, RoomEvent, createLocalAudioTrack } from "@clutchcall/livekit-compat";
```

> **WARNING:**
> **Preview / experimental.** The `livekit-compat` shim is an early-access
>   browser package, not a shipped product. It covers the core voice path: publish
>   the microphone, subscribe to remote audio, and send data messages. **Video,
>   screenshare, and per-participant E2EE key rotation are deferred.** A
>   session-setup race can sometimes deliver zero frames on connect. If this
>   occurs, retry. The shim requires encoded media transforms and does not fall
>   back to plaintext WebRTC. Today this limits it to Chromium-family browsers. A
>   server-side Python (`livekit.rtc`) equivalent is not yet available. For the
>   full deep-dive, see
>   [Keep Your Existing Runtime](/modalities/voice/runtime/keep-existing).

## Related

  - **[LiveKit Agent Integration](/modalities/voice/integrations/livekit)** — All three paths: transport plugin, front-proxy, and browser shim.
  - **[Run a LiveKit Agent on the Transport](/modalities/voice/recipes/livekit-transport)** — The recommended path: a plugin in your agent process, with no sidecar and no room.
  - **[From Self-Hosted LiveKit](/modalities/voice/migration/from-livekit)** — The wave-by-wave migration off a LiveKit SFU.
  - **[Custom Agent Runtime](/modalities/voice/integrations/custom-runtime)** — The same front-proxy pattern for any non-LiveKit runtime.
  - **[WebRTC Diversion](/modalities/voice/transport-web/webrtc-diversion)** — How the compat shim sends encoded Opus over QUIC in the browser.
