extractReasoningMiddleware loses text when text blocks overlap
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 84/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- node.js, typescript
Research direction
Start in packages/ai/src/middleware/extract-reasoning-middleware.ts and run the provided repro.mts with the MockLanguageModelV4 stream. Trace how delayedTextStart handles text IDs a and b, then verify that the middleware preserves both text blocks and produces no error chunks, matching the unwrapped output.
Written by the indexing model from the issue text.
Description
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.101zod:4.4.3- Node.js:
22.20.0and24.13.0
Code of Conduct
- I agree to follow this project's Code of Conduct
- Dominant language
- TypeScript
- Stars
- 26.9k
- Forks
- 5.2k
- Avg merge
- 18h 56m
- Merged PRs (30d)
- 526
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from vercel/ai
-
factory-active factory-automatic task-bug-reproduction-success task-identify-harness-labels-done task-identify-issue-type-done
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
-
factory-active factory-automatic task-bug-reproduction-success task-identify-harness-labels-done task-identify-issue-type-done
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
factory-active factory-automatic task-bug-reproduction-success task-identify-harness-labels-done task-identify-issue-type-done
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
factory-active factory-automatic task-bug-reproduction-success task-identify-harness-labels-done task-identify-issue-type-done
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
-
factory-active factory-automatic task-bug-reproduction-success task-identify-harness-labels-done task-identify-issue-type-done
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
Similar issues
-
calcite-components needs triage refactor
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Esri/calcite-design-system#15203 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 91/100
-
community first-timers-only good first issue hacktoberfest help wanted low hanging fruit up-for-grabs
Difficulty 1/5 Under an hour Newbie friendliness 95/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Automattic/studio#4908 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 90/100