Skip to content

feat(fal): surface billed cost as result.usage.unitsBilled#723

Merged
AlemTuzlak merged 6 commits into
mainfrom
722-fal-adapters-discard-x-fal-billable-units-surface-actual-billed-cost-as-resultusage
Jun 8, 2026
Merged

feat(fal): surface billed cost as result.usage.unitsBilled#723
AlemTuzlak merged 6 commits into
mainfrom
722-fal-adapters-discard-x-fal-billable-units-surface-actual-billed-cost-as-resultusage

Conversation

@tombeckenham

@tombeckenham tombeckenham commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

🎯 Changes

Closes #722.

The fal adapters discarded fal's response headers, so the actual billed cost of a generation was unrecoverable through the SDK — apps had to wrap fetch and scrape x-fal-billable-units keyed by request id. This surfaces that value as result.usage.unitsBilled so consumers can compute exact media-generation cost directly.

Core type (@tanstack/ai-event-client)

  • TokenUsage gains an optional unitsBilled?: number — a bare count of priced units for usage-based (non-token) billing, sitting beside durationSeconds. It's a quantity, not money (distinct from cost / costDetails), and the unit name (megapixels, seconds, …) is provider-defined and looked up via the pricing API, not carried here.

fal adapters (@tanstack/ai-fal)

  • New config.fetch wrapper (utils/billing.ts) reads x-fal-billable-units off every fal response, keyed by x-fal-request-id — the same value the client surfaces as Result.requestId, so the adapter's lookup always matches the fetch the units came from (no URL parsing, no app-side request-id registry). config.fetch is used rather than a global responseHandler because the queue ops override the response handler per request.
  • All five fal media adapters — falImage, falAudio, falVideo, falSpeech, falTranscription — populate result.usage.unitsBilled when fal reports it.

Video activity (@tanstack/ai)

  • VideoUrlResult gains an optional usage slot; threaded into the streaming generation:result chunk, and getVideoJobStatus now emits the (previously defined-but-unemitted) video:usage event and returns usage on completion.

Tests / docs / skill

  • New billing.test.ts + usage-attachment tests across image/audio/video; updated config-shape assertions for the new config.fetch.
  • New E2E route + aimock fal-queue mount + spec proving result.usage.unitsBilled reaches a consumer end-to-end through generateImage.
  • Updated media docs (image/audio/video + docs/config.json updatedAt) and the media-generation agent skill.

