ai 7.0.118 → 7.0.120

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +83 -31
  4. package/dist/index.js.map +1 -1
  5. package/dist/internal/index.js +3 -1
  6. package/dist/internal/index.js.map +1 -1
  7. package/dist/test/index.d.ts +5 -1
  8. package/dist/test/index.js +4 -0
  9. package/dist/test/index.js.map +1 -1
  10. package/docs/02-foundations/02-providers-and-models.mdx +1 -0
  11. package/docs/02-getting-started/00-choosing-a-provider.mdx +4 -4
  12. package/docs/02-getting-started/02-nextjs-app-router.mdx +2 -2
  13. package/docs/02-getting-started/03-nextjs-pages-router.mdx +2 -2
  14. package/docs/02-getting-started/04-svelte.mdx +6 -6
  15. package/docs/02-getting-started/05-nuxt.mdx +6 -6
  16. package/docs/02-getting-started/06-nodejs.mdx +2 -2
  17. package/docs/02-getting-started/07-expo.mdx +2 -2
  18. package/docs/02-getting-started/08-tanstack-start.mdx +2 -2
  19. package/docs/03-agents/06-policy-tool-approvals.mdx +2 -2
  20. package/docs/03-agents/07-workflow-agent.mdx +10 -10
  21. package/docs/03-ai-sdk-core/20-prompt-engineering.mdx +1 -1
  22. package/docs/03-ai-sdk-core/26-reasoning.mdx +1 -1
  23. package/docs/03-ai-sdk-core/35-image-generation.mdx +38 -0
  24. package/docs/03-ai-sdk-core/36-transcription.mdx +1 -0
  25. package/docs/03-ai-sdk-core/45-provider-management.mdx +4 -4
  26. package/docs/03-ai-sdk-harnesses/02-harness-agent.mdx +51 -9
  27. package/docs/04-ai-sdk-ui/02-chatbot.mdx +1 -1
  28. package/docs/07-reference/01-ai-sdk-core/61-wrap-image-model.mdx +16 -1
  29. package/docs/07-reference/04-ai-sdk-workflow/01-workflow-agent.mdx +8 -8
  30. package/package.json +12 -12
  31. package/src/middleware/wrap-image-model.ts +13 -0
  32. package/src/telemetry/tracing-channel-publisher.ts +1 -0
  33. package/src/test/mock-image-model-v4.ts +8 -0
  34. package/src/ui/chat.ts +36 -7
  35. package/src/ui/process-ui-message-stream.ts +9 -10
  36. package/src/util/write-to-server-response.ts +41 -2
@@ -79,7 +79,7 @@ import { customProvider, gateway } from 'ai';
79
79
  export const anthropic = customProvider({
80
80
  languageModels: {
81
81
  opus: gateway('anthropic/claude-opus-5.5'),
82
- sonnet: gateway('anthropic/claude-sonnet-5'),
82
+ sonnet: gateway('anthropic/claude-sonnet-5.5'),
83
83
  haiku: gateway('anthropic/claude-haiku-4.5'),
84
84
  },
85
85
  fallbackProvider: gateway,
@@ -100,7 +100,7 @@ import {
100
100
 
101
101
  export const myProvider = customProvider({
102
102
  languageModels: {
103
- 'text-medium': gateway('anthropic/claude-sonnet-5'),
103
+ 'text-medium': gateway('anthropic/claude-sonnet-5.5'),
104
104
  'text-small': gateway('openai/gpt-6-luna'),
105
105
  'reasoning-medium': wrapLanguageModel({
106
106
  model: gateway('openai/gpt-6-astra'),
@@ -154,7 +154,7 @@ const myOpenAI = customProvider({
154
154
  // custom provider with skills interface:
155
155
  const myAnthropic = customProvider({
156
156
  languageModels: {
157
- sonnet: anthropic('claude-sonnet-5'),
157
+ sonnet: anthropic('claude-sonnet-5-5'),
158
158
  },
159
159
  skills: anthropic.skills(),
160
160
  });
@@ -330,7 +330,7 @@ import { createProviderRegistry, customProvider, uploadSkill } from 'ai';
330
330
 
331
331
  const registry = createProviderRegistry({
332
332
  anthropic: customProvider({
333
- languageModels: { sonnet: anthropic('claude-sonnet-5') },
333
+ languageModels: { sonnet: anthropic('claude-sonnet-5-5') },
334
334
  skills: anthropic.skills(),
335
335
  }),
336
336
  });
@@ -138,6 +138,39 @@ Harness runtimes execute their built-in tools internally. For those tools,
138
138
  lifecycle after the runtime reports the result. The callbacks are still
139
139
  delivered before the tool result is published to the result stream.
140
140
 
141
+ ## Runtime Context
142
+
143
+ Set `runtimeContext` on `HarnessAgent` to make application context available to
144
+ every turn. Use `prepareCall` to replace it for each new prompt turn. The
145
+ prepared context is available through `generate()` and `stream()` and remains
146
+ the same during in-memory continuations with `continueGenerate()` and
147
+ `continueStream()`. Lifecycle callbacks receive the full context, and it is
148
+ also available on each result's `finalStep.runtimeContext`.
149
+
150
+ ```ts
151
+ const agent = new HarnessAgent({
152
+ harness: claudeCode,
153
+ runtimeContext: {
154
+ conversationId: 'conversation-123',
155
+ userId: 'user-456',
156
+ },
157
+ telemetry: {
158
+ includeRuntimeContext: {
159
+ conversationId: true,
160
+ },
161
+ },
162
+ onEnd: ({ runtimeContext }) => {
163
+ console.log(runtimeContext.userId);
164
+ },
165
+ });
166
+ ```
167
+
168
+ Runtime context is excluded from telemetry by default. Use
169
+ `telemetry.includeRuntimeContext` to allowlist individual top-level properties;
170
+ only properties set to `true` are sent to telemetry integrations. Lifecycle
171
+ callbacks and results still receive the full context, so avoid storing secrets
172
+ that those consumers should not access.
173
+
141
174
  ## Generate Structured Output
142
175
 
143
176
  Set `output` when constructing `HarnessAgent` to require the same typed output
@@ -227,7 +260,7 @@ HTTP routes that need multi-turn continuity.
227
260
  ## Change Settings Between Turns
228
261
 
229
262
  Use `callOptionsSchema` and `prepareCall` to derive `model`, `skills`,
230
- `instructions`, and `tools` for each new turn. This follows the same
263
+ `instructions`, `tools`, and `runtimeContext` for each new turn. This follows the same
231
264
  call-options pattern as `ToolLoopAgent`:
232
265
 
233
266
  ```ts
@@ -253,6 +286,7 @@ const agent = new HarnessAgent({
253
286
  ...call,
254
287
  model: options.useCheaperModel ? 'claude-haiku-4-5' : undefined,
255
288
  instructions: `Work as the ${options.area} specialist.`,
289
+ runtimeContext: { area: options.area },
256
290
  skills: [options.area === 'frontend' ? frontendSkill : backendSkill],
257
291
  tools: options.enablePolicyTool ? { getPolicy } : undefined,
258
292
  }),
@@ -390,6 +424,7 @@ const session = await agent.createSession({
390
424
  sandboxSession,
391
425
  // Rebind this when the suspended turn used host-only tool context:
392
426
  toolsContext,
427
+ runtimeContext,
393
428
  });
394
429
 
395
430
  const result = await agent.continueStream({ session });
@@ -398,10 +433,12 @@ const result = await agent.continueStream({ session });
398
433
  Use `continueStream()` for incremental output, or `continueGenerate()` to drain
399
434
  the continued turn and return a `GenerateTextResult`.
400
435
 
401
- `toolsContext` is intentionally not stored in continuation state because it can
402
- contain credentials or non-serializable host objects. When a suspended turn
403
- used static or `prepareCall`-derived tool context, pass the same per-tool map to
404
- `createSession` to rebind it in the process that continues the turn.
436
+ `toolsContext` and `runtimeContext` are intentionally not stored in
437
+ continuation state because they can contain credentials or non-serializable
438
+ host objects. When a suspended turn used context derived by `prepareCall`,
439
+ pass the same values to `createSession` to rebind them in the process that
440
+ continues the turn. After a process resume without rebinding, the continuation
441
+ uses the agent's constructor-level `runtimeContext` and `toolsContext`.
405
442
 
406
443
  ## Stop After a Harness Step
407
444
 
@@ -481,9 +518,10 @@ const agent = new HarnessAgent({
481
518
  ```
482
519
 
483
520
  `workDir` is optional. When provided, it must be relative to the sandbox's
484
- default working directory and is used as the session working directory. When
485
- omitted, regular sessions use the default `<harnessId>-<sessionId>` directory,
486
- while `onBootstrap` receives the sandbox's default working directory.
521
+ default working directory and is used as the session working directory. Set it
522
+ to `'.'` to use the sandbox's default working directory itself. When omitted,
523
+ regular sessions use the default `<harnessId>-<sessionId>` directory, while
524
+ `onBootstrap` receives the sandbox's default working directory.
487
525
 
488
526
  ## Prepare Reusable Sandboxes
489
527
 
@@ -555,6 +593,8 @@ supplied, their values must match.
555
593
  - `stopWhen`: condition(s) for finishing a result slice after a completed
556
594
  harness tool step that can continue into another model step.
557
595
  - `tools`: AI SDK tools executed by the host when the harness calls them.
596
+ - `runtimeContext`: application context forwarded to lifecycle callbacks and
597
+ results for every turn; `prepareCall` can replace it for a new prompt turn.
558
598
  - `activeTools`: allowlist of built-in and host-executed tools the harness can
559
599
  call.
560
600
  - `inactiveTools`: denylist of built-in and host-executed tools the harness
@@ -563,7 +603,9 @@ supplied, their values must match.
563
603
  - `permissionMode`: built-in tool permission mode.
564
604
  - `toolApproval`: approval status map for host-executed tools.
565
605
  - `sandboxConfig`: sandbox working-directory and lifecycle hook configuration.
566
- - `telemetry`, `debug`, and `onLog`: observability and diagnostics.
606
+ - `telemetry`, `debug`, and `onLog`: observability and diagnostics. Use
607
+ `telemetry.includeRuntimeContext` to allowlist runtime context properties for
608
+ telemetry.
567
609
 
568
610
  Telemetry reports each turn's resolved model, instructions, and active
569
611
  host-defined tools. Skills are adapter context and do not have a corresponding
@@ -947,7 +947,7 @@ Check out the [stream protocol guide](/docs/ai-sdk-ui/stream-protocol) for more
947
947
  ## Reasoning
948
948
 
949
949
  Some models such as DeepSeek `deepseek-r1`
950
- and Anthropic `claude-sonnet-5` support reasoning tokens.
950
+ and Anthropic `claude-sonnet-5-5` support reasoning tokens.
951
951
  These tokens are typically sent before the message content.
952
952
  You can forward them to the client with the `sendReasoning` option:
953
953
 
@@ -61,4 +61,19 @@ const { image } = await generateImage({
61
61
 
62
62
  ### Returns
63
63
 
64
- A new `ImageModelV4` instance with middleware applied.
64
+ A new `ImageModelV4` instance with middleware applied. The wrapped model
65
+ preserves the original model's `supportsFileInputs` and `supportsMaskInputs`
66
+ capabilities by default. Middleware can override them with
67
+ `overrideSupportsFileInputs` and `overrideSupportsMaskInputs`. Returning
68
+ `undefined` from an override intentionally marks that capability as unknown.
69
+
70
+ ```ts
71
+ const model = wrapImageModel({
72
+ model: originalModel,
73
+ middleware: {
74
+ specificationVersion: 'v4',
75
+ overrideSupportsFileInputs: () => true,
76
+ overrideSupportsMaskInputs: () => false,
77
+ },
78
+ });
79
+ ```
@@ -17,7 +17,7 @@ import { tool } from 'ai';
17
17
  import { z } from 'zod';
18
18
 
19
19
  const agent = new WorkflowAgent({
20
- model: 'anthropic/claude-sonnet-5',
20
+ model: 'anthropic/claude-sonnet-5.5',
21
21
  instructions: 'You are a helpful assistant.',
22
22
  tools: {
23
23
  weather: tool({
@@ -71,7 +71,7 @@ To see `WorkflowAgent` in action, check out [these examples](#examples).
71
71
  type: 'LanguageModel',
72
72
  isRequired: true,
73
73
  description:
74
- "The language model to use. A string compatible with the Vercel AI Gateway (e.g., 'anthropic/claude-sonnet-5') or a provider instance (e.g., `openai('gpt-6-astra')`).",
74
+ "The language model to use. A string compatible with the Vercel AI Gateway (e.g., 'anthropic/claude-sonnet-5.5') or a provider instance (e.g., `openai('gpt-6-astra')`).",
75
75
  },
76
76
  {
77
77
  name: 'instructions',
@@ -808,7 +808,7 @@ Infers the UI message type for a `WorkflowAgent` instance. Optionally accepts a
808
808
  import { WorkflowAgent, InferWorkflowAgentUIMessage } from '@ai-sdk/workflow';
809
809
 
810
810
  const agent = new WorkflowAgent({
811
- model: 'anthropic/claude-sonnet-5',
811
+ model: 'anthropic/claude-sonnet-5.5',
812
812
  tools: { weather: weatherTool },
813
813
  });
814
814
 
@@ -835,7 +835,7 @@ import { tool } from 'ai';
835
835
  import { z } from 'zod';
836
836
 
837
837
  const agent = new WorkflowAgent({
838
- model: 'anthropic/claude-sonnet-5',
838
+ model: 'anthropic/claude-sonnet-5.5',
839
839
  instructions: 'You are a helpful assistant.',
840
840
  tools: {
841
841
  weather: tool({
@@ -890,7 +890,7 @@ export async function chat(messages: UIMessage[]) {
890
890
  const modelMessages = await convertToModelMessages(messages);
891
891
 
892
892
  const agent = new WorkflowAgent({
893
- model: 'anthropic/claude-sonnet-5',
893
+ model: 'anthropic/claude-sonnet-5.5',
894
894
  instructions: 'You are a flight booking assistant.',
895
895
  tools: {
896
896
  searchFlights: tool({
@@ -937,7 +937,7 @@ import { WorkflowAgent, Output } from '@ai-sdk/workflow';
937
937
  import { z } from 'zod';
938
938
 
939
939
  const analysisAgent = new WorkflowAgent({
940
- model: 'anthropic/claude-sonnet-5',
940
+ model: 'anthropic/claude-sonnet-5.5',
941
941
  });
942
942
 
943
943
  const result = await analysisAgent.stream({
@@ -977,7 +977,7 @@ import { tool } from 'ai';
977
977
  import { z } from 'zod';
978
978
 
979
979
  const agent = new WorkflowAgent({
980
- model: 'anthropic/claude-sonnet-5',
980
+ model: 'anthropic/claude-sonnet-5.5',
981
981
  experimental_toolApprovalSecret: {
982
982
  environmentVariable: 'TOOL_APPROVAL_SECRET',
983
983
  },
@@ -1009,7 +1009,7 @@ serialize it. A stream-level reference overrides the constructor value.
1009
1009
  import { WorkflowAgent } from '@ai-sdk/workflow';
1010
1010
 
1011
1011
  const agent = new WorkflowAgent({
1012
- model: 'anthropic/claude-sonnet-5',
1012
+ model: 'anthropic/claude-sonnet-5.5',
1013
1013
  tools: { weather: weatherTool },
1014
1014
 
1015
1015
  // Agent-wide callbacks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.118",
3
+ "version": "7.0.120",
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,20 +42,20 @@
42
42
  }
43
43
  },
44
44
  "dependencies": {
45
- "@ai-sdk/gateway": "4.0.96",
46
- "@ai-sdk/provider": "4.0.18",
47
- "@ai-sdk/provider-utils": "5.0.49"
45
+ "@ai-sdk/gateway": "4.0.98",
46
+ "@ai-sdk/provider": "4.0.19",
47
+ "@ai-sdk/provider-utils": "5.0.50"
48
48
  },
49
49
  "devDependencies": {
50
- "@ai-sdk/amazon-bedrock": "5.0.97",
51
- "@ai-sdk/deepseek": "3.0.54",
52
- "@ai-sdk/google": "4.0.82",
53
- "@ai-sdk/groq": "4.0.50",
54
- "@ai-sdk/huggingface": "2.0.57",
55
- "@ai-sdk/moonshotai": "3.0.58",
56
- "@ai-sdk/openai": "4.0.78",
50
+ "@ai-sdk/amazon-bedrock": "5.0.99",
51
+ "@ai-sdk/deepseek": "3.0.55",
52
+ "@ai-sdk/google": "4.0.84",
53
+ "@ai-sdk/groq": "4.0.51",
54
+ "@ai-sdk/huggingface": "2.0.58",
55
+ "@ai-sdk/moonshotai": "3.0.59",
56
+ "@ai-sdk/openai": "4.0.79",
57
57
  "@ai-sdk/test-server": "2.0.2",
58
- "@ai-sdk/xai": "5.0.10",
58
+ "@ai-sdk/xai": "5.0.11",
59
59
  "@edge-runtime/vm": "^5.0.0",
60
60
  "@smithy/eventstream-codec": "^4.3.3",
61
61
  "@smithy/util-utf8": "^4.3.3",
@@ -48,6 +48,8 @@ const doWrap = ({
48
48
  overrideProvider,
49
49
  overrideModelId,
50
50
  overrideMaxImagesPerCall,
51
+ overrideSupportsFileInputs,
52
+ overrideSupportsMaskInputs,
51
53
  },
52
54
  modelId,
53
55
  providerId,
@@ -71,11 +73,22 @@ const doWrap = ({
71
73
  ? maxImagesPerCallRaw.bind(model)
72
74
  : maxImagesPerCallRaw;
73
75
 
76
+ const supportsFileInputs =
77
+ overrideSupportsFileInputs !== undefined
78
+ ? overrideSupportsFileInputs({ model })
79
+ : model.supportsFileInputs;
80
+ const supportsMaskInputs =
81
+ overrideSupportsMaskInputs !== undefined
82
+ ? overrideSupportsMaskInputs({ model })
83
+ : model.supportsMaskInputs;
84
+
74
85
  return {
75
86
  specificationVersion: 'v4',
76
87
  provider: providerId ?? overrideProvider?.({ model }) ?? model.provider,
77
88
  modelId: modelId ?? overrideModelId?.({ model }) ?? model.modelId,
78
89
  maxImagesPerCall,
90
+ supportsFileInputs,
91
+ supportsMaskInputs,
79
92
  async doGenerate(
80
93
  params: ImageModelV4CallOptions,
81
94
  ): Promise<ImageModelV4Result> {
@@ -136,6 +136,7 @@ export function openTelemetryChannelSpanContext({
136
136
  completion: PromiseLike<unknown>;
137
137
  }): TracingChannelContext | undefined {
138
138
  if (!isNodeRuntime()) {
139
+ Promise.resolve(completion).catch(() => {});
139
140
  return undefined;
140
141
  }
141
142
 
@@ -6,6 +6,8 @@ export class MockImageModelV4 implements ImageModelV4 {
6
6
  readonly provider: ImageModelV4['provider'];
7
7
  readonly modelId: ImageModelV4['modelId'];
8
8
  readonly maxImagesPerCall: ImageModelV4['maxImagesPerCall'];
9
+ readonly supportsFileInputs: ImageModelV4['supportsFileInputs'];
10
+ readonly supportsMaskInputs: ImageModelV4['supportsMaskInputs'];
9
11
 
10
12
  doGenerate: ImageModelV4['doGenerate'];
11
13
 
@@ -13,16 +15,22 @@ export class MockImageModelV4 implements ImageModelV4 {
13
15
  provider = 'mock-provider',
14
16
  modelId = 'mock-model-id',
15
17
  maxImagesPerCall = 1,
18
+ supportsFileInputs,
19
+ supportsMaskInputs,
16
20
  doGenerate = notImplemented,
17
21
  }: {
18
22
  provider?: ImageModelV4['provider'];
19
23
  modelId?: ImageModelV4['modelId'];
20
24
  maxImagesPerCall?: ImageModelV4['maxImagesPerCall'];
25
+ supportsFileInputs?: ImageModelV4['supportsFileInputs'];
26
+ supportsMaskInputs?: ImageModelV4['supportsMaskInputs'];
21
27
  doGenerate?: ImageModelV4['doGenerate'];
22
28
  } = {}) {
23
29
  this.provider = provider;
24
30
  this.modelId = modelId;
25
31
  this.maxImagesPerCall = maxImagesPerCall;
32
+ this.supportsFileInputs = supportsFileInputs;
33
+ this.supportsMaskInputs = supportsMaskInputs;
26
34
  this.doGenerate = doGenerate;
27
35
  }
28
36
  }
package/src/ui/chat.ts CHANGED
@@ -263,6 +263,9 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
263
263
  private pendingApprovalMessageId: string | undefined;
264
264
  private activeResponse: ActiveResponse<UI_MESSAGE> | undefined = undefined;
265
265
  private activeResumeRequest: ActiveResumeRequest | undefined = undefined;
266
+ private resumableStreamState:
267
+ | StreamingUIMessageState<UI_MESSAGE>
268
+ | undefined = undefined;
266
269
  private jobExecutor = new SerialJobExecutor();
267
270
 
268
271
  constructor({
@@ -723,6 +726,10 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
723
726
  trigger: 'submit-message' | 'resume-stream' | 'regenerate-message';
724
727
  messageId?: string;
725
728
  } & ChatRequestOptions) {
729
+ if (trigger !== 'resume-stream') {
730
+ this.resumableStreamState = undefined;
731
+ }
732
+
726
733
  const abortController = new AbortController();
727
734
  const activeResumeRequest =
728
735
  trigger === 'resume-stream' ? { abortController } : undefined;
@@ -760,6 +767,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
760
767
  await reconnect?.cancel().catch(() => {});
761
768
  if (isCurrentRequest()) {
762
769
  this.setStatus({ status: 'ready' });
770
+ this.resumableStreamState = undefined;
763
771
  }
764
772
  clearActiveResumeRequest();
765
773
  return;
@@ -767,6 +775,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
767
775
 
768
776
  if (reconnect == null) {
769
777
  this.setStatus({ status: 'ready' });
778
+ this.resumableStreamState = undefined;
770
779
  clearActiveResumeRequest();
771
780
  return; // no active stream found, so we do not resume
772
781
  }
@@ -779,6 +788,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
779
788
  ) {
780
789
  if (isCurrentRequest()) {
781
790
  this.setStatus({ status: 'ready' });
791
+ this.resumableStreamState = undefined;
782
792
  }
783
793
  clearActiveResumeRequest();
784
794
  return;
@@ -820,13 +830,17 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
820
830
 
821
831
  try {
822
832
  const response = {
823
- state: createStreamingUIMessageState({
824
- lastMessage:
825
- trigger === 'resume-stream' || trigger === 'regenerate-message'
826
- ? undefined
827
- : this.state.snapshot(responseMessage),
828
- messageId: this.generateId(),
829
- }),
833
+ state:
834
+ trigger === 'resume-stream' && this.resumableStreamState != null
835
+ ? this.resumableStreamState
836
+ : createStreamingUIMessageState({
837
+ lastMessage:
838
+ trigger === 'resume-stream' ||
839
+ trigger === 'regenerate-message'
840
+ ? undefined
841
+ : this.state.snapshot(responseMessage),
842
+ messageId: this.generateId(),
843
+ }),
830
844
  abortController,
831
845
  } as ActiveResponse<UI_MESSAGE>;
832
846
 
@@ -916,12 +930,18 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
916
930
  if (isAbort) {
917
931
  if (isCurrentRequest()) {
918
932
  this.setStatus({ status: 'ready' });
933
+ if (this.resumableStreamState === response.state) {
934
+ this.resumableStreamState = undefined;
935
+ }
919
936
  }
920
937
  return null;
921
938
  }
922
939
 
923
940
  if (isCurrentRequest()) {
924
941
  this.setStatus({ status: 'ready' });
942
+ if (this.resumableStreamState === response.state) {
943
+ this.resumableStreamState = undefined;
944
+ }
925
945
  }
926
946
  } catch (err) {
927
947
  // Ignore abort errors as they are expected.
@@ -929,6 +949,9 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
929
949
  isAbort = true;
930
950
  if (isCurrentRequest()) {
931
951
  this.setStatus({ status: 'ready' });
952
+ if (this.resumableStreamState === activeResponse?.state) {
953
+ this.resumableStreamState = undefined;
954
+ }
932
955
  }
933
956
  return null;
934
957
  }
@@ -948,6 +971,12 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
948
971
  isDisconnect = true;
949
972
  }
950
973
 
974
+ if (isDisconnect) {
975
+ this.resumableStreamState = activeResponse?.state;
976
+ } else if (this.resumableStreamState === activeResponse?.state) {
977
+ this.resumableStreamState = undefined;
978
+ }
979
+
951
980
  if (this.onError && err instanceof Error) {
952
981
  this.onError(err);
953
982
  }
@@ -31,8 +31,6 @@ import {
31
31
  type UIMessage,
32
32
  type UIMessagePart,
33
33
  } from './ui-messages';
34
- import { warnIfUIMessageHasDeprecatedRawInput } from './warn-if-ui-message-has-deprecated-raw-input';
35
-
36
34
  export type StreamingUIMessageState<UI_MESSAGE extends UIMessage> = {
37
35
  message: UI_MESSAGE;
38
36
  activeTextParts: Record<string, TextUIPart>;
@@ -732,15 +730,12 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
732
730
  toolCallId: chunk.toolCallId,
733
731
  toolName: chunk.toolName,
734
732
  state: 'output-error',
735
- input: undefined,
736
- rawInput: chunk.input,
733
+ input: chunk.input,
737
734
  errorText: chunk.errorText,
738
735
  providerExecuted: chunk.providerExecuted,
739
736
  providerMetadata: chunk.providerMetadata,
740
737
  toolMetadata: chunk.toolMetadata,
741
738
  });
742
-
743
- warnIfUIMessageHasDeprecatedRawInput([state.message]);
744
739
  }
745
740
 
746
741
  write();
@@ -821,7 +816,8 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
821
816
  providerExecuted: chunk.providerExecuted,
822
817
  providerMetadata: chunk.providerMetadata,
823
818
  title: toolInvocation.title,
824
- toolMetadata: toolInvocation.toolMetadata,
819
+ toolMetadata:
820
+ chunk.toolMetadata ?? toolInvocation.toolMetadata,
825
821
  },
826
822
  toolInvocation,
827
823
  );
@@ -837,7 +833,8 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
837
833
  preliminary: chunk.preliminary,
838
834
  providerMetadata: chunk.providerMetadata,
839
835
  title: toolInvocation.title,
840
- toolMetadata: toolInvocation.toolMetadata,
836
+ toolMetadata:
837
+ chunk.toolMetadata ?? toolInvocation.toolMetadata,
841
838
  },
842
839
  toolInvocation as ToolUIPart<InferUIMessageTools<UI_MESSAGE>>,
843
840
  );
@@ -861,7 +858,8 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
861
858
  providerExecuted: chunk.providerExecuted,
862
859
  providerMetadata: chunk.providerMetadata,
863
860
  title: toolInvocation.title,
864
- toolMetadata: toolInvocation.toolMetadata,
861
+ toolMetadata:
862
+ chunk.toolMetadata ?? toolInvocation.toolMetadata,
865
863
  },
866
864
  toolInvocation,
867
865
  );
@@ -877,7 +875,8 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
877
875
  providerExecuted: chunk.providerExecuted,
878
876
  providerMetadata: chunk.providerMetadata,
879
877
  title: toolInvocation.title,
880
- toolMetadata: toolInvocation.toolMetadata,
878
+ toolMetadata:
879
+ chunk.toolMetadata ?? toolInvocation.toolMetadata,
881
880
  },
882
881
  toolInvocation as ToolUIPart<InferUIMessageTools<UI_MESSAGE>>,
883
882
  );
@@ -6,6 +6,10 @@ type FlushableServerResponse = ServerResponse & {
6
6
 
7
7
  /**
8
8
  * Writes the content of a stream to a server response.
9
+ *
10
+ * When the client disconnects before the stream has been fully written
11
+ * (premature `close`), the stream is cancelled so that upstream resources
12
+ * can be released, and no further chunks are written.
9
13
  */
10
14
  export function writeToServerResponse({
11
15
  response,
@@ -32,11 +36,36 @@ export function writeToServerResponse({
32
36
  }
33
37
 
34
38
  const reader = stream.getReader();
39
+
40
+ // Detect client disconnects. `close` also fires after a regular `end()`;
41
+ // `writableFinished` distinguishes the two cases.
42
+ let clientDisconnected = false;
43
+ let onDisconnect: (() => void) | undefined;
44
+ const handleClose = () => {
45
+ if (response.writableFinished) {
46
+ return;
47
+ }
48
+
49
+ clientDisconnected = true;
50
+ onDisconnect?.();
51
+
52
+ // cancelling the reader resolves a pending read with `done: true`:
53
+ reader.cancel(new Error('Client disconnected.')).catch(() => {});
54
+ };
55
+ response.once('close', handleClose);
56
+
57
+ // The response may have been destroyed before the close listener was
58
+ // registered. Cancel immediately instead of waiting indefinitely for the
59
+ // next stream chunk.
60
+ if (response.destroyed) {
61
+ handleClose();
62
+ }
63
+
35
64
  const read = async () => {
36
65
  try {
37
66
  while (true) {
38
67
  const { done, value } = await reader.read();
39
- if (done) break;
68
+ if (done || clientDisconnected || response.destroyed) break;
40
69
 
41
70
  // Respect backpressure: if write() returns false, wait for 'drain' event
42
71
  const canContinue = response.write(value);
@@ -47,12 +76,22 @@ export function writeToServerResponse({
47
76
 
48
77
  if (!canContinue) {
49
78
  await new Promise<void>(resolve => {
79
+ // don't wait for `drain` on a disconnected response:
80
+ onDisconnect = resolve;
50
81
  response.once('drain', resolve);
51
82
  });
83
+ onDisconnect = undefined;
52
84
  }
53
85
  }
54
86
  } finally {
55
- response.end();
87
+ response.off('close', handleClose);
88
+
89
+ if (clientDisconnected || response.destroyed) {
90
+ // release the stream; ending a destroyed response is not possible:
91
+ reader.cancel().catch(() => {});
92
+ } else {
93
+ response.end();
94
+ }
56
95
  }
57
96
  };
58
97