Skip to main content
Realtime voice providers give WednesdayAI full-duplex spoken conversations: audio streams in from a client, a provider model transcribes and responds, and synthesized audio streams back out. As a provider author you implement one plugin object — a provider descriptor plus a bridge that owns the provider connection — and WednesdayAI supplies session management, the gateway relay, and a shared tool for consulting the configured agent. This page covers the provider SDK contract, the types you implement, the gateway wire protocol clients use, and the agent consult tool. Operator setup (installing a provider, config keys, restarts) lives in Realtime voice setup. The reference implementation is extensions/openai-realtime in the WednesdayAI repo (src/provider.ts for the descriptor, src/openai-bridge.ts for the bridge).

Provider SDK contract

A realtime voice provider is a plain object registered from a plugin’s register function. The contract, re-exported from openclaw/plugin-sdk:
Required members are id, label, isConfigured, and createBridge. Everything else — aliases, defaultModel, models, autoSelectOrder, capabilities, resolveConfig, createBrowserSession — is optional. Register it from the plugin entry point:
The plugin manifest (openclaw.plugin.json) declares the id and the user-facing config schema:
How the pieces resolve at runtime:
  • User config lives under talk.providers.<id> in openclaw.json. The gateway passes the raw block to resolveConfig, then to isConfigured to decide whether the provider is selectable. A provider that is not configured is never picked.
  • Provider ids are normalized (trimmed, lowercased) on registration and lookup. Registering a second provider with the same normalized id is silently ignored — first registration wins.
  • createBridge is synchronous: it constructs your bridge and returns it. It must not connect yet — the gateway calls bridge.connect() itself.

Async safety

The register function may be synchronous or async, but must never perform synchronous blocking I/O — it runs during gateway startup and a blocking call stalls the event loop for every channel.
Two bridge lifecycle rules:
  • createBridge() must not fire any callbacks synchronously. The session runtime binds onReady, onToolCall, and the rest after createBridge returns; events fired during construction are silently discarded. Defer everything to after connect() is called.
  • Bridge callbacks (onAudio, onTranscript, onToolCall, …) fire on the shared event loop. Keep them fast; never block. Long work triggered by a tool call belongs in the tool-result path, not inside the callback.

Provider types

All types below are exported from openclaw/plugin-sdk.

Audio formats

Two audio formats exist in the contract, both mono:
The gateway-relay transport always runs PCM16 24 kHz in both directions. The SDK exports resampling and μ-law conversion helpers (resamplePcm, resamplePcmTo8k, pcmToMulaw, mulawToPcm, convertPcmToMulaw8k) if your upstream needs a different rate.

Capabilities

RealtimeVoiceProviderCapabilities declares what you support; clients and admin surfaces read it:

The bridge

The bridge owns the provider connection for one voice session:
You receive the callbacks you must call, plus session parameters, in RealtimeVoiceBridgeCreateRequest — a combination of RealtimeVoiceBridgeCallbacks and optional cfg, providerConfig, audioFormat, instructions, model, voice, autoRespondToAudio, interruptResponseOnInputAudio, and tools. The callbacks: onMark pairs with the session’s mark strategy. The gateway relay uses "transport": your onMark emission travels to the client, and the client acknowledges it; the gateway then calls your acknowledgeMark(). Use marks so the gateway knows when barge-in should cancel still-playing audio versus treat input as echo.

Tools and tool results

Voice tools use a plain JSON-Schema function shape (RealtimeVoiceTool): { type: "function", name, description, parameters }. When the provider model calls one, emit onToolCall with { itemId, callId, name, args }. Whoever owns the session (the client, or server-side consult handling) computes a result and submits it back through submitToolResult(callId, result, options). RealtimeVoiceToolResultOptions controls what happens after the result is submitted:
  • suppressResponse?: boolean — submit the result without prompting the provider to generate a new assistant response. Use it when another surface already delivered the user-visible answer.
  • willContinue?: boolean — more results for this call follow; the provider should keep waiting.
If your provider buffers multiple in-flight tool calls, set supportsToolResultContinuation: true on the bridge object.

Barge-in options

handleBargeIn receives RealtimeVoiceBargeInOptions:
  • audioPlaybackActive?: boolean — the caller confirms assistant audio is still playing in its output sink, letting you interrupt even without real playback marks.
  • force?: boolean — interrupt even when audio-duration guards would classify the event as echo.

