ai 7.0.22 → 7.0.25

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.
@@ -83,10 +83,13 @@ const durationInSeconds = await result.durationInSeconds; // duration in seconds
83
83
  The `audio` stream must contain raw audio chunks. `Uint8Array` chunks are raw bytes; `string` chunks are base64-encoded raw bytes. Always set `inputAudioFormat` to match the chunks you send.
84
84
 
85
85
  <Note>
86
- Streaming transcription requires a provider model instance (e.g.
87
- `openai.transcription('gpt-realtime-whisper')`). String model IDs resolve
88
- through the global provider (AI Gateway by default), which does not support
89
- streaming transcription yet.
86
+ String model IDs resolve through the global provider (AI Gateway by
87
+ default). AI Gateway supports streaming transcription for supported models
88
+ (e.g. `openai/gpt-realtime-whisper`, `xai/grok-stt`), so string IDs work:
89
+ `experimental_streamTranscribe({ model: 'openai/gpt-realtime-whisper', ...
90
+ })`. You can also pass a provider model instance (e.g.
91
+ `openai.transcription('gpt-realtime-whisper')`) to stream directly against
92
+ the provider.
90
93
  </Note>
91
94
 
92
95
  OpenAI streaming transcription uses `openai.transcription('gpt-realtime-whisper')`. xAI uses the same `xai.transcription()` model for request/response and streaming transcription; `experimental_streamTranscribe` uses xAI's WebSocket STT transport under the hood.
