ai 7.0.104 → 7.0.106

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.
@@ -11,8 +11,8 @@ is one state, not a batch of unrelated inputs.
11
11
 
12
12
  This API and the evaluation model specification are experimental and may change
13
13
  in patch releases. Pass an evaluation model instance, resolve a model through a
14
- provider registry, or use a string ID with an evaluation-capable default provider.
15
- Evaluation does not fall back to Vercel AI Gateway.
14
+ provider registry, or use a string ID. Strings resolve through Vercel AI Gateway
15
+ unless you configure an evaluation-capable default provider.
16
16
 
17
17
  ```ts
18
18
  import { experimental_evaluate, type Experimental_EvaluationModel } from 'ai';
@@ -125,8 +125,26 @@ evaluation models.
125
125
 
126
126
  ### Default-provider strings
127
127
 
128
- To use strings directly, configure the default provider once at application
129
- startup:
128
+ String IDs use Vercel AI Gateway by default. Configure Gateway authentication
129
+ with `AI_GATEWAY_API_KEY` or Vercel OIDC, then pass a Gateway model ID:
130
+
131
+ ```ts
132
+ import { experimental_evaluate } from 'ai';
133
+
134
+ const result = await experimental_evaluate({
135
+ model: 'typesafe-ai/jev-latest',
136
+ state: 'I was charged twice. Please refund the extra charge.',
137
+ questions: {
138
+ refund: {
139
+ type: 'boolean',
140
+ instructions: 'Is the customer asking for a refund?',
141
+ },
142
+ },
143
+ });
144
+ ```
145
+
146
+ To resolve strings through your own provider or aliases, configure the default
147
+ provider once at application startup:
130
148
 
131
149
  ```ts
132
150
  // Using the registry above, expose an alias through a custom provider:
@@ -152,9 +170,10 @@ also resolve through this global default. Prefer model instances in aliases to
152
170
  avoid resolution cycles. Global configuration affects other AI SDK functions
153
171
  too; avoid changing it per request in a shared process.
154
172
 
155
- Strings require an explicitly configured provider with an `evaluationModel`
156
- method. There is no assumed Gateway evaluation support. Missing providers throw
157
- `NoSuchProviderError`; missing models or evaluation capabilities throw
173
+ An explicitly configured default provider must expose an `evaluationModel`
174
+ method; Gateway is used only when no default provider is configured. Missing
175
+ registry providers throw `NoSuchProviderError`; missing models or evaluation
176
+ capabilities throw
158
177
  `NoSuchModelError` with `modelType: 'evaluationModel'`. Unsupported model versions
159
178
  throw `UnsupportedModelVersionError`, including models returned by registries.
160
179
  The stable `ProviderV4` and `ProviderRegistryProvider` interfaces are unchanged;
@@ -473,8 +473,8 @@ This simplifies provider usage and makes it easier to switch between providers w
473
473
 
474
474
  Custom providers accept `evaluationModels` aliases, and registries expose
475
475
  `evaluationModel('provider:model')`. These methods return model instances for
476
- `experimental_evaluate`. An explicitly configured default provider with an
477
- `evaluationModel` method also enables direct string IDs; evaluation does not
478
- implicitly use Gateway. Registry middleware for language and image models does
479
- not apply to evaluation. See [Evaluation](/docs/ai-sdk-core/evaluation#model-aliases-and-registries)
476
+ `experimental_evaluate`. Direct string IDs use Gateway by default, or an
477
+ explicitly configured default provider with an `evaluationModel` method. Registry
478
+ middleware for language and image models does not apply to evaluation.
479
+ See [Evaluation](/docs/ai-sdk-core/evaluation#model-aliases-and-registries)
480
480
  for aliases, default-provider configuration, and capability differences.
@@ -14,15 +14,15 @@ state. See [Evaluation](/docs/ai-sdk-core/evaluation) for examples and semantics
14
14
 
15
15
  ## Parameters
16
16
 
17
- | Parameter | Type | Description |
18
- | ----------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
19
- | `model` | `Experimental_EvaluationModel` | Required experimental v4 model instance or a string ID resolved by an explicitly configured evaluation-capable default provider. |
20
- | `state` | `string \| object \| array` | Required JSON-compatible shared state. |
21
- | `questions` | `Record<string, Experimental_EvaluationQuestion>` | Required nonempty question map. |
22
- | `maxRetries` | `number` | Nonnegative integer; defaults to 2. |
23
- | `abortSignal` | `AbortSignal` | Cancels evaluation. |
24
- | `headers` | `Record<string, string>` | Additional HTTP headers. |
25
- | `providerOptions` | `ProviderOptions` | Provider-specific options. |
17
+ | Parameter | Type | Description |
18
+ | ----------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
19
+ | `model` | `Experimental_EvaluationModel` | Required experimental v4 model instance or a string ID resolved by Gateway or an explicitly configured evaluation-capable default provider. |
20
+ | `state` | `string \| object \| array` | Required JSON-compatible shared state. |
21
+ | `questions` | `Record<string, Experimental_EvaluationQuestion>` | Required nonempty question map. |
22
+ | `maxRetries` | `number` | Nonnegative integer; defaults to 2. |
23
+ | `abortSignal` | `AbortSignal` | Cancels evaluation. |
24
+ | `headers` | `Record<string, string>` | Additional HTTP headers. |
25
+ | `providerOptions` | `ProviderOptions` | Provider-specific options. |
26
26
 
27
27
  ## Result
28
28
 
@@ -312,6 +312,7 @@ retain experimental evaluation access.
312
312
  Unavailable evaluation capabilities or models throw `NoSuchModelError` with
313
313
  `modelType: 'evaluationModel'`; unknown registry providers throw
314
314
  `NoSuchProviderError`. These capabilities are structural extensions and are not
315
- added to the stable `ProviderV4` contract. Evaluation does not assume Gateway
316
- support. See [Evaluation](/docs/ai-sdk-core/evaluation#model-aliases-and-registries)
315
+ added to the stable `ProviderV4` contract. Direct evaluation string IDs default
316
+ to Gateway when no default provider is configured.
317
+ See [Evaluation](/docs/ai-sdk-core/evaluation#model-aliases-and-registries)
317
318
  for runnable usage patterns.
@@ -207,13 +207,15 @@ The `customProvider` function returns a `Provider` instance. It has the followin
207
207
  Pass `evaluationModels: Record<string, Experimental_EvaluationModel>` to define
208
208
  aliases for evaluation model instances or string IDs. The returned provider adds
209
209
  `evaluationModel(alias): Experimental_EvaluationModelV4`. String aliases resolve
210
- through an explicitly configured default provider with an `evaluationModel`
211
- method. Unknown aliases use the fallback provider's evaluation factory when
212
- available; model failures and unsupported questions do not trigger substitution.
210
+ through Gateway by default, or an explicitly configured default provider with
211
+ an `evaluationModel` method. Unknown aliases use the fallback provider's evaluation
212
+ factory when available; model failures and unsupported questions do not trigger
213
+ substitution.
213
214
 
214
215
  Unavailable evaluation capabilities or models throw `NoSuchModelError` with
215
216
  `modelType: 'evaluationModel'`; unknown registry providers throw
216
217
  `NoSuchProviderError`. These capabilities are structural extensions and are not
217
- added to the stable `ProviderV4` contract. Evaluation does not assume Gateway
218
- support. See [Evaluation](/docs/ai-sdk-core/evaluation#model-aliases-and-registries)
218
+ added to the stable `ProviderV4` contract. Direct evaluation string IDs default
219
+ to Gateway when no default provider is configured.
220
+ See [Evaluation](/docs/ai-sdk-core/evaluation#model-aliases-and-registries)
219
221
  for runnable usage patterns.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.104",
3
+ "version": "7.0.106",
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.84",
45
+ "@ai-sdk/gateway": "4.0.86",
46
46
  "@ai-sdk/provider": "4.0.17",
47
- "@ai-sdk/provider-utils": "5.0.43"
47
+ "@ai-sdk/provider-utils": "5.0.44"
48
48
  },
49
49
  "devDependencies": {
50
- "@ai-sdk/amazon-bedrock": "5.0.86",
51
- "@ai-sdk/deepseek": "3.0.47",
52
- "@ai-sdk/google": "4.0.74",
53
- "@ai-sdk/groq": "4.0.44",
54
- "@ai-sdk/huggingface": "2.0.51",
55
- "@ai-sdk/moonshotai": "3.0.52",
56
- "@ai-sdk/openai": "4.0.69",
50
+ "@ai-sdk/amazon-bedrock": "5.0.87",
51
+ "@ai-sdk/deepseek": "3.0.48",
52
+ "@ai-sdk/google": "4.0.75",
53
+ "@ai-sdk/groq": "4.0.45",
54
+ "@ai-sdk/huggingface": "2.0.52",
55
+ "@ai-sdk/moonshotai": "3.0.53",
56
+ "@ai-sdk/openai": "4.0.70",
57
57
  "@ai-sdk/test-server": "2.0.1",
58
- "@ai-sdk/xai": "5.0.2",
58
+ "@ai-sdk/xai": "5.0.3",
59
59
  "@edge-runtime/vm": "^5.0.0",
60
60
  "@smithy/eventstream-codec": "^4.3.3",
61
61
  "@smithy/util-utf8": "^4.3.3",
@@ -202,6 +202,7 @@ export async function startBatch<
202
202
  prompt: standardizedPrompt,
203
203
  supportedUrls,
204
204
  download: undefined,
205
+ abortSignal: operationAbortSignal,
205
206
  provider: batchApi.provider.split('.')[0],
206
207
  }),
207
208
  tools: preparedTools,
@@ -379,6 +379,7 @@ export async function generateObject<
379
379
  prompt: standardizedPrompt,
380
380
  supportedUrls: await model.supportedUrls,
381
381
  download,
382
+ abortSignal,
382
383
  provider: model.provider.split('.')[0],
383
384
  });
384
385
 
@@ -612,6 +612,7 @@ class DefaultStreamObjectResult<
612
612
  prompt: standardizedPrompt,
613
613
  supportedUrls: await model.supportedUrls,
614
614
  download,
615
+ abortSignal,
615
616
  provider: model.provider.split('.')[0],
616
617
  }),
