agent-swarm.devagent-swarm.dev
Guides

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:

  1. Cost (USD). Each adapter writes one session_costs row per CLI invocation. The API may recompute it from the seeded pricing table.
  2. Context-window usage. Each adapter emits context_usage events; the API persists snapshots and updates aggregate columns on agent_tasks.

This page is the single source of truth for how both numbers are produced.

New session_costs rows keep two USD values:

  • harnessCostUsd is the adapter-reported number. It is useful for comparison, but advisory: adapters can have stale local rates or incomplete usage.
  • totalCostUsd is 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 other costSource paths 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:

OutcomecostSourceStored 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 familyMeaning of reported inputTokensUncached-input calculation
claude, claude-managed, pi, opencodeExcludes cache readsinputTokens as reported
codexIncludes cache readsmax(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' AND provider='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 under provider='gemini'.
  • Manual overrides (claude-managed runtime_hour at $0.08/hr, devin acu at $2.25): MANUAL_PRICING_OVERRIDES in the same file. Each entry carries its source URL and a verified date.
  • Runtime refresh: src/be/pricing-refresh.ts updates 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.ts when 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 + outputTokens

Helpers: 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 via claude-managed-pricing.ts, and the API's recompute path overrides with the canonical value.
  • codex: input_tokens from the SDK is the SUM across every model call in a turn (cached + uncached), and each turn.completed payload is accumulated across the session before CostData is 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 tagged peak-proxy predate this change. Cache writes are NULL in CostData because this adapter does not report them.
  • pi-mono: cost passes through verbatim from pi-ai's stats.cost. Context snapshots tag contextFormula: 'pi-delegated'. durationMs is now real wallclock (was hardcoded 0). Per-turn outputTokens are derived from session-stats delta.
  • opencode: passthrough through OpenRouter. The unified formula applies; contextPercent is 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) — peakContextTokens remains null for devin tasks.

Gotchas & known limitations

  • Internal-ai Gemini calls are not yet costed. src/utils/internal-ai/models.ts:19-25 routes through OpenRouter for summarization/rating but doesn't yet emit session_costs rows. The pricing table now has gemini rows ready; instrumentation is a follow-up.
  • Codex input_tokens is a turn-sum, not a peak. Chatty turns over-report by design after Phase 9 (the clamp at 100% keeps the gauge sensible). Old peak-proxy-tagged rows in task_context_snapshots are correct for their formula but not directly comparable to new input-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.createdAt and task_context_snapshots.createdAt are TEXT ISO 8601; pricing.effective_from / budgets.createdAt are INTEGER epoch-ms. Documented in 046_budgets_and_pricing.sql:17-22; not a near-term cleanup.

On this page