> This is a page from the ElevenLabs documentation. For a complete page index, fetch https://elevenlabs.io/docs/llms.txt. For the full documentation in a single file, fetch https://elevenlabs.io/docs/llms-full.txt.

# Integration eines benutzerdefinierten LLM

## Überblick

Die [native Twilio-Integration](/docs/de/eleven-agents/phone-numbers/twilio-integration/native-integration) von ElevenAgents deckt den Fall ab, in dem ElevenLabs das LLM hostet. Nutzen Sie diesen Leitfaden, wenn Sie die volle Kontrolle über das LLM-Gehirn auf Ihrem eigenen Server benötigen — mit Ihrem eigenen Modell, einer RAG-Pipeline, dem Routing von Funktionsaufrufen oder anderer serverseitiger Logik — und der Agent weiterhin über eine Twilio-Telefonnummer erreichbar ist.

Der Teil mit dem benutzerdefinierten LLM wird über das [Speech Engine SDK](/docs/de/eleven-api/guides/cookbooks/speech-engine) bereitgestellt. Es öffnet einen WebSocket zwischen ElevenLabs und Ihrem Server, sodass Ihr LLM Antworten während des Gesprächs streamen kann. Der Twilio-Teil nutzt [Media Streams](https://www.twilio.com/docs/voice/media-streams), um Gesprächsaudio an den Agenten weiterzuleiten.

## Architektur

Das Speech Engine SDK stellt im Gesprächssystem des Agenten zwei WebSocket-Endpunkte bereit:

* Der **Brain-WebSocket** läuft auf Ihrem Server. ElevenLabs verbindet sich damit, um Transkripte zu übermitteln und vom LLM generierten Text zu empfangen.
* Der **Gesprächs-WebSocket** läuft bei ElevenLabs. Clients verbinden sich damit, um Audio zu senden und synthetisiertes Audio zurückzuerhalten. Die Twilio-Bridge verbindet sich über eine signierte URL und leitet μ-law-Audio in beide Richtungen weiter.

Da Twilio Media Streams und die Speech Engine beide `ulaw_8000` verwenden, leitet die Bridge base64-kodiertes Audio ohne Transkodierung weiter.

```mermaid
sequenceDiagram
    participant Caller
    participant Twilio
    participant Bridge as Bridge Server
    participant EL as ElevenLabs (conversation WS)
    participant Brain as Brain Server

    Caller->>Twilio: Dial number
    Twilio->>Bridge: POST /incoming-call
    Bridge-->>Twilio: TwiML <Connect><Stream>
    Twilio->>Bridge: WebSocket /media-stream
    Bridge->>EL: Open conversation WebSocket (signed URL)

    loop Conversation
        Caller->>Twilio: Speak
        Twilio->>Bridge: media event (μ-law base64)
        Bridge->>EL: user_audio_chunk
        EL->>Brain: user_transcript
        Brain-->>EL: agent_response (streamed)
        EL->>Bridge: audio event (μ-law base64)
        Bridge->>Twilio: media event
        Twilio->>Caller: Play audio
    end
```

Bridge und Brain-Server können im selben Prozess laufen, falls das praktischer ist — das folgende Beispiel kombiniert beide.

## Wann Sie dieses Muster verwenden sollten

Sowohl dieser Leitfaden als auch die [native Twilio-Integration](/docs/de/eleven-agents/phone-numbers/twilio-integration/native-integration) stellen einen Agenten über eine Twilio-Telefonnummer bereit. Der Unterschied besteht darin, wer das LLM betreibt:

* **Native Integration**: ElevenLabs hostet das LLM, Sie konfigurieren es über den Agenten. Einfacher.
* **Benutzerdefiniertes LLM über das Speech Engine SDK** (dieser Leitfaden): Sie hosten das LLM auf Ihrem eigenen Server. Volle Kontrolle über Modell, RAG, Funktionsaufrufe und Geschäftslogik. Mehr Komponenten.

Wenn Ihre LLM-Logik in die Standard-Agentenkonfiguration passt, nutzen Sie die native Integration. Verwenden Sie diesen Leitfaden, wenn Ihr Gehirn Code auf Ihrer eigenen Infrastruktur ausführen muss.

Dieses Muster verwendet das Speech Engine SDK, das über eine WebSocket-Verbindung zwischen Ihrem Server und der ElevenLabs API kommuniziert. Sie können auch den Leitfaden [Benutzerdefiniertes LLM](/docs/de/eleven-agents/customization/llm/custom-llm) verwenden, der statt des Speech Engine SDK einen OpenAI-kompatiblen HTTP-Endpunkt nutzt.

Der Hauptunterschied zwischen beiden sind WebSockets gegenüber HTTP-Anfragen. WebSockets halten eine einzelne Verbindung aufrecht, statt für jeden Gesprächszug eine neue HTTP-Verbindung aufzubauen. Das kann die Latenz verringern.

## Voraussetzungen

* Ein [Twilio](https://www.twilio.com/)-Konto und eine sprachfähige Telefonnummer.
* Eine Speech-Engine-Ressource. Folgen Sie dem [Speech-Engine-Schnellstart](/docs/de/eleven-api/guides/cookbooks/speech-engine), um eine zu erstellen und das Brain-Server-Muster kennenzulernen.
* Einen öffentlichen HTTPS-Tunnel, zum Beispiel [ngrok](https://ngrok.com). Twilio ruft Ihre Bridge über das öffentliche Internet auf.
* Python 3.9+ oder Node.js 18+.

## Agenten für μ-law-Audio konfigurieren

Twilio Media Streams verwendet μ-law-Audio mit 8 kHz. Konfigurieren Sie die Speech Engine so, dass sie dasselbe Format akzeptiert und ausgibt, damit die Bridge nicht transkodieren muss.

**`configure_engine.py`**

```python title="configure_engine.py"
import asyncio
import os
from elevenlabs import AsyncElevenLabs

elevenlabs = AsyncElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])


async def update_engine():
    await elevenlabs.speech_engine.update(
        speech_engine_id="seng_8k3m9xr4hjnfg983brhmhkd98n6",
        asr={"user_input_audio_format": "ulaw_8000"},
        tts={
            "model_id": "eleven_flash_v2",
            "agent_output_audio_format": "ulaw_8000",
        },
        speech_engine={
            "request_headers": {"x-api-key": os.environ["SHARED_SECRET"]},
        },
    )


asyncio.run(update_engine())
```

**`configure-engine.mts`**

```typescript title="configure-engine.mts"
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
import "dotenv/config";

const elevenlabs = new ElevenLabsClient({
  apiKey: process.env.ELEVENLABS_API_KEY,
});

await elevenlabs.speechEngine.update("seng_8k3m9xr4hjnfg983brhmhkd98n6", {
  asr: { userInputAudioFormat: "ulaw_8000" },
  tts: {
    modelId: "eleven_flash_v2",
    agentOutputAudioFormat: "ulaw_8000",
  },
  speechEngine: {
    requestHeaders: { "x-api-key": process.env.SHARED_SECRET! },
  },
});
```

`eleven_flash_v2` hält die Text-to-Speech-Latenz niedrig, was bei einem Telefongespräch wichtig ist. Der Block `request_headers` weist ElevenLabs an, bei jeder Brain-WebSocket-Verbindung `x-api-key: <shared-secret>` einzuschließen — der Brain-Server prüft den Header, damit nur Ihre Speech Engine ihn erreichen kann.

## Bridge-Server erstellen

Die Bridge stellt drei Routen bereit:

* `POST /incoming-call` — Twilio-Webhook. Gibt TwiML zurück, das Twilio anweist, einen Media Stream zu `/media-stream` zu öffnen.
* `GET /media-stream` — Twilio-Media-Streams-WebSocket. Leitet Audio zum und vom Speech-Engine-Gesprächs-WebSocket weiter.
* `GET /ws` — Brain-WebSocket. ElevenLabs verbindet sich hier, wenn ein Gespräch startet. Führt den Standardserver `engine.serve()` / `engine.attach()` aus.

#### Abhängigkeiten installieren

**`Python`**

```bash title="Python"
pip install "elevenlabs" "aiohttp" "twilio" "python-dotenv"
```

**`Node`**

```bash title="Node"
npm install @elevenlabs/elevenlabs-js express ws twilio dotenv openai
```

#### Signierte URL für die Speech Engine erstellen

Die Bridge fordert jedes Mal eine signierte URL an, wenn ein neuer Anruf eingeht. Die URL enthält die Speech-Engine-ID und eine einmalige Signatur, sodass die Bridge nie den rohen API-Schlüssel benötigt.

**`bridge.py`**

```python title="bridge.py"
from elevenlabs import AsyncElevenLabs

elevenlabs = AsyncElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])

async def signed_url() -> str:
    response = await elevenlabs.conversational_ai.conversations.get_signed_url(
        agent_id=os.environ["SPEECH_ENGINE_ID"],
    )
    return response.signed_url
```

**`bridge.mts`**

```typescript title="bridge.mts"
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";

const elevenlabs = new ElevenLabsClient({
  apiKey: process.env.ELEVENLABS_API_KEY,
});

async function signedUrl(): Promise<string> {
  const response = await elevenlabs.conversationalAi.conversations.getSignedUrl({
    agentId: process.env.SPEECH_ENGINE_ID!,
  });
  return response.signedUrl;
}
```

#### TwiML-Antwort bereitstellen

Wenn ein Anruf eingeht, sendet Twilio einen POST an `/incoming-call`. Die Antwort ist TwiML, das einen Media Stream zum eigenen `/media-stream`-WebSocket der Bridge öffnet.

**`bridge.py`**

```python title="bridge.py"
from aiohttp import web
from twilio.request_validator import RequestValidator

validator = RequestValidator(os.environ["TWILIO_AUTH_TOKEN"])


async def incoming_call(request: web.Request) -> web.Response:
    form = await request.post()
    signature = request.headers.get("X-Twilio-Signature", "")
    url = str(request.url)
    if not validator.validate(url, dict(form), signature):
        return web.Response(status=403, text="forbidden")

    host = request.headers.get("X-Forwarded-Host") or request.host
    twiml = (
        '<?xml version="1.0" encoding="UTF-8"?>'
        "<Response><Connect>"
        f'<Stream url="wss://{host}/media-stream"/>'
        "</Connect></Response>"
    )
    return web.Response(text=twiml, content_type="text/xml")
```

**`bridge.mts`**

```typescript title="bridge.mts"
import express from "express";
import twilio from "twilio";

const app = express();
app.use(express.urlencoded({ extended: false }));

app.post(
  "/incoming-call",
  twilio.webhook({ validate: true }),
  (req, res) => {
    const host = req.headers["x-forwarded-host"] ?? req.get("host");
    const twiml = `<?xml version="1.0" encoding="UTF-8"?>
      <Response>
        <Connect>
          <Stream url="wss://${host}/media-stream"/>
        </Connect>
      </Response>`;
    res.type("text/xml").send(twiml);
  },
);
```

`RequestValidator` (Python) und `twilio.webhook({ validate: true })` (Node) prüfen den Header `X-Twilio-Signature` anhand von `TWILIO_AUTH_TOKEN`. Ohne Validierung könnte jeder im öffentlichen Internet einen POST an `/incoming-call` senden und Anrufe über Ihr Konto abrechnen.

#### Media Stream überbrücken

Der Media Stream ist ein WebSocket, der eine Reihe von JSON-Ereignissen sendet: `connected`, `start`, `media` (die Audionutzlast) und `stop`. Beim `start` öffnet die Bridge einen Speech-Engine-Gesprächs-WebSocket und leitet Audio in beide Richtungen weiter, bis der Stream geschlossen wird.

**`bridge.py`**

```python title="bridge.py" maxLines=0
import asyncio
import json

import aiohttp
from aiohttp import web


async def media_stream(request: web.Request) -> web.WebSocketResponse:
    twilio_ws = web.WebSocketResponse()
    await twilio_ws.prepare(request)

    stream_sid: str | None = None
    el_session: aiohttp.ClientSession | None = None
    el_ws: aiohttp.ClientWebSocketResponse | None = None
    pump_task: asyncio.Task | None = None

    async def pump_el_to_twilio(el: aiohttp.ClientWebSocketResponse):
        async for msg in el:
            if msg.type != aiohttp.WSMsgType.TEXT:
                continue
            event = json.loads(msg.data)
            etype = event.get("type")
            if etype == "audio":
                await twilio_ws.send_str(json.dumps({
                    "event": "media",
                    "streamSid": stream_sid,
                    "media": {"payload": event["audio_event"]["audio_base_64"]},
                }))
            elif etype == "interruption":
                await twilio_ws.send_str(json.dumps({
                    "event": "clear",
                    "streamSid": stream_sid,
                }))
            elif etype == "ping":
                event_id = event.get("ping_event", {}).get("event_id")
                await el.send_str(json.dumps({
                    "type": "pong", "event_id": event_id,
                }))

    try:
        async for msg in twilio_ws:
            if msg.type != aiohttp.WSMsgType.TEXT:
                continue
            event = json.loads(msg.data)

            if event["event"] == "start":
                stream_sid = event["start"]["streamSid"]
                el_session = aiohttp.ClientSession()
                el_ws = await el_session.ws_connect(await signed_url())
                await el_ws.send_str(json.dumps({
                    "type": "conversation_initiation_client_data",
                }))
                pump_task = asyncio.create_task(pump_el_to_twilio(el_ws))

            elif event["event"] == "media" and el_ws is not None:
                await el_ws.send_str(json.dumps({
                    "user_audio_chunk": event["media"]["payload"],
                }))

            elif event["event"] == "stop":
                break
    finally:
        if pump_task:
            pump_task.cancel()
        if el_ws and not el_ws.closed:
            await el_ws.close()
        if el_session and not el_session.closed:
            await el_session.close()

    return twilio_ws
```

**`bridge.mts`**

```typescript title="bridge.mts" maxLines=0
import { WebSocket, WebSocketServer } from "ws";
import { createServer } from "node:http";

const httpServer = createServer(app);
const wss = new WebSocketServer({ noServer: true });

httpServer.on("upgrade", (req, socket, head) => {
  if (req.url === "/media-stream") {
    wss.handleUpgrade(req, socket, head, (ws) => handleMediaStream(ws));
  } else {
    socket.destroy();
  }
});

async function handleMediaStream(twilioWs: WebSocket) {
  let streamSid: string | null = null;
  let elReady: Promise<WebSocket | null> | null = null;

  twilioWs.on("message", async (raw) => {
    const event = JSON.parse(raw.toString());

    if (event.event === "start") {
      streamSid = event.start.streamSid;
      // Convert rejection into a clean null + close so a failed signed-URL
      // fetch doesn't become an unhandled rejection on the next media event.
      elReady = openElevenLabsWebSocket(twilioWs, () => streamSid).catch((err) => {
        console.error("Failed to open Speech Engine conversation:", err);
        twilioWs.close();
        return null;
      });
    } else if (event.event === "media" && elReady) {
      const elWs = await elReady;
      if (!elWs) return;
      elWs.send(JSON.stringify({
        user_audio_chunk: event.media.payload,
      }));
    } else if (event.event === "stop") {
      twilioWs.close();
    }
  });

  twilioWs.on("close", async () => {
    (await elReady)?.close();
  });
}

async function openElevenLabsWebSocket(
  twilioWs: WebSocket,
  getStreamSid: () => string | null,
): Promise<WebSocket> {
  const elWs = new WebSocket(await signedUrl());
  await new Promise<void>((resolve, reject) => {
    elWs.once("open", () => resolve());
    elWs.once("error", reject);
  });
  elWs.send(JSON.stringify({
    type: "conversation_initiation_client_data",
  }));

  elWs.on("message", (raw) => {
    const event = JSON.parse(raw.toString());
    const streamSid = getStreamSid();
    if (event.type === "audio") {
      twilioWs.send(JSON.stringify({
        event: "media",
        streamSid,
        media: { payload: event.audio_event.audio_base_64 },
      }));
    } else if (event.type === "interruption") {
      twilioWs.send(JSON.stringify({ event: "clear", streamSid }));
    } else if (event.type === "ping") {
      elWs.send(JSON.stringify({
        type: "pong", event_id: event.ping_event?.event_id,
      }));
    }
  });

  return elWs;
}
```

Das `interruption`-Ereignis der Speech Engine löst im Twilio-Stream ein `clear`-Ereignis aus. Dadurch wird gepuffertes Audio verworfen, sodass Unterbrechen sauber funktioniert. Das `ping`-Ereignis wird mit `pong` beantwortet, um den Gesprächs-WebSocket aktiv zu halten.

#### Brain-Server parallel ausführen

Der Brain-Server ist der im [Schnellstart](/docs/de/eleven-api/guides/cookbooks/speech-engine) gezeigte Standard-Speech-Engine-Server. Die einzige Ergänzung ist die Prüfung des Shared Secret beim WebSocket-Upgrade — akzeptieren Sie die Verbindung nur, wenn `x-api-key` dem Wert entspricht, den Sie für die Speech Engine festgelegt haben.

**`bridge.py`**

```python title="bridge.py" maxLines=0
import os

from elevenlabs import AsyncElevenLabs

elevenlabs = AsyncElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])
SHARED_SECRET = os.environ["SHARED_SECRET"]


async def brain_ws(request: web.Request) -> web.WebSocketResponse:
    if request.headers.get("x-api-key") != SHARED_SECRET:
        return web.Response(status=401, text="unauthorized")

    ws = web.WebSocketResponse()
    await ws.prepare(request)

    engine = await elevenlabs.speech_engine.get(os.environ["SPEECH_ENGINE_ID"])
    session = engine.create_session(ws)

    async def on_transcript(transcript):
        # Replace this with your own LLM call; see the quickstart.
        await session.send_response("Hello, you've reached the demo.")

    session.on("user_transcript", on_transcript)
    await session.run()
    return ws


def make_app() -> web.Application:
    app = web.Application()
    app.router.add_post("/incoming-call", incoming_call)
    app.router.add_get("/media-stream", media_stream)
    app.router.add_get("/ws", brain_ws)
    return app


if __name__ == "__main__":
    web.run_app(make_app(), port=3001)
```

**`bridge.mts`**

```typescript title="bridge.mts" maxLines=0
httpServer.on("upgrade", async (req, socket, head) => {
  if (req.url === "/ws") {
    if (req.headers["x-api-key"] !== process.env.SHARED_SECRET) {
      socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
      socket.destroy();
      return;
    }
    // Hand off to engine.attach() — see the Speech Engine quickstart.
  } else if (req.url === "/media-stream") {
    wss.handleUpgrade(req, socket, head, (ws) => handleMediaStream(ws));
  } else {
    socket.destroy();
  }
});

httpServer.listen(3001);
```

Die vollständige Implementierung von `on_transcript`, einschließlich LLM-Aufruf und gestreamter Antwort, finden Sie im [Speech-Engine-Schnellstart](/docs/de/eleven-api/guides/cookbooks/speech-engine#server-setup).

## Twilio auf die Bridge verweisen

#### Bridge und öffentlichen Tunnel starten

```bash
ngrok http 3001
python bridge.py
```

Notieren Sie die von ngrok ausgegebene `https://`-URL — Twilio sendet POST-Anfragen dorthin.

#### Speech-Engine-ws_url aktualisieren

Setzen Sie `speech_engine.ws_url` auf die öffentliche WebSocket-URL Ihres Brain-Endpunkts, damit ElevenLabs weiß, wohin die Verbindung hergestellt werden soll.

```python
await elevenlabs.speech_engine.update(
    speech_engine_id="seng_8k3m9xr4hjnfg983brhmhkd98n6",
    speech_engine={"ws_url": "wss://abc123.ngrok.io/ws"},
)
```

```typescript
await elevenlabs.speechEngine.update("seng_8k3m9xr4hjnfg983brhmhkd98n6", {
  speechEngine: { wsUrl: "wss://abc123.ngrok.io/ws" },
});
```

#### Twilio-Nummer konfigurieren

Öffnen Sie in der Twilio-Konsole die **Sprachkonfiguration** Ihrer Telefonnummer:

* **Ein Anruf geht ein**: Webhook
* **URL**: `https://abc123.ngrok.io/incoming-call`
* **HTTP-Methode**: POST

Wenn die Nummer an einen Elastic SIP Trunk angehängt ist, trennen Sie sie zuerst — eine Twilio-Nummer wird entweder an einen Trunk oder an einen Webhook weitergeleitet, nicht an beides.

#### Nummer anrufen

Rufen Sie die Nummer von einem beliebigen Telefon aus an. Der Agent nimmt ab; sprechen Sie in den Anruf und Sie sollten die Antwort des Agenten hören. Bei aktiviertem Debug-Logging protokolliert die Bridge für jeden Gesprächszug die Anruf-SID, Gesprächs-ID und das Audioformat.

## Hinweise für die Produktion

* **Webhook-Validierung**: Validieren Sie stets die `X-Twilio-Signature` auf `/incoming-call`. Das obige Beispiel verwendet die Hilfsbibliothek von Twilio. Überspringen Sie diesen Schritt nicht.
* **Gemeinsames Secret**: Erzwingen Sie das gemeinsame Secret auf dem Brain-WebSocket. Andernfalls kann sich jeder, der Ihre ngrok-URL errät, verbinden und ElevenLabs imitieren.
* **Stabiler Host**: URLs der kostenlosen ngrok-Version ändern sich bei jedem Neustart. Verwenden Sie eine reservierte ngrok-Domain oder einen echten Hostnamen, damit Sie die `ws_url` der Speech Engine und den Twilio-Webhook nicht nach jedem Neustart aktualisieren müssen.
* **Latenz**: Jeder Aufruf fügt zusätzlich zur Zeit bis zum ersten Token des LLM zwei Netzwerk-Hops hinzu. Verwenden Sie ein Modell mit geringer Latenz und streamen Sie Antworten, um die wahrgenommene Latenz niedrig zu halten.
* **Ein oder zwei Prozesse**: Das Beispiel platziert Bridge und Brain auf demselben Port, sodass ein einzelner ngrok-Tunnel alles abdeckt. In der Produktion können Sie sie auf zwei Services aufteilen, sofern jeder eine öffentliche URL hat.
* **Prompt-Injection**: Gesprochene Eingaben aus einem Telefonat sind nicht vertrauenswürdige Nutzereingaben. Validieren Sie Transkripte, bevor sie Tool-Aufrufe oder Datenbankeinträge beeinflussen.

## Nächste Schritte

#### [Native Twilio-Integration](/docs/de/eleven-agents/phone-numbers/twilio-integration/native-integration)

Verwenden Sie das gehostete LLM statt eines eigenen.

#### [Speech Engine-Schnellstart](/docs/de/eleven-api/guides/cookbooks/speech-engine)

Erstellen Sie den Brain-Server mit einem Streaming-LLM durchgängig.

#### [Eigenes LLM (OpenAI-kompatibel)](/docs/de/eleven-agents/customization/llm/custom-llm)

Eine alternative Methode für eigene LLMs über einen OpenAI-kompatiblen HTTP-Endpunkt.

#### [Python-SDK-Referenz](/docs/de/eleven-api/resources/libraries/speech-engine/python-sdk-reference)

Klassen, Methoden und Ereignisse für das Speech Engine Python SDK.

#### [JavaScript-SDK-Referenz](/docs/de/eleven-api/resources/libraries/speech-engine/javascript-sdk-reference)

Klassen, Methoden und Ereignisse für das Speech Engine JavaScript SDK.