workers-ai-provider 3.2.1 → 3.3.1

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.
@@ -1,223 +1,11 @@
1
- import { GatewayDelegateError } from "./gateway-delegate";
2
-
3
1
  /**
4
- * Resumable run-path stream (RFC §7.1).
5
- *
6
- * Wraps the byte stream from a run-path response (`env.AI.run(..., {
7
- * returnRawResponse })`) so a transient mid-stream drop is recovered
8
- * transparently: the wrapper reconnects to the gateway resume endpoint and keeps
9
- * feeding bytes to the same consumer, so the downstream `@ai-sdk/*` parser never
10
- * sees the break.
11
- *
12
- * Byte alignment is the one correctness subtlety. The gateway `resume?from=N`
13
- * endpoint takes an SSE *event index* (count of `\n\n` terminators) and replays
14
- * whole events from that index. So the wrapper only ever emits *complete* events
15
- * downstream and buffers any trailing partial event. On a drop the buffered
16
- * partial is discarded and resume starts from the count of complete events
17
- * already emitted — landing exactly on the next event boundary, with no
18
- * duplicated or truncated bytes.
19
- *
20
- * Expiry: once the gateway buffer TTL (~5.5 min) elapses, resume returns 404
21
- * `{"error":"Request not found"}`. Behavior is governed by `onResumeExpired`:
22
- * `"error"` (default) surfaces a `GatewayDelegateError("resume-expired")` into
23
- * the stream; `"accept-partial"` ends the stream cleanly with whatever was
24
- * already delivered (the caller's higher layer — e.g. Think — can then continue
25
- * or regenerate).
2
+ * The resumable run-path stream engine now lives in `@cloudflare/gateway-core`
3
+ * (shared with the other Cloudflare AI Gateway packages). This module re-exports
4
+ * it so the existing `workers-ai-provider/src/resumable-stream` import path keeps
5
+ * working unchanged.
26
6
  */
27
-
28
- type AiWithFetch = Ai & {
29
- fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
30
- };
31
-
32
- export type ResumeExpiredPolicy = "error" | "accept-partial";
33
-
34
- export interface ResumableStreamOptions {
35
- /** Cloudflare AI binding (e.g. `env.AI`) — used for the resume fetch. */
36
- binding: Ai;
37
- /** Gateway id the run was issued under. */
38
- gateway: string;
39
- /** The `cf-aig-run-id` of the run to resume. */
40
- runId: string;
41
- /**
42
- * Initial run-path response body. Omit for **cross-invocation re-attach**: the
43
- * stream then starts by fetching `resume?from={fromEvent}` directly (e.g. a new
44
- * Durable Object invocation re-attaching to a run after eviction).
45
- */
46
- initial?: ReadableStream<Uint8Array>;
47
- /**
48
- * SSE event index to (re-)attach from. Defaults to `0`. Used as the starting
49
- * `from` when `initial` is omitted, and as the base offset for the event
50
- * counter (so a later reconnect resumes from the correct absolute index).
51
- */
52
- fromEvent?: number;
53
- /** What to do when the resume buffer has expired (404). Defaults to `"error"`. */
54
- onResumeExpired?: ResumeExpiredPolicy;
55
- /** Max reconnect attempts before giving up. Defaults to 5. */
56
- maxReconnects?: number;
57
- /** Fired before each reconnect with the resume `from` index and attempt number. */
58
- onReconnect?: (fromEvent: number, attempt: number) => void;
59
- /**
60
- * Fired with the cumulative SSE event offset whenever complete events are
61
- * emitted. Use it to persist `{ runId, eventOffset }` for cross-invocation
62
- * re-attach (throttle your own writes — this can fire per chunk).
63
- */
64
- onProgress?: (eventOffset: number) => void;
65
- }
66
-
67
- function concat(a: Uint8Array, b: Uint8Array): Uint8Array<ArrayBuffer> {
68
- const out = new Uint8Array(new ArrayBuffer(a.length + b.length));
69
- out.set(a, 0);
70
- out.set(b, a.length);
71
- return out;
72
- }
73
-
74
- /** Index just past the last `\n\n` in `buf`, or -1 if there is no complete event. */
75
- function lastEventBoundary(buf: Uint8Array): number {
76
- for (let i = buf.length - 2; i >= 0; i--) {
77
- if (buf[i] === 0x0a && buf[i + 1] === 0x0a) return i + 2;
78
- }
79
- return -1;
80
- }
81
-
82
- /** Count of `\n\n` terminators (= complete SSE events) in `buf`. */
83
- function countEvents(buf: Uint8Array): number {
84
- let n = 0;
85
- for (let i = 0; i + 1 < buf.length; i++) {
86
- if (buf[i] === 0x0a && buf[i + 1] === 0x0a) {
87
- n++;
88
- i++; // don't double-count "\n\n\n"
89
- }
90
- }
91
- return n;
92
- }
93
-
94
- function resumeUrl(gateway: string, runId: string, from: number): string {
95
- return `https://workers-binding.ai/ai-gateway/gateways/${gateway}/run/${runId}/resume?from=${from}`;
96
- }
97
-
98
- export function createResumableStream(options: ResumableStreamOptions): ReadableStream<Uint8Array> {
99
- const { binding, gateway, runId } = options;
100
- const maxReconnects = options.maxReconnects ?? 5;
101
- const onExpired = options.onResumeExpired ?? "error";
102
-
103
- let emittedEvents = options.fromEvent ?? 0; // absolute SSE event index reached
104
- let pending: Uint8Array<ArrayBuffer> = new Uint8Array(new ArrayBuffer(0));
105
- let reconnects = 0;
106
-
107
- // Fetch `resume?from={emittedEvents}`; on a terminal outcome (expiry / error /
108
- // network throw) it settles the controller and returns null.
109
- async function fetchResume(
110
- controller: ReadableStreamDefaultController<Uint8Array>,
111
- ): Promise<ReadableStream<Uint8Array> | null> {
112
- let res: Response;
113
- try {
114
- res = await (binding as AiWithFetch).fetch(resumeUrl(gateway, runId, emittedEvents), {
115
- method: "GET",
116
- });
117
- } catch (fetchErr) {
118
- controller.error(
119
- new GatewayDelegateError(
120
- "dispatch",
121
- `Resume request threw at event ${emittedEvents}.`,
122
- fetchErr,
123
- ),
124
- );
125
- return null;
126
- }
127
-
128
- if (res.status === 404) {
129
- if (onExpired === "accept-partial") {
130
- controller.close();
131
- return null;
132
- }
133
- controller.error(
134
- new GatewayDelegateError(
135
- "resume-expired",
136
- `Resume buffer expired (404) at event ${emittedEvents}. The gateway buffer ` +
137
- "TTL (~5.5 min) elapsed; fall back to continuation or regeneration.",
138
- ),
139
- );
140
- return null;
141
- }
142
- if (!res.ok || !res.body) {
143
- controller.error(
144
- new GatewayDelegateError(
145
- "dispatch",
146
- `Resume failed (${res.status}) at event ${emittedEvents}.`,
147
- ),
148
- );
149
- return null;
150
- }
151
- return res.body;
152
- }
153
-
154
- return new ReadableStream<Uint8Array>({
155
- async start(controller) {
156
- // In-stream wrap starts from the live body; cross-invocation re-attach
157
- // (no `initial`) starts by resuming from `fromEvent`. An initial-attach
158
- // failure is terminal — it is not charged against the reconnect budget.
159
- let current: ReadableStream<Uint8Array>;
160
- if (options.initial) {
161
- current = options.initial;
162
- } else {
163
- const body = await fetchResume(controller);
164
- if (!body) return;
165
- current = body;
166
- }
167
-
168
- for (;;) {
169
- const reader = current.getReader();
170
- try {
171
- for (;;) {
172
- const { done, value } = await reader.read();
173
- if (done) {
174
- if (pending.length > 0) {
175
- controller.enqueue(pending);
176
- pending = new Uint8Array(new ArrayBuffer(0));
177
- }
178
- controller.close();
179
- return;
180
- }
181
- if (!value || value.length === 0) continue;
182
-
183
- pending = concat(pending, value);
184
- const boundary = lastEventBoundary(pending);
185
- if (boundary > 0) {
186
- const complete = pending.slice(0, boundary);
187
- controller.enqueue(complete);
188
- emittedEvents += countEvents(complete);
189
- options.onProgress?.(emittedEvents);
190
- pending = pending.slice(boundary);
191
- }
192
- }
193
- } catch (err) {
194
- try {
195
- reader.releaseLock();
196
- } catch {
197
- // reader may already be released
198
- }
199
-
200
- if (reconnects >= maxReconnects) {
201
- controller.error(
202
- new GatewayDelegateError(
203
- "resume-expired",
204
- `Exceeded ${maxReconnects} reconnect attempts at event ${emittedEvents}.`,
205
- err,
206
- ),
207
- );
208
- return;
209
- }
210
-
211
- // Discard the unfinished partial — resume realigns on the boundary.
212
- pending = new Uint8Array(new ArrayBuffer(0));
213
- reconnects++;
214
- options.onReconnect?.(emittedEvents, reconnects);
215
-
216
- const body = await fetchResume(controller);
217
- if (!body) return;
218
- current = body;
219
- }
220
- }
221
- },
222
- });
223
- }
7
+ export {
8
+ createResumableStream,
9
+ type ResumableStreamOptions,
10
+ type ResumeExpiredPolicy,
11
+ } from "@cloudflare/gateway-core";
package/src/streaming.ts CHANGED
@@ -3,6 +3,7 @@ import type {
3
3
  LanguageModelV3StreamPart,
4
4
  LanguageModelV3Usage,
5
5
  } from "@ai-sdk/provider";
6
+ import { SSEDecoder } from "@cloudflare/gateway-core";
6
7
  import { generateId } from "ai";
7
8
  import { mapWorkersAIFinishReason } from "./map-workersai-finish-reason";
8
9
  import { mapWorkersAIUsage } from "./map-workersai-usage";
@@ -426,44 +427,3 @@ export function getMappedStream(
426
427
  }
427
428
  }
428
429
  }
429
-
430
- /**
431
- * TransformStream that decodes a raw byte stream into SSE `data:` payloads.
432
- * Each output chunk is the string content after "data: " (one per SSE event).
433
- * Handles line buffering for partial chunks.
434
- */
435
- class SSEDecoder extends TransformStream<Uint8Array, string> {
436
- constructor() {
437
- let buffer = "";
438
- const decoder = new TextDecoder();
439
-
440
- super({
441
- transform(chunk, controller) {
442
- buffer += decoder.decode(chunk, { stream: true });
443
- const lines = buffer.split("\n");
444
- buffer = lines.pop() || "";
445
-
446
- for (const line of lines) {
447
- const trimmed = line.trim();
448
- if (!trimmed) continue;
449
- if (trimmed.startsWith("data: ")) {
450
- controller.enqueue(trimmed.slice(6));
451
- } else if (trimmed.startsWith("data:")) {
452
- controller.enqueue(trimmed.slice(5));
453
- }
454
- }
455
- },
456
-
457
- flush(controller) {
458
- if (buffer.trim()) {
459
- const trimmed = buffer.trim();
460
- if (trimmed.startsWith("data: ")) {
461
- controller.enqueue(trimmed.slice(6));
462
- } else if (trimmed.startsWith("data:")) {
463
- controller.enqueue(trimmed.slice(5));
464
- }
465
- }
466
- },
467
- });
468
- }
469
- }
package/src/utils.ts CHANGED
@@ -1,6 +1,18 @@
1
1
  import type { LanguageModelV3, LanguageModelV3ToolCall } from "@ai-sdk/provider";
2
+ import {
3
+ getToolNames,
4
+ isForcedToolChoice,
5
+ normalizeMessagesForBinding as coreNormalizeMessagesForBinding,
6
+ parseLeakedToolCalls as coreParseLeakedToolCalls,
7
+ processText,
8
+ } from "@cloudflare/gateway-core";
2
9
  import { generateId } from "ai";
3
10
  import type { WorkersAIChatPrompt } from "./workersai-chat-prompt";
11
+ import { apiCallErrorFromResponse } from "./workersai-error";
12
+
13
+ // Re-exported from `@cloudflare/gateway-core` (single source of truth) so the
14
+ // existing `workers-ai-provider/src/utils` import paths keep working unchanged.
15
+ export { getToolNames, isForcedToolChoice, processText } from "@cloudflare/gateway-core";
4
16
 
5
17
  // ---------------------------------------------------------------------------
6
18
  // Workers AI quirk workarounds
@@ -13,16 +25,9 @@ import type { WorkersAIChatPrompt } from "./workersai-chat-prompt";
13
25
  * - `content` must not be null
14
26
  */
