standin.plugins.elevenlabs puts ElevenLabs on a real Microsoft Teams call. StandIn answers the call and dials your worker; this plugin answers that dial, opens one session per call, and relays the audio both ways.
Install and run
There is nothing to install beyond the SDK. ElevenLabs is reached over an ordinary WebSocket, so this plugin adds no dependency and needs no extra.9442 at the /msteams/calling path and register the public wss:// URL as your StandIn identity’s agent voice URL. The full walkthrough is in the Quickstart, and there is a runnable example at examples/elevenlabs-msteams-connector.
The audio format
In the ElevenLabs dashboard, set the agent’s input and output audio format topcm_16000. That is exactly what a Microsoft Teams call carries, so nothing resamples anything and the latency you measure is the model’s rather than the transport’s.
An agent set to anything else is refused at the first frame, with one clear log line, rather than producing a whole call of garbled audio.
What the agent can do about the call
Declare these as client tools on the agent. Nothing to implement: the plugin answers them.client_tools() prints the declarations, so there is nothing to retype into the dashboard:
show_image is widened here, because ElevenLabs is the one provider that can hand you the bytes inline: either form will do, so neither argument is required. A URL is chosen by the model, which the caller steers, so it is fetched through the SDK’s guard: public hosts only, no private or link-local addresses, and the address is re-checked at connect time to close the DNS rebind window.
Inside your own worker
The plugin is aCallHandler like any other, so you can build it yourself instead of running the module:
handler_factory runs once per call, so handler_factory=ElevenLabsHandler would call ElevenLabsConfig.from_env() again on every call, and a key removed from the environment after startup would fail the next caller rather than failing you.
server.start() returns as soon as the listener is bound, so a script that ends there exits before a single call arrives. serve(), which is what python -m standin.plugins.elevenlabs runs, is the sample above plus an await asyncio.Event().wait() to hold the process open and an await server.aclose() in a finally, which drains live calls and releases the port on the way out.
Configuration
Optional settings
Optional settings
The configuration is read once when the worker starts, not per call, so a missing key stops the worker at startup rather than surprising the first caller.
ELEVENLABS_HOST is checked against the elevenlabs.io suffix, because your API key travels to it: a wrong host would be credential leakage rather than a failed call. The reasoning behind that shape is on Configuration.Next
Realtime providers
The startup buffer, the echo guard and barge-in this plugin sits on.
Vision and the avatar
What
look, show_image and express reach.