Skip to content

feat(providers): ElevenLabs scribe_v2_realtime streaming transcription (RFC-0034 P2) - #184

Draft
eric8810 wants to merge 6 commits into
masterfrom
rfc-0034/elevenlabs-realtime
Draft

feat(providers): ElevenLabs scribe_v2_realtime streaming transcription (RFC-0034 P2)#184
eric8810 wants to merge 6 commits into
masterfrom
rfc-0034/elevenlabs-realtime

Conversation

@eric8810

Copy link
Copy Markdown
Contributor

Implements P2 of RFC-0034: ElevenLabs scribe_v2_realtime streaming transcription (do_stream on ElevenLabsTranscriptionModel, behind the existing realtime feature).

Stacked on #183 (P1, WS proxy) — until that merges, this diff includes P1's commits; it collapses to the P2 delta afterwards. P3 (Cartesia) is deferred with explicit restart triggers (RFC-0034 §4/D6); the gate comment in cartesia.rs records it.

What lands

  • Gating symmetric to OpenAI: scribe_v2_realtime*do_stream; everything else → do_generate. do_generate now rejects realtime IDs with a clear message instead of posting them to the batch REST endpoint.
  • Wire shape per the public API reference (2026-09), pinned field-by-field by 8 mock-server tests: config on the URL query (commit_strategy=manual fixed, D3; languageCode/includeTimestamps the only provider options, D5), xi-api-key handshake header, base64 input_audio_chunk frames with sample_rate, commit:true riding the last real chunk (hold-one-chunk pipeline; empty stream commits an empty chunk), partial_transcriptTranscriptPartial (replace semantics), committed_transcript[_with_timestamps]TranscriptFinal + Finish (word segments, spacing dropped), client close(1000) after the final.
  • Termination guarantee: the server never closes after a commit, so a settle window (chunk_ms, 5 s default) bounds the wait with an empty Finish — mirroring Align the request pipeline with the AI SDK #164's terminal-fuse principle.
  • Error taxonomy: the 14 documented event names classified — rate_limited/queue_overflow/resource_exhausted retryable, the rest terminal; raw event preserved in response_body.
  • Non-pcm/ulaw input formats fail fast without connecting.

Verification

  • 8 tests in aimux-providers/tests/elevenlabs_realtime_stream_test.rs (happy path with query/header/chunk-frame assertions, timestamps variant, empty-audio commit, 4-way error classification, silent-server settle, mid-session abort, symmetric gating, format rejection). Workspace clippy clean.
  • Live-API smoke pending (no key at implementation time) — same posture as RFC-0028 D4; flagged in the do_stream doc comment. Smoke through a proxy also closes P1's untested wss+rustls branch.

Closes the P2 scope of #178.

…bs/Cartesia do_stream)

P1 WS proxy tunnel honoring the global ProxyConfig (CONNECT + no_proxy,
SOCKS errors loudly); P2 ElevenLabs scribe_v2_realtime do_stream; P3
Cartesia ink-2 do_stream. No unified session abstraction (D1), minimal
parameter surface (D5). Closes #178 (research record attached there).
…proxy (RFC-0034 P1)

ws_connect now consults the global ProxyConfig that already governs
HTTP: wss uses https_url (all_url fallback), ws uses http_url (all_url
fallback), no_proxy entries are honored with reqwest NoProxy semantics
(comma-separated, suffix match, '*', port-specific entries), and matched
targets keep the direct path unchanged.

Tunneled connects: manual TCP to the proxy -> CONNECT (with Basic
Proxy-Authorization from proxy-URL userinfo) -> 2xx validation -> WS
handshake over the tunnel via client_async_tls_with_config (explicit
ring provider + webpki-roots for wss targets, Plain for ws). SOCKS and
https-scheme proxies fail loudly as UnsupportedFunctionality instead of
silently bypassing the configured proxy; proxy CONNECT rejections
surface as non-retryable ApiCall errors carrying the proxy status.

The tunnel steps are bounded by first_chunk_ms and raced against abort
by the existing ws_connect select. rustls/webpki-roots/base64 join the
optional 'ws' feature only.
Independent review (PR #183) surfaced three defects and a test gap:

- IPv6 proxy hosts never connected: url::Url::host_str returns bracketed
  literals and the socket resolver wants the bare address — strip them.
- Proxy credentials leaked into error strings: all resolve_proxy error
  paths now mask userinfo (scheme://***@host), matching the existing
  request-URL sanitization.
- CONNECT rejections were blanket non-retryable: classify by the shared
  is_retryable_status rule (407/403 stay terminal, 502/503/504 retry);
  EOF-before-response (status 0) is transient.
- Missing promised tests added: abort during the CONNECT tunnel (against
  a black-hole proxy), unresponsive proxy tripping first_chunk_ms, 503
  retryability, CONNECT wire shape (request line, Host, no stray
  Proxy-Authorization), IPv6 bracket stripping, credential masking.
- no_proxy docstring now states the CIDR divergence; init_proxy documents
  that WS reads the config live.
- RFC-0034: OQ2 verified and closed — stream_transcribe has no attempt
  retry and should not (audio input is consumed once; replayable sources
  would be new design). Status line and test list updated to match what
  landed.
…l cost

Re-review after P1: the decisive difference vs P2 (ElevenLabs) is
verifiability. ElevenLabs ships a public API reference, so mock tests
pin documented facts; Cartesia's turns API is behind a login wall, its
event schema is only inferable from the Python SDK, and the API is new
enough to churn — on top of the D4 lesson that mock-only wire shapes
can be wrong. Deferring loses nothing: ink-2 already fails honestly
with UnsupportedFunctionality on both paths, and nothing depends on it.

Restart triggers (any one): a user asks; the docs come out from behind
the login wall; a key + smoke decision. Gate comment in cartesia.rs
records the deferral. D6 added.
…n (RFC-0034 P2)

ElevenLabsTranscriptionModel gains do_stream behind the existing
'realtime' feature, gated on scribe_v2_realtime* (inverse gating in
do_generate rejects realtime IDs with a clear message instead of
posting them to the batch endpoint).

Wire shape per the public API reference (2026-09), pinned
field-by-field by local mock-server tests:
- config travels on the URL query (model_id, audio_format
  pcm_{rate}/ulaw_{rate}, commit_strategy=manual, optional
  language_code/include_timestamps from providerOptions.elevenlabs);
  xi-api-key rides the WS handshake headers
- audio: base64 in input_audio_chunk JSON with sample_rate; a
  hold-one-chunk pipeline puts commit:true on the LAST real chunk (an
  empty stream commits a single empty chunk); audio waits for
  session_started
- partial_transcript -> TranscriptPartial (replace semantics),
  committed_transcript[_with_timestamps] -> TranscriptFinal + Finish
  (segments from words, spacing entries dropped), then client
  close(1000)
- 14 documented error event names classified: rate_limited /
  queue_overflow / resource_exhausted retry, the rest terminal
- the server never closes after commit, so a settle window (chunk_ms,
  5s default) bounds the wait with an empty Finish — the stream always
  terminates
- non-pcm/ulaw input formats fail fast without connecting

Live-API smoke pending (no key at implementation time) — same posture
as RFC-0028 D4; the do_stream doc comment says so.

Also: cartesia.rs gate comment now records the P3 deferral (RFC-0034
D6); RFC status/plan rows updated.
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