@tanstack/ai-fal 0.14.6 → 0.15.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.
@@ -0,0 +1,28 @@
1
+ import { BaseFilesAdapter, FileHandle, FileUploadInput } from '@tanstack/ai/adapters';
2
+ import { StorageSettings } from '@fal-ai/client';
3
+ import { FalClientConfig } from '../utils/client.js';
4
+ export interface FalFilesConfig extends FalClientConfig {
5
+ /**
6
+ * Optional lifecycle for uploaded objects — one of fal's presets
7
+ * (`'1h' | '1d' | '7d' | '30d' | '1y' | 'never' | 'immediate'`) or a number
8
+ * of seconds. Applied as the object's `X-Fal-Object-Lifecycle-Preference`.
9
+ */
10
+ expiresIn?: StorageSettings['expiresIn'];
11
+ }
12
+ /**
13
+ * fal Files adapter — uploads media to fal storage via `fal.storage.upload`.
14
+ * The handle is a storage URL, so `fileSourceFromHandle(handle)` references it
15
+ * as a normal URL (fal endpoints accept it directly). Upload-only: fal storage
16
+ * has no retrieval/deletion API, so `get`/`delete` are unavailable.
17
+ */
18
+ export declare class FalFilesAdapter extends BaseFilesAdapter<'fal'> {
19
+ readonly name: "fal";
20
+ private readonly expiresIn?;
21
+ constructor(config?: FalFilesConfig);
22
+ upload(input: FileUploadInput): Promise<FileHandle<'fal'>>;
23
+ }
24
+ /**
25
+ * Create a fal Files adapter. Reads the API key from `FAL_KEY` unless one is
26
+ * provided in `config`.
27
+ */
28
+ export declare function falFiles(config?: FalFilesConfig): FalFilesAdapter;
@@ -0,0 +1,40 @@
1
+ import { configureFalClient } from "../utils/client.js";
2
+ import { fal } from "@fal-ai/client";
3
+ import { BaseFilesAdapter, normalizeFileUploadInput } from "@tanstack/ai/adapters";
4
+ //#region src/adapters/files.ts
5
+ /**
6
+ * fal Files adapter — uploads media to fal storage via `fal.storage.upload`.
7
+ * The handle is a storage URL, so `fileSourceFromHandle(handle)` references it
8
+ * as a normal URL (fal endpoints accept it directly). Upload-only: fal storage
9
+ * has no retrieval/deletion API, so `get`/`delete` are unavailable.
10
+ */
11
+ var FalFilesAdapter = class extends BaseFilesAdapter {
12
+ name = "fal";
13
+ expiresIn;
14
+ constructor(config) {
15
+ super();
16
+ configureFalClient(config);
17
+ this.expiresIn = config?.expiresIn;
18
+ }
19
+ async upload(input) {
20
+ const { blob, mimeType } = normalizeFileUploadInput(input);
21
+ const url = await fal.storage.upload(blob, this.expiresIn !== void 0 ? { lifecycle: { expiresIn: this.expiresIn } } : void 0);
22
+ return {
23
+ id: url,
24
+ provider: "fal",
25
+ uri: url,
26
+ ...mimeType ? { mimeType } : {}
27
+ };
28
+ }
29
+ };
30
+ /**
31
+ * Create a fal Files adapter. Reads the API key from `FAL_KEY` unless one is
32
+ * provided in `config`.
33
+ */
34
+ function falFiles(config) {
35
+ return new FalFilesAdapter(config);
36
+ }
37
+ //#endregion
38
+ export { FalFilesAdapter, falFiles };
39
+
40
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.js","names":[],"sources":["../../../src/adapters/files.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\nimport {\n BaseFilesAdapter,\n normalizeFileUploadInput,\n} from '@tanstack/ai/adapters'\nimport { configureFalClient } from '../utils/client'\nimport type { StorageSettings } from '@fal-ai/client'\nimport type { FileHandle, FileUploadInput } from '@tanstack/ai/adapters'\nimport type { FalClientConfig } from '../utils/client'\n\nexport interface FalFilesConfig extends FalClientConfig {\n /**\n * Optional lifecycle for uploaded objects — one of fal's presets\n * (`'1h' | '1d' | '7d' | '30d' | '1y' | 'never' | 'immediate'`) or a number\n * of seconds. Applied as the object's `X-Fal-Object-Lifecycle-Preference`.\n */\n expiresIn?: StorageSettings['expiresIn']\n}\n\n/**\n * fal Files adapter — uploads media to fal storage via `fal.storage.upload`.\n * The handle is a storage URL, so `fileSourceFromHandle(handle)` references it\n * as a normal URL (fal endpoints accept it directly). Upload-only: fal storage\n * has no retrieval/deletion API, so `get`/`delete` are unavailable.\n */\nexport class FalFilesAdapter extends BaseFilesAdapter<'fal'> {\n readonly name = 'fal' as const\n private readonly expiresIn?: StorageSettings['expiresIn']\n\n constructor(config?: FalFilesConfig) {\n super()\n configureFalClient(config)\n this.expiresIn = config?.expiresIn\n }\n\n async upload(input: FileUploadInput): Promise<FileHandle<'fal'>> {\n const { blob, mimeType } = normalizeFileUploadInput(input)\n const url = await fal.storage.upload(\n blob,\n this.expiresIn !== undefined\n ? { lifecycle: { expiresIn: this.expiresIn } }\n : undefined,\n )\n return {\n id: url,\n provider: 'fal',\n uri: url,\n ...(mimeType ? { mimeType } : {}),\n }\n }\n}\n\n/**\n * Create a fal Files adapter. Reads the API key from `FAL_KEY` unless one is\n * provided in `config`.\n */\nexport function falFiles(config?: FalFilesConfig): FalFilesAdapter {\n return new FalFilesAdapter(config)\n}\n"],"mappings":";;;;;;;;;;AAyBA,IAAa,kBAAb,cAAqC,iBAAwB;CAC3D,OAAgB;CAChB;CAEA,YAAY,QAAyB;EACnC,MAAM;EACN,mBAAmB,MAAM;EACzB,KAAK,YAAY,QAAQ;CAC3B;CAEA,MAAM,OAAO,OAAoD;EAC/D,MAAM,EAAE,MAAM,aAAa,yBAAyB,KAAK;EACzD,MAAM,MAAM,MAAM,IAAI,QAAQ,OAC5B,MACA,KAAK,cAAc,KAAA,IACf,EAAE,WAAW,EAAE,WAAW,KAAK,UAAU,EAAE,IAC3C,KAAA,CACN;EACA,OAAO;GACL,IAAI;GACJ,UAAU;GACV,KAAK;GACL,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;EACjC;CACF;AACF;;;;;AAMA,SAAgB,SAAS,QAA0C;CACjE,OAAO,IAAI,gBAAgB,MAAM;AACnC"}
@@ -25,6 +25,7 @@ import { FalImagePromptModalitiesFor, FalImageProviderOptions, FalModel, FalMode
25
25
  export declare class FalImageAdapter<TModel extends FalModel> extends BaseImageAdapter<TModel, FalImageProviderOptions<TModel>, Record<TModel, FalImageProviderOptions<TModel>>, Record<TModel, FalModelImageSize<TModel>>, Record<TModel, FalImagePromptModalitiesFor<TModel>>> {
26
26
  readonly kind: "image";
27
27
  readonly name: "fal";
28
+ readonly supportsFileSources = true;
28
29
  constructor(model: TModel, config?: FalClientConfig);
29
30
  generateImages(options: ImageGenerationOptions<FalImageProviderOptions<TModel>, FalModelImageSize<TModel>>): Promise<ImageGenerationResult>;
30
31
  private buildInput;
@@ -29,6 +29,7 @@ import { BaseImageAdapter } from "@tanstack/ai/adapters";
29
29
  var FalImageAdapter = class extends BaseImageAdapter {
30
30
  kind = "image";
31
31
  name = "fal";
32
+ supportsFileSources = true;
32
33
  constructor(model, config) {
33
34
  super(model, {});
34
35
  configureFalClient(config);
@@ -1 +1 @@
1
- {"version":3,"file":"image.js","names":[],"sources":["../../../src/adapters/image.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\nimport { resolveMediaPrompt } from '@tanstack/ai'\nimport { BaseImageAdapter } from '@tanstack/ai/adapters'\nimport {\n configureFalClient,\n generateId as utilGenerateId,\n} from '../utils/client'\nimport { buildFalUsage, takeBillableUnits } from '../utils/billing'\nimport { mapSizeToFalFormat } from '../image/image-provider-options'\nimport { mapImageInputsToFalFields } from '../image/image-inputs'\nimport type { OutputType, Result } from '@fal-ai/client'\nimport type { FalClientConfig } from '../utils/client'\nimport type {\n GeneratedImage,\n ImageGenerationOptions,\n ImageGenerationResult,\n ResolvedMediaPrompt,\n} from '@tanstack/ai'\nimport type {\n FalImagePromptModalitiesFor,\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 Record<TModel, FalImagePromptModalitiesFor<TModel>>\n> {\n override 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 const resolved = resolveMediaPrompt(options.prompt)\n\n if (resolved.videos.length > 0) {\n throw new Error(\n `fal.generateImages does not support video prompt parts on model ${this.model}.`,\n )\n }\n if (resolved.audios.length > 0) {\n throw new Error(\n `fal.generateImages does not support audio prompt parts on model ${this.model}.`,\n )\n }\n\n try {\n const input = this.buildInput(options, resolved)\n // Pass request-specific abortSignal only — never via fal.config(), which\n // is global and would cancel concurrent generations from other calls.\n const result = await fal.subscribe(this.model, {\n input,\n ...(options.abortSignal ? { abortSignal: options.abortSignal } : {}),\n })\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 resolved: ResolvedMediaPrompt,\n ): FalModelInput<TModel> {\n const sizeParams = mapSizeToFalFormat(options.size)\n // Order matters: size and derived image-input fields first, then\n // modelOptions (so explicit user overrides win for mask_url /\n // control_image_url / reference_image_urls), then the call-controlled\n // prompt / num_images, which always take precedence.\n const inputFields = mapImageInputsToFalFields(this.model, resolved.images)\n const input = {\n ...sizeParams,\n ...inputFields,\n ...options.modelOptions,\n // Media-only prompts (e.g. upscalers, background removal) omit the\n // prompt field entirely rather than sending an empty string.\n ...(resolved.text ? { prompt: resolved.text } : {}),\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 const usage = buildFalUsage(takeBillableUnits(response.requestId))\n\n return {\n id: response.requestId || this.generateId(),\n model: this.model,\n images,\n ...(usage ? { usage } : {}),\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.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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,IAAa,kBAAb,cAA8D,iBAM5D;CACA,OAAyB;CACzB,OAAgB;CAEhB,YAAY,OAAe,QAA0B;EACnD,MAAM,OAAO,CAAC,CAAC;EACf,mBAAmB,MAAM;CAC3B;CAEA,MAAM,eACJ,SAIgC;EAChC,MAAM,EAAE,WAAW;EAEnB,OAAO,QAAQ,6CAA6C,KAAK,SAAS;GACxE,UAAU;GACV,OAAO,KAAK;EACd,CAAC;EAED,MAAM,WAAW,mBAAmB,QAAQ,MAAM;EAElD,IAAI,SAAS,OAAO,SAAS,GAC3B,MAAM,IAAI,MACR,mEAAmE,KAAK,MAAM,EAChF;EAEF,IAAI,SAAS,OAAO,SAAS,GAC3B,MAAM,IAAI,MACR,mEAAmE,KAAK,MAAM,EAChF;EAGF,IAAI;GACF,MAAM,QAAQ,KAAK,WAAW,SAAS,QAAQ;GAG/C,MAAM,SAAS,MAAM,IAAI,UAAU,KAAK,OAAO;IAC7C;IACA,GAAI,QAAQ,cAAc,EAAE,aAAa,QAAQ,YAAY,IAAI,CAAC;GACpE,CAAC;GACD,OAAO,KAAK,kBAAkB,MAAM;EACtC,SAAS,OAAO;GACd,OAAO,OAAO,2BAA2B;IACvC;IACA,QAAQ;GACV,CAAC;GACD,MAAM;EACR;CACF;CAEA,WACE,SAIA,UACuB;EACvB,MAAM,aAAa,mBAAmB,QAAQ,IAAI;EAKlD,MAAM,cAAc,0BAA0B,KAAK,OAAO,SAAS,MAAM;EAUzE,OAAO;GARL,GAAG;GACH,GAAG;GACH,GAAG,QAAQ;GAGX,GAAI,SAAS,OAAO,EAAE,QAAQ,SAAS,KAAK,IAAI,CAAC;GACjD,YAAY,QAAQ;EAEf;CACT;CAEA,aAAwC;EACtC,OAAO,WAAe,KAAK,IAAI;CACjC;CAEA,kBACE,UACuB;EACvB,MAAM,SAAgC,CAAC;EACvC,MAAM,OAAO,SAAS;EAGtB,IAAI,YAAY,QAAQ,MAAM,QAAQ,KAAK,MAAM,GAC/C,KAAK,MAAM,OAAO,KAAK,QACrB,OAAO,KAAK,KAAK,WAAW,GAAG,CAAC;OAI/B,IAAI,WAAW,QAAQ,KAAK,SAAS,OAAO,KAAK,UAAU,UAC9D,OAAO,KAAK,KAAK,WAAW,KAAK,KAAK,CAAC;EAGzC,IAAI,OAAO,WAAW,GACpB,MAAM,IAAI,MACR,kFACE,OAAO,KAAK,IAAI,CAAC,CAAC,KAAK,GAAG,CAC9B;EAGF,MAAM,QAAQ,cAAc,kBAAkB,SAAS,SAAS,CAAC;EAEjE,OAAO;GACL,IAAI,SAAS,aAAa,KAAK,WAAW;GAC1C,OAAO,KAAK;GACZ;GACA,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;EAC3B;CACF;CAEA,WAAmB,KAA8B;EAC/C,IAAI;EACJ,IAAI,OAAO,QAAQ,UACjB,MAAM;OACD,IACL,OACA,OAAO,QAAQ,YACf,SAAS,OACT,OAAO,IAAI,QAAQ,UAEnB,MAAO,IAAwB;OAE/B,MAAM,IAAI,MACR,yFACE,QAAQ,OAAO,SAAS,OAAO,KAEnC;EAGF,IAAI,IAAI,WAAW,OAAO,GAAG;GAC3B,MAAM,cAAc,IAAI,MAAM,iCAAiC;GAC/D,IAAI,eAAe,YAAY,IAC7B,OAAO,EAAE,SAAS,YAAY,GAAG;EAErC;EACA,OAAO,EAAE,IAAI;CACf;AACF;AAEA,SAAgB,eACd,OACA,QACyB;CACzB,OAAO,IAAI,gBAAgB,OAAO,MAAM;AAC1C;AAEA,SAAgB,SACd,OACA,QACyB;CACzB,OAAO,eAAe,OAAO,MAAM;AACrC"}
1
+ {"version":3,"file":"image.js","names":[],"sources":["../../../src/adapters/image.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\nimport { resolveMediaPrompt } from '@tanstack/ai'\nimport { BaseImageAdapter } from '@tanstack/ai/adapters'\nimport {\n configureFalClient,\n generateId as utilGenerateId,\n} from '../utils/client'\nimport { buildFalUsage, takeBillableUnits } from '../utils/billing'\nimport { mapSizeToFalFormat } from '../image/image-provider-options'\nimport { mapImageInputsToFalFields } from '../image/image-inputs'\nimport type { OutputType, Result } from '@fal-ai/client'\nimport type { FalClientConfig } from '../utils/client'\nimport type {\n GeneratedImage,\n ImageGenerationOptions,\n ImageGenerationResult,\n ResolvedMediaPrompt,\n} from '@tanstack/ai'\nimport type {\n FalImagePromptModalitiesFor,\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 Record<TModel, FalImagePromptModalitiesFor<TModel>>\n> {\n override readonly kind = 'image' as const\n readonly name = 'fal' as const\n // Consumes fal storage URLs uploaded via falFiles().\n override readonly supportsFileSources = true\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 const resolved = resolveMediaPrompt(options.prompt)\n\n if (resolved.videos.length > 0) {\n throw new Error(\n `fal.generateImages does not support video prompt parts on model ${this.model}.`,\n )\n }\n if (resolved.audios.length > 0) {\n throw new Error(\n `fal.generateImages does not support audio prompt parts on model ${this.model}.`,\n )\n }\n\n try {\n const input = this.buildInput(options, resolved)\n // Pass request-specific abortSignal only — never via fal.config(), which\n // is global and would cancel concurrent generations from other calls.\n const result = await fal.subscribe(this.model, {\n input,\n ...(options.abortSignal ? { abortSignal: options.abortSignal } : {}),\n })\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 resolved: ResolvedMediaPrompt,\n ): FalModelInput<TModel> {\n const sizeParams = mapSizeToFalFormat(options.size)\n // Order matters: size and derived image-input fields first, then\n // modelOptions (so explicit user overrides win for mask_url /\n // control_image_url / reference_image_urls), then the call-controlled\n // prompt / num_images, which always take precedence.\n const inputFields = mapImageInputsToFalFields(this.model, resolved.images)\n const input = {\n ...sizeParams,\n ...inputFields,\n ...options.modelOptions,\n // Media-only prompts (e.g. upscalers, background removal) omit the\n // prompt field entirely rather than sending an empty string.\n ...(resolved.text ? { prompt: resolved.text } : {}),\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 const usage = buildFalUsage(takeBillableUnits(response.requestId))\n\n return {\n id: response.requestId || this.generateId(),\n model: this.model,\n images,\n ...(usage ? { usage } : {}),\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.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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,IAAa,kBAAb,cAA8D,iBAM5D;CACA,OAAyB;CACzB,OAAgB;CAEhB,sBAAwC;CAExC,YAAY,OAAe,QAA0B;EACnD,MAAM,OAAO,CAAC,CAAC;EACf,mBAAmB,MAAM;CAC3B;CAEA,MAAM,eACJ,SAIgC;EAChC,MAAM,EAAE,WAAW;EAEnB,OAAO,QAAQ,6CAA6C,KAAK,SAAS;GACxE,UAAU;GACV,OAAO,KAAK;EACd,CAAC;EAED,MAAM,WAAW,mBAAmB,QAAQ,MAAM;EAElD,IAAI,SAAS,OAAO,SAAS,GAC3B,MAAM,IAAI,MACR,mEAAmE,KAAK,MAAM,EAChF;EAEF,IAAI,SAAS,OAAO,SAAS,GAC3B,MAAM,IAAI,MACR,mEAAmE,KAAK,MAAM,EAChF;EAGF,IAAI;GACF,MAAM,QAAQ,KAAK,WAAW,SAAS,QAAQ;GAG/C,MAAM,SAAS,MAAM,IAAI,UAAU,KAAK,OAAO;IAC7C;IACA,GAAI,QAAQ,cAAc,EAAE,aAAa,QAAQ,YAAY,IAAI,CAAC;GACpE,CAAC;GACD,OAAO,KAAK,kBAAkB,MAAM;EACtC,SAAS,OAAO;GACd,OAAO,OAAO,2BAA2B;IACvC;IACA,QAAQ;GACV,CAAC;GACD,MAAM;EACR;CACF;CAEA,WACE,SAIA,UACuB;EACvB,MAAM,aAAa,mBAAmB,QAAQ,IAAI;EAKlD,MAAM,cAAc,0BAA0B,KAAK,OAAO,SAAS,MAAM;EAUzE,OAAO;GARL,GAAG;GACH,GAAG;GACH,GAAG,QAAQ;GAGX,GAAI,SAAS,OAAO,EAAE,QAAQ,SAAS,KAAK,IAAI,CAAC;GACjD,YAAY,QAAQ;EAEf;CACT;CAEA,aAAwC;EACtC,OAAO,WAAe,KAAK,IAAI;CACjC;CAEA,kBACE,UACuB;EACvB,MAAM,SAAgC,CAAC;EACvC,MAAM,OAAO,SAAS;EAGtB,IAAI,YAAY,QAAQ,MAAM,QAAQ,KAAK,MAAM,GAC/C,KAAK,MAAM,OAAO,KAAK,QACrB,OAAO,KAAK,KAAK,WAAW,GAAG,CAAC;OAI/B,IAAI,WAAW,QAAQ,KAAK,SAAS,OAAO,KAAK,UAAU,UAC9D,OAAO,KAAK,KAAK,WAAW,KAAK,KAAK,CAAC;EAGzC,IAAI,OAAO,WAAW,GACpB,MAAM,IAAI,MACR,kFACE,OAAO,KAAK,IAAI,CAAC,CAAC,KAAK,GAAG,CAC9B;EAGF,MAAM,QAAQ,cAAc,kBAAkB,SAAS,SAAS,CAAC;EAEjE,OAAO;GACL,IAAI,SAAS,aAAa,KAAK,WAAW;GAC1C,OAAO,KAAK;GACZ;GACA,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;EAC3B;CACF;CAEA,WAAmB,KAA8B;EAC/C,IAAI;EACJ,IAAI,OAAO,QAAQ,UACjB,MAAM;OACD,IACL,OACA,OAAO,QAAQ,YACf,SAAS,OACT,OAAO,IAAI,QAAQ,UAEnB,MAAO,IAAwB;OAE/B,MAAM,IAAI,MACR,yFACE,QAAQ,OAAO,SAAS,OAAO,KAEnC;EAGF,IAAI,IAAI,WAAW,OAAO,GAAG;GAC3B,MAAM,cAAc,IAAI,MAAM,iCAAiC;GAC/D,IAAI,eAAe,YAAY,IAC7B,OAAO,EAAE,SAAS,YAAY,GAAG;EAErC;EACA,OAAO,EAAE,IAAI;CACf;AACF;AAEA,SAAgB,eACd,OACA,QACyB;CACzB,OAAO,IAAI,gBAAgB,OAAO,MAAM;AAC1C;AAEA,SAAgB,SACd,OACA,QACyB;CACzB,OAAO,eAAe,OAAO,MAAM;AACrC"}
@@ -14,6 +14,7 @@ import { FalClientConfig } from '../utils/client.js';
14
14
  export declare class FalVideoAdapter<TModel extends FalModel> extends BaseVideoAdapter<TModel, FalVideoProviderOptions<TModel>, Record<TModel, FalVideoProviderOptions<TModel>>, Record<TModel, FalModelVideoSize<TModel>>, Record<TModel, FalVideoPromptModalitiesFor<TModel>>, Record<TModel, FalModelVideoDuration<TModel>>> {
15
15
  readonly kind: "video";
16
16
  readonly name: "fal";
17
+ readonly supportsFileSources = true;
17
18
  constructor(model: TModel, config?: FalClientConfig);
18
19
  createVideoJob(options: VideoGenerationOptions<FalVideoProviderOptions<TModel>, FalModelVideoSize<TModel>, FalModelVideoDuration<TModel>>): Promise<VideoJobResult>;
19
20
  availableDurations(): DurationOptions<FalModelVideoDuration<TModel>>;
@@ -1,6 +1,6 @@
1
1
  import { buildFalUsage, takeBillableUnits } from "../utils/billing.js";
2
2
  import { configureFalClient, generateId } from "../utils/client.js";
3
- import { mapImageInputsToFalVideoFields } from "../image/image-inputs.js";
3
+ import { contentSourceToFalUrl, mapImageInputsToFalVideoFields } from "../image/image-inputs.js";
4
4
  import { getFalVideoDurationOptions, mapVideoSizeToFalFormat } from "../video/video-provider-options.js";
5
5
  import { fal } from "@fal-ai/client";
6
6
  import { resolveMediaPrompt } from "@tanstack/ai";
@@ -31,10 +31,10 @@ function mapAudioInputsToFalFields(audioInputs) {
31
31
  if (!audioInputs || audioInputs.length === 0) return {};
32
32
  const [part, ...rest] = audioInputs;
33
33
  if (!part || rest.length > 0) throw new Error(`fal: exactly one audio prompt part is supported (received ${audioInputs.length}).`);
34
- return { audio_url: part.source.type === "url" ? part.source.value : `data:${part.source.mimeType};base64,${part.source.value}` };
34
+ return { audio_url: contentSourceToFalUrl(part.source) };
35
35
  }
36
36
  function videoPartToUrl(part) {
37
- return part.source.type === "url" ? part.source.value : `data:${part.source.mimeType};base64,${part.source.value}`;
37
+ return contentSourceToFalUrl(part.source);
38
38
  }
39
39
  /**
40
40
  * Maps fal.ai queue status to TanStack AI video status.
@@ -63,6 +63,7 @@ function mapFalStatusToVideoStatus(falStatus) {
63
63
  var FalVideoAdapter = class extends BaseVideoAdapter {
64
64
  kind = "video";
65
65
  name = "fal";
66
+ supportsFileSources = true;
66
67
  constructor(model, config) {
67
68
  super({}, model);
68
69
  configureFalClient(config);
@@ -1 +1 @@
1
- {"version":3,"file":"video.js","names":[],"sources":["../../../src/adapters/video.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\nimport { resolveMediaPrompt } from '@tanstack/ai'\nimport { BaseVideoAdapter, snapToDurationOption } from '@tanstack/ai/adapters'\nimport {\n configureFalClient,\n generateId as utilGenerateId,\n} from '../utils/client'\nimport { buildFalUsage, takeBillableUnits } from '../utils/billing'\nimport {\n getFalVideoDurationOptions,\n mapVideoSizeToFalFormat,\n} from '../video/video-provider-options'\nimport { mapImageInputsToFalVideoFields } from '../image/image-inputs'\nimport type { DurationOptions } from '@tanstack/ai/adapters'\nimport type {\n AudioPart,\n MediaInputMetadata,\n VideoGenerationOptions,\n VideoJobResult,\n VideoPart,\n VideoStatusResult,\n VideoUrlResult,\n} from '@tanstack/ai'\nimport type {\n FalModel,\n FalModelInput,\n FalModelVideoDuration,\n FalModelVideoSize,\n FalVideoPromptModalitiesFor,\n FalVideoProviderOptions,\n} from '../model-meta'\nimport type { FalClientConfig } from '../utils/client'\n\n/**\n * Map video conditioning inputs onto fal field names.\n * Video-to-video endpoints on fal almost universally use `video_url`; the\n * occasional model takes `video_urls` (rare). Mirror the image-input logic\n * positionally with a `reference` role escape hatch via `reference_video_urls`.\n */\nfunction mapVideoInputsToFalFields(\n videoInputs?: ReadonlyArray<VideoPart<MediaInputMetadata>>,\n): Record<string, unknown> {\n if (!videoInputs || videoInputs.length === 0) return {}\n const references: Array<string> = []\n const sources: Array<string> = []\n for (const part of videoInputs) {\n const url = videoPartToUrl(part)\n if (\n part.metadata?.role === 'reference' ||\n part.metadata?.role === 'character'\n ) {\n references.push(url)\n } else {\n sources.push(url)\n }\n }\n const out: Record<string, unknown> = {}\n if (references.length > 0) out.reference_video_urls = references\n if (sources.length === 1) {\n out.video_url = sources[0]\n } else if (sources.length > 1) {\n out.video_urls = sources\n }\n return out\n}\n\nfunction mapAudioInputsToFalFields(\n audioInputs?: ReadonlyArray<AudioPart<MediaInputMetadata>>,\n): Record<string, unknown> {\n if (!audioInputs || audioInputs.length === 0) return {}\n const [part, ...rest] = audioInputs\n if (!part || rest.length > 0) {\n throw new Error(\n `fal: exactly one audio prompt part is supported (received ${audioInputs.length}).`,\n )\n }\n return {\n audio_url:\n part.source.type === 'url'\n ? part.source.value\n : `data:${part.source.mimeType};base64,${part.source.value}`,\n }\n}\n\nfunction videoPartToUrl(part: VideoPart<MediaInputMetadata>): string {\n return part.source.type === 'url'\n ? part.source.value\n : `data:${part.source.mimeType};base64,${part.source.value}`\n}\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 Record<TModel, FalVideoPromptModalitiesFor<TModel>>,\n Record<TModel, FalModelVideoDuration<TModel>>\n> {\n override 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 FalModelVideoDuration<TModel>\n >,\n ): Promise<VideoJobResult> {\n const { 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 resolved = resolveMediaPrompt(options.prompt)\n const sizeParams = mapVideoSizeToFalFormat(size)\n const inputImageFields = mapImageInputsToFalVideoFields(\n this.model,\n resolved.images,\n )\n const videoFields = mapVideoInputsToFalFields(resolved.videos)\n const audioFields = mapAudioInputsToFalFields(resolved.audios)\n\n const input = {\n ...sizeParams,\n ...inputImageFields,\n ...videoFields,\n ...audioFields,\n // modelOptions applied after derived media fields so explicit user\n // overrides (video_url, reference_video_urls, audio_url, ...) win.\n ...modelOptions,\n // Media-only prompts omit the prompt field rather than sending an\n // empty string (e.g. pure image-to-video endpoints).\n ...(resolved.text ? { prompt: resolved.text } : {}),\n ...(duration !== undefined ? { duration } : {}),\n } as FalModelInput<TModel>\n\n // Submit to queue and get request ID. Request-specific abortSignal only —\n // never via fal.config() (global; would cancel concurrent jobs).\n const { request_id } = await fal.queue.submit(this.model, {\n input,\n ...(options.abortSignal ? { abortSignal: options.abortSignal } : {}),\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 override availableDurations(): DurationOptions<\n FalModelVideoDuration<TModel>\n > {\n return getFalVideoDurationOptions(this.model)\n }\n\n override snapDuration(\n seconds: number,\n ): FalModelVideoDuration<TModel> | undefined {\n return snapToDurationOption(seconds, this.availableDurations())\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 ...(statusResponse.queue_position != null\n ? {\n progress: Math.max(0, 100 - statusResponse.queue_position * 10),\n }\n : {}),\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: unknown) {\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 err = error as { body?: { detail?: unknown }; message?: string }\n const detail = err.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: ${err.message || String(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 const usage = buildFalUsage(takeBillableUnits(result.requestId))\n\n return {\n jobId,\n url,\n ...(usage ? { usage } : {}),\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"],"mappings":"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"}
1
+ {"version":3,"file":"video.js","names":[],"sources":["../../../src/adapters/video.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\nimport { resolveMediaPrompt } from '@tanstack/ai'\nimport { BaseVideoAdapter, snapToDurationOption } from '@tanstack/ai/adapters'\nimport {\n configureFalClient,\n generateId as utilGenerateId,\n} from '../utils/client'\nimport { buildFalUsage, takeBillableUnits } from '../utils/billing'\nimport {\n getFalVideoDurationOptions,\n mapVideoSizeToFalFormat,\n} from '../video/video-provider-options'\nimport {\n contentSourceToFalUrl,\n mapImageInputsToFalVideoFields,\n} from '../image/image-inputs'\nimport type { DurationOptions } from '@tanstack/ai/adapters'\nimport type {\n AudioPart,\n MediaInputMetadata,\n VideoGenerationOptions,\n VideoJobResult,\n VideoPart,\n VideoStatusResult,\n VideoUrlResult,\n} from '@tanstack/ai'\nimport type {\n FalModel,\n FalModelInput,\n FalModelVideoDuration,\n FalModelVideoSize,\n FalVideoPromptModalitiesFor,\n FalVideoProviderOptions,\n} from '../model-meta'\nimport type { FalClientConfig } from '../utils/client'\n\n/**\n * Map video conditioning inputs onto fal field names.\n * Video-to-video endpoints on fal almost universally use `video_url`; the\n * occasional model takes `video_urls` (rare). Mirror the image-input logic\n * positionally with a `reference` role escape hatch via `reference_video_urls`.\n */\nfunction mapVideoInputsToFalFields(\n videoInputs?: ReadonlyArray<VideoPart<MediaInputMetadata>>,\n): Record<string, unknown> {\n if (!videoInputs || videoInputs.length === 0) return {}\n const references: Array<string> = []\n const sources: Array<string> = []\n for (const part of videoInputs) {\n const url = videoPartToUrl(part)\n if (\n part.metadata?.role === 'reference' ||\n part.metadata?.role === 'character'\n ) {\n references.push(url)\n } else {\n sources.push(url)\n }\n }\n const out: Record<string, unknown> = {}\n if (references.length > 0) out.reference_video_urls = references\n if (sources.length === 1) {\n out.video_url = sources[0]\n } else if (sources.length > 1) {\n out.video_urls = sources\n }\n return out\n}\n\nfunction mapAudioInputsToFalFields(\n audioInputs?: ReadonlyArray<AudioPart<MediaInputMetadata>>,\n): Record<string, unknown> {\n if (!audioInputs || audioInputs.length === 0) return {}\n const [part, ...rest] = audioInputs\n if (!part || rest.length > 0) {\n throw new Error(\n `fal: exactly one audio prompt part is supported (received ${audioInputs.length}).`,\n )\n }\n return {\n audio_url: contentSourceToFalUrl(part.source),\n }\n}\n\nfunction videoPartToUrl(part: VideoPart<MediaInputMetadata>): string {\n return contentSourceToFalUrl(part.source)\n}\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 Record<TModel, FalVideoPromptModalitiesFor<TModel>>,\n Record<TModel, FalModelVideoDuration<TModel>>\n> {\n override readonly kind = 'video' as const\n readonly name = 'fal' as const\n // Consumes fal storage URLs uploaded via falFiles().\n override readonly supportsFileSources = true\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 FalModelVideoDuration<TModel>\n >,\n ): Promise<VideoJobResult> {\n const { 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 resolved = resolveMediaPrompt(options.prompt)\n const sizeParams = mapVideoSizeToFalFormat(size)\n const inputImageFields = mapImageInputsToFalVideoFields(\n this.model,\n resolved.images,\n )\n const videoFields = mapVideoInputsToFalFields(resolved.videos)\n const audioFields = mapAudioInputsToFalFields(resolved.audios)\n\n const input = {\n ...sizeParams,\n ...inputImageFields,\n ...videoFields,\n ...audioFields,\n // modelOptions applied after derived media fields so explicit user\n // overrides (video_url, reference_video_urls, audio_url, ...) win.\n ...modelOptions,\n // Media-only prompts omit the prompt field rather than sending an\n // empty string (e.g. pure image-to-video endpoints).\n ...(resolved.text ? { prompt: resolved.text } : {}),\n ...(duration !== undefined ? { duration } : {}),\n } as FalModelInput<TModel>\n\n // Submit to queue and get request ID. Request-specific abortSignal only —\n // never via fal.config() (global; would cancel concurrent jobs).\n const { request_id } = await fal.queue.submit(this.model, {\n input,\n ...(options.abortSignal ? { abortSignal: options.abortSignal } : {}),\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 override availableDurations(): DurationOptions<\n FalModelVideoDuration<TModel>\n > {\n return getFalVideoDurationOptions(this.model)\n }\n\n override snapDuration(\n seconds: number,\n ): FalModelVideoDuration<TModel> | undefined {\n return snapToDurationOption(seconds, this.availableDurations())\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 ...(statusResponse.queue_position != null\n ? {\n progress: Math.max(0, 100 - statusResponse.queue_position * 10),\n }\n : {}),\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: unknown) {\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 err = error as { body?: { detail?: unknown }; message?: string }\n const detail = err.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: ${err.message || String(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 const usage = buildFalUsage(takeBillableUnits(result.requestId))\n\n return {\n jobId,\n url,\n ...(usage ? { usage } : {}),\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"],"mappings":";;;;;;;;;;;;;;AA0CA,SAAS,0BACP,aACyB;CACzB,IAAI,CAAC,eAAe,YAAY,WAAW,GAAG,OAAO,CAAC;CACtD,MAAM,aAA4B,CAAC;CACnC,MAAM,UAAyB,CAAC;CAChC,KAAK,MAAM,QAAQ,aAAa;EAC9B,MAAM,MAAM,eAAe,IAAI;EAC/B,IACE,KAAK,UAAU,SAAS,eACxB,KAAK,UAAU,SAAS,aAExB,WAAW,KAAK,GAAG;OAEnB,QAAQ,KAAK,GAAG;CAEpB;CACA,MAAM,MAA+B,CAAC;CACtC,IAAI,WAAW,SAAS,GAAG,IAAI,uBAAuB;CACtD,IAAI,QAAQ,WAAW,GACrB,IAAI,YAAY,QAAQ;MACnB,IAAI,QAAQ,SAAS,GAC1B,IAAI,aAAa;CAEnB,OAAO;AACT;AAEA,SAAS,0BACP,aACyB;CACzB,IAAI,CAAC,eAAe,YAAY,WAAW,GAAG,OAAO,CAAC;CACtD,MAAM,CAAC,MAAM,GAAG,QAAQ;CACxB,IAAI,CAAC,QAAQ,KAAK,SAAS,GACzB,MAAM,IAAI,MACR,6DAA6D,YAAY,OAAO,GAClF;CAEF,OAAO,EACL,WAAW,sBAAsB,KAAK,MAAM,EAC9C;AACF;AAEA,SAAS,eAAe,MAA6C;CACnE,OAAO,sBAAsB,KAAK,MAAM;AAC1C;;;;;;;;AAsBA,SAAS,0BACP,WAC6B;CAC7B,QAAQ,WAAR;EACE,KAAK,YACH,OAAO;EACT,KAAK,eACH,OAAO;EACT,KAAK,aACH,OAAO;EACT,SACE,OAAO;CACX;AACF;;;;;;;;;;AAWA,IAAa,kBAAb,cAA8D,iBAO5D;CACA,OAAyB;CACzB,OAAgB;CAEhB,sBAAwC;CAExC,YAAY,OAAe,QAA0B;EACnD,MAAM,CAAC,GAAG,KAAK;EACf,mBAAmB,MAAM;CAC3B;CAEA,MAAM,eACJ,SAKyB;EACzB,MAAM,EAAE,MAAM,UAAU,cAAc,WAAW;EAEjD,OAAO,QAAQ,6CAA6C,KAAK,SAAS;GACxE,UAAU;GACV,OAAO,KAAK;EACd,CAAC;EAED,IAAI;GACF,MAAM,WAAW,mBAAmB,QAAQ,MAAM;GAClD,MAAM,aAAa,wBAAwB,IAAI;GAC/C,MAAM,mBAAmB,+BACvB,KAAK,OACL,SAAS,MACX;GACA,MAAM,cAAc,0BAA0B,SAAS,MAAM;GAC7D,MAAM,cAAc,0BAA0B,SAAS,MAAM;GAE7D,MAAM,QAAQ;IACZ,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IAGH,GAAG;IAGH,GAAI,SAAS,OAAO,EAAE,QAAQ,SAAS,KAAK,IAAI,CAAC;IACjD,GAAI,aAAa,KAAA,IAAY,EAAE,SAAS,IAAI,CAAC;GAC/C;GAIA,MAAM,EAAE,eAAe,MAAM,IAAI,MAAM,OAAO,KAAK,OAAO;IACxD;IACA,GAAI,QAAQ,cAAc,EAAE,aAAa,QAAQ,YAAY,IAAI,CAAC;GACpE,CAAC;GAED,OAAO;IACL,OAAO;IACP,OAAO,KAAK;GACd;EACF,SAAS,OAAO;GACd,OAAO,OAAO,4BAA4B;IACxC;IACA,QAAQ;GACV,CAAC;GACD,MAAM;EACR;CACF;CAEA,qBAEE;EACA,OAAO,2BAA2B,KAAK,KAAK;CAC9C;CAEA,aACE,SAC2C;EAC3C,OAAO,qBAAqB,SAAS,KAAK,mBAAmB,CAAC;CAChE;CAEA,MAAM,eAAe,OAA2C;EAC9D,MAAM,iBAAkB,MAAM,IAAI,MAAM,OAAO,KAAK,OAAO;GACzD,WAAW;GACX,MAAM;EACR,CAAC;EAED,OAAO;GACL;GACA,QAAQ,0BAA0B,eAAe,MAAM;GACvD,GAAI,eAAe,kBAAkB,OACjC,EACE,UAAU,KAAK,IAAI,GAAG,MAAM,eAAe,iBAAiB,EAAE,EAChE,IACA,CAAC;EACP;CACF;CAEA,MAAM,YAAY,OAAwC;EACxD,IAAI;EACJ,IAAI;GACF,SAAS,MAAM,IAAI,MAAM,OAAO,KAAK,OAAO,EAC1C,WAAW,MACb,CAAC;EACH,SAAS,OAAgB;GAGvB,MAAM,MAAM;GACZ,MAAM,SAAS,IAAI,MAAM;GACzB,IAAI,MAAM,QAAQ,MAAM,GAAG;IACzB,MAAM,WAAW,OAAO,KACrB,MACC,EAAE,MAAM,GAAG,EAAE,IAAI,KAAK,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,GAC/C;IACA,MAAM,IAAI,MAAM,4BAA4B,SAAS,KAAK,IAAI,GAAG;GACnE;GACA,MAAM,IAAI,MACR,oCAAoC,IAAI,WAAW,OAAO,KAAK,GACjE;EACF;EAEA,MAAM,OAAO,OAAO;EAGpB,MAAM,MAAM,KAAK,OAAO,OAAO,KAAK;EACpC,IAAI,CAAC,KACH,MAAM,IAAI,MAAM,iCAAiC;EAGnD,MAAM,QAAQ,cAAc,kBAAkB,OAAO,SAAS,CAAC;EAE/D,OAAO;GACL;GACA;GACA,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;EAC3B;CACF;CAEA,aAAwC;EACtC,OAAO,WAAe,KAAK,IAAI;CACjC;AACF;;;;;;AAOA,SAAgB,eACd,OACA,QACyB;CACzB,OAAO,IAAI,gBAAgB,OAAO,MAAM;AAC1C;;;;;;AAOA,SAAgB,SACd,OACA,QACyB;CACzB,OAAO,eAAe,OAAO,MAAM;AACrC"}
@@ -1,6 +1,13 @@
1
1
  import { FalImageFieldName } from './generated/image-field-overrides.js';
2
- import { ImagePart, MediaInputMetadata } from '@tanstack/ai';
2
+ import { ContentPartSource, ImagePart, MediaInputMetadata } from '@tanstack/ai';
3
3
  import { FalModel, FalModelInput } from '../model-meta.js';
4
+ /**
5
+ * Convert a content source into a URL string for fal's URL-based input fields.
6
+ * URL sources pass through; fal storage handles (a storage URL) pass through
7
+ * after a provider check; base64 data becomes a `data:<mime>;base64,<value>`
8
+ * URI which fal endpoints accept on the wire.
9
+ */
10
+ export declare function contentSourceToFalUrl(source: ContentPartSource): string;
4
11
  /**
5
12
  * The image-conditioning fields the mappers may set, narrowed to the ones
6
13
  * that actually exist on the given endpoint's input type. For endpoints
@@ -1,6 +1,18 @@
1
1
  import { FAL_IMAGE_FIELD_OVERRIDES } from "./generated/image-field-overrides.js";
2
+ import { fileReferenceFor, isFileSource } from "@tanstack/ai";
2
3
  //#region src/image/image-inputs.ts
3
4
  /**
5
+ * Convert a content source into a URL string for fal's URL-based input fields.
6
+ * URL sources pass through; fal storage handles (a storage URL) pass through
7
+ * after a provider check; base64 data becomes a `data:<mime>;base64,<value>`
8
+ * URI which fal endpoints accept on the wire.
9
+ */
10
+ function contentSourceToFalUrl(source) {
11
+ if (isFileSource(source)) return fileReferenceFor(source, "fal");
12
+ if (source.type === "url") return source.value;
13
+ return `data:${source.mimeType};base64,${source.value}`;
14
+ }
15
+ /**
4
16
  * Default field per routing role. Endpoint-specific deviations live in the
5
17
  * generated `FAL_IMAGE_FIELD_OVERRIDES` map (regenerate with
6
18
  * `pnpm generate:fal-image-fields`); these defaults must stay in sync with
@@ -145,14 +157,12 @@ function mapImageInputsToFalVideoFields(model, imageInputs) {
145
157
  }
146
158
  /**
147
159
  * Convert a TanStack ImagePart into a string suitable for fal's URL-based
148
- * input fields. URL sources pass through; data sources are emitted as a
149
- * `data:<mime>;base64,<value>` URI which fal endpoints accept on the wire.
160
+ * input fields.
150
161
  */
151
162
  function imagePartToUrl(part) {
152
- if (part.source.type === "url") return part.source.value;
153
- return `data:${part.source.mimeType};base64,${part.source.value}`;
163
+ return contentSourceToFalUrl(part.source);
154
164
  }
155
165
  //#endregion
156
- export { mapImageInputsToFalFields, mapImageInputsToFalVideoFields };
166
+ export { contentSourceToFalUrl, mapImageInputsToFalFields, mapImageInputsToFalVideoFields };
157
167
 
158
168
  //# sourceMappingURL=image-inputs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"image-inputs.js","names":[],"sources":["../../../src/image/image-inputs.ts"],"sourcesContent":["import { FAL_IMAGE_FIELD_OVERRIDES } from './generated/image-field-overrides'\nimport type {\n FalImageFieldName,\n FalImageFieldOverride,\n} from './generated/image-field-overrides'\nimport type { ImagePart, MediaInputMetadata } from '@tanstack/ai'\nimport type { FalModel, FalModelInput } from '../model-meta'\n\n/**\n * The image-conditioning fields the mappers may set, narrowed to the ones\n * that actually exist on the given endpoint's input type. For endpoints\n * unknown to the installed `@fal-ai/client` this widens to all known field\n * names.\n */\nexport type FalImageInputFields<TModel extends string> = Partial<\n Pick<\n FalModelInput<TModel>,\n Extract<keyof FalModelInput<TModel>, FalImageFieldName>\n >\n>\n\n/**\n * Default field per routing role. Endpoint-specific deviations live in the\n * generated `FAL_IMAGE_FIELD_OVERRIDES` map (regenerate with\n * `pnpm generate:fal-image-fields`); these defaults must stay in sync with\n * `DEFAULTS` in scripts/generate-fal-image-field-map.ts.\n */\nconst DEFAULT_FIELDS = {\n single: 'image_url',\n multi: 'image_urls',\n mask: 'mask_url',\n control: 'control_image_url',\n reference: 'reference_image_urls',\n start: 'start_image_url',\n end: 'end_image_url',\n} satisfies Required<FalImageFieldOverride>\n\n/**\n * Field names that accept an array of images. The generator asserts the\n * SDK types agree with this set, so wrap-vs-scalar decisions stay correct.\n */\nconst LIST_FIELDS = new Set<string>([\n 'image_urls',\n 'input_image_urls',\n 'ref_image_urls',\n 'reference_image_urls',\n])\n\n/** Resolve the per-role field names for a model: defaults + generated overrides. */\nfunction fieldSpecFor(model: string): Required<FalImageFieldOverride> {\n const overrides = (\n FAL_IMAGE_FIELD_OVERRIDES as Record<string, FalImageFieldOverride>\n )[model]\n return { ...DEFAULT_FIELDS, ...overrides }\n}\n\n/**\n * Assign URLs to a field, wrapping or unwrapping based on whether the field\n * takes an array. When two roles resolve to the same list field (e.g.\n * sources and references both land on `image_urls` for nano-banana edit)\n * the values are merged in assignment order; two roles resolving to the\n * same scalar field is ambiguous and throws. Throws when multiple images\n * target a scalar field.\n */\nfunction assignField(\n fields: Record<string, unknown>,\n field: string,\n urls: Array<string>,\n model: string,\n what: string,\n): void {\n if (urls.length === 0) return\n const existing = fields[field]\n if (LIST_FIELDS.has(field)) {\n fields[field] = Array.isArray(existing) ? [...existing, ...urls] : urls\n } else if (existing !== undefined) {\n throw new Error(\n `fal: multiple inputs map to '${field}' on model ${model}. Drop one of the conflicting inputs or pass the field explicitly via modelOptions.`,\n )\n } else if (urls.length === 1) {\n fields[field] = urls[0]\n } else {\n throw new Error(\n `fal: model ${model} accepts a single ${what} image via '${field}' (received ${urls.length}).`,\n )\n }\n}\n\ninterface RoleBuckets {\n sources: Array<string>\n masks: Array<string>\n controls: Array<string>\n references: Array<string>\n starts: Array<string>\n ends: Array<string>\n}\n\nfunction bucketByRole(\n imageInputs: ReadonlyArray<ImagePart<MediaInputMetadata>>,\n): RoleBuckets {\n const buckets: RoleBuckets = {\n sources: [],\n masks: [],\n controls: [],\n references: [],\n starts: [],\n ends: [],\n }\n for (const part of imageInputs) {\n const url = imagePartToUrl(part)\n const role = part.metadata?.role\n if (role === 'mask') buckets.masks.push(url)\n else if (role === 'control') buckets.controls.push(url)\n else if (role === 'reference' || role === 'character')\n buckets.references.push(url)\n else if (role === 'start_frame') buckets.starts.push(url)\n else if (role === 'end_frame') buckets.ends.push(url)\n else buckets.sources.push(url)\n }\n return buckets\n}\n\n/**\n * Map the prompt's image parts onto fal.ai image-endpoint fields.\n *\n * fal endpoints use different field names for image-conditioned generation\n * (~80% use `image_url` for single; the rest use `image_urls`,\n * `reference_image_urls`, `mask_url`, `control_image_url`, etc.). Field\n * names are resolved per endpoint from the generated\n * `FAL_IMAGE_FIELD_OVERRIDES` map (derived from the fal SDK's endpoint\n * types), falling back to the defaults above for endpoints the installed\n * SDK doesn't know:\n *\n * - parts with `metadata.role === 'mask'` → spec.mask (single)\n * - parts with `metadata.role === 'control'` → spec.control (single)\n * - `role === 'reference' | 'character'` → spec.reference\n * - `role === 'start_frame' | 'end_frame'` → treated as sources (frame\n * roles only apply to video generation)\n * - remaining parts → spec.single / spec.multi\n *\n * Users can always override the resulting field shape via `modelOptions`\n * (spread before these fields), or pass everything through `modelOptions`\n * directly when the mapping doesn't match an obscure endpoint.\n */\nexport function mapImageInputsToFalFields<TModel extends FalModel>(\n model: TModel,\n imageInputs?: ReadonlyArray<ImagePart<MediaInputMetadata>>,\n): FalImageInputFields<TModel> {\n if (!imageInputs || imageInputs.length === 0) return {}\n\n const spec = fieldSpecFor(model)\n const { sources, masks, controls, references, starts, ends } =\n bucketByRole(imageInputs)\n // Frame roles aren't meaningful for image generation; treat as the\n // primary source. The video mapper handles start/end framing.\n const allSources = [...sources, ...starts, ...ends]\n\n if (masks.length > 1) {\n throw new Error(\n `fal: only one input with metadata.role === 'mask' is supported per request (received ${masks.length}).`,\n )\n }\n if (controls.length > 1) {\n throw new Error(\n `fal: only one input with metadata.role === 'control' is supported per request (received ${controls.length}).`,\n )\n }\n\n const fields: Record<string, unknown> = {}\n const sourceField = allSources.length > 1 ? spec.multi : spec.single\n assignField(fields, sourceField, allSources, model, 'source')\n assignField(fields, spec.reference, references, model, 'reference')\n assignField(fields, spec.mask, masks, model, 'mask')\n assignField(fields, spec.control, controls, model, 'control')\n\n return fields as FalImageInputFields<TModel>\n}\n\n/**\n * Map the prompt's image parts onto fal.ai video-endpoint fields.\n *\n * Video endpoints often expose a start frame as `image_url` (76% of i2v\n * models) plus an optional `end_image_url`. Multi-reference video models\n * (Kling O3, Seedance reference-to-video) use `reference_image_urls` or\n * `image_urls`. Field names resolve through the same generated override\n * map as the image mapper — e.g. `role: 'start_frame'` lands on `image_url`\n * for Kling/Veo image-to-video and `first_frame_url` for Pixverse. Mapping:\n *\n * - `metadata.role === 'start_frame'` → spec.start\n * - `metadata.role === 'end_frame'` → spec.end\n * - `metadata.role === 'reference' | 'character'` → spec.reference\n * - `metadata.role === 'mask' | 'control'` → throws (no video routing)\n * - remaining parts (no role) → spec.single / spec.multi\n */\nexport function mapImageInputsToFalVideoFields<TModel extends FalModel>(\n model: TModel,\n imageInputs?: ReadonlyArray<ImagePart<MediaInputMetadata>>,\n): FalImageInputFields<TModel> {\n if (!imageInputs || imageInputs.length === 0) return {}\n\n const spec = fieldSpecFor(model)\n const { sources, masks, controls, references, starts, ends } =\n bucketByRole(imageInputs)\n // Mask / control roles have no video-specific routing; silently repurposing\n // them as source frames would hide the problem, so reject them instead.\n if (masks.length > 0 || controls.length > 0) {\n const role = masks.length > 0 ? 'mask' : 'control'\n throw new Error(\n `fal: metadata.role === '${role}' is not supported for video generation on model ${model}. ` +\n `Remove the role or pass the field explicitly via modelOptions.`,\n )\n }\n\n if (starts.length > 1) {\n throw new Error(\n `fal: only one input with metadata.role === 'start_frame' is supported (received ${starts.length}).`,\n )\n }\n if (ends.length > 1) {\n throw new Error(\n `fal: only one input with metadata.role === 'end_frame' is supported (received ${ends.length}).`,\n )\n }\n\n const fields: Record<string, unknown> = {}\n const sourceField = sources.length > 1 ? spec.multi : spec.single\n assignField(fields, sourceField, sources, model, 'source')\n assignField(fields, spec.reference, references, model, 'reference')\n // Frame roles assign last: when an endpoint routes the start frame to its\n // generic source field (e.g. Kling image-to-video) and an unroled source\n // was also provided, assignField rejects the ambiguous combination.\n assignField(fields, spec.start, starts, model, 'start frame')\n assignField(fields, spec.end, ends, model, 'end frame')\n\n return fields as FalImageInputFields<TModel>\n}\n\n/**\n * Convert a TanStack ImagePart into a string suitable for fal's URL-based\n * input fields. URL sources pass through; data sources are emitted as a\n * `data:<mime>;base64,<value>` URI which fal endpoints accept on the wire.\n */\nfunction imagePartToUrl(part: ImagePart<MediaInputMetadata>): string {\n if (part.source.type === 'url') return part.source.value\n return `data:${part.source.mimeType};base64,${part.source.value}`\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"image-inputs.js","names":[],"sources":["../../../src/image/image-inputs.ts"],"sourcesContent":["import { fileReferenceFor, isFileSource } from '@tanstack/ai'\nimport { FAL_IMAGE_FIELD_OVERRIDES } from './generated/image-field-overrides'\nimport type {\n FalImageFieldName,\n FalImageFieldOverride,\n} from './generated/image-field-overrides'\nimport type {\n ContentPartSource,\n ImagePart,\n MediaInputMetadata,\n} from '@tanstack/ai'\nimport type { FalModel, FalModelInput } from '../model-meta'\n\n/**\n * Convert a content source into a URL string for fal's URL-based input fields.\n * URL sources pass through; fal storage handles (a storage URL) pass through\n * after a provider check; base64 data becomes a `data:<mime>;base64,<value>`\n * URI which fal endpoints accept on the wire.\n */\nexport function contentSourceToFalUrl(source: ContentPartSource): string {\n if (isFileSource(source)) {\n // A fal handle is a storage URL; throws when another provider issued it.\n return fileReferenceFor(source, 'fal')\n }\n if (source.type === 'url') return source.value\n return `data:${source.mimeType};base64,${source.value}`\n}\n\n/**\n * The image-conditioning fields the mappers may set, narrowed to the ones\n * that actually exist on the given endpoint's input type. For endpoints\n * unknown to the installed `@fal-ai/client` this widens to all known field\n * names.\n */\nexport type FalImageInputFields<TModel extends string> = Partial<\n Pick<\n FalModelInput<TModel>,\n Extract<keyof FalModelInput<TModel>, FalImageFieldName>\n >\n>\n\n/**\n * Default field per routing role. Endpoint-specific deviations live in the\n * generated `FAL_IMAGE_FIELD_OVERRIDES` map (regenerate with\n * `pnpm generate:fal-image-fields`); these defaults must stay in sync with\n * `DEFAULTS` in scripts/generate-fal-image-field-map.ts.\n */\nconst DEFAULT_FIELDS = {\n single: 'image_url',\n multi: 'image_urls',\n mask: 'mask_url',\n control: 'control_image_url',\n reference: 'reference_image_urls',\n start: 'start_image_url',\n end: 'end_image_url',\n} satisfies Required<FalImageFieldOverride>\n\n/**\n * Field names that accept an array of images. The generator asserts the\n * SDK types agree with this set, so wrap-vs-scalar decisions stay correct.\n */\nconst LIST_FIELDS = new Set<string>([\n 'image_urls',\n 'input_image_urls',\n 'ref_image_urls',\n 'reference_image_urls',\n])\n\n/** Resolve the per-role field names for a model: defaults + generated overrides. */\nfunction fieldSpecFor(model: string): Required<FalImageFieldOverride> {\n const overrides = (\n FAL_IMAGE_FIELD_OVERRIDES as Record<string, FalImageFieldOverride>\n )[model]\n return { ...DEFAULT_FIELDS, ...overrides }\n}\n\n/**\n * Assign URLs to a field, wrapping or unwrapping based on whether the field\n * takes an array. When two roles resolve to the same list field (e.g.\n * sources and references both land on `image_urls` for nano-banana edit)\n * the values are merged in assignment order; two roles resolving to the\n * same scalar field is ambiguous and throws. Throws when multiple images\n * target a scalar field.\n */\nfunction assignField(\n fields: Record<string, unknown>,\n field: string,\n urls: Array<string>,\n model: string,\n what: string,\n): void {\n if (urls.length === 0) return\n const existing = fields[field]\n if (LIST_FIELDS.has(field)) {\n fields[field] = Array.isArray(existing) ? [...existing, ...urls] : urls\n } else if (existing !== undefined) {\n throw new Error(\n `fal: multiple inputs map to '${field}' on model ${model}. Drop one of the conflicting inputs or pass the field explicitly via modelOptions.`,\n )\n } else if (urls.length === 1) {\n fields[field] = urls[0]\n } else {\n throw new Error(\n `fal: model ${model} accepts a single ${what} image via '${field}' (received ${urls.length}).`,\n )\n }\n}\n\ninterface RoleBuckets {\n sources: Array<string>\n masks: Array<string>\n controls: Array<string>\n references: Array<string>\n starts: Array<string>\n ends: Array<string>\n}\n\nfunction bucketByRole(\n imageInputs: ReadonlyArray<ImagePart<MediaInputMetadata>>,\n): RoleBuckets {\n const buckets: RoleBuckets = {\n sources: [],\n masks: [],\n controls: [],\n references: [],\n starts: [],\n ends: [],\n }\n for (const part of imageInputs) {\n const url = imagePartToUrl(part)\n const role = part.metadata?.role\n if (role === 'mask') buckets.masks.push(url)\n else if (role === 'control') buckets.controls.push(url)\n else if (role === 'reference' || role === 'character')\n buckets.references.push(url)\n else if (role === 'start_frame') buckets.starts.push(url)\n else if (role === 'end_frame') buckets.ends.push(url)\n else buckets.sources.push(url)\n }\n return buckets\n}\n\n/**\n * Map the prompt's image parts onto fal.ai image-endpoint fields.\n *\n * fal endpoints use different field names for image-conditioned generation\n * (~80% use `image_url` for single; the rest use `image_urls`,\n * `reference_image_urls`, `mask_url`, `control_image_url`, etc.). Field\n * names are resolved per endpoint from the generated\n * `FAL_IMAGE_FIELD_OVERRIDES` map (derived from the fal SDK's endpoint\n * types), falling back to the defaults above for endpoints the installed\n * SDK doesn't know:\n *\n * - parts with `metadata.role === 'mask'` → spec.mask (single)\n * - parts with `metadata.role === 'control'` → spec.control (single)\n * - `role === 'reference' | 'character'` → spec.reference\n * - `role === 'start_frame' | 'end_frame'` → treated as sources (frame\n * roles only apply to video generation)\n * - remaining parts → spec.single / spec.multi\n *\n * Users can always override the resulting field shape via `modelOptions`\n * (spread before these fields), or pass everything through `modelOptions`\n * directly when the mapping doesn't match an obscure endpoint.\n */\nexport function mapImageInputsToFalFields<TModel extends FalModel>(\n model: TModel,\n imageInputs?: ReadonlyArray<ImagePart<MediaInputMetadata>>,\n): FalImageInputFields<TModel> {\n if (!imageInputs || imageInputs.length === 0) return {}\n\n const spec = fieldSpecFor(model)\n const { sources, masks, controls, references, starts, ends } =\n bucketByRole(imageInputs)\n // Frame roles aren't meaningful for image generation; treat as the\n // primary source. The video mapper handles start/end framing.\n const allSources = [...sources, ...starts, ...ends]\n\n if (masks.length > 1) {\n throw new Error(\n `fal: only one input with metadata.role === 'mask' is supported per request (received ${masks.length}).`,\n )\n }\n if (controls.length > 1) {\n throw new Error(\n `fal: only one input with metadata.role === 'control' is supported per request (received ${controls.length}).`,\n )\n }\n\n const fields: Record<string, unknown> = {}\n const sourceField = allSources.length > 1 ? spec.multi : spec.single\n assignField(fields, sourceField, allSources, model, 'source')\n assignField(fields, spec.reference, references, model, 'reference')\n assignField(fields, spec.mask, masks, model, 'mask')\n assignField(fields, spec.control, controls, model, 'control')\n\n return fields as FalImageInputFields<TModel>\n}\n\n/**\n * Map the prompt's image parts onto fal.ai video-endpoint fields.\n *\n * Video endpoints often expose a start frame as `image_url` (76% of i2v\n * models) plus an optional `end_image_url`. Multi-reference video models\n * (Kling O3, Seedance reference-to-video) use `reference_image_urls` or\n * `image_urls`. Field names resolve through the same generated override\n * map as the image mapper — e.g. `role: 'start_frame'` lands on `image_url`\n * for Kling/Veo image-to-video and `first_frame_url` for Pixverse. Mapping:\n *\n * - `metadata.role === 'start_frame'` → spec.start\n * - `metadata.role === 'end_frame'` → spec.end\n * - `metadata.role === 'reference' | 'character'` → spec.reference\n * - `metadata.role === 'mask' | 'control'` → throws (no video routing)\n * - remaining parts (no role) → spec.single / spec.multi\n */\nexport function mapImageInputsToFalVideoFields<TModel extends FalModel>(\n model: TModel,\n imageInputs?: ReadonlyArray<ImagePart<MediaInputMetadata>>,\n): FalImageInputFields<TModel> {\n if (!imageInputs || imageInputs.length === 0) return {}\n\n const spec = fieldSpecFor(model)\n const { sources, masks, controls, references, starts, ends } =\n bucketByRole(imageInputs)\n // Mask / control roles have no video-specific routing; silently repurposing\n // them as source frames would hide the problem, so reject them instead.\n if (masks.length > 0 || controls.length > 0) {\n const role = masks.length > 0 ? 'mask' : 'control'\n throw new Error(\n `fal: metadata.role === '${role}' is not supported for video generation on model ${model}. ` +\n `Remove the role or pass the field explicitly via modelOptions.`,\n )\n }\n\n if (starts.length > 1) {\n throw new Error(\n `fal: only one input with metadata.role === 'start_frame' is supported (received ${starts.length}).`,\n )\n }\n if (ends.length > 1) {\n throw new Error(\n `fal: only one input with metadata.role === 'end_frame' is supported (received ${ends.length}).`,\n )\n }\n\n const fields: Record<string, unknown> = {}\n const sourceField = sources.length > 1 ? spec.multi : spec.single\n assignField(fields, sourceField, sources, model, 'source')\n assignField(fields, spec.reference, references, model, 'reference')\n // Frame roles assign last: when an endpoint routes the start frame to its\n // generic source field (e.g. Kling image-to-video) and an unroled source\n // was also provided, assignField rejects the ambiguous combination.\n assignField(fields, spec.start, starts, model, 'start frame')\n assignField(fields, spec.end, ends, model, 'end frame')\n\n return fields as FalImageInputFields<TModel>\n}\n\n/**\n * Convert a TanStack ImagePart into a string suitable for fal's URL-based\n * input fields.\n */\nfunction imagePartToUrl(part: ImagePart<MediaInputMetadata>): string {\n return contentSourceToFalUrl(part.source)\n}\n"],"mappings":"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"}
@@ -5,5 +5,6 @@ export type { FalLiveVideoModel, FalLiveVideoProviderOptions, FalLiveVideoModelP
5
5
  export { FalSpeechAdapter, falSpeech } from './adapters/speech.js';
6
6
  export { FalTranscriptionAdapter, falTranscription, } from './adapters/transcription.js';
7
7
  export { FalAudioAdapter, falAudio } from './adapters/audio.js';
8
+ export { FalFilesAdapter, falFiles, type FalFilesConfig, } from './adapters/files.js';
8
9
  export { type FalImageProviderOptions, type FalVideoProviderOptions, type FalSpeechProviderOptions, type FalTranscriptionProviderOptions, type FalAudioProviderOptions, type FalModel, type FalModelInput, type FalModelOutput, type FalModelImageSize, type FalModelVideoSize, type FalModelVideoDuration, } from './model-meta.js';
9
10
  export { getFalApiKeyFromEnv, configureFalClient, generateId, type FalClientConfig, } from './utils/client.js';
package/dist/esm/index.js CHANGED
@@ -5,4 +5,5 @@ import { FAL_LIVE_VIDEO_APP, FAL_LIVE_VIDEO_MODELS, FalLiveVideoAdapter, allowed
5
5
  import { FalSpeechAdapter, falSpeech } from "./adapters/speech.js";
6
6
  import { FalTranscriptionAdapter, falTranscription } from "./adapters/transcription.js";
7
7
  import { FalAudioAdapter, falAudio } from "./adapters/audio.js";
8
- export { FAL_LIVE_VIDEO_APP, FAL_LIVE_VIDEO_MODELS, FalAudioAdapter, FalImageAdapter, FalLiveVideoAdapter, FalSpeechAdapter, FalTranscriptionAdapter, FalVideoAdapter, allowedFalLiveVideoProxyTarget, configureFalClient, createFalLiveVideo, falAudio, falImage, falLiveVideo, falSpeech, falTranscription, falVideo, generateId, getFalApiKeyFromEnv, isFalLiveVideoModel };
8
+ import { FalFilesAdapter, falFiles } from "./adapters/files.js";
9
+ export { FAL_LIVE_VIDEO_APP, FAL_LIVE_VIDEO_MODELS, FalAudioAdapter, FalFilesAdapter, FalImageAdapter, FalLiveVideoAdapter, FalSpeechAdapter, FalTranscriptionAdapter, FalVideoAdapter, allowedFalLiveVideoProxyTarget, configureFalClient, createFalLiveVideo, falAudio, falFiles, falImage, falLiveVideo, falSpeech, falTranscription, falVideo, generateId, getFalApiKeyFromEnv, isFalLiveVideoModel };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-fal",
3
- "version": "0.14.6",
3
+ "version": "0.15.1",
4
4
  "description": "fal.ai adapter for TanStack AI image, video, live, audio, speech, and transcription generation.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -59,10 +59,10 @@
59
59
  "devDependencies": {
60
60
  "@vitest/coverage-v8": "4.1.10",
61
61
  "vite": "^8.2.1",
62
- "@tanstack/ai": "0.59.0"
62
+ "@tanstack/ai": "0.61.0"
63
63
  },
64
64
  "peerDependencies": {
65
- "@tanstack/ai": "^0.59.0"
65
+ "@tanstack/ai": "^0.61.0"
66
66
  },
67
67
  "scripts": {
68
68
  "build": "vite build",
@@ -0,0 +1,59 @@
1
+ import { fal } from '@fal-ai/client'
2
+ import {
3
+ BaseFilesAdapter,
4
+ normalizeFileUploadInput,
5
+ } from '@tanstack/ai/adapters'
6
+ import { configureFalClient } from '../utils/client'
7
+ import type { StorageSettings } from '@fal-ai/client'
8
+ import type { FileHandle, FileUploadInput } from '@tanstack/ai/adapters'
9
+ import type { FalClientConfig } from '../utils/client'
10
+
11
+ export interface FalFilesConfig extends FalClientConfig {
12
+ /**
13
+ * Optional lifecycle for uploaded objects — one of fal's presets
14
+ * (`'1h' | '1d' | '7d' | '30d' | '1y' | 'never' | 'immediate'`) or a number
15
+ * of seconds. Applied as the object's `X-Fal-Object-Lifecycle-Preference`.
16
+ */
17
+ expiresIn?: StorageSettings['expiresIn']
18
+ }
19
+
20
+ /**
21
+ * fal Files adapter — uploads media to fal storage via `fal.storage.upload`.
22
+ * The handle is a storage URL, so `fileSourceFromHandle(handle)` references it
23
+ * as a normal URL (fal endpoints accept it directly). Upload-only: fal storage
24
+ * has no retrieval/deletion API, so `get`/`delete` are unavailable.
25
+ */
26
+ export class FalFilesAdapter extends BaseFilesAdapter<'fal'> {
27
+ readonly name = 'fal' as const
28
+ private readonly expiresIn?: StorageSettings['expiresIn']
29
+
30
+ constructor(config?: FalFilesConfig) {
31
+ super()
32
+ configureFalClient(config)
33
+ this.expiresIn = config?.expiresIn
34
+ }
35
+
36
+ async upload(input: FileUploadInput): Promise<FileHandle<'fal'>> {
37
+ const { blob, mimeType } = normalizeFileUploadInput(input)
38
+ const url = await fal.storage.upload(
39
+ blob,
40
+ this.expiresIn !== undefined
41
+ ? { lifecycle: { expiresIn: this.expiresIn } }
42
+ : undefined,
43
+ )
44
+ return {
45
+ id: url,
46
+ provider: 'fal',
47
+ uri: url,
48
+ ...(mimeType ? { mimeType } : {}),
49
+ }
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Create a fal Files adapter. Reads the API key from `FAL_KEY` unless one is
55
+ * provided in `config`.
56
+ */
57
+ export function falFiles(config?: FalFilesConfig): FalFilesAdapter {
58
+ return new FalFilesAdapter(config)
59
+ }
@@ -53,6 +53,8 @@ export class FalImageAdapter<TModel extends FalModel> extends BaseImageAdapter<
53
53
  > {
54
54
  override readonly kind = 'image' as const
55
55
  readonly name = 'fal' as const
56
+ // Consumes fal storage URLs uploaded via falFiles().
57
+ override readonly supportsFileSources = true
56
58
 
57
59
  constructor(model: TModel, config?: FalClientConfig) {
58
60
  super(model, {})
@@ -10,7 +10,10 @@ import {
10
10
  getFalVideoDurationOptions,
11
11
  mapVideoSizeToFalFormat,
12
12
  } from '../video/video-provider-options'
13
- import { mapImageInputsToFalVideoFields } from '../image/image-inputs'
13
+ import {
14
+ contentSourceToFalUrl,
15
+ mapImageInputsToFalVideoFields,
16
+ } from '../image/image-inputs'
14
17
  import type { DurationOptions } from '@tanstack/ai/adapters'
15
18
  import type {
16
19
  AudioPart,
@@ -75,17 +78,12 @@ function mapAudioInputsToFalFields(
75
78
  )
76
79
  }
77
80
  return {
78
- audio_url:
79
- part.source.type === 'url'
80
- ? part.source.value
81
- : `data:${part.source.mimeType};base64,${part.source.value}`,
81
+ audio_url: contentSourceToFalUrl(part.source),
82
82
  }
83
83
  }
84
84
 
85
85
  function videoPartToUrl(part: VideoPart<MediaInputMetadata>): string {
86
- return part.source.type === 'url'
87
- ? part.source.value
88
- : `data:${part.source.mimeType};base64,${part.source.value}`
86
+ return contentSourceToFalUrl(part.source)
89
87
  }
90
88
 
91
89
  type FalQueueStatus = 'IN_QUEUE' | 'IN_PROGRESS' | 'COMPLETED'
@@ -142,6 +140,8 @@ export class FalVideoAdapter<TModel extends FalModel> extends BaseVideoAdapter<
142
140
  > {
143
141
  override readonly kind = 'video' as const
144
142
  readonly name = 'fal' as const
143
+ // Consumes fal storage URLs uploaded via falFiles().
144
+ override readonly supportsFileSources = true
145
145
 
146
146
  constructor(model: TModel, config?: FalClientConfig) {
147
147
  super({}, model)
@@ -1,11 +1,31 @@
1
+ import { fileReferenceFor, isFileSource } from '@tanstack/ai'
1
2
  import { FAL_IMAGE_FIELD_OVERRIDES } from './generated/image-field-overrides'
2
3
  import type {
3
4
  FalImageFieldName,
4
5
  FalImageFieldOverride,
5
6
  } from './generated/image-field-overrides'
6
- import type { ImagePart, MediaInputMetadata } from '@tanstack/ai'
7
+ import type {
8
+ ContentPartSource,
9
+ ImagePart,
10
+ MediaInputMetadata,
11
+ } from '@tanstack/ai'
7
12
  import type { FalModel, FalModelInput } from '../model-meta'
8
13
 
14
+ /**
15
+ * Convert a content source into a URL string for fal's URL-based input fields.
16
+ * URL sources pass through; fal storage handles (a storage URL) pass through
17
+ * after a provider check; base64 data becomes a `data:<mime>;base64,<value>`
18
+ * URI which fal endpoints accept on the wire.
19
+ */
20
+ export function contentSourceToFalUrl(source: ContentPartSource): string {
21
+ if (isFileSource(source)) {
22
+ // A fal handle is a storage URL; throws when another provider issued it.
23
+ return fileReferenceFor(source, 'fal')
24
+ }
25
+ if (source.type === 'url') return source.value
26
+ return `data:${source.mimeType};base64,${source.value}`
27
+ }
28
+
9
29
  /**
10
30
  * The image-conditioning fields the mappers may set, narrowed to the ones
11
31
  * that actually exist on the given endpoint's input type. For endpoints
@@ -237,10 +257,8 @@ export function mapImageInputsToFalVideoFields<TModel extends FalModel>(
237
257
 
238
258
  /**
239
259
  * Convert a TanStack ImagePart into a string suitable for fal's URL-based
240
- * input fields. URL sources pass through; data sources are emitted as a
241
- * `data:<mime>;base64,<value>` URI which fal endpoints accept on the wire.
260
+ * input fields.
242
261
  */
243
262
  function imagePartToUrl(part: ImagePart<MediaInputMetadata>): string {
244
- if (part.source.type === 'url') return part.source.value
245
- return `data:${part.source.mimeType};base64,${part.source.value}`
263
+ return contentSourceToFalUrl(part.source)
246
264
  }
package/src/index.ts CHANGED
@@ -50,6 +50,16 @@ export {
50
50
 
51
51
  export { FalAudioAdapter, falAudio } from './adapters/audio'
52
52
 
53
+ // ============================================================================
54
+ // Files Adapter (storage upload)
55
+ // ============================================================================
56
+
57
+ export {
58
+ FalFilesAdapter,
59
+ falFiles,
60
+ type FalFilesConfig,
61
+ } from './adapters/files'
62
+
53
63
  // ============================================================================
54
64
  // Model Types (from fal.ai's type system)
55
65
  // ============================================================================