Skip to content

Commit 75c3396

Browse files
authored
fix (ai): handle errors in 2nd streamText doStream call (#7309)
## Background Errors in the 2nd doStream call where not reported in the streamText onError callback. ## Summary Catch and handle the errors.
1 parent 9d69c52 commit 75c3396

File tree

3 files changed

+95
-5
lines changed

3 files changed

+95
-5
lines changed

‎.changeset/hungry-nails-return.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): handle errors in 2nd streamText doStream call

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

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,7 +1417,9 @@ describe('streamText', () => {
14171417
]
14181418
`);
14191419
});
1420+
});
14201421

1422+
describe('errors', () => {
14211423
it('should forward error in doStream as error stream part', async () => {
14221424
const result = streamText({
14231425
model: new MockLanguageModelV2({
@@ -1441,6 +1443,80 @@ describe('streamText', () => {
14411443
},
14421444
]);
14431445
});
1446+
1447+
it('should invoke onError callback when error is thrown', async () => {
1448+
const onError = vi.fn();
1449+
1450+
const result = streamText({
1451+
model: new MockLanguageModelV2({
1452+
doStream: async () => {
1453+
throw new Error('test error');
1454+
},
1455+
}),
1456+
prompt: 'test-input',
1457+
onError,
1458+
});
1459+
1460+
await result.consumeStream();
1461+
1462+
expect(onError).toHaveBeenCalledWith({
1463+
error: new Error('test error'),
1464+
});
1465+
});
1466+
1467+
it('should invoke onError callback when error is thrown in 2nd step', async () => {
1468+
const onError = vi.fn();
1469+
let responseCount = 0;
1470+
1471+
const result = streamText({
1472+
model: new MockLanguageModelV2({
1473+
doStream: async ({ prompt, tools, toolChoice }) => {
1474+
if (responseCount++ === 0) {
1475+
return {
1476+
stream: convertArrayToReadableStream([
1477+
{
1478+
type: 'response-metadata',
1479+
id: 'id-0',
1480+
modelId: 'mock-model-id',
1481+
timestamp: new Date(0),
1482+
},
1483+
{
1484+
type: 'tool-call',
1485+
id: 'call-1',
1486+
toolCallId: 'call-1',
1487+
toolName: 'tool1',
1488+
input: `{ "value": "value" }`,
1489+
},
1490+
{
1491+
type: 'finish',
1492+
finishReason: 'tool-calls',
1493+
usage: testUsage,
1494+
},
1495+
]),
1496+
response: { headers: { call: '1' } },
1497+
};
1498+
}
1499+
1500+
throw new Error('test error');
1501+
},
1502+
}),
1503+
prompt: 'test-input',
1504+
tools: {
1505+
tool1: {
1506+
inputSchema: z.object({ value: z.string() }),
1507+
execute: async () => 'result1',
1508+
},
1509+
},
1510+
stopWhen: stepCountIs(3),
1511+
onError,
1512+
});
1513+
1514+
await result.consumeStream();
1515+
1516+
expect(onError).toHaveBeenCalledWith({
1517+
error: new Error('test error'),
1518+
});
1519+
});
14441520
});
14451521

14461522
describe('result.pipeUIMessageStreamToResponse', async () => {

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1383,11 +1383,20 @@ class DefaultStreamTextResult<TOOLS extends ToolSet, OUTPUT, PARTIAL_OUTPUT>
13831383
}),
13841384
);
13851385

1386-
await streamStep({
1387-
currentStep: currentStep + 1,
1388-
responseMessages,
1389-
usage: combinedUsage,
1390-
});
1386+
try {
1387+
await streamStep({
1388+
currentStep: currentStep + 1,
1389+
responseMessages,
1390+
usage: combinedUsage,
1391+
});
1392+
} catch (error) {
1393+
controller.enqueue({
1394+
type: 'error',
1395+
error,
1396+
});
1397+
1398+
self.closeStream();
1399+
}
13911400
} else {
13921401
controller.enqueue({
13931402
type: 'finish',

0 commit comments

Comments
 (0)