Skip to content

Commit 1003609

Browse files
Backport: fix(ai): skip stringifying text when streaming partial text (#14200)
This is an automated backport of #14123 to the release-v6.0 branch. FYI @aayush-kapoor Co-authored-by: Aayush Kapoor <83492835+aayush-kapoor@users.noreply.github.com>
1 parent 9de7d7b commit 1003609

File tree

3 files changed

+58
-5
lines changed

3 files changed

+58
-5
lines changed

.changeset/empty-cups-love.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ai": patch
3+
---
4+
5+
fix(ai): skip stringifying text when streaming partial text

packages/ai/src/generate-text/stream-text.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15190,6 +15190,50 @@ describe('streamText', () => {
1519015190
`);
1519115191
});
1519215192

15193+
it('should not call JSON.stringify for string partial outputs', async () => {
15194+
const originalStringify = JSON.stringify;
15195+
const stringifySpy = vi.spyOn(JSON, 'stringify').mockImplementation(((
15196+
...args: Parameters<typeof JSON.stringify>
15197+
) => {
15198+
if (typeof args[0] === 'string') {
15199+
throw new Error(
15200+
'JSON.stringify should not be called for string partial outputs',
15201+
);
15202+
}
15203+
return originalStringify.call(JSON, ...args);
15204+
}) as typeof JSON.stringify);
15205+
15206+
try {
15207+
const result = streamText({
15208+
model: createTestModel({
15209+
stream: convertArrayToReadableStream([
15210+
{ type: 'text-start', id: '1' },
15211+
{ type: 'text-delta', id: '1', delta: 'Hello, ' },
15212+
{ type: 'text-delta', id: '1', delta: 'world!' },
15213+
{ type: 'text-end', id: '1' },
15214+
{
15215+
type: 'finish',
15216+
finishReason: { unified: 'stop', raw: 'stop' },
15217+
usage: testUsage,
15218+
},
15219+
]),
15220+
}),
15221+
prompt: 'prompt',
15222+
output: Output.text(),
15223+
});
15224+
15225+
expect(await convertAsyncIterableToArray(result.partialOutputStream))
15226+
.toMatchInlineSnapshot(`
15227+
[
15228+
"Hello, ",
15229+
"Hello, world!",
15230+
]
15231+
`);
15232+
} finally {
15233+
stringifySpy.mockRestore();
15234+
}
15235+
});
15236+
1519315237
it('should resolve output promise with the correct content', async () => {
1519415238
const result = streamText({
1519515239
model: createTestModel({

packages/ai/src/generate-text/stream-text.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -600,7 +600,7 @@ function createOutputTransformStream<
600600
let text = '';
601601
let textChunk = '';
602602
let textProviderMetadata: ProviderMetadata | undefined = undefined;
603-
let lastPublishedJson = '';
603+
let lastPublishedValue = '';
604604

605605
function publishTextChunk({
606606
controller,
@@ -673,11 +673,15 @@ function createOutputTransformStream<
673673

674674
// null should be allowed (valid JSON value) but undefined should not:
675675
if (result !== undefined) {
676-
// only send new json if it has changed:
677-
const currentJson = JSON.stringify(result.partial);
678-
if (currentJson !== lastPublishedJson) {
676+
// only send new value if it has changed:
677+
// For string partials (text output), compare directly to avoid unnecessary JSON.stringify overhead
678+
const currentValue =
679+
typeof result.partial === 'string'
680+
? result.partial
681+
: JSON.stringify(result.partial);
682+
if (currentValue !== lastPublishedValue) {
679683
publishTextChunk({ controller, partialOutput: result.partial });
680-
lastPublishedJson = currentJson;
684+
lastPublishedValue = currentValue;
681685
}
682686
}
683687
},

0 commit comments

Comments
 (0)