ai 7.0.90 → 7.0.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -127,7 +127,15 @@ import {
127
127
  isStopConditionMet,
128
128
  type StopCondition,
129
129
  } from './stop-condition';
130
- import { streamLanguageModelCall } from './stream-language-model-call';
130
+ import {
131
+ streamLanguageModelCall,
132
+ type LanguageModelStreamPart,
133
+ } from './stream-language-model-call';
134
+ import {
135
+ createStreamRetryAttemptBoundaryPart,
136
+ isStreamRetryAttemptBoundaryPart,
137
+ type StreamRetryAttemptBoundaryPart,
138
+ } from './stream-retry-attempt-boundary';
131
139
  import type {
132
140
  ConsumeStreamOptions,
133
141
  StreamTextResult,
@@ -193,9 +201,14 @@ const isOutputChunkType = {
193
201
  'model-call-end': false,
194
202
  error: false,
195
203
  raw: false,
196
- } as const satisfies Record<ExecuteToolsStreamPart['type'], boolean>;
197
-
198
- function isOutputChunk(chunk: ExecuteToolsStreamPart): boolean {
204
+ } as const satisfies Record<
205
+ Exclude<ExecuteToolsStreamPart, StreamRetryAttemptBoundaryPart>['type'],
206
+ boolean
207
+ >;
208
+
209
+ function isOutputChunk(
210
+ chunk: Exclude<ExecuteToolsStreamPart, StreamRetryAttemptBoundaryPart>,
211
+ ): boolean {
199
212
  if (!isOutputChunkType[chunk.type]) {
200
213
  return false;
201
214
  }
@@ -257,14 +270,29 @@ export type StreamTextTransform<TOOLS extends ToolSet> = (options: {
257
270
  stopStream: () => void;
258
271
  }) => TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>;
259
272
 
273
+ /** A result that requests recovery from a streamed provider error. */
274
+ export type StreamTextOnErrorResult = { retry: true };
275
+
260
276
  /**
261
- * Callback that is set using the `onError` option.
277
+ * Existing observer callback that is set using the `onError` option.
262
278
  *
263
279
  * @param event - The event that is passed to the callback.
264
280
  */
265
- export type StreamTextOnErrorCallback = Callback<{
281
+ export type StreamTextOnErrorCallback = Callback<{ error: unknown }>;
282
+
283
+ /**
284
+ * Retry-capable callback that is set using the `onError` option.
285
+ *
286
+ * @param event - The event that is passed to the callback.
287
+ */
288
+ export type StreamTextOnErrorRetryCallback = (event: {
266
289
  error: unknown;
267
- }>;
290
+ }) =>
291
+ | PromiseLike<void | StreamTextOnErrorResult>
292
+ | void
293
+ | StreamTextOnErrorResult;
294
+
295
+ type StreamTextOnErrorHandler = (event: { error: unknown }) => void;
268
296
 
269
297
  /**
270
298
  * Callback that is set using the `onChunk` option.
@@ -344,6 +372,7 @@ export type StreamTextOnAbortCallback<
344
372
  * If set and supported by the model, calls will generate deterministic results.
345
373
  *
346
374
  * @param maxRetries - Maximum number of retries. Set to 0 to disable retries. Default: 2.
375
+ * @param streamRetries - Maximum number of retries for provider errors received after streaming starts. Set to 0 to disable automatic stream retries while allowing `onError` to request retries. Omit to disable all stream retry behavior. Default: 0.
347
376
  * @param abortSignal - An optional abort signal that can be used to cancel the call.
348
377
  * @param timeout - An optional timeout in milliseconds. The call will be aborted if it takes longer than the specified timeout.
349
378
  * @param headers - Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers.
@@ -388,6 +417,7 @@ export function streamText<
388
417
  messages,
389
418
  allowSystemInMessages,
390
419
  maxRetries,
420
+ streamRetries,
391
421
  abortSignal,
392
422
  timeout,
393
423
  headers,
@@ -410,9 +440,7 @@ export function streamText<
410
440
  experimental_download: download,
411
441
  includeRawChunks,
412
442
  onChunk,
413
- onError = ({ error }) => {
414
- console.error(error);
415
- },
443
+ onError: onErrorArg,
416
444
  onFinish,
417
445
  onEnd = onFinish,
418
446
  onAbort,
@@ -597,9 +625,35 @@ export function streamText<
597
625
  /**
598
626
  * Callback that is invoked when an error occurs during streaming.
599
627
  * You can use it to log errors.
628
+ * Return `{ retry: true }` to retry the current model step after a provider
629
+ * error is received from the response stream when `streamRetries` is
630
+ * explicitly configured.
600
631
  * The stream processing will pause until the callback promise is resolved.
601
632
  */
602
- onError?: StreamTextOnErrorCallback;
633
+ onError?:
634
+ | StreamTextOnErrorCallback
635
+ | StreamTextOnErrorRetryCallback
636
+ | StreamTextOnErrorHandler;
637
+
638
+ /**
639
+ * Maximum number of automatic retries for provider errors received after
640
+ * response streaming has started. Each retry reruns only the current model
641
+ * step. Completed earlier steps and their tool results are preserved.
642
+ *
643
+ * Partial output from a failed attempt that was already emitted cannot be
644
+ * retracted and remains in consumer-facing streams. It is excluded from
645
+ * the recovered step result, structured output parsing, response messages,
646
+ * and subsequent model steps.
647
+ *
648
+ * Set to `0` to disable automatic retries while allowing `onError` to
649
+ * request one retry. When automatic retries are configured, `onError` can
650
+ * request at most one additional retry after they are exhausted. Omit this
651
+ * option to disable all stream retry behavior and preserve incremental tool
652
+ * streaming for existing `onError` observers.
653
+ *
654
+ * @default 0 (stream retry behavior disabled when omitted)
655
+ */
656
+ streamRetries?: number;
603
657
 
604
658
  /**
605
659
  * Callback that is called when the LLM response and all request tool executions
@@ -781,6 +835,11 @@ export function streamText<
781
835
  firstChunkTimeoutMs != null ? new AbortController() : undefined;
782
836
  const chunkAbortController =
783
837
  chunkTimeoutMs != null ? new AbortController() : undefined;
838
+ const onError: StreamTextOnErrorHandler =
839
+ onErrorArg ??
840
+ (({ error }) => {
841
+ console.error(error);
842
+ });
784
843
  const resolvedOnStart = onStart ?? experimental_onStart;
785
844
  const resolvedOnStepStart = onStepStart ?? experimental_onStepStart;
786
845
  const resolvedOnLanguageModelCallStart =
@@ -798,6 +857,7 @@ export function streamText<
798
857
  headers,
799
858
  settings,
800
859
  maxRetries,
860
+ streamRetries,
801
861
  abortSignal: mergeAbortSignals(
802
862
  abortSignal,
803
863
  totalTimeoutMs,
@@ -836,6 +896,7 @@ export function streamText<
836
896
  timeout,
837
897
  onChunk,
838
898
  onError,
899
+ canRetryStreamViaOnError: streamRetries !== undefined && onErrorArg != null,
839
900
  onEnd,
840
901
  onAbort,
841
902
  onStepFinish: resolvedOnStepEnd,
@@ -864,6 +925,31 @@ export type EnrichedStreamPart<TOOLS extends ToolSet, PARTIAL_OUTPUT> = {
864
925
  partialOutput: PARTIAL_OUTPUT | undefined;
865
926
  };
866
927
 
928
+ type StreamRetryBoundaryMetadata = {
929
+ request: LanguageModelRequestMetadata;
930
+ warnings: Array<CallWarning>;
931
+ };
932
+
933
+ const streamRetryBoundarySymbol = Symbol('streamRetryBoundary');
934
+
935
+ type StreamRetryBoundaryPart = {
936
+ [streamRetryBoundarySymbol]: StreamRetryBoundaryMetadata;
937
+ };
938
+
939
+ type InternalTextStreamPart<TOOLS extends ToolSet> =
940
+ | TextStreamPart<TOOLS>
941
+ | StreamRetryBoundaryPart;
942
+
943
+ type InternalEnrichedStreamPart<TOOLS extends ToolSet, PARTIAL_OUTPUT> =
944
+ | EnrichedStreamPart<TOOLS, PARTIAL_OUTPUT>
945
+ | StreamRetryBoundaryPart;
946
+
947
+ function isStreamRetryBoundaryPart(
948
+ part: object,
949
+ ): part is StreamRetryBoundaryPart {
950
+ return streamRetryBoundarySymbol in part;
951
+ }
952
+
867
953
  async function markPromiseAsHandled<T>(promise: Promise<T>): Promise<void> {
868
954
  try {
869
955
  await promise;
@@ -876,8 +962,8 @@ function createOutputTransformStream<
876
962
  >(
877
963
  output: OUTPUT,
878
964
  ): TransformStream<
879
- TextStreamPart<TOOLS>,
880
- EnrichedStreamPart<TOOLS, InferPartialOutput<OUTPUT>>
965
+ InternalTextStreamPart<TOOLS>,
966
+ InternalEnrichedStreamPart<TOOLS, InferPartialOutput<OUTPUT>>
881
967
  > {
882
968
  let firstTextChunkId: string | undefined = undefined;
883
969
  let text = '';
@@ -885,32 +971,61 @@ function createOutputTransformStream<
885
971
  let textProviderMetadata: ProviderMetadata | undefined = undefined;
886
972
  let lastPublishedValue = '';
887
973
 
974
+ function resetAttemptState() {
975
+ firstTextChunkId = undefined;
976
+ text = '';
977
+ textChunk = '';
978
+ textProviderMetadata = undefined;
979
+ lastPublishedValue = '';
980
+ }
981
+
982
+ function enqueueChunk({
983
+ controller,
984
+ chunk,
985
+ }: {
986
+ controller: TransformStreamDefaultController<
987
+ InternalEnrichedStreamPart<TOOLS, InferPartialOutput<OUTPUT>>
988
+ >;
989
+ chunk: EnrichedStreamPart<TOOLS, InferPartialOutput<OUTPUT>>;
990
+ }) {
991
+ controller.enqueue(chunk);
992
+ }
993
+
888
994
  function publishTextChunk({
889
995
  controller,
890
996
  partialOutput = undefined,
891
997
  }: {
892
998
  controller: TransformStreamDefaultController<
893
- EnrichedStreamPart<TOOLS, InferPartialOutput<OUTPUT>>
999
+ InternalEnrichedStreamPart<TOOLS, InferPartialOutput<OUTPUT>>
894
1000
  >;
895
1001
  partialOutput?: InferPartialOutput<OUTPUT>;
896
1002
  }) {
897
- controller.enqueue({
898
- part: {
899
- type: 'text-delta',
900
- id: firstTextChunkId!,
901
- text: textChunk,
902
- providerMetadata: textProviderMetadata,
1003
+ enqueueChunk({
1004
+ controller,
1005
+ chunk: {
1006
+ part: {
1007
+ type: 'text-delta',
1008
+ id: firstTextChunkId!,
1009
+ text: textChunk,
1010
+ providerMetadata: textProviderMetadata,
1011
+ },
1012
+ partialOutput,
903
1013
  },
904
- partialOutput,
905
1014
  });
906
1015
  textChunk = '';
907
1016
  }
908
1017
 
909
1018
  return new TransformStream<
910
- TextStreamPart<TOOLS>,
911
- EnrichedStreamPart<TOOLS, InferPartialOutput<OUTPUT>>
1019
+ InternalTextStreamPart<TOOLS>,
1020
+ InternalEnrichedStreamPart<TOOLS, InferPartialOutput<OUTPUT>>
912
1021
  >({
913
1022
  async transform(chunk, controller) {
1023
+ if (isStreamRetryBoundaryPart(chunk)) {
1024
+ resetAttemptState();
1025
+ controller.enqueue(chunk);
1026
+ return;
1027
+ }
1028
+
914
1029
  // ensure that we publish the last text chunk before the step finish:
915
1030
  if (chunk.type === 'finish-step' && textChunk.length > 0) {
916
1031
  publishTextChunk({ controller });
@@ -921,7 +1036,10 @@ function createOutputTransformStream<
921
1036
  chunk.type !== 'text-start' &&
922
1037
  chunk.type !== 'text-end'
923
1038
  ) {
924
- controller.enqueue({ part: chunk, partialOutput: undefined });
1039
+ enqueueChunk({
1040
+ controller,
1041
+ chunk: { part: chunk, partialOutput: undefined },
1042
+ });
925
1043
  return;
926
1044
  }
927
1045
 
@@ -930,12 +1048,18 @@ function createOutputTransformStream<
930
1048
  if (firstTextChunkId == null) {
931
1049
  firstTextChunkId = chunk.id;
932
1050
  } else if (chunk.id !== firstTextChunkId) {
933
- controller.enqueue({ part: chunk, partialOutput: undefined });
1051
+ enqueueChunk({
1052
+ controller,
1053
+ chunk: { part: chunk, partialOutput: undefined },
1054
+ });
934
1055
  return;
935
1056
  }
936
1057
 
937
1058
  if (chunk.type === 'text-start') {
938
- controller.enqueue({ part: chunk, partialOutput: undefined });
1059
+ enqueueChunk({
1060
+ controller,
1061
+ chunk: { part: chunk, partialOutput: undefined },
1062
+ });
939
1063
  return;
940
1064
  }
941
1065
 
@@ -943,7 +1067,10 @@ function createOutputTransformStream<
943
1067
  if (textChunk.length > 0) {
944
1068
  publishTextChunk({ controller });
945
1069
  }
946
- controller.enqueue({ part: chunk, partialOutput: undefined });
1070
+ enqueueChunk({
1071
+ controller,
1072
+ chunk: { part: chunk, partialOutput: undefined },
1073
+ });
947
1074
  return;
948
1075
  }
949
1076
 
@@ -952,7 +1079,10 @@ function createOutputTransformStream<
952
1079
  textProviderMetadata = chunk.providerMetadata ?? textProviderMetadata;
953
1080
 
954
1081
  if (chunk.text.length === 0 && chunk.providerMetadata != null) {
955
- controller.enqueue({ part: chunk, partialOutput: undefined });
1082
+ enqueueChunk({
1083
+ controller,
1084
+ chunk: { part: chunk, partialOutput: undefined },
1085
+ });
956
1086
  return;
957
1087
  }
958
1088
 
@@ -976,6 +1106,95 @@ function createOutputTransformStream<
976
1106
  });
977
1107
  }
978
1108
 
1109
+ function applyStreamTextTransforms<TOOLS extends ToolSet>({
1110
+ stream,
1111
+ transforms,
1112
+ tools,
1113
+ stopStream,
1114
+ }: {
1115
+ stream: ReadableStream<InternalTextStreamPart<TOOLS>>;
1116
+ transforms: Array<StreamTextTransform<TOOLS>>;
1117
+ tools: TOOLS;
1118
+ stopStream: () => void;
1119
+ }): ReadableStream<InternalTextStreamPart<TOOLS>> {
1120
+ const sourceReader = stream.getReader();
1121
+ let sourceDone = false;
1122
+ let pendingBoundary: StreamRetryBoundaryPart | undefined;
1123
+ let transformedSegmentReader:
1124
+ | ReadableStreamDefaultReader<TextStreamPart<TOOLS>>
1125
+ | undefined;
1126
+
1127
+ const createTransformedSegmentReader = () => {
1128
+ let segment = new ReadableStream<TextStreamPart<TOOLS>>(
1129
+ {
1130
+ async pull(controller) {
1131
+ const { done, value } = await sourceReader.read();
1132
+
1133
+ if (done) {
1134
+ sourceDone = true;
1135
+ controller.close();
1136
+ return;
1137
+ }
1138
+
1139
+ if (isStreamRetryBoundaryPart(value)) {
1140
+ pendingBoundary = value;
1141
+ controller.close();
1142
+ return;
1143
+ }
1144
+
1145
+ controller.enqueue(value);
1146
+ },
1147
+ cancel(reason) {
1148
+ return sourceReader.cancel(reason);
1149
+ },
1150
+ },
1151
+ // Do not prefetch the next source chunk. `stopStream` is invoked from a
1152
+ // user transform and must close the gate before another chunk enters it.
1153
+ { highWaterMark: 0 },
1154
+ );
1155
+
1156
+ for (const transform of transforms) {
1157
+ segment = segment.pipeThrough(
1158
+ transform({
1159
+ tools,
1160
+ stopStream,
1161
+ }),
1162
+ );
1163
+ }
1164
+
1165
+ return segment.getReader();
1166
+ };
1167
+
1168
+ return new ReadableStream<InternalTextStreamPart<TOOLS>>({
1169
+ async pull(controller) {
1170
+ transformedSegmentReader ??= createTransformedSegmentReader();
1171
+
1172
+ const { done, value } = await transformedSegmentReader.read();
1173
+
1174
+ if (!done) {
1175
+ controller.enqueue(value);
1176
+ return;
1177
+ }
1178
+
1179
+ transformedSegmentReader = undefined;
1180
+
1181
+ if (pendingBoundary != null) {
1182
+ controller.enqueue(pendingBoundary);
1183
+ pendingBoundary = undefined;
1184
+ return;
1185
+ }
1186
+
1187
+ if (sourceDone) {
1188
+ controller.close();
1189
+ }
1190
+ },
1191
+ async cancel(reason) {
1192
+ await transformedSegmentReader?.cancel(reason);
1193
+ await sourceReader.cancel(reason);
1194
+ },
1195
+ });
1196
+ }
1197
+
979
1198
  class DefaultStreamTextResult<
980
1199
  TOOLS extends ToolSet,
981
1200
  RUNTIME_CONTEXT extends Context,
@@ -1000,7 +1219,7 @@ class DefaultStreamTextResult<
1000
1219
  private outputPromise: Promise<InferCompleteOutput<OUTPUT>> | undefined;
1001
1220
 
1002
1221
  private readonly addStream: (
1003
- stream: ReadableStream<TextStreamPart<TOOLS>>,
1222
+ stream: ReadableStream<InternalTextStreamPart<TOOLS>>,
1004
1223
  callbacks?: {
1005
1224
  onError?: (error: unknown) => void;
1006
1225
  onCancel?: () => void;
@@ -1023,6 +1242,7 @@ class DefaultStreamTextResult<
1023
1242
  headers,
1024
1243
  settings,
1025
1244
  maxRetries: maxRetriesArg,
1245
+ streamRetries: streamRetriesArg,
1026
1246
  abortSignal,
1027
1247
  stepTimeoutMs,
1028
1248
  stepAbortController,
@@ -1056,6 +1276,7 @@ class DefaultStreamTextResult<
1056
1276
  timeout,
1057
1277
  onChunk,
1058
1278
  onError,
1279
+ canRetryStreamViaOnError,
1059
1280
  onEnd,
1060
1281
  onAbort,
1061
1282
  onStepFinish,
@@ -1075,6 +1296,7 @@ class DefaultStreamTextResult<
1075
1296
  headers: Record<string, string | undefined> | undefined;
1076
1297
  settings: LanguageModelCallOptions;
1077
1298
  maxRetries: number | undefined;
1299
+ streamRetries: number | undefined;
1078
1300
  abortSignal: AbortSignal | undefined;
1079
1301
  stepTimeoutMs: number | undefined;
1080
1302
  stepAbortController: AbortController | undefined;
@@ -1117,7 +1339,8 @@ class DefaultStreamTextResult<
1117
1339
 
1118
1340
  // callbacks:
1119
1341
  onChunk: undefined | StreamTextOnChunkCallback<TOOLS>;
1120
- onError: StreamTextOnErrorCallback;
1342
+ onError: StreamTextOnErrorHandler;
1343
+ canRetryStreamViaOnError: boolean;
1121
1344
  onEnd:
1122
1345
  | undefined
1123
1346
  | StreamTextOnEndCallback<
@@ -1241,20 +1464,36 @@ class DefaultStreamTextResult<
1241
1464
  }
1242
1465
  > = createIdMap();
1243
1466
  let recordedNoOutputError: NoOutputGeneratedError | undefined;
1467
+ const errorsHandledForStreamRetry = new Set<unknown>();
1244
1468
 
1245
1469
  const eventProcessor = new TransformStream<
1246
- EnrichedStreamPart<TOOLS, InferPartialOutput<OUTPUT>>,
1470
+ InternalEnrichedStreamPart<TOOLS, InferPartialOutput<OUTPUT>>,
1247
1471
  EnrichedStreamPart<TOOLS, InferPartialOutput<OUTPUT>>
1248
1472
  >({
1249
1473
  async transform(chunk, controller) {
1250
- controller.enqueue(chunk); // forward the chunk to the next stream
1474
+ if (isStreamRetryBoundaryPart(chunk)) {
1475
+ const retryBoundary = chunk[streamRetryBoundarySymbol];
1476
+ recordedContent = [];
1477
+ activeReasoningContent = createIdMap();
1478
+ activeTextContent = createIdMap();
1479
+ recordedRequest = retryBoundary.request;
1480
+ recordedRequestMessages = retryBoundary.request.messages ?? [];
1481
+ recordedWarnings = retryBoundary.warnings;
1482
+ return;
1483
+ }
1251
1484
 
1252
1485
  const { part } = chunk;
1486
+ controller.enqueue(chunk); // forward the chunk to the next stream
1253
1487
 
1254
- await notify({
1255
- event: { chunk: part },
1256
- callbacks: onChunk,
1257
- });
1488
+ const callbacksHandledForStreamRetry =
1489
+ part.type === 'error' && errorsHandledForStreamRetry.has(part.error);
1490
+
1491
+ if (!callbacksHandledForStreamRetry) {
1492
+ await notify({
1493
+ event: { chunk: part },
1494
+ callbacks: onChunk,
1495
+ });
1496
+ }
1258
1497
 
1259
1498
  if (part.type === 'error') {
1260
1499
  const error = wrapGatewayError(part.error);
@@ -1263,10 +1502,16 @@ class DefaultStreamTextResult<
1263
1502
  recordedNoOutputError = error;
1264
1503
  }
1265
1504
 
1266
- await notify({
1267
- event: { error },
1268
- callbacks: onError,
1269
- });
1505
+ if (callbacksHandledForStreamRetry) {
1506
+ errorsHandledForStreamRetry.delete(part.error);
1507
+ } else {
1508
+ await notify({
1509
+ event: { error },
1510
+ callbacks: async event => {
1511
+ await onError(event);
1512
+ },
1513
+ });
1514
+ }
1270
1515
  }
1271
1516
 
1272
1517
  if (
@@ -1581,13 +1826,14 @@ class DefaultStreamTextResult<
1581
1826
  });
1582
1827
 
1583
1828
  // initialize the stitchable stream and the transformed stream:
1584
- const stitchableStream = createStitchableStream<TextStreamPart<TOOLS>>();
1829
+ const stitchableStream =
1830
+ createStitchableStream<InternalTextStreamPart<TOOLS>>();
1585
1831
  this.addStream = stitchableStream.addStream;
1586
1832
  this.closeStream = stitchableStream.close;
1587
1833
 
1588
1834
  // resilient stream that handles abort signals and errors:
1589
1835
  const reader = stitchableStream.stream.getReader();
1590
- let stream = new ReadableStream<TextStreamPart<TOOLS>>({
1836
+ let stream = new ReadableStream<InternalTextStreamPart<TOOLS>>({
1591
1837
  async start(controller) {
1592
1838
  // send start event:
1593
1839
  controller.enqueue({ type: 'start' });
@@ -1659,19 +1905,18 @@ class DefaultStreamTextResult<
1659
1905
  }),
1660
1906
  );
1661
1907
 
1662
- // transform the stream before output parsing
1663
- // to enable replacement of stream segments:
1664
- for (const transform of transforms) {
1665
- stream = stream.pipeThrough(
1666
- transform({
1667
- tools: tools as TOOLS,
1668
- stopStream() {
1669
- stitchableStream.terminate();
1670
- isRunning = false;
1671
- },
1672
- }),
1673
- );
1674
- }
1908
+ // Transform each retry attempt independently. The retry boundary is
1909
+ // intercepted outside user transforms, so transforms may filter or
1910
+ // reconstruct any public stream part without losing logical isolation.
1911
+ stream = applyStreamTextTransforms({
1912
+ stream,
1913
+ transforms,
1914
+ tools: tools as TOOLS,
1915
+ stopStream() {
1916
+ stitchableStream.terminate();
1917
+ isRunning = false;
1918
+ },
1919
+ });
1675
1920
 
1676
1921
  this.baseStream = stream
1677
1922
  .pipeThrough(createOutputTransformStream(output ?? text()))
@@ -1681,6 +1926,12 @@ class DefaultStreamTextResult<
1681
1926
  maxRetries: maxRetriesArg,
1682
1927
  abortSignal,
1683
1928
  });
1929
+ const { maxRetries: streamRetries } = prepareRetries({
1930
+ maxRetries: streamRetriesArg,
1931
+ abortSignal,
1932
+ parameter: 'streamRetries',
1933
+ defaultMaxRetries: 0,
1934
+ });
1684
1935
 
1685
1936
  const callSettings = prepareLanguageModelCallOptions(settings);
1686
1937
 
@@ -2128,79 +2379,262 @@ class DefaultStreamTextResult<
2128
2379
  const stepStartTimestampMs = now();
2129
2380
 
2130
2381
  const { retry } = prepareRetries({ maxRetries, abortSignal });
2382
+ let hasNotifiedStepStart = false;
2383
+
2384
+ const callLanguageModel = () =>
2385
+ runInStepTracingChannelContext(() =>
2386
+ retry(async () =>
2387
+ streamLanguageModelCall({
2388
+ model: prepareStepResult?.model ?? model,
2389
+ tools: stepModelTools as TOOLS,
2390
+ toolOrder: stepToolOrder,
2391
+ toolChoice: prepareStepResult?.toolChoice ?? toolChoice,
2392
+ instructions: stepInstructions,
2393
+ messages: stepMessages,
2394
+ allowSystemInMessages,
2395
+ repairToolCall,
2396
+ refineToolInput,
2397
+ abortSignal,
2398
+ headers,
2399
+ includeRawChunks: include.rawChunks,
2400
+ providerOptions: stepProviderOptions,
2401
+ download,
2402
+ output,
2403
+ callId,
2404
+ executeLanguageModelCallInTelemetryContext:
2405
+ telemetryDispatcher.executeLanguageModelCall,
2406
+ toolsContext,
2407
+ experimental_sandbox: stepSandbox,
2408
+ onLanguageModelCallStart: filterNullable(
2409
+ onLanguageModelCallStart,
2410
+ telemetryDispatcher.onLanguageModelCallStart as
2411
+ | undefined
2412
+ | OnLanguageModelCallStartCallback,
2413
+ ),
2414
+ onLanguageModelCallEnd: filterNullable(
2415
+ onLanguageModelCallEnd,
2416
+ telemetryDispatcher.onLanguageModelCallEnd as
2417
+ | undefined
2418
+ | OnLanguageModelCallEndCallback<TOOLS>,
2419
+ ),
2420
+ onStart: async ({ promptMessages }) => {
2421
+ if (hasNotifiedStepStart) {
2422
+ return;
2423
+ }
2424
+ hasNotifiedStepStart = true;
2425
+
2426
+ await notify({
2427
+ event: {
2428
+ callId,
2429
+ provider: stepModel.provider,
2430
+ modelId: stepModel.modelId,
2431
+ stepNumber: recordedSteps.length,
2432
+ instructions: stepInstructions,
2433
+ messages: stepMessages,
2434
+ tools,
2435
+ toolChoice: prepareStepResult?.toolChoice ?? toolChoice,
2436
+ activeTools:
2437
+ prepareStepResult?.activeTools ?? activeTools,
2438
+ toolOrder: stepToolOrder,
2439
+ steps: [...recordedSteps],
2440
+ providerOptions: stepProviderOptions,
2441
+ runtimeContext,
2442
+ toolsContext,
2443
+ output,
2444
+ promptMessages,
2445
+ stepTools,
2446
+ stepToolChoice,
2447
+ },
2448
+ callbacks: [onStepStart, telemetryDispatcher.onStepStart],
2449
+ });
2450
+ },
2451
+ _internal: {
2452
+ now,
2453
+ },
2454
+ ...stepCallSettings,
2455
+ }),
2456
+ ),
2457
+ );
2458
+
2459
+ const initialLanguageModelCall = await callLanguageModel();
2460
+ let request = initialLanguageModelCall.request;
2461
+ let response = initialLanguageModelCall.response;
2462
+ let languageModelStreamReader =
2463
+ initialLanguageModelCall.stream.getReader();
2464
+ let automaticStreamRetryCount = 0;
2465
+ let callbackStreamRetryCount = 0;
2466
+ let bufferedAttemptParts: LanguageModelStreamPart<TOOLS>[] = [];
2467
+ const outputChunksHandledBeforeBuffering = new WeakSet<object>();
2468
+ const openTextParts = new Set<string>();
2469
+ const openReasoningParts = new Set<string>();
2470
+ let enqueueStreamRetryAttemptBoundary = false;
2471
+ const shouldBufferToolParts =
2472
+ streamRetries > 0 || canRetryStreamViaOnError;
2473
+
2474
+ const languageModelStream = new ReadableStream<
2475
+ LanguageModelStreamPart<TOOLS> | StreamRetryAttemptBoundaryPart
2476
+ >({
2477
+ async pull(controller) {
2478
+ const enqueueAttemptPart = (
2479
+ part: LanguageModelStreamPart<TOOLS>,
2480
+ ) => {
2481
+ switch (part.type) {
2482
+ case 'text-start':
2483
+ openTextParts.add(part.id);
2484
+ break;
2485
+ case 'text-end':
2486
+ openTextParts.delete(part.id);
2487
+ break;
2488
+ case 'reasoning-start':
2489
+ openReasoningParts.add(part.id);
2490
+ break;
2491
+ case 'reasoning-end':
2492
+ openReasoningParts.delete(part.id);
2493
+ break;
2494
+ }
2495
+
2496
+ controller.enqueue(part);
2497
+ };
2498
+
2499
+ const flushBufferedAttemptParts = () => {
2500
+ for (const part of bufferedAttemptParts) {
2501
+ enqueueAttemptPart(part);
2502
+ }
2503
+ bufferedAttemptParts = [];
2504
+ };
2505
+
2506
+ const closeOpenAttemptParts = () => {
2507
+ for (const id of openTextParts) {
2508
+ controller.enqueue({ type: 'text-end', id });
2509
+ }
2510
+ openTextParts.clear();
2511
+
2512
+ for (const id of openReasoningParts) {
2513
+ controller.enqueue({ type: 'reasoning-end', id });
2514
+ }
2515
+ openReasoningParts.clear();
2516
+ };
2517
+
2518
+ while (true) {
2519
+ const { done, value } = await languageModelStreamReader.read();
2520
+
2521
+ if (enqueueStreamRetryAttemptBoundary) {
2522
+ controller.enqueue(
2523
+ createStreamRetryAttemptBoundaryPart({
2524
+ warnings:
2525
+ !done && value.type === 'model-call-start'
2526
+ ? value.warnings
2527
+ : [],
2528
+ }),
2529
+ );
2530
+ enqueueStreamRetryAttemptBoundary = false;
2531
+ }
2532
+
2533
+ if (done) {
2534
+ flushBufferedAttemptParts();
2535
+ controller.close();
2536
+ return;
2537
+ }
2538
+
2539
+ const isToolPart =
2540
+ value.type === 'tool-input-start' ||
2541
+ value.type === 'tool-input-delta' ||
2542
+ value.type === 'tool-input-end' ||
2543
+ value.type === 'tool-call' ||
2544
+ value.type === 'tool-approval-request' ||
2545
+ value.type === 'tool-approval-response' ||
2546
+ value.type === 'tool-result' ||
2547
+ value.type === 'tool-error';
2548
+
2549
+ if (value.type === 'model-call-end') {
2550
+ flushBufferedAttemptParts();
2551
+ enqueueAttemptPart(value);
2552
+ return;
2553
+ }
2554
+
2555
+ if (
2556
+ shouldBufferToolParts &&
2557
+ value.type !== 'error' &&
2558
+ (isToolPart || bufferedAttemptParts.length > 0)
2559
+ ) {
2560
+ if (isOutputChunk(value)) {
2561
+ clearFirstChunkTimeout();
2562
+ resetChunkTimeout();
2563
+ outputChunksHandledBeforeBuffering.add(value);
2564
+ }
2131
2565
 
2132
- const {
2133
- stream: languageModelStream,
2134
- request,
2135
- response,
2136
- } = await runInStepTracingChannelContext(() =>
2137
- retry(async () =>
2138
- streamLanguageModelCall({
2139
- model: prepareStepResult?.model ?? model,
2140
- tools: stepModelTools as TOOLS,
2141
- toolOrder: stepToolOrder,
2142
- toolChoice: prepareStepResult?.toolChoice ?? toolChoice,
2143
- instructions: stepInstructions,
2144
- messages: stepMessages,
2145
- allowSystemInMessages,
2146
- repairToolCall,
2147
- refineToolInput,
2148
- abortSignal,
2149
- headers,
2150
- includeRawChunks: include.rawChunks,
2151
- providerOptions: stepProviderOptions,
2152
- download,
2153
- output,
2154
- callId,
2155
- executeLanguageModelCallInTelemetryContext:
2156
- telemetryDispatcher.executeLanguageModelCall,
2157
- toolsContext,
2158
- experimental_sandbox: stepSandbox,
2159
- onLanguageModelCallStart: filterNullable(
2160
- onLanguageModelCallStart,
2161
- telemetryDispatcher.onLanguageModelCallStart as
2162
- | undefined
2163
- | OnLanguageModelCallStartCallback,
2164
- ),
2165
- onLanguageModelCallEnd: filterNullable(
2166
- onLanguageModelCallEnd,
2167
- telemetryDispatcher.onLanguageModelCallEnd as
2168
- | undefined
2169
- | OnLanguageModelCallEndCallback<TOOLS>,
2170
- ),
2171
- onStart: async ({ promptMessages }) => {
2172
- await notify({
2173
- event: {
2174
- callId,
2175
- provider: stepModel.provider,
2176
- modelId: stepModel.modelId,
2177
- stepNumber: recordedSteps.length,
2178
- instructions: stepInstructions,
2179
- messages: stepMessages,
2180
- tools,
2181
- toolChoice: prepareStepResult?.toolChoice ?? toolChoice,
2182
- activeTools:
2183
- prepareStepResult?.activeTools ?? activeTools,
2184
- toolOrder: stepToolOrder,
2185
- steps: [...recordedSteps],
2186
- providerOptions: stepProviderOptions,
2187
- runtimeContext,
2188
- toolsContext,
2189
- output,
2190
- promptMessages,
2191
- stepTools,
2192
- stepToolChoice,
2193
- },
2194
- callbacks: [onStepStart, telemetryDispatcher.onStepStart],
2566
+ bufferedAttemptParts.push(value);
2567
+ continue;
2568
+ }
2569
+
2570
+ if (value.type !== 'error') {
2571
+ enqueueAttemptPart(value);
2572
+ return;
2573
+ }
2574
+
2575
+ await notify({
2576
+ event: { chunk: value },
2577
+ callbacks: onChunk,
2578
+ });
2579
+ const error = wrapGatewayError(value.error);
2580
+ let onErrorResult: unknown;
2581
+ try {
2582
+ onErrorResult = await onError({ error });
2583
+ } catch {}
2584
+ const callbackRequestedRetry =
2585
+ canRetryStreamViaOnError &&
2586
+ typeof onErrorResult === 'object' &&
2587
+ onErrorResult != null &&
2588
+ 'retry' in onErrorResult &&
2589
+ onErrorResult.retry === true;
2590
+ const automaticRetry =
2591
+ automaticStreamRetryCount < streamRetries;
2592
+ const callbackRetry =
2593
+ !automaticRetry &&
2594
+ callbackRequestedRetry &&
2595
+ callbackStreamRetryCount < 1;
2596
+
2597
+ if (!automaticRetry && !callbackRetry) {
2598
+ flushBufferedAttemptParts();
2599
+ errorsHandledForStreamRetry.add(value.error);
2600
+ controller.enqueue(value);
2601
+ return;
2602
+ }
2603
+
2604
+ if (automaticRetry) {
2605
+ automaticStreamRetryCount++;
2606
+ } else {
2607
+ callbackStreamRetryCount++;
2608
+ }
2609
+
2610
+ await languageModelStreamReader.cancel(error);
2611
+ bufferedAttemptParts = [];
2612
+ closeOpenAttemptParts();
2613
+
2614
+ let retryLanguageModelCall: Awaited<
2615
+ ReturnType<typeof callLanguageModel>
2616
+ >;
2617
+ try {
2618
+ retryLanguageModelCall = await callLanguageModel();
2619
+ } catch (retryError) {
2620
+ controller.enqueue({
2621
+ type: 'error',
2622
+ error: retryError,
2195
2623
  });
2196
- },
2197
- _internal: {
2198
- now,
2199
- },
2200
- ...stepCallSettings,
2201
- }),
2202
- ),
2203
- );
2624
+ controller.close();
2625
+ return;
2626
+ }
2627
+ request = retryLanguageModelCall.request;
2628
+ response = retryLanguageModelCall.response;
2629
+ languageModelStreamReader =
2630
+ retryLanguageModelCall.stream.getReader();
2631
+ enqueueStreamRetryAttemptBoundary = true;
2632
+ }
2633
+ },
2634
+ cancel(reason) {
2635
+ return languageModelStreamReader.cancel(reason);
2636
+ },
2637
+ });
2204
2638
 
2205
2639
  startFirstChunkTimeout();
2206
2640
 
@@ -2259,14 +2693,14 @@ class DefaultStreamTextResult<
2259
2693
 
2260
2694
  // Conditionally include request.body based on include settings.
2261
2695
  // Large payloads (e.g., base64-encoded images) can cause memory issues.
2262
- const stepRequest: LanguageModelRequestMetadata = {
2696
+ const getStepRequest = (): LanguageModelRequestMetadata => ({
2263
2697
  ...request,
2264
2698
  body: include.requestBody ? request?.body : undefined,
2265
2699
  messages: include.requestMessages
2266
2700
  ? cloneModelMessages(stepMessages)
2267
2701
  : undefined,
2268
- };
2269
- recordedRequestMessages = stepRequest.messages ?? [];
2702
+ });
2703
+ recordedRequestMessages = getStepRequest().messages ?? [];
2270
2704
 
2271
2705
  const stepToolCalls: TypedToolCall<TOOLS>[] = [];
2272
2706
  const stepToolOutputs: ToolOutput<TOOLS>[] = [];
@@ -2287,10 +2721,10 @@ class DefaultStreamTextResult<
2287
2721
  let stepUsage: LanguageModelUsage = createNullLanguageModelUsage();
2288
2722
  let stepProviderMetadata: ProviderMetadata | undefined;
2289
2723
  let stepFirstChunk = true;
2290
- let modelCallPerformance: Omit<
2724
+ const createModelCallPerformance = (): Omit<
2291
2725
  StepResultPerformance,
2292
2726
  'stepTimeMs' | 'toolExecutionMs'
2293
- > = {
2727
+ > => ({
2294
2728
  responseTimeMs: 0,
2295
2729
  effectiveOutputTokensPerSecond: 0,
2296
2730
  outputTokensPerSecond: undefined,
@@ -2298,26 +2732,63 @@ class DefaultStreamTextResult<
2298
2732
  effectiveTotalTokensPerSecond: 0,
2299
2733
  timeToFirstOutputMs: undefined,
2300
2734
  timeBetweenOutputChunksMs: undefined,
2301
- };
2735
+ });
2736
+ let modelCallPerformance = createModelCallPerformance();
2302
2737
  const toolExecutionMs: Record<string, number> = {};
2303
- let stepResponse: { id: string; timestamp: Date; modelId: string } = {
2738
+ const createStepResponse = () => ({
2304
2739
  id: generateId(),
2305
2740
  timestamp: new Date(),
2306
2741
  modelId: model.modelId,
2307
- };
2742
+ });
2743
+ let stepResponse: {
2744
+ id: string;
2745
+ timestamp: Date;
2746
+ modelId: string;
2747
+ } = createStepResponse();
2308
2748
 
2309
2749
  // maps provider-assigned IDs to stream-unique IDs for the text and
2310
2750
  // reasoning parts that are active in this step
2311
2751
  const textPartIds = new Map<string, string>();
2312
2752
  const reasoningPartIds = new Map<string, string>();
2313
2753
 
2754
+ const enqueueStepPart = (
2755
+ controller: TransformStreamDefaultController<
2756
+ InternalTextStreamPart<TOOLS>
2757
+ >,
2758
+ part: TextStreamPart<TOOLS>,
2759
+ ) => {
2760
+ controller.enqueue(part);
2761
+ };
2762
+
2314
2763
  self.addStream(
2315
2764
  streamWithToolResults.pipeThrough(
2316
2765
  new TransformStream<
2317
2766
  ExecuteToolsStreamPart<TOOLS>,
2318
- TextStreamPart<TOOLS>
2767
+ InternalTextStreamPart<TOOLS>
2319
2768
  >({
2320
2769
  async transform(chunk, controller): Promise<void> {
2770
+ if (isStreamRetryAttemptBoundaryPart(chunk)) {
2771
+ warnings = chunk.warnings;
2772
+ stepFinishReason = 'other';
2773
+ stepRawFinishReason = undefined;
2774
+ hasReceivedTerminalChunk = false;
2775
+ hasReceivedOutputChunk = false;
2776
+ stepUsage = createNullLanguageModelUsage();
2777
+ stepProviderMetadata = undefined;
2778
+ modelCallPerformance = createModelCallPerformance();
2779
+ stepResponse = createStepResponse();
2780
+ textPartIds.clear();
2781
+ reasoningPartIds.clear();
2782
+
2783
+ controller.enqueue({
2784
+ [streamRetryBoundarySymbol]: {
2785
+ request: getStepRequest(),
2786
+ warnings,
2787
+ } satisfies StreamRetryBoundaryMetadata,
2788
+ });
2789
+ return;
2790
+ }
2791
+
2321
2792
  if (chunk.type === 'model-call-start') {
2322
2793
  warnings = chunk.warnings;
2323
2794
  return; // stream start chunks are sent immediately and do not count as first chunk
@@ -2327,9 +2798,9 @@ class DefaultStreamTextResult<
2327
2798
  stepFirstChunk = false;
2328
2799
 
2329
2800
  // Step start:
2330
- controller.enqueue({
2801
+ enqueueStepPart(controller, {
2331
2802
  type: 'start-step',
2332
- request: stepRequest,
2803
+ request: getStepRequest(),
2333
2804
  warnings: warnings ?? [],
2334
2805
  });
2335
2806
  }
@@ -2337,13 +2808,21 @@ class DefaultStreamTextResult<
2337
2808
  const chunkType = chunk.type;
2338
2809
 
2339
2810
  if (isOutputChunk(chunk)) {
2340
- if (!hasReceivedOutputChunk) {
2811
+ const timeoutHandledBeforeBuffering =
2812
+ outputChunksHandledBeforeBuffering.has(chunk);
2813
+
2814
+ if (
2815
+ !hasReceivedOutputChunk &&
2816
+ !timeoutHandledBeforeBuffering
2817
+ ) {
2341
2818
  // Clear before forwarding the first output so a timeout
2342
2819
  // cannot race with already-visible generated content.
2343
2820
  clearFirstChunkTimeout();
2344
2821
  }
2345
2822
  hasReceivedOutputChunk = true;
2346
- resetChunkTimeout();
2823
+ if (!timeoutHandledBeforeBuffering) {
2824
+ resetChunkTimeout();
2825
+ }
2347
2826
  }
2348
2827
 
2349
2828
  switch (chunkType) {
@@ -2355,14 +2834,14 @@ class DefaultStreamTextResult<
2355
2834
  case 'tool-input-end':
2356
2835
  case 'tool-input-delta':
2357
2836
  case 'tool-approval-request': {
2358
- controller.enqueue(chunk);
2837
+ enqueueStepPart(controller, chunk);
2359
2838
  break;
2360
2839
  }
2361
2840
 
2362
2841
  case 'text-start': {
2363
2842
  const id = reserveTextPartId(chunk.id);
2364
2843
  textPartIds.set(chunk.id, id);
2365
- controller.enqueue({ ...chunk, id });
2844
+ enqueueStepPart(controller, { ...chunk, id });
2366
2845
  break;
2367
2846
  }
2368
2847
 
@@ -2371,7 +2850,7 @@ class DefaultStreamTextResult<
2371
2850
  chunk.text.length > 0 ||
2372
2851
  chunk.providerMetadata != null
2373
2852
  ) {
2374
- controller.enqueue({
2853
+ enqueueStepPart(controller, {
2375
2854
  ...chunk,
2376
2855
  id: textPartIds.get(chunk.id) ?? chunk.id,
2377
2856
  });
@@ -2380,7 +2859,7 @@ class DefaultStreamTextResult<
2380
2859
  }
2381
2860
 
2382
2861
  case 'text-end': {
2383
- controller.enqueue({
2862
+ enqueueStepPart(controller, {
2384
2863
  ...chunk,
2385
2864
  id: textPartIds.get(chunk.id) ?? chunk.id,
2386
2865
  });
@@ -2391,12 +2870,12 @@ class DefaultStreamTextResult<
2391
2870
  case 'reasoning-start': {
2392
2871
  const id = reserveReasoningPartId(chunk.id);
2393
2872
  reasoningPartIds.set(chunk.id, id);
2394
- controller.enqueue({ ...chunk, id });
2873
+ enqueueStepPart(controller, { ...chunk, id });
2395
2874
  break;
2396
2875
  }
2397
2876
 
2398
2877
  case 'reasoning-delta': {
2399
- controller.enqueue({
2878
+ enqueueStepPart(controller, {
2400
2879
  ...chunk,
2401
2880
  id: reasoningPartIds.get(chunk.id) ?? chunk.id,
2402
2881
  });
@@ -2404,7 +2883,7 @@ class DefaultStreamTextResult<
2404
2883
  }
2405
2884
 
2406
2885
  case 'reasoning-end': {
2407
- controller.enqueue({
2886
+ enqueueStepPart(controller, {
2408
2887
  ...chunk,
2409
2888
  id: reasoningPartIds.get(chunk.id) ?? chunk.id,
2410
2889
  });
@@ -2413,20 +2892,20 @@ class DefaultStreamTextResult<
2413
2892
  }
2414
2893
 
2415
2894
  case 'tool-call': {
2416
- controller.enqueue(chunk);
2895
+ enqueueStepPart(controller, chunk);
2417
2896
  // store tool calls for onEnd callback and toolCalls promise:
2418
2897
  stepToolCalls.push(chunk);
2419
2898
  break;
2420
2899
  }
2421
2900
 
2422
2901
  case 'tool-approval-response': {
2423
- controller.enqueue(chunk);
2902
+ enqueueStepPart(controller, chunk);
2424
2903
  stepToolApprovalResponses.push(chunk);
2425
2904
  break;
2426
2905
  }
2427
2906
 
2428
2907
  case 'tool-result': {
2429
- controller.enqueue(chunk);
2908
+ enqueueStepPart(controller, chunk);
2430
2909
 
2431
2910
  if (!chunk.preliminary) {
2432
2911
  stepToolOutputs.push(chunk);
@@ -2436,7 +2915,7 @@ class DefaultStreamTextResult<
2436
2915
  }
2437
2916
 
2438
2917
  case 'tool-error': {
2439
- controller.enqueue(chunk);
2918
+ enqueueStepPart(controller, chunk);
2440
2919
  stepToolOutputs.push(chunk);
2441
2920
  break;
2442
2921
  }
@@ -2471,14 +2950,14 @@ class DefaultStreamTextResult<
2471
2950
 
2472
2951
  case 'error': {
2473
2952
  hasReceivedTerminalChunk = true;
2474
- controller.enqueue(chunk);
2953
+ enqueueStepPart(controller, chunk);
2475
2954
  stepFinishReason = 'error';
2476
2955
  break;
2477
2956
  }
2478
2957
 
2479
2958
  case 'raw': {
2480
2959
  if (include.rawChunks) {
2481
- controller.enqueue(chunk);
2960
+ enqueueStepPart(controller, chunk);
2482
2961
  }
2483
2962
  break;
2484
2963
  }
@@ -2496,7 +2975,7 @@ class DefaultStreamTextResult<
2496
2975
  // output instead of recording an empty step. incomplete
2497
2976
  // streams with partial output retain the partial result:
2498
2977
  if (!hasReceivedTerminalChunk && !hasReceivedOutputChunk) {
2499
- controller.enqueue({
2978
+ enqueueStepPart(controller, {
2500
2979
  type: 'error',
2501
2980
  error: new NoOutputGeneratedError({
2502
2981
  message:
@@ -2528,7 +3007,7 @@ class DefaultStreamTextResult<
2528
3007
  },
2529
3008
  };
2530
3009
 
2531
- controller.enqueue(finishStepPart);
3010
+ enqueueStepPart(controller, finishStepPart);
2532
3011
 
2533
3012
  const combinedUsage = addLanguageModelUsage(usage, stepUsage);
2534
3013
 
@@ -2609,7 +3088,7 @@ class DefaultStreamTextResult<
2609
3088
  }),
2610
3089
  );
2611
3090
  } catch (error) {
2612
- controller.enqueue({
3091
+ enqueueStepPart(controller, {
2613
3092
  type: 'error',
2614
3093
  error,
2615
3094
  });
@@ -2617,7 +3096,7 @@ class DefaultStreamTextResult<
2617
3096
  self.closeStream();
2618
3097
  }
2619
3098
  } else {
2620
- controller.enqueue({
3099
+ enqueueStepPart(controller, {
2621
3100
  type: 'finish',
2622
3101
  finishReason: stepFinishReason,
2623
3102
  rawFinishReason: stepRawFinishReason,