diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index a2a91cd47b5e..32d9a29af605 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -358,12 +358,21 @@ export const getUsage = (input: { model: Provider.Model; usage: Usage; metadata? ), ) + // Bedrock GPT-5.6 Luna includes cached tokens in its input count, while the + // AI SDK normalizer adds the cache fields again. Remove that duplicate before + // splitting the prompt into cached and non-cached tokens below. + const duplicatedBedrockCache = + input.model.api.npm === "@ai-sdk/amazon-bedrock" && input.model.api.id.includes("openai.gpt-5.6-luna") + ? cacheReadInputTokens + cacheWriteInputTokens + : 0 + const contextTokens = safe(inputTokens - duplicatedBedrockCache) + // AI SDK v6 normalized inputTokens to include cached tokens across all providers // (including Anthropic/Bedrock which previously excluded them). Always subtract cache // tokens to get the non-cached input count for separate cost calculation. - const adjustedInputTokens = safe(inputTokens - cacheReadInputTokens - cacheWriteInputTokens) + const adjustedInputTokens = safe(contextTokens - cacheReadInputTokens - cacheWriteInputTokens) - const total = input.usage.totalTokens + const total = duplicatedBedrockCache ? safe(contextTokens + outputTokens) : input.usage.totalTokens const tokens = { total, @@ -376,7 +385,6 @@ export const getUsage = (input: { model: Provider.Model; usage: Usage; metadata? }, } - const contextTokens = inputTokens const costInfo = input.model.cost?.tiers ?.filter((item) => item.tier.type === "context" && contextTokens > item.tier.size) diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index c76dd98b8614..10e6f3bb509d 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -9,6 +9,7 @@ import * as Stream from "effect/Stream" import { Config } from "@/config/config" import { LLM } from "../../src/session/llm" import { SessionCompaction } from "../../src/session/compaction" +import { isOverflow } from "../../src/session/overflow" import { Token } from "@/util/token" import { Plugin } from "../../src/plugin" import { provideTmpdirInstance, TestInstance } from "../fixture/fixture" @@ -62,6 +63,7 @@ function createModel(opts: { input?: number cost?: Provider.Model["cost"] npm?: string + apiID?: string }): Provider.Model { return { id: "test-model", @@ -81,7 +83,7 @@ function createModel(opts: { input: { text: true, image: false, audio: false, video: false }, output: { text: true, image: false, audio: false, video: false }, }, - api: { npm: opts.npm ?? "@ai-sdk/anthropic" }, + api: { id: opts.apiID ?? "test-model", npm: opts.npm ?? "@ai-sdk/anthropic" }, options: {}, } as Provider.Model } @@ -1704,6 +1706,35 @@ describe("SessionNs.getUsage", () => { expect(result.tokens.cache.read).toBe(200) }) + test("does not double count cached input from Bedrock GPT-5.6", () => { + const model = createModel({ + context: 200_000, + input: 200_000, + output: 64_000, + npm: "@ai-sdk/amazon-bedrock", + apiID: "us.openai.gpt-5.6-luna", + }) + const result = SessionNs.getUsage({ + model, + usage: usage({ + inputTokens: 302_820, + outputTokens: 639, + totalTokens: 303_459, + cacheReadInputTokens: 132_976, + cacheWriteInputTokens: 18_433, + }), + }) + + expect(result.tokens).toEqual({ + total: 152_050, + input: 2, + output: 639, + reasoning: 0, + cache: { read: 132_976, write: 18_433 }, + }) + expect(isOverflow({ cfg: {} as ConfigV1.Info, tokens: result.tokens, model })).toBe(false) + }) + test("handles anthropic cache write metadata", () => { const model = createModel({ context: 100_000, output: 32_000 }) const result = SessionNs.getUsage({