ai 7.0.142 → 7.0.143

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.
@@ -1,4 +1,5 @@
1
1
  import {
2
+ AISDKError,
2
3
  getErrorMessage,
3
4
  UnsupportedFunctionalityError,
4
5
  type LanguageModelV4,
@@ -44,6 +45,7 @@ import { standardizePrompt } from '../prompt/standardize-prompt';
44
45
  import { wrapGatewayError } from '../prompt/wrap-gateway-error';
45
46
  import type { TelemetryDispatcher } from '../telemetry/telemetry';
46
47
  import type { TelemetryOptions } from '../telemetry/telemetry-options';
48
+ import { getGlobalTelemetryIntegrations } from '../telemetry/telemetry-registry';
47
49
  import { createTextStreamResponse } from '../text-stream/create-text-stream-response';
48
50
  import { pipeTextStreamToResponse } from '../text-stream/pipe-text-stream-to-response';
49
51
  import { toTextStream } from '../text-stream/to-text-stream';
@@ -68,6 +70,7 @@ import { toUIMessageStream as toUIMessageStreamHelper } from '../ui-message-stre
68
70
  import type { InferUIMessageChunk } from '../ui-message-stream/ui-message-chunks';
69
71
  import type { UIMessageStreamResponseInit } from '../ui-message-stream/ui-message-stream-response-init';
70
72
  import {
73
+ asAsyncIterableStream,
71
74
  createAsyncIterableStream,
72
75
  type AsyncIterableStream,
73
76
  } from '../util/async-iterable-stream';
@@ -111,6 +114,10 @@ import type {
111
114
  OnLanguageModelCallStartCallback,
112
115
  } from './language-model-events';
113
116
  import { text, type Output } from './output';
117
+ import {
118
+ createSingleConsumerStreamTextResult,
119
+ type StreamTextSingleConsumerResult,
120
+ } from './stream-text-single-consumer-result';
114
121
  import type {
115
122
  InferCompleteOutput,
116
123
  InferElementOutput,
@@ -171,6 +178,14 @@ import {
171
178
  prepareToolCallConcurrency,
172
179
  } from './tool-call-concurrency';
173
180
 
181
+ /** Internal policy, fixed before the model request starts. */
182
+ type StreamTextRetention = {
183
+ /** Keep a replay branch for independently consumed stream projections. */
184
+ replay: boolean;
185
+ /** Collect complete content for results, callbacks, or subsequent steps. */
186
+ collectContent: boolean;
187
+ };
188
+
174
189
  const originalGenerateId = createIdGenerator({
175
190
  prefix: 'aitxt',
176
191
  size: 24,
@@ -284,6 +299,21 @@ export type StreamTextOnAbortCallback<
284
299
  RUNTIME_CONTEXT extends Context,
285
300
  > = Callback<GenerateTextAbortEvent<TOOLS, RUNTIME_CONTEXT>>;
286
301
 
302
+ type StreamTextOptions<
303
+ TOOLS extends ToolSet,
304
+ RUNTIME_CONTEXT extends Context,
305
+ OUTPUT extends Output,
306
+ > = Parameters<typeof streamTextInternal<TOOLS, RUNTIME_CONTEXT, OUTPUT>>[0] & {
307
+ /**
308
+ * Consume the stream once without replay or final-content getters.
309
+ * Content is collected only when required by tools, output parsing,
310
+ * full-content callbacks, or telemetry integrations.
311
+ *
312
+ * @default false
313
+ */
314
+ experimental_lowMemory?: boolean;
315
+ };
316
+
287
317
  /**
288
318
  * Generate a text and call tools for a given prompt using a language model.
289
319
  *
@@ -330,6 +360,8 @@ export type StreamTextOnAbortCallback<
330
360
  * @param runtimeContext - User-defined runtime context that flows through the entire generation lifecycle.
331
361
  * @param experimental_refineToolInput - Optional mapping of tool names to functions that refine parsed tool inputs before tools are executed and before outputs, callbacks, and telemetry are recorded.
332
362
  *
363
+ * @param experimental_lowMemory - Consume the stream once without replay or final-content getters. Default: false.
364
+ *
333
365
  * @param onChunk - Callback that is called for each chunk of the stream. The stream processing will pause until the callback promise is resolved.
334
366
  * @param onError - Callback that is called when an error occurs during streaming. You can use it to log errors.
335
367
  * @param onStart - Callback invoked when generation begins, before any LLM calls.
@@ -356,6 +388,80 @@ export function streamText<
356
388
  TOOLS extends ToolSet,
357
389
  RUNTIME_CONTEXT extends Context = Context,
358
390
  OUTPUT extends Output = Output<string, string, never>,
391
+ >(
392
+ options: StreamTextOptions<TOOLS, RUNTIME_CONTEXT, OUTPUT> & {
393
+ experimental_lowMemory?: false;
394
+ },
395
+ ): StreamTextResult<TOOLS, RUNTIME_CONTEXT, OUTPUT>;
396
+ export function streamText<
397
+ TOOLS extends ToolSet,
398
+ RUNTIME_CONTEXT extends Context = Context,
399
+ OUTPUT extends Output = Output<string, string, never>,
400
+ >(
401
+ options: StreamTextOptions<TOOLS, RUNTIME_CONTEXT, OUTPUT> & {
402
+ experimental_lowMemory: true;
403
+ },
404
+ ): StreamTextSingleConsumerResult<TOOLS, RUNTIME_CONTEXT, OUTPUT>;
405
+ export function streamText<
406
+ TOOLS extends ToolSet,
407
+ RUNTIME_CONTEXT extends Context = Context,
408
+ OUTPUT extends Output = Output<string, string, never>,
409
+ >(
410
+ options: StreamTextOptions<TOOLS, RUNTIME_CONTEXT, OUTPUT>,
411
+ ):
412
+ | StreamTextResult<TOOLS, RUNTIME_CONTEXT, OUTPUT>
413
+ | StreamTextSingleConsumerResult<TOOLS, RUNTIME_CONTEXT, OUTPUT>;
414
+ export function streamText<
415
+ TOOLS extends ToolSet,
416
+ RUNTIME_CONTEXT extends Context = Context,
417
+ OUTPUT extends Output = Output<string, string, never>,
418
+ >({
419
+ experimental_lowMemory = false,
420
+ ...options
421
+ }: StreamTextOptions<TOOLS, RUNTIME_CONTEXT, OUTPUT>):
422
+ | StreamTextResult<TOOLS, RUNTIME_CONTEXT, OUTPUT>
423
+ | StreamTextSingleConsumerResult<TOOLS, RUNTIME_CONTEXT, OUTPUT> {
424
+ if (!experimental_lowMemory) {
425
+ return streamTextInternal<TOOLS, RUNTIME_CONTEXT, OUTPUT>({ ...options });
426
+ }
427
+
428
+ const telemetry = options.telemetry ?? options.experimental_telemetry;
429
+ const integrations =
430
+ telemetry?.integrations != null
431
+ ? asArray(telemetry.integrations)
432
+ : getGlobalTelemetryIntegrations();
433
+
434
+ // Decide before execution so getter access cannot change collection.
435
+ const collectContent =
436
+ (options.tools != null && Object.keys(options.tools).length > 0) ||
437
+ options.toolChoice != null ||
438
+ options.output != null ||
439
+ options.prepareStep != null ||
440
+ options.stopWhen != null ||
441
+ options.onEnd != null ||
442
+ options.onFinish != null ||
443
+ options.onStepEnd != null ||
444
+ options.onStepFinish != null ||
445
+ options.onAbort != null ||
446
+ options.onLanguageModelCallEnd != null ||
447
+ options.experimental_onLanguageModelCallEnd != null ||
448
+ (telemetry?.isEnabled !== false && integrations.length > 0);
449
+
450
+ return createSingleConsumerStreamTextResult(
451
+ streamTextInternal<TOOLS, RUNTIME_CONTEXT, OUTPUT>({
452
+ ...options,
453
+ _internal: {
454
+ ...options._internal,
455
+ retention: { replay: false, collectContent },
456
+ },
457
+ }),
458
+ );
459
+ }
460
+
461
+ function streamTextInternal<
462
+ TOOLS extends ToolSet,
463
+ RUNTIME_CONTEXT extends Context = Context,
464
+ OUTPUT extends Output = Output<string, string, never>,
359
465
  >({
360
466
  model,
361
467
  tools,
@@ -416,6 +522,7 @@ export function streamText<
416
522
  now = originalNow,
417
523
  generateId = originalGenerateId,
418
524
  generateCallId = originalGenerateCallId,
525
+ retention = { replay: true, collectContent: true },
419
526
  } = {},
420
527
  ...settings
421
528
  }: LanguageModelCallOptions &
@@ -781,6 +888,8 @@ export function streamText<
781
888
  now?: () => number;
782
889
  generateId?: IdGenerator;
783
890
  generateCallId?: IdGenerator;
891
+ /** Internal policy used by the streaming-only facade. */
892
+ retention?: StreamTextRetention;
784
893
  };
785
894
  }): StreamTextResult<TOOLS, RUNTIME_CONTEXT, OUTPUT> {
786
895
  const resolvedToolCallConcurrency =
@@ -812,6 +921,7 @@ export function streamText<
812
921
  onToolExecutionEnd ?? experimental_onToolCallFinish;
813
922
  const resolvedOnStepEnd = onStepEnd ?? onStepFinish;
814
923
  return new DefaultStreamTextResult<TOOLS, RUNTIME_CONTEXT, OUTPUT>({
924
+ retention,
815
925
  model: resolveLanguageModel(model),
816
926
  telemetry,
817
927
  headers,
@@ -922,6 +1032,7 @@ function createOutputTransformStream<
922
1032
  OUTPUT extends Output,
923
1033
  >(
924
1034
  output: OUTPUT,
1035
+ collectOutput = true,
925
1036
  ): TransformStream<
926
1037
  InternalTextStreamPart<TOOLS>,
927
1038
  InternalEnrichedStreamPart<TOOLS, InferPartialOutput<OUTPUT>>
@@ -983,6 +1094,14 @@ function createOutputTransformStream<
983
1094
  InternalEnrichedStreamPart<TOOLS, InferPartialOutput<OUTPUT>>
984
1095
  >({
985
1096
  async transform(chunk, controller) {
1097
+ if (!collectOutput) {
1098
+ controller.enqueue(
1099
+ isStreamRetryBoundaryPart(chunk)
1100
+ ? chunk
1101
+ : { part: chunk, partialOutput: undefined },
1102
+ );
1103
+ return;
1104
+ }
986
1105
  if (isStreamRetryBoundaryPart(chunk)) {
987
1106
  resetOutputState();
988
1107
  controller.enqueue(chunk);
@@ -1204,7 +1323,11 @@ class DefaultStreamTextResult<
1204
1323
 
1205
1324
  private tools: TOOLS | undefined;
1206
1325
 
1326
+ private readonly retention: StreamTextRetention;
1327
+ private streamClaimed = false;
1328
+
1207
1329
  constructor({
1330
+ retention,
1208
1331
  model,
1209
1332
  telemetry,
1210
1333
  headers,
@@ -1260,6 +1383,7 @@ class DefaultStreamTextResult<
1260
1383
  download,
1261
1384
  include,
1262
1385
  }: {
1386
+ retention: StreamTextRetention;
1263
1387
  model: LanguageModelV4;
1264
1388
  telemetry: TelemetryOptions<RUNTIME_CONTEXT, TOOLS> | undefined;
1265
1389
  headers: Record<string, string | undefined> | undefined;
@@ -1348,6 +1472,7 @@ class DefaultStreamTextResult<
1348
1472
  onToolExecutionStart: undefined | OnToolExecutionStartCallback<TOOLS>;
1349
1473
  onToolExecutionEnd: undefined | OnToolExecutionEndCallback<TOOLS>;
1350
1474
  }) {
1475
+ this.retention = retention;
1351
1476
  this.outputSpecification = output;
1352
1477
  this.tools = tools;
1353
1478
  const resolvedToolCallers = resolveToolCallerConfiguration({
@@ -1381,7 +1506,7 @@ class DefaultStreamTextResult<
1381
1506
  let recordedRequest: Omit<LanguageModelRequestMetadata, 'messages'> = {};
1382
1507
  let recordedRequestMessages: Array<ModelMessage> = [];
1383
1508
  let recordedWarnings: Array<CallWarning> = [];
1384
- const recordedSteps: StepResult<TOOLS, RUNTIME_CONTEXT>[] = [];
1509
+ let recordedSteps: StepResult<TOOLS, RUNTIME_CONTEXT>[] = [];
1385
1510
  const initialResponseMessages: Array<ResponseMessage> = [];
1386
1511
  let stepMessagesForNextStep: Array<ModelMessage> | undefined;
1387
1512
  let currentStepMessages: Array<ModelMessage> = [];
@@ -1499,7 +1624,7 @@ class DefaultStreamTextResult<
1499
1624
  part.type === 'tool-approval-response' ||
1500
1625
  part.type === 'tool-error'
1501
1626
  ) {
1502
- recordedContent.push(part);
1627
+ if (retention.collectContent) recordedContent.push(part);
1503
1628
  }
1504
1629
 
1505
1630
  if (part.type === 'text-start') {
@@ -1509,7 +1634,8 @@ class DefaultStreamTextResult<
1509
1634
  providerMetadata: part.providerMetadata,
1510
1635
  };
1511
1636
 
1512
- recordedContent.push(activeTextContent[part.id]);
1637
+ if (retention.collectContent)
1638
+ recordedContent.push(activeTextContent[part.id]);
1513
1639
  }
1514
1640
 
1515
1641
  if (part.type === 'text-delta') {
@@ -1526,7 +1652,7 @@ class DefaultStreamTextResult<
1526
1652
  return;
1527
1653
  }
1528
1654
 
1529
- activeText.text += part.text;
1655
+ if (retention.collectContent) activeText.text += part.text;
1530
1656
  activeText.providerMetadata =
1531
1657
  part.providerMetadata ?? activeText.providerMetadata;
1532
1658
  }
@@ -1562,7 +1688,8 @@ class DefaultStreamTextResult<
1562
1688
  providerMetadata: part.providerMetadata,
1563
1689
  };
1564
1690
 
1565
- recordedContent.push(activeReasoningContent[part.id]);
1691
+ if (retention.collectContent)
1692
+ recordedContent.push(activeReasoningContent[part.id]);
1566
1693
  }
1567
1694
 
1568
1695
  if (part.type === 'reasoning-delta') {
@@ -1579,7 +1706,7 @@ class DefaultStreamTextResult<
1579
1706
  return;
1580
1707
  }
1581
1708
 
1582
- activeReasoning.text += part.text;
1709
+ if (retention.collectContent) activeReasoning.text += part.text;
1583
1710
  activeReasoning.providerMetadata =
1584
1711
  part.providerMetadata ?? activeReasoning.providerMetadata;
1585
1712
  }
@@ -1605,17 +1732,18 @@ class DefaultStreamTextResult<
1605
1732
  }
1606
1733
 
1607
1734
  if (part.type === 'file' || part.type === 'reasoning-file') {
1608
- recordedContent.push({
1609
- type: part.type,
1610
- file: part.file,
1611
- ...(part.providerMetadata != null
1612
- ? { providerMetadata: part.providerMetadata }
1613
- : {}),
1614
- });
1735
+ if (retention.collectContent)
1736
+ recordedContent.push({
1737
+ type: part.type,
1738
+ file: part.file,
1739
+ ...(part.providerMetadata != null
1740
+ ? { providerMetadata: part.providerMetadata }
1741
+ : {}),
1742
+ });
1615
1743
  }
1616
1744
 
1617
1745
  if (part.type === 'tool-result' && !part.preliminary) {
1618
- recordedContent.push(part);
1746
+ if (retention.collectContent) recordedContent.push(part);
1619
1747
  }
1620
1748
 
1621
1749
  if (part.type === 'start-step') {
@@ -1719,7 +1847,7 @@ class DefaultStreamTextResult<
1719
1847
  self._totalUsage.resolve(totalUsage);
1720
1848
 
1721
1849
  // aggregate results:
1722
- self._steps.resolve(recordedSteps);
1850
+ self._steps.resolve(retention.replay ? recordedSteps : []);
1723
1851
 
1724
1852
  if (isAborted) {
1725
1853
  return;
@@ -1752,7 +1880,11 @@ class DefaultStreamTextResult<
1752
1880
  const parsedOutput =
1753
1881
  output == null
1754
1882
  ? undefined
1755
- : await self.getOutputPromise().catch(() => undefined);
1883
+ : await (
1884
+ retention.replay
1885
+ ? self.getOutputPromise()
1886
+ : self.parseOutput(finalStep)
1887
+ ).catch(() => undefined);
1756
1888
 
1757
1889
  await onEnd({
1758
1890
  ...event,
@@ -1806,6 +1938,17 @@ class DefaultStreamTextResult<
1806
1938
  ]);
1807
1939
  } catch (error) {
1808
1940
  controller.error(error);
1941
+ } finally {
1942
+ if (!retention.replay) {
1943
+ // Replace our references; never mutate arrays delivered to callbacks.
1944
+ recordedSteps = [];
1945
+ recordedContent = [];
1946
+ recordedRequestMessages = [];
1947
+ currentStepMessages = [];
1948
+ stepMessagesForNextStep = undefined;
1949
+ activeTextContent = createIdMap();
1950
+ activeReasoningContent = createIdMap();
1951
+ }
1809
1952
  }
1810
1953
  },
1811
1954
  });
@@ -1924,7 +2067,12 @@ class DefaultStreamTextResult<
1924
2067
  });
