extractReasoningMiddleware loses text when text blocks overlap

Open Beginner friendly
#21,050 3 comments 0 reactions 0 assignees View on GitHub

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
Domain
api, backend

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

factory-active factory-automatic task-bug-reproduction-success task-identify-harness-labels-done task-identify-issue-type-done
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
  • 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from vercel/ai

All issues in vercel/ai

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.