Realtime Voice¶
Speech-to-speech AI conversations using providers like Google Gemini Live, OpenAI Realtime, and xAI Grok Realtime. Audio flows directly between the client and the AI provider; transcriptions are emitted into the Room so other channels (supervisor dashboards, logging) see the conversation.
RealtimeVoiceChannel¶
RealtimeVoiceChannel ¶
RealtimeVoiceChannel(channel_id, *, provider, transport, owns_transport=True, system_prompt=None, voice=None, tools=None, temperature=None, input_sample_rate=16000, output_sample_rate=24000, transport_sample_rate=None, emit_transcription_events=True, tool_handler=None, human_input_handler=None, mute_on_tool_call=False, tool_result_max_length=16384, tool_timeout_seconds=10.0, tool_timeouts=None, pipeline=None, recording=None, skills=None, script_executor=None, skill_delivery_mode=None, server_instructions=None, tool_recovery=True, tool_search=None, tool_search_pinned=None, tool_search_threshold=20, reasoning_backend=None, reasoning_timeout_s=120.0, tool_policy=None)
Bases: RealtimeToolRecoveryMixin, RealtimeToolsMixin, RealtimeToolGateMixin, RealtimeDelegationMixin, RealtimeTranscriptionMixin, RealtimeSpeechMixin, RealtimeAudioMixin, RealtimeResponseMixin, VoiceRecordingHooksMixin, VoicePipelineMixin, RealtimeModelHost, FrameworkAwareChannel, Channel
Real-time voice channel using speech-to-speech AI providers.
Wraps APIs like OpenAI Realtime and Gemini Live as a first-class RoomKit channel. Audio flows directly between the user's browser and the provider; transcriptions are emitted into the Room so other channels (supervisor dashboards, logging) see the conversation.
Category is TRANSPORT so that:
- on_event() receives broadcasts: another channel's text enters the
sessions as its author's quoted words (RFC §12.4)
- deliver() is called but returns empty (customer is on voice)
Example
from roomkit.voice.realtime.mock import MockRealtimeProvider, MockRealtimeTransport
provider = MockRealtimeProvider() transport = MockRealtimeTransport()
channel = RealtimeVoiceChannel( "realtime-1", provider=provider, transport=transport, system_prompt="You are a helpful agent.", ) kit.register_channel(channel)
Initialize realtime voice channel.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
channel_id
|
str
|
Unique channel identifier. |
required |
provider
|
RealtimeVoiceProvider
|
The realtime voice provider (OpenAI, Gemini, etc.). |
required |
transport
|
VoiceBackend
|
The audio transport (WebSocket, etc.). |
required |
owns_transport
|
bool
|
Close the transport with this channel (default True). Set False for a shared transport with unsubscribe support, such as FastRTC. Only this channel's sessions and callbacks close. |
True
|
system_prompt
|
str | None
|
Default system prompt for the AI. |
None
|
voice
|
str | None
|
Default voice ID for audio output. |
None
|
tools
|
list[dict[str, Any] | Any] | None
|
Tool definitions as dicts, or Tool objects with
|
None
|
temperature
|
float | None
|
Default sampling temperature. |
None
|
input_sample_rate
|
int
|
Default input audio sample rate (Hz). |
16000
|
output_sample_rate
|
int
|
Default output audio sample rate (Hz). |
24000
|
transport_sample_rate
|
int | None
|
Sample rate of audio from the transport (Hz).
When set and different from provider rates, enables automatic
resampling. Transports can override it in session metadata:
|
None
|
emit_transcription_events
|
bool
|
If True, emit final transcriptions as RoomEvents so other channels see them. |
True
|
tool_handler
|
ToolHandler | None
|
Async callable to execute tool calls.
Signature: |
None
|
human_input_handler
|
HumanInputToolHandler | None
|
The tools that ask a person, as on an
|
None
|
mute_on_tool_call
|
bool
|
If True, mute the transport microphone during
tool execution to prevent barge-in that causes providers
(e.g. Gemini) to silently drop the tool result. Defaults
to False — use |
False
|
tool_result_max_length
|
int
|
Maximum character length of tool results before truncation. Large results (e.g. SVG payloads) can overflow the provider's context window. Defaults to 16384. |
16384
|
tool_timeout_seconds
|
float | None
|
How long one tool call may take before its
handler is cancelled and the call fails (RFC §21.6), so a
handler that never answers cannot leave the caller in silence.
Defaults to 10 s; |
10.0
|
tool_timeouts
|
Mapping[str, float | None] | None
|
A bound per tool name, above the default ( |
None
|
tool_policy
|
ToolPolicy | None
|
Allow/deny rules for the session's tools, with the
meaning they have on an |
None
|
pipeline
|
AudioPipelineConfig | None
|
Optional |
None
|
recording
|
Any | None
|
Optional |
None
|
skills
|
SkillRegistry | None
|
Optional |
None
|
script_executor
|
ScriptExecutor | None
|
Optional |
None
|
skill_delivery_mode
|
str | None
|
How skill bodies reach the model.
|
None
|
server_instructions
|
Mapping[str, str | None] | None
|
What the MCP servers whose tools this
channel serves published for the model
(:attr: |
None
|
tool_recovery
|
bool
|
If True, detect tool calls that the model speaks
as text (e.g. |
True
|
tool_search
|
bool | None
|
Auto-enable Tool Search when the catalogue is
large enough to exceed the realtime model's reliable
tool-selection window (Google: 10–20 active tools on
Gemini Live). Pass |
None
|
tool_search_pinned
|
list[str] | None
|
Tool names that should ALWAYS be
visible (never hidden behind search). Use for tools
the agent calls reflexively — e.g. |
None
|
tool_search_threshold
|
int
|
Auto-activation threshold and the cap on how many tools may be live at once. Defaults to 20 to match Google's published recommendation. |
20
|
reasoning_backend
|
ReasoningBackend | None
|
Serves the integrator-side reasoning delegations of a full-duplex provider (RFC §12.4.1): when the model hands work over, the backend receives the transcript recorded since the previous delegation and its outputs return to the model as spoken or silent context. Its tool calls pass the same pre-execution gate as any realtime tool call. Only a full-duplex provider delegates; on any other provider the backend never runs. |
None
|
reasoning_timeout_s
|
float
|
Bound on one delegation's run. A backend that exceeds it is abandoned and the model is told so. Defaults to 120 seconds. |
120.0
|
wait_idle
async
¶
Wait until the selected sessions in the room are idle (not speaking).
session_ids=None includes every session. A proactive delivery can
restrict the wait to its pinned destinations without waiting on others.
An idle session has submitted its tool results, finished the provider response that follows them, and all audio has been forwarded to the transport. A queued transport such as SIP may still be playing that audio.
set_framework ¶
Set the framework reference for event routing.
Called automatically when the channel is registered with RoomKit.
on_trace ¶
Register a trace observer and bridge to the transport.
configure ¶
Update channel defaults for future sessions.
Active sessions are not affected — use reconfigure_session
for those. A tool under a name the channel, its reasoning backend or
orchestration serves, or given twice, is refused (RFC §21.1,
§12.4.1), and so is a name no vendor accepts (RFC §6.7).
start_audio_stream
async
¶
Open the realtime audio path on the provider.
Low-level escape hatch for opening the audio stream without
injecting any text. Most callers should use
inject_text(..., start_audio_stream=True) instead — that
composes the open + inject in a single call. No-op on providers
that don't need it.
on_room_attached
async
¶
Compile the patterns that find a copy of a runtime mark as the channel joins a room, off the event loop: the first injection of a session (a greeting) then does not wait for them (RFC §6.4).
inject_text
async
¶
Inject a text turn into the provider session.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
VoiceSession
|
The active voice session. |
required |
text
|
str
|
Text to inject. |
required |
role
|
str
|
The intent (RFC §12.4) — |
'user'
|
silent
|
bool
|
If True, add to conversation context without requesting a response. The agent sees the text on its next turn but does not react immediately. |
False
|
start_audio_stream
|
bool
|
If True, open the realtime audio path on the provider before sending the text. Set this on the first inject in outbound flows where the app speaks first (e.g. SIP dial greetings); no-op on providers that don't need priming (OpenAI, xAI). |
False
|
chain_depth
|
int
|
The chain depth of what the text stands for; the model's answer to it is one deeper (RFC §12.4). 0, the default, for text that opens a chain. |
0
|
A copy of a runtime mark in text is replaced before the provider takes it (RFC §6.4).
A session the channel no longer serves is not sent to:
not_sent / realtime_session_gone, as on a conference.
inject_image
async
¶
Inject an image into the provider session for multimodal analysis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
VoiceSession
|
The active voice session. |
required |
image_data
|
bytes
|
Raw image bytes. |
required |
mime_type
|
str
|
MIME type of the image. |
'image/png'
|
prompt
|
str
|
Optional text prompt accompanying the image, a copy of a runtime mark in it replaced (RFC §6.4). |
''
|
silent
|
bool
|
If True, add to context without requesting a response. |
False
|
start_session
async
¶
Start a new realtime voice session.
Connects both the transport (client audio) and the provider (AI service), then fires a framework event.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
room_id
|
str
|
The room to join. |
required |
participant_id
|
str
|
The participant's ID. |
required |
connection
|
Any
|
Protocol-specific connection (e.g. WebSocket), or an
awaitable resolving to it. With an awaitable, provider setup
starts while the connection is pending (e.g. SIP ringing).
Cancel the start task if the connection will never arrive.
Both branches belong to this session and are rolled back on
failure. The channel publishes the session only when both are ready.
Provider callbacks wait for publication in a bounded startup
queue; exceeding 2 MiB of audio or 500 events aborts startup.
Monotonic timestamps are written to metadata's
|
required |
metadata
|
dict[str, Any] | None
|
Optional session metadata. May include overrides for system_prompt, voice, tools, temperature. |
None
|
organization_id
|
str | None
|
The organization the caller acts for (RFC §17.2). The room is read scoped to it before the session exists: another organization's room is not found, and none of its recordings is told of the session. Left unset, the room is not read. |
None
|
Returns:
| Type | Description |
|---|---|
VoiceSession
|
The created VoiceSession. |
Raises:
| Type | Description |
|---|---|
RoomNotFoundError
|
organization_id is set and the room is missing, another organization's, or unreadable because the channel is not registered with a framework. |
end_session
async
¶
End a realtime voice session.
Disconnects both provider and transport, fires framework event.
Concurrent callers share one teardown: the first owns it, and a caller
that arrives while it runs (close(), the transport's disconnect
callback, a hangup tool) waits for it instead of tearing the session
down a second time. A joiner learns that the teardown ended, not how:
the owner's exception is the owner's. The work stays in the owner's
task, so a tool that is the first to end its own session is still the
current task the tool sweep spares; a joiner cancelled while waiting
leaves the owner's teardown untouched; and a call re-entered from the
teardown itself (a handler of the ended event ending the session it is
told about) returns at once instead of waiting on its own task. A
subclass's own teardown rides _before_session_teardown and is
covered by the same arbitration.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
VoiceSession
|
The session to end. |
required |
reconfigure_session
async
¶
reconfigure_session(session, *, system_prompt=None, voice=None, tools=None, temperature=None, provider_config=None)
Reconfigure an active session with new agent parameters.
Used during agent handoff to switch the AI personality, voice, and tools. Providers with session resumption (e.g. Gemini Live) preserve conversation history across the reconfiguration.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
VoiceSession
|
The active session to reconfigure. |
required |
system_prompt
|
str | None
|
New system instructions for the AI. |
None
|
voice
|
str | None
|
New voice ID for audio output. |
None
|
tools
|
list[dict[str, Any]] | None
|
New tool/function definitions; one under a name no vendor
accepts raises |
None
|
temperature
|
float | None
|
New sampling temperature. |
None
|
provider_config
|
dict[str, Any] | None
|
Provider-specific configuration overrides. |
None
|
connect_session
async
¶
Accept a realtime voice session via process_inbound.
Delegates to :meth:start_session which handles provider/transport
connection, resampling, and framework events.
disconnect_session
async
¶
Clean up realtime sessions on remote disconnect.
update_binding ¶
Update cached bindings for all sessions in a room.
Called by the framework after mute/unmute/set_access so the
audio gate in _pipeline_on_audio_received (pipeline path)
or _forward_client_audio (direct path) sees the new state.
handle_inbound
async
¶
Not used directly — audio flows via start_session.
on_event
async
¶
React to events from other channels — TEXT INJECTION.
A text another channel broadcast enters each session of the room as
content its author wrote, quoted after their name, never as the
application's instruction, whatever the event carries (RFC §12.4).
The application directs the model with inject_text and the
system intent, or kit.deliver(..., instruction=True). Skips
events from this channel (self-loop prevention).
deliver
async
¶
No-op for realtime voice — content is injected via kit.deliver()'s
_deliver_to_realtime_voice path, which calls inject_text
directly. Events broadcast through channel.deliver() would double-
feed the session.
ToolHandler¶
Async callback for executing tool/function calls from the AI provider. Receives (tool_name, arguments) and returns a JSON string.
Provider ABC¶
RealtimeVoiceProvider ¶
Bases: ABC
Abstract base class for speech-to-speech AI providers.
Wraps APIs like OpenAI Realtime and Gemini Live that handle audio-in → audio-out with built-in AI, VAD, and transcription.
The provider manages a bidirectional audio/event stream with the AI service. Callbacks are registered for events the provider emits.
Example
provider = OpenAIRealtimeProvider(api_key="sk-...", model="gpt-realtime-2.1")
provider.on_audio(handle_audio) provider.on_transcription(handle_transcription) provider.on_tool_call(handle_tool_call)
await provider.connect(session, system_prompt="You are a helpful agent.") await provider.send_audio(session, audio_bytes) await provider.disconnect(session)
Subclasses must call super().__init__() to initialise
the callback lists.
model_name
property
¶
Identifier of the model behind this session, for logs and traces.
Deliberately not abstract, unlike
:attr:~roomkit.providers.ai.base.AIProvider.model_name: every
conversational provider runs one named model, but a speech-to-speech
service need not expose one — ElevenLabs binds an agent configured in
its dashboard, PersonaPlex serves a single self-hosted model. Those
keep this default, which returns :attr:name.
So a caller must read the value as "the best identifier this provider
can give", not as a guaranteed model id: it may be a provider name.
Compare it against :meth:available_models when the distinction
matters.
For a composed stack the question has several answers, and the override says which stage it names — Deepgram's is its think model, not its speech-to-text or its voice.
supports_mid_session_reconfigure
property
¶
Whether reconfigure(...) can safely run mid-session.
Some realtime models (notably the gemini-3.x Live family)
reject send_client_content after the first model turn with
a WebSocket 1007 close and offer no documented alternative for
dynamic system_instruction updates. The base reconfigure also
tears down the live WebSocket and reconnects via session
resumption, which on those models is fragile when the system
prompt is non-trivial and silently drops in-flight tool calls
(their call_id is connection-scoped).
Channels that orchestrate dynamic tool / skill exposure must
check this flag before calling reconfigure and fall back to
delivering the same information through a different surface
(e.g. baking it into system_instruction at session start,
or returning it through the tool result that triggered the
change). Providers default to True for backwards
compatibility; subclasses override to False when their
upstream model cannot safely reconfigure.
supports_tools
property
¶
Whether the model can call tools (RFC §12.4).
False for a provider whose service never issues a function call:
the channel then declares none to it and warns once, rather than leave
a catalogue nobody can call.
supports_context_preservation
property
¶
Whether provider_config={"preserve_context": True} is supported.
In this mode the provider must retain tool-delivered instructions verbatim for the session: no compression, eviction, or reconnection with uncertain history. It must end the session and emit an error before continuing without that context. Provider duration limits still apply; this does not promise an indefinitely long session.
full_duplex
property
¶
Whether the model listens and speaks at the same time (RFC §12.4.1).
A full-duplex provider (OpenAI GPT-Live) handles being talked over by
itself and exposes no response or speech boundaries on its wire: it
synthesizes on_response_start/on_response_end from its own
output, and its interrupt() and truncate_audio() are no-ops.
The channel reads this flag to leave interruption to the model — no
playback flush, no gating of provider audio on user speech, and a
pipeline VAD kept to the observation role. Such a provider also holds
no tools of its own: reasoning and tool use are delegated to a backend
model, announced through :meth:on_delegation.
available_voices
classmethod
¶
Curated, offline catalog of voices this provider offers.
No API key or network required — call it on the class to discover the
voice ids that :meth:connect accepts. The base returns an empty
list; each provider overrides it with its catalog.
available_models
classmethod
¶
Curated, offline catalog of speech-to-speech models this provider runs.
The realtime counterpart of :meth:available_voices — call it on the
class to discover the model ids the constructor accepts.
Deliberately not folded into
:meth:~roomkit.providers.ai.base.AIProvider.available_models for the
reason the image catalog gives (RFC §25.6): the sets are disjoint — no
realtime id answers a chat completion, and no chat id opens a realtime
session — so merging them would oblige every consumer of the
conversational catalog to filter out models it can never use.
The base returns an empty list, and two providers keep it on purpose:
Deepgram composes its agent from stages that have catalogs of their own
(speak is the voice catalog, think reads the vendors' chat
catalogs), and ElevenLabs binds a dashboard-configured agent, so
neither has an end-to-end model id to list.
list_voices
async
¶
Voices reported live by the provider's API.
The base implementation returns the curated :meth:available_voices.
Providers whose API exposes a voices endpoint (e.g. ElevenLabs) override
this to query it, backfilling metadata from the catalog via
:meth:_merge_curated. Fixed-voice providers (OpenAI Realtime, Gemini
Live) keep the curated list.
connect
abstractmethod
async
¶
connect(session, *, system_prompt=None, voice=None, tools=None, temperature=None, input_sample_rate=16000, output_sample_rate=24000, server_vad=True, provider_config=None)
Connect a session to the provider's AI service.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
VoiceSession
|
The realtime session to connect. |
required |
system_prompt
|
str | None
|
System instructions for the AI. |
None
|
voice
|
str | None
|
Voice ID for audio output. |
None
|
tools
|
list[dict[str, Any]] | None
|
Tool/function definitions the AI can call. |
None
|
temperature
|
float | None
|
Sampling temperature. |
None
|
input_sample_rate
|
int
|
Sample rate of input audio (Hz). |
16000
|
output_sample_rate
|
int
|
Desired sample rate for output audio (Hz). |
24000
|
server_vad
|
bool
|
Whether to use server-side voice activity detection. |
True
|
provider_config
|
dict[str, Any] | None
|
Provider-specific configuration options. Each provider documents which keys it accepts. |
None
|
send_audio
abstractmethod
async
¶
Send audio data to the provider for processing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
VoiceSession
|
The active session. |
required |
audio
|
bytes
|
Raw PCM audio bytes. |
required |
inject_text
abstractmethod
async
¶
Inject text and report the provider's submission boundary.
Return VoiceInjectionResult to distinguish a completed send from
a guaranteed non-submission or uncertain acceptance. Returning None
is supported, but proactive delivery reports its outcome as unknown.
The role is an intent, mapped by each provider onto what its wire
offers (RFC §12.4): "system" is an instruction from the
application — how to behave, or what to do now — and "user" is
content for the conversation: a user turn the model answers on a
turn-based provider, words the model says aloud on a full-duplex one.
Text that directs the model, an opening greeting included, is always
"system"; sent as "user" it would be voiced by a full-duplex
model instead of followed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
VoiceSession
|
The active session. |
required |
text
|
str
|
Text to inject. |
required |
role
|
str
|
The intent — |
'user'
|
silent
|
bool
|
If True, add to conversation context without requesting a response. The agent sees the text on its next turn but does not react immediately. |
False
|
inject_image
async
¶
Inject an image into the conversation for multimodal analysis.
Not all providers support vision. The default implementation
raises NotImplementedError. Providers with multimodal input
(e.g. Gemini Live) should override this.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
VoiceSession
|
The active session. |
required |
image_data
|
bytes
|
Raw image bytes. |
required |
mime_type
|
str
|
MIME type of the image. |
'image/png'
|
prompt
|
str
|
Optional text prompt accompanying the image. |
''
|
silent
|
bool
|
If True, add to context without requesting a response. |
False
|
submit_tool_result
abstractmethod
async
¶
Submit a tool call result back to the provider.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
VoiceSession
|
The active session. |
required |
call_id
|
str
|
The tool call ID from the on_tool_call callback. |
required |
result
|
str
|
JSON-serialized result string. |
required |
submit_tool_error
async
¶
Submit the result of a call that failed: refused, failed, blocked, served by nothing (RFC §12.4).
A provider whose protocol marks a result as an error overrides this so the model does not read a refusal as a success. The default submits it as any result: the body says it failed.
submit_delegation_output
async
¶
Return a reasoning backend's output to the model (RFC §12.4.1).
Answers a delegation the provider announced through
:meth:on_delegation with target="integrator". spoken=True
asks the model to relay the text to the user in its own words;
spoken=False adds it as silent context the model may draw on.
A provider that bounds one append splits the text rather than refuse
it. Only full-duplex providers implement this; the default raises,
since a provider without delegation has nothing to answer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
VoiceSession
|
The active session. |
required |
delegation_id
|
str
|
Identifier from the |
required |
text
|
str
|
The backend's output. |
required |
spoken
|
bool
|
Whether the model should voice it. |
required |
interrupt
abstractmethod
async
¶
Interrupt the current AI response.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
VoiceSession
|
The active session. |
required |
truncate_audio
async
¶
Synchronize provider context with the audio the user actually heard.
Realtime providers that keep conversation state may generate audio faster than a transport can play it. When playback is interrupted, implementations can override this hook to remove the unheard tail from their server-side context. Providers that manage playback themselves, or whose protocol has no equivalent operation, keep the default no-op.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
VoiceSession
|
The active session. |
required |
audio_end_ms
|
int
|
Played duration of the interrupted response in milliseconds, measured from physical playback onset. |
required |
disconnect
abstractmethod
async
¶
Disconnect a session from the provider.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
VoiceSession
|
The session to disconnect. |
required |
reconfigure
async
¶
reconfigure(session, *, system_prompt=None, voice=None, tools=None, temperature=None, provider_config=None)
Reconfigure a session with new parameters.
Used during agent handoff to switch the AI personality, voice, and tools. The default implementation disconnects and reconnects; providers with session resumption (e.g. Gemini Live) should override to preserve conversation context.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
VoiceSession
|
The active session to reconfigure. |
required |
system_prompt
|
str | None
|
New system instructions. |
None
|
voice
|
str | None
|
New voice ID. |
None
|
tools
|
list[dict[str, Any]] | None
|
New tool/function definitions. |
None
|
temperature
|
float | None
|
New sampling temperature. |
None
|
provider_config
|
dict[str, Any] | None
|
Provider-specific configuration overrides. |
None
|
send_event
async
¶
Send a raw provider-specific event to the underlying service.
This is an escape hatch for sending protocol-level messages that
are not covered by the standard provider API (e.g. OpenAI's
session.update or input_audio_buffer.commit).
The default implementation raises :exc:NotImplementedError.
Providers that support raw events should override this.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
VoiceSession
|
The active session. |
required |
event
|
dict[str, Any]
|
A JSON-serializable dict that will be sent verbatim to the provider's underlying connection. |
required |
is_responding ¶
Check if the provider is actively generating a response.
Returns True between response.created and response.done.
on_tool_call ¶
Register callback for tool/function calls from the AI.
Called as (session, call_id, name, arguments). arguments is the
call's arguments as a mapping, or the text the model wrote when they
do not read as one (invalid JSON, an array, a fragment): read the
wire's arguments with
:func:~roomkit.providers.ai.tool_calls.readable_arguments, or, on a
wire that says whether the response cut the call, with
:func:~roomkit.providers.ai.tool_calls.realtime_call_arguments, which
marks a cut call :class:~roomkit.providers.ai.tool_calls.CutArguments.
The channel refuses a call that arrives as text (one marked cut as cut
off), so it never runs on a mapping that only passes for arguments
(RFC §6.4, §12.4).
on_tool_call_cancelled ¶
Register callback for tool calls the model will not read (RFC §12.4).
Called as (session, call_ids) for every call the provider
abandons, whatever the cause: the model discarding it (Gemini Live's
tool_call_cancellation when the user interrupts), a reconnect
orphaning it (call ids are connection-scoped), the provider's own
wait on the result timing out, the connection or the session ending.
The channel cancels the handler still running for such a call and
reports it to ON_TOOL_CALL's observers as cancelled.
on_delegation ¶
Register callback for reasoning delegations (RFC §12.4.1).
Called as (session, delegation_id, target) when a full-duplex
model hands reasoning or tool use to a backend. target is
"hosted" when the provider's service runs the backend — its
function calls then arrive through :meth:on_tool_call — and
"integrator" when the application must answer through
:meth:submit_delegation_output. The request carries no task text:
the backend works out what was asked from the conversation.
on_response_start ¶
Register callback for when the AI starts generating a response.
on_usage ¶
Register callback for the usage this provider reports (RFC §12.4.2).
Called as (session, usage) every time the provider records what a
session consumed: the two token totals and whatever breakdown its API
sends beside them, the cumulative duration of a provider billed by
session seconds, a hosted backend's own tokens.
This is the surface to bill a call from. :attr:VoiceSession.last_usage
holds the same map, but the next report replaces it and the channel
clears it at the end of each turn, so a reader that polls can miss a
turn while a callback sees every one.
start_audio_stream
async
¶
Open the audio input path on the provider.
Some providers need to be nudged into "audio is flowing" mode before other operations (e.g. text injection) are safe — for instance, when the application wants to trigger a greeting before the remote side has spoken. Providers that care override this to send an initial silent audio frame (or flip their protocol state); providers that don't inherit the no-op.
Safe to call unconditionally. No-op if the session is not active or the stream has already been opened.
send_activity_start
async
¶
Signal that user speech activity has started.
Used in manual VAD mode: local VAD detects speech and the channel
calls this to inform the provider. The provider translates this
into its protocol's activity signal (e.g. Gemini ActivityStart).
Default: no-op. Override when the provider supports manual mode.
send_activity_end
async
¶
Signal that user speech activity has ended.
Used in manual VAD mode: local VAD detects silence and the channel
calls this to inform the provider. The provider translates this
into its protocol's activity signal (e.g. Gemini ActivityEnd).
Default: no-op. Override when the provider supports manual mode.
Transport ABC¶
All realtime transports extend VoiceBackend.
Session & State¶
See VoiceSession and VoiceSessionState.
Events¶
RealtimeTranscriptionEvent
dataclass
¶
RealtimeTranscriptionEvent(session, text, role, is_final, was_barge_in=False, item_id=None, timestamp=_utcnow())
Transcription produced by the realtime provider.
Fired through ON_TRANSCRIPTION hooks. For final transcriptions, the channel emits a RoomEvent so other channels see the conversation.
was_barge_in
class-attribute
instance-attribute
¶
True if this transcription resulted from a barge-in (user interrupted the AI while it was speaking).
item_id
class-attribute
instance-attribute
¶
Provider-specific item ID for correlation.
timestamp
class-attribute
instance-attribute
¶
When the transcription was received.
RealtimeSpeechEvent
dataclass
¶
Speech activity detected by the realtime provider's server-side VAD.
RealtimeToolCallEvent
dataclass
¶
Deprecated — use :class:roomkit.models.tool_call.ToolCallEvent instead.
Kept for backward compatibility. The unified ON_TOOL_CALL hook
fires :class:ToolCallEvent from both AIChannel and RealtimeVoiceChannel.
timestamp
class-attribute
instance-attribute
¶
When the tool call was received.
RealtimeErrorEvent
dataclass
¶
Error from the realtime provider.
timestamp
class-attribute
instance-attribute
¶
When the error occurred.
RealtimeDelegationEvent
dataclass
¶
A full-duplex model handed reasoning or tool use to a backend.
Fired through ON_REALTIME_DELEGATION hooks (RFC §12.4.1) for both delegation targets. It names the delegation and where it went; it carries no task text, because the wire carries none either.
delegation_id
instance-attribute
¶
Provider-assigned identifier, opaque; correlates the outputs returned.
target
instance-attribute
¶
"hosted": the provider's service runs the backend.
"integrator": the channel's ReasoningBackend does.
timestamp
class-attribute
instance-attribute
¶
When the delegation was announced.
Callback Types¶
| Callback | Signature |
|---|---|
RealtimeAudioCallback |
(RealtimeSession, bytes) -> Any |
RealtimeTranscriptionCallback |
(RealtimeSession, str, str, bool) -> Any |
RealtimeSpeechStartCallback |
(RealtimeSession) -> Any |
RealtimeSpeechEndCallback |
(RealtimeSession) -> Any |
RealtimeToolCallCallback |
(RealtimeSession, str, str, dict) -> Any |
RealtimeResponseStartCallback |
(RealtimeSession) -> Any |
RealtimeResponseEndCallback |
(RealtimeSession) -> Any |
RealtimeErrorCallback |
(RealtimeSession, str, str) -> Any |
RealtimeDelegationCallback |
(RealtimeSession, str, str) -> Any — (session, delegation_id, target) |
Concrete Providers¶
Gemini Live¶
from roomkit.providers.gemini.realtime import GeminiLiveProvider
provider = GeminiLiveProvider(
api_key="...",
model="gemini-3.8-live",
)
Install with: pip install roomkit[realtime-gemini]
Supports provider_config metadata keys: language, top_p, top_k, max_output_tokens, seed, enable_affective_dialog, thinking_budget, proactive_audio, start_of_speech_sensitivity, end_of_speech_sensitivity, silence_duration_ms, prefix_padding_ms, no_interruption.
OpenAI Realtime¶
from roomkit.providers.openai.realtime import OpenAIRealtimeProvider
provider = OpenAIRealtimeProvider(
api_key="...",
model="gpt-realtime-2.1",
)
Supports provider_config metadata keys: codec, stt_model, language,
transcription_prompt, noise_reduction, turn_detection_type, eagerness,
threshold, silence_duration_ms, prefix_padding_ms, idle_timeout_ms,
interrupt_response, create_response, speed, reasoning_effort.
Install with: pip install roomkit[realtime-openai]
OpenAI GPT-Live (full-duplex)¶
from roomkit.providers.openai.live import HostedReasoning, OpenAILiveProvider
provider = OpenAILiveProvider(
api_key="...",
model="gpt-live-1",
delegation=HostedReasoning(model="gpt-5.6-terra", instructions="..."),
)
A different API from OpenAI Realtime: full-duplex, no tools on the live model,
reasoning delegated to a backend (RFC §12.4.1). full_duplex is True, so the
channel runs no barge-in path; response and speech boundaries are synthesized
from transcript deltas with turn_gap_ms of quiet. Supports provider_config
metadata keys: codec, history. See the guide.
Install with: pip install roomkit[realtime-openai]
OpenAILiveProvider ¶
OpenAILiveProvider(*, api_key, model=_DEFAULT_MODEL, base_url=None, delegation=None, turn_gap_ms=800, close_timeout_s=5.0)
Bases: OpenAILiveClientMixin, OpenAILiveHostedDelegationMixin, OpenAILiveEventHandlersMixin
Speech-to-speech provider for OpenAI GPT-Live (/v1/live/sessions).
Full-duplex. The model handles being talked over by itself, so
:attr:full_duplex is True: the channel never flushes playback or
gates the model's audio on user speech, :meth:interrupt and
:meth:truncate_audio do nothing, and a pipeline VAD stays in the
observation role. The wire carries no response or speech boundaries;
this provider synthesizes them from transcript deltas and assistant audio
activity, with a quiet gap of turn_gap_ms per speaker (RFC §12.4.1).
Transcripts. Partial transcriptions carry deltas for both roles and the final carries the whole turn, closed by the gap. A channel that keeps a transcript ledger reads the partials.
Reasoning delegation. The model holds no tools. Pass
:class:HostedReasoning to let OpenAI run the backend model — the
channel's tools become its tools and its function calls flow through the
usual on_tool_call / submit_tool_result path — or
:class:IntegratorReasoning (the default) to serve delegations from a
ReasoningBackend configured on the channel.
Fixed session. Model, instructions, voice, audio format, delegation
mode and seeded history are set once by session.start.
:meth:reconfigure appends a changed system prompt and, in hosted mode,
updates the backend's tools without replacing the session; a voice change
reconnects. :attr:supports_mid_session_reconfigure is therefore
False.
Text injection is paraphrased. inject_text maps a system role
to an instructions append and a user role to a spoken-context append
(or a silent one with silent=True); the model relays the text in its
own words rather than reading it. The session takes no user text once
started, so user is what the model will say, never a turn it answers:
an instruction, the opening greeting included, is role="system"
(OpenAI's documented way to have the model speak first), and a line the
application wrote (role="assistant") becomes an instructions append
asking the model to say it — as commentary, the model improvised its own. An append is
measured with the model's
tokenizer (tiktoken, installed with the extra; UTF-8 bytes bound it
when that is missing) and split on sentence boundaries only where it
exceeds the API's per-append bound — the model voices each piece of a
split commentary. Image injection is not available on the Live endpoint.
Audio. One wire format serves both directions, chosen from the
channel's output_sample_rate: PCM16 at 16 or 24 kHz, or G.711 at
8 kHz with provider_config={"codec": "pcmu" | "pcma"}. Input at a
different input_sample_rate is resampled here.
Usage. The live model bills session seconds, reported in
session._last_usage["live_seconds"]; a hosted backend's token usage
is reported under session._last_usage["backend"] with its own model.
provider_config keys: codec ("pcm", "pcmu", "pcma")
and history (a list of {"role", "text"} text messages seeding the
session, at most 128).
Requires the websockets package (pip install 'roomkit[realtime-openai]').
Example
provider = OpenAILiveProvider( api_key="sk-...", delegation=HostedReasoning(model="gpt-5.6-terra", instructions="..."), )
reconfigure
async
¶
reconfigure(session, *, system_prompt=None, voice=None, tools=None, temperature=None, provider_config=None)
Apply what the fixed session can take; reconnect for the rest.
A changed system prompt is appended to the instructions and, in
hosted mode, a changed tool list reaches the backend through
session.update. A voice change (or a new codec) needs a new
session: the connection is replaced and reseeded from the remembered
settings (RFC §12.4.1).
HostedReasoning
dataclass
¶
HostedReasoning(model, instructions=None, reasoning_effort=None, max_output_tokens=None, service_tier=None, parallel_tool_calls=None, extra=dict())
Hosted backend: OpenAI runs the Responses model the live model delegates to.
The channel's tools become this model's tools; its function calls reach
the channel through on_tool_call and are answered through
submit_tool_result, exactly as with any other provider (RFC §12.4.1,
hosted backend).
Attributes:
| Name | Type | Description |
|---|---|---|
model |
str
|
Responses model id (e.g. |
instructions |
str | None
|
The backend's own instructions. |
reasoning_effort |
str | None
|
|
max_output_tokens |
int | None
|
Output cap for one delegated response. |
service_tier |
str | None
|
Responses service tier ( |
parallel_tool_calls |
bool | None
|
Whether the backend may call several tools at once. |
extra |
Mapping[str, Any]
|
Other |
IntegratorReasoning
dataclass
¶
Integrator backend: the channel's ReasoningBackend answers delegations.
The model signals only that it is handing work over; the provider fires
on_delegation(session, delegation_id, "integrator") and the answer
returns through submit_delegation_output (RFC §12.4.1, integrator
backend). Tools given to connect() stay off the wire: they are the
backend's, and the channel keeps them as its declared catalogue.
xAI Grok Realtime¶
from roomkit.providers.xai.config import XAIRealtimeConfig
from roomkit.providers.xai.realtime import XAIRealtimeProvider
provider = XAIRealtimeProvider(
XAIRealtimeConfig(
api_key="...",
model="grok-3-fast",
voice="eve",
transcription_model="grok-2-audio",
)
)
Install with: pip install websockets
Supports provider_config metadata keys: turn_detection_type, threshold, silence_duration_ms, prefix_padding_ms, transcription_model, input_audio_format, output_audio_format, modalities.
Native tools: web_search, x_search (passed as {"type": "web_search"}).
Available voices: eve, ara, rex, sal, leo.
Deepgram Voice Agent¶
from roomkit.providers.deepgram import DeepgramAgentConfig, DeepgramAgentProvider
provider = DeepgramAgentProvider(
DeepgramAgentConfig(
api_key="...",
listen_model="nova-3", # STT stage
think_model="gpt-4o-mini", # LLM stage
speak_model="aura-2-thalia-en", # TTS stage
)
)
Install with: pip install roomkit[realtime-deepgram]
Supports provider_config metadata keys: listen_model, listen_version, listen_language, keyterms, smart_format, think_provider, think_model, think_endpoint, context_length, speak_model, speak_language, greeting, tags, input_encoding, output_encoding, output_container, output_bitrate, settings.
Turn detection is always Deepgram's (server_vad=False is ignored with a warning), transcriptions are final-only, and reconfigure() patches the live session through UpdateThink / UpdateSpeak instead of reconnecting. See the Realtime Voice Providers guide.
DeepgramAgentProvider ¶
Bases: RealtimeVoiceProvider
Realtime voice provider using the Deepgram Voice Agent API.
Requires the websockets package.
Example::
from roomkit.providers.deepgram import DeepgramAgentConfig, DeepgramAgentProvider
provider = DeepgramAgentProvider(DeepgramAgentConfig(api_key="..."))
provider.on_audio(handle_output_audio)
provider.on_transcription(handle_transcription)
await provider.connect(session, system_prompt="You are a helpful assistant.")
await provider.send_audio(session, audio_bytes)
Provider config keys (via the provider_config dict):
listen_model, listen_version, listen_language (str): override the STT stage.
keyterms (list[str]), smart_format (bool): further STT tuning.
think_provider, think_model (str), think_endpoint (dict), context_length:
override the LLM stage — think_endpoint points at a self-hosted,
OpenAI-compatible server.
speak_model, speak_language (str): override the TTS stage.
speak_provider (dict): full agent.speak.provider replacement, sent
verbatim — names a non-Deepgram TTS vendor (eleven_labs,
cartesia…) with that vendor's own fields. Takes precedence over
voice/speak_model.
speak_endpoint (dict): agent.speak.endpoint (URL + auth headers) —
required for BYO-key TTS vendors; Deepgram-managed ones need none.
max_prompt_chars (int | None): per-session override of the managed-LLM
prompt-size warning threshold; None disables the warning.
greeting (str): line the agent speaks when the session opens.
input_encoding, output_encoding (str): linear16 (default), mulaw,
alaw, opus… Use mulaw at 8000 Hz for a telephony transport
and no resampling is needed on either leg.
output_container (str), output_bitrate (int): container/bitrate for
encodings that need them.
tags (list[str]): labels attached to the session in Deepgram's dashboard.
settings (dict): deep-merged into the final Settings payload, last.
Escape hatch for fields this provider does not model explicitly.
Known limitations
- Turn detection is always Deepgram's;
connect(server_vad=False)is not honoured and warns. ConversationTextis final-only, so no interim transcriptions.- The protocol has no client-side interrupt message. Barge-in is signalled
the other way, by
UserStartedSpeaking, which this provider surfaces ason_speech_start— that is what drives RoomKit's playback flush. - Deepgram caps a session at two hours, warning at 1 h 55
(
MAXIMUM_SESSION_LENGTH_APPROACHING).
model_name
property
¶
The think model — the stage that decides what the agent says.
Deepgram composes its agent from three stages (listen, think, speak),
so no single id describes it end to end. This names the reasoning one,
because that is the model a log line about a tool call is asking
about; listen_model and speak_model stay on the config.
inject_text
async
¶
Inject text into the conversation (RFC §12.4).
role="assistant" puts the words in the agent's mouth
(InjectAgentMessage); role="user" makes the agent hear them and
answer (InjectUserMessage).
Deepgram has no message that adds to the conversation silently, so a
silent injection is appended to the system prompt via UpdatePrompt
instead: the agent takes it into account on its next turn without
reacting to it now. Only a system one joins the instructions; any
other intent is content, appended in a <context> block of its own
so it never reads as more of them (RFC §12.4). UpdatePrompt never
starts a turn, so a non-silent system instruction travels as
InjectUserMessage, which the agent acts on at once; a standing
instruction ("speak more slowly from now on") is role="system",
silent=True.
interrupt
async
¶
Mark the agent's turn as over locally.
The Voice Agent protocol has no client-side interrupt: Deepgram signals
barge-in the other way, with UserStartedSpeaking, and the channel
flushes playback on the resulting on_speech_start. All that is left
here is to stop reporting the session as responding.
reconfigure
async
¶
reconfigure(session, *, system_prompt=None, voice=None, tools=None, temperature=None, provider_config=None)
Update the live session in place, without reconnecting.
UpdateThink carries prompt, model and functions in a single message
and UpdateSpeak swaps the voice, so — unlike the base implementation —
an agent handoff keeps the WebSocket and the conversation context.
DeepgramAgentConfig ¶
Bases: BaseModel
Configuration for the Deepgram Voice Agent speech-to-speech provider.
Deepgram composes an agent from three independently chosen pieces — listen
(speech-to-text), think (the LLM) and speak (text-to-speech) — so the
defaults here name one model per stage rather than a single end-to-end model.
Every field is overridable per session through connect(provider_config=...).
Distinct from :class:~roomkit.voice.stt.deepgram.DeepgramConfig, which
configures the batch/streaming transcription API — same vendor, different
protocol and different endpoint.
Attributes:
| Name | Type | Description |
|---|---|---|
api_key |
SecretStr
|
Deepgram API key (sent as |
base_url |
str
|
Voice Agent WebSocket URL. Point at
|
listen_model |
str
|
Speech-to-text model (e.g. |
listen_version |
str | None
|
API version for the listen provider — required by Flux
( |
listen_language |
str | None
|
Language code for transcription (e.g. |
think_provider |
str
|
LLM provider type ( |
think_model |
str
|
LLM model id served by |
speak_model |
str
|
Aura voice id — see :mod: |
speak_language |
str | None
|
Language code for synthesis, when the voice supports it. |
speak_provider |
dict[str, Any] | None
|
Full |
speak_endpoint |
dict[str, Any] | None
|
|
greeting |
str | None
|
Optional line the agent speaks as soon as the session opens. |
keepalive_interval |
float
|
Seconds between |
max_prompt_chars |
int | None
|
Warn when the system prompt exceeds this many characters.
Defaults to Deepgram's documented 25,000-character cap for managed
LLMs, past which Deepgram truncates the prompt ( |
Reasoning Delegation¶
The integrator-side backend a full-duplex model's delegations are served through (RFC §12.4.1). See the Reasoning Delegation guide.
ReasoningBackend ¶
Bases: ABC
The backend a full-duplex model's integrator-side delegation goes to.
Implementations work out the request from request.transcript, answer
it with their own model, context and tools, and yield
:class:ReasoningOutput as they go. Tool calls MUST go through
request.execute_tool so the channel's gate applies (RFC §12.4.1).
ReasoningRequest
dataclass
¶
ReasoningRequest(session, delegation_id, transcript, first, tools=list(), execute_tool=None, execute_tool_call=None, report_refusal=None, unavailable=dict(), report_call=None, participant_role=None)
What a backend receives for one delegation (RFC §12.4.1).
Attributes:
| Name | Type | Description |
|---|---|---|
session |
VoiceSession
|
The voice session whose model delegated. |
delegation_id |
str
|
Opaque provider identifier; returned unchanged with every output. |
transcript |
list[TranscriptLine]
|
User and assistant lines recorded since the previous request — the backend's only account of what was asked. |
first |
bool
|
Whether this is the session's first request, in which case the transcript is the whole conversation so far. |
tools |
list[dict[str, Any]]
|
The channel's declared tool catalogue, as tool dicts. The backend offers these to its model. |
execute_tool |
ToolExecutor | None
|
Runs one tool call through the channel's pre-execution
gate (declared catalogue, tool policy, skill gating, argument
schema, |
execute_tool_call |
ToolCallExecutor | None
|
The same call, returning a :class: |
report_refusal |
RefusalReporter | None
|
Reports a call the backend's own loop ended before
the gate (its arguments did not read, a stop cut it) to the
channel's ON_TOOL_CALL observers, as |
unavailable |
dict[str, str]
|
The session's tools the model is not offered, each with the refusal a call to it reads (the tool policy's, a skill's gating), so a backend refuses it in the gate's words (RFC §21.1). |
report_call |
CallReporter | None
|
Reports a call the backend's own provider served,
outside the gate, to the channel's ON_TOOL_CALL hooks, as
|
participant_role |
str | None
|
The session participant's role when the
delegation was handed over ( |
ToolCallResult
dataclass
¶
One tool call's outcome, as a backend's model reads it (RFC §12.4.1).
Attributes:
| Name | Type | Description |
|---|---|---|
text |
str
|
What the model reads: the result, or why the call failed. |
is_error |
bool
|
The call was refused, failed, blocked, served by nothing or cancelled, as every tool loop marks such a call (RFC §9.3). |
refused |
bool
|
Among those, the call was refused (a gate, a hook's block before it ran, its handler's refusal): the backend's loop reads it refused, and any other error failed (RFC §9.3, §12.4.1). |
ReasoningOutput
dataclass
¶
One piece of a backend's output, on its way to the model.
Attributes:
| Name | Type | Description |
|---|---|---|
text |
str
|
What the backend produced. |
spoken |
bool
|
|
is_final |
bool
|
The answer to the request, as opposed to progress toward it. |
TranscriptLine
dataclass
¶
One speaker's contribution in the transcript handed to a backend.
AgentReasoningBackend ¶
Bases: ReasoningBackend
A backend that is an agent like any other, driven by the voice (RFC §12.4.1).
Each delegation runs on the agent's tool loop, the AI channel's, with the
agent's settings and everything that loop does: its round cap, deadline
and budget, its retry of an empty answer and of a call the provider could
not parse, its refusal of a call whose arguments do not read, its span and
its usage (RFC §6.4). The tools it offers are the voice session's
catalogue, each call served through the voice channel's gate, which
bounds and reports it: the agent's own tool_timeout_seconds and
tool_timeouts do not apply. The agent's own tools, skills or sandbox
would bypass that gate, so an agent carrying any is refused. The agent is
the backend's: its tool handler and its reports are taken over, so an
agent registered with a kit, whose hooks would judge each call a second
time, is refused too.
Each request becomes one user message carrying the transcript. Text the
model writes before a tool round is yielded as progress, silent by default
and spoken with spoken_progress=True; its final answer is yielded
spoken. A turn that did not complete (its round cap, deadline or budget
cut it) has no answer, and its narration is not passed off as one: the
run raises :class:ReasoningCutShortError, as a provider error raises its
own, and the channel's spoken fallback answers. The backend keeps its own
conversation per voice session, and serves a session's delegations one at
a time, so each sees what the one before it worked out.
Example
backend = AgentReasoningBackend( Agent("reasoner", provider=OpenAIAIProvider(...), system_prompt=PROMPT) ) channel = RealtimeVoiceChannel( "voice", provider=live, transport=transport, tools=[check_flight, rebook_flight], reasoning_backend=backend, )
served_names ¶
The names the agent serves itself (its large-result re-read, its
Tool Search unless tool_search=False), which it answers before
the channel's gate (RFC §12.4.1).
close
async
¶
Close the agent the backend owns (it refuses one the kit has): its running turns are cut and its provider released.
AIProviderReasoningBackend ¶
AIProviderReasoningBackend(provider, *, system_prompt=None, max_tool_rounds=5, temperature=None, spoken_progress=False)
Bases: AgentReasoningBackend
Default backend: an :class:~roomkit.providers.ai.base.AIProvider as an
agent of its own (:class:AgentReasoningBackend).
Example
backend = AIProviderReasoningBackend( AnthropicAIProvider(AnthropicConfig(api_key="...")), system_prompt=BACKEND_PROMPT, )
ReasoningCutShortError ¶
Bases: TurnCutShortError
A backend's turn ended before its answer: the round cap, the deadline or the budget cut it. The channel answers the delegation with its spoken fallback (RFC §12.4.1).
Transports¶
WebSocket Transport¶
from roomkit.voice.realtime.ws_transport import WebSocketRealtimeTransport
transport = WebSocketRealtimeTransport()
Handles bidirectional audio over WebSocket. Client sends {"type": "audio", "data": "<base64 PCM>"}, server sends audio, transcriptions, and speaking indicators as JSON messages.
Install with: pip install roomkit[websocket]
FastRTC Transport (WebRTC)¶
from roomkit.voice.realtime.fastrtc_transport import (
FastRTCRealtimeTransport,
mount_fastrtc_realtime,
)
transport = FastRTCRealtimeTransport(
input_sample_rate=16000, # Mic input sample rate
output_sample_rate=24000, # Provider output sample rate
)
# Mount WebRTC endpoints on FastAPI app
mount_fastrtc_realtime(app, transport, path="/rtc-realtime")
WebRTC-based transport using FastRTC in passthrough mode (no VAD). Audio flows bidirectionally between the browser and the speech-to-speech AI provider, which handles its own server-side VAD.
Unlike the FastRTCVoiceBackend (which uses ReplyOnPause for VAD in the traditional VoiceChannel pipeline), this transport simply passes audio through — ideal for speech-to-speech providers like OpenAI Realtime or Gemini Live.
Install with: pip install roomkit[fastrtc]
Connection flow:
mount_fastrtc_realtime(app, transport)creates a FastRTCStreamwith a passthrough handler- Browser connects via WebRTC → FastRTC calls
handler.copy()→start_up() start_up()readswebrtc_idfrom FastRTC context, registers with transport- Transport fires
on_client_connectedcallback (if set) — use to auto-create sessions - App calls
channel.start_session(room_id, participant_id, connection=webrtc_id) start_session()→transport.accept(session, webrtc_id)maps session to handlerreceive()/emit()now flow audio with session context
Audio format conversion:
FastRTC works with (sample_rate, numpy.ndarray[int16]) tuples. The transport ABC uses raw bytes (PCM16 LE). Conversion is automatic:
- Inbound:
audio_array.tobytes()→ fire callbacks with bytes - Outbound:
np.frombuffer(audio_bytes, dtype=np.int16)→ return as(rate, array)
DataChannel messages:
JSON messages (transcriptions, speaking indicators) are sent via the WebRTC DataChannel using send_message().
| Transport | Protocol | VAD | Dependency |
|---|---|---|---|
WebSocketRealtimeTransport |
WebSocket | Provider-side | roomkit[websocket] |
FastRTCRealtimeTransport |
WebRTC | Provider-side | roomkit[fastrtc] |
Lazy-loaded via roomkit.voice.get_fastrtc_realtime_transport() and roomkit.voice.get_mount_fastrtc_realtime() to avoid requiring fastrtc/numpy at import time.
Mock Classes¶
MockRealtimeProvider ¶
Bases: RealtimeVoiceProvider
Mock realtime voice provider for testing.
Tracks all method calls and provides helpers to simulate provider events (transcriptions, audio, tool calls, etc.).
Example
provider = MockRealtimeProvider()
Track calls¶
await provider.connect(session, system_prompt="Hello") assert provider.calls[-1].method == "connect"
Simulate events¶
await provider.simulate_transcription(session, "Hi there", "user", True) await provider.simulate_audio(session, b"audio-data") await provider.simulate_tool_call(session, "call-1", "get_weather", {"city": "NYC"})
simulate_transcription
async
¶
Simulate a transcription event from the provider.
simulate_tool_call
async
¶
Simulate a tool call from the provider.
arguments are read as a real provider reads the wire's
(:func:~roomkit.providers.ai.tool_calls.readable_arguments): text
that reads as an object becomes that mapping, and text that does not
reaches the callbacks as text, an unreadable call. A
:class:~roomkit.providers.ai.tool_calls.CutArguments is read as a
call its response cut, by the rule real providers apply
(:func:~roomkit.providers.ai.tool_calls.realtime_call_arguments).
simulate_tool_call_cancellation
async
¶
Simulate the model abandoning outstanding tool calls (RFC §12.4).
simulate_usage
async
¶
Simulate a usage report from the provider (RFC §12.4.2).
Lands on session.last_usage and fires on_usage, the way a real
provider's own report does. details stands in for the breakdown an
API sends beside its two totals.
simulate_delegation
async
¶
Simulate the model handing reasoning to a backend (RFC §12.4.1).
simulate_response_start
async
¶
Simulate response generation starting.
MockRealtimeTransport ¶
Bases: VoiceBackend
Mock realtime audio transport for testing.
Tracks all method calls and provides helpers to simulate client events (audio, disconnection).
Example
transport = MockRealtimeTransport()
await transport.accept(session, "fake-ws") assert transport.calls[-1].method == "accept"
Simulate client audio¶
await transport.simulate_client_audio(session, b"audio-data")
Usage Example¶
from roomkit import RoomKit, RealtimeVoiceChannel
from roomkit.providers.gemini.realtime import GeminiLiveProvider
from roomkit.voice.realtime.ws_transport import WebSocketRealtimeTransport
# Configure provider and transport
provider = GeminiLiveProvider(api_key="...")
transport = WebSocketRealtimeTransport()
# Create and register the channel
channel = RealtimeVoiceChannel(
"realtime-voice",
provider=provider,
transport=transport,
system_prompt="You are a helpful voice assistant.",
voice="Aoede",
)
kit = RoomKit()
kit.register_channel(channel)
# Create room and attach
room = await kit.create_room(room_id="voice-room")
await kit.attach_channel("voice-room", "realtime-voice")
# When a WebSocket connection arrives:
session = await channel.start_session(
"voice-room", "participant-1", websocket
)
# Transcriptions are emitted as RoomEvents —
# other channels in the room see the conversation.
# Text from other channels enters the AI session as its author's
# quoted words, never as an instruction: a supervisor typing
# "Offer 20% discount" via WebSocket reaches the model as
# supervisor-ws: “Offer 20% discount”
# To direct the model, the application delivers an instruction:
await kit.deliver(
"voice-room", "Offer 20% discount", channel_id="realtime-voice", instruction=True
)
With Tool Calling¶
import json
from roomkit import Tool
async def lookup_order(order_id: str) -> str:
return json.dumps({"status": "shipped", "eta": "Tomorrow"})
order_tool = Tool(
name="lookup_order",
description="Look up an order by ID",
parameters={
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"],
},
handler=lookup_order,
)
channel = RealtimeVoiceChannel(
"realtime-voice",
provider=provider,
transport=transport,
system_prompt="You are a helpful assistant with access to tools.",
tools=[order_tool],
)
With MCP (Advanced)¶
For MCP integration, MCPToolProvider lists the server's tools on entry and routes calls to it through tool_handler:
from roomkit.tools import MCPToolProvider
async with MCPToolProvider.from_url("http://localhost:9998/mcp") as mcp:
channel = RealtimeVoiceChannel(
"realtime-voice",
provider=provider,
transport=transport,
system_prompt="You are a helpful assistant with access to tools.",
tools=mcp.get_tools_as_dicts(),
tool_handler=mcp.as_tool_handler(),
)
... # run the room; leaving the block closes the connection
The provider reads every page of the listing and never lists again on a call; see the MCP Tool Provider guide.
With Delegation¶
Wire the delegate_task tool into a voice agent:
from roomkit.tasks import DelegateHandler, setup_realtime_delegation, build_delegate_tool
handler = DelegateHandler(kit, notify="realtime-voice")
tool = build_delegate_tool([("exec-agent", "Runs background tasks")])
setup_realtime_delegation(channel, handler, tool=tool)
See setup_realtime_delegation() for details.
With Vision Injection¶
Wire screen/camera vision into the voice session:
from roomkit import setup_realtime_vision
setup_realtime_vision(kit, room_id="room-1", voice_channel_id="realtime-voice")
Vision descriptions are injected via inject_text(silent=True) — the agent sees the context on its next turn without interrupting the conversation. Unchanged descriptions are automatically deduplicated.
See setup_realtime_vision() for details.
With FastRTC (WebRTC)¶
from fastapi import FastAPI
from roomkit import RoomKit, RealtimeVoiceChannel
from roomkit.providers.gemini.realtime import GeminiLiveProvider
from roomkit.voice.realtime.fastrtc_transport import (
FastRTCRealtimeTransport,
mount_fastrtc_realtime,
)
app = FastAPI()
provider = GeminiLiveProvider(api_key="...")
transport = FastRTCRealtimeTransport(
input_sample_rate=16000,
output_sample_rate=24000,
)
channel = RealtimeVoiceChannel(
"realtime-voice",
provider=provider,
transport=transport,
system_prompt="You are a helpful voice assistant.",
voice="Aoede",
)
kit = RoomKit()
kit.register_channel(channel)
# Mount WebRTC endpoints
mount_fastrtc_realtime(app, transport, path="/rtc-realtime")
# Auto-create sessions when WebRTC clients connect
async def on_connected(webrtc_id: str):
room = await kit.create_room()
await kit.attach_channel(room.id, "realtime-voice")
await channel.start_session(room.id, "user-1", connection=webrtc_id)
transport.on_client_connected(on_connected)