Skip to content

Cost accounting

VectorStep tracks tokens end-to-end (see Team attribution), but tokens aren’t money. This page covers converting them, using either a pricing table you maintain, or live OpenRouter pricing — and how each is persisted so a historical run’s cost never drifts.

VectorStep never bundles a price list. Provider pricing changes often, differs by contract (Azure PTUs, OpenRouter markups, enterprise discounts), and a stale bundled table produces confidently wrong dollar figures — worse than no number at all. There are two ways to get a real, persisted cost: a manual table you maintain, or opting into live OpenRouter pricing — see below.

Every priced component (a step’s primary call, its verifier, its grounding-judge) resolves to exactly one of three tiers, in this order:

Tier When it applies Persisted as Badge colour
Manual A pricing.models entry matches pipeline_steps.cost_manual Blue
Live, exact pricing.live_pricing.enabled, and the component’s provider genuinely is openrouter — a match against the exact model string actually called pipeline_steps.cost_live_exact Green
Live, cross-provider The component ran against a different provider (e.g. anthropic directly), but a live OpenRouter listing for a similar model exists, and pricing.live_pricing.allow_cross_provider is true pipeline_steps.cost_live_cross_provider Amber

If none of the three resolves, the component is unpriced. Resolution is a fact, not a preference: given a component’s provider/model and the configured tables, exactly one tier is correct — there is no per-pipeline or per-step override of which tier gets computed. What a pipeline can override is which of these tiers it trusts enough to gate on — see Budget guardrail below.

All three tiers are computed once, at step-save time, and never recomputed. This is the central change from earlier versions of live pricing: a historical run’s cost badge shows the same number today as it did the day the run happened, regardless of how OpenRouter’s catalog has moved since. pipeline_steps.cost is the sum of whichever tier columns are non-null for that step.

pricing:
currency: USD # display label only — no FX conversion anywhere
models:
# example — verify against your provider's current pricing before relying on this
- match: {provider: anthropic, model: "claude-sonnet-4-6"}
input_per_mtok: 3.00 # currency units per 1,000,000 input tokens
output_per_mtok: 15.00
- match: {provider: anthropic, model: "claude-haiku"} # prefix match
input_per_mtok: 1.00
output_per_mtok: 5.00
- match: {provider: openrouter} # provider-only fallback
input_per_mtok: 2.00
output_per_mtok: 8.00
team_budgets: # optional — advisory only, see below
payments: 500
platform: 1000

Resolution is longest-prefix match, scoped by provider first: filter entries whose match.provider equals the component’s persisted provider (or has no provider constraint), then the entry with the longest match.model that prefixes the component’s model string wins. A provider-only entry (no model key) is the fallback for that provider if no model prefix matched.

pricing:
live_pricing:
enabled: true
refresh_interval_seconds: 3600 # how often the OpenRouter catalog is re-fetched
allow_cross_provider: false # opt-in — see below

With enabled: true, any component with no manual match is looked up against OpenRouter’s public catalog (GET https://openrouter.ai/api/v1/models, no auth required, refreshed on a background schedule). If the component’s own provider genuinely is openrouter, this is a match against the exact model that was actually called — as real a rate as manual pricing, just not typed in by hand — persisted as live, exact (green).

If the component ran against a different provider entirely (e.g. a direct Anthropic call) and OpenRouter happens to list a similar-sounding model, allow_cross_provider: true lets that listing price it anyway, persisted as live, cross-provider (amber). This is a genuinely weaker claim — a different vendor’s rate, possibly a different contract, matched by a fuzzy name comparison that ignores version numbers — which is why it needs its own explicit opt-in on top of live_pricing.enabled, rather than happening automatically the moment live pricing is on.

Both enabled and allow_cross_provider are operator-level settings in config.yaml only — there is no per-pipeline override, because whether a tier gets computed and stored at all is a deployment policy, not an authoring decision. What a pipeline author controls is a separate question: how much of that persisted cost they trust enough to let it stop a run.

A step’s primary, verifier, and grounding-judge calls are priced independently, so they can land in different tiers on the same step — e.g. a primary call priced manually, with a verifier call that fell through to a live cross-provider estimate. Both amounts are persisted in full, on their own columns; nothing is lost or blended. The step’s badge shows the weakest tier present (amber beats green beats blue — a badge must never look more certain than its shakiest contributor) with a marker, and the hover tooltip itemises every component: its role, its cost, its tier, and (for a live tier) the exact catalog id and per-Mtok rates used at the time.

budget.max_usd works exactly like budget.max_tokens: the runner accumulates counted cost per completed step and aborts the run with status=aborted if the total exceeds the ceiling. At least one of max_tokens/max_usd is required if budget: is present at all; both may be set together (whichever trips first aborts the run and names which limit it was).

budget:
max_tokens: 50000
max_usd: 5.00 # abort run if counted cost across all steps exceeds this
count_pricing: live_exact # optional — see below, defaults to live_cross_provider

count_pricing — how much of the persisted cost counts

Section titled “count_pricing — how much of the persisted cost counts”

All three tiers are always persisted in full, regardless of count_pricing — this setting only decides which of that already-real money is allowed to gate the run. It’s a trust threshold, not a checklist: one value naming the least-trusted tier still allowed to contribute, drawn from the same trust order the tiers are already in.

count_pricing Counts toward budget.max_usd
manual Only cost_manual — strictest; nothing but rates you typed in yourself can abort a run
live_exact cost_manual + cost_live_exact
live_cross_provider (default) Everything — cost_manual + cost_live_exact + cost_live_cross_provider

Settable on the pipeline’s budget: block, and overridable per step (None on a step means “inherit the pipeline’s value”):

steps:
- name: risky-step
count_pricing: manual # only a manually-priced overrun should stop THIS step's contribution
...

A mixed-tier step contributes only the portion of its cost at or above the threshold — a step with cost_manual: 0.0121 and cost_live_cross_provider: 0.0022 under count_pricing: live_exact adds 0.0121 to the accumulator and excludes 0.0022. This is the payoff of persisting the tiers separately rather than as one blended number: the author’s options aren’t limited to “count all of this step’s cost” or “count none of it.”

Once, at step-save time, from whichever tier resolves — never recomputed later, for any tier. Pricing-table changes and OpenRouter catalog updates apply going forward only; historical cost reflects what the tokens actually cost (or were estimated to cost) when they were bought. POST /reload/SIGHUP re-reads the manual pricing table for future steps; already-persisted costs never change.

Verifier and grounding-judge tokens are priced too

Section titled “Verifier and grounding-judge tokens are priced too”

A trust feature that hid its own cost would undermine the point of the feature. Verifier tokens and grounding-judge tokens are both resolved against the same three tiers, independently, and folded into the step’s total cost alongside the primary call — see Mixed-tier steps above for what happens when they land on different tiers. A grounding: block that never fired (no trace to check, or the call errored) contributes nothing at all, same as a step with no verifier configured.

pricing.team_budgets sets a per-team currency-units-per-calendar-month (UTC) figure, surfaced on /ui/insights/teams as a spend-vs-budget bar and the vectorstep_team_budget_ratio{team} gauge (month-to-date spend / budget). Month-to-date spend here is the real, persisted total across all three tiers — no separate “approximate spend” figure any more, since live pricing is no longer an ephemeral display-time number.

  • Run detail — per-step and run-total cost, tier-coloured badges with a full itemised tooltip, and a 4-decimal sub-cent display when a step’s cost rounds to $0.00 at the normal 2 decimals. See Run detail.
  • Insights — pipelines/teams/models/providers pages all carry a cost column/card alongside tokens, with a per-source breakdown available. See Insights.
  • /stats/* JSON endpoints — same rollup functions as the UI, so they can’t disagree; the cost object’s by_source breakdown is additive, existing keys (total, unpriced_steps, currency) are unchanged. See Analytics API.
  • Prometheusvectorstep_pipeline_cost_total{pipeline, team, model, provider, source}, a single counter in pricing.currency’s units, with source one of manual/live_exact/live_cross_provider. Use sum without(source) for the total, or filter on source for just the tiers you trust. There is no separate approximate-cost metric to remember not to add in. See Observability.

Display formatting is $12.34 for USD, 12.34 EUR otherwise, always via one template helper.

If you’d rather not trust any price at all — manual or live — gate on raw token count instead. budget.max_tokens works independently of pricing entirely:

budget:
max_tokens: 50000 # abort run if accumulated tokens across all steps exceeds this

This is the right choice when pricing accuracy is itself in question (a brand-new model with no manual rate yet, or live_pricing deliberately left off) but you still want a hard ceiling on a runaway pipeline. max_tokens and max_usd can be set together — whichever trips first aborts the run and names which limit it was.

FX conversion; price history tables (persisted cost is the history); run-blocking team quotas; Gateway-side pricing (VectorStep is the system of record for spend); live pricing from any source other than OpenRouter’s public catalog; per-step cost ceilings (the accumulator is a single run-level running total checked after each step completes — there is no “no single step may exceed $X”); team-level count_pricing (deferred — see Team attribution).