standin.plugins.deepgram puts Deepgram 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. Deepgram 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/deepgram-msteams-connector.
The audio format
Audio is pinned tolinear16 at 16 kHz in both directions when the session starts. That is exactly what a Microsoft Teams call carries, so the hot path is a copy and nothing resamples anything.
What the agent can do about the call
These are declared automatically when the call starts, so there is nothing to set up on the provider’s side. Nothing to implement: the plugin answers them.show_image takes a public url, 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. display is fullscreen or overlay, and this plugin routes the call through CallTools.dispatch, so the model’s choice is honoured. See Vision and the avatar for what each mode is for, and Sending a picture for putting a picture into chat instead.
A Voice Agent hears but does not see, so look needs a vision model of your choosing. Set STANDIN_VISION_API_URL and STANDIN_VISION_MODEL to any OpenAI-compatible endpoint that accepts images, including one you run yourself. The frame is sent for inference and not stored, and only the description comes back. Without it the agent is told plainly that looking is unavailable, rather than being left with silence. The whole surface is on Vision and the avatar.
Pass describer= to point this plugin at a different vision model from the one the environment names, which is how one worker answers two identities with two vision budgets. Build it with FrameDescriber.from_env() or its constructor, both on Vision and the avatar.
How context reaches the agent
The Voice Agent API has no non-interrupting context message, so this plugin folds participant counts, key presses and recording changes into the agent’s prompt and pushes it with an update. That has a cost worth knowing: the prompt is resent in full on every change, so the context section is a rolling window of the last eight notes rather than a transcript. A meeting where people join and leave repeatedly keeps the recent notes and drops the old ones, which is the right trade when the alternative is a prompt that grows for the length of the call. Context that arrives before the agent socket is open is held rather than dropped, then folded in once it exists. The “there are N people here, stay quiet” line and the recording change both land in exactly that gap.Tools of your own
A custom tool runs in your worker, so it can reach whatever your worker can reach:description is what the model reads to decide whether to call it, so write it for a model rather than for a developer. Keep the handler fast: the caller is waiting in silence while it runs.
A custom tool may not shadow a built-in one. That is refused when the handler is built rather than at the first call, because a shadowed end_call is an agent that has quietly lost the ability to hang up.
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=DeepgramHandler would call DeepgramConfig.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.deepgram 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. Both hosts are checked against the deepgram.com suffix, because your API key travels to them: 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.