Skip to content

feat(voice): Add OpenAI Realtime SIP channel (POC) - #102

Draft
xinghaohuang91 wants to merge 2 commits into
mainfrom
feat/openai-realtime-sip-channel
Draft

feat(voice): Add OpenAI Realtime SIP channel (POC)#102
xinghaohuang91 wants to merge 2 commits into
mainfrom
feat/openai-realtime-sip-channel

Conversation

@xinghaohuang91

Copy link
Copy Markdown
Contributor

Summary

POC for speech-to-speech (S2S) voice support in TAC: OpenAIRealtimeSipChannel
bridges Twilio phone calls to OpenAI's Realtime API over SIP trunking, as an
alternative to the existing ConversationRelay-based VoiceChannel.

Twilio's Elastic SIP Trunking forwards accepted calls directly to OpenAI at
the SIP/SRTP level — audio never passes through TAC. The channel only
handles:

  • the realtime.call.incoming webhook (decide whether/how to accept a call,
    via a new on_call_incoming callback)
  • optionally, a sideband control WebSocket (JSON events only, no audio) for
    transcript capture and tool calling, opened only when a tool is registered
    or transcription is requested

New pieces:

  • src/tac/channels/openai_realtime_sip/ — channel, client (REST
    accept/reject/hangup/refer + control WebSocket), config, models
  • src/tac/server/openai_realtime_sip_server.py — minimal standalone
    FastAPI server exposing just the one webhook route (deliberately separate
    from TACFastAPIServer, since this channel shares no routes/signature
    scheme with existing voice/messaging channels)
  • getting_started/examples/features/openai_realtime_sip_voice.py — runnable
    example with a registered tool and transcript printing
  • S2S_POC.md — architecture + setup + trade-offs write-up (first of a
    planned 3-section doc; this PR covers section 1 only)
  • new optional dependency group tac[openai-realtime-sip]

How to test

  1. uv sync --extra server --extra openai-realtime-sip
  2. Follow the setup steps in S2S_POC.md / the example's docstring (OpenAI
    webhook + project ID, Twilio Elastic SIP Trunk with an Origination URI
    pointing at sip:$PROJECT_ID@sip.api.openai.com;transport=tls)
  3. uv run python getting_started/examples/features/openai_realtime_sip_voice.py
    behind a public tunnel (e.g. ngrok), then call the Twilio number

Verified end-to-end against a real Twilio SIP Trunk + OpenAI project: calls
connect and complete, transcript capture (both caller and model speech) and
tool calling both confirmed working.

Type of Change

  • New feature
  • Bug fix
  • Breaking change
  • Documentation update
  • Refactoring
  • Release / version bump

Checklist

  • Tests added/updated — POC status: only manual end-to-end testing and
    unit-level smoke tests of the event-dispatch logic (_handle_control_event,
    _handle_function_call) so far; no tests/test_openai_realtime_*.py
    yet. Flagging for follow-up before this leaves POC status.
  • Documentation updated (S2S_POC.md, example docstring)
  • Tested E2E (real Twilio SIP Trunk + OpenAI project, see above)

SDK Parity

This is the Python SDK.

  • Change is Python-specific (no TypeScript update needed) — this is a POC
    exploring a new architecture, not yet a stable API to port.

xinghaohuang91 and others added 2 commits August 12, 2026 14:13
Adds OpenAIRealtimeSipChannel, bridging Twilio calls to OpenAI's Realtime
API over SIP trunking. Twilio forwards accepted calls directly to OpenAI
at the SIP/SRTP level, so audio never passes through TAC — the channel
only handles the realtime.call.incoming webhook (accept/reject) and,
optionally, a sideband control WebSocket for transcript capture and
tool calling.

Verified against a real Twilio SIP Trunk + OpenAI project: calls connect
and complete successfully, transcript capture and tool calling both work
end-to-end. This is a POC — no automated test suite yet, only manual and
unit-level smoke testing of the event-dispatch logic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds OpenAIRealtimeMediaStreamsChannel as a second S2S option alongside
OpenAIRealtimeSipChannel: Twilio streams call audio to our own WebSocket via
<Connect><Stream>, and this channel bridges it to/from OpenAI's Realtime
WebSocket directly, so it owns the audio path for the life of the call
(unlike the SIP channel, which never touches audio).

- OpenAIRealtimeMediaStreamsChannel: audio bridge with precise barge-in
  (truncates the model's reply at the exact ms played, clears Twilio's
  buffer), inline transcript capture and tool calling (no sideband
  connection needed — already holding one audio connection open)
- Call state lives on ConversationSession.metadata; a small
  RealtimeWebSocketManager tracks just the Twilio/OpenAI socket pair
- Session config (model/voice/instructions/tools) is supplied by reusing
  TAC.on_message_ready (called with an empty user_message and no
  memory_response, returning a JSON-encoded session dict) rather than a
  parallel callback — see S2S_POC.md for the reasoning
- TACTool.to_realtime_format() promoted to tools/base.py, shared by both
  Realtime channels instead of duplicated
- OpenAIRealtimeMediaStreamsServer: minimal standalone FastAPI host (TwiML +
  Media Stream WebSocket)
- S2S_POC.md Section 2: architecture, setup, trade-offs vs. Section 1

Verified end-to-end against a real Twilio number + OpenAI project: calls
connect, barge-in truncates correctly mid-reply, tool calls execute and the
model continues with the result, and transcript capture matches what was
said on both sides.

Known limitation: on_message_ready is shared with text channels on the same
TAC instance — an app combining this channel with e.g. an SMS channel needs
its own branching logic (on ConversationSession.channel) in that one
callback, since there's no built-in way to distinguish "build a session
config" from "build a reply" calls.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant