@tanstack/ai-fal 0.0.1 → 0.6.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tanner Linsley
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,36 @@
1
+ import { BaseImageAdapter } from '@tanstack/ai/adapters';
2
+ import { FalClientConfig } from '../utils.js';
3
+ import { ImageGenerationOptions, ImageGenerationResult } from '@tanstack/ai';
4
+ import { FalImageProviderOptions, FalModel, FalModelImageSize } from '../model-meta.js';
5
+ /**
6
+ * fal.ai image generation adapter with full type inference.
7
+ *
8
+ * Uses fal.ai's comprehensive type system to provide autocomplete
9
+ *
10
+ * and type safety for all 600+ supported models.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const adapter = falImage('fal-ai/flux/dev')
15
+ * const result = await adapter.generateImages({
16
+ * model: 'fal-ai/flux/dev',
17
+ * prompt: 'a cat',
18
+ * modelOptions: {
19
+ * num_inference_steps: 28, // Type-safe! Autocomplete works
20
+ * guidance_scale: 3.5,
21
+ * },
22
+ * })
23
+ * ```
24
+ */
25
+ export declare class FalImageAdapter<TModel extends FalModel> extends BaseImageAdapter<TModel, FalImageProviderOptions<TModel>, Record<TModel, FalImageProviderOptions<TModel>>, Record<TModel, FalModelImageSize<TModel>>> {
26
+ readonly kind: "image";
27
+ readonly name: "fal";
28
+ constructor(model: TModel, config?: FalClientConfig);
29
+ generateImages(options: ImageGenerationOptions<FalImageProviderOptions<TModel>, FalModelImageSize<TModel>>): Promise<ImageGenerationResult>;
30
+ private buildInput;
31
+ protected generateId(): string;
32
+ private transformResponse;
33
+ private parseImage;
34
+ }
35
+ export declare function createFalImage<TModel extends FalModel>(model: TModel, config?: FalClientConfig): FalImageAdapter<TModel>;
36
+ export declare function falImage<TModel extends FalModel>(model: TModel, config?: FalClientConfig): FalImageAdapter<TModel>;
@@ -0,0 +1,71 @@
1
+ import { fal } from "@fal-ai/client";
2
+ import { BaseImageAdapter } from "@tanstack/ai/adapters";
3
+ import { configureFalClient, generateId } from "../utils/client.js";
4
+ import { mapSizeToFalFormat } from "../image/image-provider-options.js";
5
+ class FalImageAdapter extends BaseImageAdapter {
6
+ constructor(model, config) {
7
+ super({}, model);
8
+ this.kind = "image";
9
+ this.name = "fal";
10
+ configureFalClient(config);
11
+ }
12
+ async generateImages(options) {
13
+ const input = this.buildInput(options);
14
+ const result = await fal.subscribe(this.model, { input });
15
+ return this.transformResponse(result);
16
+ }
17
+ buildInput(options) {
18
+ const sizeParams = mapSizeToFalFormat(options.size);
19
+ const input = {
20
+ ...options.modelOptions,
21
+ ...sizeParams,
22
+ prompt: options.prompt,
23
+ num_images: options.numberOfImages
24
+ };
25
+ return input;
26
+ }
27
+ generateId() {
28
+ return generateId(this.name);
29
+ }
30
+ transformResponse(response) {
31
+ const images = [];
32
+ const data = response.data;
33
+ if ("images" in data && Array.isArray(data.images)) {
34
+ for (const img of data.images) {
35
+ images.push(this.parseImage(img));
36
+ }
37
+ } else if ("image" in data && data.image && typeof data.image === "object") {
38
+ images.push(this.parseImage(data.image));
39
+ }
40
+ return {
41
+ id: response.requestId || this.generateId(),
42
+ model: this.model,
43
+ images
44
+ };
45
+ }
46
+ parseImage(img) {
47
+ const url = img.url;
48
+ if (url.startsWith("data:")) {
49
+ const base64Match = url.match(/^data:image\/[^;]+;base64,(.+)$/);
50
+ if (base64Match) {
51
+ return {
52
+ b64Json: base64Match[1],
53
+ url
54
+ };
55
+ }
56
+ }
57
+ return { url };
58
+ }
59
+ }
60
+ function createFalImage(model, config) {
61
+ return new FalImageAdapter(model, config);
62
+ }
63
+ function falImage(model, config) {
64
+ return createFalImage(model, config);
65
+ }
66
+ export {
67
+ FalImageAdapter,
68
+ createFalImage,
69
+ falImage
70
+ };
71
+ //# sourceMappingURL=image.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image.js","sources":["../../../src/adapters/image.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\nimport { BaseImageAdapter } from '@tanstack/ai/adapters'\nimport { configureFalClient, generateId as utilGenerateId } from '../utils'\nimport { mapSizeToFalFormat } from '../image/image-provider-options'\nimport type { OutputType, Result } from '@fal-ai/client'\nimport type { FalClientConfig } from '../utils'\nimport type {\n GeneratedImage,\n ImageGenerationOptions,\n ImageGenerationResult,\n} from '@tanstack/ai'\nimport type {\n FalImageProviderOptions,\n FalModel,\n FalModelImageSize,\n FalModelInput,\n} from '../model-meta'\n\n/**\n * fal.ai image generation adapter with full type inference.\n *\n * Uses fal.ai's comprehensive type system to provide autocomplete\n *\n * and type safety for all 600+ supported models.\n *\n * @example\n * ```typescript\n * const adapter = falImage('fal-ai/flux/dev')\n * const result = await adapter.generateImages({\n * model: 'fal-ai/flux/dev',\n * prompt: 'a cat',\n * modelOptions: {\n * num_inference_steps: 28, // Type-safe! Autocomplete works\n * guidance_scale: 3.5,\n * },\n * })\n * ```\n */\nexport class FalImageAdapter<TModel extends FalModel> extends BaseImageAdapter<\n TModel,\n FalImageProviderOptions<TModel>,\n Record<TModel, FalImageProviderOptions<TModel>>,\n Record<TModel, FalModelImageSize<TModel>>\n> {\n readonly kind = 'image' as const\n readonly name = 'fal' as const\n\n constructor(model: TModel, config?: FalClientConfig) {\n super({}, model)\n configureFalClient(config)\n }\n\n async generateImages(\n options: ImageGenerationOptions<\n FalImageProviderOptions<TModel>,\n FalModelImageSize<TModel>\n >,\n ): Promise<ImageGenerationResult> {\n const input = this.buildInput(options)\n const result = await fal.subscribe(this.model, { input })\n return this.transformResponse(result)\n }\n\n private buildInput(\n options: ImageGenerationOptions<FalImageProviderOptions<TModel>, string>,\n ): FalModelInput<TModel> {\n const sizeParams = mapSizeToFalFormat(options.size)\n const input = {\n ...options.modelOptions,\n ...sizeParams,\n prompt: options.prompt,\n num_images: options.numberOfImages,\n } as FalModelInput<TModel>\n return input\n }\n\n protected override generateId(): string {\n return utilGenerateId(this.name)\n }\n\n private transformResponse(\n response: Result<OutputType<TModel>>,\n ): ImageGenerationResult {\n const images: Array<GeneratedImage> = []\n const data = response.data\n\n // Handle array of images (most models return { images: [...] })\n if ('images' in data && Array.isArray(data.images)) {\n for (const img of data.images) {\n images.push(this.parseImage(img))\n }\n }\n // Handle single image response (some models return { image: {...} })\n else if ('image' in data && data.image && typeof data.image === 'object') {\n images.push(this.parseImage(data.image))\n }\n\n return {\n id: response.requestId || this.generateId(),\n model: this.model,\n images,\n }\n }\n\n private parseImage(img: { url: string }): GeneratedImage {\n const url = img.url\n // Check if it's a base64 data URL\n if (url.startsWith('data:')) {\n const base64Match = url.match(/^data:image\\/[^;]+;base64,(.+)$/)\n if (base64Match) {\n return {\n b64Json: base64Match[1],\n url,\n }\n }\n }\n return { url }\n }\n}\n\nexport function createFalImage<TModel extends FalModel>(\n model: TModel,\n config?: FalClientConfig,\n): FalImageAdapter<TModel> {\n return new FalImageAdapter(model, config)\n}\n\nexport function falImage<TModel extends FalModel>(\n model: TModel,\n config?: FalClientConfig,\n): FalImageAdapter<TModel> {\n return createFalImage(model, config)\n}\n"],"names":["utilGenerateId"],"mappings":";;;;AAsCO,MAAM,wBAAiD,iBAK5D;AAAA,EAIA,YAAY,OAAe,QAA0B;AACnD,UAAM,CAAA,GAAI,KAAK;AAJjB,SAAS,OAAO;AAChB,SAAS,OAAO;AAId,uBAAmB,MAAM;AAAA,EAC3B;AAAA,EAEA,MAAM,eACJ,SAIgC;AAChC,UAAM,QAAQ,KAAK,WAAW,OAAO;AACrC,UAAM,SAAS,MAAM,IAAI,UAAU,KAAK,OAAO,EAAE,OAAO;AACxD,WAAO,KAAK,kBAAkB,MAAM;AAAA,EACtC;AAAA,EAEQ,WACN,SACuB;AACvB,UAAM,aAAa,mBAAmB,QAAQ,IAAI;AAClD,UAAM,QAAQ;AAAA,MACZ,GAAG,QAAQ;AAAA,MACX,GAAG;AAAA,MACH,QAAQ,QAAQ;AAAA,MAChB,YAAY,QAAQ;AAAA,IAAA;AAEtB,WAAO;AAAA,EACT;AAAA,EAEmB,aAAqB;AACtC,WAAOA,WAAe,KAAK,IAAI;AAAA,EACjC;AAAA,EAEQ,kBACN,UACuB;AACvB,UAAM,SAAgC,CAAA;AACtC,UAAM,OAAO,SAAS;AAGtB,QAAI,YAAY,QAAQ,MAAM,QAAQ,KAAK,MAAM,GAAG;AAClD,iBAAW,OAAO,KAAK,QAAQ;AAC7B,eAAO,KAAK,KAAK,WAAW,GAAG,CAAC;AAAA,MAClC;AAAA,IACF,WAES,WAAW,QAAQ,KAAK,SAAS,OAAO,KAAK,UAAU,UAAU;AACxE,aAAO,KAAK,KAAK,WAAW,KAAK,KAAK,CAAC;AAAA,IACzC;AAEA,WAAO;AAAA,MACL,IAAI,SAAS,aAAa,KAAK,WAAA;AAAA,MAC/B,OAAO,KAAK;AAAA,MACZ;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEQ,WAAW,KAAsC;AACvD,UAAM,MAAM,IAAI;AAEhB,QAAI,IAAI,WAAW,OAAO,GAAG;AAC3B,YAAM,cAAc,IAAI,MAAM,iCAAiC;AAC/D,UAAI,aAAa;AACf,eAAO;AAAA,UACL,SAAS,YAAY,CAAC;AAAA,UACtB;AAAA,QAAA;AAAA,MAEJ;AAAA,IACF;AACA,WAAO,EAAE,IAAA;AAAA,EACX;AACF;AAEO,SAAS,eACd,OACA,QACyB;AACzB,SAAO,IAAI,gBAAgB,OAAO,MAAM;AAC1C;AAEO,SAAS,SACd,OACA,QACyB;AACzB,SAAO,eAAe,OAAO,MAAM;AACrC;"}
@@ -0,0 +1,34 @@
1
+ import { BaseVideoAdapter } from '@tanstack/ai/adapters';
2
+ import { VideoGenerationOptions, VideoJobResult, VideoStatusResult, VideoUrlResult } from '@tanstack/ai';
3
+ import { FalModel, FalModelVideoSize, FalVideoProviderOptions } from '../model-meta.js';
4
+ import { FalClientConfig } from '../utils.js';
5
+ /**
6
+ * fal.ai video generation adapter.
7
+ * Supports MiniMax, Luma, Kling, Hunyuan, and other fal.ai video models.
8
+ *
9
+ * Uses fal.ai's comprehensive type system to provide autocomplete
10
+ * and type safety for all supported video models.
11
+ *
12
+ * @experimental Video generation is an experimental feature and may change.
13
+ */
14
+ export declare class FalVideoAdapter<TModel extends FalModel> extends BaseVideoAdapter<TModel, FalVideoProviderOptions<TModel>, Record<TModel, FalVideoProviderOptions<TModel>>, Record<TModel, FalModelVideoSize<TModel>>> {
15
+ readonly kind: "video";
16
+ readonly name: "fal";
17
+ constructor(model: TModel, config?: FalClientConfig);
18
+ createVideoJob(options: VideoGenerationOptions<FalVideoProviderOptions<TModel>, FalModelVideoSize<TModel>>): Promise<VideoJobResult>;
19
+ getVideoStatus(jobId: string): Promise<VideoStatusResult>;
20
+ getVideoUrl(jobId: string): Promise<VideoUrlResult>;
21
+ protected generateId(): string;
22
+ }
23
+ /**
24
+ * Create a fal.ai video adapter with an explicit API key.
25
+ *
26
+ * @experimental Video generation is an experimental feature and may change.
27
+ */
28
+ export declare function createFalVideo<TModel extends FalModel>(model: TModel, config?: FalClientConfig): FalVideoAdapter<TModel>;
29
+ /**
30
+ * Create a fal.ai video adapter using config.apiKey or the FAL_KEY environment variable.
31
+ *
32
+ * @experimental Video generation is an experimental feature and may change.
33
+ */
34
+ export declare function falVideo<TModel extends FalModel>(model: TModel, config?: FalClientConfig): FalVideoAdapter<TModel>;
@@ -0,0 +1,81 @@
1
+ import { fal } from "@fal-ai/client";
2
+ import { BaseVideoAdapter } from "@tanstack/ai/adapters";
3
+ import { configureFalClient, generateId } from "../utils/client.js";
4
+ import { mapVideoSizeToFalFormat } from "../video/video-provider-options.js";
5
+ function mapFalStatusToVideoStatus(falStatus) {
6
+ switch (falStatus) {
7
+ case "IN_QUEUE":
8
+ return "pending";
9
+ case "IN_PROGRESS":
10
+ return "processing";
11
+ case "COMPLETED":
12
+ return "completed";
13
+ default:
14
+ return "processing";
15
+ }
16
+ }
17
+ class FalVideoAdapter extends BaseVideoAdapter {
18
+ constructor(model, config) {
19
+ super({}, model);
20
+ this.kind = "video";
21
+ this.name = "fal";
22
+ configureFalClient(config);
23
+ }
24
+ async createVideoJob(options) {
25
+ const { prompt, size, duration, modelOptions } = options;
26
+ const sizeParams = mapVideoSizeToFalFormat(size);
27
+ const input = {
28
+ ...modelOptions,
29
+ ...sizeParams,
30
+ prompt,
31
+ ...duration ? { duration } : {}
32
+ };
33
+ const { request_id } = await fal.queue.submit(this.model, {
34
+ input
35
+ });
36
+ return {
37
+ jobId: request_id,
38
+ model: this.model
39
+ };
40
+ }
41
+ async getVideoStatus(jobId) {
42
+ const statusResponse = await fal.queue.status(this.model, {
43
+ requestId: jobId,
44
+ logs: true
45
+ });
46
+ return {
47
+ jobId,
48
+ status: mapFalStatusToVideoStatus(statusResponse.status),
49
+ progress: statusResponse.queue_position != null ? Math.max(0, 100 - statusResponse.queue_position * 10) : void 0
50
+ };
51
+ }
52
+ async getVideoUrl(jobId) {
53
+ const result = await fal.queue.result(this.model, {
54
+ requestId: jobId
55
+ });
56
+ const data = result.data;
57
+ const url = data.video?.url || data.video_url;
58
+ if (!url) {
59
+ throw new Error("Video URL not found in response");
60
+ }
61
+ return {
62
+ jobId,
63
+ url
64
+ };
65
+ }
66
+ generateId() {
67
+ return generateId(this.name);
68
+ }
69
+ }
70
+ function createFalVideo(model, config) {
71
+ return new FalVideoAdapter(model, config);
72
+ }
73
+ function falVideo(model, config) {
74
+ return createFalVideo(model, config);
75
+ }
76
+ export {
77
+ FalVideoAdapter,
78
+ createFalVideo,
79
+ falVideo
80
+ };
81
+ //# sourceMappingURL=video.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"video.js","sources":["../../../src/adapters/video.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\nimport { BaseVideoAdapter } from '@tanstack/ai/adapters'\nimport { configureFalClient, generateId as utilGenerateId } from '../utils'\nimport { mapVideoSizeToFalFormat } from '../video/video-provider-options'\nimport type {\n VideoGenerationOptions,\n VideoJobResult,\n VideoStatusResult,\n VideoUrlResult,\n} from '@tanstack/ai'\nimport type {\n FalModel,\n FalModelInput,\n FalModelVideoSize,\n FalVideoProviderOptions,\n} from '../model-meta'\nimport type { FalClientConfig } from '../utils'\n\ntype FalQueueStatus = 'IN_QUEUE' | 'IN_PROGRESS' | 'COMPLETED'\n\ninterface FalStatusResponse {\n status: FalQueueStatus\n queue_position?: number\n logs?: Array<{ message: string }>\n}\n\ninterface FalVideoResultData {\n video?: { url: string }\n video_url?: string\n}\n\n/**\n * Maps fal.ai queue status to TanStack AI video status.\n */\nfunction mapFalStatusToVideoStatus(\n falStatus: FalQueueStatus,\n): VideoStatusResult['status'] {\n switch (falStatus) {\n case 'IN_QUEUE':\n return 'pending'\n case 'IN_PROGRESS':\n return 'processing'\n case 'COMPLETED':\n return 'completed'\n default:\n return 'processing'\n }\n}\n\n/**\n * fal.ai video generation adapter.\n * Supports MiniMax, Luma, Kling, Hunyuan, and other fal.ai video models.\n *\n * Uses fal.ai's comprehensive type system to provide autocomplete\n * and type safety for all supported video models.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport class FalVideoAdapter<TModel extends FalModel> extends BaseVideoAdapter<\n TModel,\n FalVideoProviderOptions<TModel>,\n Record<TModel, FalVideoProviderOptions<TModel>>,\n Record<TModel, FalModelVideoSize<TModel>>\n> {\n readonly kind = 'video' as const\n readonly name = 'fal' as const\n\n constructor(model: TModel, config?: FalClientConfig) {\n super({}, model)\n configureFalClient(config)\n }\n\n async createVideoJob(\n options: VideoGenerationOptions<\n FalVideoProviderOptions<TModel>,\n FalModelVideoSize<TModel>\n >,\n ): Promise<VideoJobResult> {\n const { prompt, size, duration, modelOptions } = options\n const sizeParams = mapVideoSizeToFalFormat(size)\n\n const input = {\n ...modelOptions,\n ...sizeParams,\n prompt,\n ...(duration ? { duration } : {}),\n } as FalModelInput<TModel>\n\n // Submit to queue and get request ID\n const { request_id } = await fal.queue.submit(this.model, {\n input,\n })\n\n return {\n jobId: request_id,\n model: this.model,\n }\n }\n\n async getVideoStatus(jobId: string): Promise<VideoStatusResult> {\n const statusResponse = (await fal.queue.status(this.model, {\n requestId: jobId,\n logs: true,\n })) as FalStatusResponse\n\n return {\n jobId,\n status: mapFalStatusToVideoStatus(statusResponse.status),\n progress:\n statusResponse.queue_position != null\n ? Math.max(0, 100 - statusResponse.queue_position * 10)\n : undefined,\n }\n }\n\n async getVideoUrl(jobId: string): Promise<VideoUrlResult> {\n const result = await fal.queue.result(this.model, {\n requestId: jobId,\n })\n\n const data = result.data as FalVideoResultData\n\n // Different models return video URL in different formats\n const url = data.video?.url || data.video_url\n if (!url) {\n throw new Error('Video URL not found in response')\n }\n\n return {\n jobId,\n url,\n }\n }\n\n protected override generateId(): string {\n return utilGenerateId(this.name)\n }\n}\n\n/**\n * Create a fal.ai video adapter with an explicit API key.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport function createFalVideo<TModel extends FalModel>(\n model: TModel,\n config?: FalClientConfig,\n): FalVideoAdapter<TModel> {\n return new FalVideoAdapter(model, config)\n}\n\n/**\n * Create a fal.ai video adapter using config.apiKey or the FAL_KEY environment variable.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport function falVideo<TModel extends FalModel>(\n model: TModel,\n config?: FalClientConfig,\n): FalVideoAdapter<TModel> {\n return createFalVideo(model, config)\n}\n"],"names":["utilGenerateId"],"mappings":";;;;AAkCA,SAAS,0BACP,WAC6B;AAC7B,UAAQ,WAAA;AAAA,IACN,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EAAA;AAEb;AAWO,MAAM,wBAAiD,iBAK5D;AAAA,EAIA,YAAY,OAAe,QAA0B;AACnD,UAAM,CAAA,GAAI,KAAK;AAJjB,SAAS,OAAO;AAChB,SAAS,OAAO;AAId,uBAAmB,MAAM;AAAA,EAC3B;AAAA,EAEA,MAAM,eACJ,SAIyB;AACzB,UAAM,EAAE,QAAQ,MAAM,UAAU,iBAAiB;AACjD,UAAM,aAAa,wBAAwB,IAAI;AAE/C,UAAM,QAAQ;AAAA,MACZ,GAAG;AAAA,MACH,GAAG;AAAA,MACH;AAAA,MACA,GAAI,WAAW,EAAE,aAAa,CAAA;AAAA,IAAC;AAIjC,UAAM,EAAE,eAAe,MAAM,IAAI,MAAM,OAAO,KAAK,OAAO;AAAA,MACxD;AAAA,IAAA,CACD;AAED,WAAO;AAAA,MACL,OAAO;AAAA,MACP,OAAO,KAAK;AAAA,IAAA;AAAA,EAEhB;AAAA,EAEA,MAAM,eAAe,OAA2C;AAC9D,UAAM,iBAAkB,MAAM,IAAI,MAAM,OAAO,KAAK,OAAO;AAAA,MACzD,WAAW;AAAA,MACX,MAAM;AAAA,IAAA,CACP;AAED,WAAO;AAAA,MACL;AAAA,MACA,QAAQ,0BAA0B,eAAe,MAAM;AAAA,MACvD,UACE,eAAe,kBAAkB,OAC7B,KAAK,IAAI,GAAG,MAAM,eAAe,iBAAiB,EAAE,IACpD;AAAA,IAAA;AAAA,EAEV;AAAA,EAEA,MAAM,YAAY,OAAwC;AACxD,UAAM,SAAS,MAAM,IAAI,MAAM,OAAO,KAAK,OAAO;AAAA,MAChD,WAAW;AAAA,IAAA,CACZ;AAED,UAAM,OAAO,OAAO;AAGpB,UAAM,MAAM,KAAK,OAAO,OAAO,KAAK;AACpC,QAAI,CAAC,KAAK;AACR,YAAM,IAAI,MAAM,iCAAiC;AAAA,IACnD;AAEA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEmB,aAAqB;AACtC,WAAOA,WAAe,KAAK,IAAI;AAAA,EACjC;AACF;AAOO,SAAS,eACd,OACA,QACyB;AACzB,SAAO,IAAI,gBAAgB,OAAO,MAAM;AAC1C;AAOO,SAAS,SACd,OACA,QACyB;AACzB,SAAO,eAAe,OAAO,MAAM;AACrC;"}
@@ -0,0 +1,2 @@
1
+ import { FalModelImageSize, FalModelImageSizeInput } from '../model-meta.js';
2
+ export declare function mapSizeToFalFormat<TModel extends string>(size: FalModelImageSize<TModel> | undefined): FalModelImageSizeInput<TModel> | undefined;
@@ -0,0 +1,15 @@
1
+ function mapSizeToFalFormat(size) {
2
+ if (!size) return void 0;
3
+ const match = size.match(/^(\d+:\d+)(?:_(.+))?$/);
4
+ return {
5
+ image_size: size,
6
+ aspect_ratio: match?.[1],
7
+ ...match?.[2] && {
8
+ resolution: match?.[2]
9
+ }
10
+ };
11
+ }
12
+ export {
13
+ mapSizeToFalFormat
14
+ };
15
+ //# sourceMappingURL=image-provider-options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-provider-options.js","sources":["../../../src/image/image-provider-options.ts"],"sourcesContent":["import type { FalModelImageSize, FalModelImageSizeInput } from '../model-meta'\n\nexport function mapSizeToFalFormat<TModel extends string>(\n size: FalModelImageSize<TModel> | undefined,\n): FalModelImageSizeInput<TModel> | undefined {\n if (!size) return undefined\n\n // \"16:9_4K\" → { aspect_ratio, resolution }\n // \"16:9\" → { aspect_ratio }\n // no match → { image_size }\n const match = size.match(/^(\\d+:\\d+)(?:_(.+))?$/)\n\n return {\n image_size: size,\n aspect_ratio: match?.[1],\n ...(match?.[2] && {\n resolution: match?.[2],\n }),\n }\n}\n"],"names":[],"mappings":"AAEO,SAAS,mBACd,MAC4C;AAC5C,MAAI,CAAC,KAAM,QAAO;AAKlB,QAAM,QAAQ,KAAK,MAAM,uBAAuB;AAEhD,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,cAAc,QAAQ,CAAC;AAAA,IACvB,GAAI,QAAQ,CAAC,KAAK;AAAA,MAChB,YAAY,QAAQ,CAAC;AAAA,IAAA;AAAA,EACvB;AAEJ;"}
@@ -0,0 +1,4 @@
1
+ export { FalImageAdapter, falImage } from './adapters/image.js';
2
+ export { FalVideoAdapter, falVideo } from './adapters/video.js';
3
+ export { type FalImageProviderOptions, type FalVideoProviderOptions, type FalModel, type FalModelInput, type FalModelOutput, type FalModelImageSize, type FalModelVideoSize, } from './model-meta.js';
4
+ export { getFalApiKeyFromEnv, configureFalClient, generateId, type FalClientConfig, } from './utils.js';
@@ -0,0 +1,13 @@
1
+ import { FalImageAdapter, falImage } from "./adapters/image.js";
2
+ import { FalVideoAdapter, falVideo } from "./adapters/video.js";
3
+ import { configureFalClient, generateId, getFalApiKeyFromEnv } from "./utils/client.js";
4
+ export {
5
+ FalImageAdapter,
6
+ FalVideoAdapter,
7
+ configureFalClient,
8
+ falImage,
9
+ falVideo,
10
+ generateId,
11
+ getFalApiKeyFromEnv
12
+ };
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;"}
@@ -0,0 +1,76 @@
1
+ import { EndpointTypeMap } from '@fal-ai/client/endpoints';
2
+ export type { EndpointTypeMap } from '@fal-ai/client/endpoints';
3
+ /**
4
+ * All known fal.ai model IDs with autocomplete support.
5
+ * Also accepts any string for custom/new models.
6
+ */
7
+ export type FalModel = keyof EndpointTypeMap | (string & {});
8
+ /**
9
+ * Utility type to extract the input type for a specific fal model.
10
+ *
11
+ * @example
12
+ * type FluxInput = FalModelInput<'fal-ai/flux/dev'>
13
+ * // { prompt: string; num_inference_steps?: number; ... }
14
+ */
15
+ export type FalModelInput<TModel extends string> = TModel extends keyof EndpointTypeMap ? EndpointTypeMap[TModel]['input'] : Record<string, any>;
16
+ /**
17
+ * Utility type to extract the output type for a specific fal model.
18
+ *
19
+ * @example
20
+ * type FluxOutput = FalModelOutput<'fal-ai/flux/dev'>
21
+ * // { images: Array<Image>; seed: number; ... }
22
+ */
23
+ export type FalModelOutput<TModel extends string> = TModel extends keyof EndpointTypeMap ? EndpointTypeMap[TModel]['output'] : unknown;
24
+ /**
25
+ * Extract the image_size type supported by a specific fal model.
26
+ * Returns never if the model doesn't support image_size.
27
+ *
28
+ * @example
29
+ * type FluxSize = FalModelImageSize<'fal-ai/flux/dev'>
30
+ * // "square_hd" | "square" | "portrait_4_3" | "portrait_16_9" | ...
31
+ */
32
+ export type FalModelImageSize<TModel extends string> = TModel extends keyof EndpointTypeMap ? 'image_size' extends keyof EndpointTypeMap[TModel]['input'] ? NonNullable<Exclude<FalModelInput<TModel>['image_size'], object>> : 'aspect_ratio' extends keyof EndpointTypeMap[TModel]['input'] ? 'resolution' extends keyof EndpointTypeMap[TModel]['input'] ? `${NonNullable<FalModelInput<TModel>['aspect_ratio']>}_${NonNullable<FalModelInput<TModel>['resolution']>}` : NonNullable<FalModelInput<TModel>['aspect_ratio']> : never : string;
33
+ export type FalModelImageSizeInput<TModel extends string> = TModel extends keyof EndpointTypeMap ? 'aspect_ratio' extends keyof EndpointTypeMap[TModel]['input'] ? 'resolution' extends keyof EndpointTypeMap[TModel]['input'] ? {
34
+ aspect_ratio: FalModelInput<TModel>['aspect_ratio'];
35
+ resolution: FalModelInput<TModel>['resolution'];
36
+ } : {
37
+ aspect_ratio: NonNullable<FalModelInput<TModel>['aspect_ratio']>;
38
+ } : {
39
+ image_size: FalModelImageSize<TModel>;
40
+ } : {
41
+ image_size: string;
42
+ };
43
+ /**
44
+ * Provider options for image generation, excluding fields TanStack AI handles.
45
+ * Use this for the `modelOptions` parameter in image generation.
46
+ *
47
+ * @example
48
+ * type FluxOptions = FalImageProviderOptions<'fal-ai/flux/dev'>
49
+ * // { num_inference_steps?: number; guidance_scale?: number; seed?: number; ... }
50
+ */
51
+ export type FalImageProviderOptions<TModel extends string> = Omit<FalModelInput<TModel>, 'prompt'>;
52
+ /**
53
+ * Extract the video size type supported by a specific fal model.
54
+ * Video models typically use aspect_ratio and/or resolution fields.
55
+ *
56
+ * Follows the same pattern as FalModelImageSize:
57
+ * - aspect_ratio + resolution → "16:9_720p"
58
+ * - aspect_ratio only → "16:9"
59
+ * - unknown models → string
60
+ */
61
+ export type FalModelVideoSize<TModel extends string> = TModel extends keyof EndpointTypeMap ? 'aspect_ratio' extends keyof EndpointTypeMap[TModel]['input'] ? 'resolution' extends keyof EndpointTypeMap[TModel]['input'] ? `${NonNullable<FalModelInput<TModel>['aspect_ratio']>}_${NonNullable<FalModelInput<TModel>['resolution']>}` : NonNullable<FalModelInput<TModel>['aspect_ratio']> : never : string;
62
+ export type FalModelVideoSizeInput<TModel extends string> = TModel extends keyof EndpointTypeMap ? 'aspect_ratio' extends keyof EndpointTypeMap[TModel]['input'] ? 'resolution' extends keyof EndpointTypeMap[TModel]['input'] ? {
63
+ aspect_ratio: FalModelInput<TModel>['aspect_ratio'];
64
+ resolution: FalModelInput<TModel>['resolution'];
65
+ } : {
66
+ aspect_ratio: NonNullable<FalModelInput<TModel>['aspect_ratio']>;
67
+ } : {
68
+ aspect_ratio: string;
69
+ } : {
70
+ aspect_ratio: string;
71
+ };
72
+ /**
73
+ * Provider options for video generation, excluding fields TanStack AI handles.
74
+ * Use this for the `modelOptions` parameter in video generation.
75
+ */
76
+ export type FalVideoProviderOptions<TModel extends string> = TModel extends keyof EndpointTypeMap ? Omit<FalModelInput<TModel>, 'prompt'> : Record<string, any>;
@@ -0,0 +1,7 @@
1
+ export interface FalClientConfig {
2
+ apiKey: string;
3
+ proxyUrl?: string;
4
+ }
5
+ export declare function getFalApiKeyFromEnv(): string;
6
+ export declare function configureFalClient(config?: FalClientConfig): void;
7
+ export declare function generateId(prefix: string): string;
@@ -0,0 +1,47 @@
1
+ import { fal } from "@fal-ai/client";
2
+ function getEnvironment() {
3
+ if (typeof globalThis !== "undefined") {
4
+ const win = globalThis.window;
5
+ if (win?.env) {
6
+ return win.env;
7
+ }
8
+ }
9
+ if (typeof process !== "undefined") {
10
+ return process.env;
11
+ }
12
+ return void 0;
13
+ }
14
+ function getFalApiKeyFromEnv() {
15
+ const env = getEnvironment();
16
+ const key = env?.FAL_KEY;
17
+ if (!key) {
18
+ throw new Error(
19
+ "FAL_KEY is required. Please set it in your environment variables or use the factory function with an explicit API key."
20
+ );
21
+ }
22
+ return key;
23
+ }
24
+ function configureFalClient(config) {
25
+ if (config?.proxyUrl) {
26
+ fal.config({
27
+ proxyUrl: config.proxyUrl
28
+ });
29
+ } else {
30
+ const apiKey = config?.apiKey ?? getFalApiKeyFromEnv();
31
+ if (!apiKey) {
32
+ throw new Error("API key is required");
33
+ }
34
+ fal.config({
35
+ credentials: apiKey
36
+ });
37
+ }
38
+ }
39
+ function generateId(prefix) {
40
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`;
41
+ }
42
+ export {
43
+ configureFalClient,
44
+ generateId,
45
+ getFalApiKeyFromEnv
46
+ };
47
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sources":["../../../src/utils/client.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\n\nexport interface FalClientConfig {\n apiKey: string\n proxyUrl?: string\n}\n\ninterface EnvObject {\n FAL_KEY?: string\n}\n\ninterface WindowWithEnv {\n env?: EnvObject\n}\n\nfunction getEnvironment(): EnvObject | undefined {\n if (typeof globalThis !== 'undefined') {\n const win = (globalThis as { window?: WindowWithEnv }).window\n if (win?.env) {\n return win.env\n }\n }\n if (typeof process !== 'undefined') {\n return process.env as EnvObject\n }\n return undefined\n}\n\nexport function getFalApiKeyFromEnv(): string {\n const env = getEnvironment()\n const key = env?.FAL_KEY\n\n if (!key) {\n throw new Error(\n 'FAL_KEY is required. Please set it in your environment variables or use the factory function with an explicit API key.',\n )\n }\n\n return key\n}\n\nexport function configureFalClient(config?: FalClientConfig): void {\n if (config?.proxyUrl) {\n fal.config({\n proxyUrl: config.proxyUrl,\n })\n } else {\n const apiKey = config?.apiKey ?? getFalApiKeyFromEnv()\n if (!apiKey) {\n throw new Error('API key is required')\n }\n fal.config({\n credentials: apiKey,\n })\n }\n}\n\nexport function generateId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`\n}\n"],"names":[],"mappings":";AAeA,SAAS,iBAAwC;AAC/C,MAAI,OAAO,eAAe,aAAa;AACrC,UAAM,MAAO,WAA0C;AACvD,QAAI,KAAK,KAAK;AACZ,aAAO,IAAI;AAAA,IACb;AAAA,EACF;AACA,MAAI,OAAO,YAAY,aAAa;AAClC,WAAO,QAAQ;AAAA,EACjB;AACA,SAAO;AACT;AAEO,SAAS,sBAA8B;AAC5C,QAAM,MAAM,eAAA;AACZ,QAAM,MAAM,KAAK;AAEjB,MAAI,CAAC,KAAK;AACR,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AAEA,SAAO;AACT;AAEO,SAAS,mBAAmB,QAAgC;AACjE,MAAI,QAAQ,UAAU;AACpB,QAAI,OAAO;AAAA,MACT,UAAU,OAAO;AAAA,IAAA,CAClB;AAAA,EACH,OAAO;AACL,UAAM,SAAS,QAAQ,UAAU,oBAAA;AACjC,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,MAAM,qBAAqB;AAAA,IACvC;AACA,QAAI,OAAO;AAAA,MACT,aAAa;AAAA,IAAA,CACd;AAAA,EACH;AACF;AAEO,SAAS,WAAW,QAAwB;AACjD,SAAO,GAAG,MAAM,IAAI,KAAK,KAAK,IAAI,KAAK,OAAA,EAAS,SAAS,EAAE,EAAE,UAAU,CAAC,CAAC;AAC3E;"}
@@ -0,0 +1 @@
1
+ export { getFalApiKeyFromEnv, configureFalClient, generateId, type FalClientConfig, } from './client.js';
@@ -0,0 +1,2 @@
1
+ import { FalModelVideoSize, FalModelVideoSizeInput } from '../model-meta.js';
2
+ export declare function mapVideoSizeToFalFormat<TModel extends string>(size: FalModelVideoSize<TModel> | undefined): FalModelVideoSizeInput<TModel> | undefined;
@@ -0,0 +1,14 @@
1
+ function mapVideoSizeToFalFormat(size) {
2
+ if (!size) return void 0;
3
+ const match = size.match(/^(\d+:\d+)(?:_(.+))?$/);
4
+ return {
5
+ aspect_ratio: match?.[1],
6
+ ...match?.[2] && {
7
+ resolution: match[2]
8
+ }
9
+ };
10
+ }
11
+ export {
12
+ mapVideoSizeToFalFormat
13
+ };
14
+ //# sourceMappingURL=video-provider-options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"video-provider-options.js","sources":["../../../src/video/video-provider-options.ts"],"sourcesContent":["import type {\n FalModelInput,\n FalModelVideoSize,\n FalModelVideoSizeInput,\n} from '../model-meta'\n\nexport function mapVideoSizeToFalFormat<TModel extends string>(\n size: FalModelVideoSize<TModel> | undefined,\n): FalModelVideoSizeInput<TModel> | undefined {\n if (!size) return undefined\n\n // \"16:9_720p\" → { aspect_ratio, resolution }\n // \"16:9\" → { aspect_ratio }\n const match = size.match(/^(\\d+:\\d+)(?:_(.+))?$/)\n\n return {\n aspect_ratio: match?.[1] as NonNullable<\n FalModelInput<TModel>['aspect_ratio']\n >,\n ...(match?.[2] && {\n resolution: match[2] as NonNullable<FalModelInput<TModel>['resolution']>,\n }),\n } as FalModelVideoSizeInput<TModel>\n}\n"],"names":[],"mappings":"AAMO,SAAS,wBACd,MAC4C;AAC5C,MAAI,CAAC,KAAM,QAAO;AAIlB,QAAM,QAAQ,KAAK,MAAM,uBAAuB;AAEhD,SAAO;AAAA,IACL,cAAc,QAAQ,CAAC;AAAA,IAGvB,GAAI,QAAQ,CAAC,KAAK;AAAA,MAChB,YAAY,MAAM,CAAC;AAAA,IAAA;AAAA,EACrB;AAEJ;"}
package/package.json CHANGED
@@ -1,10 +1,54 @@
1
1
  {
2
2
  "name": "@tanstack/ai-fal",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for @tanstack/ai-fal",
3
+ "version": "0.6.1",
4
+ "description": "fal.ai adapter for TanStack AI",
5
+ "author": "",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/TanStack/ai.git",
10
+ "directory": "packages/typescript/ai-fal"
11
+ },
12
+ "type": "module",
13
+ "module": "./dist/esm/index.js",
14
+ "types": "./dist/esm/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/esm/index.d.ts",
18
+ "import": "./dist/esm/index.js"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "src"
24
+ ],
5
25
  "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
