Repository navigation
fix: dropping an xAI streaming transcription closes the socket - #311
Merged
eric8810 merged 7 commits intoOct 25, 2026
Merged
Conversation
@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
In @ai-sdk/xai 5.0.12, cancelling the transcription stream runs cleanup, which calls connection.close() (xai-transcription-model.ts:290-298, :459-462). aimux only sent a close frame after transcript.done or an error event; a stream dropped earlier released the transport without one. The socket now lives in a guard that, when dropped before it was closed, sends the close frame on the runtime. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
8 tasks
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
…/xai-stt-closes-the-socket-when-the-stream-is-dropped # Conflicts: # aimux-providers/src/xai/transcription.rs
This was referenced Oct 20, 2026
cunninghamcard-bit
changed the base branch from
fix/xai-stt-connect-failure-errors-the-stream
to
master
October 20, 2026 11:06
This was referenced Oct 20, 2026
…opped Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
cunninghamcard-bit
marked this pull request as ready for review
October 24, 2026 10:12
eric8810
reviewed
Oct 25, 2026
eric8810
left a comment
Contributor
There was a problem hiding this comment.
drop 后关闭 socket 的主要路径成立,但 Socket(Some(ws)) 在 async_stream::stream! 的第一条语句里,宏体首次 poll 才执行;已连接但从未 poll 的流 drop 时仍不会发 close frame。建议在宏外包装 connected,并加一个“drop 前不调用 next()”的测试。CHANGELOG 里“在 stream 内打开”也不准确:ws_connect 仍在 do_stream 里 await。
…the-socket-when-the-stream-is-dropped # Conflicts: # CHANGELOG.md
…en dropped The socket guard was built in the first statement of the async_stream::stream! body, which runs only on the first poll. A stream that do_stream returned with an open socket but that was dropped before next() released the transport without a close frame. The guard is now built in do_stream, right after the connect, and moved into the stream. The CHANGELOG said the socket is opened inside the stream; do_stream still awaits ws_connect. The entry now says what changed: a connect failure arrives as the stream's error. Test: dropping_an_unpolled_stream_closes_the_socket (tests/xai_transcription_test.rs) fails without the change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
Contributor
Author
PR 描述的调用栈和测试段已按新提交更新。分支此前已合入最新 master(只有 CHANGELOG 冲突)。本地检查:fmt、边界脚本、clippy、lib + xAI 测试通过。 Generated by Claude Code Generated by Claude Code |
eric8810
approved these changes
Oct 25, 2026
eric8810
left a comment
Contributor
There was a problem hiding this comment.
drop-before-poll 已修复,Socket 在 stream 外构造,未 poll 的 drop 也会发 close;测试覆盖未 poll/polled 两种路径。Approve。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the 2 behaviour differences in xAI found while porting the upstream tests. Each difference is its own commit (listed below) and adds a test that fails on master and a CHANGELOG
Fixedentry. They were separate PRs (#310 and this one). To keep one fix PR per vendor, the others are closed and this PR carries all of them. The xAI test PR (#312) stacks on this branch. Part of the #276 list.1852ed5d,e6084f08c2c70271,470453791. xAI streaming transcription connect failure errors the stream
Before: xAI streaming transcription opened its WebSocket in
do_streamand returned a connect failure (a refused connection, a failing custom connector) as theErrofdo_streamitself.After: as in
@ai-sdk/xai5.0.12,do_streamstill opens the socket before it returns (upstream'sReadableStreamstartruns while doStream builds the stream), but a connect failure no longer failsdo_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).How it works today
What this PR changes
The socket is still opened before
do_streamreturns, so a stream dropped unread has connected, as upstream's has (its caseshould strip undefined header values before the WebSocket constructorreads the handshake after cancelling the unread stream).Tests (
tests/xai_transcription_test.rs):a_connect_failure_errors_the_streampoints the model at a closed port and expectsdo_streamto resolve and the stream to hold one error; it fails on master withdo_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.Note: 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.2. dropping an xAI streaming transcription closes the socket
Before: xAI streaming transcription sent the WebSocket close frame only after
transcript.doneor anerrorevent. A stream dropped earlier (the caller cancels, or an audio send fails) released the transport without a close frame.After: as in
@ai-sdk/xai5.0.12, where cancelling the stream runscleanupand callsconnection.close(), dropping the stream before it closed the socket sends the close frame. The audio stream is dropped with it, as before.Found while porting the upstream xAI tests (case
doStream › should close the WebSocket and stop reading audio when the stream is cancelled).How it works today
What this PR changes
The guard is built outside the
async_stream::stream!body because that body runs only on the first poll: a guard built inside it (the first version of this commit) left a stream that was connected but never polled to drop the bareWsConnectionwithout a close frame.47045379moves it out, as eric pointed out in review.Tests (
tests/xai_transcription_test.rs):dropping_the_stream_closes_the_socketruns a local WebSocket server, reads the stream start, drops the stream and expects the server to see a close frame; it fails on master withno close frame was sent when the stream was dropped.dropping_an_unpolled_stream_closes_the_socketdrops the stream without callingnext(); it fails with the guard built inside the stream body (no close frame was sent when the unpolled stream was dropped). The upstream case is ported in the xAI test PR.Docs: CHANGELOG
Fixed.Note: The close runs on a spawned task because
Dropcannot await; with no tokio runtime (dropped outside one) the transport is released as before. Only xAI changes here.Type of change
Checklist
cargo +1.99 fmt -p aimux-providers -- --checkpassescargo +1.99 clippy -p aimux-providers --lib --tests -- -D warningspassescargo +1.99 test -p aimux-providers --test xai_transcription_testpasses (21)scripts/check_provider_boundaries.shpassesNotes for reviewers
Generated by Claude Code