ai 7.0.105 → 7.0.107

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 (34) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/index.js +278 -88
  3. package/dist/index.js.map +1 -1
  4. package/dist/internal/index.d.ts +15 -3
  5. package/dist/internal/index.js +153 -46
  6. package/dist/internal/index.js.map +1 -1
  7. package/docs/03-ai-sdk-core/19-tool-search.mdx +4 -2
  8. package/docs/07-reference/01-ai-sdk-core/23-tool-search.mdx +3 -2
  9. package/package.json +11 -11
  10. package/src/batch/batch.ts +1 -0
  11. package/src/generate-object/generate-object.ts +1 -0
  12. package/src/generate-object/stream-object.ts +1 -0
  13. package/src/generate-speech/generate-speech.ts +44 -4
  14. package/src/generate-text/generate-text.ts +28 -15
  15. package/src/generate-text/invoke-tool-callbacks-from-stream.ts +68 -16
  16. package/src/generate-text/prune-messages.ts +13 -0
  17. package/src/generate-text/stream-language-model-call.ts +1 -0
  18. package/src/generate-text/stream-text.ts +29 -12
  19. package/src/generate-video/generate-video.ts +61 -10
  20. package/src/middleware/extract-json-middleware.ts +35 -9
  21. package/src/middleware/simulate-streaming-middleware.ts +7 -1
  22. package/src/middleware/wrap-provider.ts +2 -0
  23. package/src/prompt/convert-to-language-model-prompt.ts +4 -2
  24. package/src/prompt/create-tool-model-output.ts +20 -11
  25. package/src/test/mock-server-response.ts +19 -3
  26. package/src/text-stream/pipe-text-stream-to-response.ts +3 -5
  27. package/src/ui/http-chat-transport.ts +12 -2
  28. package/src/ui/last-assistant-message-is-complete-with-approval-responses.ts +1 -1
  29. package/src/ui/last-assistant-message-is-complete-with-tool-calls.ts +2 -1
  30. package/src/ui-message-stream/pipe-ui-message-stream-to-response.ts +1 -3
  31. package/src/ui-message-stream/read-ui-message-stream.ts +13 -4
  32. package/src/util/data-url.ts +13 -1
  33. package/src/util/download/download-function.ts +5 -2
  34. package/src/util/write-to-server-response.ts +7 -3
