Skip to content

feat: add fal.ai as a first-class provider (record + replay)#153

Merged
jpr5 merged 1 commit intoCopilotKit:mainfrom
openstory-so:152-add-fal-ai-as-a-first-class-provider-record-replay
May 5, 2026
Merged

feat: add fal.ai as a first-class provider (record + replay)#153
jpr5 merged 1 commit intoCopilotKit:mainfrom
openstory-so:152-add-fal-ai-as-a-first-class-provider-record-replay

Conversation

@tombeckenham
Copy link
Copy Markdown
Contributor

Summary

  • Adds a general fal.ai handler (src/fal.ts) supporting arbitrary JSON request/response payloads — image, video, motion, music, etc. — alongside the existing audio-only src/fal-audio.ts.
  • Routes by x-fal-target-host header to mirror the @fal-ai/client server-side requestMiddleware convention (since proxyUrl is browser-only). Falls through to the legacy /fal/queue/... and /fal/run/... paths for back-compat.
  • Adds mock.onFalQueue(/model/, payload) (primary) and mock.onFalRun(/model/, payload) (sync alias) to LLMock. Queue submit auto-mints a request_id and returns the standard envelope; status/result lookups go through a per-testId TTL+bounded FalQueueStateMap (mirrors the FalJobMap pattern from fal-audio.ts and the VideoStateMap from video.ts).
  • Adds a RawJSONResponse ({ json: unknown }) variant to FixtureResponse so the recorder can save fal payloads verbatim instead of mis-classifying them as ImageResponse / VideoResponse.

Closes #152.

Surface

Method Path x-fal-target-host Behaviour
POST /fal/{owner}/{model} queue.fal.run queue submit (auto-mints request_id, returns envelope)
GET /fal/{owner}/{model}/requests/{id}/status queue.fal.run queue status (COMPLETED)
GET /fal/{owner}/{model}/requests/{id} queue.fal.run queue result (the matched JSON payload)
PUT /fal/{owner}/{model}/requests/{id}/cancel queue.fal.run ALREADY_COMPLETED
POST /fal/{owner}/{model} fal.run sync run (returns JSON directly, no envelope)
POST /fal/storage/upload/initiate rest.fal.ai / rest.alpha.fal.ai synthesised upload envelope stub
legacy /fal/queue/submit/{model}, /fal/queue/requests/{id}/..., /fal/run/{model} (none) unchanged — fal-audio.ts

Usage

const mock = await LLMock.create({ port: 4010 });
mock.onFalQueue(/flux/, { images: [{ url: 'https://example.com/cat.png' }] });
mock.onFalQueue(/kling/, { video: { url: 'https://example.com/v.mp4' } });

// In your app:
fal.config({
  requestMiddleware: async (req) => {
    const original = new URL(req.url);
    if (!FAL_HOSTS.has(original.hostname)) return req;
    const rewritten = new URL('http://localhost:4010/fal');
    rewritten.pathname += original.pathname;
    rewritten.search = original.search;
    return {
      ...req,
      url: rewritten.toString(),
      headers: { ...req.headers, 'x-fal-target-host': original.hostname },
    };
  },
});

await fal.queue.submit('fal-ai/flux/dev', { input: { prompt: 'a cat' } });
// → returns { request_id, status_url, response_url, cancel_url, queue_position: 0 }

In record mode (record.providers.fal: 'https://queue.fal.run', or omit and let the header drive it), unmatched requests proxy through to fal.ai and are saved as endpoint: "fal" fixtures with the verbatim JSON payload.

Test plan

  • pnpm test — 2751 passed, 36 skipped, 78 test files
  • pnpm run lint, pnpm run format:check (the only formatting warning is in pre-existing .claude/settings.local.json)
  • New src/__tests__/fal.test.ts (12 tests): queue lifecycle, host-mirror routing, sync run, cancel, error fixtures, storage stub, testId isolation, legacy back-compat, record + replay
  • Existing src/__tests__/fal-audio.test.ts still green
  • Existing src/__tests__/recorder.test.ts still green
  • Smoke test against real @fal-ai/client in a downstream app (TanStack Start)

Notes

  • recorder.ts already tees SSE streams progressively (added in a prior commit), so the streaming-relay concern raised in the issue's first comment is no longer a blocker for fal.
  • The new helpers expose onFalQueue as the primary entry point and onFalRun as a thin alias, since fal.queue.* is how clients actually call fal in modern code.

🤖 Generated with Claude Code

Adds a general fal.ai handler that supports arbitrary JSON
request/response payloads (image, video, motion, music, etc.) — not
just audio. Routes by `x-fal-target-host` header to mirror the
@fal-ai/client server-side requestMiddleware convention, and falls
through to the existing /fal/queue/... and /fal/run/... audio paths
for back-compat.

Closes CopilotKit#152.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 5, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@copilotkit/aimock@153

commit: 05baea0

@jpr5
Copy link
Copy Markdown
Contributor

jpr5 commented May 5, 2026

Great work on this — clean routing design, solid test coverage, and the RawJSONResponse type is the right call for preserving fal's arbitrary payloads.

One thing we caught during review: validateFixtures() in fixture-loader.ts doesn't include isJSONResponse in its recognition chain, so recorded fal fixtures fail validation on reload from disk (breaking the record→restart→replay cycle). We'll push a fix for that on main after merge.

Merging now — thanks!

@jpr5 jpr5 marked this pull request as ready for review May 5, 2026 00:21
@jpr5 jpr5 merged commit 8fd6337 into CopilotKit:main May 5, 2026
23 checks passed
@tombeckenham
Copy link
Copy Markdown
Contributor Author

Cool. That was fast!

@jpr5
Copy link
Copy Markdown
Contributor

jpr5 commented May 5, 2026

I live to serve! 🙏

@jpr5
Copy link
Copy Markdown
Contributor

jpr5 commented May 5, 2026

Shipped as 1.18.0 FYI.

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.

Add fal.ai as a first-class provider (record + replay)

2 participants