@tanstack/ai-fal 0.11.0 → 0.12.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.
@@ -1,6 +1,6 @@
1
- import { BaseVideoAdapter } from '@tanstack/ai/adapters';
1
+ import { BaseVideoAdapter, DurationOptions } from '@tanstack/ai/adapters';
2
2
  import { VideoGenerationOptions, VideoJobResult, VideoStatusResult, VideoUrlResult } from '@tanstack/ai';
3
- import { FalModel, FalModelVideoSize, FalVideoPromptModalitiesFor, FalVideoProviderOptions } from '../model-meta.js';
3
+ import { FalModel, FalModelVideoDuration, FalModelVideoSize, FalVideoPromptModalitiesFor, FalVideoProviderOptions } from '../model-meta.js';
4
4
  import { FalClientConfig } from '../utils/client.js';
5
5
  /**
6
6
  * fal.ai video generation adapter.
@@ -11,11 +11,13 @@ import { FalClientConfig } from '../utils/client.js';
11
11
  *
12
12
  * @experimental Video generation is an experimental feature and may change.
13
13
  */
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>>> {
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
17
  constructor(model: TModel, config?: FalClientConfig);
18
- createVideoJob(options: VideoGenerationOptions<FalVideoProviderOptions<TModel>, FalModelVideoSize<TModel>>): Promise<VideoJobResult>;
18
+ createVideoJob(options: VideoGenerationOptions<FalVideoProviderOptions<TModel>, FalModelVideoSize<TModel>, FalModelVideoDuration<TModel>>): Promise<VideoJobResult>;
19
+ availableDurations(): DurationOptions<FalModelVideoDuration<TModel>>;
20
+ snapDuration(seconds: number): FalModelVideoDuration<TModel> | undefined;
19
21
  getVideoStatus(jobId: string): Promise<VideoStatusResult>;
20
22
  getVideoUrl(jobId: string): Promise<VideoUrlResult>;
21
23
  protected generateId(): string;
@@ -1,10 +1,10 @@
1
1
  import { buildFalUsage, takeBillableUnits } from "../utils/billing.js";
2
2
  import { configureFalClient, generateId } from "../utils/client.js";
3
3
  import { mapImageInputsToFalVideoFields } from "../image/image-inputs.js";
4
- import { mapVideoSizeToFalFormat } from "../video/video-provider-options.js";
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";
7
- import { BaseVideoAdapter } from "@tanstack/ai/adapters";
7
+ import { BaseVideoAdapter, snapToDurationOption } from "@tanstack/ai/adapters";
8
8
  //#region src/adapters/video.ts
9
9
  /**
10
10
  * Map video conditioning inputs onto fal field names.
@@ -86,7 +86,7 @@ var FalVideoAdapter = class extends BaseVideoAdapter {
86
86
  ...audioFields,
87
87
  ...modelOptions,
88
88
  ...resolved.text ? { prompt: resolved.text } : {},
89
- ...duration ? { duration } : {}
89
+ ...duration !== void 0 ? { duration } : {}
90
90
  };
91
91
  const { request_id } = await fal.queue.submit(this.model, {
92
92
  input,
@@ -104,6 +104,12 @@ var FalVideoAdapter = class extends BaseVideoAdapter {
104
104
  throw error;
105
105
  }
106
106
  }
107
+ availableDurations() {
108
+ return getFalVideoDurationOptions(this.model);
109
+ }
110
+ snapDuration(seconds) {
111
+ return snapToDurationOption(seconds, this.availableDurations());
112
+ }
107
113
  async getVideoStatus(jobId) {
108
114
  const statusResponse = await fal.queue.status(this.model, {
109
115
  requestId: jobId,
@@ -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 } from '@tanstack/ai/adapters'\nimport {\n configureFalClient,\n generateId as utilGenerateId,\n} from '../utils/client'\nimport { buildFalUsage, takeBillableUnits } from '../utils/billing'\nimport { mapVideoSizeToFalFormat } from '../video/video-provider-options'\nimport { mapImageInputsToFalVideoFields } from '../image/image-inputs'\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 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> {\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 >,\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 ? { 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 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 { 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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"}
@@ -3,5 +3,5 @@ export { FalVideoAdapter, falVideo } from './adapters/video.js';
3
3
  export { FalSpeechAdapter, falSpeech } from './adapters/speech.js';
4
4
  export { FalTranscriptionAdapter, falTranscription, } from './adapters/transcription.js';
5
5
  export { FalAudioAdapter, falAudio } from './adapters/audio.js';
6
- export { type FalImageProviderOptions, type FalVideoProviderOptions, type FalSpeechProviderOptions, type FalTranscriptionProviderOptions, type FalAudioProviderOptions, type FalModel, type FalModelInput, type FalModelOutput, type FalModelImageSize, type FalModelVideoSize, } from './model-meta.js';
6
+ 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';
7
7
  export { getFalApiKeyFromEnv, configureFalClient, generateId, type FalClientConfig, } from './utils/client.js';
@@ -88,6 +88,21 @@ export type FalModelVideoSizeInput<TModel extends string> = TModel extends keyof
88
88
  aspect_ratio?: string;
89
89
  resolution?: string;
90
90
  };
91
+ /**
92
+ * Extract the `duration` field type from a fal video model's input.
93
+ * Falls back to `string | number | undefined` for models not in the SDK's
94
+ * `EndpointTypeMap`.
95
+ *
96
+ * Shapes seen in the wild:
97
+ * - `'5' | '10'` (Kling, Pika): discrete numeric strings
98
+ * - `'5s' | '9s'` (Luma): keyword strings with unit
99
+ * - `'4s' | '6s' | '8s'` (Veo3 via FAL): keyword strings
100
+ * - `'2' | … | '15'` (WAN-25): discrete-range numeric strings
101
+ * - undefined (Minimax, Hunyuan): no duration field; passing one is a type error
102
+ */
103
+ export type FalModelVideoDuration<TModel extends string> = TModel extends keyof EndpointTypeMap ? 'duration' extends keyof EndpointTypeMap[TModel]['input'] ? Extract<NonNullable<EndpointTypeMap[TModel]['input'] extends {
104
+ duration?: infer D;
105
+ } ? D : never>, string | number> : undefined : string | number | undefined;
91
106
  /**
92
107
  * Prompt input modalities for a fal image endpoint, derived from the SDK's
93
108
  * endpoint input type: an endpoint accepts image prompt parts exactly when
@@ -111,7 +126,7 @@ export type FalVideoPromptModalitiesFor<TModel extends string> = TModel extends
111
126
  * video/audio) are optional here even when the endpoint requires them —
112
127
  * they're usually supplied as prompt parts instead.
113
128
  */
114
- export type FalVideoProviderOptions<TModel extends string> = TModel extends keyof EndpointTypeMap ? WithOptionalMediaInputFields<Omit<FalModelInput<TModel>, 'prompt'>> : Record<string, unknown>;
129
+ export type FalVideoProviderOptions<TModel extends string> = TModel extends keyof EndpointTypeMap ? WithOptionalMediaInputFields<Omit<FalModelInput<TModel>, 'prompt' | 'duration'>> : Record<string, unknown>;
115
130
  /**
116
131
  * Provider options for TTS, excluding fields TanStack AI handles.
117
132
  * Use this for the `modelOptions` parameter in speech generation.
@@ -1,2 +1,4 @@
1
- import { FalModelVideoSize, FalModelVideoSizeInput } from '../model-meta.js';
2
- export declare function mapVideoSizeToFalFormat<TModel extends string>(size: FalModelVideoSize<TModel> | undefined): FalModelVideoSizeInput<TModel> | undefined;
1
+ import { DurationOptions } from '@tanstack/ai/adapters';
2
+ import { FalModelVideoDuration, FalModelVideoSize, FalModelVideoSizeInput } from '../model-meta.js';
3
+ export declare function mapVideoSizeToFalFormat<TModel extends string>(size?: FalModelVideoSize<TModel>): FalModelVideoSizeInput<TModel>;
4
+ export declare function getFalVideoDurationOptions<TModel extends string>(model: TModel): DurationOptions<FalModelVideoDuration<TModel>>;
@@ -1,17 +1,119 @@
1
1
  //#region src/video/video-provider-options.ts
2
2
  function mapVideoSizeToFalFormat(size) {
3
- if (!size) return void 0;
3
+ if (!size) return {};
4
4
  if (size.includes("_")) {
5
- const [aspect_ratio, resolution] = size.split("_");
5
+ const [aspectRatio, resolution] = size.split("_");
6
6
  return {
7
- aspect_ratio,
7
+ aspect_ratio: aspectRatio,
8
8
  resolution
9
9
  };
10
10
  }
11
11
  if (size.includes(":")) return { aspect_ratio: size };
12
12
  return { resolution: size };
13
13
  }
14
+ /**
15
+ * Identity helper that checks each entry against the SDK's `duration` union
16
+ * for its own key, so a typo'd endpoint id or a stale union fails to compile.
17
+ */
18
+ function durationMap(map) {
19
+ return map;
20
+ }
21
+ var KLING_5_10 = {
22
+ kind: "discrete",
23
+ values: ["5", "10"]
24
+ };
25
+ var VEO_4_6_8 = {
26
+ kind: "discrete",
27
+ values: [
28
+ "4s",
29
+ "6s",
30
+ "8s"
31
+ ]
32
+ };
33
+ var LTX_6_8_10 = {
34
+ kind: "discrete",
35
+ values: [
36
+ "6",
37
+ "8",
38
+ "10"
39
+ ]
40
+ };
41
+ var KLING_3_15 = {
42
+ kind: "discrete",
43
+ values: [
44
+ "3",
45
+ "4",
46
+ "5",
47
+ "6",
48
+ "7",
49
+ "8",
50
+ "9",
51
+ "10",
52
+ "11",
53
+ "12",
54
+ "13",
55
+ "14",
56
+ "15"
57
+ ]
58
+ };
59
+ /**
60
+ * Curated map of per-model duration options for popular fal.ai video models.
61
+ * Values are checked at compile time against `@fal-ai/client`'s
62
+ * `EndpointTypeMap` input types via `durationMap`.
63
+ *
64
+ * Models not listed here fall back to `{ kind: 'none' }` — honest "we don't
65
+ * know" rather than guessing. The type-level `FalModelVideoDuration<TModel>`
66
+ * still derives from the SDK types, so autocomplete works for SDK-known
67
+ * models that aren't curated here.
68
+ */
69
+ var FAL_VIDEO_DURATIONS = durationMap({
70
+ "fal-ai/kling-video/v1.6/standard/text-to-video": KLING_5_10,
71
+ "fal-ai/kling-video/v1.6/pro/text-to-video": KLING_5_10,
72
+ "fal-ai/kling-video/v2.6/pro/text-to-video": KLING_5_10,
73
+ "fal-ai/kling-video/v2.6/pro/image-to-video": KLING_5_10,
74
+ "fal-ai/kling-video/v3/pro/text-to-video": KLING_3_15,
75
+ "fal-ai/kling-video/v3/pro/image-to-video": KLING_3_15,
76
+ "fal-ai/pika/v2.2/text-to-video": KLING_5_10,
77
+ "fal-ai/luma-dream-machine/ray-2": {
78
+ kind: "discrete",
79
+ values: ["5s", "9s"]
80
+ },
81
+ "fal-ai/veo3": VEO_4_6_8,
82
+ "fal-ai/veo3/image-to-video": VEO_4_6_8,
83
+ "fal-ai/veo3.1": VEO_4_6_8,
84
+ "fal-ai/veo3.1/image-to-video": VEO_4_6_8,
85
+ "fal-ai/veo3.1/fast": VEO_4_6_8,
86
+ "fal-ai/veo3.1/fast/image-to-video": VEO_4_6_8,
87
+ "fal-ai/ltx-2.3/text-to-video": LTX_6_8_10,
88
+ "fal-ai/ltx-2.3/text-to-video/fast": LTX_6_8_10,
89
+ "fal-ai/ltx-2.3/image-to-video": LTX_6_8_10,
90
+ "fal-ai/ltx-2.3/image-to-video/fast": LTX_6_8_10,
91
+ "fal-ai/wan-25-preview/text-to-video": {
92
+ kind: "discrete",
93
+ values: [
94
+ "2",
95
+ "3",
96
+ "4",
97
+ "5",
98
+ "6",
99
+ "7",
100
+ "8",
101
+ "9",
102
+ "10",
103
+ "11",
104
+ "12",
105
+ "13",
106
+ "14",
107
+ "15"
108
+ ]
109
+ },
110
+ "fal-ai/minimax/video-01": { kind: "none" },
111
+ "fal-ai/hunyuan-video-v1.5/text-to-video": { kind: "none" }
112
+ });
113
+ function getFalVideoDurationOptions(model) {
114
+ return FAL_VIDEO_DURATIONS[model] ?? { kind: "none" };
115
+ }
14
116
  //#endregion
15
- export { mapVideoSizeToFalFormat };
117
+ export { getFalVideoDurationOptions, mapVideoSizeToFalFormat };
16
118
 
17
119
  //# sourceMappingURL=video-provider-options.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"video-provider-options.js","names":[],"sources":["../../../src/video/video-provider-options.ts"],"sourcesContent":["import type { FalModelVideoSize, FalModelVideoSizeInput } from '../model-meta'\n\nexport function mapVideoSizeToFalFormat<TModel extends string>(\n size: FalModelVideoSize<TModel> | undefined,\n): FalModelVideoSizeInput<TModel> | undefined {\n if (!size) return undefined\n\n // \"16:9_720p\" → { aspect_ratio, resolution }\n // \"16:9\" → { aspect_ratio }\n // \"720p\" → { resolution }\n if (size.includes('_')) {\n const [aspect_ratio, resolution] = size.split('_')\n return {\n aspect_ratio,\n resolution,\n } as FalModelVideoSizeInput<TModel>\n }\n\n if (size.includes(':')) {\n return { aspect_ratio: size } as FalModelVideoSizeInput<TModel>\n }\n\n return { resolution: size } as FalModelVideoSizeInput<TModel>\n}\n"],"mappings":";AAEA,SAAgB,wBACd,MAC4C;CAC5C,IAAI,CAAC,MAAM,OAAO,KAAA;CAKlB,IAAI,KAAK,SAAS,GAAG,GAAG;EACtB,MAAM,CAAC,cAAc,cAAc,KAAK,MAAM,GAAG;EACjD,OAAO;GACL;GACA;EACF;CACF;CAEA,IAAI,KAAK,SAAS,GAAG,GACnB,OAAO,EAAE,cAAc,KAAK;CAG9B,OAAO,EAAE,YAAY,KAAK;AAC5B"}
1
+ {"version":3,"file":"video-provider-options.js","names":[],"sources":["../../../src/video/video-provider-options.ts"],"sourcesContent":["import type { DurationOptions } from '@tanstack/ai/adapters'\nimport type {\n FalModelVideoDuration,\n FalModelVideoSize,\n FalModelVideoSizeInput,\n} from '../model-meta'\n\nexport function mapVideoSizeToFalFormat<TModel extends string>(\n size?: FalModelVideoSize<TModel>,\n): FalModelVideoSizeInput<TModel> {\n if (!size) {\n return {} as FalModelVideoSizeInput<TModel>\n }\n\n if (size.includes('_')) {\n const [aspectRatio, resolution] = size.split('_')\n return {\n aspect_ratio: aspectRatio,\n resolution,\n } as FalModelVideoSizeInput<TModel>\n }\n\n if (size.includes(':')) {\n return { aspect_ratio: size } as FalModelVideoSizeInput<TModel>\n }\n\n return { resolution: size } as FalModelVideoSizeInput<TModel>\n}\n\n/**\n * Identity helper that checks each entry against the SDK's `duration` union\n * for its own key, so a typo'd endpoint id or a stale union fails to compile.\n */\nfunction durationMap<\n const T extends {\n [K in keyof T & string]: DurationOptions<FalModelVideoDuration<K>>\n },\n>(map: T): T {\n return map\n}\n\nconst KLING_5_10 = { kind: 'discrete', values: ['5', '10'] } as const\nconst VEO_4_6_8 = { kind: 'discrete', values: ['4s', '6s', '8s'] } as const\nconst LTX_6_8_10 = { kind: 'discrete', values: ['6', '8', '10'] } as const\nconst KLING_3_15 = {\n kind: 'discrete',\n values: [\n '3',\n '4',\n '5',\n '6',\n '7',\n '8',\n '9',\n '10',\n '11',\n '12',\n '13',\n '14',\n '15',\n ],\n} as const\n\n/**\n * Curated map of per-model duration options for popular fal.ai video models.\n * Values are checked at compile time against `@fal-ai/client`'s\n * `EndpointTypeMap` input types via `durationMap`.\n *\n * Models not listed here fall back to `{ kind: 'none' }` — honest \"we don't\n * know\" rather than guessing. The type-level `FalModelVideoDuration<TModel>`\n * still derives from the SDK types, so autocomplete works for SDK-known\n * models that aren't curated here.\n */\nconst FAL_VIDEO_DURATIONS = durationMap({\n 'fal-ai/kling-video/v1.6/standard/text-to-video': KLING_5_10,\n 'fal-ai/kling-video/v1.6/pro/text-to-video': KLING_5_10,\n 'fal-ai/kling-video/v2.6/pro/text-to-video': KLING_5_10,\n 'fal-ai/kling-video/v2.6/pro/image-to-video': KLING_5_10,\n 'fal-ai/kling-video/v3/pro/text-to-video': KLING_3_15,\n 'fal-ai/kling-video/v3/pro/image-to-video': KLING_3_15,\n 'fal-ai/pika/v2.2/text-to-video': KLING_5_10,\n 'fal-ai/luma-dream-machine/ray-2': {\n kind: 'discrete',\n values: ['5s', '9s'],\n },\n 'fal-ai/veo3': VEO_4_6_8,\n 'fal-ai/veo3/image-to-video': VEO_4_6_8,\n 'fal-ai/veo3.1': VEO_4_6_8,\n 'fal-ai/veo3.1/image-to-video': VEO_4_6_8,\n 'fal-ai/veo3.1/fast': VEO_4_6_8,\n 'fal-ai/veo3.1/fast/image-to-video': VEO_4_6_8,\n 'fal-ai/ltx-2.3/text-to-video': LTX_6_8_10,\n 'fal-ai/ltx-2.3/text-to-video/fast': LTX_6_8_10,\n 'fal-ai/ltx-2.3/image-to-video': LTX_6_8_10,\n 'fal-ai/ltx-2.3/image-to-video/fast': LTX_6_8_10,\n 'fal-ai/wan-25-preview/text-to-video': {\n kind: 'discrete',\n values: [\n '2',\n '3',\n '4',\n '5',\n '6',\n '7',\n '8',\n '9',\n '10',\n '11',\n '12',\n '13',\n '14',\n '15',\n ],\n },\n 'fal-ai/minimax/video-01': { kind: 'none' },\n 'fal-ai/hunyuan-video-v1.5/text-to-video': { kind: 'none' },\n})\n\nexport function getFalVideoDurationOptions<TModel extends string>(\n model: TModel,\n): DurationOptions<FalModelVideoDuration<TModel>> {\n const entry = (FAL_VIDEO_DURATIONS as Record<string, unknown>)[model]\n // The map is keyed by literal ids but the adapter's TModel is an open string;\n // the lookup result can't be correlated by TS, hence the one cast here.\n return (entry ?? { kind: 'none' }) as DurationOptions<\n FalModelVideoDuration<TModel>\n >\n}\n"],"mappings":";AAOA,SAAgB,wBACd,MACgC;CAChC,IAAI,CAAC,MACH,OAAO,CAAC;CAGV,IAAI,KAAK,SAAS,GAAG,GAAG;EACtB,MAAM,CAAC,aAAa,cAAc,KAAK,MAAM,GAAG;EAChD,OAAO;GACL,cAAc;GACd;EACF;CACF;CAEA,IAAI,KAAK,SAAS,GAAG,GACnB,OAAO,EAAE,cAAc,KAAK;CAG9B,OAAO,EAAE,YAAY,KAAK;AAC5B;;;;;AAMA,SAAS,YAIP,KAAW;CACX,OAAO;AACT;AAEA,IAAM,aAAa;CAAE,MAAM;CAAY,QAAQ,CAAC,KAAK,IAAI;AAAE;AAC3D,IAAM,YAAY;CAAE,MAAM;CAAY,QAAQ;EAAC;EAAM;EAAM;CAAI;AAAE;AACjE,IAAM,aAAa;CAAE,MAAM;CAAY,QAAQ;EAAC;EAAK;EAAK;CAAI;AAAE;AAChE,IAAM,aAAa;CACjB,MAAM;CACN,QAAQ;EACN;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF;;;;;;;;;;;AAYA,IAAM,sBAAsB,YAAY;CACtC,kDAAkD;CAClD,6CAA6C;CAC7C,6CAA6C;CAC7C,8CAA8C;CAC9C,2CAA2C;CAC3C,4CAA4C;CAC5C,kCAAkC;CAClC,mCAAmC;EACjC,MAAM;EACN,QAAQ,CAAC,MAAM,IAAI;CACrB;CACA,eAAe;CACf,8BAA8B;CAC9B,iBAAiB;CACjB,gCAAgC;CAChC,sBAAsB;CACtB,qCAAqC;CACrC,gCAAgC;CAChC,qCAAqC;CACrC,iCAAiC;CACjC,sCAAsC;CACtC,uCAAuC;EACrC,MAAM;EACN,QAAQ;GACN;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF;CACF;CACA,2BAA2B,EAAE,MAAM,OAAO;CAC1C,2CAA2C,EAAE,MAAM,OAAO;AAC5D,CAAC;AAED,SAAgB,2BACd,OACgD;CAIhD,OAHe,oBAAgD,UAG9C,EAAE,MAAM,OAAO;AAGlC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-fal",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "description": "fal.ai adapter for TanStack AI image, video, audio, speech, and transcription generation.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -54,10 +54,10 @@
54
54
  "devDependencies": {
55
55
  "@vitest/coverage-v8": "4.1.10",
56
56
  "vite": "^8.2.1",
57
- "@tanstack/ai": "0.46.0"
57
+ "@tanstack/ai": "0.48.0"
58
58
  },
59
59
  "peerDependencies": {
60
- "@tanstack/ai": "^0.46.0"
60
+ "@tanstack/ai": "^0.48.0"
61
61
  },
62
62
  "scripts": {
63
63
  "build": "vite build",
@@ -1,13 +1,17 @@
1
1
  import { fal } from '@fal-ai/client'
2
2
  import { resolveMediaPrompt } from '@tanstack/ai'
3
- import { BaseVideoAdapter } from '@tanstack/ai/adapters'
3
+ import { BaseVideoAdapter, snapToDurationOption } from '@tanstack/ai/adapters'
4
4
  import {
5
5
  configureFalClient,
6
6
  generateId as utilGenerateId,
7
7
  } from '../utils/client'
8
8
  import { buildFalUsage, takeBillableUnits } from '../utils/billing'
9
- import { mapVideoSizeToFalFormat } from '../video/video-provider-options'
9
+ import {
10
+ getFalVideoDurationOptions,
11
+ mapVideoSizeToFalFormat,
12
+ } from '../video/video-provider-options'
10
13
  import { mapImageInputsToFalVideoFields } from '../image/image-inputs'
14
+ import type { DurationOptions } from '@tanstack/ai/adapters'
11
15
  import type {
12
16
  AudioPart,
13
17
  MediaInputMetadata,
@@ -20,6 +24,7 @@ import type {
20
24
  import type {
21
25
  FalModel,
22
26
  FalModelInput,
27
+ FalModelVideoDuration,
23
28
  FalModelVideoSize,
24
29
  FalVideoPromptModalitiesFor,
25
30
  FalVideoProviderOptions,
@@ -132,7 +137,8 @@ export class FalVideoAdapter<TModel extends FalModel> extends BaseVideoAdapter<
132
137
  FalVideoProviderOptions<TModel>,
133
138
  Record<TModel, FalVideoProviderOptions<TModel>>,
134
139
  Record<TModel, FalModelVideoSize<TModel>>,
135
- Record<TModel, FalVideoPromptModalitiesFor<TModel>>
140
+ Record<TModel, FalVideoPromptModalitiesFor<TModel>>,
141
+ Record<TModel, FalModelVideoDuration<TModel>>
136
142
  > {
137
143
  override readonly kind = 'video' as const
138
144
  readonly name = 'fal' as const
@@ -145,7 +151,8 @@ export class FalVideoAdapter<TModel extends FalModel> extends BaseVideoAdapter<
145
151
  async createVideoJob(
146
152
  options: VideoGenerationOptions<
147
153
  FalVideoProviderOptions<TModel>,
148
- FalModelVideoSize<TModel>
154
+ FalModelVideoSize<TModel>,
155
+ FalModelVideoDuration<TModel>
149
156
  >,
150
157
  ): Promise<VideoJobResult> {
151
158
  const { size, duration, modelOptions, logger } = options
@@ -176,7 +183,7 @@ export class FalVideoAdapter<TModel extends FalModel> extends BaseVideoAdapter<
176
183
  // Media-only prompts omit the prompt field rather than sending an
177
184
  // empty string (e.g. pure image-to-video endpoints).
178
185
  ...(resolved.text ? { prompt: resolved.text } : {}),
179
- ...(duration ? { duration } : {}),
186
+ ...(duration !== undefined ? { duration } : {}),
180
187
  } as FalModelInput<TModel>
181
188
 
182
189
  // Submit to queue and get request ID. Request-specific abortSignal only —
@@ -199,6 +206,18 @@ export class FalVideoAdapter<TModel extends FalModel> extends BaseVideoAdapter<
199
206
  }
200
207
  }
201
208
 
209
+ override availableDurations(): DurationOptions<
210
+ FalModelVideoDuration<TModel>
211
+ > {
212
+ return getFalVideoDurationOptions(this.model)
213
+ }
214
+
215
+ override snapDuration(
216
+ seconds: number,
217
+ ): FalModelVideoDuration<TModel> | undefined {
218
+ return snapToDurationOption(seconds, this.availableDurations())
219
+ }
220
+
202
221
  async getVideoStatus(jobId: string): Promise<VideoStatusResult> {
203
222
  const statusResponse = (await fal.queue.status(this.model, {
204
223
  requestId: jobId,
package/src/index.ts CHANGED
@@ -46,6 +46,7 @@ export {
46
46
  type FalModelOutput,
47
47
  type FalModelImageSize,
48
48
  type FalModelVideoSize,
49
+ type FalModelVideoDuration,
49
50
  } from './model-meta'
50
51
  // ============================================================================
51
52
  // Utils
package/src/model-meta.ts CHANGED
@@ -144,6 +144,32 @@ export type FalModelVideoSizeInput<TModel extends string> =
144
144
  : never
145
145
  : { aspect_ratio?: string; resolution?: string }
146
146
 
147
+ /**
148
+ * Extract the `duration` field type from a fal video model's input.
149
+ * Falls back to `string | number | undefined` for models not in the SDK's
150
+ * `EndpointTypeMap`.
151
+ *
152
+ * Shapes seen in the wild:
153
+ * - `'5' | '10'` (Kling, Pika): discrete numeric strings
154
+ * - `'5s' | '9s'` (Luma): keyword strings with unit
155
+ * - `'4s' | '6s' | '8s'` (Veo3 via FAL): keyword strings
156
+ * - `'2' | … | '15'` (WAN-25): discrete-range numeric strings
157
+ * - undefined (Minimax, Hunyuan): no duration field; passing one is a type error
158
+ */
159
+ export type FalModelVideoDuration<TModel extends string> =
160
+ TModel extends keyof EndpointTypeMap
161
+ ? 'duration' extends keyof EndpointTypeMap[TModel]['input']
162
+ ? Extract<
163
+ NonNullable<
164
+ EndpointTypeMap[TModel]['input'] extends { duration?: infer D }
165
+ ? D
166
+ : never
167
+ >,
168
+ string | number
169
+ >
170
+ : undefined
171
+ : string | number | undefined
172
+
147
173
  /**
148
174
  * Prompt input modalities for a fal image endpoint, derived from the SDK's
149
175
  * endpoint input type: an endpoint accepts image prompt parts exactly when
@@ -194,7 +220,9 @@ export type FalVideoPromptModalitiesFor<TModel extends string> =
194
220
  */
195
221
  export type FalVideoProviderOptions<TModel extends string> =
196
222
  TModel extends keyof EndpointTypeMap
197
- ? WithOptionalMediaInputFields<Omit<FalModelInput<TModel>, 'prompt'>>
223
+ ? WithOptionalMediaInputFields<
224
+ Omit<FalModelInput<TModel>, 'prompt' | 'duration'>
225
+ >
198
226
  : Record<string, unknown>
199
227
 
200
228
  /**
@@ -1,17 +1,21 @@
1
- import type { FalModelVideoSize, FalModelVideoSizeInput } from '../model-meta'
1
+ import type { DurationOptions } from '@tanstack/ai/adapters'
2
+ import type {
3
+ FalModelVideoDuration,
4
+ FalModelVideoSize,
5
+ FalModelVideoSizeInput,
6
+ } from '../model-meta'
2
7
 
3
8
  export function mapVideoSizeToFalFormat<TModel extends string>(
4
- size: FalModelVideoSize<TModel> | undefined,
5
- ): FalModelVideoSizeInput<TModel> | undefined {
6
- if (!size) return undefined
9
+ size?: FalModelVideoSize<TModel>,
10
+ ): FalModelVideoSizeInput<TModel> {
11
+ if (!size) {
12
+ return {} as FalModelVideoSizeInput<TModel>
13
+ }
7
14
 
8
- // "16:9_720p" → { aspect_ratio, resolution }
9
- // "16:9" → { aspect_ratio }
10
- // "720p" → { resolution }
11
15
  if (size.includes('_')) {
12
- const [aspect_ratio, resolution] = size.split('_')
16
+ const [aspectRatio, resolution] = size.split('_')
13
17
  return {
14
- aspect_ratio,
18
+ aspect_ratio: aspectRatio,
15
19
  resolution,
16
20
  } as FalModelVideoSizeInput<TModel>
17
21
  }
@@ -22,3 +26,103 @@ export function mapVideoSizeToFalFormat<TModel extends string>(
22
26
 
23
27
  return { resolution: size } as FalModelVideoSizeInput<TModel>
24
28
  }
29
+
30
+ /**
31
+ * Identity helper that checks each entry against the SDK's `duration` union
32
+ * for its own key, so a typo'd endpoint id or a stale union fails to compile.
33
+ */
34
+ function durationMap<
35
+ const T extends {
36
+ [K in keyof T & string]: DurationOptions<FalModelVideoDuration<K>>
37
+ },
38
+ >(map: T): T {
39
+ return map
40
+ }
41
+
42
+ const KLING_5_10 = { kind: 'discrete', values: ['5', '10'] } as const
43
+ const VEO_4_6_8 = { kind: 'discrete', values: ['4s', '6s', '8s'] } as const
44
+ const LTX_6_8_10 = { kind: 'discrete', values: ['6', '8', '10'] } as const
45
+ const KLING_3_15 = {
46
+ kind: 'discrete',
47
+ values: [
48
+ '3',
49
+ '4',
50
+ '5',
51
+ '6',
52
+ '7',
53
+ '8',
54
+ '9',
55
+ '10',
56
+ '11',
57
+ '12',
58
+ '13',
59
+ '14',
60
+ '15',
61
+ ],
62
+ } as const
63
+
64
+ /**
65
+ * Curated map of per-model duration options for popular fal.ai video models.
66
+ * Values are checked at compile time against `@fal-ai/client`'s
67
+ * `EndpointTypeMap` input types via `durationMap`.
68
+ *
69
+ * Models not listed here fall back to `{ kind: 'none' }` — honest "we don't
70
+ * know" rather than guessing. The type-level `FalModelVideoDuration<TModel>`
71
+ * still derives from the SDK types, so autocomplete works for SDK-known
72
+ * models that aren't curated here.
73
+ */
74
+ const FAL_VIDEO_DURATIONS = durationMap({
75
+ 'fal-ai/kling-video/v1.6/standard/text-to-video': KLING_5_10,
76
+ 'fal-ai/kling-video/v1.6/pro/text-to-video': KLING_5_10,
77
+ 'fal-ai/kling-video/v2.6/pro/text-to-video': KLING_5_10,
78
+ 'fal-ai/kling-video/v2.6/pro/image-to-video': KLING_5_10,
79
+ 'fal-ai/kling-video/v3/pro/text-to-video': KLING_3_15,
80
+ 'fal-ai/kling-video/v3/pro/image-to-video': KLING_3_15,
81
+ 'fal-ai/pika/v2.2/text-to-video': KLING_5_10,
82
+ 'fal-ai/luma-dream-machine/ray-2': {
83
+ kind: 'discrete',
84
+ values: ['5s', '9s'],
85
+ },
86
+ 'fal-ai/veo3': VEO_4_6_8,
87
+ 'fal-ai/veo3/image-to-video': VEO_4_6_8,
88
+ 'fal-ai/veo3.1': VEO_4_6_8,
89
+ 'fal-ai/veo3.1/image-to-video': VEO_4_6_8,
90
+ 'fal-ai/veo3.1/fast': VEO_4_6_8,
91
+ 'fal-ai/veo3.1/fast/image-to-video': VEO_4_6_8,
92
+ 'fal-ai/ltx-2.3/text-to-video': LTX_6_8_10,
93
+ 'fal-ai/ltx-2.3/text-to-video/fast': LTX_6_8_10,
94
+ 'fal-ai/ltx-2.3/image-to-video': LTX_6_8_10,
95
+ 'fal-ai/ltx-2.3/image-to-video/fast': LTX_6_8_10,
96
+ 'fal-ai/wan-25-preview/text-to-video': {
97
+ kind: 'discrete',
98
+ values: [
99
+ '2',
100
+ '3',
101
+ '4',
102
+ '5',
103
+ '6',
104
+ '7',
105
+ '8',
106
+ '9',
107
+ '10',
108
+ '11',
109
+ '12',
110
+ '13',
111
+ '14',
112
+ '15',
113
+ ],
114
+ },
115
+ 'fal-ai/minimax/video-01': { kind: 'none' },
116
+ 'fal-ai/hunyuan-video-v1.5/text-to-video': { kind: 'none' },
117
+ })
118
+
119
+ export function getFalVideoDurationOptions<TModel extends string>(
120
+ model: TModel,
121
+ ): DurationOptions<FalModelVideoDuration<TModel>> {
122
+ const entry = (FAL_VIDEO_DURATIONS as Record<string, unknown>)[model]
123
+ // The map is keyed by literal ids but the adapter's TModel is an open string;
124
+ // the lookup result can't be correlated by TS, hence the one cast here.
125
+ return (entry ?? { kind: 'none' }) as DurationOptions<
126
+ FalModelVideoDuration<TModel>
127
+ >
128
+ }