Wire protocol

Clients open realtime voice through gateway WebSocket methods (schema: src/gateway/protocol/schema/talk-realtime.ts). The default and recommended transport is gateway-relay: the gateway hosts the provider bridge and relays audio, so provider credentials never reach the client.

Methods

Transport selection in talk.realtime.session:
  • Omitted or "gateway-relay" — always the gateway relay.
  • "auto" — the provider’s createBrowserSession if it exists, otherwise the relay.
  • "webrtc", "provider-websocket", "managed-room" — delegated to createBrowserSession; if the provider does not implement it, the request fails with transport "<t>" is not supported by provider "<id>".
A successful relay session returns:

Relay events

The gateway broadcasts session events to the owning connection on the talk.realtime.relay channel. Each event carries its relaySessionId plus a type: audio, clear, and mark events may be dropped if the client connection falls behind; error and close are always delivered.

Limits

  • Sessions expire after 30 minutes; the gateway closes them with reason completed.
  • Each talk.realtime.audio frame is capped at 512 KiB of base64 text; larger frames are rejected.
  • Maximum 2 concurrent relay sessions per client connection and 64 per gateway.
  • Relay sessions are bound to the connection that created them — audio for another connection’s relaySessionId fails with Unknown realtime relay session.

Agent consult tool

Realtime sessions that should answer with the assistant’s real knowledge — memory, workspace context, agent tools — use the shared consult tool instead of reimplementing delegation. The SDK exports the descriptor and all helpers from openclaw/plugin-sdk:
REALTIME_VOICE_AGENT_CONSULT_TOOL is a RealtimeVoiceTool named openclaw_agent_consult with one required argument: When you receive it via onToolCall, parse with parseRealtimeVoiceAgentConsultArgs — it tolerates providers that deliver args as a JSON string, and accepts question, prompt, query, or task as the key for the question. It throws if no question is present. Tool exposure is policy-driven. resolveRealtimeVoiceAgentConsultToolPolicy(value, fallback) normalizes a configured value to one of three policies: resolveRealtimeVoiceAgentConsultTools(policy, customTools) merges the consult tool with your own realtime tools; the consult tool is always first and custom tools cannot replace its contract by name. resolveRealtimeVoiceAgentConsultToolsAllow(policy) returns the paired allowlist for the delegated agent turn (undefined for owner, [] for none). Running the consult:
  1. While the delegated agent turn runs, feed buildRealtimeVoiceAgentConsultWorkingResponse() to the voice model — it instructs it to tell the user briefly that it is checking, then wait.
  2. Build the delegated prompt with buildRealtimeVoiceAgentConsultPrompt({ args, transcript, surface, userLabel }). It bounds the transcript to the last 12 entries so long conversations do not crowd out the live request, and instructs the agent to return only concise speakable text — no markdown, tool logs, or private reasoning.
  3. Collect the answer with collectRealtimeVoiceAgentConsultVisibleText(payloads) — it skips error-channel and reasoning payloads so hidden text is never spoken.
  4. Submit the result to the voice provider with submitToolResult(callId, result) — or with suppressResponse: true if another channel already delivered the answer.
buildRealtimeVoiceAgentConsultPolicyInstructions({ toolPolicy, consultPolicy }) adds spoken-behavior instructions for the voice model: "always" consults before every substantive answer, "substantive" consults only when facts, memory, or tools are needed, and "auto" (or omitted) adds nothing.

Troubleshooting

  • Unknown realtime relay session — the session expired (30-minute TTL), was already closed, or belongs to a different client connection. Open a new session.
  • Too many active realtime relay sessions / ... for this connection — gateway caps: 64 sessions total, 2 per connection. Stop idle sessions.
  • Realtime relay audio frame is too large — split audio into frames under 512 KiB of base64.
  • transport "..." is not supported by provider "..." — you requested a browser transport from a provider without createBrowserSession. Use "auto" or "gateway-relay", or implement createBrowserSession.
  • Sessions close on the first provider error event — the reference OpenAI bridge treats every error event as fatal, including non-fatal cancel-race and rate-limit advisories. If your provider emits recoverable errors, classify them before calling onError.