1925
2068
 
1926
2069
  this.baseStream = stream
1927
- .pipeThrough(createOutputTransformStream(output ?? text()))
2070
+ .pipeThrough(
2071
+ createOutputTransformStream(
2072
+ output ?? text(),
2073
+ retention.replay || output != null,
2074
+ ),
2075
+ )
1928
2076
  .pipeThrough(eventProcessor);
1929
2077
 
1930
2078
  const { maxRetries } = prepareRetries({
@@ -2474,6 +2622,7 @@ class DefaultStreamTextResult<
2474
2622
  },
2475
2623
  _internal: {
2476
2624
  now,
2625
+ collectContent: retention.collectContent,
2477
2626
  },
2478
2627
  ...stepCallSettings,
2479
2628
  }),
@@ -3179,7 +3328,7 @@ class DefaultStreamTextResult<
3179
3328
  get steps() {
3180
3329
  // when any of the promises are accessed, the stream is consumed
3181
3330
  // so it resolves without needing to consume the stream separately
3182
- this.consumeStream();
3331
+ if (this.retention.replay) this.consumeStream();
3183
3332
 
3184
3333
  return this._steps.promise;
3185
3334
  }
@@ -3279,7 +3428,7 @@ class DefaultStreamTextResult<
3279
3428
  get totalUsage() {
3280
3429
  // when any of the promises are accessed, the stream is consumed
3281
3430
  // so it resolves without needing to consume the stream separately
3282
- this.consumeStream();
3431
+ if (this.retention.replay) this.consumeStream();
3283
3432
 
3284
3433
  return this._totalUsage.promise;
3285
3434
  }
