Repository navigation
feat(fal): declare endpoint billing components in deploy metadata - #1204
Merged
Merged
Conversation
dimi-fal
force-pushed
the
dimi/billing-estimate-mode
branch
from
October 6, 2026 22:54
fc3f169 to
d199c41
Compare
Endpoints can declare the billable components they report and pair a pure
estimate function:
def estimate_edit(input: EditInput) -> UsageEstimate: ...
@fal.endpoint("/edit", billing=EDIT_BILLING, estimate=estimate_edit)
def edit(self, input: EditInput) -> EditOutput: ...
- build_metadata publishes declarations under metadata["billing_components"]
keyed by path, outside the OpenAPI spec.
- A request carrying x-app-fal-estimate runs the estimate instead of the
handler, after the endpoint's normal body parsing and validation, and
always returns x-fal-billable-units: 0.
- The estimate must be a plain function taking only the endpoint's input
model; this is checked when the app is built. Sync and async estimates and
handlers are supported, and the OpenAPI spec is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drop estimate= and estimate mode: estimates can be served by a separate
app built with the existing App and endpoint APIs, so the SDK only needs to
publish what each endpoint bills.
@fal.endpoint("/edit", billing=EDIT_BILLING)
def edit(self, input: EditInput) -> EditOutput: ...
build_metadata publishes the declarations under
metadata["billing_components"] keyed by path, outside the OpenAPI spec.
Request handling and the OpenAPI spec are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BillingDeclaration.declaration() returned any dict, so a malformed
declaration reached deploy metadata unchecked and failed only when a
pricing formula was later checked against it. The published shape is now a
BillingComponents TypedDict, {version: 1, schema_id, app: <object JSON
Schema>}, and build_metadata() refuses one that does not match.
fal.endpoint() also requires declaration to be callable, and the
declaration is kept off RouteSignature, which collect_routes() hashes, so a
declaration need not be hashable (a plain dataclass works).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dimi-fal
force-pushed
the
dimi/billing-estimate-mode
branch
from
October 13, 2026 10:45
f4e0c80 to
b46cbe2
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b46cbe2. Configure here.
fal.wma.App.build_metadata rebuilt the metadata without calling super(), so endpoint billing declarations were dropped from deploy metadata. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fal.BillingComponents and fal.BillingDeclaration are public, so apps can type declaration() as the TypedDict the SDK publishes and have mypy check version, schema_id and app. endpoint() also refuses a class passed as billing= (billing=Schema instead of Schema(...)), which previously failed only when deploy metadata was built. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build_metadata reuses the OpenAPI document App.build_metadata already built instead of creating a second app instance and building OpenAPI again for asyncapi(). asyncapi() is unchanged for callers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gothos
previously approved these changes
Oct 13, 2026
Gothos
left a comment
Contributor
There was a problem hiding this comment.
I approve the metadata changes in general. I do feel a stricter check for serialization on the metadata might be better (it just checks that is an object at the moment).
Deploy metadata travels as JSON (a protobuf Struct, then JSONB). check_declaration now refuses values JSON cannot carry (Decimal, sets, NaN, infinity) and values it would silently change (tuples, non-string keys), so a bad declaration fails at deploy with its endpoint named. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gothos
approved these changes
Oct 14, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.

Lets an endpoint declare the billable components it reports, so they can be published with the deployed revision without overriding
build_metadatain every app.Changes
billing=onfal.endpointtakes any object (not a class) with adeclaration()method.fal.BillingComponents(the TypedDict published per endpoint) andfal.BillingDeclaration(the protocol) are exported, so a declaration can be typed and mypy checks itsversion,schema_idandapp:App.build_metadata()publishes the declarations undermetadata["billing_components"][path], separately from the OpenAPI spec. Apps without billing declarations publish exactly what they did before.App.get_billing_components()returns the declarations per path.Each declaration is checked before publishing: exactly
version(1),schema_idand an object JSON Schemaapp, and it must survive a JSON round trip unchanged. Deploy metadata travels as a protobufStructand then JSONB, soDecimal, sets, NaN and infinity are refused, as are tuples and non-string keys, which JSON would silently change. The schema's detailed shape and vocabulary (metrics, dimension values) are not checked here: the registry builds them, and the platform validates them when a revision is registered.WMA apps (
fal.wma.App) build their metadata fromApp.build_metadata(), so their declarations are published too;asyncapiis rendered from that same OpenAPI document, with one app instance.Request handling and the OpenAPI spec are unchanged.
billing=is rejected on websocket endpoints: they have no response headers to report components through. Session and websocket billing is tracked in BIL-676.Validation
tests/unit/test_billing_declarations.pyplustest_app.py,test_request_validation_errors.pyand the WMA contract, example and SDK tests: 315 passed, including the six serialization cases. The registry's real declarations (xai-tts, the estimator tests) pass the check.build_metadata, and its publishedasyncapiequalsasyncapi().tests/unit/wma: 554 passed.TestAioiceOrderlyTeardown::test_stock_close_emits_the_retry_racefails only when run with the whole suite (order-dependent); it passes alone and within its file.typed_astenvironment issue), so the pinned mypy 1.3.0 was run with the hook's arguments: no errors in the changed files. A typed caller returningfal.BillingComponentspasses, and a wrongversionis reported.🤖 Generated with Claude Code
Note
Medium Risk
Changes deploy metadata shape and decorator validation, but request paths and OpenAPI stay unchanged; bad declarations fail at metadata build time rather than at runtime billing.
Overview
Endpoints can now declare billable component schemas via a new
billing=argument onfal.endpoint, without touching OpenAPI or per-appbuild_metadataoverrides.Declarations are objects with a
declaration()method returningfal.BillingComponents(version,schema_id, object JSON Schemaapp).App.build_metadata()addsbilling_componentskeyed by path when any endpoint declares billing; apps without billing behave as before.check_declarationrejects malformed or non–JSON-round-trip-safe payloads before publish. Websocket routes cannot usebilling=; the declaration lives on the endpoint function, not the hashed route signature.fal.wma.Appnow builds metadata throughsuper().build_metadata()(so WMA apps pick up billing too) and renders AsyncAPI from the same OpenAPI document via_render_asyncapi.BillingComponentsandBillingDeclarationare exported fromfal.Unit tests cover metadata shape, OpenAPI stability, validation edge cases, and WMA integration.
Reviewed by Cursor Bugbot for commit aae74a6. Bugbot is set up for automated code reviews on this repo. Configure here.