Skip to content

v2 SessionRunner publishes Step.Ended.cost: 0; Copilot refresh drops Anthropic cache-write pricing in token fallback #30706

Closed

Description

@DanMaly

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:

  1. The new v2 SessionRunner publishes Step.Ended with cost: 0 for 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.
  2. The GitHub Copilot model refresh sets cache.write to 0. 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 receive cacheWriteInputTokens.

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_aiu

So 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),
  })
  return

The 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_aiu is 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 SessionRunner should calculate Step.Ended.cost from normalized usage, mirroring Session.getUsage: prefer Copilot's totalNanoAiu when 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 cacheWriteInputTokens and the model cost metadata has a non-zero cache.write rate.
  • The Copilot model refresh should not unconditionally zero cache.write for Anthropic models. If /models does not expose a cache-write price, preserving models.dev / configured cache.write is safer than dropping it (it only affects the fallback, but that fallback should still be correct).

Related issues

Possible fix shape

  1. Add a shared cost calculator for Usage + ModelV2.Info.cost that mirrors Session.getUsage, including the Copilot totalNanoAiu short-circuit and tiered/context pricing.
  2. Use it in packages/core/src/session/runner/publish-llm-event.ts instead of cost: 0.
  3. In the Copilot model refresh, preserve prev.cost.cache.write / models.dev cache-write pricing for Anthropic models when /models only exposes cached-input read pricing.
  4. Add tests for:
    • non-zero Step.Ended.cost from a fixed usage payload + model cost table (v2 runner)
    • Copilot totalNanoAiu taking precedence over token math
    • cache-write cost included in the token fallback for Anthropic-style usage
    • Copilot model refresh not discarding existing Anthropic cache.write pricing

Activity

  1. changed the title [-]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[/+] on Jun 4, 2026
  2. Necmttn commented on Jun 21, 2026

    @Necmttn

    I would make Step.Ended.cost carry provenance instead of treating zero as a value. Suggested shape: cost.amount, cost.currency, and cost_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.

  3. added
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    bugSomething isn't working
    on Jul 5, 2026
  4. opencode-agent commented on Jul 8, 2026

    @opencode-agent
    Contributor

    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.

  5. opencode-agent commented on Jul 10, 2026

    @opencode-agent
    Contributor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

2.0bugSomething isn't workingcoreAnything pertaining to core functionality of the application (opencode server stuff)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions