Repository navigation
Description
Summary
On the current dev branch, GitHub Copilot cost tracking is mostly working in the active runtime, but there are two real gaps once you look closer. The original version of this issue overstated the problem ("reports zero cost"); this is a corrected write-up.
There are two runners on dev, and they behave differently:
| Active runtime | New v2 runner | |
|---|---|---|
| File | packages/opencode/src/session/processor.ts |
packages/core/src/session/runner/publish-llm-event.ts |
| Wired via | app-runtime.ts → SessionProcessor |
@opencode/v2/SessionRunner (in-progress packages/core rewrite) |
Step.Ended.cost |
cost: usage.cost |
cost: 0 (hardcoded) |
Accumulates assistant.cost |
yes (+= usage.cost) |
no |
So Copilot cost is not universally zero. In the active path it is computed — and for Copilot specifically it is taken directly from GitHub's own reported per-request billing. The two genuine gaps are:
- The new v2
SessionRunnerpublishesStep.Endedwithcost: 0for every LLM step and never calculates cost. This is the in-progress rewrite, not the live path, but it is a real hole that will under-report once it goes live. - The GitHub Copilot model refresh sets
cache.writeto0. This only affects the token-based cost fallback (used when Copilot does not return per-request usage), but in that fallback it under-reports Anthropic Copilot models that receivecacheWriteInputTokens.
How Copilot cost actually works today (active path)
processor.ts calls Session.getUsage (packages/opencode/src/session/session.ts), which has a Copilot-specific branch:
const totalNanoAiu = input.metadata?.["copilot"]?.["totalNanoAiu"]
return {
cost:
typeof totalNanoAiu === "number" && Number.isFinite(totalNanoAiu) && totalNanoAiu >= 0
? new Decimal(totalNanoAiu).div(100_000_000_000).toNumber()
: safe(
new Decimal(0)
.add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000))
.add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.read).mul(costInfo?.cache?.read ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.write).mul(costInfo?.cache?.write ?? 0).div(1_000_000))
.add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000))
.toNumber(),
),
tokens,
}totalNanoAiu is read straight from the Copilot API response (copilot_usage.total_nano_aiu) in packages/opencode/src/session/llm/ai-sdk.ts:
const usage = raw.copilot_usage ?? response?.copilot_usage
// ...
const total = (usage as Record<string, unknown>).total_nano_aiuSo for a normal Copilot request, cost is GitHub's own billed AIU figure for that request, converted to USD (total_nano_aiu / 1e11). That figure already includes cache-write billing — which is exactly what the write: 0 comment in the model refresh refers to. Token-table pricing (and therefore cache.write) is only consulted in the fallback branch, when total_nano_aiu is absent.
processor.ts then publishes the real value and accumulates it:
case "step-finish": {
const usage = Session.getUsage({ /* model, usage, metadata */ })
// ...
yield* events.publish(SessionEvent.Step.Ended, {
// ...
cost: usage.cost,
tokens: usage.tokens,
})
ctx.assistantMessage.cost += usage.cost
ctx.assistantMessage.tokens = usage.tokens
}Gap 1: v2 SessionRunner hardcodes Step.Ended.cost: 0
In packages/core/src/session/runner/publish-llm-event.ts, step-finish publishes a hardcoded zero and never calls a cost calculator:
case "step-finish":
yield* flush()
yield* events.publish(SessionEvent.Step.Ended, {
sessionID: input.sessionID,
timestamp: yield* timestamp,
assistantMessageID: yield* currentAssistantMessageID(),
finish: event.reason,
cost: 0,
tokens: tokens(event.usage),
})
returnThe same file already extracts token categories, including cache-write:
const tokens = (usage: Usage | undefined) => {
const reasoning = safe(usage?.reasoningTokens)
const read = safe(usage?.cacheReadInputTokens)
const write = safe(usage?.cacheWriteInputTokens)
return {
input: safe(usage?.nonCachedInputTokens),
output: safe(usage?.visibleOutputTokens),
reasoning,
cache: { read, write },
}
}But there is no equivalent of Session.getUsage here — neither the totalNanoAiu branch nor token-based pricing. When this runner becomes the live path, Copilot (and every other provider) will report zero cost until a calculator is wired in.
Gap 2: Copilot refresh drops cache-write pricing in the token fallback
packages/opencode/src/plugin/github-copilot/models.ts derives input/output/cache-read pricing from Copilot /models billing.token_prices, but hardcodes cache.write to zero:
const prices = remote.billing?.token_prices
// Copilot prices are AIC per billing batch; OpenCode stores USD per million tokens.
const usdPerMillion = prices ? 10_000 / prices.batch_size : 0
// ...
cost: {
input: (prices?.default.input_price ?? 0) * usdPerMillion,
output: (prices?.default.output_price ?? 0) * usdPerMillion,
cache: {
read: (prices?.default.cache_price ?? 0) * usdPerMillion,
// `/models` exposes cached-input reads only; per-request billing accounts for cache writes.
write: 0,
},
},The comment is correct for the common case: when Copilot returns total_nano_aiu, per-request billing already accounts for cache writes, so the token-table cache.write is never used. The problem is the fallback: if total_nano_aiu is missing, getUsage falls back to token math with cache.write = 0, under-reporting Anthropic Copilot models that received cacheWriteInputTokens.
GitHub's public Copilot pricing docs give separate cache-write prices for Anthropic models:
Anthropic models include a cache write cost in addition to cached input.
Source: https://docs.github.com/en/copilot/reference/copilot-billing/models-and-pricing
Raw data (cache_write per Anthropic model): https://github.com/github/docs/blob/main/data/tables/copilot/models-and-pricing.yml
Examples (per 1M tokens):
- Claude Haiku 4.5: input $1.00 / cached input $0.10 / cache write $1.25 / output $5.00
- Claude Sonnet 4/4.5/4.6: input $3.00 / cached input $0.30 / cache write $3.75 / output $15.00
- Claude Opus 4.5/4.6/4.7/4.8: input $5.00 / cached input $0.50 / cache write $6.25 / output $25.00
Why this matters
- For the active runtime, normal Copilot requests are costed correctly from
total_nano_aiu. No action needed there for the common case. - The fallback path under-reports Anthropic Copilot cache writes whenever
total_nano_aiuis absent, because the refresh discards cache-write pricing. - The v2 SessionRunner will under-report everything (cost
0) until it gains a cost calculation step.
Downstream telemetry/plugins that emit assistant.cost can only report what opencode stores, so both gaps propagate.
Expected behavior
- The v2
SessionRunnershould calculateStep.Ended.costfrom normalized usage, mirroringSession.getUsage: prefer Copilot'stotalNanoAiuwhen present, otherwise fall back to token × model-cost (input / output / cache-read / cache-write, reasoning at output rate, respecting tiered/context pricing). - In the token fallback, cache-write tokens should be priced when both the provider reports
cacheWriteInputTokensand the model cost metadata has a non-zerocache.writerate. - The Copilot model refresh should not unconditionally zero
cache.writefor Anthropic models. If/modelsdoes not expose a cache-write price, preservingmodels.dev/ configuredcache.writeis safer than dropping it (it only affects the fallback, but that fallback should still be correct).
Related issues
- Multi-step tool calls overwrite token counts instead of accumulating (output, reasoning, cache.write lost) #21913 — multi-step token accumulation and
cache.writetoken loss, but not cost calculation or Copilot Anthropic cache-write pricing. - Model cost shows $0 when provider omits cost in models.dev #29971 —
$0display when a provider omits cost inmodels.dev; different cause (here cost metadata exists). - Github copilot costs hardcoded to 0 #24787 — older Copilot hardcoded-zero-cost path, closed.
- Github Copilot: Tracking Premium Requests #768 — legacy premium request tracking, closed after the AI Credits transition.
Possible fix shape
- Add a shared cost calculator for
Usage+ModelV2.Info.costthat mirrorsSession.getUsage, including the CopilottotalNanoAiushort-circuit and tiered/context pricing. - Use it in
packages/core/src/session/runner/publish-llm-event.tsinstead ofcost: 0. - In the Copilot model refresh, preserve
prev.cost.cache.write/models.devcache-write pricing for Anthropic models when/modelsonly exposes cached-input read pricing. - Add tests for:
- non-zero
Step.Ended.costfrom a fixed usage payload + model cost table (v2 runner) - Copilot
totalNanoAiutaking precedence over token math - cache-write cost included in the token fallback for Anthropic-style usage
- Copilot model refresh not discarding existing Anthropic
cache.writepricing
- non-zero
Activity
[-]Copilot cost tracking reports zero cost and drops Anthropic cache-write pricing[/-][+]v2 SessionRunner publishes Step.Ended.cost: 0; Copilot refresh drops Anthropic cache-write pricing in token fallback[/+]Necmttn commentedon Jun 21, 2026
I would make
Step.Ended.costcarry provenance instead of treating zero as a value. Suggested shape:cost.amount,cost.currency, andcost_source=provider_reported|price_table_estimated|unavailable|not_applicable.That lets v2 publish a step before pricing is implemented without telling dashboards the step was free. For Copilot, provider-reported billing should win when present; token fallback should be clearly marked estimated and include missing cache-write/cache-read bucket diagnostics.
Generated with ax.
opencode-agent commentedon Jul 8, 2026
Audit update (2026-07-08): #35468 merged the main V2 step/session token and cost accounting path, so the original hardcoded-zero gap is resolved. Keeping this open only as an umbrella for the remaining Copilot-specific/provider-billing work in #35765 and auxiliary title/compaction usage in #35781. Consider closing this once those narrower issues are sufficient.
opencode-agent commentedon Jul 10, 2026
Audit: the broad V2 step-cost bug appears superseded/completed by PR #35468, which calculates V2 step costs, persists session token/cost totals, and prefers Copilot provider-reported AIU billing when available. Remaining narrower follow-ups are tracked in #35765 (typed Copilot billing contract) and #35781 (auxiliary title/compaction LLM usage). Closing this older umbrella issue to avoid duplicate tracking.