@@ -3287,7 +3436,7 @@ class DefaultStreamTextResult<
3287
3436
  get finishReason() {
3288
3437
  // when any of the promises are accessed, the stream is consumed
3289
3438
  // so it resolves without needing to consume the stream separately
3290
- this.consumeStream();
3439
+ if (this.retention.replay) this.consumeStream();
3291
3440
 
3292
3441
  return this._finishReason.promise;
3293
3442
  }
@@ -3295,7 +3444,7 @@ class DefaultStreamTextResult<
3295
3444
  get rawFinishReason() {
3296
3445
  // when any of the promises are accessed, the stream is consumed
3297
3446
  // so it resolves without needing to consume the stream separately
3298
- this.consumeStream();
3447
+ if (this.retention.replay) this.consumeStream();
3299
3448
 
3300
3449
  return this._rawFinishReason.promise;
3301
3450
  }
@@ -3309,17 +3458,69 @@ class DefaultStreamTextResult<
3309
3458
  * However, the LLM results are expected to be small enough to not cause issues.
3310
3459
  */
3311
3460
  private teeStream() {
3461
+ if (!this.retention.replay) {
3462
+ if (this.streamClaimed) {
3463
+ throw new AISDKError({
3464
+ name: 'AI_StreamAlreadyConsumedError',
3465
+ message:
3466
+ 'This result supports one stream consumer. Choose one stream projection or response helper.',
3467
+ });
3468
+ }
3469
+ this.streamClaimed = true;
3470
+ const reader = this.baseStream.getReader();
3471
+ const rejectResults = (error: unknown) =>
3472
+ this.rejectResultPromises(error);
3473
+ return new ReadableStream(
3474
+ {
3475
+ async pull(controller) {
3476
+ try {
3477
+ const { done, value } = await reader.read();
3478
+ if (done) {
3479
+ reader.releaseLock();
3480
+ controller.close();
3481
+ } else {
3482
+ controller.enqueue(value);
3483
+ }
3484
+ } catch (error) {
3485
+ rejectResults(error);
3486
+ reader.releaseLock();
3487
+ controller.error(error);
3488
+ }
3489
+ },
3490
+ async cancel(reason) {
3491
+ rejectResults(
3492
+ reason ??
3493
+ new DOMException('The stream was cancelled.', 'AbortError'),
3494
+ );
3495
+ try {
3496
+ await reader.cancel(reason);
3497
+ } finally {
3498
+ reader.releaseLock();
3499
+ }
3500
+ },
3501
+ },
3502
+ { highWaterMark: 0 },
3503
+ );
3504
+ }
3312
3505
  const [stream1, stream2] = this.baseStream.tee();
3313
3506
  this.baseStream = stream2;
3314
3507
  return stream1;
3315
3508
  }
3316
3509
 
3317
3510
  get textStream(): AsyncIterableStream<string> {
3318
- return createAsyncIterableStream(toTextStream({ stream: this.stream }));
3511
+ return this.asIterableStream(toTextStream({ stream: this.stream }));
3512
+ }
3513
+
3514
+ private asIterableStream<T>(
3515
+ stream: ReadableStream<T>,
3516
+ ): AsyncIterableStream<T> {
3517
+ return this.retention.replay
3518
+ ? createAsyncIterableStream(stream)
3519
+ : asAsyncIterableStream(stream);
3319
3520
  }
3320
3521
 
3321
3522
  get stream(): AsyncIterableStream<TextStreamPart<TOOLS>> {
3322
- return createAsyncIterableStream(
3523
+ return this.asIterableStream(
3323
3524
  this.teeStream().pipeThrough(
3324
3525
  new TransformStream<
3325
3526
  EnrichedStreamPart<TOOLS, InferPartialOutput<OUTPUT>>,
@@ -3362,9 +3563,11 @@ class DefaultStreamTextResult<
3362
3563
  }
3363
3564
 
3364
3565
  async consumeStream(options?: ConsumeStreamOptions): Promise<void> {
3566
+ // A second consumer must not reject the first consumer's result promises.
3567
+ const stream = this.stream;
3365
3568
  try {
3366
3569
  await consumeStream({
3367
- stream: this.stream,
3570
+ stream,
3368
3571
  onError: error => {
3369
3572
  this.rejectResultPromises(error);
3370
3573
  options?.onError?.(error);
@@ -3383,7 +3586,13 @@ class DefaultStreamTextResult<
3383
3586
  }
3384
3587
 
3385
3588
  get partialOutputStream(): AsyncIterableStream<InferPartialOutput<OUTPUT>> {
3386
- return createAsyncIterableStream(
3589
+ if (!this.retention.replay && this.outputSpecification == null) {
3590
+ throw new UnsupportedFunctionalityError({
3591
+ functionality:
3592
+ 'partialOutputStream without an explicit output specification with experimental_lowMemory enabled',
3593
+ });
3594
+ }
3595
+ return this.asIterableStream(
3387
3596
  this.teeStream().pipeThrough(
3388
3597
  new TransformStream<
3389
3598
  EnrichedStreamPart<TOOLS, InferPartialOutput<OUTPUT>>,
@@ -3410,27 +3619,31 @@ class DefaultStreamTextResult<
3410
3619
  });
3411
3620
  }
3412
3621
 
3413
- return createAsyncIterableStream(this.teeStream().pipeThrough(transform));
3622
+ return this.asIterableStream(this.teeStream().pipeThrough(transform));
3414
3623
  }
3415
3624
 
3416
3625
  private getOutputPromise(): Promise<InferCompleteOutput<OUTPUT>> {
3417
3626
  if (this.outputPromise == null) {
3418
- this.outputPromise = this.finalStep.then(step => {
3419
- const output = this.outputSpecification ?? text();
3420
- return output.parseCompleteOutput(
3421
- { text: step.text },
3422
- {
3423
- response: step.response,
3424
- usage: step.usage,
3425
- finishReason: step.finishReason,
3426
- },
3427
- );
3428
- });
3627
+ this.outputPromise = this.finalStep.then(step => this.parseOutput(step));
3429
3628
  }
3430
3629
 
3431
3630
  return this.outputPromise;
3432
3631
  }
3433
3632
 
3633
+ private parseOutput(
3634
+ step: StepResult<TOOLS, RUNTIME_CONTEXT>,
3635
+ ): Promise<InferCompleteOutput<OUTPUT>> {
3636
+ const output = this.outputSpecification ?? text();
3637
+ return output.parseCompleteOutput(
3638
+ { text: step.text },
3639
+ {
3640
+ response: step.response,
3641
+ usage: step.usage,
3642
+ finishReason: step.finishReason,
3643
+ },
3644
+ );
3645
+ }
3646
+
3434
3647
  get output(): Promise<InferCompleteOutput<OUTPUT>> {
3435
3648
  return this.getOutputPromise();
3436
3649
  }
package/src/ui/chat.ts CHANGED
@@ -196,7 +196,7 @@ export type ChatOnFinishCallback<UI_MESSAGE extends UIMessage> = (options: {
196
196
  export interface ChatInit<UI_MESSAGE extends UIMessage> {
197
197
  /**
198
198
  * A unique identifier for the chat. If not provided, a random one will be
199
- * generated.
199
+ * generated when first accessed.
200
200
  */
201
201
  id?: string;
202
202
 
@@ -249,7 +249,12 @@ export interface ChatInit<UI_MESSAGE extends UIMessage> {
249
249
  }
250
250
 
251
251
  export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
252
- readonly id: string;
252
+ private _id: string | undefined;
253
+
254
+ get id(): string {
255
+ return (this._id ??= this.generateId());
256
+ }
257
+
253
258
  readonly generateId: IdGenerator;
254
259
 
255
260
  protected state: ChatState<UI_MESSAGE>;
@@ -280,7 +285,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
280
285
 
281
286
  constructor({
282
287
  generateId = generateIdFunc,
283
- id = generateId(),
288
+ id,
284
289
  transport = new DefaultChatTransport(),
285
290
  messageMetadataSchema,
286
291
  dataPartSchemas,
@@ -293,7 +298,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
293
298
  }: Omit<ChatInit<UI_MESSAGE>, 'messages'> & {
294
299
  state: ChatState<UI_MESSAGE>;
295
300
  }) {
296
- this.id = id;
301
+ this._id = id;
297
302
  this.transport = transport;
298
303
  this.generateId = generateId;
299
304
  this.messageMetadataSchema = messageMetadataSchema;