Skip to content

fal queue responses omit x-fal-request-id / x-fal-billable-units, so @tanstack/ai-fal can't surface unitsBilled on replay #269

Description

@tombeckenham

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.nresult.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

  1. Always emit x-fal-request-id: job.requestId on queue-status and queue-result responses (matches real fal; prerequisite for any units to attach).
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions