@tanstack/ai-fal 0.6.16 → 0.6.17

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.
@@ -10,9 +10,22 @@ class FalImageAdapter extends BaseImageAdapter {
10
10
  configureFalClient(config);
11
11
  }
12
12
  async generateImages(options) {
13
- const input = this.buildInput(options);
14
- const result = await fal.subscribe(this.model, { input });
15
- return this.transformResponse(result);
13
+ const { logger } = options;
14
+ logger.request(`activity=generateImage provider=fal model=${this.model}`, {
15
+ provider: "fal",
16
+ model: this.model
17
+ });
18
+ try {
19
+ const input = this.buildInput(options);
20
+ const result = await fal.subscribe(this.model, { input });
21
+ return this.transformResponse(result);
22
+ } catch (error) {
23
+ logger.errors("fal.generateImage fatal", {
24
+ error,
25
+ source: "fal.generateImage"
26
+ });
27
+ throw error;
28
+ }
16
29
  }
17
30
  buildInput(options) {
18
31
  const sizeParams = mapSizeToFalFormat(options.size);
@@ -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 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;"}
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 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,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,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;"}
@@ -22,21 +22,33 @@ class FalVideoAdapter extends BaseVideoAdapter {
22
22
  configureFalClient(config);
23
23
  }
24
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,
25
+ const { prompt, size, duration, modelOptions, logger } = options;
26
+ logger.request(`activity=generateVideo provider=fal model=${this.model}`, {
27
+ provider: "fal",
38
28
  model: this.model
39
- };
29
+ });
30
+ try {
31
+ const sizeParams = mapVideoSizeToFalFormat(size);
32
+ const input = {
33
+ ...modelOptions,
34
+ ...sizeParams,
35
+ prompt,
36
+ ...duration ? { duration } : {}
37
+ };
38
+ const { request_id } = await fal.queue.submit(this.model, {
39
+ input
40
+ });
41
+ return {
42
+ jobId: request_id,
43
+ model: this.model
44
+ };
45
+ } catch (error) {
46
+ logger.errors("fal.createVideoJob fatal", {
47
+ error,
48
+ source: "fal.createVideoJob"
49
+ });
50
+ throw error;
51
+ }
40
52
  }
41
53
  async getVideoStatus(jobId) {
42
54
  const statusResponse = await fal.queue.status(this.model, {
@@ -1 +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 *\n * Note: fal.ai does not return a FAILED queue status. Errors surface\n * as exceptions when fetching results from a COMPLETED job (e.g. 422\n * validation errors). Those are handled in getVideoUrl().\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 let result\n try {\n result = await fal.queue.result(this.model, {\n requestId: jobId,\n })\n } catch (error: any) {\n // fal.ai may report COMPLETED status but throw on result fetch\n // (e.g. 422 validation errors). Extract the detailed error info.\n const detail = error?.body?.detail\n if (Array.isArray(detail)) {\n const messages = detail.map(\n (d: { msg?: string; loc?: Array<string> }) =>\n d.loc ? `${d.loc.join('.')}: ${d.msg}` : d.msg,\n )\n throw new Error(`Video generation failed: ${messages.join('; ')}`)\n }\n throw new Error(\n `Failed to retrieve video result: ${error.message || error}`,\n )\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":";;;;AAsCA,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,QAAI;AACJ,QAAI;AACF,eAAS,MAAM,IAAI,MAAM,OAAO,KAAK,OAAO;AAAA,QAC1C,WAAW;AAAA,MAAA,CACZ;AAAA,IACH,SAAS,OAAY;AAGnB,YAAM,SAAS,OAAO,MAAM;AAC5B,UAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,cAAM,WAAW,OAAO;AAAA,UACtB,CAAC,MACC,EAAE,MAAM,GAAG,EAAE,IAAI,KAAK,GAAG,CAAC,KAAK,EAAE,GAAG,KAAK,EAAE;AAAA,QAAA;AAE/C,cAAM,IAAI,MAAM,4BAA4B,SAAS,KAAK,IAAI,CAAC,EAAE;AAAA,MACnE;AACA,YAAM,IAAI;AAAA,QACR,oCAAoC,MAAM,WAAW,KAAK;AAAA,MAAA;AAAA,IAE9D;AAEA,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;"}
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 *\n * Note: fal.ai does not return a FAILED queue status. Errors surface\n * as exceptions when fetching results from a COMPLETED job (e.g. 422\n * validation errors). Those are handled in getVideoUrl().\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, logger } = options\n\n logger.request(`activity=generateVideo provider=fal model=${this.model}`, {\n provider: 'fal',\n model: this.model,\n })\n\n try {\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 } catch (error) {\n logger.errors('fal.createVideoJob fatal', {\n error,\n source: 'fal.createVideoJob',\n })\n throw error\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 let result\n try {\n result = await fal.queue.result(this.model, {\n requestId: jobId,\n })\n } catch (error: any) {\n // fal.ai may report COMPLETED status but throw on result fetch\n // (e.g. 422 validation errors). Extract the detailed error info.\n const detail = error?.body?.detail\n if (Array.isArray(detail)) {\n const messages = detail.map(\n (d: { msg?: string; loc?: Array<string> }) =>\n d.loc ? `${d.loc.join('.')}: ${d.msg}` : d.msg,\n )\n throw new Error(`Video generation failed: ${messages.join('; ')}`)\n }\n throw new Error(\n `Failed to retrieve video result: ${error.message || error}`,\n )\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":";;;;AAsCA,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,cAAc,WAAW;AAEzD,WAAO,QAAQ,6CAA6C,KAAK,KAAK,IAAI;AAAA,MACxE,UAAU;AAAA,MACV,OAAO,KAAK;AAAA,IAAA,CACb;AAED,QAAI;AACF,YAAM,aAAa,wBAAwB,IAAI;AAE/C,YAAM,QAAQ;AAAA,QACZ,GAAG;AAAA,QACH,GAAG;AAAA,QACH;AAAA,QACA,GAAI,WAAW,EAAE,aAAa,CAAA;AAAA,MAAC;AAIjC,YAAM,EAAE,eAAe,MAAM,IAAI,MAAM,OAAO,KAAK,OAAO;AAAA,QACxD;AAAA,MAAA,CACD;AAED,aAAO;AAAA,QACL,OAAO;AAAA,QACP,OAAO,KAAK;AAAA,MAAA;AAAA,IAEhB,SAAS,OAAO;AACd,aAAO,OAAO,4BAA4B;AAAA,QACxC;AAAA,QACA,QAAQ;AAAA,MAAA,CACT;AACD,YAAM;AAAA,IACR;AAAA,EACF;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,QAAI;AACJ,QAAI;AACF,eAAS,MAAM,IAAI,MAAM,OAAO,KAAK,OAAO;AAAA,QAC1C,WAAW;AAAA,MAAA,CACZ;AAAA,IACH,SAAS,OAAY;AAGnB,YAAM,SAAS,OAAO,MAAM;AAC5B,UAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,cAAM,WAAW,OAAO;AAAA,UACtB,CAAC,MACC,EAAE,MAAM,GAAG,EAAE,IAAI,KAAK,GAAG,CAAC,KAAK,EAAE,GAAG,KAAK,EAAE;AAAA,QAAA;AAE/C,cAAM,IAAI,MAAM,4BAA4B,SAAS,KAAK,IAAI,CAAC,EAAE;AAAA,MACnE;AACA,YAAM,IAAI;AAAA,QACR,oCAAoC,MAAM,WAAW,KAAK;AAAA,MAAA;AAAA,IAE9D;AAEA,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;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-fal",
3
- "version": "0.6.16",
3
+ "version": "0.6.17",
4
4
  "description": "fal.ai adapter for TanStack AI",
5
5
  "author": "",
6
6
  "license": "MIT",
@@ -36,10 +36,10 @@
36
36
  "devDependencies": {
37
37
  "@vitest/coverage-v8": "4.0.14",
38
38
  "vite": "^7.2.7",
39
- "@tanstack/ai": "0.12.0"
39
+ "@tanstack/ai": "0.13.0"
40
40
  },
41
41
  "peerDependencies": {
42
- "@tanstack/ai": "0.12.0"
42
+ "@tanstack/ai": "0.13.0"
43
43
  },
44
44
  "scripts": {
45
45
  "build": "vite build",
@@ -56,9 +56,24 @@ export class FalImageAdapter<TModel extends FalModel> extends BaseImageAdapter<
56
56
  FalModelImageSize<TModel>
57
57
  >,
58
58
  ): Promise<ImageGenerationResult> {
59
- const input = this.buildInput(options)
60
- const result = await fal.subscribe(this.model, { input })
61
- return this.transformResponse(result)
59
+ const { logger } = options
60
+
61
+ logger.request(`activity=generateImage provider=fal model=${this.model}`, {
62
+ provider: 'fal',
63
+ model: this.model,
64
+ })
65
+
66
+ try {
67
+ const input = this.buildInput(options)
68
+ const result = await fal.subscribe(this.model, { input })
69
+ return this.transformResponse(result)
70
+ } catch (error) {
71
+ logger.errors('fal.generateImage fatal', {
72
+ error,
73
+ source: 'fal.generateImage',
74
+ })
75
+ throw error
76
+ }
62
77
  }
63
78
 
64
79
  private buildInput(
@@ -80,24 +80,38 @@ export class FalVideoAdapter<TModel extends FalModel> extends BaseVideoAdapter<
80
80
  FalModelVideoSize<TModel>
81
81
  >,
82
82
  ): Promise<VideoJobResult> {
83
- const { prompt, size, duration, modelOptions } = options
84
- const sizeParams = mapVideoSizeToFalFormat(size)
85
-
86
- const input = {
87
- ...modelOptions,
88
- ...sizeParams,
89
- prompt,
90
- ...(duration ? { duration } : {}),
91
- } as FalModelInput<TModel>
92
-
93
- // Submit to queue and get request ID
94
- const { request_id } = await fal.queue.submit(this.model, {
95
- input,
96
- })
83
+ const { prompt, size, duration, modelOptions, logger } = options
97
84
 
98
- return {
99
- jobId: request_id,
85
+ logger.request(`activity=generateVideo provider=fal model=${this.model}`, {
86
+ provider: 'fal',
100
87
  model: this.model,
88
+ })
89
+
90
+ try {
91
+ const sizeParams = mapVideoSizeToFalFormat(size)
92
+
93
+ const input = {
94
+ ...modelOptions,
95
+ ...sizeParams,
96
+ prompt,
97
+ ...(duration ? { duration } : {}),
98
+ } as FalModelInput<TModel>
99
+
100
+ // Submit to queue and get request ID
101
+ const { request_id } = await fal.queue.submit(this.model, {
102
+ input,
103
+ })
104
+
105
+ return {
106
+ jobId: request_id,
107
+ model: this.model,
108
+ }
109
+ } catch (error) {
110
+ logger.errors('fal.createVideoJob fatal', {
111
+ error,
112
+ source: 'fal.createVideoJob',
113
+ })
114
+ throw error
101
115
  }
102
116
  }
103
117