workers-ai-provider 3.2.0 → 3.3.0
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/README.md +24 -15
- package/dist/anthropic.d.mts +1 -1
- package/dist/{gateway-delegate-BfaUTwDZ.d.mts → gateway-delegate-BAALxuBO.d.mts} +75 -11
- package/dist/{gateway-provider-1USFWm7c.mjs → gateway-provider-BjPLZLob.mjs} +560 -168
- package/dist/gateway-provider-BjPLZLob.mjs.map +1 -0
- package/dist/gateway-provider.mjs +1 -1
- package/dist/google.d.mts +1 -1
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +388 -279
- package/dist/index.mjs.map +1 -1
- package/dist/openai.d.mts +1 -1
- package/package.json +5 -4
- package/src/gateway-delegate.ts +290 -86
- package/src/gateway-provider.ts +10 -34
- package/src/gateway-providers.ts +13 -455
- package/src/index.ts +144 -8
- package/src/resumable-stream.ts +9 -221
- package/src/streaming.ts +1 -41
- package/src/utils.ts +40 -125
- package/src/workersai-chat-language-model.ts +37 -16
- package/src/workersai-embedding-model.ts +22 -11
- package/src/workersai-error.ts +70 -0
- package/src/workersai-image-model.ts +19 -11
- package/src/workersai-reranking-model.ts +16 -5
- package/src/workersai-speech-model.ts +35 -13
- package/src/workersai-transcription-model.ts +15 -4
- package/dist/gateway-provider-1USFWm7c.mjs.map +0 -1
package/src/resumable-stream.ts
CHANGED
|
@@ -1,223 +1,11 @@
|
|
|
1
|
-
import { GatewayDelegateError } from "./gateway-delegate";
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
29
|
-
|
|
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
|
|
17
|
-
|
|
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
|
|
163
|
-
|
|
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
|
|
204
|
-
|
|
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
|
|
261
|
-
|
|
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
|
|
539
|
+
* `tool_calls`, assigning AI-SDK tool-call ids.
|
|
545
540
|
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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,
|