Description
extractReasoningMiddleware loses a text block's start event when a provider opens two text blocks before emitting their deltas. The final text then omits the earlier block, and fullStream contains text part a not found errors.
The same stream works without the middleware. There are no reasoning tags in this reproduction, so adding the middleware should preserve both blocks.
The cause appears to be delayedTextStart in packages/ai/src/middleware/extract-reasoning-middleware.ts: it stores one event for the whole stream, while the rest of the extraction state is keyed by text ID. text-start(b) overwrites the saved text-start(a). When a's delta arrives, the middleware emits b's start followed by a's delta.
Reproduction
No API key or network calls needed. Install ai@7.0.101 and zod@4.4.3, save as repro.mts, and run node repro.mts with Node 24:
import { extractReasoningMiddleware, streamText, wrapLanguageModel } from 'ai';
import { MockLanguageModelV4 } from 'ai/test';
for (const wrapped of [false, true]) {
const model = new MockLanguageModelV4({ doStream: { stream: new ReadableStream({ start(controller) {
controller.enqueue({ type: 'stream-start', warnings: [] });
controller.enqueue({ type: 'text-start', id: 'a' });
controller.enqueue({ type: 'text-start', id: 'b' });
controller.enqueue({ type: 'text-delta', id: 'a', delta: 'Alpha.' });
controller.enqueue({ type: 'text-delta', id: 'b', delta: 'Beta.' });
controller.enqueue({ type: 'text-end', id: 'a' });
controller.enqueue({ type: 'text-end', id: 'b' });
controller.enqueue({ type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage: {
inputTokens: { total: 1, noCache: 1, cacheRead: 0, cacheWrite: 0 },
outputTokens: { total: 1, text: 1, reasoning: 0 },
} });
controller.close();
} }) } });
const errors: unknown[] = [];
const result = streamText({
model: wrapped ? wrapLanguageModel({ model, middleware: extractReasoningMiddleware({ tagName: 'think' }) }) : model,
prompt: 'Synthetic prompt.',
maxRetries: 0,
});
for await (const event of result.fullStream) {
if (event.type === 'error') errors.push(event.error);
}
console.log({ wrapped, text: await result.text, errors });
}
Actual output:
{ wrapped: false, text: 'Alpha.Beta.', errors: [] }
{
wrapped: true,
text: 'Beta.',
errors: [ 'text part a not found', 'text part a not found' ]
}
Expected: both runs return Alpha.Beta. with no error chunks.
I reproduced this on Node 22.20.0 and 24.13.0. Sequential text blocks pass with or without the middleware, and overlapping blocks pass without it. The reproduction passes strict TypeScript checking. The middleware source is unchanged on main at 12845693d7a6a517dc633d6b6a2e4f5bfd24d2ec (source comparison; runtime tests used the published package).
I checked #7774 / #8036, which introduced the delayed start, and #7305. This case concerns distinct text IDs losing their start event, rather than when reasoning is displayed. PR #15583 concerns unfinished tag buffering.
I used AI assistance to investigate and prepare the reproduction and tests.
AI SDK Version
ai: 7.0.101
zod: 4.4.3
- Node.js:
22.20.0 and 24.13.0
Code of Conduct
Description
extractReasoningMiddlewareloses a text block's start event when a provider opens two text blocks before emitting their deltas. The final text then omits the earlier block, andfullStreamcontainstext part a not founderrors.The same stream works without the middleware. There are no reasoning tags in this reproduction, so adding the middleware should preserve both blocks.
The cause appears to be
delayedTextStartinpackages/ai/src/middleware/extract-reasoning-middleware.ts: it stores one event for the whole stream, while the rest of the extraction state is keyed by text ID.text-start(b)overwrites the savedtext-start(a). When a's delta arrives, the middleware emits b's start followed by a's delta.Reproduction
No API key or network calls needed. Install
ai@7.0.101andzod@4.4.3, save asrepro.mts, and runnode repro.mtswith Node 24:Actual output:
Expected: both runs return
Alpha.Beta.with no error chunks.I reproduced this on Node 22.20.0 and 24.13.0. Sequential text blocks pass with or without the middleware, and overlapping blocks pass without it. The reproduction passes strict TypeScript checking. The middleware source is unchanged on
mainat12845693d7a6a517dc633d6b6a2e4f5bfd24d2ec(source comparison; runtime tests used the published package).I checked #7774 / #8036, which introduced the delayed start, and #7305. This case concerns distinct text IDs losing their start event, rather than when reasoning is displayed. PR #15583 concerns unfinished tag buffering.
I used AI assistance to investigate and prepare the reproduction and tests.
AI SDK Version
ai:7.0.101zod:4.4.322.20.0and24.13.0Code of Conduct