@tanstack/ai-fal 0.7.3 → 0.7.4

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.
@@ -1 +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 { logger } = options\n\n logger.request(`activity=generateImage provider=fal model=${this.model}`, {\n provider: 'fal',\n model: this.model,\n })\n\n try {\n const input = this.buildInput(options)\n const result = await fal.subscribe(this.model, { input })\n return this.transformResponse(result)\n } catch (error) {\n logger.errors('fal.generateImage fatal', {\n error,\n source: 'fal.generateImage',\n })\n throw error\n }\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 if (images.length === 0) {\n throw new Error(\n 'Unexpected fal image response shape. Expected images[] or image{}. Got keys: ' +\n Object.keys(data).join(','),\n )\n }\n\n return {\n id: response.requestId || this.generateId(),\n model: this.model,\n images,\n }\n }\n\n private parseImage(img: unknown): GeneratedImage {\n let url: string\n if (typeof img === 'string') {\n url = img\n } else if (\n img &&\n typeof img === 'object' &&\n 'url' in img &&\n typeof (img as { url: unknown }).url === 'string'\n ) {\n url = (img as { url: string }).url\n } else {\n throw new Error(\n `Invalid image payload from fal response: expected string or { url: string }, received ${\n img === null ? 'null' : typeof img\n }`,\n )\n }\n\n if (url.startsWith('data:')) {\n const base64Match = url.match(/^data:image\\/[^;]+;base64,(.+)$/)\n if (base64Match && base64Match[1]) {\n return { b64Json: base64Match[1] }\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,OAAO,EAAE;AAJjB,SAAS,OAAO;AAChB,SAAS,OAAO;AAId,uBAAmB,MAAM;AAAA,EAC3B;AAAA,EAEA,MAAM,eACJ,SAIgC;AAChC,UAAM,EAAE,WAAW;AAEnB,WAAO,QAAQ,6CAA6C,KAAK,KAAK,IAAI;AAAA,MACxE,UAAU;AAAA,MACV,OAAO,KAAK;AAAA,IAAA,CACb;AAED,QAAI;AACF,YAAM,QAAQ,KAAK,WAAW,OAAO;AACrC,YAAM,SAAS,MAAM,IAAI,UAAU,KAAK,OAAO,EAAE,OAAO;AACxD,aAAO,KAAK,kBAAkB,MAAM;AAAA,IACtC,SAAS,OAAO;AACd,aAAO,OAAO,2BAA2B;AAAA,QACvC;AAAA,QACA,QAAQ;AAAA,MAAA,CACT;AACD,YAAM;AAAA,IACR;AAAA,EACF;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,QAAI,OAAO,WAAW,GAAG;AACvB,YAAM,IAAI;AAAA,QACR,kFACE,OAAO,KAAK,IAAI,EAAE,KAAK,GAAG;AAAA,MAAA;AAAA,IAEhC;AAEA,WAAO;AAAA,MACL,IAAI,SAAS,aAAa,KAAK,WAAA;AAAA,MAC/B,OAAO,KAAK;AAAA,MACZ;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEQ,WAAW,KAA8B;AAC/C,QAAI;AACJ,QAAI,OAAO,QAAQ,UAAU;AAC3B,YAAM;AAAA,IACR,WACE,OACA,OAAO,QAAQ,YACf,SAAS,OACT,OAAQ,IAAyB,QAAQ,UACzC;AACA,YAAO,IAAwB;AAAA,IACjC,OAAO;AACL,YAAM,IAAI;AAAA,QACR,yFACE,QAAQ,OAAO,SAAS,OAAO,GACjC;AAAA,MAAA;AAAA,IAEJ;AAEA,QAAI,IAAI,WAAW,OAAO,GAAG;AAC3B,YAAM,cAAc,IAAI,MAAM,iCAAiC;AAC/D,UAAI,eAAe,YAAY,CAAC,GAAG;AACjC,eAAO,EAAE,SAAS,YAAY,CAAC,EAAA;AAAA,MACjC;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;"}
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 { logger } = options\n\n logger.request(`activity=generateImage provider=fal model=${this.model}`, {\n provider: 'fal',\n model: this.model,\n })\n\n try {\n const input = this.buildInput(options)\n const result = await fal.subscribe(this.model, { input })\n return this.transformResponse(result)\n } catch (error) {\n logger.errors('fal.generateImage fatal', {\n error,\n source: 'fal.generateImage',\n })\n throw error\n }\n }\n\n private buildInput(\n options: ImageGenerationOptions<\n FalImageProviderOptions<TModel>,\n FalModelImageSize<TModel>\n >,\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 if (images.length === 0) {\n throw new Error(\n 'Unexpected fal image response shape. Expected images[] or image{}. Got keys: ' +\n Object.keys(data).join(','),\n )\n }\n\n return {\n id: response.requestId || this.generateId(),\n model: this.model,\n images,\n }\n }\n\n private parseImage(img: unknown): GeneratedImage {\n let url: string\n if (typeof img === 'string') {\n url = img\n } else if (\n img &&\n typeof img === 'object' &&\n 'url' in img &&\n typeof (img as { url: unknown }).url === 'string'\n ) {\n url = (img as { url: string }).url\n } else {\n throw new Error(\n `Invalid image payload from fal response: expected string or { url: string }, received ${\n img === null ? 'null' : typeof img\n }`,\n )\n }\n\n if (url.startsWith('data:')) {\n const base64Match = url.match(/^data:image\\/[^;]+;base64,(.+)$/)\n if (base64Match && base64Match[1]) {\n return { b64Json: base64Match[1] }\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,OAAO,EAAE;AAJjB,SAAS,OAAO;AAChB,SAAS,OAAO;AAId,uBAAmB,MAAM;AAAA,EAC3B;AAAA,EAEA,MAAM,eACJ,SAIgC;AAChC,UAAM,EAAE,WAAW;AAEnB,WAAO,QAAQ,6CAA6C,KAAK,KAAK,IAAI;AAAA,MACxE,UAAU;AAAA,MACV,OAAO,KAAK;AAAA,IAAA,CACb;AAED,QAAI;AACF,YAAM,QAAQ,KAAK,WAAW,OAAO;AACrC,YAAM,SAAS,MAAM,IAAI,UAAU,KAAK,OAAO,EAAE,OAAO;AACxD,aAAO,KAAK,kBAAkB,MAAM;AAAA,IACtC,SAAS,OAAO;AACd,aAAO,OAAO,2BAA2B;AAAA,QACvC;AAAA,QACA,QAAQ;AAAA,MAAA,CACT;AACD,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEQ,WACN,SAIuB;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,QAAI,OAAO,WAAW,GAAG;AACvB,YAAM,IAAI;AAAA,QACR,kFACE,OAAO,KAAK,IAAI,EAAE,KAAK,GAAG;AAAA,MAAA;AAAA,IAEhC;AAEA,WAAO;AAAA,MACL,IAAI,SAAS,aAAa,KAAK,WAAA;AAAA,MAC/B,OAAO,KAAK;AAAA,MACZ;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEQ,WAAW,KAA8B;AAC/C,QAAI;AACJ,QAAI,OAAO,QAAQ,UAAU;AAC3B,YAAM;AAAA,IACR,WACE,OACA,OAAO,QAAQ,YACf,SAAS,OACT,OAAQ,IAAyB,QAAQ,UACzC;AACA,YAAO,IAAwB;AAAA,IACjC,OAAO;AACL,YAAM,IAAI;AAAA,QACR,yFACE,QAAQ,OAAO,SAAS,OAAO,GACjC;AAAA,MAAA;AAAA,IAEJ;AAEA,QAAI,IAAI,WAAW,OAAO,GAAG;AAC3B,YAAM,cAAc,IAAI,MAAM,iCAAiC;AAC/D,UAAI,eAAe,YAAY,CAAC,GAAG;AACjC,eAAO,EAAE,SAAS,YAAY,CAAC,EAAA;AAAA,MACjC;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;"}
@@ -1,12 +1,16 @@
1
1
  function mapSizeToFalFormat(size) {
2
2
  if (!size) return void 0;
3
3
  if (typeof size === "string") {
4
- const match = size.match(/^(\d+:\d+)(?:_(.+))?$/);
5
- if (match) {
6
- return {
7
- aspect_ratio: match[1],
8
- ...match[2] ? { resolution: match[2] } : {}
9
- };
4
+ if (size.includes("_")) {
5
+ const [first, second] = size.split("_");
6
+ if (first && first.includes(":")) {
7
+ return {
8
+ aspect_ratio: first,
9
+ resolution: second
10
+ };
11
+ }
12
+ } else if (size.includes(":")) {
13
+ return { aspect_ratio: size };
10
14
  }
11
15
  }
12
16
  return {
@@ -1 +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 if (typeof size === 'string') {\n const match = size.match(/^(\\d+:\\d+)(?:_(.+))?$/)\n if (match) {\n return {\n aspect_ratio: match[1],\n ...(match[2] ? { resolution: match[2] } : {}),\n } as FalModelImageSizeInput<TModel>\n }\n }\n\n return {\n image_size: size,\n } as FalModelImageSizeInput<TModel>\n}\n"],"names":[],"mappings":"AAEO,SAAS,mBACd,MAC4C;AAC5C,MAAI,CAAC,KAAM,QAAO;AAKlB,MAAI,OAAO,SAAS,UAAU;AAC5B,UAAM,QAAQ,KAAK,MAAM,uBAAuB;AAChD,QAAI,OAAO;AACT,aAAO;AAAA,QACL,cAAc,MAAM,CAAC;AAAA,QACrB,GAAI,MAAM,CAAC,IAAI,EAAE,YAAY,MAAM,CAAC,MAAM,CAAA;AAAA,MAAC;AAAA,IAE/C;AAAA,EACF;AAEA,SAAO;AAAA,IACL,YAAY;AAAA,EAAA;AAEhB;"}
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 // \"4K\" → { resolution } (no colon, no underscore, model has `resolution`)\n // \"square_hd\" → { image_size } (no colon, no resolution field on model)\n if (typeof size === 'string') {\n if (size.includes('_')) {\n const [first, second] = size.split('_')\n if (first && first.includes(':')) {\n return {\n aspect_ratio: first,\n resolution: second,\n } as FalModelImageSizeInput<TModel>\n }\n } else if (size.includes(':')) {\n return { aspect_ratio: size } as FalModelImageSizeInput<TModel>\n }\n }\n\n return {\n image_size: size,\n } as FalModelImageSizeInput<TModel>\n}\n"],"names":[],"mappings":"AAEO,SAAS,mBACd,MAC4C;AAC5C,MAAI,CAAC,KAAM,QAAO;AAMlB,MAAI,OAAO,SAAS,UAAU;AAC5B,QAAI,KAAK,SAAS,GAAG,GAAG;AACtB,YAAM,CAAC,OAAO,MAAM,IAAI,KAAK,MAAM,GAAG;AACtC,UAAI,SAAS,MAAM,SAAS,GAAG,GAAG;AAChC,eAAO;AAAA,UACL,cAAc;AAAA,UACd,YAAY;AAAA,QAAA;AAAA,MAEhB;AAAA,IACF,WAAW,KAAK,SAAS,GAAG,GAAG;AAC7B,aAAO,EAAE,cAAc,KAAA;AAAA,IACzB;AAAA,EACF;AAEA,SAAO;AAAA,IACL,YAAY;AAAA,EAAA;AAEhB;"}
@@ -29,15 +29,15 @@ export type FalModelOutput<TModel extends string> = TModel extends keyof Endpoin
29
29
  * type FluxSize = FalModelImageSize<'fal-ai/flux/dev'>
30
30
  * // "square_hd" | "square" | "portrait_4_3" | "portrait_16_9" | ...
31
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;
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'] ? `${Extract<NonNullable<FalModelInput<TModel>['aspect_ratio']>, string>}_${Extract<NonNullable<FalModelInput<TModel>['resolution']>, string>}` : Extract<NonNullable<FalModelInput<TModel>['aspect_ratio']>, string> : undefined : string;
33
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
34
  aspect_ratio: FalModelInput<TModel>['aspect_ratio'];
35
35
  resolution: FalModelInput<TModel>['resolution'];
36
36
  } : {
37
37
  aspect_ratio: NonNullable<FalModelInput<TModel>['aspect_ratio']>;
38
- } : {
38
+ } : 'image_size' extends keyof EndpointTypeMap[TModel]['input'] ? {
39
39
  image_size: FalModelImageSize<TModel>;
40
- } : {
40
+ } : never : {
41
41
  image_size: string;
42
42
  };
43
43
  /**
@@ -53,21 +53,23 @@ export type FalImageProviderOptions<TModel extends string> = Omit<FalModelInput<
53
53
  * Extract the video size type supported by a specific fal model.
54
54
  * Video models typically use aspect_ratio and/or resolution fields.
55
55
  *
56
- * Follows the same pattern as FalModelImageSize:
57
56
  * - aspect_ratio + resolution → "16:9_720p"
58
57
  * - aspect_ratio only → "16:9"
58
+ * - resolution only → "720p"
59
+ * - neither → undefined (the model takes no size param)
59
60
  * - unknown models → string
60
61
  */
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 FalModelVideoSize<TModel extends string> = TModel extends keyof EndpointTypeMap ? 'aspect_ratio' extends keyof EndpointTypeMap[TModel]['input'] ? 'resolution' extends keyof EndpointTypeMap[TModel]['input'] ? `${Extract<NonNullable<FalModelInput<TModel>['aspect_ratio']>, string>}_${Extract<NonNullable<FalModelInput<TModel>['resolution']>, string>}` : Extract<NonNullable<FalModelInput<TModel>['aspect_ratio']>, string> : 'resolution' extends keyof EndpointTypeMap[TModel]['input'] ? Extract<NonNullable<FalModelInput<TModel>['resolution']>, string> : undefined : string;
62
63
  export type FalModelVideoSizeInput<TModel extends string> = TModel extends keyof EndpointTypeMap ? 'aspect_ratio' extends keyof EndpointTypeMap[TModel]['input'] ? 'resolution' extends keyof EndpointTypeMap[TModel]['input'] ? {
63
64
  aspect_ratio: FalModelInput<TModel>['aspect_ratio'];
64
65
  resolution: FalModelInput<TModel>['resolution'];
65
66
  } : {
66
67
  aspect_ratio: NonNullable<FalModelInput<TModel>['aspect_ratio']>;
67
- } : {
68
- aspect_ratio: string;
69
- } : {
70
- aspect_ratio: string;
68
+ } : 'resolution' extends keyof EndpointTypeMap[TModel]['input'] ? {
69
+ resolution: NonNullable<FalModelInput<TModel>['resolution']>;
70
+ } : never : {
71
+ aspect_ratio?: string;
72
+ resolution?: string;
71
73
  };
72
74
  /**
73
75
  * Provider options for video generation, excluding fields TanStack AI handles.
@@ -1,12 +1,16 @@
1
1
  function mapVideoSizeToFalFormat(size) {
2
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
- };
3
+ if (size.includes("_")) {
4
+ const [aspect_ratio, resolution] = size.split("_");
5
+ return {
6
+ aspect_ratio,
7
+ resolution
8
+ };
9
+ }
10
+ if (size.includes(":")) {
11
+ return { aspect_ratio: size };
12
+ }
13
+ return { resolution: size };
10
14
  }
11
15
  export {
12
16
  mapVideoSizeToFalFormat
@@ -1 +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;"}
1
+ {"version":3,"file":"video-provider-options.js","sources":["../../../src/video/video-provider-options.ts"],"sourcesContent":["import type { FalModelVideoSize, FalModelVideoSizeInput } 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 // \"720p\" → { resolution }\n if (size.includes('_')) {\n const [aspect_ratio, resolution] = size.split('_')\n return {\n aspect_ratio,\n resolution,\n } as unknown as FalModelVideoSizeInput<TModel>\n }\n\n if (size.includes(':')) {\n return { aspect_ratio: size } as unknown as FalModelVideoSizeInput<TModel>\n }\n\n return { resolution: size } as unknown as FalModelVideoSizeInput<TModel>\n}\n"],"names":[],"mappings":"AAEO,SAAS,wBACd,MAC4C;AAC5C,MAAI,CAAC,KAAM,QAAO;AAKlB,MAAI,KAAK,SAAS,GAAG,GAAG;AACtB,UAAM,CAAC,cAAc,UAAU,IAAI,KAAK,MAAM,GAAG;AACjD,WAAO;AAAA,MACL;AAAA,MACA;AAAA,IAAA;AAAA,EAEJ;AAEA,MAAI,KAAK,SAAS,GAAG,GAAG;AACtB,WAAO,EAAE,cAAc,KAAA;AAAA,EACzB;AAEA,SAAO,EAAE,YAAY,KAAA;AACvB;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-fal",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "description": "fal.ai adapter for TanStack AI",
5
5
  "author": "",
6
6
  "license": "MIT",
@@ -36,16 +36,16 @@
36
36
  "transcription"
37
37
  ],
38
38
  "dependencies": {
39
- "@fal-ai/client": "^1.9.4",
39
+ "@fal-ai/client": "^1.10.1",
40
40
  "@tanstack/ai-utils": "0.2.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@vitest/coverage-v8": "4.0.14",
44
44
  "vite": "^7.2.7",
45
- "@tanstack/ai": "0.16.0"
45
+ "@tanstack/ai": "0.17.0"
46
46
  },
47
47
  "peerDependencies": {
48
- "@tanstack/ai": "0.16.0"
48
+ "@tanstack/ai": "0.17.0"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "vite build",
@@ -77,7 +77,10 @@ export class FalImageAdapter<TModel extends FalModel> extends BaseImageAdapter<
77
77
  }
78
78
 
79
79
  private buildInput(
80
- options: ImageGenerationOptions<FalImageProviderOptions<TModel>, string>,
80
+ options: ImageGenerationOptions<
81
+ FalImageProviderOptions<TModel>,
82
+ FalModelImageSize<TModel>
83
+ >,
81
84
  ): FalModelInput<TModel> {
82
85
  const sizeParams = mapSizeToFalFormat(options.size)
83
86
  const input = {
@@ -5,16 +5,21 @@ export function mapSizeToFalFormat<TModel extends string>(
5
5
  ): FalModelImageSizeInput<TModel> | undefined {
6
6
  if (!size) return undefined
7
7
 
8
- // "16:9_4K" → { aspect_ratio, resolution }
9
- // "16:9" → { aspect_ratio }
10
- // no match → { image_size }
8
+ // "16:9_4K" → { aspect_ratio, resolution }
9
+ // "16:9" → { aspect_ratio }
10
+ // "4K" → { resolution } (no colon, no underscore, model has `resolution`)
11
+ // "square_hd" → { image_size } (no colon, no resolution field on model)
11
12
  if (typeof size === 'string') {
12
- const match = size.match(/^(\d+:\d+)(?:_(.+))?$/)
13
- if (match) {
14
- return {
15
- aspect_ratio: match[1],
16
- ...(match[2] ? { resolution: match[2] } : {}),
17
- } as FalModelImageSizeInput<TModel>
13
+ if (size.includes('_')) {
14
+ const [first, second] = size.split('_')
15
+ if (first && first.includes(':')) {
16
+ return {
17
+ aspect_ratio: first,
18
+ resolution: second,
19
+ } as FalModelImageSizeInput<TModel>
20
+ }
21
+ } else if (size.includes(':')) {
22
+ return { aspect_ratio: size } as FalModelImageSizeInput<TModel>
18
23
  }
19
24
  }
20
25
 
package/src/model-meta.ts CHANGED
@@ -51,9 +51,9 @@ export type FalModelImageSize<TModel extends string> =
51
51
  ? NonNullable<Exclude<FalModelInput<TModel>['image_size'], object>>
52
52
  : 'aspect_ratio' extends keyof EndpointTypeMap[TModel]['input']
53
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
54
+ ? `${Extract<NonNullable<FalModelInput<TModel>['aspect_ratio']>, string>}_${Extract<NonNullable<FalModelInput<TModel>['resolution']>, string>}`
55
+ : Extract<NonNullable<FalModelInput<TModel>['aspect_ratio']>, string>
56
+ : undefined
57
57
  : string
58
58
 
59
59
  export type FalModelImageSizeInput<TModel extends string> =
@@ -65,7 +65,9 @@ export type FalModelImageSizeInput<TModel extends string> =
65
65
  resolution: FalModelInput<TModel>['resolution']
66
66
  }
67
67
  : { aspect_ratio: NonNullable<FalModelInput<TModel>['aspect_ratio']> }
68
- : { image_size: FalModelImageSize<TModel> }
68
+ : 'image_size' extends keyof EndpointTypeMap[TModel]['input']
69
+ ? { image_size: FalModelImageSize<TModel> }
70
+ : never
69
71
  : { image_size: string }
70
72
 
71
73
  /**
@@ -85,18 +87,21 @@ export type FalImageProviderOptions<TModel extends string> = Omit<
85
87
  * Extract the video size type supported by a specific fal model.
86
88
  * Video models typically use aspect_ratio and/or resolution fields.
87
89
  *
88
- * Follows the same pattern as FalModelImageSize:
89
90
  * - aspect_ratio + resolution → "16:9_720p"
90
91
  * - aspect_ratio only → "16:9"
92
+ * - resolution only → "720p"
93
+ * - neither → undefined (the model takes no size param)
91
94
  * - unknown models → string
92
95
  */
93
96
  export type FalModelVideoSize<TModel extends string> =
94
97
  TModel extends keyof EndpointTypeMap
95
98
  ? 'aspect_ratio' extends keyof EndpointTypeMap[TModel]['input']
96
99
  ? '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
+ ? `${Extract<NonNullable<FalModelInput<TModel>['aspect_ratio']>, string>}_${Extract<NonNullable<FalModelInput<TModel>['resolution']>, string>}`
101
+ : Extract<NonNullable<FalModelInput<TModel>['aspect_ratio']>, string>
102
+ : 'resolution' extends keyof EndpointTypeMap[TModel]['input']
103
+ ? Extract<NonNullable<FalModelInput<TModel>['resolution']>, string>
104
+ : undefined
100
105
  : string
101
106
 
102
107
  export type FalModelVideoSizeInput<TModel extends string> =
@@ -108,8 +113,10 @@ export type FalModelVideoSizeInput<TModel extends string> =
108
113
  resolution: FalModelInput<TModel>['resolution']
109
114
  }
110
115
  : { aspect_ratio: NonNullable<FalModelInput<TModel>['aspect_ratio']> }
111
- : { aspect_ratio: string }
112
- : { aspect_ratio: string }
116
+ : 'resolution' extends keyof EndpointTypeMap[TModel]['input']
117
+ ? { resolution: NonNullable<FalModelInput<TModel>['resolution']> }
118
+ : never
119
+ : { aspect_ratio?: string; resolution?: string }
113
120
 
114
121
  /**
115
122
  * Provider options for video generation, excluding fields TanStack AI handles.
@@ -1,8 +1,4 @@
1
- import type {
2
- FalModelInput,
3
- FalModelVideoSize,
4
- FalModelVideoSizeInput,
5
- } from '../model-meta'
1
+ import type { FalModelVideoSize, FalModelVideoSizeInput } from '../model-meta'
6
2
 
7
3
  export function mapVideoSizeToFalFormat<TModel extends string>(
8
4
  size: FalModelVideoSize<TModel> | undefined,
@@ -11,14 +7,18 @@ export function mapVideoSizeToFalFormat<TModel extends string>(
11
7
 
12
8
  // "16:9_720p" → { aspect_ratio, resolution }
13
9
  // "16:9" → { aspect_ratio }
14
- const match = size.match(/^(\d+:\d+)(?:_(.+))?$/)
10
+ // "720p" → { resolution }
11
+ if (size.includes('_')) {
12
+ const [aspect_ratio, resolution] = size.split('_')
13
+ return {
14
+ aspect_ratio,
15
+ resolution,
16
+ } as unknown as FalModelVideoSizeInput<TModel>
17
+ }
15
18
 
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>
19
+ if (size.includes(':')) {
20
+ return { aspect_ratio: size } as unknown as FalModelVideoSizeInput<TModel>
21
+ }
22
+
23
+ return { resolution: size } as unknown as FalModelVideoSizeInput<TModel>
24
24
  }