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.
Three pricing tiers
Section titled “Three pricing tiers”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. 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.
The manual pricing table
Section titled “The manual pricing table”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 cache_read_per_mtok: 0.30 # optional — prompt-cache reads, see below cache_write_per_mtok: 3.75 # optional — prompt-cache writes - 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: 1000Resolution 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.
Prompt-cache tokens
Section titled “Prompt-cache tokens”Anthropic bills prompt-cache tokens apart from ordinary input: a cache read costs
a fraction of the input rate, and a cache write costs a little more than it. The
Gateway marks each agent’s system prompt and tool list as cacheable, so a multi-turn
agent normally reads most of its prompt from the cache after the first call.
Anthropic’s input_tokens figure does not include those tokens, so they are
recorded separately as cache_read_tokens and cache_write_tokens on the
step (shown in the run API; input_tokens keeps meaning “fresh input only”).
To price them, give the model’s entry the two optional rates,
cache_read_per_mtok and cache_write_per_mtok. Take them from your provider’s
pricing page, as with the other rates. They are not derived from input_per_mtok,
because the ratio is provider-specific.
- No cache tokens, no cache rate needed. Models and providers that don’t report cache usage are priced exactly as before.
- Cache tokens but no matching rate means unknown, not zero. The step’s cost is left NULL (and its run’s total flagged incomplete), rather than quietly counting only the input and output tokens.
- Live pricing reads the cache rates from OpenRouter’s catalog where the listing publishes them, and treats a listing without them the same way.
- Writes are priced at the 5-minute rate. The Gateway does not request Anthropic’s 1-hour cache, so every write is a 5-minute write.
- The token budget is unchanged.
budget.max_tokensstill counts input and output tokens only; cache tokens count towardmax_usdthrough their cost.
Live OpenRouter pricing
Section titled “Live OpenRouter pricing”pricing: live_pricing: enabled: true refresh_interval_seconds: 3600 # how often the OpenRouter catalog is re-fetched allow_cross_provider: false # opt-in — see belowWith 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.
Mixed-tier steps
Section titled “Mixed-tier steps”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 guardrail
Section titled “Budget guardrail”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_providerA budget that cannot trip
Section titled “A budget that cannot trip”budget.max_usd only counts spend that has a price at or above the count_pricing
trust level. A gateway step whose agent’s model has no such price adds nothing to
the total, so the budget can never trip on it. POST /pipelines/validate (and the
validate_pipeline MCP tool) returns these as warnings — the pipeline is still
valid — naming each step, agent and model, and the service logs the same warning when
it starts and after each reload. The check asks the Gateway which model each agent
uses, and covers the agent’s model_fallbacks as well: a run that fails over to an
unpriced fallback spends money the budget never sees, so that is flagged separately
(“fallback model …”). If the Gateway is unreachable it reports only the case that needs
no model (no pricing configured at all).
Failed steps count too
Section titled “Failed steps count too”A step that fails after the model answered (a reply that isn’t valid JSON, or one
cut off at max_tokens) still spent money, so its tokens and cost are saved on the
step row and counted toward the budget. With retry:, every attempt that reached
the model is priced against its own model and summed onto the one row; the
Cost breakdown lists them as attempt_1, attempt_2, and so on. A step with
on_failure: policy: continue is counted and the budget checked right after it, so
failures alone can trip max_usd. An attempt that never reached the model (a
timeout or connection error) has no token data, so it adds nothing — and, as above,
a failed attempt on an unpriced model makes the step’s cost unknown rather than a
partial sum.
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.”
When cost is computed
Section titled “When cost is computed”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.
Team budgets
Section titled “Team budgets”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.
Where cost shows up
Section titled “Where cost shows up”- 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’sby_sourcebreakdown is additive, existing keys (total,unpriced_steps,currency) are unchanged. See Analytics API.- Prometheus —
vectorstep_pipeline_cost_total{pipeline, team, model, provider, source}, a single counter inpricing.currency’s units, withsourceone ofmanual/live_exact/live_cross_provider. Usesum without(source)for the total, or filter onsourcefor 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.
Where next: token-only gating
Section titled “Where next: token-only gating”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 thisThis 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.
Out of scope
Section titled “Out of scope”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).