Skip to content

feat: Introduce fal.ai adapter for image and video generation#237

Open
tombeckenham wants to merge 34 commits intoTanStack:mainfrom
tombeckenham:feat/fal-ai-adapter
Open

feat: Introduce fal.ai adapter for image and video generation#237
tombeckenham wants to merge 34 commits intoTanStack:mainfrom
tombeckenham:feat/fal-ai-adapter

Conversation

@tombeckenham
Copy link

@tombeckenham tombeckenham commented Jan 20, 2026

Summary

  • Add @tanstack/ai-fal package with image and video generation adapters
  • Leverage fal's type system (EndpointTypeMap) for autocomplete on 600+ models
  • Type-safe modelOptions that exclude fields TanStack AI handles (prompt, size, etc.)
  • Video adapter is experimental, supporting MiniMax, Luma, Kling, Hunyuan, etc.

Test plan

  • pnpm test:types passes
  • pnpm test:lib passes (27 tests)
  • pnpm build succeeds

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Fal AI image generation integration supporting multiple models with customizable sizes and parameters.
    • Added Fal AI video generation integration with text-to-video and image-to-video modes, including job status polling.
    • Added example React application demonstrating image and video generation capabilities with model selection and prompt management.
  • Chores

    • Updated configuration files and project structure for new Fal AI package.

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>
@tombeckenham tombeckenham requested a review from a team January 20, 2026 09:34
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

This PR introduces a new @tanstack/ai-fal package that integrates fal.ai's image and video generation capabilities into TanStack AI through type-safe adapters, plus a full React example application demonstrating both features with server-side orchestration.

Changes

Cohort / File(s) Summary
Configuration & Cleanup
.claude/settings.local.json, .gitignore
Removed local settings file with Bash command allowlist; added it to .gitignore for exclusion.
AI-FAL Core Package
packages/typescript/ai-fal/package.json, packages/typescript/ai-fal/src/adapters/image.ts, packages/typescript/ai-fal/src/adapters/video.ts
New adapters for Fal image and video generation APIs, implementing generic type-safe interfaces (FalImageAdapter, FalVideoAdapter) with model-specific input/output handling, response transformation, and ID generation.
AI-FAL Type System & Utilities
packages/typescript/ai-fal/src/model-meta.ts, packages/typescript/ai-fal/src/image/image-provider-options.ts, packages/typescript/ai-fal/src/utils/client.ts, packages/typescript/ai-fal/src/utils/index.ts, packages/typescript/ai-fal/src/index.ts
Centralized type definitions (FalModel, FalModelInput, FalModelOutput, FalModelImageSize), size mapping utilities (mapSizeToFalFormat), client configuration (FalClientConfig, configureFalClient, getFalApiKeyFromEnv), and public API barrel exports.
AI-FAL Testing & Configuration
packages/typescript/ai-fal/tests/image-adapter.test.ts, packages/typescript/ai-fal/tests/video-adapter.test.ts, packages/typescript/ai-fal/tsconfig.json, packages/typescript/ai-fal/vite.config.ts, packages/typescript/ai-fal/eslint.config.ts
Comprehensive test suites for image and video adapters with mocked Fal client; project build/test configuration with TypeScript, Vitest, and ESLint setup.
React Example Application - Scaffolding
examples/ts-react-fal/package.json, examples/ts-react-fal/.env.example, examples/ts-react-fal/tsconfig.json, examples/ts-react-fal/vite.config.ts
New example project configuration with dependencies (React, TanStack libraries, Tailwind), environment setup, and build tooling (TypeScript, Vite).
React Example - Components
examples/ts-react-fal/src/components/Header.tsx, examples/ts-react-fal/src/components/ImageGenerator.tsx, examples/ts-react-fal/src/components/VideoGenerator.tsx
UI components implementing image and video generation workflows with state management, model selection, prompt input, polling-based status tracking, and per-model result display.
React Example - Routes & Infrastructure
examples/ts-react-fal/src/routes/__root.tsx, examples/ts-react-fal/src/routes/index.tsx, examples/ts-react-fal/src/router.tsx, examples/ts-react-fal/src/routeTree.gen.ts
TanStack Router setup with root layout (head, shell component), index route with tab-based UI, router factory, and generated type-safe route tree.
React Example - Data & Styling
examples/ts-react-fal/src/lib/models.ts, examples/ts-react-fal/src/lib/prompts.ts, examples/ts-react-fal/src/lib/server-functions.ts, examples/ts-react-fal/src/styles.css
Model and prompt definitions with type-safe access; server functions (generateImage, createVideoJob, getVideoStatus, getVideoUrl) dispatching to per-model Fal adapters; Tailwind-integrated CSS with typography/smoothing.

