1import * as ai from "@distilled.cloud/cloudflare/ai";
2import * as Effect from "effect/Effect";
3import { isResolved } from "../../Diff.ts";
4import * as Provider from "../../Provider.ts";
5import { Resource } from "../../Resource.ts";
6import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
7import type { Providers } from "../Providers.ts";
8
9export interface ModelProps {
10 /** Cloudflare-managed model name, such as `@cf/baai/bge-m3`. */
11 modelName: string;
12}
13
14export interface ModelAttributes {
15 /** Account used to validate and subscribe to the model. */
16 accountId: string;
17 /** Cloudflare-managed model name. */
18 modelName: string;
19}
20
21export type Model = Resource<
22 "Cloudflare.AI.Model",
23 ModelProps,
24 ModelAttributes,
25 never,
26 Providers
27>;
28
29/**
30 * A persisted, non-owning handle to a Cloudflare Workers AI catalog model.
31 *
32 * Deployment validates the model through its schema endpoint and records its
33 * name and account in Alchemy state. It does not provision or invoke a model.
34 * Destruction removes only the handle from state; Cloudflare owns the model.
35 *
36 * ### Selecting a Model
37 * **Example:** Observe a Workers AI model
38 * ```typescript
39 * const model = yield* Cloudflare.AI.Model("Embeddings", {
40 * modelName: "@cf/baai/bge-m3",
41 * });
42 * ```
43 *
44 * ### Subscribing to Batch Events
45 * **Example:** Model-specific Queue subscription
46 * ```typescript
47 * const model = yield* Cloudflare.AI.Model("Embeddings", {
48 * modelName: "@cf/baai/bge-m3",
49 * });
50 * const queue = yield* Cloudflare.Queues.Queue("BatchEvents");
51 * yield* Cloudflare.Queues.Subscription("ModelEvents", {
52 * source: model,
53 * events: ["batch.queued", "batch.succeeded", "batch.failed"],
54 * queueId: queue.queueId,
55 * });
56 * ```
57 * These events require asynchronous batch inference. Synchronous inference
58 * does not emit batch events.
59 *
60 * ### Referencing a Persisted Handle
61 * **Example:** Read a model handle from another stack
62 * ```typescript
63 * const model = yield* Cloudflare.AI.Model.ref("Embeddings", {
64 * stack: "models",
65 * stage: "production",
66 * });
67 * ```
68 * The handle must already be deployed. A reference neither invokes the model
69 * nor takes ownership of the source stack's state.
70 *
71 * @resource
72 * @product Workers AI
73 * @category AI
74 */
75export const Model = Resource<Model>("Cloudflare.AI.Model");
76
77const observeModel = (accountId: string, modelName: string) =>
78 ai
79 .getModelSchema({ accountId, model: modelName })
80 .pipe(Effect.as({ accountId, modelName }));
81
82export const ModelProvider = () =>
83 Provider.succeed(Model, {
84 stables: ["accountId", "modelName"],
85 diff: Effect.fn(function* ({ news, output }) {
86 const { accountId } = yield* yield* CloudflareEnvironment;
87 if (
88 (output && accountId !== output.accountId) ||
89 !isResolved(news) ||
90 (output && news.modelName !== output.modelName)
91 ) {
92 return { action: "replace" } as const;
93 }
94 }),
95 read: Effect.fn(function* ({ output, olds }) {
96 const { accountId } = yield* yield* CloudflareEnvironment;
97 const modelName = output?.modelName ?? olds?.modelName;
98 return modelName === undefined
99 ? undefined
100 : yield* observeModel(output?.accountId ?? accountId, modelName);
101 }),
102 reconcile: Effect.fn(function* ({ news }) {
103 const { accountId } = yield* yield* CloudflareEnvironment;
104 return yield* observeModel(accountId, news.modelName);
105 }),
106 delete: () => Effect.void,
107 });