1/**
2 * HTTP client for Cloudflare Clef decisions through Workers AI REST.
3 *
4 * @stability unstable
5 * @since 4.0.0
6 */
7import * as AiError from "effect/ai/AiError"
8import * as Config from "effect/Config"
9import * as Context from "effect/Context"
10import * as Effect from "effect/Effect"
11import { flow, identity } from "effect/Function"
12import * as HttpClient from "effect/http/HttpClient"
13import * as HttpClientRequest from "effect/http/HttpClientRequest"
14import * as HttpClientResponse from "effect/http/HttpClientResponse"
15import * as Layer from "effect/Layer"
16import * as Redacted from "effect/Redacted"
17import * as Schema from "effect/Schema"
18import * as CloudflareSchema from "./CloudflareSchema.ts"
19import * as Errors from "./internal/errors.ts"
20
21/**
22 * Low-level Cloudflare decision operations.
23 *
24 * @stability unstable
25 * @category services
26 * @since 4.0.0
27 */
28export interface Service {
29 readonly client: HttpClient.HttpClient
30 readonly createDecisions: (
31 request: typeof CloudflareSchema.DecisionsRequest.Encoded
32 ) => Effect.Effect<typeof CloudflareSchema.DecisionsResponse.Type, AiError.AiError>
33}
34
35/**
36 * Cloudflare Workers AI client service.
37 *
38 * @stability unstable
39 * @category services
40 * @since 4.0.0
41 */
42export class CloudflareClient
43 extends Context.Service<CloudflareClient, Service>()("@effect/ai-cloudflare/CloudflareClient")
44{}
45
46/**
47 * Account, authentication, API base URL, and HTTP customization options.
48 *
49 * @stability unstable
50 * @category options
51 * @since 4.0.0
52 */
53export interface Options {
54 readonly accountId: string
55 readonly apiKey: Redacted.Redacted<string>
56 readonly apiUrl?: string | undefined
57 readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined
58}
59
60const decodeResponse = HttpClientResponse.schemaBodyJson(Schema.Union([
61 Schema.Struct({ success: Schema.Literal(true), result: CloudflareSchema.DecisionsResponse }),
62 Schema.Struct({ success: Schema.Literal(false) })
63]))
64
65/**
66 * Builds a Cloudflare client without automatic retries.
67 *
68 * @stability unstable
69 * @category constructors
70 * @since 4.0.0
71 */
72export const make = Effect.fnUntraced(
73 function*(options: Options): Effect.fn.Return<Service, never, HttpClient.HttpClient> {
74 const base = yield* HttpClient.HttpClient
75 const client = base.pipe(
76 HttpClient.mapRequest(flow(
77 HttpClientRequest.prependUrl(options.apiUrl ?? "https://api.cloudflare.com/client/v4"),
78 HttpClientRequest.bearerToken(Redacted.value(options.apiKey)),
79 HttpClientRequest.acceptJson
80 )),
81 HttpClient.filterStatusOk,
82 options.transformClient ?? identity
83 )
84 const path = `/accounts/${encodeURIComponent(options.accountId)}/ai/run/@cf/cloudflare/`
85 return CloudflareClient.of({
86 client,
87 createDecisions: (payload) =>
88 HttpClientRequest.bodyJson(HttpClientRequest.post(path + payload.model), payload).pipe(
89 Effect.mapError((error) =>
90 AiError.make({
91 module: "CloudflareClient",
92 method: "createDecisions",
93 reason: new AiError.InvalidRequestError({ description: String(error) })
94 })
95 ),
96 Effect.flatMap(client.execute),
97 Effect.flatMap((response) =>
98 decodeResponse(response).pipe(
99 Effect.flatMap((body) => body.success ? Effect.succeed(body.result) : Errors.mapResponseError(response))
100 )
101 ),
102 Effect.catchTags({
103 HttpClientError: Errors.mapHttpClientError,
104 SchemaError: (error) =>
105 Effect.fail(AiError.make({
106 module: "CloudflareClient",
107 method: "createDecisions",
108 reason: AiError.InvalidOutputError.fromSchemaError(error)
109 }))
110 })
111 )
112 })
113 }
114)
115
116/**
117 * Provides a Cloudflare client from explicit options.
118 *
119 * @stability unstable
120 * @category layers
121 * @since 4.0.0
122 */
123export const layer = (options: Options): Layer.Layer<CloudflareClient, never, HttpClient.HttpClient> =>
124 Layer.effect(CloudflareClient, make(options))
125
126/**
127 * Provides a client from configuration, defaulting to CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN.
128 *
129 * @stability unstable
130 * @category layers
131 * @since 4.0.0
132 */
133export const layerConfig = (options?: {
134 readonly accountId?: Config.Config<string> | undefined
135 readonly apiKey?: Config.Config<Redacted.Redacted<string>> | undefined
136 readonly apiUrl?: Config.Config<string> | undefined
137 readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined
138}): Layer.Layer<CloudflareClient, Config.ConfigError, HttpClient.HttpClient> =>
139 Layer.effect(
140 CloudflareClient,
141 Effect.gen(function*() {
142 return yield* make({
143 accountId: yield* (options?.accountId ?? Config.String("CLOUDFLARE_ACCOUNT_ID")),
144 apiKey: yield* (options?.apiKey ?? Config.Redacted("CLOUDFLARE_API_TOKEN")),
145 apiUrl: options?.apiUrl ? yield* options.apiUrl : undefined,
146 transformClient: options?.transformClient
147 })
148 })
149 )