ai 7.0.105 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.105",
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.85",
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),
@@ -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
  );