Skip to content

github-copilot: session cost ignores cache writes and long-context pricing #52089

Closed

Description

@stevoland

Summary

GitHub Copilot session costs are significantly under-reported. Copilot's /models API exposes cache_write_price and long_context prices, and its usage payloads report cache_write_tokens, but opencode ignores all three. Cache writes are priced at $0 for Claude models and billed as plain input for GPT models, and long-context pricing is never applied.

Environment

  • opencode version: 2.0.19
  • OS: Darwin 25.6.0 (arm64)
  • Terminal: Ghostty (TERM=xterm-ghostty, COLORTERM=truecolor)
  • Shell: /bin/zsh
  • Install/channel: latest
  • Active plugins: local/internal plugins only (none touch provider pricing or usage)

Reproduction

  1. Log in with GitHub Copilot and run a session with claude-opus-5.5 (or any Claude model) that creates prompt-cache entries.
  2. Run opencode models (or inspect the model): every Copilot model has cost.cache.write: 0.
  3. Compare the session cost with the GitHub pricing table (https://docs.github.com/en/copilot/reference/copilot-billing/models-and-pricing).
  4. Run any GPT-5.6 / GPT-6 session and check the token stats: cache.write is always 0.

Expected Behavior

Session cost matches GitHub's per-token pricing, including cache writes and long-context tiers.

Actual Behavior

Example Opus 5.5 session:

Tokens Count opencode rate GitHub rate
input 104 $4 $4
output + reasoning 21,123 $20 $20
cache read 3,478,395 $0.20 $0.20
cache write 235,970 $0 $5

opencode reports $1.12; the correct cost is $2.30.

A controlled check against Copilot's quota counter (/copilot_internal/user → quota_snapshots.premium_interactions) over a ~10 minute window showed GitHub consuming ~140 credits while opencode recorded ~6.6 credits of cost.

Additional Context

1. packages/core/src/github-copilot/models.ts drops prices

GET {api}/models (with X-GitHub-Api-Version: 2026-08-01) returns, for example:

"token_prices": {
  "batch_size": 1000000,
  "default":      { "input_price": 400, "cache_read_price": 20, "cache_write_price": 500, "output_price": 2000, "max_prompt_tokens": 272000 },
  "long_context": { "input_price": 800, "cache_read_price": 80, "cache_write_price": 1000, "output_price": 3000, "max_prompt_tokens": 922000 }
}

(that one is gpt-5.6-sol). But build() hardcodes:

write: Money.USDPerMillionTokens.zero,

and the schema has no cache_write_price, long_context, or cache_write_1h_price. SessionUsage.calculateCost already supports context tiers, so long_context could map to { tier: { type: "context", size: default.max_prompt_tokens } }.

2. Copilot Responses/Chat adapters ignore cache_write_tokens

Copilot /responses reports cache writes:

1st call: input_tokens 8273, input_tokens_details: { cache_write_tokens: 8270, cached_tokens: 0 }
2nd call: input_tokens 8273, input_tokens_details: { cache_write_tokens: 0,    cached_tokens: 8270 }

github-copilot/responses/openai-responses-language-model.ts only reads cached_tokens and sets cacheWrite: undefined (both the generate and stream paths; the stream usage schema only declares cached_tokens). chat/openai-compatible-chat-language-model.ts does the same. Cache writes therefore land in noCache and are billed at the input price (GPT-5.6 / GPT-6 charge 1.25x input for cache writes). noCache should also subtract cache_write_tokens.

Across a month of usage, every GPT model in opencode stats shows cache.write: 0.

Related: session/usage.ts has TODO(#35765): Use Copilot's reported billed amount....

Activity

  1. github-actions commented on Sep 29, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    • #51845: Reports GitHub Copilot cache-write tokens omitted from session cost estimates, including Claude Opus 5.5.
    • #42910: Reports session cost estimates that fail to apply higher context-tier pricing.
  2. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report. I've confirmed both parts on 2.0.19 and on the latest dev (86ce19d), using a local mock of Copilot's /models and /responses endpoints built from the payloads in your report:

    • Prices: for a /models entry with cache_write_price: 500 and a long_context block, the model ends up with cost.cache.write: 0 and no long-context tier.
    • Usage: for a /responses result reporting cache_write_tokens: 8270, both the streaming and non-streaming paths return cacheWrite as undefined and noCache: 8273, so cache writes are billed at the plain input price.

    The script below reproduces it without credentials. The Chat Completions adapter has the same cacheWrite: undefined code, though I didn't run it. Related: #35765, which would prefer Copilot's own reported billed amount when it is available.

    Reproduction script (repro.ts)
    // Reproduces #52089: Copilot cache-write / long-context prices and cache_write_tokens are dropped.
    // Run (after `bun install` in the checkout):
    //   cd <checkout>/packages/core && REPO=<checkout> bun /path/to/repro.ts
    // Uses a local mock of Copilot's /models and /responses endpoints; no credentials needed.
    const REPO = process.env.REPO ?? "/workspace/repo"
    const { CopilotModels } = await import(`${REPO}/packages/core/src/github-copilot/models.ts`)
    const { createOpenaiCompatible } = await import(`${REPO}/packages/core/src/github-copilot/copilot-provider.ts`)
    
    const remoteModel = {
      model_picker_enabled: true,
      id: "gpt-5.6-sol",
      name: "GPT-5.6 Sol",
      version: "gpt-5.6-sol-2026-08-01",
      supported_endpoints: ["/responses"],
      billing: {
        token_prices: {
          batch_size: 1000000,
          default: { input_price: 400, cache_read_price: 20, cache_write_price: 500, output_price: 2000, max_prompt_tokens: 272000 },
          long_context: { input_price: 800, cache_read_price: 80, cache_write_price: 1000, output_price: 3000, max_prompt_tokens: 922000 },
        },
      },
      capabilities: {
        family: "gpt-5.6",
        limits: { max_context_window_tokens: 922000, max_output_tokens: 128000, max_prompt_tokens: 922000 },
        supports: { tool_calls: true, streaming: true },
      },
    }
    
    const usage = {
      input_tokens: 8273,
      input_tokens_details: { cache_write_tokens: 8270, cached_tokens: 0 },
      output_tokens: 10,
      output_tokens_details: { reasoning_tokens: 0 },
      total_tokens: 8283,
    }
    const response = {
      id: "resp_1",
      object: "response",
      created_at: 1760000000,
      model: "gpt-5.6-sol",
      status: "completed",
      output: [{ type: "message", id: "msg_1", role: "assistant", status: "completed", content: [{ type: "output_text", text: "hi", annotations: [] }] }],
      usage,
    }
    
    const server = Bun.serve({
      port: 0,
      async fetch(req) {
        const url = new URL(req.url)
        if (url.pathname === "/models") return Response.json({ data: [remoteModel] })
        if (url.pathname === "/responses") {
          const body = await req.json()
          if (!body.stream) return Response.json(response)
          const events = [
            { type: "response.created", response: { ...response, status: "in_progress", output: [], usage: null } },
            { type: "response.output_item.added", output_index: 0, item: { type: "message", id: "msg_1", role: "assistant", status: "in_progress", content: [] } },
            { type: "response.output_text.delta", item_id: "msg_1", output_index: 0, content_index: 0, delta: "hi" },
            { type: "response.output_item.done", output_index: 0, item: response.output[0] },
            { type: "response.completed", response },
          ]
          const text = events.map((e) => `data: ${JSON.stringify(e)}\n\n`).join("") + "data: [DONE]\n\n"
          return new Response(text, { headers: { "content-type": "text/event-stream" } })
        }
        return new Response("not found", { status: 404 })
      },
    })
    const base = `http://localhost:${server.port}`
    
    const snapshot = await CopilotModels.load(base, {})
    const models = CopilotModels.derive(base, snapshot, [])
    const model = [...models.values()][0]
    console.log("model.cost (remote: write=$5/M default, long_context tier $8/$0.80/$10/$30 above 272k):")
    console.log(JSON.stringify(model.cost, null, 2))
    
    const lm = createOpenaiCompatible({ baseURL: base, apiKey: "x", name: "copilot" }).responses("gpt-5.6-sol")
    const prompt = [{ role: "user", content: [{ type: "text", text: "hello" }] }]
    const gen = await lm.doGenerate({ prompt } as any)
    console.log("doGenerate inputTokens (remote: cache_write_tokens=8270):", JSON.stringify(gen.usage.inputTokens))
    const stream = await lm.doStream({ prompt } as any)
    const reader = stream.stream.getReader()
    for (;;) {
      const { done, value } = await reader.read()
      if (done) break
      if ((value as any).type === "finish") console.log("doStream inputTokens:", JSON.stringify((value as any).usage.inputTokens))
    }
    server.stop(true)

    Run it: bun repro.ts

  3. golfromeo-fr commented on Oct 28, 2026

    @golfromeo-fr

    Opened #59803 covering both halves of this issue: the builder now parses cache_write_price and the optional long_context tier (its own prices and max_prompt_tokens) from the /models payload and emits a context-tier cost entry — SessionUsage.calculateCost already selected tiers per request, so no calculator change was needed. Both usage adapters map cache_write_tokens into cacheWrite instead of dropping it, and the non-cached input stays consistent with the total. Tested against the Claude Haiku 5.5 rate card, including the ≤100K boundary.

  4. rekram1-node commented on Nov 10, 2026

    @rekram1-node
    Collaborator

    Fixed in v2.2.7 by #62034. Session cost for GitHub Copilot now uses the amount Copilot reports it charged for each request (copilot_usage) instead of the catalog estimate, so cache writes and long-context pricing are included. If you still see a cost that doesn't match Copilot's billing on 2.2.7, let me know and I'll reopen.

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions