Skip to content

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

    • Add ai-fal package: fal.ai image & video adapters, size-mapping utilities, model typings, and API-key/client helpers.
  • Tests

    • Add comprehensive tests for image & video adapters, size mapping, and client configuration.
  • Chores

    • Initialize package build/test/config tooling, update .gitignore to re-ignore build output and ignore local AI settings, and remove a local AI settings file.

✏️ Tip: You can customize this high-level summary in your review settings.

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

Adds a new TypeScript package @tanstack/ai-fal including fal.ai image and video adapters, model typings, utilities, tests, and build config; deletes .claude/settings.local.json and updates .gitignore to re-add .output and ignore the claude settings file.

Changes

Cohort / File(s) Summary
Repo config
\.claude/settings.local.json`, `.gitignore``
Deleted .claude/settings.local.json. Updated .gitignore to re-add .output and add .claude/settings.local.json.
Package manifest & build
\packages/typescript/ai-fal/package.json`, `packages/typescript/ai-fal/tsconfig.json`, `packages/typescript/ai-fal/vite.config.ts``
New package manifest, TypeScript config, and Vite/Vitest config (entrypoints, scripts, deps, test/coverage settings).
Adapters — Image
\packages/typescript/ai-fal/src/adapters/image.ts`, `packages/typescript/ai-fal/src/image/image-provider-options.ts``
New FalImageAdapter, factory helpers (createFalImage, falImage), size mapping (mapSizeToFalFormat), request assembly, response normalization (URL & base64), deterministic ID generation, and provider option types.
Adapters — Video
\packages/typescript/ai-fal/src/adapters/video.ts` `
New FalVideoAdapter, factory helpers (createFalVideo, falVideo), job submission, status polling/mapping, result retrieval (URL), aspect-ratio handling, and provider option types.
Model metadata
\packages/typescript/ai-fal/src/model-meta.ts` `
New typed model surface: FalModel, FalModelInput, FalModelOutput, FalImageProviderOptions, FalVideoProviderOptions (typed against @fal-ai/client EndpointTypeMap).
Utilities
\packages/typescript/ai-fal/src/utils/client.ts`, `packages/typescript/ai-fal/src/utils/index.ts``
New FalClientConfig, getFalApiKeyFromEnv(), configureFalClient(), generateId(), and barrel re-exports.
Public barrel
\packages/typescript/ai-fal/src/index.ts``
Package-level re-exports for adapters, factories, size utils, model types, and client utilities.
Tests
\packages/typescript/ai-fal/tests/image-adapter.test.ts`, `packages/typescript/ai-fal/tests/video-adapter.test.ts``
New tests mocking @fal-ai/client validating image/video flows, size mapping, base64 handling, options propagation, status mapping, and URL extraction.

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: { image(s) }, requestId }
    FAL-->>Adapter: response
    Adapter->>Adapter: transformResponse -> ImageGenerationResult
    Adapter-->>App: ImageGenerationResult
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped through types and adapters bright,

Fal.ai joined the stack tonight.
Prompts and jobs, URLs in hand,
I stitched new bindings across the land.
A little rabbit cheers this build tonight ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is missing required sections from the template: the 🎯 Changes section explanation and the ✅ Checklist with required items are not present or incomplete. Add the complete 🎯 Changes section describing the motivation for changes and ensure all checklist items are present and checked, including the Contributing guide acknowledgment and pnpm run test:pr confirmation.
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: introducing fal.ai adapters for image and video generation, which aligns with the primary objective of the changeset.

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

✨ Finishing touches
  • 📝 Generate docstrings

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()

Comment on lines +57 to +112
async generateImages(
options: ImageGenerationOptions<FalImageProviderOptions<TModel>>,
): Promise<ImageGenerationResult> {
const { model, prompt, numberOfImages, size, modelOptions } = options

// Build the input object - spread modelOptions first, then override with standard options
const input: FalModelInput<TModel> = {
...modelOptions,
prompt,
...(size ? { image_size: this.mapSizeToFalFormat(size) } : {}),
...(numberOfImages ? { num_images: numberOfImages } : {}),
}

const result = await fal.subscribe(model as TModel, { input })

return this.transformResponse(model, result)
}