15
27
  export function normalizeMessagesForBinding(messages: WorkersAIChatPrompt): WorkersAIChatPrompt {
16
- return messages.map((msg) => {
17
- const normalized = { ...msg };
18
-
19
- // content: null → content: ""
20
- if (normalized.content === null || normalized.content === undefined) {
21
- (normalized as { content: string }).content = "";
22
- }
23
-
24
- return normalized;
25
- });
28
+ return coreNormalizeMessagesForBinding(
29
+ messages as unknown as Record<string, unknown>[],
30
+ ) as unknown as WorkersAIChatPrompt;
26
31
  }
27
32
 
28
33
  // ---------------------------------------------------------------------------
@@ -151,7 +156,9 @@ export function createRun(config: CreateRunConfig): AiRun {
151
156
  signal: signal as AbortSignal | undefined,
152
157
  });
153
158
 
154
- // Check for HTTP errors before processing
159
+ // Check for HTTP errors before processing. Surface as an APICallError so
160
+ // the AI SDK can classify retryability from the status (429 / 5xx → retry)
161
+ // and honor any Retry-After header.
155
162
  if (!response.ok && !returnRawResponse) {
156
163
  let errorBody: string;
157
164
  try {
@@ -159,9 +166,10 @@ export function createRun(config: CreateRunConfig): AiRun {
159
166
  } catch {
160
167
  errorBody = "<unable to read response body>";
161
168
  }
162
- throw new Error(
163
- `Workers AI API error (${response.status} ${response.statusText}): ${errorBody}`,
164
- );
169
+ throw apiCallErrorFromResponse(response, errorBody, {
170
+ url,
171
+ requestBodyValues: inputs,
172
+ });
165
173
  }
166
174
 
167
175
  if (returnRawResponse) {
@@ -200,9 +208,10 @@ export function createRun(config: CreateRunConfig): AiRun {
200
208
  } catch {
201
209
  errorBody = "<unable to read response body>";
202
210
  }
203
- throw new Error(
204
- `Workers AI API error (${retryResponse.status} ${retryResponse.statusText}): ${errorBody}`,
205
- );
211
+ throw apiCallErrorFromResponse(retryResponse, errorBody, {
212
+ url,
213
+ requestBodyValues: inputs,
214
+ });
206
215
  }
207
216
 
208
217
  const retryData = await retryResponse.json<{
@@ -257,9 +266,10 @@ export async function createRunBinary(
257
266
  } catch {
258
267
  errorBody = "<unable to read response body>";
259
268
  }
260
- throw new Error(
261
- `Workers AI API error (${response.status} ${response.statusText}): ${errorBody}`,
262
- );
269
+ throw apiCallErrorFromResponse(response, errorBody, {
270
+ url,
271
+ requestBodyValues: { contentType, byteLength: audioBytes.byteLength },
272
+ });
263
273
  }
264
274
 
265
275
  const data = await response.json<{ result?: Record<string, unknown> }>();
@@ -524,81 +534,24 @@ export function processPartialToolCalls(partialToolCalls: PartialToolCall[]) {
524
534
  // Forced tool-call salvage (gpt-oss harmony quirk)
525
535
  // ---------------------------------------------------------------------------
526
536
 
527
- /**
528
- * Was a specific tool forced for this request?
529
- *
530
- * True for both `tool_choice: "required"` and the named-function form
531
- * `{ type: "function", function: { name } }`.
532
- */
533
- export function isForcedToolChoice(toolChoice: unknown): boolean {
534
- if (toolChoice === "required") return true;
535
- return (
536
- typeof toolChoice === "object" &&
537
- toolChoice !== null &&
538
- (toolChoice as { type?: unknown }).type === "function"
539
- );
540
- }
541
-
542
537
  /**
543
538
  * Parse tool calls that a model leaked as JSON text instead of structured
544
- * `tool_calls`. Shared by the non-streaming salvage and the streaming buffer.
539
+ * `tool_calls`, assigning AI-SDK tool-call ids.
545
540
  *
546
- * Only JSON objects whose `name` is one of `knownToolNames` are recovered;
547
- * everything else (prose, harmony channel/role leaks like `{"name":"analysis"}`,
548
- * hallucinated names) is ignored to avoid fabricating bogus calls.
541
+ * The recovery logic (which JSON shapes count as a leaked call) lives in
542
+ * `@cloudflare/gateway-core`; this wrapper only layers the framework id on each
543
+ * neutral result so the existing `LanguageModelV3ToolCall` shape is preserved.
549
544
  */
550
545
  export function parseLeakedToolCalls(
551
546
  text: string,
552
547
  knownToolNames: Set<string>,
553
548
  ): LanguageModelV3ToolCall[] {
554
- let parsed: unknown;
555
- try {
556
- parsed = JSON.parse(text.trim());
557
- } catch {
558
- return [];
559
- }
560
-
561
- const candidates = Array.isArray(parsed) ? parsed : [parsed];
562
- const salvaged: LanguageModelV3ToolCall[] = [];
563
-
564
- for (const candidate of candidates) {
565
- if (typeof candidate !== "object" || candidate === null) continue;
566
- const obj = candidate as Record<string, unknown>;
567
- const name = obj.name;
568
- if (typeof name !== "string" || !knownToolNames.has(name)) continue;
569
-
570
- // Arguments may be wrapped (`arguments`/`parameters`) or flattened as
571
- // siblings of `name`.
572
- let args: unknown;
573
- if ("arguments" in obj) {
574
- args = obj.arguments;
575
- } else if ("parameters" in obj) {
576
- args = obj.parameters;
577
- } else {
578
- const { name: _name, ...rest } = obj;
579
- args = rest;
580
- }
581
-
582
- salvaged.push({
583
- input: typeof args === "string" ? args : JSON.stringify(args ?? {}),
584
- toolCallId: createAISDKToolCallId(undefined),
585
- type: "tool-call",
586
- toolName: name,
587
- });
588
- }
589
-
590
- return salvaged;
591
- }
592
-
593
- /** Collect the requested tool names from mapped tools. */
594
- export function getToolNames(
595
- tools: Array<{ function: { name?: string } }> | undefined,
596
- ): Set<string> {
597
- return new Set(
598
- (tools ?? [])
599
- .map((tool) => tool.function?.name)
600
- .filter((name): name is string => typeof name === "string"),
601
- );
549
+ return coreParseLeakedToolCalls(text, knownToolNames).map((call) => ({
550
+ input: call.input,
551
+ toolCallId: createAISDKToolCallId(undefined),
552
+ type: "tool-call",
553
+ toolName: call.toolName,
554
+ }));
602
555
  }
603
556
 
604
557
  /**
@@ -646,41 +599,3 @@ export function salvageToolCallsFromText(
646
599
  const salvaged = parseLeakedToolCalls(text, knownToolNames);
647
600
  return salvaged.length > 0 ? salvaged : null;
648
601
  }
649
-
650
- // ---------------------------------------------------------------------------
651
- // Text extraction
652
- // ---------------------------------------------------------------------------
653
-
654
- /**
655
- * Extract text from a Workers AI response, handling multiple response formats:
656
- * - OpenAI format: { choices: [{ message: { content: "..." } }] }
657
- * - Native format: { response: "..." }
658
- * - Structured output quirk: { response: { ... } } (object instead of string)
659
- * - Structured output quirk: { response: "{ ... }" } (JSON string)
660
- */
661
- export function processText(output: Record<string, unknown>): string | undefined {
662
- // OpenAI format
663
- const choices = output.choices as Array<{ message?: { content?: string | null } }> | undefined;
664
- const choiceContent = choices?.[0]?.message?.content;
665
- if (choiceContent != null && String(choiceContent).length > 0) {
666
- return String(choiceContent);
667
- }
668
-
669
- if ("response" in output) {
670
- const response = output.response;
671
- // Object response (structured output quirk #2)
672
- if (typeof response === "object" && response !== null) {
673
- return JSON.stringify(response);
674
- }
675
- // Numeric response (quirk #9)
676
- if (typeof response === "number") {
677
- return String(response);
678
- }
679
- // Null response (e.g., tool-call-only responses)
680
- if (response === null || response === undefined) {
681
- return undefined;
682
- }
683
- return String(response);
684
- }
685
- return undefined;
686
- }
@@ -13,6 +13,7 @@ import {
13
13
  salvageToolCallsFromText,
14
14
  } from "./utils";
15
15
  import type { WorkersAIChatSettings } from "./workersai-chat-settings";
16
+ import { normalizeBindingError } from "./workersai-error";
16
17
  import type { TextGenerationModels } from "./workersai-models";
17
18
 
18
19
  type WorkersAIChatConfig = {
@@ -277,14 +278,24 @@ export class WorkersAIChatLanguageModel implements LanguageModelV3 {
277
278
  });
278
279
  const runOptions = this.getRunOptions();
279
280
 
280
- const output = await this.config.binding.run(
281
- args.model as keyof AiModels,
282
- inputs as AiModels[keyof AiModels]["inputs"],
283
- {
284
- ...runOptions,
285
- signal: options.abortSignal,
286
- } as AiOptions,
287
- );
281
+ let output: unknown;
282
+ try {
283
+ output = await this.config.binding.run(
284
+ args.model as keyof AiModels,
285
+ inputs as AiModels[keyof AiModels]["inputs"],
286
+ {
287
+ ...runOptions,
288
+ signal: options.abortSignal,
289
+ } as AiOptions,
290
+ );
291
+ } catch (error) {
292
+ // Normalize binding failures (e.g. 3040 "out of capacity" → 429) into a
293
+ // retryable APICallError so the AI SDK's maxRetries can engage.
294
+ throw normalizeBindingError(error, {
295
+ model: args.model,
296
+ requestBodyValues: inputs,
297
+ });
298
+ }
288
299
 
289
300
  if (output instanceof ReadableStream) {
290
301
  throw new Error(
@@ -325,14 +336,24 @@ export class WorkersAIChatLanguageModel implements LanguageModelV3 {
325
336
  });
326
337
  const runOptions = this.getRunOptions();
327
338
 
328
- const response = await this.config.binding.run(
329
- args.model as keyof AiModels,
330
- inputs as AiModels[keyof AiModels]["inputs"],
331
- {
332
- ...runOptions,
333
- signal: options.abortSignal,
334
- } as AiOptions,
335
- );
339
+ let response: unknown;
340
+ try {
341
+ response = await this.config.binding.run(
342
+ args.model as keyof AiModels,
343
+ inputs as AiModels[keyof AiModels]["inputs"],
344
+ {
345
+ ...runOptions,
346
+ signal: options.abortSignal,
347
+ } as AiOptions,
348
+ );
349
+ } catch (error) {
350
+ // Normalize binding failures (e.g. 3040 "out of capacity" → 429) into a
351
+ // retryable APICallError so the AI SDK's maxRetries can engage.
352
+ throw normalizeBindingError(error, {
353
+ model: args.model,
354
+ requestBodyValues: inputs,
355
+ });
356
+ }
336
357
 
337
358
  // If the binding returned a stream, pipe it through the SSE mapper
338
359
  if (response instanceof ReadableStream) {
@@ -4,6 +4,7 @@ import type {
4
4
  EmbeddingModelV3Result,
5
5
  } from "@ai-sdk/provider";
6
6
  import { TooManyEmbeddingValuesForCallError } from "@ai-sdk/provider";
7
+ import { normalizeBindingError } from "./workersai-error";
7
8
  import type { EmbeddingModels } from "./workersai-models";
8
9
 
9
10
  export type WorkersAIEmbeddingConfig = {
@@ -72,17 +73,27 @@ export class WorkersAIEmbeddingModel implements EmbeddingModelV3 {
72
73
  ...passthroughOptions
73
74
  } = this.settings;
74
75
 
75
- const response = await this.config.binding.run(
76
- this.modelId as keyof AiModels,
77
- {
78
- text: values,
79
- },
80
- {
81
- gateway: this.config.gateway ?? gateway,
82
- signal: abortSignal,
83
- ...passthroughOptions,
84
- } as AiOptions,
85
- );
76
+ let response: unknown;
77
+ try {
78
+ response = await this.config.binding.run(
79
+ this.modelId as keyof AiModels,
80
+ {
81
+ text: values,
82
+ },
83
+ {
84
+ gateway: this.config.gateway ?? gateway,
85
+ signal: abortSignal,
86
+ ...passthroughOptions,
87
+ } as AiOptions,
88
+ );
89
+ } catch (error) {
90
+ // Normalize binding failures (e.g. 3040 "out of capacity" → 429) into a
91
+ // retryable APICallError so the AI SDK's maxRetries can engage.
92
+ throw normalizeBindingError(error, {
93
+ model: this.modelId,
94
+ requestBodyValues: { text: values },
95
+ });
96
+ }
86
97
 
87
98
  return {
88
99
  embeddings: (response as { data: number[][] }).data,