Skip to content

fix: xAI streaming transcription connect failure errors the stream - #310

Closed
cunninghamcard-bit wants to merge 2 commits into
masterfrom
fix/xai-stt-connect-failure-errors-the-stream
Closed

cunninghamcard-bit wants to merge 2 commits into
masterfrom
fix/xai-stt-connect-failure-errors-the-stream

Conversation

@cunninghamcard-bit

@cunninghamcard-bit cunninghamcard-bit commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Before: xAI streaming transcription opened its WebSocket in do_stream and returned a connect failure (a refused connection, a failing custom connector) as the Err of do_stream itself.

After: as in @ai-sdk/xai 5.0.12, do_stream still opens the socket before it returns (upstream's ReadableStream start runs while doStream builds the stream), but a connect failure no longer fails do_stream: the returned stream yields it as its only item and ends, which drops the caller's audio stream (upstream cancels it).

Found while porting the upstream xAI tests (case doStream › should cancel the audio stream when the WebSocket constructor throws). Part of the #276 list.

How it works today

XaiTranscriptionModel::do_stream                      aimux-providers/src/xai/transcription.rs:210   // upstream: XaiTranscriptionModel.doStream  xai-transcription-model.ts
└─ stream::do_stream                                  transcription.rs:295
   ├─ Opts::parse, multichannel check, warnings       :296-334
   ├─ ws_connect(&WebSocketRequest::for_transcription(..)).await?
   │                                                  :338   ← connect failure returns Err from do_stream
   │                                                         // upstream: connectToWebSocket runs in the ReadableStream's start (:276-358);
   │                                                         //   a failure goes to finishWithError → cleanup cancels audio → controller.error
   ├─ async_stream::stream! { audio / ws.next() loop } :355
   └─ Ok(TranscriptionStreamResult { stream, .. })    :489
aimux-provider-utils/src/ws.rs:477  ws_connect → connector.connect or tungstenite

What this PR changes

stream::do_stream                                     transcription.rs:295
├─ let connected = ws_connect(..).await               :340   no `?`: the Result is handed to the stream
└─ async_stream::stream! {                            :357
     let mut ws = match connected {                   :358
         Ok(ws) => ws,
         Err(e) => { yield Err(e); return; }          ← the stream's error; ending the stream drops `audio`
     };
     … the existing loop, unchanged
   }

The socket is still opened before do_stream returns, so a stream dropped unread has connected, as upstream's has (its case should strip undefined header values before the WebSocket constructor reads the handshake after cancelling the unread stream).

Tests (tests/xai_transcription_test.rs): a_connect_failure_errors_the_stream points the model at a closed port and expects do_stream to resolve and the stream to hold one error; it fails on master with do_stream resolves; the connect failure arrives on the stream: ApiCall(... "websocket connect failed: ... Connection refused"). The upstream case is ported in the xAI test PR.

Docs: CHANGELOG Fixed.

Type of change

  • Bug fix (non-breaking)
  • New feature (non-breaking)
  • Breaking change
  • New provider
  • New / updated binding
  • Docs / RFC

Checklist

  • cargo +1.99 fmt -p aimux-providers -- --check passes
  • cargo +1.99 clippy -p aimux-providers --lib --tests -- -D warnings passes
  • cargo +1.99 test -p aimux-providers --test xai_transcription_test passes (19)
  • scripts/check_provider_boundaries.sh passes
  • Updated relevant docs (CHANGELOG)
  • Commit messages follow Conventional Commits

Notes for reviewers

Only xAI changes here. ElevenLabs streaming transcription also returns a connect failure from do_stream (elevenlabs.rs:941); it is not touched, since its upstream package is ported separately.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5

@ai-sdk/xai 5.0.12 opens the STT WebSocket inside the returned stream
(xai-transcription-model.ts:276-358), so doStream resolves and a connect
failure errors the stream, cancelling the caller's audio. aimux connected
before returning and failed do_stream itself. The connect now runs first
inside the stream; on failure the stream yields the error and ends, which
drops the audio.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
Upstream's ReadableStream start runs when doStream builds the stream, so
the WebSocket is opened before doStream resolves even if the stream is
never read. Connecting on the stream's first poll would skip the
connection for a stream dropped unread; connect in do_stream instead and
hand the outcome to the stream, which still yields a failure as its
error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5

Copy link
Copy Markdown
Contributor Author

Folded into #311 together with the other xAI fixes, so each vendor has one fix PR. This change is commits 1852ed5d and e6084f08 there. Closing this one; the branch stays as it is.


Generated by Claude Code

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