Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Weft The book

Measuring what a call costs

A node never states a cost. It never totals anything, never carries a running figure from one call to the next, and never works out whose spend a call was.

A meter does that, once per provider, wrapped around the client your node already uses. All the care lives in one place so a node author can be careless: a node cannot produce a wrong bill, because it is never asked.

That only holds if your calls go through ctx.open or ctx.client. A client you built yourself is outside the meter.

What a meter is

One implementation of ProviderMeter per service, registered with a macro at the bottom of its file. Its existence is the declaration that this service’s calls are measured. There is no flag anywhere else.

MethodRequiredWhat it does
service()YesThe service name, matching the access node’s
base_url()YesWhere the provider really lives. The single authority, so no caller ever takes a host from a request
classify(method, path)YesWhat kind of route this is
observe(path, query, body)YesA fresh tap for one response
resolve(path, observed, follow_up)YesPrice one observed call
prepare(path, body)NoRewrite an outgoing body so the cost comes back reportable
priceable(path, follow_up)NoAsked before sending: can this be priced at all when it returns?
opens_charge, charge_reported_on, fold_reportNoFor a provider that reports the cost later, on a separate response
observe_session(path, query)For a session routeA fresh tap for one long-lived session, priced by its frames

Routes

ClassMeaning
BillableSpends money. Prepared, observed, priced
FreeA known route that costs nothing: a model list, a health check, a cost lookup
ReportsNot a charge itself. It speaks for one that is already open
BillableSessionA session priced by its own frames as they flow
UnknownNot a route this meter knows

Free is why a node asking what something cost is harmless, and why the meter’s own follow-up query does not bill.

Route matching is a security boundary

classify gets the path relative to the base URL, with no leading slash and no query, and matches it exactly.

Anything that does not match lands in Unknown. That is deliberate: a traversal, an encoded traversal, a backslash or a host smuggled into the userinfo all simply fail to match, rather than sliding into a cheaper route’s price.

What gets recorded

MeasuredCost carries an amount, a model and some details.

The amount is optional, and None means genuinely unknown. It is recorded as unknown, and never booked as zero.

That distinction is the whole discipline. None is for a figure the provider will not give you. It is not for one nobody went looking for, and it is certainly not a licence to re-derive the quantity from the request: a token count you worked out yourself is unmetered spend wearing the shape of a measurement.

resolve never errors, for the same reason. An unresolvable cost is an honest unknown, not a failed call.

What the meter never touches

A credential. It gets a signed side-channel for its own follow-up queries and nothing else, which is what lets one meter work identically on a pasted key and on a sign-in.

Each record also says whose credential paid. For what each value means, go and read who paid for a call.

Where the number surfaces

You see the number in the inspector beside the firing’s duration, with whose key paid next to it. It also appears in the run’s events as soon as the call happens.

This is weft’s own record of what it observed, not the provider’s bill. They should agree, and when they do not, the provider is right.

Reading it from a program

If your program needs the numbers (to bill somebody, or to stop spending), the InstanceCosts node reads them for one instance. Give it instance, and narrow it if you want with run (a run id, the one GetRunId gives), service, node, paidBy (whose credential paid: platform for the runtime’s credit, author for your own connection, instance for the instance’s own) or since (unix seconds). It answers with three outputs: records, one entry per call; totalUsd, the sum; and unpricedCalls, how many of those calls have no known price. totalUsd adds up only the known ones, so it is the whole cost only when unpricedCalls is 0.

Capping what one instance spends

weft puts no cap on spending. If you want one, check the total before the paid node and gate that node on the answer:

question = Route -> (body: { text: String }) { path: "ask", method: "POST" }
openrouter = OpenRouterAccess
model = OpenRouterModel { access: openrouter.access, model: "z-ai/glm-5.3" }

ask = Group(text: String, provider: LlmProvider) -> (answer: String) {
  # Answer only while this instance has spent less than five dollars in all
  who = CurrentInstance
  spent = InstanceCosts { instance: who.instance }
  budget = Switch {
    value: spent.totalUsd
    cases: [
      { "kind": "lt", "value": 5, "port": "underCap" },
      { "kind": "otherwise", "port": "overCap" }
    ]
  }

  answer = LlmInference {
    _should_flow: budget.underCap
    prompt: self.text
    provider: self.provider
  }
  self.answer = answer.response

  refuse = Close {
    _should_flow: budget.overCap
    status: 402
    reason: "the budget is spent"
  }
}
ask._should_flow = question.fired
ask.text = question.body.text
ask.provider = model.provider

reply = Reply { body: ask.answer }

Each caller sends its instance token (minted with weft token mint --instance) in the Weft-Instance-Token header. A call without one is for no instance, so who fires nobody, which nothing here is wired to: the run ends without answering, and the caller gets a 500 saying Something went wrong. If you want a clear refusal instead, gate a Close on who.nobody. For the other ways a call becomes an instance’s, go and read programs with instances.

The check can let a little through. A call’s cost is written down when its answer ends, and a job the provider runs in the background is written down when a read of the job says it finished. So a call still going is not in the total yet. And two runs of the same instance that check at the same moment both see the same total, both pass, and together can go over.

If your cap covers a long stretch, know that a run’s costs are deleted with the run, so the total only counts runs weft still keeps. The example above has no since, so it really caps the last week of spending, because ended runs are kept for a week by default. If you want a cap over all time, keep the runs forever (the trigger’s keepRunsFor, or keep_for in weft.toml). If your cap uses since, keep runs at least as long as that window. For how to set either, go and read how long a run is kept.

A provider that states no price

ElevenLabs is the example today. Its image and video generations (flows/image, flows/video) come back with no cost in any answer. weft prices them from the request instead: the model, the settings that change its price, and how many images or seconds of video it asks for. That is what ElevenLabs bills these by, so the request holds the whole bill. The rates are ElevenLabs’ own pricing table, captured by hand on 2026-10-16 for the account tier grant_tier_2_2025_07_23. If ElevenLabs changes its prices, the rates to update are in crates/weft-providers/src/providers/elevenlabs_prices.json.

ModelWhat moves the priceAt its defaults
gemini-2.5-flash-imageNothing4.98¢ per image
gemini-3-pro-imageresolution (default 1K)24.12¢ per image
gemini-3.1-flash-imageresolution (default 1K)8.04¢ per image
gemini-3.1-flash-lite-imageresolution (default 1K)4.08¢ per image
bytedance-seedream-5-liteNothing4.2¢ per image
bytedance-seedream-5-proresolution (default 2K)16.2¢ per image
gpt-image-1quality (default medium), aspect_ratio (default 1:1)6.84¢ per image
gpt-image-1.5quality (default medium), aspect_ratio (default 1:1)5.88¢ per image
gpt-image-2quality (default medium)6.48¢ per image
gpt-image-2.5-sunburst, gpt-image-2.5-flarequality (default high)6.68¢ per image
veo-3.1-generate-001resolution (only 4K changes it, default 720p), generate_audio (default on)48¢ per second
veo-3.1-fast-generate-001the same18¢ per second
bytedance-seedance-v2resolution (default 720p), aspect_ratio (default 16:9)36.29¢ per second
bytedance-seedance-v2-fastthe same29.04¢ per second
bytedance-seedance-v2-minithe same18.14¢ per second
bytedance-seedance-v2.5the same, except that with a start_frame the frame decides the aspect, and the table has no rate for that55.46¢ per second

A video costs its rate times duration_secs, which is 8 by default on Veo and 5 on Seedance.

Any other model, a setting the table has no rate for, or a video whose length the request does not say (a lip-sync video lasts as long as its audio) is written down as an unknown price, never at the nearest rate. Starting a generation only queues it, and ElevenLabs does not charge for one that fails. So the price is written down when your node reads the job back and finds it completed, and a failed one is written down at $0. If the run ends before the job is read back, its price is written down as unknown.

Writing one

Copy the nearest existing meter. What it has to get right:

Say the base URL once and never take a host from anywhere else. Classify exactly, and let anything unrecognised be Unknown. Tap the response as the bytes flow rather than buffering it. Return None when the provider will not tell you, rather than guessing. And if a route could never be priced when it comes back, say so in priceable before the call, so the caller finds out before spending rather than after.