Builds session.update with turn_detection: null on handshake (S4,
encoded in the translator's build_openai_session_update; Task 4).
Runs a select! loop over tap-side input (audio_in → append,
function_call_output → conversation.item.create) and OpenAI-side
input (response.audio.delta → tap audio_out, speech_started/stopped
→ tap speech_started/stopped, function_call_arguments.done → tap
function_call). 401 surfaces as OpenAiClientError::AuthFailed.
URL + headers shape tested directly. Full pump loop tested via
MockRealtimeBrain in Task 10 — neither side of this pump is a real
network endpoint in unit tests.
Pure functions, no I/O, no call state. The OpenAI-side WS client
(Task 5) and the tap-side WS server (Task 9) call these per event.
S4 turn-ownership decision (spec §4.3, load-bearing per ADR-0008)
encoded in build_openai_session_update: turn_detection: null. OpenAI's
server-side VAD is disabled; the FOB reflex loop (step 4) owns
turn-taking; tap playout stays core-authoritative (slice-2 §4.1).
S4 test verifies the load-bearing assertion
(v['session']['turn_detection'] == Null).
Audio is pass-through — OpenAI's PCM base64 (LE i16 24 kHz mono) is
indentical to slice-2's tap PCM wire shape (spec §3.5); decode +
re-encode as a PcmFrame so the playout ring stays byte-aligned for
slice-2's playout-buffer invariants (samples: 480).
Workspace member 8 of 8. Library + binary; mirrored slice-2's
rutster-tap-echo shape. Three stub modules (api_key, translator,
openai_client) filled in by Tasks 3-5. No behavioral code yet —
this task only locks the crate boundary + the new async-trait dep
(the Tool trait in Task 6 needs it for async-fns-in-trait-objects).