10
- }
26
+ "ai",
27
+ "fal",
28
+ "tanstack",
29
+ "adapter",
30
+ "image-generation",
31
+ "video-generation"
32
+ ],
33
+ "dependencies": {
34
+ "@fal-ai/client": "^1.9.1"
35
+ },
36
+ "devDependencies": {
37
+ "@vitest/coverage-v8": "4.0.14",
38
+ "vite": "^7.2.7",
39
+ "@tanstack/ai": "0.6.1"
40
+ },
41
+ "peerDependencies": {
42
+ "@tanstack/ai": "0.6.1"
43
+ },
44
+ "scripts": {
45
+ "build": "vite build",
46
+ "clean": "premove ./build ./dist",
47
+ "lint:fix": "eslint ./src --fix",
48
+ "test:build": "publint --strict",
49
+ "test:eslint": "eslint ./src",
50
+ "test:lib": "vitest run",
51
+ "test:lib:dev": "pnpm test:lib --watch",
52
+ "test:types": "tsc"
53
+ }
54
+ }
@@ -0,0 +1,133 @@
1
+ import { fal } from '@fal-ai/client'
2
+ import { BaseImageAdapter } from '@tanstack/ai/adapters'
3
+ import { configureFalClient, generateId as utilGenerateId } from '../utils'
4
+ import { mapSizeToFalFormat } from '../image/image-provider-options'
5
+ import type { OutputType, Result } from '@fal-ai/client'
6
+ import type { FalClientConfig } from '../utils'
7
+ import type {
8
+ GeneratedImage,
9
+ ImageGenerationOptions,
10
+ ImageGenerationResult,
11
+ } from '@tanstack/ai'
12
+ import type {
13
+ FalImageProviderOptions,
14
+ FalModel,
15
+ FalModelImageSize,
16
+ FalModelInput,
17
+ } from '../model-meta'
18
+
19
+ /**
20
+ * fal.ai image generation adapter with full type inference.
21
+ *
22
+ * Uses fal.ai's comprehensive type system to provide autocomplete
23
+ *
24
+ * and type safety for all 600+ supported models.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const adapter = falImage('fal-ai/flux/dev')
29
+ * const result = await adapter.generateImages({
30
+ * model: 'fal-ai/flux/dev',
31
+ * prompt: 'a cat',
32
+ * modelOptions: {
33
+ * num_inference_steps: 28, // Type-safe! Autocomplete works
34
+ * guidance_scale: 3.5,
35
+ * },
36
+ * })
37
+ * ```
38
+ */
39
+ export class FalImageAdapter<TModel extends FalModel> extends BaseImageAdapter<
40
+ TModel,
41
+ FalImageProviderOptions<TModel>,
42
+ Record<TModel, FalImageProviderOptions<TModel>>,
43
+ Record<TModel, FalModelImageSize<TModel>>
44
+ > {
45
+ readonly kind = 'image' as const
46
+ readonly name = 'fal' as const
47
+
48
+ constructor(model: TModel, config?: FalClientConfig) {
49
+ super({}, model)
50
+ configureFalClient(config)
51
+ }
52
+
53
+ async generateImages(
54
+ options: ImageGenerationOptions<
55
+ FalImageProviderOptions<TModel>,
56
+ FalModelImageSize<TModel>
57
+ >,
58
+ ): Promise<ImageGenerationResult> {
59
+ const input = this.buildInput(options)
60
+ const result = await fal.subscribe(this.model, { input })
61
+ return this.transformResponse(result)
62
+ }
63
+
64
+ private buildInput(
65
+ options: ImageGenerationOptions<FalImageProviderOptions<TModel>, string>,
66
+ ): FalModelInput<TModel> {
67
+ const sizeParams = mapSizeToFalFormat(options.size)
68
+ const input = {
69
+ ...options.modelOptions,
70
+ ...sizeParams,
71
+ prompt: options.prompt,
72
+ num_images: options.numberOfImages,
73
+ } as FalModelInput<TModel>
74
+ return input
75
+ }
76
+
77
+ protected override generateId(): string {
78
+ return utilGenerateId(this.name)
79
+ }
80
+
81
+ private transformResponse(
82
+ response: Result<OutputType<TModel>>,
83
+ ): ImageGenerationResult {
84
+ const images: Array<GeneratedImage> = []
85
+ const data = response.data
86
+
87
+ // Handle array of images (most models return { images: [...] })
88
+ if ('images' in data && Array.isArray(data.images)) {
89
+ for (const img of data.images) {
90
+ images.push(this.parseImage(img))
91
+ }
92
+ }
93
+ // Handle single image response (some models return { image: {...} })
94
+ else if ('image' in data && data.image && typeof data.image === 'object') {
95
+ images.push(this.parseImage(data.image))
96
+ }
97
+
98
+ return {
99
+ id: response.requestId || this.generateId(),
100
+ model: this.model,
101
+ images,
102
+ }
103
+ }
104
+
105
+ private parseImage(img: { url: string }): GeneratedImage {
106
+ const url = img.url
107
+ // Check if it's a base64 data URL
108
+ if (url.startsWith('data:')) {
109
+ const base64Match = url.match(/^data:image\/[^;]+;base64,(.+)$/)
110
+ if (base64Match) {
111
+ return {
112
+ b64Json: base64Match[1],
113
+ url,
114
+ }
115
+ }
116
+ }
117
+ return { url }
118
+ }
119
+ }
120
+
121
+ export function createFalImage<TModel extends FalModel>(
122
+ model: TModel,
123
+ config?: FalClientConfig,
124
+ ): FalImageAdapter<TModel> {
125
+ return new FalImageAdapter(model, config)
126
+ }
127
+
128
+ export function falImage<TModel extends FalModel>(
129
+ model: TModel,
130
+ config?: FalClientConfig,
131
+ ): FalImageAdapter<TModel> {
132
+ return createFalImage(model, config)
133
+ }
@@ -0,0 +1,162 @@
1
+ import { fal } from '@fal-ai/client'
2
+ import { BaseVideoAdapter } from '@tanstack/ai/adapters'
3
+ import { configureFalClient, generateId as utilGenerateId } from '../utils'
4
+ import { mapVideoSizeToFalFormat } from '../video/video-provider-options'
5
+ import type {
6
+ VideoGenerationOptions,
7
+ VideoJobResult,
8
+ VideoStatusResult,
9
+ VideoUrlResult,
10
+ } from '@tanstack/ai'
11
+ import type {
12
+ FalModel,
13
+ FalModelInput,
14
+ FalModelVideoSize,
15
+ FalVideoProviderOptions,
16
+ } from '../model-meta'
17
+ import type { FalClientConfig } from '../utils'
18
+
19
+ type FalQueueStatus = 'IN_QUEUE' | 'IN_PROGRESS' | 'COMPLETED'
20
+
21
+ interface FalStatusResponse {
22
+ status: FalQueueStatus
23
+ queue_position?: number
24
+ logs?: Array<{ message: string }>
25
+ }
26
+
27
+ interface FalVideoResultData {
28
+ video?: { url: string }
29
+ video_url?: string
30
+ }
31
+
32
+ /**
33
+ * Maps fal.ai queue status to TanStack AI video status.
34
+ */
35
+ function mapFalStatusToVideoStatus(
36
+ falStatus: FalQueueStatus,
37
+ ): VideoStatusResult['status'] {
38
+ switch (falStatus) {
39
+ case 'IN_QUEUE':
40
+ return 'pending'
41
+ case 'IN_PROGRESS':
42
+ return 'processing'
43
+ case 'COMPLETED':
44
+ return 'completed'
45
+ default:
46
+ return 'processing'
47
+ }
48
+ }
49
+
50
+ /**
51
+ * fal.ai video generation adapter.
52
+ * Supports MiniMax, Luma, Kling, Hunyuan, and other fal.ai video models.
53
+ *
54
+ * Uses fal.ai's comprehensive type system to provide autocomplete
55
+ * and type safety for all supported video models.
56
+ *
57
+ * @experimental Video generation is an experimental feature and may change.
58
+ */
59
+ export class FalVideoAdapter<TModel extends FalModel> extends BaseVideoAdapter<
60
+ TModel,
61
+ FalVideoProviderOptions<TModel>,
62
+ Record<TModel, FalVideoProviderOptions<TModel>>,
63
+ Record<TModel, FalModelVideoSize<TModel>>
64
+ > {
65
+ readonly kind = 'video' as const
66
+ readonly name = 'fal' as const
67
+
68
+ constructor(model: TModel, config?: FalClientConfig) {
69
+ super({}, model)
70
+ configureFalClient(config)
71
+ }
72
+
73
+ async createVideoJob(
74
+ options: VideoGenerationOptions<
75
+ FalVideoProviderOptions<TModel>,
76
+ FalModelVideoSize<TModel>
77
+ >,
78
+ ): Promise<VideoJobResult> {
79
+ const { prompt, size, duration, modelOptions } = options
80
+ const sizeParams = mapVideoSizeToFalFormat(size)
81
+
82
+ const input = {
83
+ ...modelOptions,
84
+ ...sizeParams,
85
+ prompt,
86
+ ...(duration ? { duration } : {}),
87
+ } as FalModelInput<TModel>
88
+
89
+ // Submit to queue and get request ID
90
+ const { request_id } = await fal.queue.submit(this.model, {
91
+ input,
92
+ })
93
+
94
+ return {
95
+ jobId: request_id,
96
+ model: this.model,
97
+ }
98
+ }
99
+
100
+ async getVideoStatus(jobId: string): Promise<VideoStatusResult> {
101
+ const statusResponse = (await fal.queue.status(this.model, {
102
+ requestId: jobId,
103
+ logs: true,
104
+ })) as FalStatusResponse
105
+
106
+ return {
107
+ jobId,
108
+ status: mapFalStatusToVideoStatus(statusResponse.status),
109
+ progress:
110
+ statusResponse.queue_position != null
111
+ ? Math.max(0, 100 - statusResponse.queue_position * 10)
112
+ : undefined,
113
+ }
114
+ }
115
+
116
+ async getVideoUrl(jobId: string): Promise<VideoUrlResult> {
117
+ const result = await fal.queue.result(this.model, {
118
+ requestId: jobId,
119
+ })
120
+
121
+ const data = result.data as FalVideoResultData
122
+
123
+ // Different models return video URL in different formats
124
+ const url = data.video?.url || data.video_url
125
+ if (!url) {
126
+ throw new Error('Video URL not found in response')
127
+ }
128
+
129
+ return {
130
+ jobId,
131
+ url,
132
+ }
133
+ }
134
+
135
+ protected override generateId(): string {
136
+ return utilGenerateId(this.name)
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Create a fal.ai video adapter with an explicit API key.
142
+ *
143
+ * @experimental Video generation is an experimental feature and may change.
144
+ */
145
+ export function createFalVideo<TModel extends FalModel>(
146
+ model: TModel,
147
+ config?: FalClientConfig,
148
+ ): FalVideoAdapter<TModel> {
149
+ return new FalVideoAdapter(model, config)
150
+ }
151
+
152
+ /**
153
+ * Create a fal.ai video adapter using config.apiKey or the FAL_KEY environment variable.
154
+ *
155
+ * @experimental Video generation is an experimental feature and may change.
156
+ */
157
+ export function falVideo<TModel extends FalModel>(
158
+ model: TModel,
159
+ config?: FalClientConfig,
160
+ ): FalVideoAdapter<TModel> {
161
+ return createFalVideo(model, config)
162
+ }
@@ -0,0 +1,20 @@
1
+ import type { FalModelImageSize, FalModelImageSizeInput } from '../model-meta'
2
+
3
+ export function mapSizeToFalFormat<TModel extends string>(
4
+ size: FalModelImageSize<TModel> | undefined,
5
+ ): FalModelImageSizeInput<TModel> | undefined {
6
+ if (!size) return undefined
7
+
8
+ // "16:9_4K" → { aspect_ratio, resolution }
9
+ // "16:9" → { aspect_ratio }
10
+ // no match → { image_size }
11
+ const match = size.match(/^(\d+:\d+)(?:_(.+))?$/)
12
+
13
+ return {
14
+ image_size: size,
15
+ aspect_ratio: match?.[1],
16
+ ...(match?.[2] && {
17
+ resolution: match?.[2],
18
+ }),
19
+ }
20
+ }
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ // ============================================================================
2
+ // Image Adapter
3
+ // ============================================================================
4
+
5
+ export { FalImageAdapter, falImage } from './adapters/image'
6
+
7
+ // ============================================================================
8
+ // Video Adapter (Experimental)
9
+ // ============================================================================
10
+
11
+ export { FalVideoAdapter, falVideo } from './adapters/video'
12
+
13
+ // ============================================================================
14
+ // Model Types (from fal.ai's type system)
15
+ // ============================================================================
16
+
17
+ export {
18
+ type FalImageProviderOptions,
19
+ type FalVideoProviderOptions,
20
+ type FalModel,
21
+ type FalModelInput,
22
+ type FalModelOutput,
23
+ type FalModelImageSize,
24
+ type FalModelVideoSize,
25
+ } from './model-meta'
26
+ // ============================================================================
27
+ // Utils
28
+ // ============================================================================
29
+
30
+ export {
31
+ getFalApiKeyFromEnv,
32
+ configureFalClient,
33
+ generateId,
34
+ type FalClientConfig,
35
+ } from './utils'
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Re-export fal.ai's comprehensive type system for full model support.
3
+ * The fal.ai SDK provides types for 600+ models through EndpointTypeMap.
4
+ * These types give you full autocomplete and type safety for any model.
5
+ */
6
+ import type { EndpointTypeMap } from '@fal-ai/client/endpoints'
7
+
8
+ export type { EndpointTypeMap } from '@fal-ai/client/endpoints'
9
+
10
+ /**
11
+ * All known fal.ai model IDs with autocomplete support.
12
+ * Also accepts any string for custom/new models.
13
+ */
14
+ export type FalModel = keyof EndpointTypeMap | (string & {})
15
+
16
+ /**
17
+ * Utility type to extract the input type for a specific fal model.
18
+ *
19
+ * @example
20
+ * type FluxInput = FalModelInput<'fal-ai/flux/dev'>
21
+ * // { prompt: string; num_inference_steps?: number; ... }
22
+ */
23
+ export type FalModelInput<TModel extends string> =
24
+ TModel extends keyof EndpointTypeMap
25
+ ? EndpointTypeMap[TModel]['input']
26
+ : Record<string, any>
27
+
28
+ /**
29
+ * Utility type to extract the output type for a specific fal model.
30
+ *
31
+ * @example
32
+ * type FluxOutput = FalModelOutput<'fal-ai/flux/dev'>
33
+ * // { images: Array<Image>; seed: number; ... }
34
+ */
35
+ export type FalModelOutput<TModel extends string> =
36
+ TModel extends keyof EndpointTypeMap
37
+ ? EndpointTypeMap[TModel]['output']
38
+ : unknown
39
+
40
+ /**
41
+ * Extract the image_size type supported by a specific fal model.
42
+ * Returns never if the model doesn't support image_size.
43
+ *
44
+ * @example
45
+ * type FluxSize = FalModelImageSize<'fal-ai/flux/dev'>
46
+ * // "square_hd" | "square" | "portrait_4_3" | "portrait_16_9" | ...
47
+ */
48
+ export type FalModelImageSize<TModel extends string> =
49
+ TModel extends keyof EndpointTypeMap
50
+ ? 'image_size' extends keyof EndpointTypeMap[TModel]['input']
51
+ ? NonNullable<Exclude<FalModelInput<TModel>['image_size'], object>>
52
+ : 'aspect_ratio' extends keyof EndpointTypeMap[TModel]['input']
53
+ ? 'resolution' extends keyof EndpointTypeMap[TModel]['input']
54
+ ? `${NonNullable<FalModelInput<TModel>['aspect_ratio']>}_${NonNullable<FalModelInput<TModel>['resolution']>}`
55
+ : NonNullable<FalModelInput<TModel>['aspect_ratio']>
56
+ : never
57
+ : string
58
+
59
+ export type FalModelImageSizeInput<TModel extends string> =
60
+ TModel extends keyof EndpointTypeMap
61
+ ? 'aspect_ratio' extends keyof EndpointTypeMap[TModel]['input']
62
+ ? 'resolution' extends keyof EndpointTypeMap[TModel]['input']
63
+ ? {
64
+ aspect_ratio: FalModelInput<TModel>['aspect_ratio']
65
+ resolution: FalModelInput<TModel>['resolution']
66
+ }
67
+ : { aspect_ratio: NonNullable<FalModelInput<TModel>['aspect_ratio']> }
68
+ : { image_size: FalModelImageSize<TModel> }
69
+ : { image_size: string }
70
+
71
+ /**
72
+ * Provider options for image generation, excluding fields TanStack AI handles.
73
+ * Use this for the `modelOptions` parameter in image generation.
74
+ *
75
+ * @example
76
+ * type FluxOptions = FalImageProviderOptions<'fal-ai/flux/dev'>
77
+ * // { num_inference_steps?: number; guidance_scale?: number; seed?: number; ... }
78
+ */
79
+ export type FalImageProviderOptions<TModel extends string> = Omit<
80
+ FalModelInput<TModel>,
81
+ 'prompt'
82
+ >
83
+
84
+ /**
85
+ * Extract the video size type supported by a specific fal model.
86
+ * Video models typically use aspect_ratio and/or resolution fields.
87
+ *
88
+ * Follows the same pattern as FalModelImageSize:
89
+ * - aspect_ratio + resolution → "16:9_720p"
90
+ * - aspect_ratio only → "16:9"
91
+ * - unknown models → string
92
+ */
93
+ export type FalModelVideoSize<TModel extends string> =
94
+ TModel extends keyof EndpointTypeMap
95
+ ? 'aspect_ratio' extends keyof EndpointTypeMap[TModel]['input']
96
+ ? 'resolution' extends keyof EndpointTypeMap[TModel]['input']
97
+ ? `${NonNullable<FalModelInput<TModel>['aspect_ratio']>}_${NonNullable<FalModelInput<TModel>['resolution']>}`
98
+ : NonNullable<FalModelInput<TModel>['aspect_ratio']>
99
+ : never
100
+ : string
101
+
102
+ export type FalModelVideoSizeInput<TModel extends string> =
103
+ TModel extends keyof EndpointTypeMap
104
+ ? 'aspect_ratio' extends keyof EndpointTypeMap[TModel]['input']
105
+ ? 'resolution' extends keyof EndpointTypeMap[TModel]['input']
106
+ ? {
107
+ aspect_ratio: FalModelInput<TModel>['aspect_ratio']
108
+ resolution: FalModelInput<TModel>['resolution']
109
+ }
110
+ : { aspect_ratio: NonNullable<FalModelInput<TModel>['aspect_ratio']> }
111
+ : { aspect_ratio: string }
112
+ : { aspect_ratio: string }
113
+
114
+ /**
115
+ * Provider options for video generation, excluding fields TanStack AI handles.
116
+ * Use this for the `modelOptions` parameter in video generation.
117
+ */
118
+ export type FalVideoProviderOptions<TModel extends string> =
119
+ TModel extends keyof EndpointTypeMap
120
+ ? Omit<FalModelInput<TModel>, 'prompt'>
121
+ : Record<string, any>
@@ -0,0 +1,60 @@
1
+ import { fal } from '@fal-ai/client'
2
+
3
+ export interface FalClientConfig {
4
+ apiKey: string
5
+ proxyUrl?: string
6
+ }
7
+
8
+ interface EnvObject {
9
+ FAL_KEY?: string
10
+ }
11
+
12
+ interface WindowWithEnv {
13
+ env?: EnvObject
14
+ }
15
+
16
+ function getEnvironment(): EnvObject | undefined {
17
+ if (typeof globalThis !== 'undefined') {
18
+ const win = (globalThis as { window?: WindowWithEnv }).window
19
+ if (win?.env) {
20
+ return win.env
21
+ }
22
+ }
23
+ if (typeof process !== 'undefined') {
24
+ return process.env as EnvObject
25
+ }
26
+ return undefined
27
+ }
28
+
29
+ export function getFalApiKeyFromEnv(): string {
30
+ const env = getEnvironment()
31
+ const key = env?.FAL_KEY
32
+
33
+ if (!key) {
34
+ throw new Error(
35
+ 'FAL_KEY is required. Please set it in your environment variables or use the factory function with an explicit API key.',
36
+ )
37
+ }
38
+
39
+ return key
40
+ }
41
+
42
+ export function configureFalClient(config?: FalClientConfig): void {
43
+ if (config?.proxyUrl) {
44
+ fal.config({
45
+ proxyUrl: config.proxyUrl,
46
+ })
47
+ } else {
48
+ const apiKey = config?.apiKey ?? getFalApiKeyFromEnv()
49
+ if (!apiKey) {
50
+ throw new Error('API key is required')
51
+ }
52
+ fal.config({
53
+ credentials: apiKey,
54
+ })
55
+ }
56
+ }
57
+
58
+ export function generateId(prefix: string): string {
59
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`
60
+ }
@@ -0,0 +1,6 @@
1
+ export {
2
+ getFalApiKeyFromEnv,
3
+ configureFalClient,
4
+ generateId,
5
+ type FalClientConfig,
6
+ } from './client'
@@ -0,0 +1,24 @@
1
+ import type {
2
+ FalModelInput,
3
+ FalModelVideoSize,
4
+ FalModelVideoSizeInput,
5
+ } from '../model-meta'
6
+
7
+ export function mapVideoSizeToFalFormat<TModel extends string>(
8
+ size: FalModelVideoSize<TModel> | undefined,
9
+ ): FalModelVideoSizeInput<TModel> | undefined {
10
+ if (!size) return undefined
11
+
12
+ // "16:9_720p" → { aspect_ratio, resolution }
13
+ // "16:9" → { aspect_ratio }
14
+ const match = size.match(/^(\d+:\d+)(?:_(.+))?$/)
15
+
16
+ return {
17
+ aspect_ratio: match?.[1] as NonNullable<
18
+ FalModelInput<TModel>['aspect_ratio']
19
+ >,
20
+ ...(match?.[2] && {
21
+ resolution: match[2] as NonNullable<FalModelInput<TModel>['resolution']>,
22
+ }),
23
+ } as FalModelVideoSizeInput<TModel>
24
+ }
package/README.md DELETED
@@ -1,45 +0,0 @@
1
- # @tanstack/ai-fal
2
-
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
4
-
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
6
-
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
8
-
9
- ## Purpose
10
-
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@tanstack/ai-fal`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
15
-
16
- ## What is OIDC Trusted Publishing?
17
-
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
19
-
20
- ## Setup Instructions
21
-
22
- To properly configure OIDC trusted publishing for this package:
23
-
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
28
-
29
- ## DO NOT USE THIS PACKAGE
30
-
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
36
-
37
- ## More Information
38
-
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
42
-
43
- ---
44
-
45
- **Maintained for OIDC setup purposes only**