From fa51e12fa10a106738f56a4ed72efad8ad51eecd Mon Sep 17 00:00:00 2001 From: Tony Coder <407243179@qq.com> Date: Mon, 17 Aug 2026 08:54:53 +0000 Subject: [PATCH] fix(core): price OpenAI cached input tokens at the cache read rate OpenAI includes cached input in prompt_tokens and bills it at a discount, but calculateOpenAICost charged every prompt token at the full input rate, over-costing cache hits. Adds a cachedInput rate for the models that support prompt caching, prices the cached portion separately from the remaining input tokens, and shows a Cache Read line in the breakdown like the Anthropic branch does. Co-authored-by: Tony Coder <407243179@qq.com> --- core/llm/utils/calculateRequestCost.ts | 37 ++++++++++--- core/llm/utils/calculateRequestCost.vitest.ts | 53 +++++++++++++++++++ 2 files changed, 82 insertions(+), 8 deletions(-) diff --git a/core/llm/utils/calculateRequestCost.ts b/core/llm/utils/calculateRequestCost.ts index 794524d448d..994c7ba3f19 100644 --- a/core/llm/utils/calculateRequestCost.ts +++ b/core/llm/utils/calculateRequestCost.ts @@ -155,11 +155,16 @@ function calculateOpenAICost( // Normalize model name const normalizedModel = model.toLowerCase(); - // Define pricing per million tokens (MTok) by model family prefix - const pricing: Record = { + // Define pricing per million tokens (MTok) by model family prefix. + // cachedInput is only set for models that support prompt caching; the others + // bill cached input, if it is ever reported, at the standard input rate. + const pricing: Record< + string, + { input: number; output: number; cachedInput?: number } + > = { // GPT-4o models (most specific first) - "gpt-4o-mini": { input: 0.15, output: 0.6 }, - "gpt-4o": { input: 2.5, output: 10 }, + "gpt-4o-mini": { input: 0.15, output: 0.6, cachedInput: 0.075 }, + "gpt-4o": { input: 2.5, output: 10, cachedInput: 1.25 }, // GPT-4 Turbo models "gpt-4-turbo": { input: 10, output: 30 }, @@ -188,16 +193,26 @@ function calculateOpenAICost( return null; // Unknown model } + // OpenAI reports cached input as part of promptTokens, so the cached portion + // has to be priced separately instead of at the full input rate + const cacheReadRate = modelPricing.cachedInput ?? modelPricing.input; + const cachedTokens = Math.min( + Math.max(usage.promptTokensDetails?.cachedTokens ?? 0, 0), + usage.promptTokens, + ); + const uncachedTokens = usage.promptTokens - cachedTokens; + // Calculate costs - const inputCost = (usage.promptTokens / 1_000_000) * modelPricing.input; + const inputCost = (uncachedTokens / 1_000_000) * modelPricing.input; const outputCost = (usage.completionTokens / 1_000_000) * modelPricing.output; + const cacheReadCost = (cachedTokens / 1_000_000) * cacheReadRate; // Build breakdown components const breakdownParts: string[] = []; - if (usage.promptTokens > 0) { + if (uncachedTokens > 0) { breakdownParts.push( - `Input: ${usage.promptTokens.toLocaleString()} tokens × $${modelPricing.input}/MTok = $${inputCost.toFixed(6)}`, + `Input: ${uncachedTokens.toLocaleString()} tokens × $${modelPricing.input}/MTok = $${inputCost.toFixed(6)}`, ); } @@ -207,7 +222,13 @@ function calculateOpenAICost( ); } - const totalCost = inputCost + outputCost; + if (cachedTokens > 0) { + breakdownParts.push( + `Cache Read: ${cachedTokens.toLocaleString()} tokens × $${cacheReadRate}/MTok = $${cacheReadCost.toFixed(6)}`, + ); + } + + const totalCost = inputCost + outputCost + cacheReadCost; // Build final breakdown string let breakdown = `Model: ${model}\n`; diff --git a/core/llm/utils/calculateRequestCost.vitest.ts b/core/llm/utils/calculateRequestCost.vitest.ts index 32183ca1b49..56783448c0d 100644 --- a/core/llm/utils/calculateRequestCost.vitest.ts +++ b/core/llm/utils/calculateRequestCost.vitest.ts @@ -163,6 +163,44 @@ describe("calculateRequestCost", () => { description: "GPT-3.5 Turbo", }, + // OpenAI prompt caching (promptTokens includes cachedTokens) + { + provider: "openai", + model: "gpt-4o", + promptTokens: 1000, + completionTokens: 500, + cachedTokens: 800, + expectedCost: 0.0065, + description: "GPT-4o with cache reads", + }, + { + provider: "openai", + model: "gpt-4o-mini", + promptTokens: 1000, + completionTokens: 500, + cachedTokens: 1000, + expectedCost: 0.000375, + description: "GPT-4o-mini fully cached input", + }, + { + provider: "openai", + model: "gpt-4-turbo", + promptTokens: 1000, + completionTokens: 500, + cachedTokens: 400, + expectedCost: 0.025, + description: "GPT-4 Turbo cache reads bill at the input rate", + }, + { + provider: "openai", + model: "gpt-4o", + promptTokens: 500, + completionTokens: 0, + cachedTokens: 1000, + expectedCost: 0.000625, + description: "GPT-4o cached tokens exceeding prompt tokens", + }, + // Edge cases { provider: "anthropic", @@ -253,4 +291,19 @@ describe("calculateRequestCost", () => { }); }, ); + + it("excludes cached tokens from the OpenAI input breakdown", () => { + const result = calculateRequestCost("openai", "gpt-4o", { + promptTokens: 1000, + completionTokens: 500, + promptTokensDetails: { cachedTokens: 800 }, + }); + + expect(result!.breakdown).toContain( + "Input: 200 tokens × $2.5/MTok = $0.000500", + ); + expect(result!.breakdown).toContain( + "Cache Read: 800 tokens × $1.25/MTok = $0.001000", + ); + }); });