@@ -0,0 +1,117 @@
1
+ ---
2
+ title: Secure URL Fetching
3
+ description: How the AI SDK protects server-side fetches of URLs returned by model providers, and how to harden your deployment further.
4
+ ---
5
+
6
+ # Secure URL Fetching
7
+
8
+ Many providers return a **URL in their response body** — a generated image,
9
+ audio, or video to download, or a polling URL to check job status. The AI SDK
10
+ fetches these server-side and returns the result to your code. Because that URL
11
+ comes from an external service, a malicious or compromised provider (or anyone
12
+ able to tamper with the response) could point it at an internal address such as
13
+ a cloud-metadata endpoint (`http://169.254.169.254/…`), a private host
14
+ (`http://10.0.0.5/…`), or `localhost`.
15
+
16
+ To prevent that, the SDK validates every response-supplied URL before fetching
17
+ it. This happens automatically inside the provider packages — you don't need to
18
+ configure anything.
19
+
20
+ ## What the SDK protects against
21
+
22
+ When the SDK fetches a URL taken from a provider response, it:
23
+
24
+ - **Rejects private, loopback, and link-local targets** — IPv4 (`10/8`,
25
+ `172.16/12`, `192.168/16`, `127/8`, `169.254/16`, CGNAT, multicast, …) and the
26
+ equivalent IPv6 ranges, plus `localhost` and `.local`. Non-`http(s)` schemes
27
+ are rejected too.
28
+ - **Re-validates every redirect hop** — a URL that passes but then redirects to
29
+ an internal address is blocked; the redirect is never followed blindly.
30
+ - **Strips risky request headers** — proxy-forwarding, cloud-metadata, and
31
+ cookie headers are removed before the request.
32
+ - **Drops credentials across origins** — caller headers (`Authorization`,
33
+ `Cookie`, and provider-specific API-key headers alike) are not sent to a host
34
+ on a different origin than the provider's; a redirect that crosses origin
35
+ drops all of them except the user-agent.
36
+
37
+ A blocked URL surfaces as a `DownloadError`.
38
+
39
+ ## Self-hosted and local endpoints
40
+
41
+ URLs that are same-origin with the provider endpoint **you configured** (e.g. a
42
+ custom `baseURL` pointing at a self-hosted or `localhost` deployment) are
43
+ exempt from these checks — they target exactly the host you told the SDK to
44
+ talk to. Any redirect off that origin is still validated.
45
+
46
+ ## Limitation: DNS resolution and DNS rebinding
47
+
48
+ The built-in guard inspects the URL **as a string**. It deliberately does
49
+ **not resolve DNS**, so two attacks remain out of scope at this layer:
50
+
51
+ 1. **Hostname that resolves to a private IP** — a literal host that looks public
52
+ but whose DNS record points at an internal address.
53
+ 2. **DNS rebinding** — a host that resolves to a public IP when validated and a
54
+ private IP a moment later when the socket actually connects (a
55
+ time-of-check/time-of-use window).
56
+
57
+ ### Why this isn't built in
58
+
59
+ Closing these requires resolving DNS and pinning the resolved IP **at connect
60
+ time** — Node-only capabilities (`node:dns`, a custom `undici` dispatcher). The
61
+ SDK's provider utilities are **cross-runtime**: they run on the edge, in the
62
+ browser, and on Bun/Deno, with no Node-only dependencies, so those APIs aren't
63
+ available there. The threat is also specifically a **server-side** one — on the
64
+ edge and in the browser, outbound `fetch` cannot reach a host's internal network
65
+ or metadata endpoint in the first place. So connect-time IP pinning is only
66
+ meaningful, and only available, on a Node server — which is exactly where you
67
+ can add it yourself.
68
+
69
+ ## Hardening your deployment
70
+
71
+ If your server fetches provider-supplied URLs and you want to close the DNS
72
+ gaps, use one (ideally both) of these:
73
+
74
+ ### 1. Restrict outbound egress at the network layer
75
+
76
+ Deny your server's network egress to `169.254.0.0/16`, RFC-1918 ranges, and
77
+ loopback. This is the most robust control and is independent of application
78
+ code.
79
+
80
+ ### 2. Inject a hardened `fetch`
81
+
82
+ Every provider accepts a custom `fetch`. On Node, back it with an `undici`
83
+ `Agent` whose `connect.lookup` validates the resolved IP and lets the socket
84
+ connect only to a safe address — closing both the hostname-to-private and the
85
+ DNS-rebinding windows:
86
+
87
+ ```ts
88
+ import { Agent, fetch as undiciFetch } from 'undici';
89
+ import { lookup } from 'node:dns';
90
+
91
+ // Your own check that returns true for private/loopback/link-local addresses.
92
+ declare function isUnsafeAddress(ip: string): boolean;
93
+
94
+ const safeLookup: typeof lookup = (hostname, options, callback) => {
95
+ lookup(hostname, options as any, (err, address, family) => {
96
+ if (!err && typeof address === 'string' && isUnsafeAddress(address)) {
97
+ callback(new Error(`Refusing to connect to ${address}`), '', 0);
98
+ return;
99
+ }
100
+ (callback as any)(err, address, family);
101
+ });
102
+ };
103
+
104
+ const safeDispatcher = new Agent({ connect: { lookup: safeLookup } });
105
+
106
+ const safeFetch: typeof fetch = (input, init) =>
107
+ undiciFetch(input, { ...init, dispatcher: safeDispatcher }) as any;
108
+ ```
109
+
110
+ ```ts
111
+ import { createFal } from '@ai-sdk/fal';
112
+
113
+ const fal = createFal({ fetch: safeFetch });
114
+ ```
115
+
116
+ The SDK's built-in validation and your connect-time pinning are complementary —
117
+ keep both.
@@ -48,7 +48,7 @@ console.log(await result.text);
48
48
  name: 'model',
49
49
  type: 'TranscriptionModelV4',
50
50
  description:
51
- 'The transcription model to use. The model must support streaming (`doStream`). String model IDs resolve through the global provider (AI Gateway by default), which does not support streaming transcription yet.',
51
+ "The transcription model to use. The model must support streaming (`doStream`). String model IDs resolve through the global provider (AI Gateway by default), which supports streaming transcription for supported models (e.g. `openai/gpt-realtime-whisper`, `xai/grok-stt`): `experimental_streamTranscribe({ model: 'openai/gpt-realtime-whisper', ... })`.",
52
52
  },