protected override generateId(): string {
return utilGenerateId(this.name)
}

/**
* Maps TanStack AI size format (WIDTHxHEIGHT) to fal.ai format.
* fal.ai accepts either preset names or { width, height } objects.
*/
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',
}

// Check if it's a known preset mapping
const preset = SIZE_TO_FAL_PRESET[size]
if (preset) return preset

// Try to parse as WIDTHxHEIGHT
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 as-is if it's already a preset name
return size
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's explore the structure of the ai-fal package
find packages/typescript/ai-fal -type f -name "*.ts" | head -20

Repository: TanStack/ai

Length of output: 166


🏁 Script executed:

rg "mapSizeToFalFormat" packages/typescript/ai-fal/

Repository: TanStack/ai

Length of output: 2042


🏁 Script executed:

ls -la packages/typescript/ai-fal/src/image/ 2>/dev/null || echo "Directory does not exist"

Repository: TanStack/ai

Length of output: 240


🏁 Script executed:

cat -n packages/typescript/ai-fal/src/image/image-provider-options.ts

Repository: TanStack/ai

Length of output: 1914


🏁 Script executed:

cat -n packages/typescript/ai-fal/src/adapters/image.ts | sed -n '80,115p'

Repository: TanStack/ai

Length of output: 1353


🏁 Script executed:

grep -A 20 "describe('mapSizeToFalFormat'" packages/typescript/ai-fal/tests/image-adapter.test.ts

Repository: TanStack/ai

Length of output: 932


🏁 Script executed:

# Check if mapSizeToFalFormat is exported from index.ts
grep -A 2 "mapSizeToFalFormat" packages/typescript/ai-fal/src/index.ts

Repository: TanStack/ai

Length of output: 143


🏁 Script executed:

# Verify the current usage context in adapters/image.ts
cat -n packages/typescript/ai-fal/src/adapters/image.ts | sed -n '57,75p'

Repository: TanStack/ai

Length of output: 877


Replace the private mapSizeToFalFormat() method with the shared utility to ensure consistent validation.

The codebase has a shared mapSizeToFalFormat() exported from image-provider-options.ts that properly validates sizes and returns undefined for invalid inputs. The private implementation duplicates this logic but differs in a critical way: it passes unknown sizes through as-is instead of returning undefined, which can forward invalid values to fal.ai. Import and use the shared utility instead, setting image_size only when a valid value is returned.

🔧 Suggested refactor
-import type { OutputType, Result } from '@fal-ai/client'
+import type { OutputType, Result } from '@fal-ai/client'
+import { mapSizeToFalFormat } from '../image/image-provider-options'
@@
-    const input: FalModelInput<TModel> = {
-      ...modelOptions,
-      prompt,
-      ...(size ? { image_size: this.mapSizeToFalFormat(size) } : {}),
-      ...(numberOfImages ? { num_images: numberOfImages } : {}),
-    }
+    const imageSize = size ? mapSizeToFalFormat(size) : undefined
+    const input: FalModelInput<TModel> = {
+      ...modelOptions,
+      prompt,
+      ...(imageSize ? { image_size: imageSize } : {}),
+      ...(numberOfImages ? { num_images: numberOfImages } : {}),
+    }
@@
-  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
-  }
🤖 Prompt for AI Agents
In `@packages/typescript/ai-fal/src/adapters/image.ts` around lines 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.

