@effect-uai/fal 0.17.0 → 0.18.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
CHANGED
|
@@ -12,18 +12,18 @@ rest of the open-weights field behind one key.
|
|
|
12
12
|
pnpm add @effect-uai/fal @effect-uai/core effect
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
ESM-only. Requires `effect
|
|
15
|
+
ESM-only. Requires `effect@>=4.0.1` and `@effect-uai/core` as peers.
|
|
16
16
|
|
|
17
17
|
## Usage
|
|
18
18
|
|
|
19
19
|
```ts
|
|
20
20
|
import { Config, Effect, Layer } from "effect"
|
|
21
|
-
import { FetchHttpClient } from "effect/
|
|
21
|
+
import { FetchHttpClient } from "effect/http"
|
|
22
22
|
import { layer as falLayer } from "@effect-uai/fal/FalImageGenerator"
|
|
23
23
|
|
|
24
24
|
const provider = Layer.unwrap(
|
|
25
25
|
Effect.gen(function* () {
|
|
26
|
-
const apiKey = yield* Config.
|
|
26
|
+
const apiKey = yield* Config.Redacted("FAL_API_KEY")
|
|
27
27
|
return falLayer({ apiKey })
|
|
28
28
|
}),
|
|
29
29
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FalImageEditModel, FalImageModel } from "./models.mjs";
|
|
2
2
|
import { Context, Effect, Layer, Option, Redacted, Schema } from "effect";
|
|
3
|
-
import { HttpClient } from "effect/
|
|
3
|
+
import { HttpClient } from "effect/http";
|
|
4
4
|
import * as AiError from "@effect-uai/core/AiError";
|
|
5
5
|
import { GeneratedImage, ImageSource } from "@effect-uai/core/Image";
|
|
6
6
|
import { CommonImageEditRequest, CommonImageGenerateRequest, ImageGenerator, ImageGeneratorService, ImageResponse } from "@effect-uai/core/ImageGenerator";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
2
|
import { httpError, missingImageField, transportFailure } from "./codec.mjs";
|
|
3
|
-
import { Context, Effect,
|
|
4
|
-
import {
|
|
3
|
+
import { Context, Effect, Layer, Match, Option, Redacted, Ref, Result, Schema, Stream, pipe } from "effect";
|
|
4
|
+
import { Base64 } from "effect/encoding";
|
|
5
|
+
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
5
6
|
import * as AiError from "@effect-uai/core/AiError";
|
|
6
7
|
import * as Capabilities from "@effect-uai/core/Capabilities";
|
|
7
8
|
import { imageBase64, imageUrl, pixelsOf } from "@effect-uai/core/Image";
|
|
@@ -78,7 +79,7 @@ const imageSizeOf = (request) => Effect.gen(function* () {
|
|
|
78
79
|
* every `ImageSource` has a wire form here. This is the one adapter
|
|
79
80
|
* where passing a `url` reference costs nothing.
|
|
80
81
|
*/
|
|
81
|
-
const referenceUrl = Match.type().pipe(Match.tag("url", (i) => i.url), Match.tag("base64", (i) => `data:${i.mimeType};base64,${i.base64}`), Match.tag("bytes", (i) => `data:${i.mimeType};base64,${
|
|
82
|
+
const referenceUrl = Match.type().pipe(Match.tag("url", (i) => i.url), Match.tag("base64", (i) => `data:${i.mimeType};base64,${i.base64}`), Match.tag("bytes", (i) => `data:${i.mimeType};base64,${Base64.encode(i.bytes)}`), Match.exhaustive);
|
|
82
83
|
/** `_urls` takes the set, `_url` takes one. True of every spelling fal uses. */
|
|
83
84
|
const takesMany = (field) => field.endsWith("_urls");
|
|
84
85
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FalImageGenerator.mjs","names":[],"sources":["../src/FalImageGenerator.ts"],"sourcesContent":["import {\n Context,\n Effect,\n Encoding,\n Layer,\n Match,\n Option,\n Redacted,\n Ref,\n Result,\n Schema,\n Stream,\n pipe,\n} from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport * as Capabilities from \"@effect-uai/core/Capabilities\"\nimport type { GeneratedImage, ImageSource } from \"@effect-uai/core/Image\"\nimport { imageBase64, imageUrl, pixelsOf } from \"@effect-uai/core/Image\"\nimport type {\n CommonImageEditRequest,\n CommonImageGenerateRequest,\n CommonStreamImageEditRequest,\n CommonStreamImageRequest,\n ImageGeneratorService,\n ImageResponse,\n ImageStreamEvent,\n} from \"@effect-uai/core/ImageGenerator\"\nimport { ImageGenerator } from \"@effect-uai/core/ImageGenerator\"\nimport { PROVIDER, httpError, missingImageField, transportFailure } from \"./codec.js\"\nimport type { FalImageEditModel, FalImageModel } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/** A named preset (`\"landscape_4_3\"`, `\"auto_2K\"`, …) or exact pixels. */\nexport type FalImageSize = string | { readonly width: number; readonly height: number }\n\n/**\n * Wire knobs shared by most fal image endpoints. fal is not one API but a\n * catalogue of models behind one envelope, so only `prompt`, `num_images`\n * and the output envelope are dependable across all of them; the fields\n * here are the ones the image endpoints agree on.\n */\ntype FalImageKnobs = {\n /** Sent as `image_size`. Takes precedence over the portable ratio and tier. */\n readonly imageSize?: FalImageSize\n readonly seed?: number\n readonly outputFormat?: \"jpeg\" | \"png\" | \"webp\"\n /** Diffusion knobs, open-weights models only. */\n readonly numInferenceSteps?: number\n readonly guidanceScale?: number\n readonly enableSafetyChecker?: boolean\n /**\n * `false` returns fal CDN links instead of inline bytes. The default is\n * `true`: every other adapter here hands back bytes, and fal's links\n * expire.\n */\n readonly syncMode?: boolean\n /**\n * Fields only one endpoint takes: `acceleration`, `loras`,\n * `safety_tolerance`, `thinking_level`, `aspect_ratio`. Merged last, so\n * it also overrides anything derived above. Snake case, as the wire\n * spells it. The escape hatch exists because fal's schemas are\n * per-model and tabling them would go stale in a week.\n */\n readonly input?: Record<string, unknown>\n}\n\nexport type FalImageGenerateRequest = Omit<CommonImageGenerateRequest, \"model\"> & {\n readonly model: FalImageModel\n} & FalImageKnobs\n\nexport type FalImageEditRequest = Omit<CommonImageEditRequest, \"model\"> & {\n /** Edit is a *different endpoint* on fal, not a flag on the same one. */\n readonly model: FalImageEditModel\n} & FalImageKnobs\n\n/**\n * Per-image extras on `GeneratedImage.providerData.fal`. All optional:\n * which of them an endpoint reports is per-model. Read with\n * {@link imageDataOf}.\n */\nconst ImageData = Schema.Struct({\n width: Schema.optional(Schema.Number),\n height: Schema.optional(Schema.Number),\n fileName: Schema.optional(Schema.String),\n fileSize: Schema.optional(Schema.Number),\n})\nexport type FalImageData = typeof ImageData.Type\n\n/** Response-level extras on `ImageResponse.providerData.fal`. */\nconst ResponseData = Schema.Struct({\n /** The seed the request resolved to. Send it back for the same image. */\n seed: Schema.optional(Schema.Number),\n timings: Schema.optional(Schema.Unknown),\n hasNsfwConcepts: Schema.optional(Schema.Array(Schema.Boolean)),\n})\nexport type FalResponseData = typeof ResponseData.Type\n\nexport type FalImageGeneratorService = {\n readonly generate: (\n request: FalImageGenerateRequest,\n ) => Effect.Effect<ImageResponse, AiError.AiError>\n readonly edit: (request: FalImageEditRequest) => Effect.Effect<ImageResponse, AiError.AiError>\n /** Both always `Unsupported`: `ImageStreaming` is not registered by {@link layer}. */\n readonly streamGeneration: ImageGeneratorService[\"streamGeneration\"]\n readonly streamEdit: ImageGeneratorService[\"streamEdit\"]\n}\n\n/**\n * Provider-typed service tag. Yield this for exact pixel sizes, seeds,\n * diffusion knobs and the per-endpoint `input` passthrough; yield the\n * generic `ImageGenerator` tag for provider-portable code. Both are\n * registered by {@link layer}.\n */\nexport class FalImageGenerator extends Context.Service<\n FalImageGenerator,\n FalImageGeneratorService\n>()(\"@betalyra/effect-uai/providers/fal/FalImageGenerator\") {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Codec - size\n//\n// The portable request carries a ratio and a tier; fal endpoints take\n// `image_size`, either a named preset or `{ width, height }`. Pixels are\n// derived because the preset names differ per model family while the\n// object form is the shape they share.\n// ---------------------------------------------------------------------------\n\n/**\n * The typed `imageSize` wins over the portable pair, which is then\n * dropped. `undefined` means \"send no size\" and let the endpoint pick.\n */\nexport const imageSizeOf = (\n request: Pick<FalImageGenerateRequest, \"imageSize\" | \"aspectRatio\" | \"resolution\">,\n): Effect.Effect<FalImageSize | undefined, AiError.AiError> =>\n Effect.gen(function* () {\n if (request.imageSize !== undefined) {\n const overridden = \"`imageSize` sets the dimensions directly.\"\n yield* Capabilities.warnDroppedWhen(request.aspectRatio, {\n provider: PROVIDER,\n capability: \"aspectRatio\",\n field: \"aspectRatio\",\n reason: overridden,\n })\n yield* Capabilities.warnDroppedWhen(request.resolution, {\n provider: PROVIDER,\n capability: \"resolution\",\n field: \"resolution\",\n reason: overridden,\n })\n return request.imageSize\n }\n return yield* Result.match(pixelsOf(request), {\n onSuccess: Effect.succeed<FalImageSize | undefined>,\n onFailure: (reason) =>\n Effect.fail(\n new AiError.InvalidRequest({\n provider: PROVIDER,\n param: \"aspectRatio\",\n raw: `${reason} Set \\`imageSize\\` directly to bypass the derivation.`,\n }),\n ),\n })\n })\n\n// ---------------------------------------------------------------------------\n// Codec - request\n// ---------------------------------------------------------------------------\n\n/**\n * fal reads reference images from URLs, and a data URI is a URL, so\n * every `ImageSource` has a wire form here. This is the one adapter\n * where passing a `url` reference costs nothing.\n */\nconst referenceUrl: (image: ImageSource) => string = Match.type<ImageSource>().pipe(\n Match.tag(\"url\", (i) => i.url),\n Match.tag(\"base64\", (i) => `data:${i.mimeType};base64,${i.base64}`),\n Match.tag(\"bytes\", (i) => `data:${i.mimeType};base64,${Encoding.encodeBase64(i.bytes)}`),\n Match.exhaustive,\n)\n\n/** `_urls` takes the set, `_url` takes one. True of every spelling fal uses. */\nconst takesMany = (field: string): boolean => field.endsWith(\"_urls\")\n\n/**\n * References under the field the endpoint reads. A single-image field\n * gets the last reference rather than the first: callers pass anchors\n * before the image being changed, so the last is the one meant.\n */\nconst referenceFields = (\n images: ReadonlyArray<ImageSource>,\n field: string,\n): Record<string, unknown> => {\n const last = images[images.length - 1]\n return images.length === 0 || last === undefined\n ? {}\n : takesMany(field)\n ? { [field]: images.map(referenceUrl) }\n : { [field]: referenceUrl(last) }\n}\n\nexport type WireBody = Record<string, unknown> & {\n readonly prompt: string\n}\n\nexport const buildBody = (\n request: FalImageGenerateRequest,\n images: ReadonlyArray<ImageSource>,\n field: string = \"image_urls\",\n): Effect.Effect<WireBody, AiError.AiError> =>\n Effect.gen(function* () {\n const imageSize = yield* imageSizeOf(request)\n const references = referenceFields(images, field)\n return {\n prompt: request.prompt,\n // Inline bytes rather than an expiring CDN link, unless asked otherwise.\n sync_mode: request.syncMode ?? true,\n ...(imageSize !== undefined && { image_size: imageSize }),\n ...(request.n !== undefined && { num_images: request.n }),\n ...(request.seed !== undefined && { seed: request.seed }),\n ...(request.outputFormat !== undefined && { output_format: request.outputFormat }),\n ...(request.numInferenceSteps !== undefined && {\n num_inference_steps: request.numInferenceSteps,\n }),\n ...(request.guidanceScale !== undefined && { guidance_scale: request.guidanceScale }),\n ...(request.enableSafetyChecker !== undefined && {\n enable_safety_checker: request.enableSafetyChecker,\n }),\n ...references,\n ...request.input,\n }\n })\n\n// ---------------------------------------------------------------------------\n// Codec - response\n// ---------------------------------------------------------------------------\n\n/**\n * Every field is optional *and* nullable, because fal's per-model\n * schemas disagree about both: one endpoint declares `width` required,\n * the next declares it nullable and sends `null`. Only `url` is\n * dependable. Anything beyond it reaches the caller through\n * `providerData` rather than a promised field.\n */\nconst ImageFile = Schema.Struct({\n url: Schema.String,\n content_type: Schema.optional(Schema.NullOr(Schema.String)),\n width: Schema.optional(Schema.NullOr(Schema.Number)),\n height: Schema.optional(Schema.NullOr(Schema.Number)),\n file_name: Schema.optional(Schema.NullOr(Schema.String)),\n file_size: Schema.optional(Schema.NullOr(Schema.Number)),\n})\ntype ImageFile = typeof ImageFile.Type\n\n/** A handful of endpoints return `image` singular; the rest return `images`. */\nconst Wire = Schema.Struct({\n images: Schema.optional(Schema.NullOr(Schema.Array(ImageFile))),\n image: Schema.optional(Schema.NullOr(ImageFile)),\n has_nsfw_concepts: Schema.optional(Schema.NullOr(Schema.Array(Schema.Boolean))),\n /** The seed a request resolved to, which is what makes a run repeatable. */\n seed: Schema.optional(Schema.NullOr(Schema.Number)),\n timings: Schema.optional(Schema.NullOr(Schema.Unknown)),\n})\ntype Wire = typeof Wire.Type\n\nconst decodeBody = Schema.decodeUnknownEffect(Schema.fromJsonString(Wire))\n\n/**\n * Decode from the raw body rather than parsed JSON, so a shape this\n * adapter cannot read comes back with the body attached. Output schemas\n * are per-model here, and \"we could not read it\" is unactionable without\n * the thing we could not read.\n */\nexport const decodeWire = (body: string): Effect.Effect<Wire, AiError.AiError> =>\n decodeBody(body).pipe(\n Effect.mapError(\n () =>\n new AiError.GenerationFailed({\n provider: PROVIDER,\n message: \"fal returned a body with no readable image field.\",\n // `sync_mode` puts a whole image in the body, so the head is\n // what gets attached: enough to see the shape, not megabytes.\n raw: body.length > 2000 ? `${body.slice(0, 2000)}… (${body.length} bytes)` : body,\n }),\n ),\n )\n\nconst DATA_URI = /^data:([^;,]+);base64,(.*)$/s\n\n/** `sync_mode` returns a data URI; without it, a link that expires. */\nconst dataUri = (url: string): Option.Option<readonly [string, string]> =>\n pipe(\n Option.fromNullOr(DATA_URI.exec(url)),\n Option.flatMap(([, mimeType, base64]) =>\n mimeType !== undefined && base64 !== undefined\n ? Option.some([mimeType, base64] as const)\n : Option.none(),\n ),\n )\n\n/** Only what the endpoint actually reported; absent keys stay absent. */\nconst imageExtras = (file: ImageFile): FalImageData => ({\n ...(file.width != null && { width: file.width }),\n ...(file.height != null && { height: file.height }),\n ...(file.file_name != null && { fileName: file.file_name }),\n ...(file.file_size != null && { fileSize: file.file_size }),\n})\n\n/** No fal image model documents a watermark, so none is claimed. */\nconst generatedImage = (file: ImageFile): GeneratedImage => {\n const extras = imageExtras(file)\n return {\n image: Option.match(dataUri(file.url), {\n onNone: () => imageUrl(file.url, file.content_type ?? undefined),\n onSome: ([mimeType, base64]) => imageBase64(base64, mimeType),\n }),\n ...(Object.keys(extras).length > 0 && { providerData: { fal: extras } }),\n }\n}\n\n/** Every image flagged means nothing usable came back, which is a refusal. */\nconst refused = (wire: Wire): boolean => {\n const flags = wire.has_nsfw_concepts ?? []\n return flags.length > 0 && flags.every((flagged) => flagged)\n}\n\nexport const toResponse = (wire: Wire): Effect.Effect<ImageResponse, AiError.AiError> => {\n const files = [...(wire.images ?? []), ...(wire.image == null ? [] : [wire.image])]\n if (refused(wire)) {\n return Effect.fail(\n new AiError.ContentFiltered({\n provider: PROVIDER,\n reason: \"The safety checker flagged every generated image.\",\n raw: wire,\n }),\n )\n }\n const extras: FalResponseData = {\n ...(wire.seed != null && { seed: wire.seed }),\n ...(wire.timings != null && { timings: wire.timings }),\n ...(wire.has_nsfw_concepts != null && { hasNsfwConcepts: wire.has_nsfw_concepts }),\n }\n // fal bills per image and per megapixel, not per token, so there is no\n // usage to report.\n return files.length === 0\n ? Effect.fail(\n new AiError.GenerationFailed({\n provider: PROVIDER,\n message: \"fal returned no image.\",\n raw: wire,\n }),\n )\n : Effect.succeed({\n images: files.map(generatedImage),\n usage: {},\n ...(Object.keys(extras).length > 0 && { providerData: { fal: extras } }),\n })\n}\n\n// ---------------------------------------------------------------------------\n// Reading the extras\n// ---------------------------------------------------------------------------\n\nconst decodeImageData = Schema.decodeUnknownOption(Schema.Struct({ fal: ImageData }))\nconst decodeResponseData = Schema.decodeUnknownOption(Schema.Struct({ fal: ResponseData }))\n\n/**\n * Pixel dimensions and file metadata for one image, where the endpoint\n * reported them. `providerData` is a shared slot, so this reads only the\n * `fal` key and returns `None` for an image from another provider.\n */\nexport const imageDataOf = (image: GeneratedImage): Option.Option<FalImageData> =>\n Option.map(decodeImageData(image.providerData), (d) => d.fal)\n\n/** The seed a request resolved to, plus timings and safety flags. */\nexport const responseDataOf = (response: ImageResponse): Option.Option<FalResponseData> =>\n Option.map(decodeResponseData(response.providerData), (d) => d.fal)\n\n// ---------------------------------------------------------------------------\n// HTTP\n//\n// The synchronous endpoint: one request, one response. The queue API\n// (`queue.fal.run`) exists for jobs long enough to outlive a connection,\n// which image generation is not.\n// ---------------------------------------------------------------------------\n\nconst host = (cfg: Config): string => cfg.baseUrl ?? \"https://fal.run\"\n\ntype Attempt = { readonly status: number; readonly body: string }\n\nconst attempt = (\n cfg: Config,\n model: string,\n request: FalImageGenerateRequest,\n images: ReadonlyArray<ImageSource>,\n field: string,\n): Effect.Effect<Attempt, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const body = yield* buildBody(request, images, field)\n const response = yield* client\n .execute(\n HttpClientRequest.post(`${host(cfg)}/${model}`).pipe(\n HttpClientRequest.setHeader(\"authorization\", `Key ${Redacted.value(cfg.apiKey)}`),\n HttpClientRequest.bodyJsonUnsafe(body),\n ),\n )\n .pipe(Effect.mapError(transportFailure))\n const text = yield* response.text.pipe(Effect.mapError(transportFailure))\n return { status: response.status, body: text }\n })\n\nconst finish = (attempt: Attempt): Effect.Effect<ImageResponse, AiError.AiError> =>\n attempt.status >= 400\n ? Effect.flatMap(httpError(attempt.status, attempt.body), Effect.fail)\n : Effect.flatMap(decodeWire(attempt.body), toResponse)\n\n/**\n * `learned` remembers what an endpoint asked for, so only the first call\n * to one this adapter has no mapping for pays for the correction. The\n * retry is free of inference cost: fal validates before it runs anything.\n */\nconst requestImages = (\n cfg: Config,\n model: string,\n request: FalImageGenerateRequest,\n images: ReadonlyArray<ImageSource>,\n learned: Ref.Ref<Record<string, string>>,\n): Effect.Effect<ImageResponse, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n // Current endpoints take `image_urls`; older ones want `image_url` or\n // another spelling, which their 422 names. Neither the id nor the\n // owner namespace predicts it, so guess and correct.\n const remembered = yield* Ref.get(learned)\n const field = remembered[model] ?? \"image_urls\"\n const first = yield* attempt(cfg, model, request, images, field)\n if (first.status < 400 || images.length === 0) return yield* finish(first)\n\n const asked = yield* Effect.map(missingImageField(first.body), (name) =>\n Option.flatMap(name, (wanted) =>\n wanted === field ? Option.none<string>() : Option.some(wanted),\n ),\n )\n if (Option.isNone(asked)) return yield* finish(first)\n\n yield* Ref.update(learned, (known) => ({ ...known, [model]: asked.value }))\n return yield* Effect.flatMap(attempt(cfg, model, request, images, asked.value), finish)\n })\n\n/**\n * fal's `/stream` endpoint exists, but the events it carries are defined\n * by each model rather than by fal, and no image endpoint documents\n * partial images. {@link layer} therefore does not register\n * `ImageStreaming`, and calling the top-level `streamGeneration` helper\n * against this Layer alone fails to typecheck; this branch covers the\n * service being used directly.\n */\nconst streamUnsupported = (capability: string): Stream.Stream<ImageStreamEvent, AiError.AiError> =>\n Stream.fail(\n new AiError.Unsupported({\n provider: PROVIDER,\n capability,\n reason:\n \"No fal image endpoint documents a partial-image stream. Use `generate` or `edit` and render when it resolves.\",\n }),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\nexport const make = (\n cfg: Config,\n): Effect.Effect<FalImageGeneratorService, never, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const learned = yield* Ref.make<Record<string, string>>({})\n return {\n generate: (request) =>\n requestImages(cfg, request.model, request, [], learned).pipe(\n Effect.provideService(HttpClient.HttpClient, client),\n ),\n edit: (request) =>\n requestImages(cfg, request.model, request, request.images, learned).pipe(\n Effect.provideService(HttpClient.HttpClient, client),\n ),\n streamGeneration: () => streamUnsupported(\"streamGeneration\"),\n streamEdit: () => streamUnsupported(\"streamEdit\"),\n }\n })\n\n/**\n * Layer registering the provider-typed tag and the generic\n * `ImageGenerator`. A `CommonImageGenerateRequest` is structurally a\n * `FalImageGenerateRequest` with no vendor knobs set, so the generic\n * registration forwards directly.\n *\n * Does NOT register `ImageStreaming`.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<FalImageGenerator | ImageGenerator, never, HttpClient.HttpClient> =>\n Layer.merge(\n Layer.effect(FalImageGenerator, make(cfg)),\n Layer.effect(\n ImageGenerator,\n Effect.map(make(cfg), (s): ImageGeneratorService => ({\n generate: (request: CommonImageGenerateRequest) =>\n s.generate(request as FalImageGenerateRequest),\n edit: (request: CommonImageEditRequest) => s.edit(request as FalImageEditRequest),\n streamGeneration: (_request: CommonStreamImageRequest) =>\n streamUnsupported(\"streamGeneration\"),\n streamEdit: (_request: CommonStreamImageEditRequest) => streamUnsupported(\"streamEdit\"),\n })),\n ),\n )\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FalImageGenerator.mjs","names":[],"sources":["../src/FalImageGenerator.ts"],"sourcesContent":["import {\n Context,\n Effect,\n Layer,\n Match,\n Option,\n Redacted,\n Ref,\n Result,\n Schema,\n Stream,\n pipe,\n} from \"effect\"\nimport { Base64 } from \"effect/encoding\"\nimport { HttpClient, HttpClientRequest } from \"effect/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport * as Capabilities from \"@effect-uai/core/Capabilities\"\nimport type { GeneratedImage, ImageSource } from \"@effect-uai/core/Image\"\nimport { imageBase64, imageUrl, pixelsOf } from \"@effect-uai/core/Image\"\nimport type {\n CommonImageEditRequest,\n CommonImageGenerateRequest,\n CommonStreamImageEditRequest,\n CommonStreamImageRequest,\n ImageGeneratorService,\n ImageResponse,\n ImageStreamEvent,\n} from \"@effect-uai/core/ImageGenerator\"\nimport { ImageGenerator } from \"@effect-uai/core/ImageGenerator\"\nimport { PROVIDER, httpError, missingImageField, transportFailure } from \"./codec.js\"\nimport type { FalImageEditModel, FalImageModel } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/** A named preset (`\"landscape_4_3\"`, `\"auto_2K\"`, …) or exact pixels. */\nexport type FalImageSize = string | { readonly width: number; readonly height: number }\n\n/**\n * Wire knobs shared by most fal image endpoints. fal is not one API but a\n * catalogue of models behind one envelope, so only `prompt`, `num_images`\n * and the output envelope are dependable across all of them; the fields\n * here are the ones the image endpoints agree on.\n */\ntype FalImageKnobs = {\n /** Sent as `image_size`. Takes precedence over the portable ratio and tier. */\n readonly imageSize?: FalImageSize\n readonly seed?: number\n readonly outputFormat?: \"jpeg\" | \"png\" | \"webp\"\n /** Diffusion knobs, open-weights models only. */\n readonly numInferenceSteps?: number\n readonly guidanceScale?: number\n readonly enableSafetyChecker?: boolean\n /**\n * `false` returns fal CDN links instead of inline bytes. The default is\n * `true`: every other adapter here hands back bytes, and fal's links\n * expire.\n */\n readonly syncMode?: boolean\n /**\n * Fields only one endpoint takes: `acceleration`, `loras`,\n * `safety_tolerance`, `thinking_level`, `aspect_ratio`. Merged last, so\n * it also overrides anything derived above. Snake case, as the wire\n * spells it. The escape hatch exists because fal's schemas are\n * per-model and tabling them would go stale in a week.\n */\n readonly input?: Record<string, unknown>\n}\n\nexport type FalImageGenerateRequest = Omit<CommonImageGenerateRequest, \"model\"> & {\n readonly model: FalImageModel\n} & FalImageKnobs\n\nexport type FalImageEditRequest = Omit<CommonImageEditRequest, \"model\"> & {\n /** Edit is a *different endpoint* on fal, not a flag on the same one. */\n readonly model: FalImageEditModel\n} & FalImageKnobs\n\n/**\n * Per-image extras on `GeneratedImage.providerData.fal`. All optional:\n * which of them an endpoint reports is per-model. Read with\n * {@link imageDataOf}.\n */\nconst ImageData = Schema.Struct({\n width: Schema.optional(Schema.Number),\n height: Schema.optional(Schema.Number),\n fileName: Schema.optional(Schema.String),\n fileSize: Schema.optional(Schema.Number),\n})\nexport type FalImageData = typeof ImageData.Type\n\n/** Response-level extras on `ImageResponse.providerData.fal`. */\nconst ResponseData = Schema.Struct({\n /** The seed the request resolved to. Send it back for the same image. */\n seed: Schema.optional(Schema.Number),\n timings: Schema.optional(Schema.Unknown),\n hasNsfwConcepts: Schema.optional(Schema.Array(Schema.Boolean)),\n})\nexport type FalResponseData = typeof ResponseData.Type\n\nexport type FalImageGeneratorService = {\n readonly generate: (\n request: FalImageGenerateRequest,\n ) => Effect.Effect<ImageResponse, AiError.AiError>\n readonly edit: (request: FalImageEditRequest) => Effect.Effect<ImageResponse, AiError.AiError>\n /** Both always `Unsupported`: `ImageStreaming` is not registered by {@link layer}. */\n readonly streamGeneration: ImageGeneratorService[\"streamGeneration\"]\n readonly streamEdit: ImageGeneratorService[\"streamEdit\"]\n}\n\n/**\n * Provider-typed service tag. Yield this for exact pixel sizes, seeds,\n * diffusion knobs and the per-endpoint `input` passthrough; yield the\n * generic `ImageGenerator` tag for provider-portable code. Both are\n * registered by {@link layer}.\n */\nexport class FalImageGenerator extends Context.Service<\n FalImageGenerator,\n FalImageGeneratorService\n>()(\"@betalyra/effect-uai/providers/fal/FalImageGenerator\") {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Codec - size\n//\n// The portable request carries a ratio and a tier; fal endpoints take\n// `image_size`, either a named preset or `{ width, height }`. Pixels are\n// derived because the preset names differ per model family while the\n// object form is the shape they share.\n// ---------------------------------------------------------------------------\n\n/**\n * The typed `imageSize` wins over the portable pair, which is then\n * dropped. `undefined` means \"send no size\" and let the endpoint pick.\n */\nexport const imageSizeOf = (\n request: Pick<FalImageGenerateRequest, \"imageSize\" | \"aspectRatio\" | \"resolution\">,\n): Effect.Effect<FalImageSize | undefined, AiError.AiError> =>\n Effect.gen(function* () {\n if (request.imageSize !== undefined) {\n const overridden = \"`imageSize` sets the dimensions directly.\"\n yield* Capabilities.warnDroppedWhen(request.aspectRatio, {\n provider: PROVIDER,\n capability: \"aspectRatio\",\n field: \"aspectRatio\",\n reason: overridden,\n })\n yield* Capabilities.warnDroppedWhen(request.resolution, {\n provider: PROVIDER,\n capability: \"resolution\",\n field: \"resolution\",\n reason: overridden,\n })\n return request.imageSize\n }\n return yield* Result.match(pixelsOf(request), {\n onSuccess: Effect.succeed<FalImageSize | undefined>,\n onFailure: (reason) =>\n Effect.fail(\n new AiError.InvalidRequest({\n provider: PROVIDER,\n param: \"aspectRatio\",\n raw: `${reason} Set \\`imageSize\\` directly to bypass the derivation.`,\n }),\n ),\n })\n })\n\n// ---------------------------------------------------------------------------\n// Codec - request\n// ---------------------------------------------------------------------------\n\n/**\n * fal reads reference images from URLs, and a data URI is a URL, so\n * every `ImageSource` has a wire form here. This is the one adapter\n * where passing a `url` reference costs nothing.\n */\nconst referenceUrl: (image: ImageSource) => string = Match.type<ImageSource>().pipe(\n Match.tag(\"url\", (i) => i.url),\n Match.tag(\"base64\", (i) => `data:${i.mimeType};base64,${i.base64}`),\n Match.tag(\"bytes\", (i) => `data:${i.mimeType};base64,${Base64.encode(i.bytes)}`),\n Match.exhaustive,\n)\n\n/** `_urls` takes the set, `_url` takes one. True of every spelling fal uses. */\nconst takesMany = (field: string): boolean => field.endsWith(\"_urls\")\n\n/**\n * References under the field the endpoint reads. A single-image field\n * gets the last reference rather than the first: callers pass anchors\n * before the image being changed, so the last is the one meant.\n */\nconst referenceFields = (\n images: ReadonlyArray<ImageSource>,\n field: string,\n): Record<string, unknown> => {\n const last = images[images.length - 1]\n return images.length === 0 || last === undefined\n ? {}\n : takesMany(field)\n ? { [field]: images.map(referenceUrl) }\n : { [field]: referenceUrl(last) }\n}\n\nexport type WireBody = Record<string, unknown> & {\n readonly prompt: string\n}\n\nexport const buildBody = (\n request: FalImageGenerateRequest,\n images: ReadonlyArray<ImageSource>,\n field: string = \"image_urls\",\n): Effect.Effect<WireBody, AiError.AiError> =>\n Effect.gen(function* () {\n const imageSize = yield* imageSizeOf(request)\n const references = referenceFields(images, field)\n return {\n prompt: request.prompt,\n // Inline bytes rather than an expiring CDN link, unless asked otherwise.\n sync_mode: request.syncMode ?? true,\n ...(imageSize !== undefined && { image_size: imageSize }),\n ...(request.n !== undefined && { num_images: request.n }),\n ...(request.seed !== undefined && { seed: request.seed }),\n ...(request.outputFormat !== undefined && { output_format: request.outputFormat }),\n ...(request.numInferenceSteps !== undefined && {\n num_inference_steps: request.numInferenceSteps,\n }),\n ...(request.guidanceScale !== undefined && { guidance_scale: request.guidanceScale }),\n ...(request.enableSafetyChecker !== undefined && {\n enable_safety_checker: request.enableSafetyChecker,\n }),\n ...references,\n ...request.input,\n }\n })\n\n// ---------------------------------------------------------------------------\n// Codec - response\n// ---------------------------------------------------------------------------\n\n/**\n * Every field is optional *and* nullable, because fal's per-model\n * schemas disagree about both: one endpoint declares `width` required,\n * the next declares it nullable and sends `null`. Only `url` is\n * dependable. Anything beyond it reaches the caller through\n * `providerData` rather than a promised field.\n */\nconst ImageFile = Schema.Struct({\n url: Schema.String,\n content_type: Schema.optional(Schema.NullOr(Schema.String)),\n width: Schema.optional(Schema.NullOr(Schema.Number)),\n height: Schema.optional(Schema.NullOr(Schema.Number)),\n file_name: Schema.optional(Schema.NullOr(Schema.String)),\n file_size: Schema.optional(Schema.NullOr(Schema.Number)),\n})\ntype ImageFile = typeof ImageFile.Type\n\n/** A handful of endpoints return `image` singular; the rest return `images`. */\nconst Wire = Schema.Struct({\n images: Schema.optional(Schema.NullOr(Schema.Array(ImageFile))),\n image: Schema.optional(Schema.NullOr(ImageFile)),\n has_nsfw_concepts: Schema.optional(Schema.NullOr(Schema.Array(Schema.Boolean))),\n /** The seed a request resolved to, which is what makes a run repeatable. */\n seed: Schema.optional(Schema.NullOr(Schema.Number)),\n timings: Schema.optional(Schema.NullOr(Schema.Unknown)),\n})\ntype Wire = typeof Wire.Type\n\nconst decodeBody = Schema.decodeUnknownEffect(Schema.fromJsonString(Wire))\n\n/**\n * Decode from the raw body rather than parsed JSON, so a shape this\n * adapter cannot read comes back with the body attached. Output schemas\n * are per-model here, and \"we could not read it\" is unactionable without\n * the thing we could not read.\n */\nexport const decodeWire = (body: string): Effect.Effect<Wire, AiError.AiError> =>\n decodeBody(body).pipe(\n Effect.mapError(\n () =>\n new AiError.GenerationFailed({\n provider: PROVIDER,\n message: \"fal returned a body with no readable image field.\",\n // `sync_mode` puts a whole image in the body, so the head is\n // what gets attached: enough to see the shape, not megabytes.\n raw: body.length > 2000 ? `${body.slice(0, 2000)}… (${body.length} bytes)` : body,\n }),\n ),\n )\n\nconst DATA_URI = /^data:([^;,]+);base64,(.*)$/s\n\n/** `sync_mode` returns a data URI; without it, a link that expires. */\nconst dataUri = (url: string): Option.Option<readonly [string, string]> =>\n pipe(\n Option.fromNullOr(DATA_URI.exec(url)),\n Option.flatMap(([, mimeType, base64]) =>\n mimeType !== undefined && base64 !== undefined\n ? Option.some([mimeType, base64] as const)\n : Option.none(),\n ),\n )\n\n/** Only what the endpoint actually reported; absent keys stay absent. */\nconst imageExtras = (file: ImageFile): FalImageData => ({\n ...(file.width != null && { width: file.width }),\n ...(file.height != null && { height: file.height }),\n ...(file.file_name != null && { fileName: file.file_name }),\n ...(file.file_size != null && { fileSize: file.file_size }),\n})\n\n/** No fal image model documents a watermark, so none is claimed. */\nconst generatedImage = (file: ImageFile): GeneratedImage => {\n const extras = imageExtras(file)\n return {\n image: Option.match(dataUri(file.url), {\n onNone: () => imageUrl(file.url, file.content_type ?? undefined),\n onSome: ([mimeType, base64]) => imageBase64(base64, mimeType),\n }),\n ...(Object.keys(extras).length > 0 && { providerData: { fal: extras } }),\n }\n}\n\n/** Every image flagged means nothing usable came back, which is a refusal. */\nconst refused = (wire: Wire): boolean => {\n const flags = wire.has_nsfw_concepts ?? []\n return flags.length > 0 && flags.every((flagged) => flagged)\n}\n\nexport const toResponse = (wire: Wire): Effect.Effect<ImageResponse, AiError.AiError> => {\n const files = [...(wire.images ?? []), ...(wire.image == null ? [] : [wire.image])]\n if (refused(wire)) {\n return Effect.fail(\n new AiError.ContentFiltered({\n provider: PROVIDER,\n reason: \"The safety checker flagged every generated image.\",\n raw: wire,\n }),\n )\n }\n const extras: FalResponseData = {\n ...(wire.seed != null && { seed: wire.seed }),\n ...(wire.timings != null && { timings: wire.timings }),\n ...(wire.has_nsfw_concepts != null && { hasNsfwConcepts: wire.has_nsfw_concepts }),\n }\n // fal bills per image and per megapixel, not per token, so there is no\n // usage to report.\n return files.length === 0\n ? Effect.fail(\n new AiError.GenerationFailed({\n provider: PROVIDER,\n message: \"fal returned no image.\",\n raw: wire,\n }),\n )\n : Effect.succeed({\n images: files.map(generatedImage),\n usage: {},\n ...(Object.keys(extras).length > 0 && { providerData: { fal: extras } }),\n })\n}\n\n// ---------------------------------------------------------------------------\n// Reading the extras\n// ---------------------------------------------------------------------------\n\nconst decodeImageData = Schema.decodeUnknownOption(Schema.Struct({ fal: ImageData }))\nconst decodeResponseData = Schema.decodeUnknownOption(Schema.Struct({ fal: ResponseData }))\n\n/**\n * Pixel dimensions and file metadata for one image, where the endpoint\n * reported them. `providerData` is a shared slot, so this reads only the\n * `fal` key and returns `None` for an image from another provider.\n */\nexport const imageDataOf = (image: GeneratedImage): Option.Option<FalImageData> =>\n Option.map(decodeImageData(image.providerData), (d) => d.fal)\n\n/** The seed a request resolved to, plus timings and safety flags. */\nexport const responseDataOf = (response: ImageResponse): Option.Option<FalResponseData> =>\n Option.map(decodeResponseData(response.providerData), (d) => d.fal)\n\n// ---------------------------------------------------------------------------\n// HTTP\n//\n// The synchronous endpoint: one request, one response. The queue API\n// (`queue.fal.run`) exists for jobs long enough to outlive a connection,\n// which image generation is not.\n// ---------------------------------------------------------------------------\n\nconst host = (cfg: Config): string => cfg.baseUrl ?? \"https://fal.run\"\n\ntype Attempt = { readonly status: number; readonly body: string }\n\nconst attempt = (\n cfg: Config,\n model: string,\n request: FalImageGenerateRequest,\n images: ReadonlyArray<ImageSource>,\n field: string,\n): Effect.Effect<Attempt, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const body = yield* buildBody(request, images, field)\n const response = yield* client\n .execute(\n HttpClientRequest.post(`${host(cfg)}/${model}`).pipe(\n HttpClientRequest.setHeader(\"authorization\", `Key ${Redacted.value(cfg.apiKey)}`),\n HttpClientRequest.bodyJsonUnsafe(body),\n ),\n )\n .pipe(Effect.mapError(transportFailure))\n const text = yield* response.text.pipe(Effect.mapError(transportFailure))\n return { status: response.status, body: text }\n })\n\nconst finish = (attempt: Attempt): Effect.Effect<ImageResponse, AiError.AiError> =>\n attempt.status >= 400\n ? Effect.flatMap(httpError(attempt.status, attempt.body), Effect.fail)\n : Effect.flatMap(decodeWire(attempt.body), toResponse)\n\n/**\n * `learned` remembers what an endpoint asked for, so only the first call\n * to one this adapter has no mapping for pays for the correction. The\n * retry is free of inference cost: fal validates before it runs anything.\n */\nconst requestImages = (\n cfg: Config,\n model: string,\n request: FalImageGenerateRequest,\n images: ReadonlyArray<ImageSource>,\n learned: Ref.Ref<Record<string, string>>,\n): Effect.Effect<ImageResponse, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n // Current endpoints take `image_urls`; older ones want `image_url` or\n // another spelling, which their 422 names. Neither the id nor the\n // owner namespace predicts it, so guess and correct.\n const remembered = yield* Ref.get(learned)\n const field = remembered[model] ?? \"image_urls\"\n const first = yield* attempt(cfg, model, request, images, field)\n if (first.status < 400 || images.length === 0) return yield* finish(first)\n\n const asked = yield* Effect.map(missingImageField(first.body), (name) =>\n Option.flatMap(name, (wanted) =>\n wanted === field ? Option.none<string>() : Option.some(wanted),\n ),\n )\n if (Option.isNone(asked)) return yield* finish(first)\n\n yield* Ref.update(learned, (known) => ({ ...known, [model]: asked.value }))\n return yield* Effect.flatMap(attempt(cfg, model, request, images, asked.value), finish)\n })\n\n/**\n * fal's `/stream` endpoint exists, but the events it carries are defined\n * by each model rather than by fal, and no image endpoint documents\n * partial images. {@link layer} therefore does not register\n * `ImageStreaming`, and calling the top-level `streamGeneration` helper\n * against this Layer alone fails to typecheck; this branch covers the\n * service being used directly.\n */\nconst streamUnsupported = (capability: string): Stream.Stream<ImageStreamEvent, AiError.AiError> =>\n Stream.fail(\n new AiError.Unsupported({\n provider: PROVIDER,\n capability,\n reason:\n \"No fal image endpoint documents a partial-image stream. Use `generate` or `edit` and render when it resolves.\",\n }),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\nexport const make = (\n cfg: Config,\n): Effect.Effect<FalImageGeneratorService, never, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const learned = yield* Ref.make<Record<string, string>>({})\n return {\n generate: (request) =>\n requestImages(cfg, request.model, request, [], learned).pipe(\n Effect.provideService(HttpClient.HttpClient, client),\n ),\n edit: (request) =>\n requestImages(cfg, request.model, request, request.images, learned).pipe(\n Effect.provideService(HttpClient.HttpClient, client),\n ),\n streamGeneration: () => streamUnsupported(\"streamGeneration\"),\n streamEdit: () => streamUnsupported(\"streamEdit\"),\n }\n })\n\n/**\n * Layer registering the provider-typed tag and the generic\n * `ImageGenerator`. A `CommonImageGenerateRequest` is structurally a\n * `FalImageGenerateRequest` with no vendor knobs set, so the generic\n * registration forwards directly.\n *\n * Does NOT register `ImageStreaming`.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<FalImageGenerator | ImageGenerator, never, HttpClient.HttpClient> =>\n Layer.merge(\n Layer.effect(FalImageGenerator, make(cfg)),\n Layer.effect(\n ImageGenerator,\n Effect.map(make(cfg), (s): ImageGeneratorService => ({\n generate: (request: CommonImageGenerateRequest) =>\n s.generate(request as FalImageGenerateRequest),\n edit: (request: CommonImageEditRequest) => s.edit(request as FalImageEditRequest),\n streamGeneration: (_request: CommonStreamImageRequest) =>\n streamUnsupported(\"streamGeneration\"),\n streamEdit: (_request: CommonStreamImageEditRequest) => streamUnsupported(\"streamEdit\"),\n })),\n ),\n )\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-uai/fal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "fal provider for @effect-uai/core.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -44,13 +44,13 @@
|
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"effect": "4.0.
|
|
47
|
+
"effect": "4.0.1",
|
|
48
48
|
"typescript": "^7.0.2",
|
|
49
|
-
"@effect-uai/core": "0.
|
|
49
|
+
"@effect-uai/core": "0.18.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"@effect-uai/core": ">=0.14.0 <1",
|
|
53
|
-
"effect": ">=4.0.
|
|
53
|
+
"effect": ">=4.0.1 <5.0.0"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"build": "tsdown",
|
package/src/FalImageGenerator.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Context,
|
|
3
3
|
Effect,
|
|
4
|
-
Encoding,
|
|
5
4
|
Layer,
|
|
6
5
|
Match,
|
|
7
6
|
Option,
|
|
@@ -12,7 +11,8 @@ import {
|
|
|
12
11
|
Stream,
|
|
13
12
|
pipe,
|
|
14
13
|
} from "effect"
|
|
15
|
-
import {
|
|
14
|
+
import { Base64 } from "effect/encoding"
|
|
15
|
+
import { HttpClient, HttpClientRequest } from "effect/http"
|
|
16
16
|
import * as AiError from "@effect-uai/core/AiError"
|
|
17
17
|
import * as Capabilities from "@effect-uai/core/Capabilities"
|
|
18
18
|
import type { GeneratedImage, ImageSource } from "@effect-uai/core/Image"
|
|
@@ -183,7 +183,7 @@ export const imageSizeOf = (
|
|
|
183
183
|
const referenceUrl: (image: ImageSource) => string = Match.type<ImageSource>().pipe(
|
|
184
184
|
Match.tag("url", (i) => i.url),
|
|
185
185
|
Match.tag("base64", (i) => `data:${i.mimeType};base64,${i.base64}`),
|
|
186
|
-
Match.tag("bytes", (i) => `data:${i.mimeType};base64,${
|
|
186
|
+
Match.tag("bytes", (i) => `data:${i.mimeType};base64,${Base64.encode(i.bytes)}`),
|
|
187
187
|
Match.exhaustive,
|
|
188
188
|
)
|
|
189
189
|
|