53
53
  {
54
54
  name: 'audio',
@@ -147,3 +147,15 @@ console.log(await result.text);
147
147
  `fullStream` early (e.g. `break` out of the loop), the underlying provider
148
148
  connection is closed and pending result promises reject.
149
149
  </Note>
150
+
151
+ ## Wire format (experimental)
152
+
153
+ Streaming transcription over WebSocket is serialized with the experimental
154
+ transcription-stream envelope defined in `@ai-sdk/provider-utils`
155
+ (`experimental_parseTranscriptionStreamClientFrame`,
156
+ `experimental_serializeTranscriptionStreamPart`,
157
+ `experimental_parseTranscriptionStreamPart`): the client sends one
158
+ `transcription-stream.start` TEXT frame, audio as BINARY frames, and a
159
+ `transcription-stream.audio-done` TEXT frame; each server TEXT frame is one
160
+ JSON-serialized transcription stream part. AI Gateway implements the server
161
+ side of this envelope.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.22",
3
+ "version": "7.0.25",
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,9 +42,9 @@
42
42
  }
43
43
  },
44
44
  "dependencies": {
45
- "@ai-sdk/gateway": "4.0.16",
45
+ "@ai-sdk/gateway": "4.0.19",
46
46
  "@ai-sdk/provider": "4.0.3",
47
- "@ai-sdk/provider-utils": "5.0.7"
47
+ "@ai-sdk/provider-utils": "5.0.9"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@edge-runtime/vm": "^5.0.0",
@@ -171,236 +171,248 @@ export async function embedMany({
171
171
  telemetry,
172
172
  });
173
173
 
174
- await notify({
175
- event: {
176
- callId,
177
- operationId: 'ai.embedMany',
178
- provider: model.provider,
179
- modelId: model.modelId,
180
- value: values,
181
- maxRetries,
182
- headers: headersWithUserAgent,
183
- providerOptions,
184
- },
185
- callbacks: [resolvedOnStart, telemetryDispatcher.onStart],
186
- });
187
-
188
- try {
189
- const [maxEmbeddingsPerCall, supportsParallelCalls] = await Promise.all([
190
- model.maxEmbeddingsPerCall,
191
- model.supportsParallelCalls,
192
- ]);
174
+ const runInTracingChannelSpan =
175
+ telemetryDispatcher.runInTracingChannelSpan ??
176
+ (async <T>({ execute }: { execute: () => PromiseLike<T> }) =>
177
+ await execute());
178
+
179
+ const startEvent = {
180
+ callId,
181
+ operationId: 'ai.embedMany',
182
+ provider: model.provider,
183
+ modelId: model.modelId,
184
+ value: values,
185
+ maxRetries,
186
+ headers: headersWithUserAgent,
187
+ providerOptions,
188
+ };
193
189
 
194
- if (maxEmbeddingsPerCall == null || maxEmbeddingsPerCall === Infinity) {
195
- const { embeddings, usage, warnings, response, providerMetadata } =
196
- await retry(async () => {
197
- const embedCallId = generateCallId();
190
+ return await runInTracingChannelSpan({
191
+ type: 'embedMany',
192
+ event: startEvent,
193
+ execute: async () => {
194
+ await notify({
195
+ event: startEvent,
196
+ callbacks: [resolvedOnStart, telemetryDispatcher.onStart],
197
+ });
198
198
 
199
- await notify({
200
- event: {
201
- callId,
202
- embedCallId,
203
- operationId: 'ai.embedMany.doEmbed',
204
- provider: model.provider,
205
- modelId: model.modelId,
206
- values,
207
- },
208
- callbacks: [telemetryDispatcher.onEmbedStart],
209
- });
199
+ try {
200
+ const [maxEmbeddingsPerCall, supportsParallelCalls] = await Promise.all(
201
+ [model.maxEmbeddingsPerCall, model.supportsParallelCalls],
202
+ );
203
+
204
+ if (maxEmbeddingsPerCall == null || maxEmbeddingsPerCall === Infinity) {
205
+ const { embeddings, usage, warnings, response, providerMetadata } =
206
+ await retry(async () => {
207
+ const embedCallId = generateCallId();
208
+
209
+ await notify({
210
+ event: {
211
+ callId,
212
+ embedCallId,
213
+ operationId: 'ai.embedMany.doEmbed',
214
+ provider: model.provider,
215
+ modelId: model.modelId,
216
+ values,
217
+ },
218
+ callbacks: [telemetryDispatcher.onEmbedStart],
219
+ });
220
+
221
+ const modelResponse = await model.doEmbed({
222
+ values,
223
+ abortSignal,
224
+ headers: headersWithUserAgent,
225
+ providerOptions,
226
+ });
227
+
228
+ const embeddings = modelResponse.embeddings;
229
+ const usage = modelResponse.usage ?? { tokens: NaN };
230
+
231
+ await notify({
232
+ event: {
233
+ callId,
234
+ embedCallId,
235
+ operationId: 'ai.embedMany.doEmbed',
236
+ provider: model.provider,
237
+ modelId: model.modelId,
238
+ values,
239
+ embeddings,
240
+ usage,
241
+ },
242
+ callbacks: [telemetryDispatcher.onEmbedEnd],
243
+ });
244
+
245
+ return {
246
+ embeddings,
247
+ usage,
248
+ warnings: modelResponse.warnings ?? [],
249
+ providerMetadata: modelResponse.providerMetadata,
250
+ response: modelResponse.response,
251
+ };
252
+ });
210
253
 
211
- const modelResponse = await model.doEmbed({
212
- values,
213
- abortSignal,
214
- headers: headersWithUserAgent,
215
- providerOptions,
254
+ logWarnings({
255
+ warnings,
256
+ provider: model.provider,
257
+ model: model.modelId,
216
258
  });
217
259
 
218
- const embeddings = modelResponse.embeddings;
219
- const usage = modelResponse.usage ?? { tokens: NaN };
220
-
221
260
  await notify({
222
261
  event: {
223
262
  callId,
224
- embedCallId,
225
- operationId: 'ai.embedMany.doEmbed',
263
+ operationId: 'ai.embedMany',
226
264
  provider: model.provider,
227
265
  modelId: model.modelId,
228
- values,
229
- embeddings,
266
+ value: values,
267
+ embedding: embeddings,
230
268
  usage,
269
+ warnings,
270
+ providerMetadata,
271
+ response: [response],
231
272
  },
232
- callbacks: [telemetryDispatcher.onEmbedEnd],
273
+ callbacks: [resolvedOnEnd, telemetryDispatcher.onEnd],
233
274
  });
234
275
 
235
- return {
276
+ return new DefaultEmbedManyResult({
277
+ values,
236
278
  embeddings,
237
279
  usage,
238
- warnings: modelResponse.warnings ?? [],
239
- providerMetadata: modelResponse.providerMetadata,
240
- response: modelResponse.response,
241
- };
242
- });
243
-
244
- logWarnings({
245
- warnings,
246
- provider: model.provider,
247
- model: model.modelId,
248
- });
249
-
250
- await notify({
251
- event: {
252
- callId,
253
- operationId: 'ai.embedMany',
254
- provider: model.provider,
255
- modelId: model.modelId,
256
- value: values,
257
- embedding: embeddings,
258
- usage,
259
- warnings,
260
- providerMetadata,
261
- response: [response],
262
- },
263
- callbacks: [resolvedOnEnd, telemetryDispatcher.onEnd],
264
- });
265
-
266
- return new DefaultEmbedManyResult({
267
- values,
268
- embeddings,
269
- usage,
270
- warnings,
271
- providerMetadata,
272
- responses: [response],
273
- });
274
- }
275
-
276
- const valueChunks = splitArray(values, maxEmbeddingsPerCall);
277
-
278
- const embeddings: Array<Embedding> = [];
279
- const warnings: Array<Warning> = [];
280
- const responses: Array<
281
- | {
282
- headers?: Record<string, string>;
283
- body?: unknown;
280
+ warnings,
281
+ providerMetadata,
282
+ responses: [response],
283
+ });
284
284
  }
285
- | undefined
286
- > = [];
287
- let tokens = 0;
288
- let providerMetadata: ProviderMetadata | undefined;
289
-
290
- const parallelChunks = splitArray(
291
- valueChunks,
292
- supportsParallelCalls ? maxParallelCalls : 1,
293
- );
294
-
295
- for (const parallelChunk of parallelChunks) {
296
- const results = await Promise.all(
297
- parallelChunk.map(chunk => {
298
- return retry(async () => {
299
- const embedCallId = generateCallId();
300
-
301
- await notify({
302
- event: {
303
- callId,
304
- embedCallId,
305
- operationId: 'ai.embedMany.doEmbed',
306
- provider: model.provider,
307
- modelId: model.modelId,
308
- values: chunk,
309
- },
310
- callbacks: [telemetryDispatcher.onEmbedStart],
311
- });
312
285
 
313
- const modelResponse = await model.doEmbed({
314
- values: chunk,
315
- abortSignal,
316
- headers: headersWithUserAgent,
317
- providerOptions,
318
- });
286
+ const valueChunks = splitArray(values, maxEmbeddingsPerCall);
319
287
 
320
- const chunkEmbeddings = modelResponse.embeddings;
321
- const usage = modelResponse.usage ?? { tokens: NaN };
322
-
323
- await notify({
324
- event: {
325
- callId,
326
- embedCallId,
327
- operationId: 'ai.embedMany.doEmbed',
328
- provider: model.provider,
329
- modelId: model.modelId,
330
- values: chunk,
331
- embeddings: chunkEmbeddings,
332
- usage,
333
- },
334
- callbacks: [telemetryDispatcher.onEmbedEnd],
335
- });
336
-
337
- return {
338
- embeddings: chunkEmbeddings,
339
- usage,
340
- warnings: modelResponse.warnings ?? [],
341
- providerMetadata: modelResponse.providerMetadata,
342
- response: modelResponse.response,
343
- };
344
- });
345
- }),
346
- );
347
-
348
- for (const result of results) {
349
- embeddings.push(...result.embeddings);
350
- warnings.push(...result.warnings);
351
- responses.push(result.response);
352
- tokens += result.usage.tokens;
353
- if (result.providerMetadata) {
354
- if (!providerMetadata) {
355
- providerMetadata = { ...result.providerMetadata };
356
- } else {
357
- for (const [providerName, metadata] of Object.entries(
358
- result.providerMetadata,
359
- )) {
360
- providerMetadata[providerName] = {
361
- ...(providerMetadata[providerName] ?? {}),
362
- ...metadata,
363
- };
288
+ const embeddings: Array<Embedding> = [];
289
+ const warnings: Array<Warning> = [];
290
+ const responses: Array<
291
+ | {
292
+ headers?: Record<string, string>;
293
+ body?: unknown;
294
+ }
295
+ | undefined
296
+ > = [];
297
+ let tokens = 0;
298
+ let providerMetadata: ProviderMetadata | undefined;
299
+
300
+ const parallelChunks = splitArray(
301
+ valueChunks,
302
+ supportsParallelCalls ? maxParallelCalls : 1,
303
+ );
304
+
305
+ for (const parallelChunk of parallelChunks) {
306
+ const results = await Promise.all(
307
+ parallelChunk.map(chunk => {
308
+ return retry(async () => {
309
+ const embedCallId = generateCallId();
310
+
311
+ await notify({
312
+ event: {
313
+ callId,
314
+ embedCallId,
315
+ operationId: 'ai.embedMany.doEmbed',
316
+ provider: model.provider,
317
+ modelId: model.modelId,
318
+ values: chunk,
319
+ },
320
+ callbacks: [telemetryDispatcher.onEmbedStart],
321
+ });
322
+
323
+ const modelResponse = await model.doEmbed({
324
+ values: chunk,
325
+ abortSignal,
326
+ headers: headersWithUserAgent,
327
+ providerOptions,
328
+ });
329
+
330
+ const chunkEmbeddings = modelResponse.embeddings;
331
+ const usage = modelResponse.usage ?? { tokens: NaN };
332
+
333
+ await notify({
334
+ event: {
335
+ callId,
336
+ embedCallId,
337
+ operationId: 'ai.embedMany.doEmbed',
338
+ provider: model.provider,
339
+ modelId: model.modelId,
340
+ values: chunk,
341
+ embeddings: chunkEmbeddings,
342
+ usage,
343
+ },
344
+ callbacks: [telemetryDispatcher.onEmbedEnd],
345
+ });
346
+
347
+ return {
348
+ embeddings: chunkEmbeddings,
349
+ usage,
350
+ warnings: modelResponse.warnings ?? [],
351
+ providerMetadata: modelResponse.providerMetadata,
352
+ response: modelResponse.response,
353
+ };
354
+ });
355
+ }),
356
+ );
357
+
358
+ for (const result of results) {
359
+ embeddings.push(...result.embeddings);
360
+ warnings.push(...result.warnings);
361
+ responses.push(result.response);
362
+ tokens += result.usage.tokens;
363
+ if (result.providerMetadata) {
364
+ if (!providerMetadata) {
365
+ providerMetadata = { ...result.providerMetadata };
366
+ } else {
367
+ for (const [providerName, metadata] of Object.entries(
368
+ result.providerMetadata,
369
+ )) {
370
+ providerMetadata[providerName] = {
371
+ ...(providerMetadata[providerName] ?? {}),
372
+ ...metadata,
373
+ };
374
+ }
375
+ }
364
376
  }
365
377
  }
366
378
  }
379
+
380
+ logWarnings({
381
+ warnings,
382
+ provider: model.provider,
383
+ model: model.modelId,
384
+ });
385
+
386
+ await notify({
387
+ event: {
388
+ callId,
389
+ operationId: 'ai.embedMany',
390
+ provider: model.provider,
391
+ modelId: model.modelId,
392
+ value: values,
393
+ embedding: embeddings,
394
+ usage: { tokens },
395
+ warnings,
396
+ providerMetadata,
397
+ response: responses,
398
+ },
399
+ callbacks: [resolvedOnEnd, telemetryDispatcher.onEnd],
400
+ });
401
+
402
+ return new DefaultEmbedManyResult({
403
+ values,
404
+ embeddings,
405
+ usage: { tokens },
406
+ warnings,
407
+ providerMetadata: providerMetadata,
408
+ responses,
409
+ });
410
+ } catch (error) {
411
+ await telemetryDispatcher.onError?.({ callId, error });
412
+ throw error;
367
413
  }
368
- }
369
-
370
- logWarnings({
371
- warnings,
372
- provider: model.provider,
373
- model: model.modelId,
374
- });
375
-
376
- await notify({
377
- event: {
378
- callId,
379
- operationId: 'ai.embedMany',
380
- provider: model.provider,
381
- modelId: model.modelId,
382
- value: values,
383
- embedding: embeddings,
384
- usage: { tokens },
385
- warnings,
386
- providerMetadata,
387
- response: responses,
388
- },
389
- callbacks: [resolvedOnEnd, telemetryDispatcher.onEnd],
390
- });
391
-
392
- return new DefaultEmbedManyResult({
393
- values,
394
- embeddings,
395
- usage: { tokens },
396
- warnings,
397
- providerMetadata: providerMetadata,
398
- responses,
399
- });
400
- } catch (error) {
401
- await telemetryDispatcher.onError?.({ callId, error });
402
- throw error;
403
- }
414
+ },
415
+ });
404
416
  }
405
417
 
406
418
  class DefaultEmbedManyResult implements EmbedManyResult {
@@ -7,6 +7,7 @@ export type TelemetryTracingEventType =
7
7
  | 'languageModelCall'
8
8
  | 'executeTool'
9
9
  | 'embed'
10
+ | 'embedMany'
10
11
  | 'rerank';
11
12
 
12
13
  export type TelemetryTracingChannelMessage<EVENT = unknown> = {