-
-
Notifications
You must be signed in to change notification settings - Fork 115
feat: Introduce fal.ai adapter for image and video generation #237
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Add @tanstack/ai-fal package with: - Image adapter supporting 600+ fal.ai models with full type inference - Video adapter (experimental) for MiniMax, Luma, Kling, Hunyuan, etc. - Type-safe modelOptions using fal's EndpointTypeMap for autocomplete - FalModel, FalModelInput, FalModelOutput utility types - FalImageProviderOptions/FalVideoProviderOptions that exclude fields TanStack AI handles (prompt, size, etc.) - Size preset mapping utilities for fal.ai format - Comprehensive test coverage for both adapters Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds a new TypeScript package Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant App as Application
participant Adapter as FalImageAdapter
participant FAL as `@fal-ai/client`
participant API as fal.ai API
User->>App: request image generation
App->>Adapter: generateImages(options)
Adapter->>Adapter: mapSizeToFalFormat, build input
Adapter->>FAL: subscribe(model, input)
FAL->>API: POST /image/generate
API-->>FAL: { data: { images | image }, requestId }
FAL-->>Adapter: response
Adapter->>Adapter: transformResponse -> ImageGenerationResult
Adapter-->>App: ImageGenerationResult
sequenceDiagram
actor User
participant App as Application
participant Adapter as FalVideoAdapter
participant FAL as `@fal-ai/client` (queue)
participant API as fal.ai API
User->>App: start video job
App->>Adapter: createVideoJob(options)
Adapter->>Adapter: build job input (prompt, duration, aspect_ratio)
Adapter->>FAL: queue.submit(model, input)
FAL->>API: POST /queue/submit
API-->>FAL: { requestId: jobId }
FAL-->>Adapter: jobId
Adapter-->>App: VideoJobResult
loop polling
App->>Adapter: getVideoStatus(jobId)
Adapter->>FAL: queue.status(jobId)
FAL->>API: GET /queue/status
API-->>FAL: { status, queue_position?, logs? }
FAL-->>Adapter: status response
Adapter->>Adapter: mapFalStatusToVideoStatus -> progress
Adapter-->>App: VideoStatusResult
end
App->>Adapter: getVideoUrl(jobId)
Adapter->>FAL: queue.result(jobId)
FAL->>API: GET /queue/result
API-->>FAL: { data: { video { url } } }
FAL-->>Adapter: result
Adapter->>Adapter: extract URL
Adapter-->>App: VideoUrlResult
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In @.gitignore:
- Line 57: The .gitignore entry for .claude/settings.local.json conflicts with
the fact that that same file was committed; either stop tracking the local
settings file or stop ignoring it. Fix by removing the committed
.claude/settings.local.json from the repo index (so it remains only locally) and
commit that removal while keeping the .gitignore entry, or if it should be
shared, delete the .gitignore entry and rename the file to a shared name (e.g.,
settings.json) and commit the renamed file; ensure the change is committed and
the file is no longer tracked if choosing to ignore it.
In `@packages/typescript/ai-fal/package.json`:
- Around line 43-53: The package.json currently lists "@tanstack/ai" under
"dependencies" and "peerDependencies"; remove the "@tanstack/ai" entry from the
"dependencies" object so it only appears in "peerDependencies" (keep the
existing "workspace:*" value there) to ensure the adapter declares the framework
as a peer requirement and avoids bundling a duplicate dependency. Update the
"dependencies" section to no longer include the "@tanstack/ai" key while leaving
"@fal-ai/client" and all devDependencies unchanged.
In `@packages/typescript/ai-fal/src/adapters/image.ts`:
- Around line 20-22: falImage currently ignores FalImageConfig.apiKey and always
reads the API key from env, so passing apiKey in the config has no effect;
update the falImage factory (function falImage) to prefer and use the provided
config.apiKey (FalImageConfig.apiKey) as an override before falling back to
process.env.FAL_API_KEY when instantiating the Fal client or building requests,
and ensure any FalClient creation code (references to FalClientConfig) uses that
resolved key; alternatively, if you want to disallow passing the key, remove
apiKey from FalImageConfig, but the recommended fix is to honor config.apiKey.
In `@packages/typescript/ai-fal/src/adapters/video.ts`:
- Around line 17-19: falVideo currently ignores FalVideoConfig.apiKey and always
reads the API key from env; update falVideo to prefer the provided config.apiKey
as an override (e.g., use config.apiKey if present, otherwise fall back to
process.env.FAL_API_KEY) when creating the client, and ensure FalVideoConfig
(which extends FalClientConfig) remains usable; locate the falVideo
factory/constructor and replace the env-only lookup with a conditional that uses
FalVideoConfig.apiKey before falling back to the environment variable so passing
apiKey in config takes effect.
🧹 Nitpick comments (6)
packages/typescript/ai-fal/package.json (1)
15-20: Consider adding/adapterssubpath export for tree-shaking.Based on learnings, the package should export tree-shakeable adapters with clear subpath exports (e.g.,
@tanstack/ai-fal/adapters). Currently, only the root export is defined. This allows consumers to import only what they need without pulling in the entire bundle.♻️ Suggested exports structure
"exports": { ".": { "types": "./dist/esm/index.d.ts", "import": "./dist/esm/index.js" - } + }, + "./adapters": { + "types": "./dist/esm/adapters/index.d.ts", + "import": "./dist/esm/adapters/index.js" + } },packages/typescript/ai-fal/src/image/image-provider-options.ts (1)
50-61: DRY improvement: Use a const array to derive the type.The preset names are duplicated: once in
FalImageSizePresettype (lines 5-11) and again in this validation array. Consider using a const array to derive the type, eliminating the duplication.♻️ Suggested refactor
+const FAL_IMAGE_SIZE_PRESETS = [ + 'square_hd', + 'square', + 'landscape_4_3', + 'landscape_16_9', + 'portrait_4_3', + 'portrait_16_9', +] as const + -export type FalImageSizePreset = - | 'square_hd' - | 'square' - | 'landscape_4_3' - | 'landscape_16_9' - | 'portrait_4_3' - | 'portrait_16_9' +export type FalImageSizePreset = (typeof FAL_IMAGE_SIZE_PRESETS)[number] // ... later in mapSizeToFalFormat: - if ( - [ - 'square_hd', - 'square', - 'landscape_4_3', - 'landscape_16_9', - 'portrait_4_3', - 'portrait_16_9', - ].includes(size) - ) { + if ((FAL_IMAGE_SIZE_PRESETS as readonly string[]).includes(size)) { return size as FalImageSizePreset }packages/typescript/ai-fal/src/utils/client.ts (2)
42-52: Document thatproxyUrltakes precedence overapiKey.When
proxyUrlis provided,apiKeyis ignored entirely. This is likely intentional (the proxy handles authentication), but this behavior should be documented in the interface or function JSDoc to avoid confusion, especially sinceapiKeyis marked as required inFalClientConfig.📝 Suggested documentation
export interface FalClientConfig { + /** + * API key for fal.ai authentication. + * Ignored when proxyUrl is provided (proxy handles auth). + */ apiKey: string + /** + * Optional proxy URL. When provided, takes precedence over apiKey + * for client configuration. + */ proxyUrl?: string }
54-56:generateIdmay produce variable-length random suffixes.
Math.random().toString(36).substring(7)can produce strings of varying lengths (typically 5-6 characters) because small random numbers result in shorter base-36 representations. For consistent ID lengths, consider usingsubstring(2, 9)to always get 7 characters.♻️ Suggested fix for consistent length
export function generateId(prefix: string): string { - return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}` + return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}` }packages/typescript/ai-fal/src/adapters/image.ts (1)
91-124: Reuse the shared size mapping util to avoid drift.
This method duplicates logic already exported fromimage/image-provider-options.ts(also used in public API/tests). Consider delegating to the shared helper to keep mappings consistent.♻️ Suggested refactor
import { configureFalClient, getFalApiKeyFromEnv, generateId as utilGenerateId, } from '../utils' +import { mapSizeToFalFormat } from '../image/image-provider-options' @@ - if (size) { - input.image_size = this.mapSizeToFalFormat(size) - } + if (size) { + input.image_size = mapSizeToFalFormat(size) ?? size + } @@ - private mapSizeToFalFormat( - size: string, - ): string | { width: number; height: number } { - const SIZE_TO_FAL_PRESET: Record<string, string> = { - '1024x1024': 'square_hd', - '512x512': 'square', - '1024x768': 'landscape_4_3', - '768x1024': 'portrait_4_3', - '1280x720': 'landscape_16_9', - '720x1280': 'portrait_16_9', - '1920x1080': 'landscape_16_9', - '1080x1920': 'portrait_16_9', - } - - const preset = SIZE_TO_FAL_PRESET[size] - if (preset) return preset - - const match = size.match(/^(\d+)x(\d+)$/) - if (match && match[1] && match[2]) { - return { - width: parseInt(match[1], 10), - height: parseInt(match[2], 10), - } - } - - return size - } + // remove private mapSizeToFalFormat in favor of shared utilpackages/typescript/ai-fal/tests/video-adapter.test.ts (1)
146-191: Consider adding edge case and failure status tests.The status mapping tests cover the happy paths well. However, consider adding tests for:
- Edge cases for progress calculation (e.g.,
queue_position: 0orqueue_position: 15)- Failed/error status handling if the Fal API supports failure states
| * All known fal.ai model IDs with autocomplete support. | ||
| * Also accepts any string for custom/new models. | ||
| */ | ||
| export type FalModel = keyof EndpointTypeMap | (string & {}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we have a PR that will go in soon that will allow users to define their custom models and it's options. I wouldn't let the user type anything into this
| * type FluxInput = FalModelInput<'fal-ai/flux/dev'> | ||
| * // { prompt: string; num_inference_steps?: number; ... } | ||
| */ | ||
| export type FalModelInput<TModel extends string> = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why does this not extend FalModel instead of string?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same for the rest of the utilities in the file
| @@ -0,0 +1,53 @@ | |||
| { | |||
| "name": "@tanstack/ai-fal", | |||
| "version": "0.1.0", | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lower this to 0.0.1
| const { model, prompt, numberOfImages, size, modelOptions } = options | ||
|
|
||
| // Build the input object - spread modelOptions first, then override with standard options | ||
| const input: Record<string, unknown> = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be typed as the input to the fal.subscribe function and then the modelOptions shouldn't require any casts
| if (size) { | ||
| input.image_size = this.mapSizeToFalFormat(size) | ||
| } | ||
|
|
||
| // Add number of images if specified | ||
| if (numberOfImages) { | ||
| input.num_images = numberOfImages | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be a part of the input object, not done like this, I'd create a mapInputOptionsToFal method
|
|
||
| return this.transformResponse( | ||
| model, | ||
| result as { data: FalModelOutput<TModel>; requestId: string }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would prefer if there was a way around casting here
|
|
||
| private transformResponse( | ||
| model: string, | ||
| response: { data: FalModelOutput<TModel>; requestId: string }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does fal have utility types for this?
| const input: Record<string, unknown> = { | ||
| prompt, | ||
| } | ||
|
|
||
| // Add duration if specified | ||
| if (duration) { | ||
| input.duration = duration | ||
| } | ||
|
|
||
| // Parse size to aspect ratio if provided | ||
| if (size) { | ||
| const aspectRatio = this.sizeToAspectRatio(size) | ||
| if (aspectRatio) { | ||
| input.aspect_ratio = aspectRatio | ||
| } | ||
| } | ||
|
|
||
| // Merge model-specific options | ||
| if (modelOptions) { | ||
| Object.assign(input, modelOptions) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same comments as for the image jobs
AlemTuzlak
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly looks good, left a few comments
Summary
@tanstack/ai-falpackage with image and video generation adaptersEndpointTypeMap) for autocomplete on 600+ modelsmodelOptionsthat exclude fields TanStack AI handles (prompt, size, etc.)Test plan
pnpm test:typespassespnpm test:libpasses (27 tests)pnpm buildsucceeds🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests
Chores
✏️ Tip: You can customize this high-level summary in your review settings.