Repository navigation
feat(fal): surface billed cost as result.usage.unitsBilled #723
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
AlemTuzlak
merged 6 commits into
main
from
722-fal-adapters-discard-x-fal-billable-units-surface-actual-billed-cost-as-resultusage
Jun 8, 2026
Merged
Changes from 2 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
388a589
feat(fal): surface billed cost as result.usage.unitsBilled
tombeckenham 5a0b9db
ci: apply automated fixes
autofix-ci[bot] 71f2dcf
feat(examples): show fal unitsBilled in ts-react-media
tombeckenham f243ebf
fix(examples): use a runtime-valid size for grok-imagine-image
tombeckenham a8050f3
refactor(fal): inject fetch instead of overriding the global
tombeckenham cd5b19c
ci: apply automated fixes
autofix-ci[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| --- | ||
| '@tanstack/ai-event-client': minor | ||
| '@tanstack/ai-fal': minor | ||
| '@tanstack/ai': minor | ||
| --- | ||
|
|
||
| Surface fal's billed cost as `result.usage`. The fal adapters now read fal's `x-fal-billable-units` response header off the result fetch and expose the real billed quantity on the generation result, so consumers can compute exact media-generation cost without wrapping `fetch` themselves. | ||
|
|
||
| - `TokenUsage` gains an optional `unitsBilled` field for usage-based (non-token) billing, denominated in the provider's priced unit. | ||
| - `falImage`, `falAudio`, `falVideo`, `falSpeech`, and `falTranscription` populate `result.usage.unitsBilled` when fal reports it. | ||
| - `VideoUrlResult` gains an optional `usage` slot; `getVideoJobStatus` now emits the `video:usage` event and returns `usage` when the completed result reports billed units. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| import type { TokenUsage } from '@tanstack/ai' | ||
|
|
||
| /** | ||
| * Response header fal sets on a queue *result* fetch carrying the real billed | ||
| * quantity for the generation, denominated in the endpoint's priced unit. | ||
| */ | ||
| const FAL_BILLABLE_UNITS_HEADER = 'x-fal-billable-units' | ||
|
|
||
| /** | ||
| * Response header fal sets carrying the request id. The fal client surfaces this | ||
| * same value as `Result.requestId`, so keying captured billable units by it | ||
| * guarantees the adapter's lookup matches the fetch the units came from β no URL | ||
| * parsing or global correlation registry of our own design needed. | ||
| */ | ||
| const FAL_REQUEST_ID_HEADER = 'x-fal-request-id' | ||
|
|
||
| /** | ||
| * Upper bound on retained, not-yet-consumed billable-unit entries. Each | ||
| * successful generation reads-and-deletes its entry (see {@link takeBillableUnits}), | ||
| * so this only guards against an unbounded leak when a result fetch records units | ||
| * but the adapter never resolves (e.g. it throws before reading). When the cap is | ||
| * exceeded the oldest entry is evicted (Map preserves insertion order). | ||
| */ | ||
| const MAX_PENDING_ENTRIES = 256 | ||
|
|
||
| const billableUnitsByRequestId = new Map<string, number>() | ||
|
|
||
| /** | ||
| * Parse the `x-fal-billable-units` header value into a finite number. Returns | ||
| * `undefined` for a missing or non-numeric value so callers can skip attaching | ||
| * usage rather than surfacing `NaN`. | ||
| */ | ||
| export function parseBillableUnits(value: string | null): number | undefined { | ||
| if (value == null || value === '') return undefined | ||
| const parsed = Number(value) | ||
| return Number.isFinite(parsed) ? parsed : undefined | ||
| } | ||
|
|
||
| /** | ||
| * Record the billable units carried by a fal result response, keyed by the | ||
| * request id from the same response. Reading headers does not consume the body, | ||
| * so the response can be returned to the fal client untouched. | ||
| */ | ||
| export function recordBillableUnitsFromResponse(response: Response): void { | ||
| const units = parseBillableUnits( | ||
| response.headers.get(FAL_BILLABLE_UNITS_HEADER), | ||
| ) | ||
| if (units == null) return | ||
| const requestId = response.headers.get(FAL_REQUEST_ID_HEADER) | ||
| if (!requestId) return | ||
| if ( | ||
| billableUnitsByRequestId.size >= MAX_PENDING_ENTRIES && | ||
| !billableUnitsByRequestId.has(requestId) | ||
| ) { | ||
| const oldest = billableUnitsByRequestId.keys().next().value | ||
| if (oldest !== undefined) billableUnitsByRequestId.delete(oldest) | ||
| } | ||
| billableUnitsByRequestId.set(requestId, units) | ||
| } | ||
|
|
||
| /** | ||
| * Read and remove the billable units recorded for a request id. Removing on read | ||
| * keeps the registry from growing across the lifetime of the process. | ||
| */ | ||
| export function takeBillableUnits( | ||
| requestId: string | undefined, | ||
| ): number | undefined { | ||
| if (!requestId) return undefined | ||
| const units = billableUnitsByRequestId.get(requestId) | ||
| if (units !== undefined) billableUnitsByRequestId.delete(requestId) | ||
| return units | ||
| } | ||
|
|
||
| /** | ||
| * Build a {@link TokenUsage} carrying fal's billed quantity. Media generation has | ||
| * no tokens, so the token fields are zero and the real billing signal rides on | ||
| * `unitsBilled` β mirroring how the duration-billed transcription adapters | ||
| * surface `durationSeconds`. Returns `undefined` when no units were captured so | ||
| * callers can omit `usage` entirely. | ||
| */ | ||
| export function buildFalUsage( | ||
| unitsBilled: number | undefined, | ||
| ): TokenUsage | undefined { | ||
| if (unitsBilled == null) return undefined | ||
| return { | ||
| promptTokens: 0, | ||
| completionTokens: 0, | ||
| totalTokens: 0, | ||
| unitsBilled, | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Wrap the global `fetch` so every fal request's response is inspected for the | ||
| * `x-fal-billable-units` header before being returned untouched. Installed as | ||
| * fal's `config.fetch`, which (unlike a global `responseHandler`) is honoured for | ||
| * every request β the fal client forces `resultResponseHandler` per queue | ||
| * operation, clobbering any configured response handler. | ||
| * | ||
| * `globalThis.fetch` is resolved per call (late binding) rather than captured, so | ||
| * a host that swaps the global implementation (test harnesses, instrumented | ||
| * runtimes) is still respected. | ||
| */ | ||
| export function createBillingFetch(): typeof fetch { | ||
| return async (input, init) => { | ||
| const response = await globalThis.fetch(input, init) | ||
| try { | ||
| recordBillableUnitsFromResponse(response) | ||
| } catch { | ||
| // Capturing usage must never break the underlying request. | ||
| } | ||
| return response | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.