ai 7.0.90 → 7.0.92

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