Skip to content

OpenAI cache writes always reported as 0 #37745

Closed as not planned
Closed as not planned

Description

@dalemartyn

Description

Since 5.6 OpenAI is charging for cache writes. Opencode is reporting them as 0.

Cache reads work fine, so it feels like the write count isn't being picked up, which only matters now that they have started charging for them.

Plugins

No response

OpenCode version

1.18.3

Steps to reproduce

opencode stats shows read > 0, write = 0

Screenshot and/or share link

Image

Operating System

No response

Terminal

No response

Activity

  1. github-actions commented on Jul 19, 2026

    @github-actions
    Contributor

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

  2. rekram1-node commented on Jul 20, 2026

    @rekram1-node
    Collaborator

    are u using openai provider? What provider are u using? Need more info, are u using plugins? Etc etc

  3. dalemartyn commented on Jul 20, 2026

    @dalemartyn
    Author

    Yeah, I'm using the normal openai provider, signed in with a subscription (chat got plus). No plugins, vanilla install.

  4. johannes-engler-mw commented on Aug 2, 2026

    @johannes-engler-mw

    Independent reproduction, with a reconciliation against the provider's actual billing and a control group that isolates the cause to the missing cache.write count.

    Setup: opencode 1.18.5, macOS, opencode-go provider (@ai-sdk/openai-compatible), headless opencode run --format json, no plugins, isolated XDG_* dirs. 63 automated sessions on 2026-07-31 across three models (21 each).

    Result for gpt-5.6-luna: summing opencode's self-reported per-step cost gives $0.1459. The provider's own per-model daily billing view shows $0.41 for the same model on the same day — a 2.8× understatement.

    Token totals opencode recorded across those 21 luna sessions:

    input 414,350 · output 79,877 · reasoning 30,620 · cache.read 3,813,750 · cache.write 0
    

    The gap is $0.2641. At luna's $0.125/M cache-write rate that implies ~2.11M unreported cache-write tokens, against 3.81M recorded reads — a ~1.8:1 read:write ratio, which is what you'd expect from sessions with growing context. (As #36749 puts it: something had to write the prefix that is being read.)

    Control group — same gateway, same day, same harness:

    model cache-write rate opencode self-reported provider billing UI
    gpt-5.6-luna $0.125/M $0.1459 $0.41 ✗
    deepseek-v4-flash none $0.1138 $0.12 ✓
    mimo-v2.5 none $0.0882 $0.11 ✓

    This is the part I think is useful for triage: the two models that don't bill for cache writes reconcile with billing to the cent, and only the model that does is wrong. So this isn't general cost drift or a gateway-wide pricing mismatch — the error tracks exactly with whether cache writes are billable, which points at the missing cache.write count rather than at any rate.

    Blast radius on this gateway is wider than luna: qwen3.5-plus, qwen3.6-plus, qwen3.7-plus and qwen3.7-max all carry cache-write rates as well, with qwen3.7-max at $3.125/M — above its own $2.50/M input rate.

    Related: #39891 reports the same luna discrepancy via the TUI-vs-web comparison, and #36749 has the root cause on this code path (@ai-sdk/openai-compatible hardcodes cacheWrite: void 0, and usage() only reads inputTokenDetails.cacheWriteTokens) along with an offered fix.

  5. Zaczero commented on Aug 18, 2026

    @Zaczero

    I think I have the mechanism for this, and the short version is that the write tokens aren't missing —
    they're being recorded as input.

    Where it happens

    mapUsage derives the non-cached portion by subtraction:

    const mapUsage = (usage: OpenResponsesUsage | null | undefined, providerMetadataKey: string) => {
    if (!usage) return undefined
    const cached = usage.input_tokens_details?.cached_tokens
    const cacheWrite = usage.input_tokens_details?.cache_write_tokens
    const reasoning = usage.output_tokens_details?.reasoning_tokens
    const nonCached = ProviderShared.subtractTokens(usage.input_tokens, ProviderShared.sumTokens(cached, cacheWrite))
    return new Usage({
    inputTokens: usage.input_tokens,
    outputTokens: usage.output_tokens,
    nonCachedInputTokens: nonCached,
    cacheReadInputTokens: cached,
    cacheWriteInputTokens: cacheWrite,
    reasoningTokens: reasoning,
    totalTokens: ProviderShared.totalTokens(usage.input_tokens, usage.output_tokens, usage.total_tokens),
    providerMetadata: { [providerMetadataKey]: usage },
    })
    }

    Both fields are Schema.optional on input_tokens_details. When a route sends cached_tokens but no
    cache_write_tokens, cacheWrite is undefined, so it contributes nothing to the subtraction and
    the tokens that were written to cache stay inside nonCached. Downstream, SessionUsage.tokens
    coerces the undefined write to 0 via safe(), and the same tokens are already sitting in input.

    So cache.write = 0 isn't a dropped field. It's a misclassification, and it's silent because the
    arithmetic still balances.

    The natural experiment

    My database has both kinds of route side by side, which makes this easy to check. Average input per
    request, for every model with at least 250 requests that reported cache.read > 0:

    provider / model requests avg input avg cache.write
    anthropic/claude-opus-5 4,634 2 16,018
    openai/astra 4,984 3 6,301
    opencode-go/gpt-5.6-luna 276 3 81,910
    openai/gpt-5.6-sol 13,179 4,860 0
    openai/gpt-5.6-terra 11,955 6,231 0
    openai/gpt-5.6-luna 51,202 21,065 0

    Every route that reports a cache write has an average input of two to three tokens. Every route that
    doesn't carries thousands. That's the same quantity showing up in a different column, not two
    different behaviours.

    The obvious alternative — that these models simply miss cache more often, so the extra input is
    genuinely uncached content rather than an unlabelled write — is ruled out by the filter. Every row in
    these tables had cache.read > 0, and you cannot read from a cache that nothing wrote. Caching is
    demonstrably active on exactly the requests reporting zero writes. The only remaining reading is that
    the writes happened and weren't labelled.

    gpt-5.6-luna is the cleanest pair: on opencode-go it reports 81,910 average write and 3 average
    input; on openai it reports 0 write and 21,065 average input. Same model, same account.

    How much

    Because it's a reclassification rather than a loss, this can be measured directly instead of
    extrapolated. Summing input over requests that also had cache.read > 0:

    provider / model requests input recorded cache.read share of prompt volume filed as input
    openai/gpt-5.6-luna 51,202 1,078,547,903 3.62B 22.9%
    openai/gpt-5.6-terra 11,955 74,492,643 1.12B 6.2%
    openai/gpt-5.6-sol 13,179 64,044,087 1.61B 3.8%
    openai/astra 4,984 14,952 0.38B 0.0%
    anthropic/claude-opus-5 4,634 9,268 0.93B 0.0%

    That's 1.217 billion tokens on the openai provider alone that would have been booked as
    cache.write on a reporting route. The two reporting models round to 0.0%.

    The billing consequence follows from that: those tokens are priced at the input rate rather than the
    cache-write rate, so the cost is understated by the write premium on 1.2B tokens rather than being
    wrong by 100%. That is probably why the discrepancy showed up as a surprise on the invoice rather than
    as an obviously broken number.

    Same signature elsewhere

    The pattern isn't OpenAI-specific — it's every route that omits the field:

    provider / model requests avg input avg cache.write
    xai/grok-4.6 5,015 10,414 0
    xai/grok-4.3 859 8,919 0
    opencode-go/minimax-m3 9,028 3,288 0
    opencode-go/glm-5.2 253 13,015 0

    xai is the most absolute case: 5,892 assistant messages, max(cache.write) and sum(cache.write)
    both exactly zero, across two models. If a regression test is wanted, that's the cleanest input.

    What I can't tell from here

    Whether the openai route omits cache_write_tokens because the API doesn't send it, or because it
    arrives under a different key that the schema at
    open-responses.ts:182-193
    doesn't look for. That's answerable without new instrumentation, though — mapUsage already stows the
    raw payload as providerMetadata: { [providerMetadataKey]: usage }, so one captured
    response.completed from a gpt-5.6-* request would settle it.

    If the field genuinely isn't available on that route, the honest fix is probably not to invent it but
    to stop the silent reclassification: either surface non-cached input as an explicit "uncached, may
    include writes" quantity, or price it at the write rate when caching is known to be active. Reporting
    0 for a quantity that is demonstrably non-zero is the part that costs users money quietly.

  6. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.

    — from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

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

Metadata

Metadata

Assignees

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