workers-ai-provider
Version:
Workers AI Provider for the vercel AI SDK
172 lines (155 loc) • 4.6 kB
text/typescript
import type { ImageModelV3, SharedV3Warning } from "@ai-sdk/provider";
import { normalizeBindingError } from "./workersai-error";
import type { WorkersAIImageSettings } from "./workersai-image-settings";
import type { ImageGenerationModels } from "./workersai-models";
export type WorkersAIImageConfig = {
provider: string;
binding: Ai;
gateway?: GatewayOptions;
};
export class WorkersAIImageModel implements ImageModelV3 {
readonly specificationVersion = "v3";
get maxImagesPerCall(): number {
return this.settings.maxImagesPerCall ?? 1;
}
get provider(): string {
return this.config.provider;
}
constructor(
readonly modelId: ImageGenerationModels,
readonly settings: WorkersAIImageSettings,
readonly config: WorkersAIImageConfig,
) {}
async doGenerate({
prompt,
n,
size,
aspectRatio,
seed,
abortSignal,
}: Parameters<ImageModelV3["doGenerate"]>[0]): Promise<
Awaited<ReturnType<ImageModelV3["doGenerate"]>>
> {
const { width, height } = getDimensionsFromSizeString(size);
const warnings: Array<SharedV3Warning> = [];
if (aspectRatio != null) {
warnings.push({
details: "This model does not support aspect ratio. Use `size` instead.",
feature: "aspectRatio",
type: "unsupported",
});
}
const generateImage = async () => {
const inputs = {
height,
prompt: prompt ?? "",
seed,
width,
};
let output: unknown;
try {
output = (await this.config.binding.run(this.modelId as keyof AiModels, inputs, {
gateway: this.config.gateway,
signal: abortSignal,
} as AiOptions)) as unknown;
} catch (error) {
// Normalize binding failures (e.g. 3040 "out of capacity" → 429) into
// a retryable APICallError so the AI SDK's maxRetries can engage.
throw normalizeBindingError(error, {
model: this.modelId,
requestBodyValues: inputs,
});
}
return toUint8Array(output);
};
const images: Uint8Array[] = await Promise.all(
Array.from({ length: n }, () => generateImage()),
);
return {
images,
response: {
headers: {},
modelId: this.modelId,
timestamp: new Date(),
},
warnings,
};
}
}
function getDimensionsFromSizeString(size: string | undefined) {
const [width, height] = size?.split("x") ?? [undefined, undefined];
return {
height: parseInteger(height),
width: parseInteger(width),
};
}
function parseInteger(value?: string) {
if (value === "" || !value) return undefined;
const number = Number(value);
return Number.isInteger(number) ? number : undefined;
}
/**
* Convert various output types from binding.run() to Uint8Array.
* Workers AI image models return different types depending on the runtime:
* - ReadableStream<Uint8Array> (most common in workerd)
* - Uint8Array / ArrayBuffer (direct binary)
* - Response (needs .arrayBuffer())
* - { image: string } with base64 data
*/
async function toUint8Array(output: unknown): Promise<Uint8Array> {
if (output instanceof Uint8Array) {
return output;
}
if (output instanceof ArrayBuffer) {
return new Uint8Array(output);
}
if (output instanceof ReadableStream) {
const reader = (output as ReadableStream<Uint8Array>).getReader();
const chunks: Uint8Array[] = [];
let totalLength = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalLength += value.length;
}
const result = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
}
// Response object (e.g., from REST shim)
if (output instanceof Response) {
return new Uint8Array(await output.arrayBuffer());
}
// Object with binary-like properties
if (typeof output === "object" && output !== null) {
const obj = output as Record<string, unknown>;
// { image: base64string }
if (typeof obj.image === "string") {
return Uint8Array.from(atob(obj.image), (c) => c.charCodeAt(0));
}
// { data: Uint8Array }
if (obj.data instanceof Uint8Array) {
return obj.data;
}
// { data: ArrayBuffer }
if (obj.data instanceof ArrayBuffer) {
return new Uint8Array(obj.data);
}
// Try to get a body if it looks response-like
if (typeof obj.arrayBuffer === "function") {
return new Uint8Array(await (obj as unknown as Response).arrayBuffer());
}
}
throw new Error(
`Unexpected output type from image model. Got ${Object.prototype.toString.call(output)} with keys: ${
typeof output === "object" && output !== null
? JSON.stringify(Object.keys(output))
: "N/A"
}`,
);
}