ai 7.0.159 → 7.0.161

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.
@@ -12,7 +12,7 @@ You can control both the execution flow and the settings at each step of the age
12
12
  - A tool call needs approval, or
13
13
  - A stop condition is met
14
14
 
15
- A [steering message](#steering) that is waiting when the model finishes its answer keeps the loop going for another step.
15
+ A [steering message](#steering) that is waiting when the model finishes its answer keeps the loop going for another step. So does a [queued message](#queueing-messages), unless the answer was cut off.
16
16
 
17
17
  The AI SDK provides built-in loop control through two parameters: `stopWhen` for defining stopping conditions and `prepareStep` for modifying settings (model, tools, messages, and more) between steps.
18
18
 
@@ -477,6 +477,34 @@ prepareStep: ({ initialMessages, steps, experimental_steeringMessages }) => ({
477
477
  }),
478
478
  ```
479
479
 
480
+ ### Queueing Messages
481
+
482
+ `steer()` changes what the agent is doing right now. Use `queue()` for a follow-up that should wait until the agent has finished its current task:
483
+
484
+ ```ts
485
+ const steering = experimental_createSteering();
486
+
487
+ const resultPromise = agent.generate({
488
+ prompt: 'Refactor the auth module.',
489
+ experimental_steering: steering,
490
+ });
491
+
492
+ // while the agent is working:
493
+ steering.queue('Then add tests for it.');
494
+ steering.queue('Then update the readme.');
495
+
496
+ const result = await resultPromise;
497
+ ```
498
+
499
+ A queued message is added once the model finishes its current task, i.e. when it gives a final answer without tool calls. The loop then continues with the message, so the agent works through the queue one task at a time:
500
+
501
+ - Queued messages are added one per task, in the order they are queued.
502
+ - Waiting steering messages are added first. A queued message waits for the next task that finishes.
503
+ - Messages that are queued before the loop starts wait until the model has answered the prompt.
504
+ - A response that is cut off (finish reason `length` or `content-filter`) does not finish the task, so the loop ends and the queued message is reported as not delivered.
505
+ - Stop conditions still apply, and a queued message that the loop does not reach is reported as not delivered, so you can send it as a new message instead.
506
+ - Queued messages are saved in `experimental_steeringMessages` like steering messages, and `queue()` resolves with the same outcome as `steer()`.
507
+
480
508
  ### Handling the Outcome
481
509
 
482
510
  `steer()` resolves with the outcome of the message and never rejects:
@@ -16,7 +16,7 @@ to see providers that offer monitoring and tracing for AI SDK applications.
16
16
 
17
17
  ### Step 1: Register the OpenTelemetry integration
18
18
 
19
- OpenTelemetry span collection requires the `@ai-sdk/otel` package. Install it and register the integration once at application startup:
19
+ OpenTelemetry collection requires the `@ai-sdk/otel` package. Install it and register the integration once at application startup:
20
20
 
21
21
  ```bash
22
22
  pnpm install @ai-sdk/otel
@@ -513,6 +513,37 @@ The `LegacyOpenTelemetry` integration emits legacy AI SDK-specific spans.
513
513
  The `OpenTelemetry` emits spans that follow the [OpenTelemetry Semantic Conventions for GenAI](https://opentelemetry.io/docs/specs/semconv/gen-ai/).
514
514
  By default, attributes use the `gen_ai.*` prefix. Provider names are mapped to well-known values (e.g. `openai`, `anthropic`, `gcp.vertex_ai`).
515
515
 
516
+ #### Inference duration metric
517
+
518
+ For `generateText` and `streamText`, `OpenTelemetry` also records the
519
+ [`gen_ai.client.inference.duration`](https://github.com/open-telemetry/semantic-conventions-genai/blob/3fe84604a581b3d50dd5b4bd29383b462d2a5018/docs/gen-ai/client-inference.md#metric-gen_aiclientinferenceduration)
520
+ histogram in seconds, following the development GenAI semantic conventions.
521
+ Configure a global OpenTelemetry `MeterProvider` with a metric reader and exporter
522
+ to collect it. A `TracerProvider` alone does not export metrics.
523
+
524
+ Each language model call contributes one observation, including calls that fail or
525
+ are cancelled. Streaming calls record the full response duration. In a workflow
526
+ with multiple model calls, each call is recorded separately; local tool execution
527
+ time is excluded. Each attempt of a retried model stream contributes its own
528
+ observation, including failed attempts that close without a finish chunk.
529
+
530
+ The histogram includes these attributes:
531
+
532
+ - `gen_ai.operation.name`: `"chat"`
533
+ - `gen_ai.provider.name`: the normalized provider name
534
+ - `gen_ai.request.model`: the requested model ID
535
+ - `gen_ai.response.model`: the response model ID, when available
536
+ - `error.type`: the error name for thrown errors, `"AbortError"` for cancellation,
537
+ or `"_OTHER"` when a call ends with an error finish reason or an unknown error
538
+
539
+ The default bucket boundary advice is `[0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64,
540
+ 1.28, 2.56, 5.12, 10.24, 20.48, 40.96, 81.92]` seconds. Your OpenTelemetry SDK
541
+ configuration can override aggregation and bucket boundaries.
542
+
543
+ Setting `telemetry.isEnabled` to `false` disables these metrics for a call.
544
+ Setting `recordInputs` or `recordOutputs` to `false` does not disable duration
545
+ metrics. The `LegacyOpenTelemetry` integration does not emit this histogram.
546
+
516
547
  #### generateText / streamText
517
548
 
518
549
  For `generateText` and `streamText`, the integration records 3 types of spans:
@@ -579,6 +579,61 @@ to `'.'` to use the sandbox's default working directory itself. When omitted,
579
579
  regular sessions use the default `<harnessId>-<sessionId>` directory, while
580
580
  `onBootstrap` receives the sandbox's default working directory.
581
581
 
582
+ When the harness declares a sandbox bootstrap recipe, as Claude Code and Codex
583
+ do, `createSession()` runs the session's sandbox setup before the harness
584
+ starts: it runs `onBootstrap` when its marker is missing, creates the session
585
+ working directory, and calls `onSession`. For any other harness, such as Pi,
586
+ the setup runs on the first file, exec, or spawn operation that the harness or
587
+ a host tool performs on the sandbox. A session whose turns never touch the
588
+ sandbox then makes no setup calls. If the setup fails, the operation that
589
+ triggered it rejects, and the next operation runs the setup again. A sandbox
590
+ session without `defaultWorkingDirectory` still answers one `pwd` in
591
+ `createSession()`, because the session working directory is derived from it.
592
+ When the sandbox comes from the `sandbox` provider setting instead of
593
+ `sandboxSession`, the provider runs `onBootstrap` when it creates a new
594
+ sandbox, which happens in `createSession()`.
595
+
596
+ ## Start the Sandbox on First Use
597
+
598
+ Deferring the setup does not defer the sandbox itself: a session you pass to
599
+ `createSession()` already exists. To defer the sandbox as well, pass
600
+ `defaultWorkingDirectory` and a `createSandboxSession` function instead. With
601
+ a harness that has no sandbox bootstrap recipe, such as Pi, `createSession()`
602
+ returns without calling `createSandboxSession`, and the first file, exec, or
603
+ spawn operation calls it, so a turn that only chats never starts the sandbox.
604
+ For any other harness, `createSession()` calls it right away.
605
+
606
+ ```ts
607
+ import { HarnessAgent } from '@ai-sdk/harness/agent';
608
+ import { pi } from '@ai-sdk/harness-pi';
609
+ import { createVercelNetworkSandboxSessionFromNativeSandbox } from '@ai-sdk/sandbox-vercel';
610
+ import { Sandbox } from '@vercel/sandbox';
611
+
612
+ const agent = new HarnessAgent({ harness: pi });
613
+
614
+ const session = await agent.createSession({
615
+ sessionId: chatId,
616
+ resumeFrom: resumeState,
617
+ sandboxSession: {
618
+ defaultWorkingDirectory: '/vercel/sandbox',
619
+ createSandboxSession: async () =>
620
+ createVercelNetworkSandboxSessionFromNativeSandbox(
621
+ await Sandbox.getOrCreate({
622
+ name: `chat-${chatId}`,
623
+ runtime: 'node24',
624
+ }),
625
+ ),
626
+ },
627
+ });
628
+ ```
629
+
630
+ The harness needs the working directory before the sandbox exists, so
631
+ `defaultWorkingDirectory` must match the one the sandbox reports. Because the
632
+ sandbox is looked up by name, the same call works for the first turn and for
633
+ every later one. A failed `createSandboxSession` or setup is retried on the
634
+ next operation. You own the sandbox lifecycle, as with any session you pass to
635
+ `createSession()`.
636
+
582
637
  ## Prepare Reusable Sandboxes
583
638
 
584
639
  Use `agent.getSandboxTemplate()` to resolve one harness's bootstrap recipe and
@@ -857,7 +857,7 @@ To see `generateText` in action, check out [these examples](#examples).
857
857
  type: 'Experimental_Steering',
858
858
  isOptional: true,
859
859
  description:
860
- "Experimental steering object created with `experimental_createSteering()`. Messages sent with `steering.steer()` are added as user messages at the next step boundary, after the current step and its tool calls are done. A waiting message keeps the loop going for another step unless a stop condition is met. `steer()` resolves with `{ type: 'delivered', id, message, stepNumber }` or `{ type: 'not-delivered', id, message, reason }`. See [Steering](/docs/agents/loop-control#steering).",
860
+ "Experimental steering object created with `experimental_createSteering()`. Messages sent with `steering.steer()` are added as user messages at the next step boundary, after the current step and its tool calls are done. Messages sent with `steering.queue()` wait until the model finishes its current task and are added one per task. A waiting message keeps the loop going for another step unless a stop condition is met. `steer()` and `queue()` resolve with `{ type: 'delivered', id, message, stepNumber }` or `{ type: 'not-delivered', id, message, reason }`. See [Steering](/docs/agents/loop-control#steering).",
861
861
  },
862
862
  {
863
863
  name: 'experimental_download',
@@ -914,7 +914,7 @@ retain the content their contracts require.
914
914
  type: 'Experimental_Steering',
915
915
  isOptional: true,
916
916
  description:
917
- "Experimental steering object created with `experimental_createSteering()`. Messages sent with `steering.steer()` are added as user messages at the next step boundary, after the current step and its tool calls are done. A waiting message keeps the loop going for another step unless a stop condition is met. `steer()` resolves with `{ type: 'delivered', id, message, stepNumber }` or `{ type: 'not-delivered', id, message, reason }`. See [Steering](/docs/agents/loop-control#steering).",
917
+ "Experimental steering object created with `experimental_createSteering()`. Messages sent with `steering.steer()` are added as user messages at the next step boundary, after the current step and its tool calls are done. Messages sent with `steering.queue()` wait until the model finishes its current task and are added one per task. A waiting message keeps the loop going for another step unless a stop condition is met. `steer()` and `queue()` resolve with `{ type: 'delivered', id, message, stepNumber }` or `{ type: 'not-delivered', id, message, reason }`. See [Steering](/docs/agents/loop-control#steering).",
918
918
  },
919
919
  {
920
920
  name: 'experimental_download',
@@ -768,7 +768,7 @@ const result = await agent.generate({
768
768
  type: 'Experimental_Steering',
769
769
  isOptional: true,
770
770
  description:
771
- "Experimental steering object created with `experimental_createSteering()`. Messages sent with `steering.steer()` are added as user messages at the next step boundary, after the current step and its tool calls are done. A waiting message keeps the loop going for another step unless a stop condition is met. `steer()` resolves with `{ type: 'delivered', id, message, stepNumber }` or `{ type: 'not-delivered', id, message, reason }`. See [Steering](/docs/agents/loop-control#steering).",
771
+ "Experimental steering object created with `experimental_createSteering()`. Messages sent with `steering.steer()` are added as user messages at the next step boundary, after the current step and its tool calls are done. Messages sent with `steering.queue()` wait until the model finishes its current task and are added one per task. A waiting message keeps the loop going for another step unless a stop condition is met. `steer()` and `queue()` resolve with `{ type: 'delivered', id, message, stepNumber }` or `{ type: 'not-delivered', id, message, reason }`. See [Steering](/docs/agents/loop-control#steering).",
772
772
  },
773
773
  {
774
774
  name: 'options',
@@ -891,7 +891,7 @@ for await (const chunk of stream.textStream) {
891
891
  type: 'Experimental_Steering',
892
892
  isOptional: true,
893
893
  description:
894
- "Experimental steering object created with `experimental_createSteering()`. Messages sent with `steering.steer()` are added as user messages at the next step boundary, after the current step and its tool calls are done. A waiting message keeps the loop going for another step unless a stop condition is met. `steer()` resolves with `{ type: 'delivered', id, message, stepNumber }` or `{ type: 'not-delivered', id, message, reason }`. See [Steering](/docs/agents/loop-control#steering).",
894
+ "Experimental steering object created with `experimental_createSteering()`. Messages sent with `steering.steer()` are added as user messages at the next step boundary, after the current step and its tool calls are done. Messages sent with `steering.queue()` wait until the model finishes its current task and are added one per task. A waiting message keeps the loop going for another step unless a stop condition is met. `steer()` and `queue()` resolve with `{ type: 'delivered', id, message, stepNumber }` or `{ type: 'not-delivered', id, message, reason }`. See [Steering](/docs/agents/loop-control#steering).",
895
895
  },
896
896
  {
897
897
  name: 'options',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.159",
3
+ "version": "7.0.161",
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.132",
45
+ "@ai-sdk/gateway": "4.0.134",
46
46
  "@ai-sdk/provider": "4.0.35",
47
- "@ai-sdk/provider-utils": "5.0.72"
47
+ "@ai-sdk/provider-utils": "5.0.73"
48
48
  },
49
49
  "devDependencies": {
50
- "@ai-sdk/amazon-bedrock": "5.0.130",
51
- "@ai-sdk/deepseek": "3.0.77",
52
- "@ai-sdk/google": "4.0.109",
53
- "@ai-sdk/groq": "4.0.74",
54
- "@ai-sdk/huggingface": "2.0.82",
55
- "@ai-sdk/moonshotai": "3.0.82",
56
- "@ai-sdk/openai": "4.0.106",
50
+ "@ai-sdk/amazon-bedrock": "5.0.131",
51
+ "@ai-sdk/deepseek": "3.0.78",
52
+ "@ai-sdk/google": "4.0.110",
53
+ "@ai-sdk/groq": "4.0.75",
54
+ "@ai-sdk/huggingface": "2.0.83",
55
+ "@ai-sdk/moonshotai": "3.0.83",
56
+ "@ai-sdk/openai": "4.0.107",
57
57
  "@ai-sdk/test-server": "2.0.4",
58
- "@ai-sdk/xai": "5.0.35",
58
+ "@ai-sdk/xai": "5.0.37",
59
59
  "@edge-runtime/vm": "^5.0.0",
60
60
  "@smithy/eventstream-codec": "^4.3.3",
61
61
  "@smithy/util-utf8": "^4.3.3",
@@ -981,7 +981,15 @@ async function generateTextInternal<
981
981
  clientToolCalls.length === 0 &&
982
982
  pendingDeferredToolCalls.size === 0
983
983
  ) {
984
- if (!steeringRun?.hasPendingMessages()) {
984
+ // queued messages wait for a task that was not cut off:
985
+ const isTruncated =
986
+ steps[steps.length - 1].finishReason === 'length' ||
987
+ steps[steps.length - 1].finishReason === 'content-filter';
988
+
989
+ if (
990
+ !steeringRun?.hasPendingMessages() &&
991
+ (isTruncated || !steeringRun?.promoteQueuedMessage())
992
+ ) {
985
993
  return false;
986
994
  }
987
995
 
@@ -1,3 +1,4 @@
1
+ import { convertDataContentToUint8Array } from '../prompt/data-content';
1
2
  import {
2
3
  convertBase64ToUint8Array,
3
4
  convertUint8ArrayToBase64,
@@ -48,13 +49,15 @@ export class DefaultGeneratedFile implements GeneratedFile {
48
49
  mediaType,
49
50
  providerMetadata,
50
51
  }: {
51
- data: string | Uint8Array;
52
+ data: string | Uint8Array | ArrayBuffer;
52
53
  mediaType: string;
53
54
  providerMetadata?: Record<string, JSONObject>;
54
55
  }) {
55
- const isUint8Array = data instanceof Uint8Array;
56
- this.base64Data = isUint8Array ? undefined : data;
57
- this.uint8ArrayData = isUint8Array ? data : undefined;
56
+ this.base64Data = typeof data === 'string' ? data : undefined;
57
+ this.uint8ArrayData =
58
+ typeof data === 'string'
59
+ ? undefined
60
+ : convertDataContentToUint8Array(data);
58
61
  this.mediaType = mediaType;
59
62
  this.providerMetadata = providerMetadata;
60
63
  }
@@ -79,7 +79,7 @@ export function smoothStream<TOOLS extends ToolSet>({
79
79
 
80
80
  if (!buffer.startsWith(match)) {
81
81
  throw new Error(
82
- `Chunking function must return a match that is a prefix of the buffer. Received: "${match}" expected to start with "${buffer}"`,
82
+ `Chunking function must return a match that is a prefix of the buffer. Received: "${match}" which is not a prefix of buffer "${buffer}"`,
83
83
  );
84
84
  }
85
85
 
@@ -81,6 +81,24 @@ export interface Steering {
81
81
  message: string | UserModelMessage,
82
82
  options?: { id?: string },
83
83
  ): Promise<SteeringOutcome>;
84
+
85
+ /**
86
+ * Adds a user message to the run once the model has finished its current
87
+ * task, i.e. when it gives a final answer without tool calls. The run then
88
+ * continues with the message unless a stop condition is met. Queued
89
+ * messages are delivered one per task, in order, after any waiting steering
90
+ * messages.
91
+ *
92
+ * @param message - The user message, or its text.
93
+ * @param options.id - The id of the message. Sending a message again with
94
+ * the same id returns the outcome of the first message.
95
+ *
96
+ * @returns The outcome of the message. It never rejects.
97
+ */
98
+ queue(
99
+ message: string | UserModelMessage,
100
+ options?: { id?: string },
101
+ ): Promise<SteeringOutcome>;
84
102
  }
85
103
 
86
104
  /**
@@ -89,6 +107,12 @@ export interface Steering {
89
107
  export type SteeringRun = {
90
108
  hasPendingMessages(): boolean;
91
109
 
110
+ /**
111
+ * Moves the oldest queued message to the waiting messages for the next
112
+ * step. Returns `false` when no message is queued.
113
+ */
114
+ promoteQueuedMessage(): boolean;
115
+
92
116
  /**
93
117
  * Takes the waiting messages for the next step. They are resolved by
94
118
  * `resolveTakenMessages` once the prompt of the step is known.
@@ -116,6 +140,7 @@ type PendingSteer = {
116
140
  class DefaultSteering implements Steering {
117
141
  private state: 'idle' | 'running' | 'ended' = 'idle';
118
142
  private pending: Array<PendingSteer> = [];
143
+ private queued: Array<PendingSteer> = [];
119
144
  private taken: Array<PendingSteer> = [];
120
145
  private outcomes = new Map<string, Promise<SteeringOutcome>>();
121
146
 
@@ -123,8 +148,25 @@ class DefaultSteering implements Steering {
123
148
  message: string | UserModelMessage,
124
149
  options?: { id?: string },
125
150
  ): Promise<SteeringOutcome> {
126
- const id = options?.id ?? generateSteeringId();
151
+ return this.add({ message, id: options?.id, target: this.pending });
152
+ }
153
+
154
+ queue(
155
+ message: string | UserModelMessage,
156
+ options?: { id?: string },
157
+ ): Promise<SteeringOutcome> {
158
+ return this.add({ message, id: options?.id, target: this.queued });
159
+ }
127
160
 
161
+ private add({
162
+ message,
163
+ id = generateSteeringId(),
164
+ target,
165
+ }: {
166
+ message: string | UserModelMessage;
167
+ id?: string;
168
+ target: Array<PendingSteer>;
169
+ }): Promise<SteeringOutcome> {
128
170
  const existingOutcome = this.outcomes.get(id);
129
171
  if (existingOutcome != null) {
130
172
  return existingOutcome;
@@ -144,7 +186,7 @@ class DefaultSteering implements Steering {
144
186
  reason: 'finished',
145
187
  })
146
188
  : new Promise<SteeringOutcome>(resolve => {
147
- this.pending.push({ id, message: userMessage, resolve });
189
+ target.push({ id, message: userMessage, resolve });
148
190
  });
149
191
 
150
192
  this.outcomes.set(id, outcome);
@@ -168,6 +210,16 @@ class DefaultSteering implements Steering {
168
210
  hasPendingMessages: () =>
169
211
  this.state === 'running' && this.pending.length > 0,
170
212
 
213
+ promoteQueuedMessage: () => {
214
+ const next = this.state === 'running' ? this.queued.shift() : undefined;
215
+ if (next == null) {
216
+ return false;
217
+ }
218
+
219
+ this.pending.push(next);
220
+ return true;
221
+ },
222
+
171
223
  takeMessages: () => {
172
224
  if (this.state !== 'running') {
173
225
  return [];
@@ -207,9 +259,10 @@ class DefaultSteering implements Steering {
207
259
  return;
208
260
  }
209
261
 
210
- const notDelivered = [...this.taken, ...this.pending];
262
+ const notDelivered = [...this.taken, ...this.pending, ...this.queued];
211
263
  this.taken = [];
212
264
  this.pending = [];
265
+ this.queued = [];
213
266
  this.state = 'ended';
214
267
 
215
268
  for (const { id, message, resolve } of notDelivered) {
@@ -3389,7 +3389,15 @@ class DefaultStreamTextResult<
3389
3389
  clientToolCalls.length === 0 &&
3390
3390
  pendingDeferredToolCalls.size === 0
3391
3391
  ) {
3392
- if (!steeringRun?.hasPendingMessages()) {
3392
+ // queued messages wait for a task that was not cut off:
3393
+ const isTruncated =
3394
+ stepFinishReason === 'length' ||
3395
+ stepFinishReason === 'content-filter';
3396
+
3397
+ if (
3398
+ !steeringRun?.hasPendingMessages() &&
3399
+ (isTruncated || !steeringRun?.promoteQueuedMessage())
3400
+ ) {
3393
3401
  return false;
3394
3402
  }
3395
3403
 
@@ -1,4 +1,8 @@
1
- import type { ModelMessage } from '@ai-sdk/provider-utils';
1
+ import {
2
+ isArrayBuffer,
3
+ isUint8Array,
4
+ type ModelMessage,
5
+ } from '@ai-sdk/provider-utils';
2
6
 
3
7
  /**
4
8
  * Clone model messages while preserving URL instances. Node's structuredClone
@@ -7,35 +11,67 @@ import type { ModelMessage } from '@ai-sdk/provider-utils';
7
11
  export function cloneModelMessages<T extends ModelMessage>(
8
12
  messages: Array<T>,
9
13
  ): Array<T> {
10
- return messages.map(message => cloneValue(message)) as Array<T>;
14
+ return cloneValue(messages, new Map()) as Array<T>;
11
15
  }
12
16
 
13
- function cloneValue<T>(value: T): T {
17
+ function cloneValue(value: unknown, seen: Map<object, unknown>): unknown {
18
+ if (value == null || typeof value !== 'object') {
19
+ return value;
20
+ }
21
+
22
+ if (seen.has(value)) {
23
+ return seen.get(value);
24
+ }
25
+
14
26
  if (value instanceof URL) {
15
- return new URL(value.href) as T;
27
+ const copy = new URL(value.href);
28
+ seen.set(value, copy);
29
+ return copy;
16
30
  }
17
31
 
18
32
  if (Array.isArray(value)) {
19
- return value.map(item => cloneValue(item)) as T;
33
+ const copy: unknown[] = new Array(value.length);
34
+ seen.set(value, copy);
35
+ value.forEach((item, index) => {
36
+ copy[index] = cloneValue(item, seen);
37
+ });
38
+ return copy;
20
39
  }
21
40
 
22
- if (value instanceof Uint8Array) {
23
- return new Uint8Array(value) as T;
41
+ if (isUint8Array(value)) {
42
+ const copy = new Uint8Array(value);
43
+ seen.set(value, copy);
44
+ return copy;
24
45
  }
25
46
 
26
- if (value instanceof ArrayBuffer) {
27
- return value.slice(0) as T;
47
+ if (isArrayBuffer(value)) {
48
+ const copy = new Uint8Array(value).slice().buffer;
49
+ seen.set(value, copy);
50
+ return copy;
28
51
  }
29
52
 
30
- if (value instanceof Date) {
31
- return new Date(value) as T;
53
+ if (
54
+ value instanceof Date ||
55
+ Object.prototype.toString.call(value) === '[object Date]'
56
+ ) {
57
+ try {
58
+ const copy = new Date(Date.prototype.getTime.call(value));
59
+ seen.set(value, copy);
60
+ return copy;
61
+ } catch {
62
+ // A string tag alone does not make a plain object a Date.
63
+ }
32
64
  }
33
65
 
34
- if (value != null && typeof value === 'object') {
35
- return Object.fromEntries(
36
- Object.entries(value).map(([key, value]) => [key, cloneValue(value)]),
37
- ) as T;
66
+ const copy: Record<string, unknown> = {};
67
+ seen.set(value, copy);
68
+ for (const [key, val] of Object.entries(value)) {
69
+ Object.defineProperty(copy, key, {
70
+ value: cloneValue(val, seen),
71
+ writable: true,
72
+ enumerable: true,
73
+ configurable: true,
74
+ });
38
75
  }
39
-
40
- return value;
76
+ return copy;
41
77
  }
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  isBuffer,
3
+ isUint8Array,
4
+ isArrayBuffer,
3
5
  type CustomPart,
4
6
  type DataContent,
5
7
  type FilePart,
@@ -19,8 +21,8 @@ import { z, type ZodType } from '../util/zod';
19
21
 
20
22
  const fileInlineDataSchema: ZodType<DataContent> = z.union([
21
23
  z.string(),
22
- z.instanceof(Uint8Array),
23
- z.instanceof(ArrayBuffer),
24
+ z.custom<Uint8Array>(isUint8Array, { message: 'Must be a Uint8Array' }),
25
+ z.custom<ArrayBuffer>(isArrayBuffer, { message: 'Must be an ArrayBuffer' }),
24
26
  z.custom<Buffer>(isBuffer, { message: 'Must be a Buffer' }),
25
27
  ]);
26
28
 
@@ -1,4 +1,6 @@
1
1
  import {
2
+ isUint8Array,
3
+ isArrayBuffer,
2
4
  convertBase64ToUint8Array,
3
5
  convertUint8ArrayToBase64,
4
6
  type DataContent,
@@ -16,11 +18,7 @@ export function convertDataContentToBase64String(content: DataContent): string {
16
18
  return content;
17
19
  }
18
20
 
19
- if (content instanceof ArrayBuffer) {
20
- return convertUint8ArrayToBase64(new Uint8Array(content));
21
- }
22
-
23
- return convertUint8ArrayToBase64(content);
21
+ return convertUint8ArrayToBase64(convertDataContentToUint8Array(content));
24
22
  }
25
23
 
26
24
  /**
@@ -32,8 +30,12 @@ export function convertDataContentToBase64String(content: DataContent): string {
32
30
  export function convertDataContentToUint8Array(
33
31
  content: DataContent,
34
32
  ): Uint8Array {
35
- if (content instanceof Uint8Array) {
36
- return content;
33
+ if (isUint8Array(content)) {
34
+ if (content instanceof Uint8Array) {
35
+ return content;
36
+ }
37
+ const { buffer, byteOffset, byteLength } = content;
38
+ return new Uint8Array(buffer, byteOffset, byteLength);
37
39
  }
38
40
 
39
41
  if (typeof content === 'string') {
@@ -49,7 +51,7 @@ export function convertDataContentToUint8Array(
49
51
  }
50
52
  }
51
53
 
52
- if (content instanceof ArrayBuffer) {
54
+ if (isArrayBuffer(content)) {
53
55
  return new Uint8Array(content);
54
56
  }
55
57
 
@@ -1,5 +1,8 @@
1
+ import { convertDataContentToUint8Array } from './data-content';
1
2
  import type { LanguageModelV4FilePart } from '@ai-sdk/provider';
2
3
  import {
4
+ isUint8Array,
5
+ isArrayBuffer,
3
6
  isBuffer,
4
7
  isProviderReference,
5
8
  type DataContent,
@@ -58,12 +61,9 @@ function convertUrlStringToFilePartData(content: string): ConvertResult {
58
61
  }
59
62
 
60
63
  function convertInlineDataToFilePartData(content: DataContent): ConvertResult {
61
- if (content instanceof Uint8Array) {
62
- return { data: { type: 'data', data: content }, mediaType: undefined };
63
- }
64
- if (content instanceof ArrayBuffer) {
64
+ if (isUint8Array(content) || isArrayBuffer(content)) {
65
65
  return {
66
- data: { type: 'data', data: new Uint8Array(content) },
66
+ data: { type: 'data', data: convertDataContentToUint8Array(content) },
67
67
  mediaType: undefined,
68
68
  };
69
69
  }
@@ -130,6 +130,10 @@ export function convertToLanguageModelV4FilePart(
130
130
  }
131
131
  }
132
132
 
133
+ if (isUint8Array(content) || isArrayBuffer(content)) {
134
+ return convertInlineDataToFilePartData(content);
135
+ }
136
+
133
137
  if (isProviderReference(content)) {
134
138
  return {
135
139
  data: { type: 'reference', reference: content as ProviderReference },
@@ -1056,7 +1056,9 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
1056
1056
  default: {
1057
1057
  if (isDataUIMessageChunk(chunk)) {
1058
1058
  // validate data chunk if dataPartSchemas is provided
1059
- if (dataPartSchemas?.[chunk.type] != null) {
1059
+ // (schemas are keyed by the part name without the 'data-' prefix)
1060
+ const dataPartSchema = dataPartSchemas?.[chunk.type.slice(5)];
1061
+ if (dataPartSchema != null) {
1060
1062
  const partIdx = state.message.parts.findIndex(
1061
1063
  p =>
1062
1064
  'id' in p &&
@@ -1069,7 +1071,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
1069
1071
 
1070
1072
  await validateTypes({
1071
1073
  value: chunk.data,
1072
- schema: dataPartSchemas[chunk.type],
1074
+ schema: dataPartSchema,
1073
1075
  context: {
1074
1076
  field: `message.parts[${actualPartIdx}].data`,
1075
1077
  entityName: chunk.type,