Extending Realtime Providers
A Realtime Provider adapts a realtime model service to the runtime; it is neither the client nor the Orchestration Runtime itself. A product host can inject a custom provider without changing shared session or backend-work logic. The example below assembles it through the Gateway application entry point.
import { createGatewayApplication } from 'qwen-audio-agent/gateway-application'
import {
createRealtimeProviderRegistry,
} from 'qwen-audio-agent/realtime-provider'
import { privateRealtimeProvider } from './private-realtime-provider.mjs'
const realtimeProviderRegistry = createRealtimeProviderRegistry({
providers: [privateRealtimeProvider],
defaultProvider: privateRealtimeProvider.key,
})
createGatewayApplication({
realtimeProviderRegistry,
realtimeProvider: privateRealtimeProvider.key,
})The extension boundary is:
- Each Provider is an independent adapter that owns its URL, authentication, model, session, and error-classification semantics. Product differences should not be carried by reshaping another Provider.
url(),headers(), andmodel()can read the service URL, token, and model from host-owned configuration closures. The Gateway does not require product-specific environment variables.createProtocol()runs once for each Realtime connection, so connection IDs and mutable state remain isolated.- Optional synchronous
validateSessionOptions({ sessionOptions })runs before opening the upstream connection. Only reject known-invalid selections; leave unknown values to the service. connectionMessages()emits raw handshake frames after the WebSocket opens and beforesession.update.- All later events pass through
encodeOutgoing()andnormalizeIncoming(), leaving Gateway tools, tasks, and client protocols unchanged. - Protocols without transient response instructions can implement
responseInstructionsItem(response). Gateway creates and awaits that conversation item before callingresponseCreate(response), which strips unsupported wire parameters. SetperResponseInstructions: false; these instructions remain in history. conversationItemCreate(item, { contextOnly })distinguishes context delivery from interactive user input. For message items,contextOnly: truemust write context without triggering a reply; it must not merely cache the text for a laterresponseCreate. Gateway passesfalsefor interactive text/file input. Tool receipts keep the provider's native continuation semantics.- Set
automaticToolResponses: trueonly when tool results natively resume generation (Doubao Seeduplex and Google Live). Runtime sends these receipts without waiting for response completion, and the tool-batch follow-up usesensureResponse(..., { afterToolResults: true })without requesting a second reply.sendFunctionOutputacknowledges delivery with{ delivered: true, automatic: true }; this is not speech completion.createResponse: falsecannot disable the service's native continuation, nor can per-response instructions control it. Other providers keep explicit response creation and completion tracking. - Normalize events that only prove liveness to
response.activitywith aresponse_id, without forwarding thinking text. - Set
conversationItemIdEcho: falsewhen the service assigns new IDs to acknowledged conversation items. Gateway correlates the single pending item without delays or skipping acknowledgment. - Set
imageRequiresAudioStart: trueonly if the service requires audio before the first video frame. Gateway primes that timeline with 20 ms of PCM16 silence so camera input does not require opening the microphone. - Set
acknowledgesConversationItems: falsewhen the service accepts input or tool-response messages without sending a conversation-item acknowledgment. Gateway resolves the send after writing the frame. - Set
restoreConversationContext: falsewhen injected history would be interpreted as live user input instead of passive context. visibility: 'gateway-only'lets the host select a Provider without exposing it in desktop settings or the public Provider list.
A Provider must implement the full contract — model(), voice(), isConfigured(), url(), headers(), classifyError(), buildSession(), buildSpeakResponse(), buildResultInjection(), buildPermissionInjection() — plus numeric inputSampleRate and outputSampleRate fields; registration throws on any missing member.
Provider and Protocol contracts are validated during registration and connection setup, so missing methods or invalid values fail immediately.
Behavior verification
Run node --test server/test/realtime-provider-behavior.test.mjs from the repository root. The same suite exercises every built-in Provider through the real session runtime and a local WebSocket service: context-only updates, tool continuation, permissions, cancellation, queued asynchronous replies, and reconnect/context restoration. Unsupported features must return an explicit unsupported result or error; the suite does not treat them as successful delivery.
The fixtures implement native wire behavior rather than calling the adapters to construct mock replies. When adding a dialect, add its fixture and run the shared suite. This is deterministic contract coverage, not a substitute for live service validation or audio/device testing.
server/test/native-tool-continuation.test.mjs additionally covers MCP result normalization through ToolCallHandler to a local native-protocol service, including failures, transport errors, and mixed results. In Doubao Seeduplex, function calls can precede response.done; the service waits for the matching tool results before completing the interaction. Tests must preserve that ordering rather than inventing an earlier terminal event.
For Google Live, context-only history uses clientContent with turnComplete: false; committing the turn starts a reply. Unlike passive history insertion in some protocols, Google documents that clientContent also interrupts active generation. Queued context waits for idle, while an immediate permission/context delivery can interrupt. Tool responses resume generation natively, and generationComplete does not release the response slot until turnComplete arrives.
Desktop settings
Built-in frontend settings live in shared/realtime-provider-definitions.mjs, a browser-safe catalog without credentials or connection implementations. Declare a provider's name, fields, environment mappings, and defaults there; maintain shared/realtime-model-catalog.mjs when selectable models are available. The desktop chooser, form, configuration persistence, and status labels consume these definitions without provider-specific HTML panels.
The form always displays Service URL, API Key, Model, and Voice, mapped to the endpoint, credential, model, and voice slots. Providers bind only configurable slots; unbound rows remain visible but disabled and are never persisted. Bearer tokens use the API Key row while keeping their original configuration keys. Use activeDefault for suggested self-hosted endpoints on selection and modelFamily for independent voice overrides. Existing configuration keys and aliases are preserved; inactive endpoint drafts cannot block applying the selected provider. A remote Gateway continues to own its frontend configuration.
Settings metadata is separate from runtime adapters. Host-injected custom Providers are not automatically registered in the desktop chooser.