From 2941ff5e449749acfbbdaa1f3063faf9c7e98bd3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 9 Aug 2026 03:54:02 -0700 Subject: [PATCH] fix(ai): preserve Responses stream cancellation (#113801) --- .../providers/openai-responses-shared.test.ts | 20 +++++++++++++++++++ .../openai-responses-stream-internal.ts | 6 ++++++ 2 files changed, 26 insertions(+) diff --git a/packages/ai/src/providers/openai-responses-shared.test.ts b/packages/ai/src/providers/openai-responses-shared.test.ts index efc643afee9d..ed5521800db0 100644 --- a/packages/ai/src/providers/openai-responses-shared.test.ts +++ b/packages/ai/src/providers/openai-responses-shared.test.ts @@ -1653,6 +1653,26 @@ describe("processResponsesStream", () => { expect(output.usage.input).toBe(7); }); + it("preserves cancellation when the SDK swallows the abort and ends iteration", async () => { + const abort = new AbortController(); + const output = createAssistantOutput(); + async function* silentlyAbortedStream() { + yield { type: "response.created", response: { id: "resp_aborted" } }; + abort.abort(); + } + + await expect( + processResponsesStream( + silentlyAbortedStream(), + output, + new AssistantMessageEventStream(), + nativeOpenAIModel, + { signal: abort.signal }, + ), + ).rejects.toThrow("Request was aborted"); + expect(output.responseId).toBe("resp_aborted"); + }); + it.each([ ["omits arguments", undefined], ["sends empty arguments", ""], diff --git a/packages/ai/src/transports/openai-responses-stream-internal.ts b/packages/ai/src/transports/openai-responses-stream-internal.ts index ec6afabfe700..9902f653a631 100644 --- a/packages/ai/src/transports/openai-responses-stream-internal.ts +++ b/packages/ai/src/transports/openai-responses-stream-internal.ts @@ -48,6 +48,7 @@ import { type ResponsesThinkingBlock, type TextBlockReference, } from "./openai-responses-stream-terminal-internal.js"; +import { transportAbortError } from "./transport-stream-shared.js"; type ResponsesConsumedEventType = | "error" @@ -713,6 +714,11 @@ export async function processResponsesStream( throw new ResponsesStreamFailure(failure, event.response); } } + // openai-node turns an aborted SSE iterator into normal completion; preserve + // the caller's authoritative reason before classifying terminal stream state. + if (options?.signal?.aborted) { + throw transportAbortError(options.signal); + } if (streamingToolCalls.hasActive()) { throw new Error("Responses stream ended with unresolved tool calls"); }