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.
- package/CHANGELOG.md +30 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +178 -167
- package/dist/index.js.map +1 -1
- package/dist/internal/index.d.ts +1 -1
- package/dist/internal/index.js +1 -1
- package/dist/internal/index.js.map +1 -1
- package/docs/03-ai-sdk-core/36-transcription.mdx +7 -4
- package/docs/06-advanced/11-secure-url-fetching.mdx +117 -0
- package/docs/07-reference/01-ai-sdk-core/11-stream-transcribe.mdx +13 -1
- package/package.json +3 -3
- package/src/embed/embed-many.ts +219 -207
- package/src/telemetry/tracing-channel.ts +1 -0
- package/src/transcribe/stream-transcribe.ts +13 -4
|
@@ -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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
45
|
+
"@ai-sdk/gateway": "4.0.19",
|
|
46
46
|
"@ai-sdk/provider": "4.0.3",
|
|
47
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
47
|
+
"@ai-sdk/provider-utils": "5.0.9"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@edge-runtime/vm": "^5.0.0",
|
package/src/embed/embed-many.ts
CHANGED
|
@@ -171,236 +171,248 @@ export async function embedMany({
|
|
|
171
171
|
telemetry,
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
239
|
-
providerMetadata
|
|
240
|
-
|
|
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
|
-
|
|
314
|
-
values: chunk,
|
|
315
|
-
abortSignal,
|
|
316
|
-
headers: headersWithUserAgent,
|
|
317
|
-
providerOptions,
|
|
318
|
-
});
|
|
286
|
+
const valueChunks = splitArray(values, maxEmbeddingsPerCall);
|
|
319
287
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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 {
|