Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions packages/ai/src/generate-text/stream-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1028,7 +1028,32 @@ class DefaultStreamTextResult<
return;
}

activeText.text += part.text;
// PATCH: O(N²)→O(N) text accumulation. Init _chunks lazily; replace .text
// with a lazy getter that materializes from the chunks array on read.
// _chunks is non-enumerable so it doesn't leak through
// JSON.stringify / structuredClone / spread to consumers.
if (!(activeText as any)._chunks) {
Object.defineProperty(activeText, '_chunks', {
value: [activeText.text || ''],
writable: true,
enumerable: false,
configurable: true,
});
Object.defineProperty(activeText, 'text', {
get(): string {
const c = (this as any)._chunks;
return c.length === 1
? c[0]
: ((this as any)._chunks = [c.join('')])[0];
},
set(v: string) {
(this as any)._chunks = [v];
},
enumerable: true,
configurable: true,
});
}
(activeText as any)._chunks.push(part.text);
activeText.providerMetadata =
part.providerMetadata ?? activeText.providerMetadata;
}
Expand Down Expand Up @@ -1077,7 +1102,29 @@ class DefaultStreamTextResult<
return;
}

activeReasoning.text += part.text;
// PATCH: O(N²)→O(N) reasoning accumulation. Same pattern as activeText above.
if (!(activeReasoning as any)._chunks) {
Object.defineProperty(activeReasoning, '_chunks', {
value: [activeReasoning.text || ''],
writable: true,
enumerable: false,
configurable: true,
});
Object.defineProperty(activeReasoning, 'text', {
get(): string {
const c = (this as any)._chunks;
return c.length === 1
? c[0]
: ((this as any)._chunks = [c.join('')])[0];
},
set(v: string) {
(this as any)._chunks = [v];
},
enumerable: true,
configurable: true,
});
}
(activeReasoning as any)._chunks.push(part.text);
activeReasoning.providerMetadata =
part.providerMetadata ?? activeReasoning.providerMetadata;
}
Expand Down
51 changes: 49 additions & 2 deletions packages/ai/src/ui/process-ui-message-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,29 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
providerMetadata: chunk.providerMetadata,
state: 'streaming',
};
// PATCH: O(N²)→O(N) text accumulation via _chunks + lazy getter.
// See https://github.com/vercel/ai/issues/15670 for context.
// _chunks is non-enumerable so it doesn't leak through
// JSON.stringify / structuredClone / spread to consumers.
Object.defineProperty(textPart, '_chunks', {
value: [textPart.text || ''],
writable: true,
enumerable: false,
configurable: true,
});
Object.defineProperty(textPart, 'text', {
get(): string {
const c = (this as any)._chunks;
return c.length === 1
? c[0]
: ((this as any)._chunks = [c.join('')])[0];
},
set(v: string) {
(this as any)._chunks = [v];
},
enumerable: true,
configurable: true,
});
state.activeTextParts[chunk.id] = textPart;
state.message.parts.push(textPart);
write();
Expand All @@ -407,7 +430,8 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
`Ensure a "text-start" chunk is sent before any "text-delta" chunks.`,
});
}
textPart.text += chunk.delta;
// PATCH: see text-start above — chunks instead of += to avoid O(N²).
(textPart as any)._chunks.push(chunk.delta);
textPart.providerMetadata =
chunk.providerMetadata ?? textPart.providerMetadata;
write();
Expand Down Expand Up @@ -451,6 +475,28 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
providerMetadata: chunk.providerMetadata,
state: 'streaming',
};
// PATCH: O(N²)→O(N) reasoning accumulation via _chunks + lazy getter.
// _chunks is non-enumerable so it doesn't leak through
// JSON.stringify / structuredClone / spread to consumers.
Object.defineProperty(reasoningPart, '_chunks', {
value: [reasoningPart.text || ''],
writable: true,
enumerable: false,
configurable: true,
});
Object.defineProperty(reasoningPart, 'text', {
get(): string {
const c = (this as any)._chunks;
return c.length === 1
? c[0]
: ((this as any)._chunks = [c.join('')])[0];
},
set(v: string) {
(this as any)._chunks = [v];
},
enumerable: true,
configurable: true,
});
state.activeReasoningParts[chunk.id] = reasoningPart;
state.message.parts.push(reasoningPart);
write();
Expand All @@ -468,7 +514,8 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
`Ensure a "reasoning-start" chunk is sent before any "reasoning-delta" chunks.`,
});
}
reasoningPart.text += chunk.delta;
// PATCH: see reasoning-start above — chunks instead of += to avoid O(N²).
(reasoningPart as any)._chunks.push(chunk.delta);
reasoningPart.providerMetadata =
chunk.providerMetadata ?? reasoningPart.providerMetadata;
write();
Expand Down