Comment on lines +75 to +86
async createVideoJob(
options: VideoGenerationOptions<FalVideoProviderOptions<TModel>>,
): Promise<VideoJobResult> {
const { model, prompt, size, duration, modelOptions } = options

// Build the input object for fal.ai
const input: FalModelInput<TModel> = {
...modelOptions,
prompt,
...(duration ? { duration } : {}),
...(size ? { aspect_ratio: this.sizeToAspectRatio(size) } : {}),
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the video.ts file to understand the context better
head -100 packages/typescript/ai-fal/src/adapters/video.ts | cat -n

Repository: TanStack/ai

Length of output: 3355


🏁 Script executed:

# Search for the sizeToAspectRatio method implementation
rg "sizeToAspectRatio" packages/typescript/ai-fal/src/adapters/video.ts -A 10 -B 2

Repository: TanStack/ai

Length of output: 838


🏁 Script executed:

# Check the FalModelInput type definition
rg "FalModelInput" packages/typescript/ai-fal/src/ -A 5 -B 2

Repository: TanStack/ai

Length of output: 4752


🏁 Script executed:

# Look for similar patterns with conditional spreads in the codebase
rg "\.\.\.\(" packages/typescript/ai-fal/src/ -B 2 -A 1 | head -50

Repository: TanStack/ai

Length of output: 880


Avoid sending aspect_ratio: undefined to the fal.ai API.

When size is provided but not in WIDTHxHEIGHT format, sizeToAspectRatio() returns undefined, yet the spread operator still includes aspect_ratio: undefined in the payload, which the API may reject.

🔧 Suggested change
-    const input: FalModelInput<TModel> = {
-      ...modelOptions,
-      prompt,
-      ...(duration ? { duration } : {}),
-      ...(size ? { aspect_ratio: this.sizeToAspectRatio(size) } : {}),
-    }
+    const aspectRatio = size ? this.sizeToAspectRatio(size) : undefined
+    const input: FalModelInput<TModel> = {
+      ...modelOptions,
+      prompt,
+      ...(duration ? { duration } : {}),
+      ...(aspectRatio ? { aspect_ratio: aspectRatio } : {}),
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async createVideoJob(
options: VideoGenerationOptions<FalVideoProviderOptions<TModel>>,
): Promise<VideoJobResult> {
const { model, prompt, size, duration, modelOptions } = options
// Build the input object for fal.ai
const input: FalModelInput<TModel> = {
...modelOptions,
prompt,
...(duration ? { duration } : {}),
...(size ? { aspect_ratio: this.sizeToAspectRatio(size) } : {}),
}
async createVideoJob(
options: VideoGenerationOptions<FalVideoProviderOptions<TModel>>,
): Promise<VideoJobResult> {
const { model, prompt, size, duration, modelOptions } = options
// Build the input object for fal.ai
const aspectRatio = size ? this.sizeToAspectRatio(size) : undefined
const input: FalModelInput<TModel> = {
...modelOptions,
prompt,
...(duration ? { duration } : {}),
...(aspectRatio ? { aspect_ratio: aspectRatio } : {}),
}
🤖 Prompt for AI Agents
In `@packages/typescript/ai-fal/src/adapters/video.ts` around lines 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.

Comment on lines +105 to +110
return {
jobId,
status: mapFalStatusToVideoStatus(statusResponse.status),
progress: statusResponse.queue_position
? Math.max(0, 100 - statusResponse.queue_position * 10)
: undefined,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n packages/typescript/ai-fal/src/adapters/video.ts | sed -n '100,115p'

Repository: TanStack/ai

Length of output: 653


🏁 Script executed:

rg "FalStatusResponse|queue_position" packages/typescript/ai-fal/src/ -A 3 -B 1

Repository: TanStack/ai

Length of output: 1339


Use nullish check instead of truthy check for queue_position.
When queue_position = 0, the current truthy check treats it as falsy, causing progress to become undefined. Since 0 is a valid value that should calculate to progress = 100, use a nullish check to preserve it.

🔧 Suggested change
-      progress: statusResponse.queue_position
-        ? Math.max(0, 100 - statusResponse.queue_position * 10)
-        : undefined,
+      progress:
+        statusResponse.queue_position != null
+          ? Math.max(0, 100 - statusResponse.queue_position * 10)
+          : undefined,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return {
jobId,
status: mapFalStatusToVideoStatus(statusResponse.status),
progress: statusResponse.queue_position
? Math.max(0, 100 - statusResponse.queue_position * 10)
: undefined,
return {
jobId,
status: mapFalStatusToVideoStatus(statusResponse.status),
progress:
statusResponse.queue_position != null
? Math.max(0, 100 - statusResponse.queue_position * 10)
: undefined,
🤖 Prompt for AI Agents
In `@packages/typescript/ai-fal/src/adapters/video.ts` around lines 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).

@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!

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.

2 participants