From a5cca5f5aabe14c7577fbd6500796b3ddd039d61 Mon Sep 17 00:00:00 2001 From: zq Date: Thu, 6 Aug 2026 11:27:36 +0800 Subject: [PATCH] fix(session): warn on cache busts --- packages/opencode/src/session/processor.ts | 8 ++++ packages/opencode/src/session/session.ts | 27 +++++++++++++ .../opencode/test/session/compaction.test.ts | 38 +++++++++++++++++++ 3 files changed, 73 insertions(+) diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 20aa8a8404d8..6b396bf10a3d 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -440,6 +440,14 @@ const layer = Layer.effect( usage: value.usage ?? new Usage({}), metadata: value.providerMetadata, }) + const cacheWarning = Session.cacheBustWarning({ + providerID: ctx.model.providerID, + modelID: ctx.model.id, + sessionID: ctx.sessionID, + finish: value.reason, + usage, + }) + if (cacheWarning) yield* Effect.logWarning("possible Anthropic prompt cache bust", cacheWarning) ctx.assistantMessage.finish = value.reason ctx.assistantMessage.cost += usage.cost ctx.assistantMessage.tokens = usage.tokens diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index de8c3dc4cbd1..a7d7cb83938e 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -406,6 +406,33 @@ export const getUsage = (input: { model: Provider.Model; usage: Usage; metadata? } } +export function cacheBustWarning(input: { + providerID: string + modelID: string + sessionID: string + finish: string + usage: ReturnType +}) { + const cacheWriteTokens = input.usage.tokens.cache.write + const cacheReadTokens = input.usage.tokens.cache.read + if (cacheWriteTokens < 50_000) return undefined + if (cacheReadTokens !== 0) return undefined + + const provider = input.providerID.toLowerCase() + const model = input.modelID.toLowerCase() + if (!provider.includes("anthropic") && !model.includes("claude")) return undefined + + return { + providerID: input.providerID, + modelID: input.modelID, + sessionID: input.sessionID, + finish: input.finish, + cost: input.usage.cost, + cacheWriteTokens, + cacheReadTokens, + } +} + export class BusyError extends Schema.TaggedErrorClass()("SessionBusyError", { sessionID: SessionID, }) {} diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 4a4210cf08bc..e8251ef23c07 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -1579,6 +1579,44 @@ describe("SessionNs.getUsage", () => { expect(result.tokens.cache.write).toBe(300) }) + test("flags likely Anthropic cache busts when a step rewrites cache without reads", () => { + const warning = SessionNs.cacheBustWarning({ + providerID: "anthropic", + modelID: "claude-opus-5", + sessionID: "ses_test", + finish: "tool-calls", + usage: { + cost: 1.63, + tokens: { input: 3, output: 39, reasoning: 0, cache: { read: 0, write: 259_313 }, total: 259_355 }, + }, + }) + + expect(warning).toEqual({ + providerID: "anthropic", + modelID: "claude-opus-5", + sessionID: "ses_test", + finish: "tool-calls", + cost: 1.63, + cacheWriteTokens: 259_313, + cacheReadTokens: 0, + }) + }) + + test("does not flag ordinary cache hits", () => { + const warning = SessionNs.cacheBustWarning({ + providerID: "anthropic", + modelID: "claude-opus-5", + sessionID: "ses_test", + finish: "stop", + usage: { + cost: 0.03, + tokens: { input: 3, output: 39, reasoning: 0, cache: { read: 200_000, write: 0 }, total: 200_042 }, + }, + }) + + expect(warning).toBeUndefined() + }) + test("subtracts cached tokens for anthropic provider", () => { const model = createModel({ context: 100_000, output: 32_000 }) // AI SDK v6 normalizes inputTokens to include cached tokens for all providers