Cost & context computation
How cost and context-window numbers are computed across harness providers, and how to read the costSource / contextFormula badges in the UI.
The swarm tracks two related but separate numbers for every model run:
- Cost (USD). Each adapter writes one
session_costsrow per CLI invocation. The API may recompute it from the seeded pricing table. - Context-window usage. Each adapter emits
context_usageevents; the API persists snapshots and updates aggregate columns onagent_tasks.
This page is the single source of truth for how both numbers are produced.
New session_costs rows keep two USD values:
harnessCostUsdis the adapter-reported number. It is useful for comparison, but advisory: adapters can have stale local rates or incomplete usage.totalCostUsdis the API's canonical stored total. For priced rows, the API recomputes it from the active server-side pricing table at the row timestamp; for the othercostSourcepaths below, it intentionally equals the harness report.
How cost is computed
Cost flows through three layers, each annotated with the path's costSource enum value (the dashboard renders this as a small badge next to every cost).
1. Adapter (worker-local)
Every adapter emits a CostData event with its advisory local total, token breakdowns (inputTokens, cacheReadTokens, cacheWriteTokens, outputTokens, reasoningOutputTokens, thinkingTokens), and a provider tag. The dollar value comes from whatever the harness reports — Claude's stream-json carries it directly; Codex doesn't, so the adapter computes locally via computeCodexCostUsd (src/providers/codex-models.ts); pi-ai self-reports stats.cost; etc.
The adapter writes via POST /api/session-costs with the provider field set.
2. API recompute
When the API receives a POST /api/session-costs with a provider tag, it does a synchronous lookup against the pricing table for (provider, model, token_class) at the row's createdAt. Three outcomes:
| Outcome | costSource | Stored totalCostUsd |
|---|---|---|
| A tagged provider/model has the required pricing rows | 'pricing-table' | Canonical server recompute |
No provider tag supplied (legacy caller) | 'harness' | Adapter report |
| A tag was supplied but pricing cannot be completed | 'unpriced' | Adapter report |
harnessCostUsd always preserves the adapter's submitted totalCostUsd, even
when the server replaces totalCostUsd with the recomputed value.
Token classes, cache TTLs, and input semantics
Cached reads and cache creation use their own pricing token classes. For
Anthropic-billed models, cache_write is the 5-minute creation class (1.25×
the base input rate) and cache_write_1h is the one-hour class (2× the base
input rate). Claude reports both TTL totals. When a modelBreakdown entry
does not have its own TTL split, the recompute distributes its writes using the
session's 5m/1h ratio; this is an approximation for sidechains, not a claim
that each sidechain independently exposed TTL data.
Input counts have provider-specific cache-read semantics:
| Provider family | Meaning of reported inputTokens | Uncached-input calculation |
|---|---|---|
claude, claude-managed, pi, opencode | Excludes cache reads | inputTokens as reported |
codex | Includes cache reads | max(0, inputTokens - cacheReadTokens) |
Although OpenCode can route to OpenAI models, the currently shipped recompute treats its input as disjoint from cache reads; production event evidence showed that subtracting cache reads would zero most OpenCode input.
Per-model and request-priced usage
Claude's final result.modelUsage is preserved as modelBreakdown, including
sidechain/subagent entries. The API prices every entry at that entry's own
model rate, sums those totals, and stores the per-model computed costUsd in
the breakdown. Top-level stored token totals are the corresponding breakdown
sums when one exists, rather than only the main-thread usage. Because the
breakdown takes precedence, the adapter refuses to zero-fill it: a missing,
non-finite, or negative token counter on any entry drops the whole breakdown
and the session falls back to top-level usage (advisory fields like
webSearchRequests and per-model costUSD degrade per-field instead).
web_search is a request-priced class, not a token rate. The manual rows for
claude and claude-managed encode Anthropic's $10 per 1,000 requests
($0.01/request); request counts in a model breakdown are added to that
model's token cost. A missing web-search rate is treated as $0 so a small
search fee cannot discard an otherwise complete token recompute.
claude-managed also adds its $0.08/session-hour runtime_hour fee during
the server recompute, using the session duration and the manual runtime_hour
pricing row.
3. UI badge
The task-detail and task-detail-sheet views render the costSource next to every cost via <CostSourceBadge>. Mixed sources within a task aggregate render as HARNESS (the weakest claim).
When both USD values are available and differ, the badge tooltip shows harness
and recomputed numbers. The task cost view adds a visible Δ hint above 2%.
The OpenTelemetry counter agentswarm.cost.drift.usd records the absolute
non-zero difference with a drift_sign attribute, making it the operational
watchdog for stale adapter-local pricing or recompute drift.
Attribution coverage and autonomous work
The usage summary separates total spend from spend that could truthfully be
assigned to a person. attributableCostUsd is totalCostUsd minus the cost of
structurally human-free work. Attribution coverage is therefore
attributedCostUsd / attributableCostUsd, not attributed cost divided by all
spend. The response also exposes excludedCostUsd and excludedTaskCount so
the autonomous population remains visible rather than disappearing from the
report.
A task is structurally human-free when its stored task type is heartbeat,
heartbeat-checklist, or boot-triage; when its stored JSON tags contain the
heartbeat tag (including legacy rows found by the tags LIKE check); or when
it is launched by a schedule with no human creator. The schedule rule covers
both direct scheduled tasks and workflow roots whose run records that
creatorless schedule in its trigger data. Requester-less system follow-ups of
requester-less parents are also autonomous.
That classification follows the task tree recursively while descendants have
no human requester. This keeps autonomous fan-out out of the denominator, but
an explicitly attributed child is treated as a human handoff and stops the
classification along that branch. Structurally human-free rows are excluded
from attributedCostUsd even if an old or inherited requester id remains on
the row, keeping the numerator and denominator a consistent partition.
The By Person view uses the same requester data model, but it is not a grouping of the cost denominator and does not run the coverage CTE. It reports work outcomes rather than a cost score: human-requested root tasks supply Problems Initiated and Problems Shipped, while each person's full task trees supply Agents Reached, Repos Reached, and Surfaces Reached. Requester-less autonomous roots and heartbeat-classified roots do not belong to a person. The metrics stay side by side and are never summed into a composite ranking.
How the pricing table is populated
The pricing table starts with a boot seed from the vendored models.dev snapshot at src/be/modelsdev-cache.json plus a small set of manual overrides for items models.dev doesn't carry. The committed snapshot is now fallback-only for pricing freshness: it gives a cold-start DB usable rows when models.dev is unavailable, while src/be/pricing-refresh.ts owns live price updates.
After boot, the API server runs an in-process models.dev refresher once immediately and then every 12 hours. It fetches https://models.dev/api.json with If-None-Match, projects the response through the same buildModelsDevSeedRows() logic, inserts a new effective row only for new models or changed prices, and prunes history to the latest two rows per (provider, model, token_class).
The same refresh also feeds the runtime model catalog at GET /api/models-catalog (src/be/models-catalog.ts) — a slim projection of the picker-reachable providers. The UI model picker prefers that live catalog, so newly released models appear without redeploying; the committed snapshot (still symlinked at ui/src/lib/modelsdev-cache.json) remains the build-time fallback for names, labels, and context windows while the request is in flight or the server predates the endpoint.
- Projection rules live in
src/be/seed-pricing.ts:- Anthropic models → rows under both
provider='claude'ANDprovider='claude-managed'. Shortnames (opus/sonnet/haiku) also land under the current default full id. - OpenAI models →
provider='codex'. - OpenRouter models →
provider='opencode';google/*models also land underprovider='gemini'.
- Anthropic models → rows under both
- Manual overrides (claude-managed
runtime_hourat $0.08/hr, devinacuat $2.25):MANUAL_PRICING_OVERRIDESin the same file. Each entry carries its source URL and averifieddate. - Runtime refresh:
src/be/pricing-refresh.tsupdates pricing rows in-place after boot and every 12 hours. It only adds newer effective rows and never deletes pinned entries from the committed snapshot. - Snapshot refresh procedure: run
bun run scripts/refresh-modelsdev-pricing.tswhen the committed fallback/UI catalog needs a source update. Commit it alongside the PR.
Operator reference: src/providers/pricing-sources.md.
How context-window usage is computed
The unified formula
After Phase 9, every adapter uses one formula:
contextUsedTokens = inputTokens + cacheReadTokens + cacheCreateTokens + outputTokensHelpers: computeContextUsedUnified and clampContextPercent in src/utils/context-window.ts. The emitted event carries contextFormula: 'input-cache-output'.
Pi-mono is the exception: pi-ai owns the formula and we just relay its numbers. Those snapshots are tagged contextFormula: 'pi-delegated'. Devin's API doesn't report context info at all; we omit the event rather than fake zeros.
Per-model window resolution
getContextWindowSize(model) resolves:
- Shortnames (
opus/sonnet/haiku) - Family-versioned ids (
claude-sonnet-4-6) - Dated full ids (
claude-sonnet-4-6-20251004) — by stripping the 8-digit date suffix and retrying
Fallback is 200k. Pre-Phase 4 the dated form fell to 200k unconditionally — wildly wrong for opus/sonnet 4.x.
peakContextTokens and contextWindowSize
agent_tasks.peakContextTokens (renamed from totalContextTokensUsed in migration 063) is a monotonic max across all snapshots for the task — never regresses when a later snapshot reports a smaller value. This mirrors Claude Code's status-line "peak context" idea.
agent_tasks.contextWindowSize is set on the FIRST snapshot that carries one, not gated on eventType='completion'. Subsequent snapshots leave it alone.
Per-provider notes
- claude / claude-managed: token rates from models.dev. claude-managed also has a per-session-hour runtime fee (
token_class='runtime_hour'); the worker computes a preview locally viaclaude-managed-pricing.ts, and the API's recompute path overrides with the canonical value. - codex:
input_tokensfrom the SDK is the SUM across every model call in a turn (cached + uncached), and eachturn.completedpayload is accumulated across the session beforeCostDatais emitted. The unified formula uses the per-turn number as-is, accepting that chatty turns can over-report (the percent clamps at 100%). Old rows taggedpeak-proxypredate this change. Cache writes are NULL inCostDatabecause this adapter does not report them. - pi-mono: cost passes through verbatim from pi-ai's
stats.cost. Context snapshots tagcontextFormula: 'pi-delegated'.durationMsis now real wallclock (was hardcoded 0). Per-turnoutputTokensare derived from session-stats delta. - opencode: passthrough through OpenRouter. The unified formula applies;
contextPercentis clamped to [0, 100]. - devin: ACU-based pricing (
token_class='acu', $2.25 per ACU). No per-token cost. No context events (the API doesn't report context info) —peakContextTokensremainsnullfor devin tasks.
Gotchas & known limitations
- Internal-ai Gemini calls are not yet costed.
src/utils/internal-ai/models.ts:19-25routes through OpenRouter for summarization/rating but doesn't yet emitsession_costsrows. The pricing table now hasgeminirows ready; instrumentation is a follow-up. - Codex
input_tokensis a turn-sum, not a peak. Chatty turns over-report by design after Phase 9 (the clamp at 100% keeps the gauge sensible). Oldpeak-proxy-tagged rows intask_context_snapshotsare correct for their formula but not directly comparable to newinput-cache-output-tagged rows. - Model-id key mismatch. Some adapters use harness-prefixed ids (
openai-codex/gpt-5.4-mini); pricing-table seeds use the stripped form (gpt-5.4-mini). Pick one convention if you're adding a new mapping. - Timestamp convention split.
session_costs.createdAtandtask_context_snapshots.createdAtare TEXT ISO 8601;pricing.effective_from/budgets.createdAtare INTEGER epoch-ms. Documented in046_budgets_and_pricing.sql:17-22; not a near-term cleanup.
Related docs
- Harness providers — provider-specific quirks
src/providers/pricing-sources.md— operator workflowBUSINESS_USE.md— flow diagrams fortask/agent/apievents