From 9e45091f49e035a7ed7750cede432c4d73042d26 Mon Sep 17 00:00:00 2001 From: Ben Burns <803016+benjamincburns@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:37:05 +1300 Subject: [PATCH] fix(core): make cache read cost fallback to input token cost When setting up the model object, the cost calculation for cache reads was falling back to 0. For providers that charge the same price for input caching, this caused the calculated cost to be dramatically lower than it should be. Changed the fallback to use the input token cost instead of 0 in both fromModelsDevModel() and the config provider section. fixes anomalyco/opencode#17121 --- packages/opencode/src/provider/provider.ts | 9 +- .../opencode/test/provider/provider.test.ts | 92 +++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index ccd3c55b4f1..69031bcc0fc 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -775,7 +775,7 @@ export namespace Provider { input: model.cost?.input ?? 0, output: model.cost?.output ?? 0, cache: { - read: model.cost?.cache_read ?? 0, + read: model.cost?.cache_read ?? model.cost?.input ?? 0, write: model.cost?.cache_write ?? 0, }, experimentalOver200K: model.cost?.context_over_200k @@ -947,7 +947,12 @@ export namespace Provider { input: model?.cost?.input ?? existingModel?.cost?.input ?? 0, output: model?.cost?.output ?? existingModel?.cost?.output ?? 0, cache: { - read: model?.cost?.cache_read ?? existingModel?.cost?.cache.read ?? 0, + read: + model?.cost?.cache_read ?? + existingModel?.cost?.cache.read ?? + model?.cost?.input ?? + existingModel?.cost?.input ?? + 0, write: model?.cost?.cache_write ?? existingModel?.cost?.cache.write ?? 0, }, }, diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 4c6eaf8b227..ab34f5741da 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -492,6 +492,98 @@ test("model cost defaults to zero when not specified", async () => { }) }) +test("cache read cost falls back to input cost when not specified", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "test-provider": { + name: "Test Provider", + npm: "@ai-sdk/openai-compatible", + env: [], + models: { + "test-model": { + name: "Test Model", + tool_call: true, + limit: { context: 128000, output: 4096 }, + cost: { + input: 5, + output: 15, + }, + }, + }, + options: { + apiKey: "test-key", + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + const model = providers["test-provider"].models["test-model"] + expect(model.cost.input).toBe(5) + expect(model.cost.output).toBe(15) + expect(model.cost.cache.read).toBe(5) + expect(model.cost.cache.write).toBe(0) + }, + }) +}) + +test("cache read cost uses explicit value when provided", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "test-provider": { + name: "Test Provider", + npm: "@ai-sdk/openai-compatible", + env: [], + models: { + "test-model": { + name: "Test Model", + tool_call: true, + limit: { context: 128000, output: 4096 }, + cost: { + input: 5, + output: 15, + cache_read: 1, + cache_write: 2, + }, + }, + }, + options: { + apiKey: "test-key", + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + const model = providers["test-provider"].models["test-model"] + expect(model.cost.input).toBe(5) + expect(model.cost.output).toBe(15) + expect(model.cost.cache.read).toBe(1) + expect(model.cost.cache.write).toBe(2) + }, + }) +}) + test("model options are merged from existing model", async () => { await using tmp = await tmpdir({ init: async (dir) => {