Cost & model pricing
Every dollar figure in Neens — the Spend measure on a dashboard, the Cost column on the Traces list, the cost delta on a pre-prod comparison — is computed from the token counts your traces already carry, multiplied by a price for that specific model. This page explains where those prices come from, why some models are deliberately shown as unpriced rather than guessed at, and how to enter your own negotiated or self-hosted rates.
At a glance
| What it is | A dated, per-model price table — Neens-shipped list prices plus your own overrides |
| Where it lives | Settings → Model pricing |
| Key API routes | GET/POST /model-prices, PATCH/DELETE /model-prices/{id}, GET /model-prices/observed |
| Who can change it | Company admins (the manage_model_prices permission); everyone with read access can see the table |
| Unit | US dollars per million tokens ($/MTok) — the unit every vendor publishes |
| What it feeds | The spend_usd measure, persona cost dashboards, the trace Cost column, the Agent Map node costs, pre-prod deltas.cost, and the eval CLI’s cost gate rule |
There is no generic fallback rate. If Neens does not have a price for a model, it says so instead of inventing one. See Unpriced is a real state.
How Neens computes cost
Cost is derived, never stored. There is no cost column on a trace waiting to be read — Neens computes the number at query time, every time:
- Your traces record input and output token counts per span (and roll them up per trace), plus the model each model call used. Those come straight from your instrumentation — see Send traces.
- At read time, Neens sums tokens grouped by model, resolves each model against the price table, and multiplies.
So a trace on claude-sonnet-5 with 12,000 input and 3,000 output tokens costs:
(12,000 / 1,000,000) × $3.00 = $0.036 input
(3,000 / 1,000,000) × $15.00 = $0.045 output
-------
$0.081Two consequences worth internalizing:
- Correcting a price corrects history immediately. Nothing was baked in at ingest, so entering a rate today makes every existing trace show the right number (subject to effective dates — a price change does not rewrite spend from before the change).
- One price table, everywhere. The per-trace estimate and the aggregate roll-up resolve through the same table, so a trace’s Cost and the dashboard Spend it contributes to can never disagree.
The surfaces cost feeds
| Surface | What it shows |
|---|---|
spend_usd catalogue measure | Total spend, sliceable by model, org, project, and time — the basis of every cost widget |
| Persona cost dashboards | The Finance cost explorer and the spend tiles on the Executive digest, all built from spend_usd |
| Cost column + trace inspector | Per-trace estimated USD on the Traces list and in the trace drawer |
| Agent Map | Per-node cost, and which node dominates spend across a cohort |
| Pre-prod comparison | Average cost per item for the candidate and baseline, and the deltas.cost between them |
| Eval CLI gate | The cost rule’s max_avg_usd / max_delta / max_delta_pct budgets |
Where prices come from
A price is a row in a table, not a constant in the product: it names a model, carries input,
cached-input, and output rates in $/MTok, and is stamped with an effective date and a
named source. Every price you see in Neens is in one of three states, and the UI labels which:
| State | Badge | What it means |
|---|---|---|
| List price | list · eff 2026-07-01 | A price Neens ships, from a dated published source. Read-only — you override it, you don’t edit it. |
| Your rate | your rate | A price you entered: a negotiated discount, a committed-use rate, a hosted-provider price, or $0 for self-hosted. Wins over the list price. Scoped to the agent it was created under — set it in each agent whose spend it should apply to. |
| Unpriced | unpriced | Neens has no price for this model. Its tokens are counted; its dollars are not. |
How a model resolves to a price
For each model Neens sees, it looks for a price in this order — and within each step, your rate beats the shipped list price. Note that the steps are tried in order: an exact list price is reached before any family-prefix rate, including one of yours, so state your override against the exact model id you run rather than a family prefix:
Exact match
The model id as recorded on the span, lowercased — claude-sonnet-5, gpt-4o, gpt-oss:20b.
Family prefix
If there’s no exact row, Neens falls back to a family row — e.g. claude-opus- matches
claude-opus-4-6-20260315. This is what keeps a newly released dated snapshot priced sanely
instead of dropping to unpriced the day a vendor ships one.
Unpriced
No exact row and no family row ⇒ the model is unpriced. Not $0, not a guess — unpriced.
A model with no name at all (spans that never recorded gen_ai.request.model,
gen_ai.response.model, or llm.model_name) is always unpriced, and appears in the unpriced list
under an empty name. If you see that, the fix is instrumentation, not pricing —
see Send traces.
Unpriced is a real state, not a bug
This is deliberate, and it is the single most important thing to understand about cost in Neens:
- An unpriced model contributes $0 to the total — it is excluded, not estimated.
- Its tokens are still counted and reported separately, so you can see exactly how much traffic is missing a price.
- Every dollar figure that includes unpriced traffic is labelled partial, with the number of unpriced models: ⚠ partial · 2 models unpriced. Its ⓘ names the models and links to Settings → Model pricing — see Fix a spend figure that says partial.
- A single unpriced value renders as —, never
$0.00, so “we don’t know” never masquerades as “it was free”.
Neens will never silently apply a generic rate to a model it does not know. A confidently wrong dollar figure is worse than a visibly missing one: you can’t tell it’s wrong, and you’ll plan against it. If a number is incomplete, Neens says so — and tells you exactly which model to price.
Aggregate responses carry the partial state as machine-readable metadata alongside the value, so dashboards, alerts, and the digest all surface the same caveat:
"pricing": {
"partial": true,
"unpricedModels": ["gpt-4o", ""],
"unpricedInputTokens": 91000,
"unpricedOutputTokens": 12000,
"version": "42:2026-07-26T00:00:00Z"
}Fix a spend figure that says partial
A spend figure reads partial when some traces in the window ran on a model with no rate. This section is the whole fix, end to end: what the label means, how to find out which models are unpriced, and how to price them. It takes about a minute per model, and you do it once per model — not once per dashboard.
A partial total is a floor, not an estimate. The unpriced tokens contribute $0 — they are excluded, never priced at a guess. So real spend is higher than the number on screen, by whatever the unpriced traffic actually cost you. The error only ever runs in that one direction.
Where you’ll see the badge
The badge sits next to the figure it qualifies, so the caveat travels with the number:
| Surface | Where the badge appears |
|---|---|
| Overview | Under the Spend tile, and in the Spend column of the breakdown table below it |
| Dashboard widgets | Next to the value on any currency widget — spend_usd, and cost_per_case, whose ratio is a floor for the same reason |
| Insights | On the spend tile of the insights header |
| Agent Map | In the graph footer next to the cohort cost |
| Model sweeps | On the arm leaderboard and the cost estimate panel |
| Pre-prod comparison | On each run’s average cost |
In dense chrome — a table cell, a graph footer — it renders compactly as partial (2); on a headline tile it renders in full as ⚠ partial · 2 models unpriced. When the backend flags the window as partial without enumerating the models (the Agent Map payload does), it says ⚠ partial · some models unpriced rather than inventing a count.
Read the popover — it names the models
Every badge carries an ⓘ. Hover it to read it, click it to pin it open so you can move the
pointer onto the contents, and Escape or a click outside dismisses it. It’s a real button, so
you can also reach it with Tab and open it from the keyboard.
Pinning matters because the popover is not just an explanation — it contains the link you need:
Partial spend Some traces in this window ran on a model with no rate. Their tokens are excluded from the total rather than priced at a guess, so real spend is higher than shown. Unpriced models:
gpt-4o,llama-3.3-70b-instructSet prices in Settings → Model pricing (a link)
That model list is the answer to “which ones?”, and the link goes straight to
/settings?tab=pricing. A model that recorded no name at all is listed as (unknown model) —
that one is an instrumentation fix, not a
pricing one.
Then price them
Open Settings → Model pricing
Follow the link in the popover, or navigate to Settings → Model pricing yourself. A warning
banner at the top of the tab restates the scale of the gap — 2 models have no rate — spend totals
are partial until you set one — and it is the same count as unpricedCount from the API.
Find the unpriced rows — they’re already at the top
The table lists the models this agent actually ran first, and among those, unpriced ones
first, ordered by token volume. So the model costing you the most accuracy is the top row. Each
unpriced row shows the unpriced badge, the tokens seen in the window (1,204,000 in / 88,000
out), an em dash for every rate, and a Set price button.
Models you haven’t run in the window are marked Not seen in this window and sort below — they’re not what made your figure partial.
Enter the rate
Click Set price. The model id is pre-filled and locked to the row you clicked. Fill in Input ($ / 1M tokens) and Output ($ / 1M tokens) — both required — optionally Cached input, pick a Provider, set Effective from (defaults to today, UTC), and use Notes to record where the number came from. Then Save price.
Two worked examples, both taken off an invoice rather than guessed:
| Model | Input | Cached | Output | Notes |
|---|---|---|---|---|
gpt-4o | 2.50 | 1.25 | 10.00 | list rate, invoice 2026-07 |
llama-3.3-70b-instruct | 0 | (blank) | 0 | self-hosted on our own GPUs |
0 is a rate, not a blank. Enter it for any model where you pay for GPU compute rather than
tokens — a local Ollama model, a vLLM deployment. Leaving the field empty is an error (Input and
output rates are required. Enter 0 for a self-hosted model.); typing 0 stores a real, resolving
price of zero. Rates are per 1M tokens, matching how every vendor publishes them.
Re-check the figure
The badge flips to your rate, the banner’s count drops, and — because cost is computed at
query time, never stored — every existing figure recomputes on the next read, including
historical ones back to the rate’s Effective from date. Reload the dashboard: if that was the
last unpriced model in the window, the partial badge is gone and the total is complete.
Repeat for each name the popover listed. When unpricedCount reaches 0, nothing in the agent
is partial any more.
Not an admin? Changing prices needs the company admin role, and the Model pricing tab isn’t in your Settings at all without it. The badge and its popover are visible to everyone — that’s deliberate, so whoever is reading the number knows it’s incomplete — but the fix is an admin action. Send them this page and the list of model names from the popover.
The same fix, from the API
If you’d rather not click, GET /model-prices/observed gives you the identical list — every model
your spans carried over the window, with priced: false on the ones making figures partial — and
POST /model-prices sets a rate. Both are covered in
Set rates from the API. Watching unpricedCount in CI is the durable
version of this fix: it fails the day someone routes traffic to a model you haven’t priced, instead
of the day someone notices a dashboard looks low.
Which models ship priced
The shipped catalogue is deliberately small: the Anthropic family, and the two gpt-oss local
models at $0. Everything else is unpriced until you say otherwise — Neens only ships a price it
can date, name a source for, and stand behind.
Anthropic — priced
The Anthropic family ships with dated list prices (source anthropic-list-2026-07-01, effective
2026-07-01), in $/MTok:
| Model | Input | Cached input | Output |
|---|---|---|---|
claude-fable-5 | $10.00 | $1.00 | $50.00 |
claude-mythos-5 | $10.00 | $1.00 | $50.00 |
claude-opus-5 | $5.00 | $0.50 | $25.00 |
claude-opus-4-8 | $5.00 | $0.50 | $25.00 |
claude-opus-4-7 | $5.00 | $0.50 | $25.00 |
claude-opus-4-6 | $5.00 | $0.50 | $25.00 |
claude-opus-4-5 | $5.00 | $0.50 | $25.00 |
claude-sonnet-5 | $3.00 | $0.30 | $15.00 |
claude-sonnet-4-6 | $3.00 | $0.30 | $15.00 |
claude-sonnet-4-5 | $3.00 | $0.30 | $15.00 |
claude-haiku-4-5 | $1.00 | $0.10 | $5.00 |
Plus three family fallback rows so an unrecognized dated snapshot still prices sanely:
claude-opus- → $5 / $0.50 / $25, claude-sonnet- → $3 / $0.30 / $15, claude-haiku- →
$1 / $0.10 / $5.
If you buy Anthropic capacity through a committed-use agreement or a reseller, your rate is not the list rate — override it so your spend numbers are yours, not the sticker price.
gpt-oss — priced at $0
Exactly two open-weight ids ship with a price: gpt-oss:20b and gpt-oss:120b, at
$0.00 per million tokens, source self-hosted-zero-cost.
That is not “unknown” — it is a real, deliberate price. These are local-first models: they run on hardware you already pay for, so you’re billed for GPU compute and hours, not for tokens. Attaching a per-token price to them produces a number with no relationship to any invoice you will ever receive.
This is a correction, not a cop-out. Earlier releases priced any unrecognized model at a generic
mid-tier rate. That got self-hosted models badly wrong in the expensive direction — a local
gpt-oss:20b workload could show thousands of dollars of “spend” that nobody was ever billed for.
If your historical self-hosted spend looks like it collapsed, that’s the bug being fixed.
Everything else — unpriced on purpose
Every other model resolves to unpriced out of the box. That covers two groups, for two
closely related reasons.
Third-party hosted models — GPT, Gemini, and friends. gpt-4o, gpt-4o-mini,
gemini-2.5-pro and their siblings ship with no price. We will not state a third-party list price
we cannot date and verify, and we especially will not ship one that quietly drifts out of date
while your finance dashboard keeps reporting it as fact. Third-party pricing also varies by tier,
region, batch mode, and contract — the number you are billed is genuinely not knowable from here.
Open-weight models beyond gpt-oss — Llama, Qwen, Mistral, Mixtral, DeepSeek, Phi, Gemma.
llama-3.3-70b-instruct, mistral-large-latest, deepseek-chat, gemma-3-27b-it, phi-4,
qwen2.5:14b and the rest are unpriced, deliberately — Neens does not blanket them at $0.
The reason is specific: the same model name can be free or expensive, and the model string alone
doesn’t say which. mistral-large-latest might be running on your own GPUs (genuinely $0) or
through Mistral’s La Plateforme, Bedrock, Together, Fireworks, or Groq — where every token is
invoiced. A blanket $0 would silently price real, billed spend at nothing.
An under-report is more dangerous than an over-report. If Neens overstates spend, someone notices and complains. If it understates spend to zero, the number looks fine and nobody investigates — you find out from the invoice. That’s exactly the fabricated-constant bug this release exists to kill, merely inverted, so Neens asks instead of guessing.
The remedy is one action, either way, and both answers are honest:
- Self-hosting Llama on your own GPUs? Set it to
0once in Settings → Model pricing. It’s a real price, it sticks, and you never think about it again. - Paying Together, Bedrock, Groq, or DeepSeek per token? Enter the rate off your invoice, once. Now your dashboards show what you’re actually spending.
Until you do, those models’ tokens are counted but contribute $0, and every spend figure that includes them is labelled partial — so the gap is visible rather than silent. Follow Set your own rate below; it takes about a minute per model.
Set your own rate
Settings → Model pricing is the admin surface. The table lists Model · Provider · Input · Cached · Output · Source, with the models you actually run listed first — including unpriced ones, each with a Set price button.
Admin only. Changing prices requires the company admin role (the manage_model_prices
permission, the same tier as data retention).
Members and viewers don’t see the tab, and the write endpoints return 403. Every change is
recorded in the audit log.
Price a model Neens ships unpriced
Find the model (e.g. gpt-4o) — it shows the unpriced badge — and click Set price. Enter
Input, Output, and optionally Cached input, all in dollars per million tokens,
straight off your provider invoice or contract. Pick an Effective from date (defaults to
today) and save.
The badge flips to your rate, and every spend figure that included that model stops being
partial. For a model you haven’t run yet — pricing ahead of a migration, say — use Add model
price and type the model id yourself.
Override a shipped list price
A row showing list · eff 2026-07-01 is a platform price. It has an Override button, not an
inline edit — clicking it creates your row on top of the list row. Enter your negotiated rate
(e.g. claude-sonnet-5 at $2.40 / $12.00 under a committed-use agreement), add a note like
“committed-use agreement — renewal 2027-01”, and save.
Your rate now wins wherever that model resolves. The list row is still there underneath, unchanged.
Enter $0 for a self-hosted model
0 is a valid, storable price, not “unset”. Use it for any model where you pay for compute
rather than tokens — a local Ollama model, a vLLM deployment, a model on your own GPUs.
This is the step for self-hosted open-weight models: apart from gpt-oss:20b and
gpt-oss:120b, Neens ships no $0 rows, because it can’t tell a locally-served
llama-3.3-70b-instruct from one billed by a hosted provider. Set it to 0 once and the model is
priced correctly from then on — same one-time action as entering a paid rate, and just as honest.
Revert to the list price
On a row showing your rate, click Revert to list price and confirm. Your override is deleted
and the model falls back to whatever it resolves to without it — the shipped list price, or
unpriced if there isn’t one.
Platform list prices are immutable to you. You override them; you never edit them. Attempting
to PATCH or DELETE a platform row returns 409. That keeps the shipped catalogue upgradeable —
when Neens ships a newer dated price list, your overrides are preserved and never clobbered.
Set rates from the API
Every Settings action has a REST equivalent, so rates can live in your IaC repo or be applied from
a CI job. Routes are available bare and under the canonical /api/ prefix; use /api/ for
anything programmatic. Reads need any read-capable credential (a nk_live_ agent key works);
writes need an admin-role credential — an admin’s session, or a nk_live_ agent key created
with the admin role. This is not the operator nk_admin_ control-plane key, which carries no
agent scope. The $NEENS_API_KEY in the examples below is an admin-role agent key.
Which models am I running, and which are unpriced?
GET /model-prices/observed is the “state of the world” call: the distinct models actually seen in
your agent’s spans over a window, joined against the resolved price table.
curl -G https://your-neens-host/api/model-prices/observed \
-H "Authorization: Bearer nk_live_your_key_here" \
--data-urlencode "range=30d"{
"models": [
{
"model": "gpt-oss:20b",
"priced": true,
"matchedBy": "exact",
"provenance": "platform",
"effectiveFrom": "2026-07-01",
"source": "self-hosted-zero-cost",
"inputPerMillion": 0.0,
"cachedInputPerMillion": null,
"outputPerMillion": 0.0,
"inputTokens": 1204000,
"outputTokens": 88000,
"spendUsd": 0.0,
"priceId": "mp-platform-gpt-oss-20b-2026-07-01"
}
],
"unpricedCount": 2,
"version": "42:2026-07-26T00:00:00Z"
}unpricedCount is the number to watch: anything above zero means your spend figures are
partial. A nightly CI check that fails when unpricedCount > 0 is a cheap way to keep a new
model from silently landing outside your cost reporting.
Set a rate
curl -X POST https://your-neens-host/api/model-prices \
-H "Authorization: Bearer $NEENS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"modelId": "gpt-4o",
"provider": "openai",
"displayName": "GPT-4o",
"inputPerMillion": 1.88,
"cachedInputPerMillion": 0.94,
"outputPerMillion": 7.5,
"effectiveFrom": "2026-07-01",
"notes": "committed-use discount"
}'Returns 201 with the created row. Re-running the exact same call is a no-op upsert, not an
error — so re-applying your pricing manifest on every deploy is safe.
Field reference
| Field | Required | Notes |
|---|---|---|
modelId | ✅ | The exact vendor model id, or a family prefix (e.g. claude-opus-). Lowercased server-side. |
inputPerMillion | ✅ | USD per million input tokens. Must be >= 0; 0 is valid and meaningful. |
outputPerMillion | ✅ | USD per million output tokens. Must be >= 0. |
cachedInputPerMillion | USD per million cached-input tokens. Omit to bill cached input at the input rate. | |
provider | anthropic, openai, google, bedrock, meta, mistral, self_hosted, other. | |
displayName | What the table shows. | |
effectiveFrom | ISO date. Defaults to today (UTC). | |
notes | Free text — record the contract, invoice, or ticket the rate came from. |
422 on a negative rate, a blank modelId, or a malformed date.
The rest of the surface
| Call | Does |
|---|---|
GET /model-prices | The resolved table. ?includeHistory=true returns superseded rows too; ?provider= and ?q= filter. The response carries a version fingerprint that changes whenever any price changes. |
PATCH /model-prices/{id} | Edit a rate, effectiveFrom, effectiveTo, displayName, provider, or notes on one of your rows. 409 on a platform row. |
DELETE /model-prices/{id} | Remove your override — the model reverts to the list price, or to unpriced. 204. 409 on a platform row. |
GET /model-prices/observed | The models seen in your spans + their resolved price and token volume. |
Cached spend figures are keyed on the price table’s version, so an edit is reflected on the next
read — you never have to wait out a cache TTL to see a corrected number.
Effective dates and historical accuracy
Every price row carries a window: effectiveFrom (inclusive) and effectiveTo
(exclusive, empty = still current). A trace is costed at the price that was in force when it
ran — not at today’s price.
That means a price change never retroactively rewrites last quarter’s spend. Your Q3 number stays your Q3 number after you renegotiate in Q4.
Worked example
You run claude-sonnet-5 on the shipped list price of $3.00 / $15.00, effective 2026-07-01. On
1 September you sign a committed-use agreement at 20% off, so you post your rate:
curl -X POST https://your-neens-host/api/model-prices \
-H "Authorization: Bearer $NEENS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"modelId": "claude-sonnet-5",
"provider": "anthropic",
"inputPerMillion": 2.4,
"outputPerMillion": 12.0,
"effectiveFrom": "2026-09-01",
"notes": "committed-use agreement, 20% off list"
}'From then on, the same 12,000-in / 3,000-out trace costs:
| Trace ran on | Price applied | Cost |
|---|---|---|
| 2026-08-14 | List, $3.00 / $15.00 (your rate wasn’t in force yet) | $0.081 |
| 2026-09-14 | Your rate, $2.40 / $12.00 | $0.0648 |
A 30-day dashboard spanning 1 September mixes both — each trace priced by its own date. That is the point: the total is what you were actually billed, not a re-projection of today’s rate over history.
When the agreement ends, don’t delete the row — close it. PATCH it with
"effectiveTo": "2027-01-01" and the window becomes [2026-09-01, 2027-01-01): a trace on
31 December 2026 still uses your rate, a trace on 1 January 2027 falls back to whatever comes next.
Your history stays intact.
Cached input
Prompt caching is billed at a different rate from fresh input, so a price row carries a separate cached input rate. Cached tokens are treated as a subset of the input tokens: the cached portion is priced at the cached rate and the remainder at the full input rate. If a row leaves Cached input blank, cached tokens are simply billed at the input rate — a safe over-estimate rather than a silent discount.
The shipped Anthropic rows set cached input at one tenth of the input rate (the cache-read rate) —
$0.50/MTok on claude-opus-5, $0.30 on claude-sonnet-5. When you enter your own rate, fill in
Cached input if your contract prices it separately; leave it blank if it doesn’t.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| A spend figure shows ⚠ partial · N models unpriced | Some traffic ran on a model with no price | Fix a spend figure that says partial — the popover names the models; GET /model-prices/observed lists them all |
| The badge’s popover closes before you can click the link in it | You moved the pointer off the ⓘ | Click the ⓘ to pin the popover open, then click the link; Escape or a click outside closes it |
A trace’s Cost shows — | That trace’s model is unpriced | Set a price for it — the value is missing, not zero |
gpt-oss spend “dropped to zero” | Correct — gpt-oss:20b / gpt-oss:120b are priced at $0 because you pay for compute, not tokens | Nothing to do; if you’re billed per token for them by a hosted provider, override the rate |
| Llama / Mistral / DeepSeek / Qwen show as unpriced | Deliberate — Neens can’t tell a self-hosted run from a hosted, invoiced one from the model name | Enter 0 if you self-host, or your provider’s rate if you don’t — once, in Settings → Model pricing |
| An unpriced model with no name in the list | Spans didn’t record a model attribute | Emit gen_ai.request.model / gen_ai.response.model / llm.model_name — see Send traces |
409 editing a price | You’re editing a shipped list price | Use Override (a POST of your own row) instead |
403 on a write | You’re not a company admin | Ask an admin, or see Members & roles |
| A pre-prod run reports the cost gate as skipped | Neither side had a priced session, so an honest cost delta can’t be computed | Price the candidate’s model; the gate would otherwise be comparing a fabricated number |
Related
- Metrics catalogue — the
spend_usd,tokens_in, andtokens_outmeasures and themodelslice. - Model comparison — the other half of the argument: whether the cheaper model still passes your evals, sliced by model and by agent × model.
- Business KPIs —
cost_per_case, the same spend divided by the cases it handled (and a floor, not a total, while any model is unpriced). - Dashboards — build cost widgets, including the Finance cost explorer.
- Traces & sessions — the per-trace Cost column.
- Pre-prod evaluations — the
costgate rule. - Usage metering — feature-level usage counters, a separate signal from LLM spend.