import { Context, Data, Effect, type Layer } from "effect"; import type { Ai as CloudflareAi, AiAsyncBatchResponse, AiGateway as CloudflareAiGateway, AiModelListType, AiModels, AiModelsSearchObject, AiModelsSearchParams, AiOptions, AiTextEmbeddingsInput, Ai_Cf_Qwen_Qwen3_Embedding_0_6B_Input, } from "@cloudflare/workers-types"; import * as Binding from "./Binding"; import type { WorkerEnvironment } from "./Environment"; const expectedWorkersAiBinding = "Workers AI binding with run(), gateway(), and models()"; /** Error raised when a Workers AI operation fails. */ export class WorkersAiOperationError extends Data.TaggedError("WorkersAiOperationError")<{ readonly binding: string; readonly operation: string; readonly cause: unknown; }> {} /** Typed Workers AI binding definition. */ export interface WorkersAiDefinition { /** Binding name as configured in `wrangler.jsonc`. */ readonly binding: string; } export type WorkersAiBinding = CloudflareAi; export type WorkersAiOptions = AiOptions; export type WorkersAiModelsSearchParams = AiModelsSearchParams; export type WorkersAiModelsSearchObject = AiModelsSearchObject; export type WorkersAiAsyncBatchResponse = AiAsyncBatchResponse; export type WorkersAiEmbeddingInput = | AiTextEmbeddingsInput | Ai_Cf_Qwen_Qwen3_Embedding_0_6B_Input | Record; export interface WorkersAiEmbeddingResponse { readonly data: ReadonlyArray>; readonly shape: ReadonlyArray; } export interface WorkersAiClient { readonly aiGatewayLogId: Effect.Effect; readonly run: { ( model: Name, input: { readonly requests: ReadonlyArray; }, options: WorkersAiOptions & { readonly queueRequest: true; }, ): Effect.Effect; ( model: Name, input: ModelList[Name]["inputs"], options: WorkersAiOptions & { readonly returnRawResponse: true; }, ): Effect.Effect; ( model: Name, input: ModelList[Name]["inputs"], options: WorkersAiOptions & { readonly websocket: true; }, ): Effect.Effect; ( model: Name, input: ModelList[Name]["inputs"] & { readonly stream: true; }, options?: WorkersAiOptions, ): Effect.Effect; ( model: Name, input: ModelList[Name]["inputs"], options?: WorkersAiOptions, ): Effect.Effect; , Output = Record>( model: string, input: Input, options?: WorkersAiOptions, ): Effect.Effect; }; readonly runEmbedding: ( model: string, input: Input, options?: WorkersAiOptions, ) => Effect.Effect; readonly models: ( params?: WorkersAiModelsSearchParams, ) => Effect.Effect, WorkersAiOperationError>; readonly gateway: ( gatewayId: string, ) => Effect.Effect; readonly unsafeRaw: Effect.Effect>; readonly definition: WorkersAiDefinition; } declare const WorkersAiServiceTypeId: unique symbol; /** Nominal service marker for Workers AI services created with {@link make}. */ export interface WorkersAiService { readonly [WorkersAiServiceTypeId]: { readonly id: Id; }; } export type LayerOptions = { readonly binding: string; }; export interface TagClass< Self, Id extends string, ModelList extends AiModelListType = AiModels, > extends Context.ServiceClass> { readonly id: Id; readonly layer: ( options: LayerOptions, ) => Layer.Layer< Self, Binding.BindingNotFoundError | Binding.BindingValidationError, WorkerEnvironment >; } const workersAiError = (binding: string, operation: string, cause: unknown) => new WorkersAiOperationError({ binding, operation, cause }); const tryWorkersAiPromise = ( binding: string, operation: string, evaluate: () => Promise, ): Effect.Effect => Effect.tryPromise({ try: evaluate, catch: (cause) => workersAiError(binding, operation, cause), }); const tryWorkersAiSync = ( binding: string, operation: string, evaluate: () => A, ): Effect.Effect => Effect.try({ try: evaluate, catch: (cause) => workersAiError(binding, operation, cause), }); const hasFunction = (value: object, key: string): boolean => typeof Reflect.get(value, key) === "function"; export const isWorkersAiBinding = (value: unknown): value is WorkersAiBinding => typeof value === "object" && value !== null && hasFunction(value, "run") && hasFunction(value, "gateway") && hasFunction(value, "models"); export const embeddingResponse = (value: { readonly data?: ReadonlyArray>; readonly shape?: ReadonlyArray; }): WorkersAiEmbeddingResponse => ({ data: value.data ?? [], shape: value.shape ?? [], }); export const makeClient = (definition: WorkersAiDefinition) => (ai: WorkersAiBinding): WorkersAiClient => { const run = ((model: string, input: Record, options?: WorkersAiOptions) => tryWorkersAiPromise(definition.binding, "run", () => ai.run(model as string & {}, input, options), )) as WorkersAiClient["run"]; return { definition, aiGatewayLogId: tryWorkersAiSync( definition.binding, "aiGatewayLogId", () => ai.aiGatewayLogId, ), run, runEmbedding: (model, input, options) => run(model, input, options).pipe( Effect.map((response) => embeddingResponse( response as { readonly data?: ReadonlyArray>; readonly shape?: ReadonlyArray; }, ), ), ), models: (params) => tryWorkersAiPromise(definition.binding, "models", () => ai.models(params)), gateway: (gatewayId) => tryWorkersAiSync(definition.binding, "gateway", () => ai.gateway(gatewayId)), unsafeRaw: Effect.succeed(ai), }; }; export const layer = ( tag: Context.Service>, definition: WorkersAiDefinition, ) => Binding.layer( tag, definition.binding, (value): value is WorkersAiBinding => isWorkersAiBinding(value), makeClient(definition), { expected: expectedWorkersAiBinding, }, ); export const make = (id: Id) => Tag>()(id); export const Tag = () => (id: Id) => { const tag = Context.Service>()(id); const makeLayer = (definition: LayerOptions) => layer(tag, definition); return Object.assign(tag, { id, layer: makeLayer, }) as TagClass; };