617
618
  providerOptions,
@@ -964,6 +964,7 @@ export async function generateText<
964
964
  },
965
965
  supportedUrls: await stepModel.supportedUrls,
966
966
  download,
967
+ abortSignal: mergedAbortSignal,
967
968
  provider: stepModel.provider.split('.')[0],
968
969
  });
969
970
 
@@ -80,7 +80,7 @@ export function invokeToolCallbacksFromStream<
80
80
 
81
81
  delete ongoingToolCallToolNames[chunk.toolCallId];
82
82
 
83
- if (tool?.onInputAvailable != null) {
83
+ if (!chunk.invalid && tool?.onInputAvailable != null) {
84
84
  await tool.onInputAvailable({
85
85
  input: chunk.input,
86
86
  toolCallId: chunk.toolCallId,
@@ -121,6 +121,7 @@ export function pruneMessages({
121
121
  }
122
122
  }
123
123
 
124
+ const approvalIdToToolCallId = new Map<string, string>();
124
125
  const approvalIdToToolName = new Map<string, string>();
125
126
  for (const message of messages) {
126
127
  if (
@@ -129,6 +130,8 @@ export function pruneMessages({
129
130
  ) {
130
131
  for (const part of message.content) {
131
132
  if (part.type === 'tool-approval-request') {
133
+ approvalIdToToolCallId.set(part.approvalId, part.toolCallId);
134
+
132
135
  const toolName = toolCallIdToToolName.get(part.toolCallId);
133
136
  if (toolName != null) {
134
137
  approvalIdToToolName.set(part.approvalId, toolName);
@@ -138,6 +141,16 @@ export function pruneMessages({
138
141
  }
139
142
  }
140
143
 
144
+ // Approval requests depend on their originating tool call. When an
145
+ // approval response is in the retained messages, trace through its request
146
+ // so pruning does not leave a pending approval without the call to execute.
147
+ for (const approvalId of keptApprovalIds) {
148
+ const toolCallId = approvalIdToToolCallId.get(approvalId);
149
+ if (toolCallId != null) {
150
+ keptToolCallIds.add(toolCallId);
151
+ }
152
+ }
153
+
141
154
  messages = messages.map((message, messageIndex) => {
142
155
  if (
143
156
  (message.role !== 'assistant' && message.role !== 'tool') ||
@@ -306,6 +306,7 @@ export async function streamLanguageModelCall<
306
306
  },
307
307
  supportedUrls: await resolvedModel.supportedUrls,
308
308
  download,
309
+ abortSignal,
309
310
  provider: resolvedModel.provider.split('.')[0],
310
311
  });
311
312
 
@@ -968,14 +968,16 @@ function createOutputTransformStream<
968
968
  let text = '';
969
969
  let textChunk = '';
970
970
  let textProviderMetadata: ProviderMetadata | undefined = undefined;
971
- let lastPublishedValue = '';
971
+ let lastPublishedValue: string | undefined = undefined;
972
+ let hasPublishedValue = false;
972
973
 
973
- function resetAttemptState() {
974
+ function resetOutputState() {
974
975
  firstTextChunkId = undefined;
975
976
  text = '';
976
977
  textChunk = '';
977
978
  textProviderMetadata = undefined;
978
- lastPublishedValue = '';
979
+ lastPublishedValue = undefined;
980
+ hasPublishedValue = false;
979
981
  }
980
982
 
981
983
  function enqueueChunk({
@@ -1020,11 +1022,15 @@ function createOutputTransformStream<
1020
1022
  >({
1021
1023
  async transform(chunk, controller) {
1022
1024
  if (isStreamRetryBoundaryPart(chunk)) {
1023
- resetAttemptState();
1025
+ resetOutputState();
1024
1026
  controller.enqueue(chunk);
1025
1027
  return;
1026
1028
  }
1027
1029
 
1030
+ if (chunk.type === 'start-step') {
1031
+ resetOutputState();
1032
+ }
1033
+
1028
1034
  // ensure that we publish the last text chunk before the step finish:
1029
1035
  if (chunk.type === 'finish-step' && textChunk.length > 0) {
1030
1036
  publishTextChunk({ controller });
@@ -1096,9 +1102,10 @@ function createOutputTransformStream<
1096
1102
  typeof result.partial === 'string'
1097
1103
  ? result.partial
1098
1104
  : JSON.stringify(result.partial);
1099
- if (currentValue !== lastPublishedValue) {
1105
+ if (!hasPublishedValue || currentValue !== lastPublishedValue) {
1100
1106
  publishTextChunk({ controller, partialOutput: result.partial });
1101
1107
  lastPublishedValue = currentValue;
1108
+ hasPublishedValue = true;
1102
1109
  }
1103
1110
  }
1104
1111
  },
@@ -1414,6 +1421,8 @@ class DefaultStreamTextResult<
1414
1421
  const initialResponseMessages: Array<ResponseMessage> = [];
1415
1422
  let stepMessagesForNextStep: Array<ModelMessage> | undefined;
1416
1423
  let currentStepMessages: Array<ModelMessage> = [];
1424
+ let isAborted = false;
1425
+ let currentStepModel = model;
1417
1426
 
1418
1427
  // provider-assigned text/reasoning part IDs are only unique within a
1419
1428
  // single model call (e.g. Anthropic uses the content block index, which
@@ -1661,8 +1670,8 @@ class DefaultStreamTextResult<
1661
1670
  new DefaultStepResult({
1662
1671
  callId,
1663
1672
  stepNumber: recordedSteps.length,
1664
- provider: model.provider,
1665
- modelId: model.modelId,
1673
+ provider: currentStepModel.provider,
1674
+ modelId: currentStepModel.modelId,
1666
1675
  runtimeContext,
1667
1676
  toolsContext,
1668
1677
  content: recordedContent,
@@ -1691,8 +1700,8 @@ class DefaultStreamTextResult<
1691
1700
 
1692
1701
  logWarnings({
1693
1702
  warnings: recordedWarnings,
1694
- provider: model.provider,
1695
- model: model.modelId,
1703
+ provider: currentStepModel.provider,
1704
+ model: currentStepModel.modelId,
1696
1705
  });
1697
1706
 
1698
1707
  recordedSteps.push(currentStepResult);
@@ -1743,6 +1752,10 @@ class DefaultStreamTextResult<
1743
1752
  // aggregate results:
1744
1753
  self._steps.resolve(recordedSteps);
1745
1754
 
1755
+ if (isAborted) {
1756
+ return;
1757
+ }
1758
+
1746
1759
  // call onEnd callback:
1747
1760
  const finalStep = recordedSteps[recordedSteps.length - 1];
1748
1761
  const content = recordedSteps.flatMap(step => step.content);
@@ -1845,6 +1858,8 @@ class DefaultStreamTextResult<
1845
1858
  async pull(controller) {
1846
1859
  // abort handling:
1847
1860
  async function abort() {
1861
+ isAborted = true;
1862
+
1848
1863
  await notify({
1849
1864
  event: {
1850
1865
  callId,
@@ -1885,6 +1900,7 @@ class DefaultStreamTextResult<
1885
1900
  if (isAbortError(error) && abortSignal?.aborted) {
1886
1901
  await abort();
1887
1902
  } else {
1903
+ await telemetryDispatcher.onError?.({ callId, error });
1888
1904
  controller.error(error);
1889
1905
  }
1890
1906
  }
@@ -2328,6 +2344,7 @@ class DefaultStreamTextResult<
2328
2344
  const stepModel = resolveLanguageModel(
2329
2345
  prepareStepResult?.model ?? model,
2330
2346
  );
2347
+ currentStepModel = stepModel;
2331
2348
 
2332
2349
  const stepActiveTools = filterActiveTools({
2333
2350
  tools,
@@ -2752,7 +2769,7 @@ class DefaultStreamTextResult<
2752
2769
  const createStepResponse = () => ({
2753
2770
  id: generateId(),
2754
2771
  timestamp: new Date(),
2755
- modelId: model.modelId,
2772
+ modelId: stepModel.modelId,
2756
2773
  });
2757
2774
  let stepResponse: {
2758
2775
  id: string;
@@ -3375,7 +3392,7 @@ class DefaultStreamTextResult<
3375
3392
  InferPartialOutput<OUTPUT>
3376
3393
  >({
3377
3394
  transform({ partialOutput }, controller) {
3378
- if (partialOutput != null) {
3395
+ if (partialOutput !== undefined) {
3379
3396
  controller.enqueue(partialOutput);
3380
3397
  }
3381
3398
  },
@@ -19,6 +19,28 @@ function stripMarkdownCodeFenceSuffix(text: string): string {
19
19
  return text.replace(/\n?```\s*$/, '').trimEnd();
20
20
  }
21
21
 
22
+ function getPotentialSuffixStart(text: string): number {
23
+ let index = text.length;
24
+
25
+ while (index > 0 && /\s/.test(text[index - 1])) {
26
+ index--;
27
+ }
28
+
29
+ let backtickCount = 0;
30
+ while (index > 0 && backtickCount < 3 && text[index - 1] === '`') {
31
+ index--;
32
+ backtickCount++;
33
+ }
34
+
35
+ if (backtickCount > 0) {
36
+ while (index > 0 && /\s/.test(text[index - 1])) {
37
+ index--;
38
+ }
39
+ }
40
+
41
+ return index;
42
+ }
43
+
22
44
  /**
23
45
  * Middleware that extracts JSON from text content by stripping
24
46
  * markdown code fences and other formatting.
@@ -75,8 +97,6 @@ export function extractJsonMiddleware(options?: {
75
97
  }
76
98
  > = createIdMap();
77
99
 
78
- const SUFFIX_BUFFER_SIZE = 12;
79
-
80
100
  return {
81
101
  stream: stream.pipeThrough(
82
102
  new TransformStream<
@@ -145,13 +165,19 @@ export function extractJsonMiddleware(options?: {
145
165
  }
146
166
  }
147
167
 
148
- // Stream content
149
- if (
150
- block.phase === 'streaming' &&
151
- block.buffer.length > SUFFIX_BUFFER_SIZE
152
- ) {
153
- const toStream = block.buffer.slice(0, -SUFFIX_BUFFER_SIZE);
154
- block.buffer = block.buffer.slice(-SUFFIX_BUFFER_SIZE);
168
+ // Stream content while retaining anything that could still
169
+ // become trailing whitespace or a markdown fence suffix.
170
+ if (block.phase === 'streaming') {
171
+ const potentialSuffixStart = getPotentialSuffixStart(
172
+ block.buffer,
173
+ );
174
+ const toStream = block.buffer.slice(0, potentialSuffixStart);
175
+ block.buffer = block.buffer.slice(potentialSuffixStart);
176
+
177
+ if (toStream.length === 0) {
178
+ return;
179
+ }
180
+
155
181
  controller.enqueue({
156
182
  type: 'text-delta',
157
183
  id: chunk.id,
@@ -25,7 +25,13 @@ export function simulateStreamingMiddleware(): LanguageModelMiddleware {
25
25
  switch (part.type) {
26
26
  case 'text': {
27
27
  if (part.text.length > 0) {
28
- controller.enqueue({ type: 'text-start', id: String(id) });
28
+ controller.enqueue({
29
+ type: 'text-start',
30
+ id: String(id),
31
+ ...(part.providerMetadata != null
32
+ ? { providerMetadata: part.providerMetadata }
33
+ : {}),
34
+ });
29
35
  controller.enqueue({
30
36
  type: 'text-delta',
31
37
  id: String(id),
@@ -222,10 +222,8 @@ export function resolveEvaluationModel(
222
222
  ): EvaluationModelV4 {
223
223
  if (typeof model === 'string') {
224
224
  // Use the original provider so experimental methods and their receiver survive.
225
- // Evaluation does not default to Gateway.
226
- const provider = globalThis.AI_SDK_DEFAULT_PROVIDER as
227
- | EvaluationProvider
228
- | undefined;
225
+ const provider = (globalThis.AI_SDK_DEFAULT_PROVIDER ??
226
+ gateway) as EvaluationProvider;
229
227
 
230
228
  if (typeof provider?.evaluationModel !== 'function') {
231
229
  throw new NoSuchModelError({
@@ -36,7 +36,8 @@ import { MissingToolResultsError } from '../error/missing-tool-result-error';
36
36
  export async function convertToLanguageModelPrompt({
37
37
  prompt,
38
38
  supportedUrls,
39
- download = createDefaultDownloadFunction(),
39
+ download,
40
+ abortSignal,
40
41
  // `provider` is only needed here to convert legacy tool output types via `mapToolResultOutput`.
41
42
  // TODO: remove in v8 when "file-id" and "image-file-id" types are removed
42
43
  provider,
@@ -44,11 +45,12 @@ export async function convertToLanguageModelPrompt({
44
45
  prompt: StandardizedPrompt;
45
46
  supportedUrls: Record<string, RegExp[]>;
46
47
  download: DownloadFunction | undefined;
48
+ abortSignal?: AbortSignal;
47
49
  provider?: string;
48
50
  }): Promise<LanguageModelV4Prompt> {
49
51
  const downloadedAssets = await downloadAssets(
50
52
  prompt.messages,
51
- download,
53
+ download ?? createDefaultDownloadFunction(undefined, abortSignal),
52
54
  supportedUrls,
53
55
  );
54
56
 
@@ -1,9 +1,5 @@
1
1
  import { getErrorMessage, type JSONValue } from '@ai-sdk/provider';
2
- import {
3
- parseJSON,
4
- type Tool,
5
- type ToolResultOutput,
6
- } from '@ai-sdk/provider-utils';
2
+ import type { Tool, ToolResultOutput } from '@ai-sdk/provider-utils';
7
3
 
8
4
  export async function createToolModelOutput({
9
5
  toolCallId,
@@ -21,7 +17,7 @@ export async function createToolModelOutput({
21
17
  if (errorMode === 'text') {
22
18
  return { type: 'error-text', value: getErrorMessage(output) };
23
19
  } else if (errorMode === 'json') {
24
- return { type: 'error-json', value: await toJSONValue(output) };
20
+ return { type: 'error-json', value: toJSONValue(output) };
25
21
  }
26
22
 
27
23
  if (tool?.toModelOutput) {
@@ -30,16 +26,29 @@ export async function createToolModelOutput({
30
26
 
31
27
  return typeof output === 'string'
32
28
  ? { type: 'text', value: output }
33
- : { type: 'json', value: await toJSONValue(output) };
29
+ : { type: 'json', value: toJSONValue(output) };
34
30
  }
35
31
 
36
- async function toJSONValue(value: unknown): Promise<JSONValue> {
32
+ /**
33
+ * Normalizes an in-process tool output to a plain JSON value
34
+ * (applies `toJSON`, converts `Date`, drops `undefined`, etc.)
35
+ * by round-tripping it through `JSON.stringify`.
36
+ *
37
+ * The parsed text is produced by `JSON.stringify` from a value that is
38
+ * already materialized in this process, so it is not untrusted input.
39
+ * `JSON.parse` is used deliberately instead of the secure parser from
40
+ * `@ai-sdk/provider-utils`: the secure parser rejects own `__proto__` and
41
+ * `constructor.prototype` keys, which are valid data in tool outputs
42
+ * (e.g. rows from an external API). `JSON.parse` defines `__proto__` as an
43
+ * own data property and never modifies the prototype chain, so preserving
44
+ * these keys here is safe. Do not reuse this pattern for text that comes
45
+ * from outside the process; use `parseJSON` / `safeParseJSON` instead.
46
+ */
47
+ function toJSONValue(value: unknown): JSONValue {
37
48
  if (value === undefined) {
38
49
  return null;
39
50
  }
40
51
 
41
52
  const serialized = JSON.stringify(value);
42
- return serialized === undefined
43
- ? null
44
- : await parseJSON({ text: serialized });
53
+ return serialized === undefined ? null : JSON.parse(serialized);
45
54
  }
@@ -65,11 +65,17 @@ export function readUIMessageStream<UI_MESSAGE extends UIMessage>({
65
65
  }): AsyncIterableStream<UI_MESSAGE> {
66
66
  let controller: ReadableStreamDefaultController<UI_MESSAGE> | undefined;
67
67
  let hasErrored = false;
68
+ let isCancelled = false;
69
+ const abortController = new AbortController();
68
70
 
69
71
  const outputStream = new ReadableStream<UI_MESSAGE>({
70
72
  start(controllerParam) {
71
73
  controller = controllerParam;
72
74
  },
75
+ cancel() {
76
+ isCancelled = true;
77
+ abortController.abort();
78
+ },
73
79
  });
74
80
 
75
81
  const state = createStreamingUIMessageState<UI_MESSAGE>({
@@ -98,17 +104,20 @@ export function readUIMessageStream<UI_MESSAGE extends UIMessage>({
98
104
  return job({
99
105
  state,
100
106
  write: () => {
101
- controller?.enqueue(createUIMessageSnapshot(state.message));
107
+ if (!isCancelled) {
108
+ controller?.enqueue(createUIMessageSnapshot(state.message));
109
+ }
102
110
  },
103
111
  });
104
112
  },
105
113
  onError: handleError,
106
114
  }),
107
115
  onError: handleError,
116
+ abortSignal: abortController.signal,
108
117
  }).finally(() => {
109
- // Only close if no error occurred. Calling close() on an errored controller
110
- // throws "Invalid state: Controller is already closed" TypeError.
111
- if (!hasErrored) {
118
+ // Only close if no error or cancellation occurred. Both erroring and
119
+ // cancelling a stream close its controller.
120
+ if (!hasErrored && !isCancelled) {
112
121
  controller?.close();
113
122
  }
114
123
  });
@@ -34,12 +34,15 @@ export type DownloadFunction = (
34
34
  * Downloads the file if it is not supported by the model.
35
35
  */
36
36
  export const createDefaultDownloadFunction =
37
- (download: typeof originalDownload = originalDownload): DownloadFunction =>
37
+ (
38
+ download: typeof originalDownload = originalDownload,
39
+ abortSignal?: AbortSignal,
40
+ ): DownloadFunction =>
38
41
  requestedDownloads =>
39
42
  Promise.all(
40
43
  requestedDownloads.map(async requestedDownload =>
41
44
  requestedDownload.isUrlSupportedByModel
42
45
  ? null
43
- : await download(requestedDownload),
46
+ : await download({ ...requestedDownload, abortSignal }),
44
47
  ),
45
48
  );