Repository navigation
fix(ai): Call onFinish when message ui stream is cancelled in toUIMessageStream - #7902
Conversation
onFinish when request is aborted in toUIMessageStreamonFinish when request is aborted in toUIMessageStream
|
Anything blocking here? Also hitting this issue and would love if this was merged. |
|
I am not sure this PR solves the problem and it's a bit difficult to test. The problem here is that ai sdk currently uses a type: "abort" chunk to detect aborts but it looks like this chunk cannot be sent in case of a client abort because the stream can no longer be written to after the abort happens. Fixing this may require a big change, updating how aborts are detected in a lot of places |
There was a problem hiding this comment.
Pull Request Overview
Fixes a bug where onFinish callback wasn't being called when a UI message stream is cancelled or aborted, ensuring partial messages are properly persisted during browser navigation or request cancellation.
- Added
cancelmethod to transform stream to handle aborted requests - Extracted
onFinishlogic into reusablecallOnFinishfunction with duplicate call prevention - Added comprehensive test coverage for stream cancellation scenarios
Reviewed Changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| handle-ui-message-stream-finish.ts | Implements cancel handler and refactors onFinish logic to prevent duplicate calls |
| handle-ui-message-stream-finish.test.ts | Adds test cases for reader cancellation and fetch abort scenarios |
| fix-onfinish-abort.md | Documents the bug fix in changeset for release notes |
|
I found a little documented field on TransformStream that should fix the problem @lgrammel The tests I added fail without the cancel fix |
|
I want to understand why this is a problem to begin with. I changed the abort behavior in streamText to not abort a stream any more; instead abort gets turned into regular stream behavior with an abort chunk to prevent special cases like this one. Can you explain or even better share reproduction code for how such a situation can even occur? |
|
The problem was not strictly related to the abort controller but interrupting the async generator. While writing this comment I also found another issue which was related to consuming the stream with await for and doing an early break. I updated and added a new test file with the reproduction which do not pass without the fix and use the full streamText function to make sure they reproduce the real problem. Basically the smallest repro is this code: const result = streamText({
model,
prompt: 'Generate text',
});
const uiStream = result.toUIMessageStream({
onFinish: onFinishCallback,
generateMessageId: () => 'msg-async-iter',
});
// Consume only first few chunks using async iteration, then break
let chunkCount = 0;
for await (const chunk of uiStream) {
chunkCount++;
if (chunkCount >= 5) {
break; // This was not triggering the stream flush(). Instead it just stops consuming the stream and onfinish is never called
}
}
// onFinish is never calledThis code will cause the generator to stop and the stream to stop being consumed, the problem was in I updated the code to cancel the stream when the async generator breaks. With the added cancel method in the Adding cancel in const res = result.toUIMessageStreamResponse()
res.body?.cancel() |
## Background Async iterators can be exited early, e.g. by breaking out of the for loop. When that happened, the async iterator stream was not correctly closed. ## Summary * implement `return` and `throw` async iterable methods to support loop-breaking behavior * add jsdoc ## Future Work * Investigate/review other parts of #7902 ## Related Issues Build on #7902 --------- Co-authored-by: remorses <[email protected]>
|
I fixed the conflicts with upstream, ready to review |
| message: `incomplete tool input is not supported: ${part.toolCallId}`, | ||
| }); | ||
| } else { | ||
| if (part.state !== 'input-streaming') { |
There was a problem hiding this comment.
how are the changes in this file and chat.ts above related to the issue? can these changes be separated?
There was a problem hiding this comment.
without this change you cannot resume an aborted message if you abort during a tool call. because when the client sends the messages back to the ai route it can contain unfinished tool calls
There was a problem hiding this comment.
i wonder how to best handle/document this. right now from what i understand input-streaming would be dropped automatically, correct?
|
Is there a bug where (this is slightly different for the ui message stream onFinish so there may be more here, just reviewed the first test part) |
|
Are you referring to the test I added in the last commit right? I added this test to make sure I was understanding when StreamText Do you want me to remove that test? |
|
@remorses maybe i got confused by that test. if it is valuable, would it be possible to move it to a separate pr and integrate it into |
|
Sure. I can confirm onFinish is still not called during aborts, I can add test for it |
|
@remorses ideally move as much as possible into separate prs. overload is real so i appreciate minimal prs focussed on 1 problem. |
| import { MockLanguageModelV2 } from '../test/mock-language-model-v2'; | ||
| import { streamText } from './stream-text'; | ||
|
|
||
| describe('streamText onFinish with abort', () => { |
There was a problem hiding this comment.
can you integrate this test into stream-text test and in particular into the ui message stream section (since it is specifically for toUIMessageStream)
There was a problem hiding this comment.
moved to existing file
| convertReadableStreamToArray, | ||
| } from '@ai-sdk/provider-utils/test'; | ||
| import { describe, expect, it, vi } from 'vitest'; | ||
| import { beforeEach, describe, expect, it, vi } from 'vitest'; |
There was a problem hiding this comment.
can remove whole import line
gr2m
left a comment
There was a problem hiding this comment.
Great PR, thank you Tommy 💐
|
@remorses could you look into the merge conflict? Sorry for that, we can look into it ourselves but it might take a bit longer |
Resolved conflicts in packages/ai/src/generate-text/stream-text.test.ts
This reverts commit d793cdc.
|
Fixed the merge conflict |
Fix #7469
Fix #7900
Call
onFinishwhen abort controller aborts, via the TransformStream cancel method: https://developer.mozilla.org/en-US/docs/Web/API/TransformStream#browser_compatibilityThat method is currently only available in Node and Deno