#18445 · @LucasSantana-Dev · opened Mar 20, 2026 at 9:41 PM UTC · last updated Mar 21, 2026 at 8:38 PM UTC

fix(cost): account for OpenRouter cache write tokens

appfix
68
+4203 files

Score breakdown

Impact

8.0

Clarity

8.0

Urgency

7.0

Ease Of Review

8.0

Guidelines

9.0

Readiness

8.0

Size

9.0

Trust

5.0

Traction

3.0

Summary

This PR fixes a critical cost underestimation bug for OpenRouter users by correctly accounting for cache write tokens. It ensures the in-app cost tracker accurately reflects actual billing.

Open in GitHub

Description

Issue for this PR

Closes #18440

Type of change

  • [x] Bug fix
  • [ ] New feature
  • [ ] Refactor / code improvement
  • [ ] Documentation

What does this PR do?

This fixes OpenRouter cost underestimation when cache write tokens are present. OpenAI-compatible responses can report prompt_tokens_details.cache_write_tokens, but we were not carrying that through for usage accounting.

Changes in this PR:

  • parse cache_write_tokens in OpenAI-compatible provider paths (stream + non-stream)
  • persist that value into provider metadata (openrouter.usage.cacheWriteInputTokens)
  • include that metadata fallback in Session.getUsage so cache write tokens are counted in token/cost totals
  • add regression coverage for the OpenRouter metadata path in Session.getUsage tests

How did you verify your code works?

  • cd packages/opencode && bun test test/session/compaction.test.ts
  • cd packages/opencode && bun run typecheck

Screenshots / recordings

Not a UI change.

Checklist

  • [x] I have tested my changes locally
  • [x] I have not included unrelated changes in this PR

Linked Issues

#18440 Cache write tokens not accounted for when using OpenRouter, causing cost underestimation

View issue

Comments

PR comments

rekram1-node

I'm having a hard time trusting these PRs because if u look at the file paths u edited, it specifically says it is specifically and ONLY for copilot, so what does that have to do with openrouter?

That codepath should basically never be hit unless u have a custom provider configured to use the github copilot sdk which I kinda doubt?

rekram1-node

It even has a readme saying as much: https://github.com/anomalyco/opencode/tree/dev/packages/opencode/src/provider/sdk/copilot#readme

LucasSantana-Dev

It even has a readme saying as much:

https://github.com/anomalyco/opencode/tree/dev/packages/opencode/src/provider/sdk/copilot#readme

I'll have a second look on that and figure out a more structured and fitting solution for the idea i had!

Changed Files

packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts

+160
@@ -272,6 +272,11 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
providerMetadata[this.providerOptionsName].rejectedPredictionTokens =
completionTokenDetails?.rejected_prediction_tokens
}
if (responseBody.usage?.prompt_tokens_details?.cache_write_tokens != null) {
providerMetadata[this.providerOptionsName].usage = {
cacheWriteInputTokens: responseBody.usage?.prompt_tokens_details?.cache_write_tokens,
}
}
return {
content,
@@ -343,6 +348,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
promptTokens: number | undefined
promptTokensDetails: {
cachedTokens: number | undefined
cacheWriteTokens: number | undefined
}
totalTokens: number | undefined
} = {
@@ -355,6 +361,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
promptTokens: undefined,
promptTokensDetails: {
cachedTokens: undefined,
cacheWriteTokens: undefined,
},
totalTokens: undefined,
}
@@ -430,6 +437,9 @@ export class OpenAICompatibleChatLanguageModel implements Langu

packages/opencode/src/session/index.ts

+20
@@ -810,6 +810,8 @@ export namespace Session {
input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
// @ts-expect-error
input.metadata?.["venice"]?.["usage"]?.["cacheCreationInputTokens"] ??
// @ts-expect-error
input.metadata?.["openrouter"]?.["usage"]?.["cacheWriteInputTokens"] ??
0) as number,
)

packages/opencode/test/session/compaction.test.ts

+240
@@ -278,6 +278,30 @@ describe("session.getUsage", () => {
expect(result.tokens.cache.read).toBe(200)
})
test("handles cache write tokens from usage payload", () => {
const model = createModel({ context: 100_000, output: 32_000, npm: "@ai-sdk/openai-compatible" })
const result = Session.getUsage({
model,
usage: {
inputTokens: 10_000,
outputTokens: 500,
totalTokens: 10_500,
cachedInputTokens: 2_000,
},
metadata: {
openrouter: {
usage: {
cacheWriteInputTokens: 1_500,
},
},
},
})
expect(result.tokens.input).toBe(6_500)
expect(result.tokens.cache.read).toBe(2_000)
expect(result.tokens.cache.write).toBe(1_500)
})
test("handles anthropic cache write metadata", () => {
const model = createModel({ context: 100_000, output: 32_000 })
const result = Session.getUsage({