GuidesCost & model pricing

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 isA dated, per-model price table — Neens-shipped list prices plus your own overrides
Where it livesSettings → Model pricing
Key API routesGET/POST /model-prices, PATCH/DELETE /model-prices/{id}, GET /model-prices/observed
Who can change itCompany admins (the manage_model_prices permission); everyone with read access can see the table
UnitUS dollars per million tokens ($/MTok) — the unit every vendor publishes
What it feedsThe 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:

  1. 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.
  2. 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.081

Two 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

SurfaceWhat it shows
spend_usd catalogue measureTotal spend, sliceable by model, org, project, and time — the basis of every cost widget
Persona cost dashboardsThe Finance cost explorer and the spend tiles on the Executive digest, all built from spend_usd
Cost column + trace inspectorPer-trace estimated USD on the Traces list and in the trace drawer
Agent MapPer-node cost, and which node dominates spend across a cohort
Pre-prod comparisonAverage cost per item for the candidate and baseline, and the deltas.cost between them
Eval CLI gateThe 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:

StateBadgeWhat it means
List pricelist · eff 2026-07-01A price Neens ships, from a dated published source. Read-only — you override it, you don’t edit it.
Your rateyour rateA 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.
UnpricedunpricedNeens 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:

SurfaceWhere the badge appears
OverviewUnder the Spend tile, and in the Spend column of the breakdown table below it
Dashboard widgetsNext to the value on any currency widget — spend_usd, and cost_per_case, whose ratio is a floor for the same reason
InsightsOn the spend tile of the insights header
Agent MapIn the graph footer next to the cohort cost
Model sweepsOn the arm leaderboard and the cost estimate panel
Pre-prod comparisonOn 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-instruct Set 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:

ModelInputCachedOutputNotes
gpt-4o2.501.2510.00list rate, invoice 2026-07
llama-3.3-70b-instruct0(blank)0self-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:

ModelInputCached inputOutput
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 0 once 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
FieldRequiredNotes
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.
cachedInputPerMillionUSD per million cached-input tokens. Omit to bill cached input at the input rate.
provideranthropic, openai, google, bedrock, meta, mistral, self_hosted, other.
displayNameWhat the table shows.
effectiveFromISO date. Defaults to today (UTC).
notesFree 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
CallDoes
GET /model-pricesThe 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/observedThe 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 onPrice appliedCost
2026-08-14List, $3.00 / $15.00 (your rate wasn’t in force yet)$0.081
2026-09-14Your 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

SymptomCauseFix
A spend figure shows ⚠ partial · N models unpricedSome traffic ran on a model with no priceFix 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 itYou 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 unpricedSet 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 tokensNothing to do; if you’re billed per token for them by a hosted provider, override the rate
Llama / Mistral / DeepSeek / Qwen show as unpricedDeliberate — Neens can’t tell a self-hosted run from a hosted, invoiced one from the model nameEnter 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 listSpans didn’t record a model attributeEmit gen_ai.request.model / gen_ai.response.model / llm.model_name — see Send traces
409 editing a priceYou’re editing a shipped list priceUse Override (a POST of your own row) instead
403 on a writeYou’re not a company adminAsk an admin, or see Members & roles
A pre-prod run reports the cost gate as skippedNeither side had a priced session, so an honest cost delta can’t be computedPrice the candidate’s model; the gate would otherwise be comparing a fabricated number
  • Metrics catalogue — the spend_usd, tokens_in, and tokens_out measures and the model slice.
  • 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 cost gate rule.
  • Usage metering — feature-level usage counters, a separate signal from LLM spend.