Sequence Diagram(s)

sequenceDiagram
    participant User as User (Browser)
    participant Component as ImageGenerator Component
    participant Server as Server Functions
    participant FalAdapter as Fal Image Adapter
    participant FalAPI as Fal.ai API

    User->>Component: Enter prompt & select model(s)
    User->>Component: Click Generate
    
    Component->>Component: Set loading state
    Component->>Server: generateImage({prompt, model})
    
    Server->>FalAdapter: new FalImageAdapter(model)
    Server->>FalAdapter: generateImages({prompt, modelOptions})
    
    FalAdapter->>FalAdapter: buildInput(prompt, size, modelOptions)
    FalAdapter->>FalAPI: fal.subscribe(model, input)
    FalAPI-->>FalAdapter: streaming result with image URLs
    
    FalAdapter->>FalAdapter: transformResponse(result)
    FalAdapter-->>Server: ImageGenerationResult {images: [...]}
    
    Server-->>Component: {images, model}
    
    Component->>Component: Update per-model results
    Component->>User: Display generated images
Loading
sequenceDiagram
    participant User as User (Browser)
    participant Component as VideoGenerator Component
    participant Server as Server Functions
    participant FalAdapter as Fal Video Adapter
    participant FalAPI as Fal.ai Queue API

    User->>Component: Select video mode & model
    User->>Component: Enter prompt (+ image for image-to-video)
    User->>Component: Click Generate
    
    Component->>Component: Set job to "submitting"
    Component->>Server: createVideoJob({prompt, model, imageUrl?})
    
    Server->>FalAdapter: createFalVideo(model, config)
    Server->>FalAdapter: createVideoJob({prompt, duration, modelOptions})
    
    FalAdapter->>FalAdapter: buildInput(prompt, size, modelOptions)
    FalAdapter->>FalAPI: fal.queue.submit(model, input)
    FalAPI-->>FalAdapter: {jobId}
    
    FalAdapter-->>Server: VideoJobResult {jobId, model}
    Server-->>Component: {jobId, model}
    
    Component->>Component: Set job to "pending", start polling
    loop Poll Status (every N seconds)
        Component->>Server: getVideoStatus({jobId, model})
        Server->>FalAdapter: getVideoStatus(jobId)
        FalAdapter->>FalAPI: fal.queue.status(jobId)
        FalAPI-->>FalAdapter: {status: IN_QUEUE|IN_PROGRESS|COMPLETED, queue_position?, ...}
        
        FalAdapter->>FalAdapter: mapFalStatusToVideoStatus(fal_status)
        FalAdapter-->>Server: {status: pending|processing|completed, progress?}
        Server-->>Component: VideoStatusResult
        
        Component->>Component: Update per-model status & progress
        
        alt Status === completed
            Component->>Server: getVideoUrl({jobId, model})
            Server->>FalAdapter: getVideoUrl(jobId)
            FalAdapter->>FalAPI: fal.queue.result(jobId)
            FalAPI-->>FalAdapter: {video_url or video.url}
            FalAdapter-->>Server: VideoUrlResult {url}
            Server-->>Component: {url}
            Component->>Component: Stop polling, display video
            Component->>User: Show playable video
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Suggested reviewers

  • AlemTuzlak
  • jherr

Poem