Naming note: field is unitsBilled (a count), not billableUnits (which read like it'd hold a unit name). It lives on the canonical TokenUsage rather than in costDetails, since costDetails holds monetary breakdowns and this is a quantity — mirroring how duration-billed transcription surfaces durationSeconds.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Fal media adapters now surface billed units as usage.unitsBilled (image, audio, video, speech, transcription)
    • Video jobs return and emit usage on completion; VideoUrlResult supports optional usage
    • TokenUsage includes an optional unitsBilled field
  • Documentation

    • Added cost-tracking guidance and examples showing how to compute cost from unitsBilled
  • Examples

    • Media example UIs now display billed units when available
  • Tests

    • Added unit and end-to-end tests to validate billed-unit capture and propagation

The fal adapters discarded fal's response headers, so the actual billed
cost of a generation was unrecoverable through the SDK. fal returns the
real billed quantity in the `x-fal-billable-units` header on the result
fetch; this surfaces it as `result.usage.unitsBilled` so consumers can
compute exact media-generation cost without wrapping `fetch` themselves.

- `TokenUsage` gains an optional `unitsBilled` (a bare count of priced
  units, sibling to `durationSeconds`; the unit name itself is provider
  -defined and looked up via the pricing API, not carried here).
- A `config.fetch` wrapper reads `x-fal-billable-units` off every fal
  response, keyed by `x-fal-request-id` (the same value the client
  surfaces as `Result.requestId`), so the adapter's lookup always
  matches the fetch the units came from. `config.fetch` is used rather
  than `responseHandler` because the queue ops clobber a global handler.
- All five fal media adapters (image, audio, video, speech,
  transcription) populate `result.usage.unitsBilled` when fal reports it.
- `VideoUrlResult` gains a `usage` slot; `getVideoJobStatus` now emits
  the `video:usage` event and returns `usage` on completion.

Closes #722

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5527067a-6e7a-44c0-837f-69dd502b504d

📥 Commits

Reviewing files that changed from the base of the PR and between f243ebf and cd5b19c.

📒 Files selected for processing (5)
  • .changeset/fal-billable-units-usage.md
  • packages/ai-fal/src/utils/billing.ts
  • packages/ai-fal/src/utils/client.ts
  • packages/ai-fal/tests/billing.test.ts
  • testing/e2e/src/routes/api.fal-billable-units.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/fal-billable-units-usage.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/ai-fal/src/utils/billing.ts

📝 Walkthrough

Walkthrough

This PR surfaces Fal's billed units as result.usage.unitsBilled across fal adapters, adds billing utilities and a fetch wrapper to capture x-fal-billable-units, wires the wrapper into the fal client, updates adapters and video status to propagate usage, and adds tests, e2e validation, and docs.

Changes

Fal billable units surface and tracking

Layer / File(s) Summary
Type contracts for usage and billing
packages/ai-event-client/src/index.ts, packages/ai/src/types.ts
TokenUsage adds optional unitsBilled?: number; VideoUrlResult adds optional usage?: TokenUsage.
Billing utilities and client fetch wiring
packages/ai-fal/src/utils/billing.ts, packages/ai-fal/src/utils/client.ts, packages/ai-fal/src/utils/index.ts
New billing module captures/parses x-fal-billable-units, provides record/take/build helpers, and createBillingFetch() that records headers without impacting responses; integrated into fal client via fetch config.
Fal adapter implementations with usage
packages/ai-fal/src/adapters/audio.ts, packages/ai-fal/src/adapters/image.ts, packages/ai-fal/src/adapters/speech.ts, packages/ai-fal/src/adapters/transcription.ts, packages/ai-fal/src/adapters/video.ts
Adapters import billing helpers, derive billed units by requestId, build TokenUsage objects, and conditionally include usage in returned results.
Video job status with usage events
packages/ai/src/activities/generateVideo/index.ts
getVideoJobStatus return includes optional usage; emits video:usage when present; streaming generation emits final generation:result chunk with usage when available.
Billing and adapter unit tests
packages/ai-fal/tests/billing.test.ts, packages/ai-fal/tests/audio-adapter.test.ts, packages/ai-fal/tests/image-adapter.test.ts, packages/ai-fal/tests/speech-adapter.test.ts, packages/ai-fal/tests/transcription-adapter.test.ts, packages/ai-fal/tests/utils.test.ts, packages/ai-fal/tests/video-adapter.test.ts
Unit tests for parsing/recording/take semantics, createBillingFetch behavior, and adapter results with/without billed units; client config tests assert fetch is wired.
API and feature documentation
docs/media/audio-generation.md, docs/media/image-generation.md, docs/media/video-generation.md, packages/ai/skills/ai-core/media-generation/SKILL.md, docs/config.json, .changeset/fal-billable-units-usage.md
Docs updated to describe usage.unitsBilled, cost-tracking examples, and timestamps; SKILL.md includes fal cost-tracking section.
End-to-end testing infrastructure and validation
testing/e2e/global-setup.ts, testing/e2e/package.json, testing/e2e/src/routeTree.gen.ts, testing/e2e/src/routes/api.fal-billable-units.ts, testing/e2e/tests/fal-billable-units.spec.ts
Adds fal-queue aimock mount, /api/fal-billable-units route that rewrites fetch to mock, route metadata, and e2e test validating unitsBilled flows into result.usage.

Sequence Diagram

sequenceDiagram
  participant configureFalClient
  participant createBillingFetch
  participant FalAPI
  participant BillingRegistry
  participant FalAdapter
  participant Activity

  configureFalClient->>createBillingFetch: configure fetch via fal.config({ fetch })
  FalAdapter->>createBillingFetch: request result using configured fetch
  createBillingFetch->>FalAPI: network request
  FalAPI-->>createBillingFetch: response with x-fal-request-id + x-fal-billable-units
  createBillingFetch->>BillingRegistry: recordBillableUnitsFromResponse(response)
  createBillingFetch-->>FalAdapter: return response (body intact)
  FalAdapter->>BillingRegistry: takeBillableUnits(requestId)
  BillingRegistry-->>FalAdapter: unitsBilled
  FalAdapter-->>Activity: return result including usage.unitsBilled
Loading

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • AlemTuzlak
  • crutchcorn
  • jherr

Poem

🐰 A rabbit hops through billing streams,
nibbles headers, counts the beams,
units flow from queue to view,
billed and logged, now plain and true,
a tiny hop makes cost accrue.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat(fal): surface billed cost as result.usage.unitsBilled' clearly and concisely describes the main change: surfacing Fal's billed-units as result.usage.unitsBilled for cost computation.
Description check ✅ Passed The PR description follows the template with complete 🎯 Changes section, ✅ Checklist with both items checked, and 🚀 Release Impact. It provides thorough context about the implementation, naming decisions, and testing.
Linked Issues check ✅ Passed The PR fully addresses issue #722 objectives: surfaces x-fal-billable-units as result.usage.unitsBilled, implements config.fetch wrapper for header capture, updates all five fal adapters, adds usage slot to VideoUrlResult, includes tests and E2E validation, and enables consumer cost computation without app-side fetch interceptors.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #722 objectives: billing capture infrastructure, adapter implementations, type definitions, tests, documentation, and E2E validation. The xai/grok-imagine example update is a minor inline clarification tangential to the main feature.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 722-fal-adapters-discard-x-fal-billable-units-surface-actual-billed-cost-as-resultusage

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.

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

3 package(s) bumped directly, 28 bumped as dependents.

🟥 Major bumps

Package Version Reason
@tanstack/ai-event-client 0.5.4 → 1.0.0 Changeset
@tanstack/ai-fal 0.7.23 → 1.0.0 Changeset
@tanstack/ai-anthropic 0.15.1 → 1.0.0 Dependent
@tanstack/ai-code-mode 0.2.5 → 1.0.0 Dependent
@tanstack/ai-code-mode-skills 0.2.5 → 1.0.0 Dependent
@tanstack/ai-elevenlabs 0.2.20 → 1.0.0 Dependent
@tanstack/ai-gemini 0.15.1 → 1.0.0 Dependent
@tanstack/ai-grok 0.11.2 → 1.0.0 Dependent
@tanstack/ai-groq 0.4.2 → 1.0.0 Dependent
@tanstack/ai-isolate-node 0.1.30 → 1.0.0 Dependent
@tanstack/ai-isolate-quickjs 0.1.30 → 1.0.0 Dependent
@tanstack/ai-ollama 0.8.1 → 1.0.0 Dependent
@tanstack/ai-openai 0.14.1 → 1.0.0 Dependent
@tanstack/ai-openrouter 0.13.1 → 1.0.0 Dependent
@tanstack/ai-preact 0.9.4 → 1.0.0 Dependent
@tanstack/ai-react 0.15.4 → 1.0.0 Dependent
@tanstack/ai-react-ui 0.8.6 → 1.0.0 Dependent
@tanstack/ai-solid 0.13.4 → 1.0.0 Dependent
@tanstack/ai-solid-ui 0.7.6 → 1.0.0 Dependent
@tanstack/ai-svelte 0.13.4 → 1.0.0 Dependent
@tanstack/ai-vue 0.13.4 → 1.0.0 Dependent
@tanstack/openai-base 0.8.1 → 1.0.0 Dependent

🟨 Minor bumps

Package Version Reason
@tanstack/ai 0.28.0 → 0.29.0 Changeset

🟩 Patch bumps

Package Version Reason
@tanstack/ai-client 0.16.3 → 0.16.4 Dependent
@tanstack/ai-devtools-core 0.4.8 → 0.4.9 Dependent
@tanstack/ai-isolate-cloudflare 0.2.21 → 0.2.22 Dependent
@tanstack/ai-mcp 0.1.0 → 0.1.1 Dependent
@tanstack/ai-vue-ui 0.2.16 → 0.2.17 Dependent
@tanstack/preact-ai-devtools 0.1.51 → 0.1.52 Dependent
@tanstack/react-ai-devtools 0.2.51 → 0.2.52 Dependent
@tanstack/solid-ai-devtools 0.2.51 → 0.2.52 Dependent

@nx-cloud

nx-cloud Bot commented Jun 8, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 5a0b9db


☁️ Nx Cloud last updated this comment at 2026-06-08 03:38:28 UTC

@nx-cloud

nx-cloud Bot commented Jun 8, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit f243ebf

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 2s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-06-08 04:23:26 UTC

@pkg-pr-new

pkg-pr-new Bot commented Jun 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

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

@tanstack/ai-anthropic

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

@tanstack/ai-client

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

@tanstack/ai-code-mode

npm i https://pkg.pr.new/@tanstack/ai-code-mode@723

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/@tanstack/ai-code-mode-skills@723

@tanstack/ai-devtools-core

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

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/@tanstack/ai-elevenlabs@723

@tanstack/ai-event-client

npm i https://pkg.pr.new/@tanstack/ai-event-client@723

@tanstack/ai-fal

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

@tanstack/ai-gemini

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

@tanstack/ai-grok

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

@tanstack/ai-groq

npm i https://pkg.pr.new/@tanstack/ai-groq@723

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-isolate-cloudflare@723

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/@tanstack/ai-isolate-node@723

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs@723

@tanstack/ai-mcp

npm i https://pkg.pr.new/@tanstack/ai-mcp@723

@tanstack/ai-ollama

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

@tanstack/ai-openai

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

@tanstack/ai-openrouter

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

@tanstack/ai-preact

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

@tanstack/ai-react

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

@tanstack/ai-react-ui

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

@tanstack/ai-solid

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

@tanstack/ai-solid-ui

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

@tanstack/ai-svelte

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

@tanstack/ai-utils

npm i https://pkg.pr.new/@tanstack/ai-utils@723

@tanstack/ai-vue

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

@tanstack/ai-vue-ui

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

@tanstack/openai-base

npm i https://pkg.pr.new/@tanstack/openai-base@723

@tanstack/preact-ai-devtools

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

@tanstack/react-ai-devtools

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

@tanstack/solid-ai-devtools

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

commit: cd5b19c

Surface the new `result.usage.unitsBilled` in the media example so the
billed quantity is visible after a generation — a caption under each
generated image/video ("Billed N fal units"). Verified against the live
fal API: a fal-ai/flux/schnell generation reports unitsBilled: 1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/ai-fal/tests/speech-adapter.test.ts (1)

1-307: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add coverage for result.usage.unitsBilled in Fal speech adapter tests.

packages/ai-fal/src/adapters/speech.ts builds and conditionally returns usage (buildFalUsage(takeBillableUnits(response.requestId))), but packages/ai-fal/tests/speech-adapter.test.ts has no assertions for result.usage.unitsBilled (when billable units are present) and no assertions that usage is omitted when they’re absent.

Add tests mirroring the audio adapter pattern:

  • Seed billable units with seedBillableUnits
  • Assert generateSpeech(...).usage.unitsBilled is present when billable units are reported
  • Assert usage is undefined when billable units are absent
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-fal/tests/speech-adapter.test.ts` around lines 1 - 307, Add two
tests in packages/ai-fal/tests/speech-adapter.test.ts to cover usage: one that
seeds billable units (use seedBillableUnits with the response.requestId returned
by the mocked subscribe call, e.g., 'req-speech-123') then call generateSpeech
with the falSpeech adapter and assert result.usage.unitsBilled equals the seeded
value; and a second test where no billable units are seeded (mockSubscribe
returns a response without billable info) then assert result.usage is undefined.
Ensure tests reference generateSpeech and falSpeech and use the requestId from
createMockSpeechResponse so the adapter’s takeBillableUnits/buildFalUsage path
is exercised.
packages/ai-fal/tests/transcription-adapter.test.ts (1)

1-296: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add missing billing/usage tests for the Fal transcription adapter

  • packages/ai-fal/tests/transcription-adapter.test.ts covers transcription/segments behavior, but has no assertions that the adapter surfaces result.usage.unitsBilled (or omits usage when unbilled).
  • Add tests matching the existing audio-adapter/image-adapter/video-adapter pattern:
    • Seed billable units for the mocked fal requestId via seedBillableUnits
    • Assert generateTranscription(...).usage equals the expected unitsBilled shape when billed
    • Assert generateTranscription(...).usage is undefined when fal does not report billable units
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-fal/tests/transcription-adapter.test.ts` around lines 1 - 296,
Add billing/usage tests to the Fal transcription adapter tests: when mocking
fal.subscribe responses include a requestId and call
seedBillableUnits(requestId, units) before invoking generateTranscription to
assert result.usage equals the expected { unitsBilled: units } shape; also add a
test where no billable units are seeded and assert
generateTranscription(...).usage is undefined. Update tests that use
falTranscription and generateTranscription (and mockSubscribe) to seed billable
units for the mocked requestId in the billed-case test and leave it unseeded for
the unbilled-case test so assertions mirror the patterns used in
audio-adapter/image-adapter/video-adapter tests.
🧹 Nitpick comments (3)
packages/ai-fal/tests/audio-adapter.test.ts (1)

7-16: 💤 Low value

Consider extracting seedBillableUnits to a shared test utility.

The seedBillableUnits helper is duplicated across audio, image, and video adapter tests. Extracting it to a shared test util (e.g., tests/test-utils.ts) would reduce duplication and ensure consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-fal/tests/audio-adapter.test.ts` around lines 7 - 16, Extract the
duplicated seedBillableUnits helper into a shared test utility file (e.g.,
tests/test-utils.ts) so all adapter tests reuse it; move the function that calls
recordBillableUnitsFromResponse(new Response(...)) into that file, export it
(seedBillableUnits), and update audio-adapter.test.ts, image and video adapter
tests to import seedBillableUnits from the shared util instead of declaring
their own copies. Ensure the exported helper accepts (requestId: string, units:
string) and preserves the same header keys ('x-fal-request-id' and
'x-fal-billable-units') so behavior remains identical.
packages/ai-fal/tests/image-adapter.test.ts (1)

7-21: 💤 Low value

Consider extracting seedBillableUnits to a shared test utility.

The seedBillableUnits helper is duplicated across audio, image, and video adapter tests. The JSDoc here is helpful—preserving it when extracting to a shared util (e.g., tests/test-utils.ts) would benefit all tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-fal/tests/image-adapter.test.ts` around lines 7 - 21, Extract the
duplicated seedBillableUnits helper into a shared test utility module (e.g.,
tests/test-utils.ts) and export it for reuse; move the JSDoc block along with
the function to preserve documentation and behavior, keep the implementation
calling recordBillableUnitsFromResponse(new Response(...)) intact, then import
seedBillableUnits from the new utility in the audio, image, and video adapter
tests and remove the duplicate definitions there so all tests reference the
single exported function.
testing/e2e/src/routes/api.fal-billable-units.ts (1)

59-66: 💤 Low value

Error responses return HTTP 200 instead of an error status.

Line 65 returns status: 200 even when generation fails. This makes it impossible to distinguish success from failure at the HTTP level — clients must parse the JSON body to check the ok field.

If this is intentional for test simplicity, consider adding a comment explaining the rationale.

📝 Consider using a 5xx status for server errors
         } catch (error) {
           return new Response(
             JSON.stringify({
               ok: false,
               error: error instanceof Error ? error.message : String(error),
             }),
-            { status: 200, headers: { 'Content-Type': 'application/json' } },
+            { status: 500, headers: { 'Content-Type': 'application/json' } },
           )
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@testing/e2e/src/routes/api.fal-billable-units.ts` around lines 59 - 66, The
catch block currently returns a Response with status: 200 even on errors; update
the error Response in the catch clause that constructs new Response({...}) to
use an appropriate error HTTP status (e.g., 500) instead of 200 so failures are
distinguishable at the HTTP level, and if you intentionally want 200 for tests
add a short comment explaining that choice; adjust only the status property in
the Response created inside the catch (error) block that serializes { ok: false,
error: ... }.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/fal-billable-units-usage.md:
- Line 7: Update the summary sentence to refer to billed "units" (or "billed
units") rather than "billed cost" to match the actual surfaced field;
specifically, change the wording to say that the fal adapters expose the billed
units on the generation result (e.g., result.usage or result.usage.unitsBilled)
by reading the x-fal-billable-units response header so the summary aligns with
the bullets and avoids implying a currency amount.

In `@testing/e2e/src/routes/api.fal-billable-units.ts`:
- Around line 27-42: The current handler patches globalThis.fetch (using
originalFetch, FAL_QUEUE_PREFIX, mockBase) which creates a race when requests
run concurrently; instead remove the global patch and supply a per-request
custom fetch to the adapter — create a local wrapper fetch that rewrites URLs
starting with FAL_QUEUE_PREFIX to mockBase + sliced path and pass that wrapper
into the adapter/config used in this route (rather than mutating
globalThis.fetch), eliminating the try/finally restore and preventing
cross-request interference.

---

Outside diff comments:
In `@packages/ai-fal/tests/speech-adapter.test.ts`:
- Around line 1-307: Add two tests in
packages/ai-fal/tests/speech-adapter.test.ts to cover usage: one that seeds
billable units (use seedBillableUnits with the response.requestId returned by
the mocked subscribe call, e.g., 'req-speech-123') then call generateSpeech with
the falSpeech adapter and assert result.usage.unitsBilled equals the seeded
value; and a second test where no billable units are seeded (mockSubscribe
returns a response without billable info) then assert result.usage is undefined.
Ensure tests reference generateSpeech and falSpeech and use the requestId from
createMockSpeechResponse so the adapter’s takeBillableUnits/buildFalUsage path
is exercised.

In `@packages/ai-fal/tests/transcription-adapter.test.ts`:
- Around line 1-296: Add billing/usage tests to the Fal transcription adapter
tests: when mocking fal.subscribe responses include a requestId and call
seedBillableUnits(requestId, units) before invoking generateTranscription to
assert result.usage equals the expected { unitsBilled: units } shape; also add a
test where no billable units are seeded and assert
generateTranscription(...).usage is undefined. Update tests that use
falTranscription and generateTranscription (and mockSubscribe) to seed billable
units for the mocked requestId in the billed-case test and leave it unseeded for
the unbilled-case test so assertions mirror the patterns used in
audio-adapter/image-adapter/video-adapter tests.

---

Nitpick comments:
In `@packages/ai-fal/tests/audio-adapter.test.ts`:
- Around line 7-16: Extract the duplicated seedBillableUnits helper into a
shared test utility file (e.g., tests/test-utils.ts) so all adapter tests reuse
it; move the function that calls recordBillableUnitsFromResponse(new
Response(...)) into that file, export it (seedBillableUnits), and update
audio-adapter.test.ts, image and video adapter tests to import seedBillableUnits
from the shared util instead of declaring their own copies. Ensure the exported
helper accepts (requestId: string, units: string) and preserves the same header
keys ('x-fal-request-id' and 'x-fal-billable-units') so behavior remains
identical.

In `@packages/ai-fal/tests/image-adapter.test.ts`:
- Around line 7-21: Extract the duplicated seedBillableUnits helper into a
shared test utility module (e.g., tests/test-utils.ts) and export it for reuse;
move the JSDoc block along with the function to preserve documentation and
behavior, keep the implementation calling recordBillableUnitsFromResponse(new
Response(...)) intact, then import seedBillableUnits from the new utility in the
audio, image, and video adapter tests and remove the duplicate definitions there
so all tests reference the single exported function.

In `@testing/e2e/src/routes/api.fal-billable-units.ts`:
- Around line 59-66: The catch block currently returns a Response with status:
200 even on errors; update the error Response in the catch clause that
constructs new Response({...}) to use an appropriate error HTTP status (e.g.,
500) instead of 200 so failures are distinguishable at the HTTP level, and if
you intentionally want 200 for tests add a short comment explaining that choice;
adjust only the status property in the Response created inside the catch (error)
block that serializes { ok: false, error: ... }.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 25de0a13-4f5d-49cb-8492-129b9345c07c

📥 Commits

Reviewing files that changed from the base of the PR and between afb2960 and 5a0b9db.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (29)
  • .changeset/fal-billable-units-usage.md
  • docs/config.json
  • docs/media/audio-generation.md
  • docs/media/image-generation.md
  • docs/media/video-generation.md
  • packages/ai-event-client/src/index.ts
  • packages/ai-fal/src/adapters/audio.ts
  • packages/ai-fal/src/adapters/image.ts
  • packages/ai-fal/src/adapters/speech.ts
  • packages/ai-fal/src/adapters/transcription.ts
  • packages/ai-fal/src/adapters/video.ts
  • packages/ai-fal/src/utils/billing.ts
  • packages/ai-fal/src/utils/client.ts
  • packages/ai-fal/src/utils/index.ts
  • packages/ai-fal/tests/audio-adapter.test.ts
  • packages/ai-fal/tests/billing.test.ts
  • packages/ai-fal/tests/image-adapter.test.ts
  • packages/ai-fal/tests/speech-adapter.test.ts
  • packages/ai-fal/tests/transcription-adapter.test.ts
  • packages/ai-fal/tests/utils.test.ts
  • packages/ai-fal/tests/video-adapter.test.ts
  • packages/ai/skills/ai-core/media-generation/SKILL.md
  • packages/ai/src/activities/generateVideo/index.ts
  • packages/ai/src/types.ts
  • testing/e2e/global-setup.ts
  • testing/e2e/package.json
  • testing/e2e/src/routeTree.gen.ts
  • testing/e2e/src/routes/api.fal-billable-units.ts
  • testing/e2e/tests/fal-billable-units.spec.ts

Comment thread .changeset/fal-billable-units-usage.md Outdated
Comment thread testing/e2e/src/routes/api.fal-billable-units.ts Outdated
fal's generated `size`/`resolution` type for `xai/grok-imagine-image`
offers `16:9_1K` / `16:9_4K`, but the live API rejects those resolutions
("Input should be '1k' or '2k'") — the vendor enum is out of sync with
the API. `'16:9_4K'` therefore type-checked but 422'd at runtime. Pass
`aspect_ratio: '16:9'` via modelOptions instead and let the endpoint
default the resolution; verified against the live fal API.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tombeckenham tombeckenham requested review from a team and AlemTuzlak June 8, 2026 04:12
tombeckenham and others added 2 commits June 8, 2026 14:21
Address CodeRabbit + review feedback: dependency-inject the underlying
fetch rather than mutating globals.

- `createBillingFetch(baseFetch = globalThis.fetch)` now takes the fetch to
  delegate to, and `FalClientConfig` gains an optional `fetch` override that
  `configureFalClient` wraps for usage capture.
- The E2E route passes a per-request redirecting `fetch` via
  `falImage(model, { fetch })` instead of swapping `globalThis.fetch` —
  removing the concurrency race CodeRabbit flagged (no global mutation, no
  try/finally restore).
- `billing.test.ts` injects a fake fetch directly instead of
  `vi.stubGlobal('fetch')`.
- Changeset: reword "billed cost" → "billed units" to match the surfaced
  `usage.unitsBilled` field.

Verified: full `pnpm test:pr` green, fal E2E spec green, and a live
fal-ai/flux/schnell call still reports unitsBilled via the default
(no-override) path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@AlemTuzlak AlemTuzlak merged commit 22c9b42 into main Jun 8, 2026
10 checks passed
@AlemTuzlak AlemTuzlak deleted the 722-fal-adapters-discard-x-fal-billable-units-surface-actual-billed-cost-as-resultusage branch June 8, 2026 07:31
@github-actions github-actions Bot mentioned this pull request Jun 8, 2026
AlemTuzlak pushed a commit that referenced this pull request Jun 8, 2026
…test (#725)

Follow-up to #723 addressing CodeRabbit review feedback.

- Add unit coverage for `result.usage.unitsBilled` on the fal speech and
  transcription adapters (billed + unbilled cases), mirroring the existing
  audio/image/video adapter tests.
- Remove the e2e `fal-billable-units` spec, its `/api/fal-billable-units`
  route, and the hand-rolled `/fal-queue` aimock mount. aimock has no seam to
  stamp the `x-fal-billable-units` response header the feature reads, so any
  e2e test required manipulating `fetch` to redirect fal's hardcoded
  `queue.fal.run` URLs. The billed-units behavior is now covered by unit tests
  across all five fal adapters instead.
- Drop the now-orphaned `@tanstack/ai-fal` dependency from the e2e app and
  regenerate the route tree.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

fal adapters discard x-fal-billable-units — surface actual billed cost as result.usage

2 participants