@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 +21 -0
- package/dist/esm/adapters/image.d.ts +36 -0
- package/dist/esm/adapters/image.js +71 -0
- package/dist/esm/adapters/image.js.map +1 -0
- package/dist/esm/adapters/video.d.ts +34 -0
- package/dist/esm/adapters/video.js +81 -0
- package/dist/esm/adapters/video.js.map +1 -0
- package/dist/esm/image/image-provider-options.d.ts +2 -0
- package/dist/esm/image/image-provider-options.js +15 -0
- package/dist/esm/image/image-provider-options.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +13 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/model-meta.d.ts +76 -0
- package/dist/esm/utils/client.d.ts +7 -0
- package/dist/esm/utils/client.js +47 -0
- package/dist/esm/utils/client.js.map +1 -0
- package/dist/esm/utils/index.d.ts +1 -0
- package/dist/esm/video/video-provider-options.d.ts +2 -0
- package/dist/esm/video/video-provider-options.js +14 -0
- package/dist/esm/video/video-provider-options.js.map +1 -0
- package/package.json +51 -7
- package/src/adapters/image.ts +133 -0
- package/src/adapters/video.ts +162 -0
- package/src/image/image-provider-options.ts +20 -0
- package/src/index.ts +35 -0
- package/src/model-meta.ts +121 -0
- package/src/utils/client.ts +60 -0
- package/src/utils/index.ts +6 -0
- package/src/video/video-provider-options.ts +24 -0
- package/README.md +0 -45
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,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,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.
|
|
4
|
-
"description": "
|
|
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
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
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,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**
|