@@ -3,6 +3,7 @@ import type {
3
3
  Experimental_VideoModelV4CallOptions,
4
4
  Experimental_VideoModelV4File,
5
5
  Experimental_VideoModelV4Result,
6
+ Experimental_VideoModelV4OperationStatusResult,
6
7
  Experimental_VideoModelV4OperationWebhook,
7
8
  Experimental_VideoModelV4FrameImage,
8
9
  Experimental_VideoModelV4FrameType,
@@ -287,7 +288,7 @@ export async function experimental_generateVideo({
287
288
  `ai/${VERSION}`,
288
289
  );
289
290
 
290
- const { retry } = prepareRetries({
291
+ const { maxRetries, retry } = prepareRetries({
291
292
  maxRetries: maxRetriesArg,
292
293
  abortSignal,
293
294
  });
@@ -363,6 +364,7 @@ export async function experimental_generateVideo({
363
364
  callOptions,
364
365
  poll,
365
366
  webhook,
367
+ maxRetries,
366
368
  retry,
367
369
  });
368
370
  }
@@ -477,12 +479,14 @@ async function executeStartStatusFlow({
477
479
  callOptions,
478
480
  poll: pollConfig,
479
481
  webhook: webhookFactory,
482
+ maxRetries,
480
483
  retry,
481
484
  }: {
482
485
  model: Experimental_VideoModelV4;
483
486
  callOptions: Experimental_VideoModelV4CallOptions;
484
487
  poll?: GenerateVideoPollOptions;
485
488
  webhook?: GenerateVideoWebhookFactory;
489
+ maxRetries: number;
486
490
  retry: <OUTPUT>(fn: () => PromiseLike<OUTPUT>) => PromiseLike<OUTPUT>;
487
491
  }): Promise<Experimental_VideoModelV4Result> {
488
492
  // 1. If webhook and provider supports it, set up the webhook
@@ -538,6 +542,9 @@ async function executeStartStatusFlow({
538
542
  const timeoutMs = pollConfig?.timeoutMs ?? 600_000;
539
543
  const delay = pollConfig?.delay ?? defaultDelay;
540
544
  const startTime = Date.now();
545
+ const pollingTimeoutError = new Error(
546
+ `Video generation timed out after ${timeoutMs}ms.`,
547
+ );
541
548
 
542
549
  if (webhookReceived != null) {
543
550
  // 3a. Webhook flow: wait for webhook, then get final status
@@ -554,23 +561,67 @@ async function executeStartStatusFlow({
554
561
  // 3b. Polling flow (also used when webhooks are not supported)
555
562
  const elapsedMs = Date.now() - startTime;
556
563
  if (elapsedMs >= timeoutMs) {
557
- throw new Error(`Video generation timed out after ${timeoutMs}ms.`);
564
+ throw pollingTimeoutError;
558
565
  }
559
566
  await delay(Math.min(intervalMs, timeoutMs - elapsedMs), {
560
567
  abortSignal: callOptions.abortSignal,
561
568
  });
562
569
  if (Date.now() - startTime >= timeoutMs) {
563
- throw new Error(`Video generation timed out after ${timeoutMs}ms.`);
570
+ throw pollingTimeoutError;
564
571
  }
565
572
  }
566
573
 
567
- const statusResult = await retry(() =>
568
- model.doStatus!({
569
- operation: startResult.operation,
570
- abortSignal: callOptions.abortSignal,
571
- headers: callOptions.headers,
572
- }),
573
- );
574
+ let statusResult: Experimental_VideoModelV4OperationStatusResult;
575
+ if (webhookReceived != null) {
576
+ statusResult = await retry(() =>
577
+ model.doStatus!({
578
+ operation: startResult.operation,
579
+ abortSignal: callOptions.abortSignal,
580
+ headers: callOptions.headers,
581
+ }),
582
+ );
583
+ } else {
584
+ const statusTimeoutController = new AbortController();
585
+ const statusAbortSignal = mergeAbortSignals(
586
+ callOptions.abortSignal,
587
+ statusTimeoutController.signal,
588
+ );
589
+ const statusTimeoutId = setTimeout(
590
+ () => statusTimeoutController.abort(pollingTimeoutError),
591
+ timeoutMs - (Date.now() - startTime),
592
+ );
593
+ const statusTimeoutPromise = new Promise<never>((_, reject) => {
594
+ statusTimeoutController.signal.addEventListener(
595
+ 'abort',
596
+ () => reject(pollingTimeoutError),
597
+ { once: true },
598
+ );
599
+ });
600
+ const { retry: statusRetry } = prepareRetries({
601
+ maxRetries,
602
+ abortSignal: statusAbortSignal,
603
+ });
604
+
605
+ try {
606
+ statusResult = await Promise.race([
607
+ statusRetry(() =>
608
+ model.doStatus!({
609
+ operation: startResult.operation,
610
+ abortSignal: statusAbortSignal,
611
+ headers: callOptions.headers,
612
+ }),
613
+ ),
614
+ statusTimeoutPromise,
615
+ ]);
616
+ } catch (error) {
617
+ if (statusTimeoutController.signal.aborted) {
618
+ throw pollingTimeoutError;
619
+ }
620
+ throw error;
621
+ } finally {
622
+ clearTimeout(statusTimeoutId);
623
+ }
624
+ }
574
625
 
575
626
  if (statusResult.status === 'error') {
576
627
  throw new Error(statusResult.error);
@@ -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),
@@ -47,5 +47,7 @@ export function wrapProvider({
47
47
  transcriptionModel: providerV4.transcriptionModel,
48
48
  speechModel: providerV4.speechModel,
49
49
  rerankingModel: providerV4.rerankingModel,
50
+ ...(providerV4.files != null ? { files: providerV4.files } : {}),
51
+ ...(providerV4.skills != null ? { skills: providerV4.skills } : {}),
50
52
  };
51
53
  }
@@ -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
  }
@@ -3,7 +3,7 @@ import type { ServerResponse } from 'node:http';
3
3
 
4
4
  class MockServerResponse extends EventEmitter {
5
5
  writtenChunks: any[] = [];
6
- headers: Record<string, string> = {};
6
+ headers: Record<string, string | string[]> = {};
7
7
  statusCode = 0;
8
8
  statusMessage = '';
9
9
  ended = false;
@@ -17,6 +17,20 @@ class MockServerResponse extends EventEmitter {
17
17
  this.ended = true;
18
18
  }
19
19
 
20
+ setHeaders(headers: Headers): void {
21
+ this.headers = {};
22
+
23
+ for (const [key, value] of headers.entries()) {
24
+ const existingValue = this.headers[key];
25
+ this.headers[key] =
26
+ existingValue == null
27
+ ? value
28
+ : Array.isArray(existingValue)
29
+ ? [...existingValue, value]
30
+ : [existingValue, value];
31
+ }
32
+ }
33
+
20
34
  writeHead(
21
35
  statusCode: number,
22
36
  arg2: string | Record<string, string>,
@@ -26,8 +40,10 @@ class MockServerResponse extends EventEmitter {
26
40
 
27
41
  if (typeof arg2 === 'string') {
28
42
  this.statusMessage = arg2;
29
- this.headers = arg3 ?? {};
30
- } else {
43
+ if (arg3 != null) {
44
+ this.headers = arg3;
45
+ }
46
+ } else if (arg2 != null) {
31
47
  this.statusMessage = '';
32
48
  this.headers = arg2;
33
49
  }
@@ -29,11 +29,9 @@ export function pipeTextStreamToResponse({
29
29
  response,
30
30
  status,
31
31
  statusText,
32
- headers: Object.fromEntries(
33
- prepareHeaders(headers, {
34
- 'content-type': 'text/plain; charset=utf-8',
35
- }).entries(),
36
- ),
32
+ headers: prepareHeaders(headers, {
33
+ 'content-type': 'text/plain; charset=utf-8',
34
+ }),
37
35
  stream: stream.pipeThrough(new TextEncoderStream()),
38
36
  });
39
37
  }
@@ -10,6 +10,14 @@ import type { ChatTransport } from './chat-transport';
10
10
  import { createUIApiCallError } from './create-ui-api-call-error';
11
11
  import type { UIMessage } from './ui-messages';
12
12
 
13
+ function appendPathToUrl(url: string, path: string): string {
14
+ const queryOrFragmentStart = url.search(/[?#]/);
15
+
16
+ return queryOrFragmentStart === -1
17
+ ? `${url}${path}`
18
+ : `${url.slice(0, queryOrFragmentStart)}${path}${url.slice(queryOrFragmentStart)}`;
19
+ }
20
+
13
21
  export type PrepareSendMessagesRequest<UI_MESSAGE extends UIMessage> = (
14
22
  options: {
15
23
  id: string;
@@ -193,7 +201,7 @@ export abstract class HttpChatTransport<
193
201
  const response = await fetch(api, {
194
202
  method: 'POST',
195
203
  headers: {
196
- 'Content-Type': 'application/json',
204
+ 'content-type': 'application/json',
197
205
  ...headers,
198
206
  },
199
207
  body: JSON.stringify(body),
@@ -239,7 +247,9 @@ export abstract class HttpChatTransport<
239
247
  requestMetadata: options.metadata,
240
248
  });
241
249
 
242
- const api = preparedRequest?.api ?? `${this.api}/${options.chatId}/stream`;
250
+ const api =
251
+ preparedRequest?.api ??
252
+ appendPathToUrl(this.api, `/${options.chatId}/stream`);
243
253
  const headers =
244
254
  preparedRequest?.headers !== undefined
245
255
  ? normalizeHeaders(preparedRequest.headers)
@@ -34,7 +34,7 @@ export function lastAssistantMessageIsCompleteWithApprovalResponses({
34
34
  // all tool approvals must have a response
35
35
  lastStepToolInvocations.every(
36
36
  part =>
37
- part.state === 'output-available' ||
37
+ (part.state === 'output-available' && part.preliminary !== true) ||
38
38
  part.state === 'output-error' ||
39
39
  part.state === 'output-denied' ||
40
40
  part.state === 'approval-responded',
@@ -32,7 +32,8 @@ export function lastAssistantMessageIsCompleteWithToolCalls({
32
32
  lastStepToolInvocations.length > 0 &&
33
33
  lastStepToolInvocations.every(
34
34
  part =>
35
- part.state === 'output-available' || part.state === 'output-error',
35
+ (part.state === 'output-available' && part.preliminary !== true) ||
36
+ part.state === 'output-error',
36
37
  )
37
38
  );
38
39
  }
@@ -44,9 +44,7 @@ export function pipeUIMessageStreamToResponse({
44
44
  response,
45
45
  status,
46
46
  statusText,
47
- headers: Object.fromEntries(
48
- prepareHeaders(headers, UI_MESSAGE_STREAM_HEADERS).entries(),
49
- ),
47
+ headers: prepareHeaders(headers, UI_MESSAGE_STREAM_HEADERS),
50
48
  stream: sseStream.pipeThrough(new TextEncoderStream()),
51
49
  });
52
50
  }
@@ -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
  });
@@ -13,6 +13,10 @@ const { atob } = globalThis;
13
13
  export function getTextFromDataUrl(dataUrl: string): string {
14
14
  const [header, base64Content] = dataUrl.split(',');
15
15
  const mediaType = header.split(';')[0].split(':')[1];
16
+ const charsetMatch = /(?:^|;)\s*charset\s*=\s*(?:"([^"]+)"|([^;\s]+))/i.exec(
17
+ header,
18
+ );
19
+ const charset = charsetMatch?.[1] ?? charsetMatch?.[2];
16
20
 
17
21
  if (mediaType == null || base64Content == null) {
18
22
  throw new InvalidArgumentError({
@@ -23,7 +27,15 @@ export function getTextFromDataUrl(dataUrl: string): string {
23
27
  }
24
28
 
25
29
  try {
26
- return atob(base64Content);
30
+ const byteString = atob(base64Content);
31
+
32
+ if (charset == null) {
33
+ return byteString;
34
+ }
35
+
36
+ return new TextDecoder(charset).decode(
37
+ Uint8Array.from(byteString, byte => byte.codePointAt(0)!),
38
+ );
27
39
  } catch {
28
40
  throw new InvalidArgumentError({
29
41
  parameter: 'dataUrl',
@@ -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
  );
@@ -17,14 +17,18 @@ export function writeToServerResponse({
17
17
  response: ServerResponse;
18
18
  status?: number;
19
19
  statusText?: string;
20
- headers?: Record<string, string | number | string[]>;
20
+ headers?: Headers;
21
21
  stream: ReadableStream<Uint8Array>;
22
22
  }): Promise<void> {
23
23
  const statusCode = status ?? 200;
24
+ if (headers != null) {
25
+ response.setHeaders(headers);
26
+ }
27
+
24
28
  if (statusText !== undefined) {
25
- response.writeHead(statusCode, statusText, headers);
29
+ response.writeHead(statusCode, statusText);
26
30
  } else {
27
- response.writeHead(statusCode, headers);
31
+ response.writeHead(statusCode);
28
32
  }
29
33
 
30
34
  const reader = stream.getReader();