Skip to content
Moku
@moku-labs/ai pages

fal

Provider. Six tasks over one fal key: video, image, prompt-gen, music, sfx and sprite, with one upload cache and one price table.

On this page

What it does

Provider. fal serves six tasks through fal.ai. One key, one HTTP client, one upload cache and one merged price table. It registers ("video", "fal"), ("image", "fal"), ("prompt-gen", "fal"), ("music", "fal"), ("sfx", "fal") and ("sprite", "fal") with the registry in onInit.

TaskContractHow a job runs
videosubmit + pollA job takes 2 to 10 minutes. The runner journals the fal request id before it waits, so a crash or Ctrl-C resumes the same job.
image, musicestimate + execute + submit + pollThe runner drives submit + poll. The one-off facades call execute, which waits in process every pollIntervalMs, at most jobTimeoutMs.
prompt-genestimate + executeOne sync POST to fal's OpenRouter router.
sfx, spriteestimate + executeSubmit and wait in process. A sprite with model none makes no call.

Every handler validates and prices a request before it reads the key, uploads or posts. A bad request bills nothing. fal registers before apimodels and ark, so it is the default provider of video, music and sprite.

Setup

Export FAL_KEY, or put it in .env.local in the working directory. The shell wins over the file. The key is read at request time: estimates, validation and models() never need it.

app.ts
import { createApp } from "@moku-labs/ai";

export const app = createApp({
  pluginConfigs: {
    fal: {
      upload: "storage",
      requestLog: ".moku/fal-requests.jsonl"
    }
  }
});
  1. data-uri sends local files inline instead of uploading them to fal storage.
  2. One JSONL line per billable request. Off by default.

Tasks it serves

Each task's request shape is on its own page. These are the fields fal reads.

shots.moku.yaml
version: 1
name: shots
items:
  - id: s01.key
    task: image
    provider: fal
    input: { prompt: "patisserie counter at night", model: nano-banana-pro, aspect: "9:16" }
    params: { resolution: "2K" }
  - id: s01.clip
    task: video
    provider: fal
    input:
      model: kling-3-pro
      prompt: "slow push-in, she smiles"
      image: { $ref: s01.key }
      seconds: 5
      audio: true
  - id: s01.caption
    task: prompt-gen
    provider: fal
    input: { prompt: "Caption a sunset in five words.", model: anthropic/claude-sonnet-5 }
  - id: s01.score
    task: music
    provider: fal
    input: { prompt: "tense synth pulse", model: elevenlabs-music-v2.5, lengthMs: 60000 }
  - id: coin-pickup
    task: sfx
    provider: fal
    input: { prompt: "coin pickup, bright chime", model: elevenlabs-sfx-v2, durationMs: 600 }
  - id: "btn{nine=12,12,12,12}"
    task: sprite
    provider: fal
    input: { source: { $ref: s01.key }, model: birefnet, size: { width: 128, height: 64 }, padding: 2 }
  • video: model is a fal alias from the table below. image is the first frame, endImage the last frame, refs the image, audio and video references. seconds defaults to 5, aspect to 9:16, resolution to 720p (768P on MiniMax), audio to off. params is merged last into the body.
  • image: model defaults to imageDefaultModel. A request with refs goes to the model's edit endpoint. params.resolution is 1K, 2K, 4K or 1080.
  • prompt-gen: model is an OpenRouter id, default llmDefaultModel. params.reasoning, params.responseSchema, params.images and params.max_tokens are read; tool calling works through messages, tools and toolChoice.
  • music: model is required. lengthMs must fit the model's range. chunks go out as a composition plan on ElevenLabs Music.
  • sfx: model is required. The output is always mp3. Without durationMs the estimate bills the 22 s cap.
  • sprite: source is a $ref or $file. birefnet removes the background on fal; none skips the call when the source is already transparent.

music and sfx have no default model. The runner hashes the input as written, so a default would hide a provider choice in the artifact key.

Models and prices

These are the bundled tables. priceOverrides changes a row or adds one, and a model with no price fails the estimate instead of counting as $0. app.fal.models(task) prints the same list from the running app, with each model's effective price.

Video

input.model is one of these aliases. Any other value fails the item before any upload.

Aliasfal endpointEnd frameImage refsAudio refsVideo refsDuration
seedance-2.5bytedance/seedance-2.5/image-to-videoyesnonenonenone4 to 30
seedance-2.5-refbytedance/seedance-2.5/reference-to-videono2910104 to 30
minimax-h3minimax/h3/image-to-videoyesnonenonenoneinteger
minimax-h3-max-refminimax/h3-max/reference-to-videono8335 to 15
minimax-h3-max-i2vminimax/h3-max/image-to-videoyesnonenonenoneinteger
minimax-h3-refminimax/h3/reference-to-videono8335 to 15
minimax-h3-max-extendminimax/h3-max/extend-videonononenonenone5 to 15, new footage only
kling-3-profal-ai/kling-video/v3/pro/image-to-videoyesnonenonenone3 to 15
kling-o3-reffal-ai/kling-video/o3/pro/reference-to-videono4nonenone3 to 15
kling-o3-v2v-reffal-ai/kling-video/o3/pro/video-to-video/referenceno3none13 to 15
seedance-2.0-minibytedance/seedance-2.0/mini/image-to-videoyesnonenonenonestring
seedance-2.0-mini-refbytedance/seedance-2.0/mini/reference-to-videono833string
seedance-2.0-refbytedance/seedance-2.0/reference-to-videono833string
wan-3.0-refalibaba/wan-3.0/reference-to-videono9552 to 30
veo-3.1-fastfal-ai/veo3.1/fast/image-to-videonononenonenone4, 6 or 8
vidu-q3fal-ai/vidu/q3/image-to-videoyesnonenonenoneinteger
vidu-q3-reffal-ai/vidu/q3/reference-to-video/mixno3nonenoneinteger
gemini-omni-1.1-flashgoogle/gemini-omni-flash/v1.1/image-to-videoyesnonenonenone3 to 10
gemini-omni-1.1-flash-refgoogle/gemini-omni-flash/v1.1/reference-to-videono9none33 to 10

Image refs are the references after the first frame. A ref with an audio/* MIME type is an audio ref, a video/* one is a video ref, every other ref is an image. Too many refs of one kind fail the item before any upload. minimax-h3-max-extend continues a clip: put the source clip in input.image.

Video prices are USD per second of output. The lookup is <alias>@<resolution>, then <alias>+audio when audio is on, then <alias>.

KeyUSD/s
seedance-2.5@480p, seedance-2.5-ref@480p0.2205
seedance-2.5@720p, seedance-2.5-ref@720p0.473
minimax-h3@480P / @768P / @2K / @4K0.05 / 0.06 / 0.13 / 0.16
minimax-h3-ref@480P / @768P / @2K / @4K0.05 / 0.06 / 0.13 / 0.16
minimax-h3-max-ref@480P / @768P / @1080P0.05 / 0.08 / 0.16
minimax-h3-max-i2v@480P / @768P / @1080P0.05 / 0.08 / 0.16
minimax-h3-max-extend@480P / @768P / @1080P / @2K0.05 / 0.08 / 0.16 / 0.32
kling-3-pro / kling-3-pro+audio0.112 / 0.168
kling-o3-ref / kling-o3-ref+audio0.112 / 0.14
kling-o3-v2v-ref0.168
seedance-2.0-mini@480p / @720p, same for seedance-2.0-mini-ref0.0721 / 0.1547
seedance-2.0-ref@480p / @720p / @1080p0.092 / 0.3034 / 0.682
wan-3.0-ref@480p / @720p / @1080p0.05 / 0.10 / 0.20
veo-3.1-fast / veo-3.1-fast+audio / veo-3.1-fast@4k0.10 / 0.15 / 0.35
vidu-q3@360p / @540p / @720p / @1080p, same for vidu-q3-ref0.07 / 0.07 / 0.154 / 0.154
gemini-omni-1.1-flash@360p / @720p / @1080p / @4k, same for gemini-omni-1.1-flash-ref0.03 / 0.10 / 0.15 / 0.30

Cost is seconds × USD/s plus a reference surcharge on two models. minimax-h3-max-ref and minimax-h3-max-extend include 4096 reference tokens and bill 0.02 USD per 1000 above that (<alias>#refTokensIncluded, <alias>#refTokenUsdPer1k). minimax-h3-ref includes 5 images and bills 0.08 USD per extra one (minimax-h3-ref#refImagesIncluded, minimax-h3-ref#refImageUsd). Seedance 2.5 and 2.0 Mini have no bundled 1080p price: add seedance-2.5@1080p to priceOverrides to use it.

Image

AliasText endpointEdit endpointMax refsUSD per image
nano-banana-profal-ai/nano-banana-profal-ai/nano-banana-pro/edit140.15 at 1K and 2K, 0.30 at 4K
nano-banana-2.1fal-ai/nano-banana-2.1fal-ai/nano-banana-2.1/edit140.15 at 1K and 2K, 0.30 at 4K
seedream-4.5-editfal-ai/bytedance/seedream/v4.5/text-to-imagefal-ai/bytedance/seedream/v4.5/edit100.04
gpt-image-2.5 (default)openai/gpt-image-2.5/sunburst/text-to-imageopenai/gpt-image-2.5/sunburst/edit160.05, 0.06 at 2K

Keys are image:<alias>@<resolution>, then image:<alias>. GPT Image rows are the high quality price. params.quality xhigh multiplies the row by 1.78, max by 4. Nano Banana 2.1 has no fal list price: it uses Pro's as an upper bound.

prompt-gen

model is any OpenRouter id, as long as it has a price. USD per million tokens.

OpenRouter idInOut
anthropic/claude-opus-5.5 (default)420
anthropic/claude-sonnet-5210
openai/gpt-6-sol210
openai/gpt-6-astra1050
google/gemini-3.8-flash0.753.75
x-ai/grok-4.726

Keys are llm:<id>#in and llm:<id>#out. Also priced, not listed: anthropic/claude-haiku-4.5 (1 / 5) and google/gemini-2.5-flash (0.3 / 2.5). The actual cost is fal's usage.cost when fal reports it, else the token counts at the table price.

Music, sfx and sprite

TaskAliasEndpointLengthPrice
musicelevenlabs-music-v2.5fal-ai/elevenlabs/music/v2.53 000 to 600 000 ms0.80 USD per started minute
musicstable-audio-2.5fal-ai/stable-audio-25/text-to-audio1 000 to 190 000 ms0.20 USD per generation
sfxelevenlabs-sfx-v2fal-ai/elevenlabs/sound-effects/v2500 to 22 000 ms, optional0.002 USD per started second
spritebirefnetfal-ai/birefnet/v20.002 USD per image, an estimate
spritenoneno call0

Keys are music:<alias>, sfx:<alias> and sprite:<alias>.

Config

pluginConfigs.fal. Flat keys, shallow merge.

OptionTypeDefaultDescription
apiKeyEnvstring"FAL_KEY"Env var with the key, read through ctx.env per request.
queueUrlstring"https://queue.fal.run"Queue base URL. Submit is POST <queueUrl>/<endpoint> for video, image, music, sfx and sprite.
uploadUrlstringfal storage initiate URLhttps://rest.alpha.fal.ai/storage/upload/initiate?storage_type=fal-cdn-v3.
upload"storage" | "data-uri""storage"How local files reach fal. storage falls back to a data URI when the upload fails.
timeoutMsnumber60_000Timeout of one HTTP request: submit, status, result, download, chat POST.
priceOverridesRecord<string, number>{}One table for every task. Video keys are unprefixed, the other tasks carry image:, music:, sfx:, sprite: or llm:. Overrides win.
runUrlstring"https://fal.run"Sync endpoint base. prompt-gen posts to <runUrl>/openrouter/router/openai/v1/chat/completions.
imageDefaultModelstring"gpt-image-2.5"Image model when ImageRequest.model is omitted.
llmDefaultModelstring"anthropic/claude-opus-5.5"prompt-gen model when model is omitted or "default".
pollIntervalMsnumber2000Status-check cadence of the in-process wait of image, music, sfx and sprite execute.
jobTimeoutMsnumber900_000The in-process wait gives up after this with a retryable timeout. The job keeps running on fal.
requestLogstring""JSONL request log path, relative to the working directory. "" is off. Never the key or the prompt text.

API

Mounted at app.fal, reachable with ctx.require(falPlugin). No method makes a network call except upload.

MethodSignatureDescription
info() => FalInfo{ provider: "fal", configured, models }. configured is true when the key is set. models are the video aliases.
models(task: FalTask) => FalModelInfo[]One task's models in catalog order with the effective price: video and image at the default resolution, video with audio off and no refs. An unknown task throws.
upload(file: { path; mimeType }, opts?: { signal? }) => Promise<{ url }>Uploads a local file through fal storage, even with upload: "data-uri". The same bytes and MIME type return the cached URL. Never falls back to a data URI.

FalTask is "video" | "image" | "prompt-gen" | "music" | "sfx" | "sprite". The price shape narrows on "inputPerM" in info.price.

prices.ts
import { app } from "./app";

for (const model of app.fal.models("video")) {
  if ("inputPerM" in model.price) continue;
  console.log(model.id, model.price.usd, "per", model.price.per);
}

console.log(app.fal.info().configured);
  1. prompt-gen prices are per million tokens. Every other task prices usd per second, image, minute or generation.
  2. false until FAL_KEY is set. Nothing above needed it.

upload returns a URL fal can read. Use it to host a portrait before registering it with ark:

register.ts
import { app } from "./app";

const image = { path: "faces/mira.png", mimeType: "image/png", hash: "a".repeat(64) };
const { url } = await app.fal.upload(image);
const record = await app.asset.register({ image, url }, { provider: "ark" });
console.log(record.assetId);

Errors

The classes are FalErrors.RetryableProviderError, FalErrors.TerminalProviderError and FalErrors.FlaggedProviderError. Messages start with [ai] and never contain the key or the prompt.

ConditionResult
Caller abortRethrown unchanged. Uploads stop; a queue POST already sent runs to the end, so a billed job always returns its id.
Request timeout or network failureRetryable, kind: "timeout" or "network".
429Retryable, Retry-After honored.
5xxRetryable.
422 content policy, or fal text matching sensitive, likeness, nsfw, moderationFlagged, never re-queued.
Other 4xxTerminal.
Unknown model, missing image, too many refs, end frame on a model without one, missing price, bad paramsTerminal 400 before the key is read or anything is uploaded.
Key not set at poll time, or the status call answers 401 or 403Plain error. The runner marks the job expired, so the next run adopts the same job instead of paying again.
Result or download of a COMPLETED job answers 400 or 422failed, terminal: fal's verdict on the job.
Result or download fails with another 4xxRetryable 503: the clip exists and is paid for, polling goes on.
Image or music execute passes jobTimeoutMsRetryable timeout. The job stays adoptable through poll.
prompt-gen 401 or 403PromptGenUnavailableError, reason auth: promptGen moves to its next fallback provider.
prompt-gen 402 or 429PromptGenUnavailableError, reason limit.
prompt-gen tool-call arguments that are not JSONToolArgumentsError, never retried.
sfx result is not mp3Terminal 415, never returned.
An image, endImage or ref that $refs an asset itemTerminal 400: fal cannot use registered assets. Use provider ark.
input.fromDraftTerminal: fal takes no draft render. Use provider ark.

Events

Emits

None.

Listens to

None.

Depends on

  • A refused face. fal's Seedance endpoints refuse a real face and have no asset field. For real people use apimodels or ark.
  • Video refs are not measured. The plugin does not read clip lengths. Keep video refs within the model's combined seconds yourself; fal answers 422 otherwise.
  • The upload cache lives in the process. The same bytes upload once across items, attempts and runs. app.stop() keeps it; a new process uploads again.
  • No 1080p for Seedance 2.5. The bundled table has 480p and 720p only. Add seedance-2.5@1080p to priceOverrides or the estimate fails.
  • wan-3.0-ref bills its reference videos too. The estimate counts the output seconds only.
  • Video shots: keyframe, clip and score in one build file.
  • video plugin: the request shape and the submit + poll contract.
  • apimodels plugin: the same Seedance aliases, with real faces.
  • ark plugin: Seedance straight from ByteDance, drafts and assets.