ai 7.0.34 → 7.0.35

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.
@@ -421,13 +421,15 @@ type LanguageModelCallOptions = {
421
421
  * - A number representing milliseconds
422
422
  * - An object with `totalMs` property for the total timeout in milliseconds
423
423
  * - An object with `stepMs` property for the timeout of each step in milliseconds
424
- * - An object with `chunkMs` property for the timeout between stream chunks (streaming only)
424
+ * - An object with `firstChunkMs` property for the timeout until the first content chunk of each step (streaming only)
425
+ * - An object with `chunkMs` property for the timeout between content chunks (streaming only)
425
426
  * - An object with `toolMs` property for the default timeout for all tool executions
426
427
  * - An object with `tools` property for per-tool timeout overrides using `{toolName}Ms` keys
427
428
  */
428
429
  type TimeoutConfiguration<TOOLS extends ToolSet> = number | {
429
430
  totalMs?: number;
430
431
  stepMs?: number;
432
+ firstChunkMs?: number;
431
433
  chunkMs?: number;
432
434
  toolMs?: number;
433
435
  tools?: Partial<Record<`${keyof TOOLS & string}Ms`, number>>;
@@ -1965,7 +1967,8 @@ type GenerateTextStartEvent<TOOLS extends ToolSet = ToolSet, RUNTIME_CONTEXT ext
1965
1967
  readonly maxRetries: number;
1966
1968
  /**
1967
1969
  * Timeout configuration for the generation.
1968
- * Can be a number (milliseconds) or an object with totalMs, stepMs, chunkMs, toolMs, and per-tool overrides via tools.
1970
+ * Can be a number (milliseconds) or an object with totalMs, stepMs,
1971
+ * firstChunkMs (streaming only), chunkMs, toolMs, and per-tool overrides via tools.
1969
1972
  */
1970
1973
  readonly timeout: TimeoutConfiguration<TOOLS> | undefined;
1971
1974
  /** Additional HTTP headers sent with the request. */
@@ -85,7 +85,7 @@ import {
85
85
  } from "@ai-sdk/provider-utils";
86
86
 
87
87
  // src/version.ts
88
- var VERSION = true ? "7.0.34" : "0.0.0-test";
88
+ var VERSION = true ? "7.0.35" : "0.0.0-test";
89
89
 
90
90
  // src/util/download/download.ts
91
91
  var download = async ({
@@ -142,7 +142,8 @@ You can specify the timeout either as a number (milliseconds) or as an object wi
142
142
 
143
143
  - `totalMs`: The total timeout for the entire call including all steps.
144
144
  - `stepMs`: The timeout for each individual step (LLM call). This is useful for multi-step generations where you want to limit the time spent on each step independently.
145
- - `chunkMs`: The timeout between stream chunks (streaming only). The call will abort if no new chunk is received within this duration. This is useful for detecting stalled streams.
145
+ - `firstChunkMs`: The timeout until the first content-bearing output of each step (streaming only). Text deltas, reasoning deltas, tool-input deltas, generated files, and tool calls satisfy the timeout. Response metadata, stream starts, empty deltas, raw chunks, and transport activity do not satisfy or reset it.
146
+ - `chunkMs`: The timeout between content-bearing output chunks after output has started (streaming only). Non-content chunks do not reset it. This is useful for detecting streams that stall after generation begins.
146
147
  - `toolMs`: The default timeout for all tool executions. If a tool takes longer, it aborts and returns a tool-error so the model can respond or retry.
147
148
  - `tools`: Per-tool timeout overrides using `{toolName}Ms` keys (e.g. `weatherMs`, `slowApiMs`). Takes precedence over `toolMs`. Tool names are type-checked for autocomplete.
148
149
 
@@ -195,7 +196,19 @@ const result = await generateText({
195
196
  const result = streamText({
196
197
  model: __MODEL__,
197
198
  prompt: 'Invent a new holiday and describe its traditions.',
198
- timeout: { chunkMs: 5000 }, // abort if no chunk received for 5 seconds
199
+ timeout: { chunkMs: 5000 }, // abort if content stalls for 5 seconds
200
+ });
201
+ ```
202
+
203
+ #### Example: First-content timeout for streaming (streamText only)
204
+
205
+ ```ts
206
+ const result = streamText({
207
+ model: __MODEL__,
208
+ prompt: 'Invent a new holiday and describe its traditions.',
209
+ timeout: {
210
+ firstChunkMs: 10000, // 10 seconds for first content in each step
211
+ },
199
212
  });
200
213
  ```
201
214
 
@@ -79,6 +79,12 @@ npx @ai-sdk/devtools@latest
79
79
 
80
80
  Open [http://localhost:4983](http://localhost:4983) to view your AI SDK interactions.
81
81
 
82
+ ### Choose a viewer theme
83
+
84
+ The viewer uses the dark theme by default. Use the theme button in the viewer
85
+ header to switch between dark and light themes. Your selection is stored in
86
+ your browser and restored the next time you open the viewer at the same origin.
87
+
82
88
  ### Monorepo usage
83
89
 
84
90
  If you are using a monorepo setup (e.g. Turborepo, Nx), start DevTools from the same workspace where your AI SDK code runs.
@@ -192,6 +192,46 @@ const result = await agent.continueStream({ session });
192
192
  Use `continueStream()` for incremental output, or `continueGenerate()` to drain
193
193
  the continued turn and return a `GenerateTextResult`.
194
194
 
195
+ ## Stop After a Harness Step
196
+
197
+ Use `stopWhen` to opt into semantic step boundaries. Predicates run after real
198
+ harness tool steps that can continue into another model step, and receive the
199
+ completed steps from the current invocation. When a predicate matches, the
200
+ returned result finishes while the underlying turn remains unfinished. A
201
+ terminal text-only step instead consumes the turn's `finish` event and finishes
202
+ naturally.
203
+
204
+ ```ts
205
+ import { isStepCount } from 'ai';
206
+
207
+ const steppedAgent = new HarnessAgent({
208
+ harness: claudeCode,
209
+ sandbox: createVercelSandbox({
210
+ runtime: 'node24',
211
+ ports: [4000],
212
+ }),
213
+ stopWhen: isStepCount(1),
214
+ });
215
+
216
+ const session = await steppedAgent.createSession();
217
+ const result = await steppedAgent.generate({
218
+ session,
219
+ prompt: 'Create a short TODO.md for this repository.',
220
+ });
221
+
222
+ if (session.hasUnfinishedTurn()) {
223
+ const continueFrom = await session.suspendTurn();
224
+ await persistContinuationState({ chatId, continuationState: continueFrom });
225
+ }
226
+ ```
227
+
228
+ `stopWhen` has no default. When omitted, `HarnessAgent` continues running until
229
+ the turn naturally finishes or pauses for host input, preserving the behavior
230
+ of agents without step control. Pass one predicate or an array; matching any
231
+ predicate finishes the current result slice. Resume a stopped turn with
232
+ `createSession({ continueFrom })` and `continueStream()` or
233
+ `continueGenerate()`.
234
+
195
235
  ## Prepare the Sandbox
196
236
 
197
237
  Use `sandboxConfig` to prepare the sandbox before the harness starts.
@@ -325,6 +365,8 @@ console.log(preparation.identity);
325
365
  - `sandbox`: a `HarnessV1SandboxProvider`.
326
366
  - `id`: optional stable agent identifier.
327
367
  - `instructions`: instructions applied once to a fresh session.
368
+ - `stopWhen`: condition(s) for finishing a result slice after a completed
369
+ harness tool step that can continue into another model step.
328
370
  - `tools`: AI SDK tools executed by the host when the harness calls them.
329
371
  - `activeTools`: allowlist of built-in and host-executed tools the harness can
330
372
  call.
@@ -477,10 +477,10 @@ To see `streamText` in action, check out [these examples](#examples).
477
477
  },
478
478
  {
479
479
  name: 'timeout',
480
- type: 'number | { totalMs?: number; stepMs?: number; chunkMs?: number; toolMs?: number; tools?: { [toolName]Ms?: number } }',
480
+ type: 'number | { totalMs?: number; stepMs?: number; firstChunkMs?: number; chunkMs?: number; toolMs?: number; tools?: { [toolName]Ms?: number } }',
481
481
  isOptional: true,
482
482
  description:
483
- 'Timeout in milliseconds. Can be specified as a number or as an object with totalMs, stepMs, chunkMs, toolMs, and/or tools properties. totalMs sets the total timeout for the entire call. stepMs sets the timeout for each individual step (LLM call). chunkMs sets the timeout between stream chunks - the call will abort if no new chunk is received within this duration. toolMs sets the default timeout for all tool executions. tools sets per-tool timeout overrides using the pattern {toolName}Ms (e.g. weatherMs, slowApiMs) that take precedence over toolMs - tool names are type-checked for autocomplete. If a tool takes longer than its timeout, it aborts and returns a tool-error so the model can respond or retry. Can be used alongside abortSignal.',
483
+ 'Timeout in milliseconds. Can be specified as a number or as an object with totalMs, stepMs, firstChunkMs, chunkMs, toolMs, and/or tools properties. totalMs sets the total timeout for the entire call. stepMs sets the timeout for each individual step (LLM call). firstChunkMs sets the timeout until the first content-bearing output in each step; metadata, stream starts, empty deltas, raw chunks, and transport activity do not satisfy it. chunkMs sets the timeout between content-bearing output chunks after output has started; non-content chunks do not reset it. toolMs sets the default timeout for all tool executions. tools sets per-tool timeout overrides using the pattern {toolName}Ms (e.g. weatherMs, slowApiMs) that take precedence over toolMs - tool names are type-checked for autocomplete. If a tool takes longer than its timeout, it aborts and returns a tool-error so the model can respond or retry. Can be used alongside abortSignal.',
484
484
  },
485
485
  {
486
486
  name: 'headers',
@@ -4020,7 +4020,7 @@ To see `streamText` in action, check out [these examples](#examples).
4020
4020
  },
4021
4021
  {
4022
4022
  name: 'pipeUIMessageStreamToResponse',
4023
- type: '(response: ServerResponse, options?: ResponseInit & UIMessageStreamOptions) => void',
4023
+ type: '(response: ServerResponse, options?: ResponseInit & UIMessageStreamOptions) => Promise<void>',
4024
4024
  description:
4025
4025
  'Writes UI message stream output to a Node.js response-like object.',
4026
4026
  properties: [
@@ -4051,7 +4051,7 @@ To see `streamText` in action, check out [these examples](#examples).
4051
4051
  },
4052
4052
  {
4053
4053
  name: 'pipeTextStreamToResponse',
4054
- type: '(response: ServerResponse, init?: ResponseInit) => void',
4054
+ type: '(response: ServerResponse, init?: ResponseInit) => Promise<void>',
4055
4055
  description:
4056
4056
  'Writes text delta output to a Node.js response-like object. It sets a `Content-Type` header to `text/plain; charset=utf-8` and writes each text delta as a separate chunk.',
4057
4057
  properties: [
@@ -246,8 +246,9 @@ To see `ToolLoopAgent` in action, check out [these examples](#examples).
246
246
  },
247
247
  {
248
248
  name: 'timeout',
249
- type: 'number | { totalMs?: number; stepMs?: number; chunkMs?: number } | undefined',
250
- description: 'Timeout configuration for the generation.',
249
+ type: 'number | { totalMs?: number; stepMs?: number; firstChunkMs?: number; chunkMs?: number } | undefined',
250
+ description:
251
+ 'Timeout configuration for the generation. firstChunkMs and chunkMs are only enforced by stream().',
251
252
  },
252
253
  {
253
254
  name: 'headers',
@@ -360,8 +361,9 @@ To see `ToolLoopAgent` in action, check out [these examples](#examples).
360
361
  },
361
362
  {
362
363
  name: 'timeout',
363
- type: 'number | { totalMs?: number; stepMs?: number; chunkMs?: number } | undefined',
364
- description: 'Timeout configuration for the generation.',
364
+ type: 'number | { totalMs?: number; stepMs?: number; firstChunkMs?: number; chunkMs?: number } | undefined',
365
+ description:
366
+ 'Timeout configuration for the generation. firstChunkMs and chunkMs are only enforced by stream().',
365
367
  },
366
368
  {
367
369
  name: 'headers',
@@ -712,10 +714,10 @@ const result = await agent.generate({
712
714
  },
713
715
  {
714
716
  name: 'timeout',
715
- type: 'number | { totalMs?: number; stepMs?: number; chunkMs?: number }',
717
+ type: 'number | { totalMs?: number; stepMs?: number; firstChunkMs?: number; chunkMs?: number }',
716
718
  isOptional: true,
717
719
  description:
718
- 'Timeout in milliseconds. Can be specified as a number or as an object with totalMs, stepMs, and/or chunkMs properties. The call will be aborted if it takes longer than the specified timeout. Can be used alongside abortSignal.',
720
+ 'Timeout in milliseconds. Can be specified as a number or as an object with totalMs, stepMs, firstChunkMs, and/or chunkMs properties. firstChunkMs and chunkMs have no effect on generate(); they are streaming-only and are enforced by stream(). Can be used alongside abortSignal.',
719
721
  },
720
722
  {
721
723
  name: 'experimental_sandbox',
@@ -828,10 +830,10 @@ for await (const chunk of stream.textStream) {
828
830
  },
829
831
  {
830
832
  name: 'timeout',
831
- type: 'number | { totalMs?: number; stepMs?: number; chunkMs?: number }',
833
+ type: 'number | { totalMs?: number; stepMs?: number; firstChunkMs?: number; chunkMs?: number }',
832
834
  isOptional: true,
833
835
  description:
834
- 'Timeout in milliseconds. Can be specified as a number or as an object with totalMs, stepMs, and/or chunkMs properties. The call will be aborted if it takes longer than the specified timeout. Can be used alongside abortSignal.',
836
+ 'Timeout in milliseconds. Can be specified as a number or as an object with totalMs, stepMs, firstChunkMs, and/or chunkMs properties. firstChunkMs limits the wait for the first content-bearing output in each model-call step. chunkMs limits gaps between later content-bearing output chunks. Can be used alongside abortSignal.',
835
837
  },
836
838
  {
837
839
  name: 'experimental_sandbox',
@@ -17,7 +17,7 @@ The `pipeUIMessageStreamToResponse` function pipes streaming data to a Node.js S
17
17
  ## Example
18
18
 
19
19
  ```tsx
20
- pipeUIMessageStreamToResponse({
20
+ await pipeUIMessageStreamToResponse({
21
21
  response: serverResponse,
22
22
  status: 200,
23
23
  statusText: 'OK',
@@ -75,3 +75,8 @@ pipeUIMessageStreamToResponse({
75
75
  },
76
76
  ]}
77
77
  />
78
+
79
+ ### Returns
80
+
81
+ A `Promise<void>` that resolves when the stream has been written to the response
82
+ and rejects when reading or writing the stream fails.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.34",
3
+ "version": "7.0.35",
4
4
  "type": "module",
5
5
  "description": "AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.",
6
6
  "license": "Apache-2.0",
@@ -42,7 +42,7 @@
42
42
  }
43
43
  },
44
44
  "dependencies": {
45
- "@ai-sdk/gateway": "4.0.26",
45
+ "@ai-sdk/gateway": "4.0.27",
46
46
  "@ai-sdk/provider": "4.0.3",
47
47
  "@ai-sdk/provider-utils": "5.0.12"
48
48
  },
@@ -70,7 +70,10 @@ export type AgentCallParameters<
70
70
  abortSignal?: AbortSignal;
71
71
 
72
72
  /**
73
- * Timeout in milliseconds. Can be specified as a number or as an object with `totalMs`.
73
+ * Timeout in milliseconds. Can be specified as a number or as an object
74
+ * with total, per-step, first-content, inter-content, and tool timeouts.
75
+ * First-content and inter-content timeouts are only enforced by streaming
76
+ * calls.
74
77
  */
75
78
  timeout?: TimeoutConfiguration<TOOLS>;
76
79
 
@@ -66,7 +66,7 @@ export async function pipeAgentUIStreamToResponse<
66
66
  UIMessageStreamOptions<
67
67
  UIMessage<MESSAGE_METADATA, never, InferUITools<TOOLS>>
68
68
  >): Promise<void> {
69
- pipeUIMessageStreamToResponse({
69
+ return pipeUIMessageStreamToResponse({
70
70
  response,
71
71
  headers,
72
72
  status,
@@ -85,7 +85,10 @@ export interface StreamObjectResult<PARTIAL, RESULT, ELEMENT_STREAM> {
85
85
  * @param response A Node.js response-like object (ServerResponse).
86
86
  * @param init Optional headers, status code, and status text.
87
87
  */
88
- pipeTextStreamToResponse(response: ServerResponse, init?: ResponseInit): void;
88
+ pipeTextStreamToResponse(
89
+ response: ServerResponse,
90
+ init?: ResponseInit,
91
+ ): Promise<void>;
89
92
 
90
93
  /**
91
94
  * Creates a simple text stream response.
@@ -964,7 +964,7 @@ class DefaultStreamObjectResult<
964
964
  }
965
965
 
966
966
  pipeTextStreamToResponse(response: ServerResponse, init?: ResponseInit) {
967
- pipeTextStreamToResponse({
967
+ return pipeTextStreamToResponse({
968
968
  response,
969
969
  stream: this.textStream,
970
970
  ...init,
@@ -56,7 +56,8 @@ export type GenerateTextStartEvent<
56
56
 
57
57
  /**
58
58
  * Timeout configuration for the generation.
59
- * Can be a number (milliseconds) or an object with totalMs, stepMs, chunkMs, toolMs, and per-tool overrides via tools.
59
+ * Can be a number (milliseconds) or an object with totalMs, stepMs,
60
+ * firstChunkMs (streaming only), chunkMs, toolMs, and per-tool overrides via tools.
60
61
  */
61
62
  readonly timeout: TimeoutConfiguration<TOOLS> | undefined;
62
63
 
@@ -381,7 +381,7 @@ export interface StreamTextResult<
381
381
  pipeUIMessageStreamToResponse<UI_MESSAGE extends UIMessage>(
382
382
  response: ServerResponse,
383
383
  options?: UIMessageStreamResponseInit & UIMessageStreamOptions<UI_MESSAGE>,
384
- ): void;
384
+ ): Promise<void>;
385
385
 
386
386
  /**
387
387
  * Writes text delta output to a Node.js response-like object.
@@ -395,7 +395,10 @@ export interface StreamTextResult<
395
395
  * `pipeTextStreamToResponse` helpers from `'ai'` with `result.stream`
396
396
  * instead. This method will be removed in the next major release.
397
397
  */
398
- pipeTextStreamToResponse(response: ServerResponse, init?: ResponseInit): void;
398
+ pipeTextStreamToResponse(
399
+ response: ServerResponse,
400
+ init?: ResponseInit,
401
+ ): Promise<void>;
399
402
 
400
403
  /**
401
404
  * Converts the result to a streamed response object with a stream data part stream.
@@ -34,6 +34,7 @@ import { prepareTools } from '../prompt/prepare-tools';
34
34
  import type { Prompt } from '../prompt/prompt';
35
35
  import {
36
36
  getChunkTimeoutMs,
37
+ getFirstChunkTimeoutMs,
37
38
  getStepTimeoutMs,
38
39
  getTotalTimeoutMs,
39
40
  type RequestOptions,
@@ -157,28 +158,29 @@ const originalGenerateCallId = createIdGenerator({
157
158
  size: 24,
158
159
  });
159
160
 
160
- // chunk types that count as model output; used to distinguish empty
161
- // incomplete streams from incomplete streams with partial results.
162
- // exhaustive so that new chunk types must be classified explicitly:
161
+ // Chunk types that contain semantic model output. This classification is used
162
+ // for first-content and inter-content timeouts as well as to distinguish empty
163
+ // incomplete streams from incomplete streams with partial results. It is
164
+ // exhaustive so that new chunk types must be classified explicitly.
163
165
  const isOutputChunkType = {
164
166
  file: true,
165
- custom: true,
166
- source: true,
167
- 'text-start': true,
168
- 'text-end': true,
167
+ custom: false,
168
+ source: false,
169
+ 'text-start': false,
170
+ 'text-end': false,
169
171
  'text-delta': true,
170
- 'reasoning-start': true,
171
- 'reasoning-end': true,
172
+ 'reasoning-start': false,
173
+ 'reasoning-end': false,
172
174
  'reasoning-delta': true,
173
175
  'reasoning-file': true,
174
- 'tool-input-start': true,
175
- 'tool-input-end': true,
176
+ 'tool-input-start': false,
177
+ 'tool-input-end': false,
176
178
  'tool-input-delta': true,
177
- 'tool-approval-request': true,
178
- 'tool-approval-response': true,
179
+ 'tool-approval-request': false,
180
+ 'tool-approval-response': false,
179
181
  'tool-call': true,
180
- 'tool-result': true,
181
- 'tool-error': true,
182
+ 'tool-result': false,
183
+ 'tool-error': false,
182
184
  'tool-execution-end': false,
183
185
  'model-call-start': false,
184
186
  'model-call-response-metadata': false,
@@ -187,6 +189,26 @@ const isOutputChunkType = {
187
189
  raw: false,
188
190
  } as const satisfies Record<ExecuteToolsStreamPart['type'], boolean>;
189
191
 
192
+ function isOutputChunk(chunk: ExecuteToolsStreamPart): boolean {
193
+ if (!isOutputChunkType[chunk.type]) {
194
+ return false;
195
+ }
196
+
197
+ switch (chunk.type) {
198
+ case 'text-delta':
199
+ case 'reasoning-delta':
200
+ return chunk.text.length > 0;
201
+ case 'tool-input-delta':
202
+ return chunk.delta.length > 0;
203
+ case 'file':
204
+ case 'reasoning-file':
205
+ case 'tool-call':
206
+ return true;
207
+ default:
208
+ return false;
209
+ }
210
+ }
211
+
190
212
  export type StreamTextInclude = {
191
213
  /**
192
214
  * Whether to retain the request body in step results.
@@ -716,9 +738,12 @@ export function streamText<
716
738
  }): StreamTextResult<TOOLS, RUNTIME_CONTEXT, OUTPUT> {
717
739
  const totalTimeoutMs = getTotalTimeoutMs(timeout);
718
740
  const stepTimeoutMs = getStepTimeoutMs(timeout);
741
+ const firstChunkTimeoutMs = getFirstChunkTimeoutMs(timeout);
719
742
  const chunkTimeoutMs = getChunkTimeoutMs(timeout);
720
743
  const stepAbortController =
721
744
  stepTimeoutMs != null ? new AbortController() : undefined;
745
+ const firstChunkAbortController =
746
+ firstChunkTimeoutMs != null ? new AbortController() : undefined;
722
747
  const chunkAbortController =
723
748
  chunkTimeoutMs != null ? new AbortController() : undefined;
724
749
  const resolvedOnStart = onStart ?? experimental_onStart;
@@ -742,10 +767,13 @@ export function streamText<
742
767
  abortSignal,
743
768
  totalTimeoutMs,
744
769
  stepAbortController?.signal,
770
+ firstChunkAbortController?.signal,
745
771
  chunkAbortController?.signal,
746
772
  ),
747
773
  stepTimeoutMs,
748
774
  stepAbortController,
775
+ firstChunkTimeoutMs,
776
+ firstChunkAbortController,
749
777
  chunkTimeoutMs,
750
778
  chunkAbortController,
751
779
  instructions,
@@ -930,6 +958,10 @@ class DefaultStreamTextResult<
930
958
 
931
959
  private readonly addStream: (
932
960
  stream: ReadableStream<TextStreamPart<TOOLS>>,
961
+ callbacks?: {
962
+ onError?: (error: unknown) => void;
963
+ onCancel?: () => void;
964
+ },
933
965
  ) => void;
934
966
 
935
967
  private readonly closeStream: () => void;
@@ -951,6 +983,8 @@ class DefaultStreamTextResult<
951
983
  abortSignal,
952
984
  stepTimeoutMs,
953
985
  stepAbortController,
986
+ firstChunkTimeoutMs,
987
+ firstChunkAbortController,
954
988
  chunkTimeoutMs,
955
989
  chunkAbortController,
956
990
  instructions,
@@ -1000,6 +1034,8 @@ class DefaultStreamTextResult<
1000
1034
  abortSignal: AbortSignal | undefined;
1001
1035
  stepTimeoutMs: number | undefined;
1002
1036
  stepAbortController: AbortController | undefined;
1037
+ firstChunkTimeoutMs: number | undefined;
1038
+ firstChunkAbortController: AbortController | undefined;
1003
1039
  chunkTimeoutMs: number | undefined;
1004
1040
  chunkAbortController: AbortController | undefined;
1005
1041
  toolsContext: InferToolSetContext<TOOLS>;
@@ -1771,7 +1807,32 @@ class DefaultStreamTextResult<
1771
1807
  timeoutMs: stepTimeoutMs,
1772
1808
  });
1773
1809
 
1774
- // Set up chunk timeout tracking (will be reset on each chunk)
1810
+ // The first-content timeout is armed when the provider response stream
1811
+ // starts and is cleared by the first semantic output chunk.
1812
+ let firstChunkTimeoutId: ReturnType<typeof setTimeout> | undefined =
1813
+ undefined;
1814
+
1815
+ function startFirstChunkTimeout() {
1816
+ if (abortSignal?.aborted) {
1817
+ return;
1818
+ }
1819
+
1820
+ firstChunkTimeoutId = setAbortTimeout({
1821
+ abortController: firstChunkAbortController,
1822
+ label: 'First chunk',
1823
+ timeoutMs: firstChunkTimeoutMs,
1824
+ });
1825
+ }
1826
+
1827
+ function clearFirstChunkTimeout() {
1828
+ if (firstChunkTimeoutId != null) {
1829
+ clearTimeout(firstChunkTimeoutId);
1830
+ firstChunkTimeoutId = undefined;
1831
+ }
1832
+ }
1833
+
1834
+ // Chunk timeout tracking starts after semantic output begins and is
1835
+ // reset only by subsequent semantic output chunks.
1775
1836
  let chunkTimeoutId: ReturnType<typeof setTimeout> | undefined =
1776
1837
  undefined;
1777
1838
 
@@ -1799,12 +1860,24 @@ class DefaultStreamTextResult<
1799
1860
  }
1800
1861
  }
1801
1862
 
1863
+ function clearStepTimeouts() {
1864
+ clearStepTimeout();
1865
+ clearFirstChunkTimeout();
1866
+ clearChunkTimeout();
1867
+ }
1868
+
1869
+ function cleanupStepTimeouts() {
1870
+ abortSignal?.removeEventListener('abort', cleanupStepTimeouts);
1871
+ clearStepTimeouts();
1872
+ }
1873
+
1802
1874
  // The step's stream is registered lazily and consumed long after this
1803
- // function returns, so the step timer must stay armed past setup. When
1804
- // the merged abort signal fires (any step/chunk/total timeout or caller
1805
- // abort), drop both step-scoped timers so neither outlives the step.
1806
- abortSignal?.addEventListener('abort', clearStepTimeout);
1807
- abortSignal?.addEventListener('abort', clearChunkTimeout);
1875
+ // function returns, so its timers must stay armed past setup. When the
1876
+ // merged abort signal fires, drop all step-scoped timers so none
1877
+ // outlives the step.
1878
+ abortSignal?.addEventListener('abort', cleanupStepTimeouts, {
1879
+ once: true,
1880
+ });
1808
1881
 
1809
1882
  try {
1810
1883
  stepFinish = new DelayedPromise<void>();
@@ -1967,6 +2040,8 @@ class DefaultStreamTextResult<
1967
2040
  ),
1968
2041
  );
1969
2042
 
2043
+ startFirstChunkTimeout();
2044
+
1970
2045
  const streamAfterToolCallbackInvocation =
1971
2046
  invokeToolCallbacksFromStream({
1972
2047
  stream: languageModelStream,
@@ -2076,8 +2151,6 @@ class DefaultStreamTextResult<
2076
2151
  TextStreamPart<TOOLS>
2077
2152
  >({
2078
2153
  async transform(chunk, controller): Promise<void> {
2079
- resetChunkTimeout();
2080
-
2081
2154
  if (chunk.type === 'model-call-start') {
2082
2155
  warnings = chunk.warnings;
2083
2156
  return; // stream start chunks are sent immediately and do not count as first chunk
@@ -2096,8 +2169,14 @@ class DefaultStreamTextResult<
2096
2169
 
2097
2170
  const chunkType = chunk.type;
2098
2171
 
2099
- if (isOutputChunkType[chunkType]) {
2172
+ if (isOutputChunk(chunk)) {
2173
+ if (!hasReceivedOutputChunk) {
2174
+ // Clear before forwarding the first output so a timeout
2175
+ // cannot race with already-visible generated content.
2176
+ clearFirstChunkTimeout();
2177
+ }
2100
2178
  hasReceivedOutputChunk = true;
2179
+ resetChunkTimeout();
2101
2180
  }
2102
2181
 
2103
2182
  switch (chunkType) {
@@ -2217,8 +2296,7 @@ class DefaultStreamTextResult<
2217
2296
  }),
2218
2297
  });
2219
2298
 
2220
- clearStepTimeout();
2221
- clearChunkTimeout();
2299
+ cleanupStepTimeouts();
2222
2300
  self.closeStream();
2223
2301
  return;
2224
2302
  }
@@ -2298,9 +2376,8 @@ class DefaultStreamTextResult<
2298
2376
  }
2299
2377
  }
2300
2378
 
2301
- // Clear the step and chunk timeouts before the next step is started
2302
- clearStepTimeout();
2303
- clearChunkTimeout();
2379
+ // Clear this step's timeouts before the next step is started.
2380
+ cleanupStepTimeouts();
2304
2381
 
2305
2382
  if (
2306
2383
  // Continue if:
@@ -2345,12 +2422,15 @@ class DefaultStreamTextResult<
2345
2422
  },
2346
2423
  }),
2347
2424
  ),
2425
+ {
2426
+ onError: cleanupStepTimeouts,
2427
+ onCancel: cleanupStepTimeouts,
2428
+ },
2348
2429
  );
2349
2430
  } catch (error) {
2350
2431
  // Setup failed before the stream was registered, so neither the
2351
2432
  // stream's flush nor an abort will clear the timers — clear them here.
2352
- clearStepTimeout();
2353
- clearChunkTimeout();
2433
+ cleanupStepTimeouts();
2354
2434
  throw error;
2355
2435
  }
2356
2436
  }
@@ -2678,7 +2758,7 @@ class DefaultStreamTextResult<
2678
2758
  ...init
2679
2759
  }: UIMessageStreamResponseInit & UIMessageStreamOptions<UI_MESSAGE> = {},
2680
2760
  ) {
2681
- pipeUIMessageStreamToResponse({
2761
+ return pipeUIMessageStreamToResponse({
2682
2762
  response,
2683
2763
  stream: this.toUIMessageStream({
2684
2764
  originalMessages,
@@ -2696,7 +2776,7 @@ class DefaultStreamTextResult<
2696
2776
  }
2697
2777
 
2698
2778
  pipeTextStreamToResponse(response: ServerResponse, init?: ResponseInit) {
2699
- pipeTextStreamToResponse({
2779
+ return pipeTextStreamToResponse({
2700
2780
  response,
2701
2781
  stream: this.textStream,
2702
2782
  ...init,
@@ -10,6 +10,7 @@ export type CallSettings = LanguageModelCallOptions &
10
10
  export {
11
11
  getTotalTimeoutMs,
12
12
  getStepTimeoutMs,
13
+ getFirstChunkTimeoutMs,
13
14
  getChunkTimeoutMs,
14
15
  getToolTimeoutMs,
15
16
  } from './request-options';