🐰 Hops with glee through new adapters bright,
Image and video now in flight!
Types so safe, the generics align,
Server and client in perfect design—
FAL.ai's power in React's embrace,
This PR hops through cyberspace!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.97% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers key changes and test results but is missing the required checklist items and does not follow the provided template structure with explicit sections. Add the complete checklist section with checkboxes for the Contributing guide and local testing steps, and ensure the description explicitly follows the template format with all required sections.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: introducing a fal.ai adapter for image and video generation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 /adapters subpath 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 FalImageSizePreset type (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 that proxyUrl takes precedence over apiKey.

When proxyUrl is provided, apiKey is 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 since apiKey is marked as required in FalClientConfig.

📝 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: generateId may 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 using substring(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 from image/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 util
packages/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:

  1. Edge cases for progress calculation (e.g., queue_position: 0 or queue_position: 15)
  2. Failed/error status handling if the Fal API supports failure states

Copy link
Contributor

@AlemTuzlak AlemTuzlak left a 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

@nx-cloud
Copy link

nx-cloud bot commented Jan 23, 2026

View your CI Pipeline Execution ↗ for commit 0295f19

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 3m 8s View ↗
nx run-many --targets=build --exclude=examples/** ✅ Succeeded 1m 11s View ↗

☁️ Nx Cloud last updated this comment at 2026-01-23 15:32:10 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 23, 2026

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai@237

@tanstack/ai-anthropic

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-anthropic@237

@tanstack/ai-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-client@237

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-devtools-core@237

@tanstack/ai-fal

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-fal@237

@tanstack/ai-gemini

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-gemini@237

@tanstack/ai-grok

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok@237

@tanstack/ai-ollama

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-ollama@237

@tanstack/ai-openai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openai@237

@tanstack/ai-openrouter

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openrouter@237

@tanstack/ai-preact

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-preact@237

@tanstack/ai-react

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react@237

@tanstack/ai-react-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react-ui@237

@tanstack/ai-solid

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid@237

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid-ui@237

@tanstack/ai-svelte

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-svelte@237

@tanstack/ai-vue

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue@237

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue-ui@237

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/preact-ai-devtools@237

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/react-ai-devtools@237

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/solid-ai-devtools@237

commit: 0295f19

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@packages/typescript/ai-fal/src/adapters/image.ts`:
- Around line 57-112: The private mapSizeToFalFormat method duplicates shared
logic and incorrectly returns unknown sizes instead of undefined; remove this
private mapSizeToFalFormat and import the shared mapSizeToFalFormat from
image-provider-options.ts, then in generateImages (where input.image_size is
set) only include image_size when the imported utility returns a defined value
(i.e., set ...(size && mappedSize ? { image_size: mappedSize } : {}) ), ensuring
invalid sizes are filtered out before calling fal.subscribe.

In `@packages/typescript/ai-fal/src/adapters/video.ts`:
- Around line 75-86: The payload in createVideoJob sometimes includes
aspect_ratio: undefined because sizeToAspectRatio(size) can return undefined;
change the input construction in createVideoJob to call
this.sizeToAspectRatio(size) into a local variable (e.g., const aspect =
this.sizeToAspectRatio(size)) and only include aspect_ratio in the FalModelInput
if aspect is not undefined (i.e., spread { aspect_ratio: aspect }
conditionally), so the API never receives aspect_ratio: undefined; update
references to FalModelInput<TModel> and ensure other fields (prompt, duration,
modelOptions) remain unchanged.
- Around line 105-110: The progress calculation currently uses a truthy check on
statusResponse.queue_position which treats 0 as falsy and yields undefined;
update the return in the function that builds the response (the object
containing jobId and mapFalStatusToVideoStatus(...)) to use a nullish check
(e.g., check statusResponse.queue_position != null or use ??) so that 0 is
preserved and progress computes to Math.max(0, 100 -
statusResponse.queue_position * 10).
♻️ Duplicate comments (2)
packages/typescript/ai-fal/src/adapters/video.ts (1)

168-178: config.apiKey is documented but not supported.
FalVideoConfig omits apiKey, and falVideo always reads from env. Either add apiKey?: string and prefer it, or update the docs to avoid a misleading API.

🔧 Option: honor config.apiKey
-export interface FalVideoConfig extends Omit<FalClientConfig, 'apiKey'> {}
+export interface FalVideoConfig extends Omit<FalClientConfig, 'apiKey'> {
+  apiKey?: string
+}
@@
-  const apiKey = getFalApiKeyFromEnv()
+  const apiKey = config?.apiKey ?? getFalApiKeyFromEnv()
packages/typescript/ai-fal/src/adapters/image.ts (1)

194-199: config.apiKey is documented but not supported.
FalImageConfig omits apiKey, and falImage always reads from env. Either add apiKey?: string and prefer it, or update the docs to avoid a misleading API.

🔧 Option: honor config.apiKey
-export interface FalImageConfig extends Omit<FalClientConfig, 'apiKey'> {}
+export interface FalImageConfig extends Omit<FalClientConfig, 'apiKey'> {
+  apiKey?: string
+}
@@
-  const apiKey = getFalApiKeyFromEnv()
+  const apiKey = config?.apiKey ?? getFalApiKeyFromEnv()

@AlemTuzlak
Copy link
Contributor

@tombeckenham would you mind confirming that my PR didn't break anything and image/video gen still works, would really appreciate some sort of small demo app in examples repo to be able to see and test it!

@tombeckenham
Copy link
Author

I was thinking that. I've got an idea for a demo.

@jherr
Copy link
Contributor

jherr commented Jan 24, 2026

There is a testing panel that tests image and video APIs (really all the APIs)

@tombeckenham
Copy link
Author

I'm working on this. I've discovered this afternoon that the fal client is out of sync with what the fal api supports. I'll need a bit more time on this

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@scripts/compare-fal-models.ts`:
- Around line 242-244: Change the array type annotation in generateCSV to comply
with the lint rule by replacing the short form string[] with the generic form
Array<string>; specifically update the declaration of the lines variable (and
any other occurrences inside generateCSV using string[] types) to use
Array<string> so the function signature and internal typings remain consistent
with the project's ESLint configuration.

In `@scripts/fetch-fal-models.ts`:
- Around line 124-126: The inline comment above the sleep call is misleading —
it says "5 seconds" while the code calls sleep(2000) (2 seconds); update the
comment to match the actual delay or, if the intended delay is 5s, change the
argument to sleep(5000). Locate the block that checks cursor and calls sleep in
scripts/fetch-fal-models.ts (the cursor check and sleep(...) call) and make the
comment and the sleep() value consistent.
- Around line 1-6: The file header comment incorrectly states the script saves a
"TypeScript file" while scripts/fetch-fal-models.ts actually writes JSON output;
update the top comment block to say the script downloads models from the Fal API
and saves them as a generated JSON file (or "JSON output") so the description
matches the actual behavior, and adjust any phrasing referencing "TypeScript
file" to "JSON file" in that header.
🧹 Nitpick comments (1)
scripts/compare-fal-models.ts (1)

15-16: Use dynamic module resolution instead of hard-coded node_modules path for EndpointTypeMap.

The current path at line 81 assumes a specific workspace layout (packages/typescript/ai-fal/node_modules/@fal-ai/client/src/types/endpoints.d.ts) and is fragile across different pnpm configurations. Since @fal-ai/client is published with src/types/endpoints.d.ts available via typesVersions, resolve it dynamically:

Suggested approach
-import { join } from 'node:path'
+import { dirname, join } from 'node:path'
+import { createRequire } from 'node:module'
@@
-    const typesPath = join(
-      process.cwd(),
-      'packages/typescript/ai-fal/node_modules/@fal-ai/client/src/types/endpoints.d.ts',
-    )
+    const require = createRequire(import.meta.url)
+    const pkgJsonPath = require.resolve('@fal-ai/client/package.json')
+    const typesPath = join(
+      dirname(pkgJsonPath),
+      'src/types/endpoints.d.ts',
+    )

Also applies to: 77-83, 87-88

tombeckenham and others added 3 commits January 28, 2026 16:16
…s from input types

Changed the endpoint map generation script to derive output types directly from input types (replacing "Input" suffix with "Output") instead of parsing complex GET response type definitions with regex. This eliminates all warnings caused by inconsistent formatting in generated type files.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…rmatting

- Fix ESLint import/no-duplicates error by combining imports from './index'
- Add Zod schema type annotations to all SchemaMap constants
- Use specific union types (FalImageModel, FalTextModel, etc.) instead of string in combined schema maps
- Add Prettier formatting to all generated files
- Reorder Model type definition before SchemaMap (since SchemaMap references it)
- Remove unused postTypeName variable

All generated files now pass ESLint and TypeScript checks with proper type safety.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/typescript/ai-fal/src/adapters/video.ts`:
- Line 21: The FalVideoConfig interface currently omits apiKey while JSDoc
claims an apiKey override is supported; either restore an optional apiKey in
FalVideoConfig (e.g., extend FalClientConfig but allow apiKey?: string) so
callers can pass an override, or update the JSDoc/comments around FalVideoConfig
(and related usage around lines ~180-190) to state that apiKey is only read from
the environment and cannot be provided via config; update any runtime code that
reads config.apiKey (e.g., where FalVideoConfig is consumed) to match the chosen
behavior.

… scripts

- Refactored file path handling in compare-fal-models.ts and fetch-fal-models.ts to use __dirname for better compatibility with module types.
- Ensured the scripts directory is created if it doesn't exist.
- Cleaned up import statements in various files for consistency and clarity.
- Enhanced type imports in generated index.ts to reflect the new structure of endpoint maps.

These changes improve the maintainability and reliability of the scripts.
Copy link
Author

@tombeckenham tombeckenham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit happier with where this is at. It now gathers type information from fal using their open api spec, then generates both types and zod schemas. These will be super useful as you can use Ai to transform prompts to optimise for each model. It will also allow users to update UI at runtime based on what options a model will allow.

I'm stil not totally happy with the size mapping, and I need to create an example app. I'm unfortunately busy now until Monday. I'll take a look then

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@packages/typescript/ai-fal/src/adapters/image.ts`:
- Around line 89-103: The generateImages method is currently defaulting size to
'0x0' which causes image_size, aspect_ratio, and resolution to be set to
placeholder/null values and sent to fal.ai; update generateImages so that when
size is undefined or parseSize returns an invalid size you omit image_size,
aspect_ratio, and resolution from the object passed to this.inputSchema.parse.
Concretely, build the input payload by spreading modelOptions and required
fields (prompt, num_images), then conditionally add image_size:
this.mapSizeToImageSize(...), aspect_ratio: this.calculateAspectRatio(...), and
resolution: this.determineResolution(...) only if a valid size was provided
(i.e., size !== undefined and parseSize returns non-zero width/height) before
calling this.inputSchema.parse.

In `@packages/typescript/ai-fal/src/adapters/video.ts`:
- Around line 80-101: createVideoJob currently parses the input using the
adapter's schema (this.inputSchema) but submits the request using options.model,
which can cause schema/model mismatches; update the method to use the adapter's
model (this.model) when calling fal.queue.submit or else validate that
options.model === this.model before submission, and throw or coerce if they
differ so the parsed input and submitted model stay consistent (refer to
createVideoJob, this.inputSchema, this.model, and options.model).

…nsforms

- Consolidate input/output categories (e.g., text-to-image + image-to-image → image)
- Add x-fal-file-input extension to mark URL fields for Blob/File uploads
- Add TypeScript typeTransformer to generate string | Blob | File unions
- Add Zod resolver for file input fields and format:binary output fields
- Update endpoint map generation for new category structure
- Remove deprecated fetch/compare scripts from root package.json

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/typescript/ai-fal/openapi-ts.config.ts`:
- Line 265: Remove the stray debug console.log by deleting or wrapping
console.log(groupedCategoryFilenames) so it only runs under a debug/verbose
flag; update the surrounding code in openapi-ts.config.ts where
groupedCategoryFilenames is logged (or replace with
processLogger.debug/process.env.DEBUG check) to prevent spamming build logs.
🧹 Nitpick comments (2)
packages/typescript/ai-fal/openapi-ts.config.ts (2)

168-187: Consider using for...of to avoid forEach return value warning.

The static analyzer flags line 172 because findAllRefs returns a value. While this is a false positive (the return is intentionally ignored and mutation happens via the shared refs Set), using for...of is more idiomatic for imperative side-effect loops.

♻️ Proposed refactor
   if (Array.isArray(obj)) {
-    obj.forEach((item) => findAllRefs(item, refs))
+    for (const item of obj) {
+      findAllRefs(item, refs)
+    }
   } else {

281-281: Consider using for...of for consistency.

Same pattern as line 172 - Set.add() returns the Set, triggering the static analysis warning. Using for...of would be consistent with the earlier suggestion and avoid the warning.

♻️ Proposed refactor
-    unknown.forEach((u) => allUnknown.add(u))
+    for (const u of unknown) {
+      allUnknown.add(u)
+    }

- Changed file_data type from Blob | File to string in various schema definitions for consistency.
- Updated Zod schemas to reflect the new string type for file_data.
- Adjusted input and output schemas across multiple categories to ensure type uniformity.

These changes enhance type safety and simplify handling of file data in the application.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@packages/typescript/ai-fal/openapi-ts.config.ts`:
- Around line 172-174: Convert the concise arrow callbacks used with
Array.prototype.forEach to block-bodied callbacks to avoid returning values from
the callback; specifically change the forEach in findAllRefs from
obj.forEach((item) => findAllRefs(item, refs)) to obj.forEach((item) => {
findAllRefs(item, refs); }); Do the same for the other forEach callback flagged
by the linter elsewhere in this file (the similar arrow callback around the
later block) so each callback uses a block body and an explicit statement rather
than an expression return.
- Around line 347-377: The string(ctx: any) Zod resolver currently returns
undefined, disabling file-type unions; re-enable the commented blocks so inputs
with schema['x-fal-file-input'] return a z.union of z.string(), instanceof Blob
and instanceof File (using $(z).attr('union').call($.array(...))) and outputs
with schema.format === 'binary' return a z.union of instanceof Blob and
instanceof File similarly; use the existing locals ({ $, schema, symbols } and z
from symbols) and ensure the resolver returns those union expressions instead of
undefined so runtime validation matches the TypeScript types.
🧹 Nitpick comments (1)
packages/typescript/ai-fal/openapi-ts.config.ts (1)

23-48: Consider camelCase for the new constants.

KNOWN_MISSING_SCHEMAS (Line 23) and FAL_FILE_FIELD_PATTERNS (Line 40) use SCREAMING_SNAKE_CASE; consider knownMissingSchemas / falFileFieldPatterns (and update references) to align with the naming rule. As per coding guidelines, use camelCase for function and variable names throughout the codebase.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@packages/typescript/ai-fal/src/adapters/image.ts`:
- Around line 199-204: The type FalImageModel is undefined and should be
replaced with the existing FalModel; update both factory functions (the
createFalImage generic signatures and returned FalImageAdapter instantiations)
so their generic constraint reads TModel extends FalModel and the
FalImageAdapter is parameterized with that TModel, ensuring you rely on the
already-imported FalModel used elsewhere.
- Around line 77-90: The code extracts size but never uses it and several helper
symbols (parseSize, mapSizeToImageSize, calculateAspectRatio,
determineResolution, SIZE_TO_PRESET) are dead; implement size handling by
parsing the incoming size (using parseSize), mapping it to the fal.ai image size
preset (via mapSizeToImageSize or SIZE_TO_PRESET), and include the resulting
size/resolution field in the fal.subscribe input object (alongside prompt,
num_images and modelOptions) before calling fal.subscribe(this.model, ...);
ensure any resolution/aspect logic uses calculateAspectRatio and
determineResolution as needed, or if you choose to omit size support remove
SIZE_TO_PRESET, parseSize, mapSizeToImageSize, calculateAspectRatio,
determineResolution and drop size from the destructuring so no unused code
remains.
- Around line 80-82: The unconditional throw for modelOptions prevents using
prompt-only image generation; remove the hard throw in adapters/image.ts and
treat modelOptions as optional by merging it with sensible defaults (e.g.,
defaultModelOptions) before use, update the surrounding function (the image
adapter / whichever function reads modelOptions) to accept modelOptions?: Type
and only validate required subfields when they are actually necessary, and
ensure downstream calls reference the merged options object instead of the raw
modelOptions variable.
🧹 Nitpick comments (2)
packages/typescript/ai-fal/src/model-meta.ts (1)

14-14: Remove unused FalModelTest type.

This type appears to be leftover debugging/test code and is not used anywhere in the codebase. Consider removing it to keep the public API surface clean.

-export type FalModelTest = 'fal-ai/nano-banana-pro/edit'
 export type FalModel = keyof EndpointTypeMap
packages/typescript/ai-fal/src/adapters/image.ts (1)

164-172: Avoid any type for better type safety.

The img parameter uses any which loses type inference. Consider using a minimal inline type:

-      images = data.images.map((img: any) => this.parseImage(img))
+      images = data.images.map((img: { url: string }) => this.parseImage(img))

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 `@packages/typescript/ai-fal/src/adapters/image.ts`:
- Line 72: Update the inline comment that currently reads "TB Jan 20206" to the
correct date "TB Jan 2026" in packages/typescript/ai-fal/src/adapters/image.ts;
locate the comment containing the exact text "TB Jan 20206 I can think of no way
to use satisfies here because of the union type of FalModelInput with record
any" and correct the year to 2026.
- Around line 69-80: The buildInput method currently assigns options.size
directly to image_size; update buildInput (in the
ImageGenerationOptions->buildInput flow) to convert the size using
mapSizeToFalFormat before assigning to FalModelInput.image_size — import
mapSizeToFalFormat and replace image_size: options.size with image_size:
mapSizeToFalFormat(options.size) so fal.ai receives preset names or
{width,height} instead of the TanStack "1024x1024" string.
- Around line 51-59: The constructor currently ignores config.apiKey and only
uses getFalApiKeyFromEnv(); update the FalImageAdapter constructor to honor
config.apiKey by resolving apiKey = config?.apiKey ?? getFalApiKeyFromEnv() (or
simply pass the whole config into configureFalClient(config) like
FalVideoAdapter does), then validate apiKey and call configureFalClient({
apiKey, proxyUrl: config?.proxyUrl }) (or configureFalClient(config)) so the
adapter respects FalClientConfig.apiKey and still falls back to environment.
- Around line 42-47: The adapter is using broad Record types for
model->options/sizes which incorrectly assert a single-model-specific value for
all FalModel keys; update FalImageAdapter's BaseImageAdapter generic arguments
to use the concrete mappings FalImageModelProviderOptionsByName and
FalImageModelSizeByName (instead of Record<FalModel,
FalImageProviderOptions<TModel>> and Record<FalModel,
FalModelImageSize<TModel>>), and adjust the FalImageAdapter generic parameters
if needed so the second/third/fourth type args reference
FalImageProviderOptions, FalImageModelProviderOptionsByName, and
FalImageModelSizeByName respectively (matching the pattern used by
OpenAIImageAdapter) to ensure proper per-model typing.
🧹 Nitpick comments (1)
packages/typescript/ai-fal/src/adapters/image.ts (1)

86-108: Consider logging or throwing when response contains neither images nor image.

If the fal.ai response doesn't match either expected format, transformResponse silently returns an empty images array. This could mask API errors or unexpected response structures.

💡 Suggested improvement
     // Handle single image response (some models return { image: {...} })
     else if ('image' in data && data.image && typeof data.image === 'object') {
       images.push(this.parseImage(data.image))
     }
+    else if (images.length === 0) {
+      console.warn(
+        `[fal-ai] Unexpected response format: neither 'images' array nor 'image' object found`,
+      )
+    }

     return {

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@packages/typescript/ai-fal/src/model-meta.ts`:
- Around line 24-27: FalModelInput currently falls back to Record<string, any>,
losing compile-time safety—change its fallback to Record<string, unknown> so
unknown model inputs remain type-safe; then refactor FalModelImageSize to mirror
the conditional on EndpointTypeMap (i.e., check TModel extends keyof
EndpointTypeMap) and return EndpointTypeMap[TModel]['imageSize'] for known
models and never for unknown models (instead of inheriting the any fallback);
update any consumers relying on the old any fallback to handle the new
never/unknown semantics.
- Around line 11-15: FalModel currently uses "keyof EndpointTypeMap | string"
which collapses to string and loses autocomplete; change the type to use the
string & {} trick so known keys are preserved while still allowing arbitrary
strings (i.e., update the FalModel type declaration referencing EndpointTypeMap
to use "keyof EndpointTypeMap | (string & {})"); keep the existing JSDoc comment
and export name FalModel intact so editor tooling continues to offer known model
completions.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@examples/ts-react-fal/src/components/VideoGenerator.tsx`:
- Around line 46-52: The Biome lint error comes from using a concise arrow in
pollingRefs.current.forEach which creates an implicit return; update the
callbacks to use a block body that explicitly returns void (e.g., replace
pollingRefs.current.forEach((interval) => clearInterval(interval)) with a
block-style callback that calls clearInterval(interval) inside {}), and then
call pollingRefs.current.clear() as before; apply the same change to the other
occurrence that iterates pollingRefs.current (the useEffect cleanup around the
polling logic) so all forEach callbacks use explicit block bodies to satisfy the
linter.

In `@examples/ts-react-fal/src/lib/server-functions.ts`:
- Around line 5-8: The inputValidator callback currently calls
data.prompt.trim() and can throw a TypeError for non-string inputs; update the
validator used in .inputValidator to first check types (e.g., ensure typeof
data.prompt === 'string' and typeof data.model === 'string' or data.prompt !=
null) and throw the intended validation Errors ("Prompt is required", "Model is
required") when the checks fail, then call trim() and return data; apply the
same type-guarding change to the other validator at the location referenced
(lines 82-86) so both validators validate types before calling trim().

In `@examples/ts-react-fal/src/styles.css`:
- Around line 3-5: The CSS uses the Tailwind directive "@apply m-0" in
styles.css which causes a Biome parse error; fix by either enabling tailwind
directives in your Biome config (set tailwindDirectives: true) so Biome accepts
"@apply", or remove/replace the "@apply m-0" usage with equivalent plain CSS
(e.g., set margin: 0 on the body selector) to avoid Tailwind directives
entirely.
🧹 Nitpick comments (2)
examples/ts-react-fal/src/lib/models.ts (1)

1-85: Rename model constants to camelCase for guideline compliance.

IMAGE_MODELS / VIDEO_MODELS violate the camelCase variable naming rule. Consider renaming to camelCase and updating references in consuming files.

♻️ Suggested refactor
-export const IMAGE_MODELS = [
+export const imageModels = [
@@
-] as const
+] as const
@@
-export const VIDEO_MODELS = [
+export const videoModels = [
@@
-] as const
+] as const
@@
-export type ImageModel = (typeof IMAGE_MODELS)[number]
-export type VideoModel = (typeof VIDEO_MODELS)[number]
+export type ImageModel = (typeof imageModels)[number]
+export type VideoModel = (typeof videoModels)[number]

As per coding guidelines, **/*.{ts,tsx,js,jsx}: Use camelCase for function and variable names throughout the codebase.

examples/ts-react-fal/src/lib/prompts.ts (1)

3-81: Rename prompt constants to camelCase and keep return types precise.

IMAGE_PROMPTS / VIDEO_PROMPTS break the camelCase naming rule, and the helpers could return ImagePrompt / VideoPrompt to preserve the literal union types. Update consuming imports accordingly.

♻️ Suggested refactor
-export const IMAGE_PROMPTS = [
+export const imagePrompts = [
@@
-] as const
+] as const
@@
-export const VIDEO_PROMPTS = {
+export const videoPrompts = {
@@
-} as const
+} as const
@@
-export type ImagePrompt = (typeof IMAGE_PROMPTS)[number]
+export type ImagePrompt = (typeof imagePrompts)[number]
 export type VideoPrompt =
-  | (typeof VIDEO_PROMPTS.textToVideo)[number]
-  | (typeof VIDEO_PROMPTS.imageToVideo)[number]
+  | (typeof videoPrompts.textToVideo)[number]
+  | (typeof videoPrompts.imageToVideo)[number]
@@
-export function getRandomImagePrompt(): string {
-  return IMAGE_PROMPTS[Math.floor(Math.random() * IMAGE_PROMPTS.length)]
+export function getRandomImagePrompt(): ImagePrompt {
+  return imagePrompts[Math.floor(Math.random() * imagePrompts.length)]
 }
@@
-export function getRandomVideoPrompt(
-  mode: 'text-to-video' | 'image-to-video',
-): string {
+export function getRandomVideoPrompt(
+  mode: 'text-to-video' | 'image-to-video',
+): VideoPrompt {
   const prompts =
     mode === 'text-to-video'
-      ? VIDEO_PROMPTS.textToVideo
-      : VIDEO_PROMPTS.imageToVideo
+      ? videoPrompts.textToVideo
+      : videoPrompts.imageToVideo
   return prompts[Math.floor(Math.random() * prompts.length)]
 }

As per coding guidelines, **/*.{ts,tsx,js,jsx}: Use camelCase for function and variable names throughout the codebase.

@tombeckenham
Copy link
Author

I've simplified this to use the existing Fal types rather than trying to generate our own using open api specs. Having said that, the way forward is probably to use runtime validation / dynamic prompt optimization. Creating typesafe model inputs is not that useful.

It all works great though. I created an example app. Create a key on fal.ai and add $10 or more to your account to create a FAL_KEY.

image
ai-fal-video.mov

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants