diff --git a/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts b/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts index c85d3f3d178..9b84ebab32b 100644 --- a/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts +++ b/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts @@ -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 LanguageModelV2 { if (prompt_tokens_details?.cached_tokens != null) { usage.promptTokensDetails.cachedTokens = prompt_tokens_details?.cached_tokens } + if (prompt_tokens_details?.cache_write_tokens != null) { + usage.promptTokensDetails.cacheWriteTokens = prompt_tokens_details?.cache_write_tokens + } } const choice = value.choices[0] @@ -666,6 +676,11 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 { providerMetadata[providerOptionsName].rejectedPredictionTokens = usage.completionTokensDetails.rejectedPredictionTokens } + if (usage.promptTokensDetails.cacheWriteTokens != null) { + providerMetadata[providerOptionsName].usage = { + cacheWriteInputTokens: usage.promptTokensDetails.cacheWriteTokens, + } + } controller.enqueue({ type: "finish", @@ -696,6 +711,7 @@ const openaiCompatibleTokenUsageSchema = z prompt_tokens_details: z .object({ cached_tokens: z.number().nullish(), + cache_write_tokens: z.number().nullish(), }) .nullish(), completion_tokens_details: z diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index bbb7c97fd25..f93857f1500 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -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, ) diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 452926d12e1..4b2ab83b040 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -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({