Summary
aimock's built-in fal dispatcher serves queue-result (and queue-status) responses without the x-fal-request-id and x-fal-billable-units headers. As a result, the official @tanstack/ai-fal adapter cannot surface result.usage.unitsBilled when replaying against aimock, so any cost/billing accounting that depends on it silently reports zero in tests.
@tanstack/ai-fal ≥ 0.8.x captures billing by installing a config.fetch wrapper that reads two headers off every fal response and stashes the billed quantity keyed by request id:
const FAL_BILLABLE_UNITS_HEADER = "x-fal-billable-units";
const FAL_REQUEST_ID_HEADER = "x-fal-request-id";
export function recordBillableUnitsFromResponse(response) {
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; // <-- bails without the request-id header
billableUnitsByRequestId.set(requestId, units);
}
It later does takeBillableUnits(result.requestId) (the request_id aimock already returns in the queue submit body) and surfaces it as usage.n → result.usage.unitsBilled. Real fal sets both headers on the result fetch; aimock sets neither.
Where
src/fal.ts — the queue-result / queue-status cases call writeJson(...), which only sets Content-Type:
function writeJson(req, res, status, payload, pathname, journal) {
journal.add(/* ... */);
res.writeHead(status, { "Content-Type": "application/json" }); // no x-fal-request-id, no x-fal-billable-units
res.end(JSON.stringify(payload));
}
(Line numbers from the published 1.24.1 build: fal.js:781; the queue-result case writes the completed result a little above it.)
Impact
- The
x-fal-request-id omission is arguably a plain correctness gap — real fal always sets it on queue responses, and the @tanstack/ai-fal adapter (and likely others) rely on it to correlate billing with the originating request.
- Without
x-fal-billable-units, there is no way to author a fixture that exercises a consumer's cost/usage accounting path; unitsBilled is always undefined on replay.
Proposed fix
- Always emit
x-fal-request-id: job.requestId on queue-status and queue-result responses (matches real fal; prerequisite for any units to attach).
- Add an optional fixture field (e.g.
response.billableUnits) that, when present, is emitted as the x-fal-billable-units header on the completed queue-result response. Omitting it preserves today's behavior.
That keeps existing fixtures working while letting fixtures opt into a billed quantity so consumers can assert their cost path end-to-end.
Note
I'll raise a PR shortly
Summary
aimock's built-in fal dispatcher serves
queue-result(andqueue-status) responses without thex-fal-request-idandx-fal-billable-unitsheaders. As a result, the official@tanstack/ai-faladapter cannot surfaceresult.usage.unitsBilledwhen replaying against aimock, so any cost/billing accounting that depends on it silently reports zero in tests.@tanstack/ai-fal≥ 0.8.x captures billing by installing aconfig.fetchwrapper that reads two headers off every fal response and stashes the billed quantity keyed by request id:It later does
takeBillableUnits(result.requestId)(therequest_idaimock already returns in the queue submit body) and surfaces it asusage.n→result.usage.unitsBilled. Real fal sets both headers on the result fetch; aimock sets neither.Where
src/fal.ts— thequeue-result/queue-statuscases callwriteJson(...), which only setsContent-Type:(Line numbers from the published
1.24.1build:fal.js:781; the queue-result case writes the completed result a little above it.)Impact
x-fal-request-idomission is arguably a plain correctness gap — real fal always sets it on queue responses, and the@tanstack/ai-faladapter (and likely others) rely on it to correlate billing with the originating request.x-fal-billable-units, there is no way to author a fixture that exercises a consumer's cost/usage accounting path;unitsBilledis alwaysundefinedon replay.Proposed fix
x-fal-request-id: job.requestIdonqueue-statusandqueue-resultresponses (matches real fal; prerequisite for any units to attach).response.billableUnits) that, when present, is emitted as thex-fal-billable-unitsheader on the completedqueue-resultresponse. Omitting it preserves today's behavior.That keeps existing fixtures working while letting fixtures opt into a billed quantity so consumers can assert their cost path end-to-end.
Note
I'll raise a PR shortly