Skip to content

Non-cached input tokens are dropped from usage, so input is always 0 and cost is under-reported #146

Closed

Description

@chrismatheson

This finding & PR is 100% AI. Apologies if its completely inaccurate and pointless.

Versions

  • @stablekernel/opencode-cursor 0.10.0
  • @cursor/sdk 1.0.37
  • opencode v2.1.2

Summary

mapUsage passes Cursor's inputTokens to opencode as inputTokens.total. Cursor's inputTokens doesn't include cached tokens: the SDK's own totalTokens is inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens. opencode treats total as including cache and subtracts cache read and cache write to get the non-cached input. On any cached turn that comes out negative and gets clamped to 0, so non-cached input tokens never show up in opencode's token counts or cost.

Current code (src/provider, mapUsage)

inputTokens: {
  total: usage.inputTokens,
  noCache: undefined,
  cacheRead: usage.cacheReadTokens,
  cacheWrite: usage.cacheWriteTokens
}

Observed

opencode stats --days 0 --cost --models for one day of cursor/claude-opus-5-5 use, 18 steps:

Token type opencode
input 0
output ~51k
cache read ~8.9M
cache write ~1.2M
total ~10.15M

The Cursor usage dashboard showed about 14.9M tokens for the same day. The ~4.75M gap is about what you'd expect for missing non-cached input. Input has been 0 for every Cursor-model step I checked.

Expected

Non-cached input tokens are reported and priced at the model's input rate.

Suggested fix

function mapUsage(usage) {
  const cacheRead = usage.cacheReadTokens ?? 0;
  const cacheWrite = usage.cacheWriteTokens ?? 0;
  return {
    inputTokens: {
      total: usage.inputTokens + cacheRead + cacheWrite,
      noCache: usage.inputTokens,
      cacheRead,
      cacheWrite
    },
    outputTokens: {
      total: usage.outputTokens,
      text: undefined,
      reasoning: usage.reasoningTokens
    }
  };
}

The suggestion also passes reasoningTokens through, since the SDK includes it in the turn-ended usage when present.

Possibly related

The price table (MODEL_COST) is applied through two routes: the config hook, which lets user provider.cursor.models entries override it, and the provider.models route, which always uses the built-in table. If list prices don't match a user's actual Cursor rates, there's no reliable way to override them. Could user-configured cost be honoured on both routes?

Activity

  1. added a commit that references this issue on Oct 28, 2026
  2. justin-carper commented on Oct 28, 2026

    @justin-carper
    Collaborator

    Thanks for the detailed report. The bug was real, but the cause was different from the one you described.

    Cursor's inputTokens already includes cache reads and writes. @cursor/sdk computes totalTokens = inputTokens + outputTokens, with no separate cache terms. So passing inputTokens as inputTokens.total was correct.

    The bug: the plugin left noCache undefined. opencode 2.x reads noCache directly as the non-cached input, so input always showed as 0 and was never priced. The fix (#147) sets noCache = inputTokens − cacheRead − cacheWrite, clamped at 0, and passes reasoning tokens through as outputTokens.reasoning.

    Fixed in 0.10.1 and later. To upgrade: run opencode plugin update, or delete ~/.cache/opencode/npm/@stablekernel/opencode-cursor@latest and restart opencode.

    Honoring a cost you configure yourself on provider.models is a separate problem. Open a new issue if you want it tracked.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions