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’sregister function. The contract, re-exported from openclaw/plugin-sdk:
id, label, isConfigured, and createBridge. Everything else — aliases, defaultModel, models, autoSelectOrder, capabilities, resolveConfig, createBrowserSession — is optional.
Register it from the plugin entry point:
openclaw.plugin.json) declares the id and the user-facing config schema:
- User config lives under
talk.providers.<id>inopenclaw.json. The gateway passes the raw block toresolveConfig, then toisConfiguredto 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.
createBridgeis synchronous: it constructs your bridge and returns it. It must not connect yet — the gateway callsbridge.connect()itself.
Async safety
Theregister 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.
createBridge()must not fire any callbacks synchronously. The session runtime bindsonReady,onToolCall, and the rest aftercreateBridgereturns; events fired during construction are silently discarded. Defer everything to afterconnect()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 fromopenclaw/plugin-sdk.
Audio formats
Two audio formats exist in the contract, both mono: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: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.
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’screateBrowserSessionif it exists, otherwise the relay."webrtc","provider-websocket","managed-room"— delegated tocreateBrowserSession; if the provider does not implement it, the request fails withtransport "<t>" is not supported by provider "<id>".
Relay events
The gateway broadcasts session events to the owning connection on thetalk.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.audioframe 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
relaySessionIdfails withUnknown 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 fromopenclaw/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:
- 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. - 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. - Collect the answer with
collectRealtimeVoiceAgentConsultVisibleText(payloads)— it skips error-channel and reasoning payloads so hidden text is never spoken. - Submit the result to the voice provider with
submitToolResult(callId, result)— or withsuppressResponse: trueif 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 withoutcreateBrowserSession. Use"auto"or"gateway-relay", or implementcreateBrowserSession.- Sessions close on the first provider
errorevent — 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 callingonError.
Related
- Realtime voice setup — operator-facing install and config
- Write your first plugin
- Plugin manifest reference
- Plugin SDK reference