From 726df355dc61a17a57cc6c2846592d82ff4fd707 Mon Sep 17 00:00:00 2001 From: Santhi Prakash Date: Tue, 18 Aug 2026 20:23:01 +0000 Subject: [PATCH 1/2] fix(core): price OpenAI prompt-cache input at the documented cached rate (#13104) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit calculateOpenAICost() previously billed every prompt token at the standard input rate, ignoring usage.promptTokensDetails.cached_tokens. For OpenAI requests that hit the prompt cache (gpt-4o / gpt-4o-mini with stable system prompts), this over-reported costs by the cache-read discount. Add cachedInput to the pricing table for the families that document a cache rate (gpt-4o, gpt-4o-mini), split prompt tokens into uncached and cached portions, and bill each at its own rate. Models without a documented cachedInput rate keep their existing behavior — cachedTokens is ignored and the full prompt is billed at the standard input rate. Includes regression tests covering: gpt-4o with partial cache, fully cached input, zero cached tokens, gpt-4o-mini, gpt-4 (no rate defined), and clamping cachedTokens when the API reports more cached than total prompt tokens. Verified by extracting calculateOpenAICost into a standalone Node script and running 11 cases against hand-computed expected costs (all pass). --- core/llm/utils/calculateRequestCost.ts | 53 +++++++++++++--- core/llm/utils/calculateRequestCost.vitest.ts | 62 +++++++++++++++++++ 2 files changed, 107 insertions(+), 8 deletions(-) diff --git a/core/llm/utils/calculateRequestCost.ts b/core/llm/utils/calculateRequestCost.ts index 794524d448d..b0a2264a911 100644 --- a/core/llm/utils/calculateRequestCost.ts +++ b/core/llm/utils/calculateRequestCost.ts @@ -155,11 +155,18 @@ 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 set only for model families where OpenAI documents a + // prompt-cache rate. prompt_tokens from the API includes the cached portion, + // so the input cost is split: uncached tokens at the input rate and cached + // tokens at the cachedInput 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 +195,46 @@ function calculateOpenAICost( return null; // Unknown model } - // Calculate costs - const inputCost = (usage.promptTokens / 1_000_000) * modelPricing.input; + // Split prompt tokens into uncached and cached portions. prompt_tokens from + // the OpenAI usage object already includes cached tokens; cached_tokens is + // reported under prompt_tokens_details.cached_tokens. cachedTokens is + // clamped to promptTokens to defend against malformed usage payloads that + // report more cached tokens than total prompt tokens. If the model has no + // documented cachedInput rate, the cached portion is not subtracted from + // the input cost — those prompt tokens are billed at the standard input + // rate (the API wouldn't have returned a cached_tokens value for them in + // the first place). + const reportedCachedTokens = + usage.promptTokensDetails?.cachedTokens ?? 0; + const cachedTokens = + modelPricing.cachedInput !== undefined + ? Math.min(reportedCachedTokens, usage.promptTokens) + : 0; + const uncachedPromptTokens = usage.promptTokens - cachedTokens; + + const cachedInputCost = + cachedTokens > 0 && modelPricing.cachedInput !== undefined + ? (cachedTokens / 1_000_000) * modelPricing.cachedInput + : 0; + const inputCost = + (uncachedPromptTokens / 1_000_000) * modelPricing.input + cachedInputCost; const outputCost = (usage.completionTokens / 1_000_000) * modelPricing.output; // Build breakdown components const breakdownParts: string[] = []; - if (usage.promptTokens > 0) { + if (uncachedPromptTokens > 0) { breakdownParts.push( - `Input: ${usage.promptTokens.toLocaleString()} tokens × $${modelPricing.input}/MTok = $${inputCost.toFixed(6)}`, + `Input: ${uncachedPromptTokens.toLocaleString()} tokens × $${modelPricing.input}/MTok = $${( + (uncachedPromptTokens / 1_000_000) * + modelPricing.input + ).toFixed(6)}`, + ); + } + + if (cachedTokens > 0 && modelPricing.cachedInput !== undefined) { + breakdownParts.push( + `Cached Input: ${cachedTokens.toLocaleString()} tokens × $${modelPricing.cachedInput}/MTok = $${cachedInputCost.toFixed(6)}`, ); } diff --git a/core/llm/utils/calculateRequestCost.vitest.ts b/core/llm/utils/calculateRequestCost.vitest.ts index 32183ca1b49..9a482908420 100644 --- a/core/llm/utils/calculateRequestCost.vitest.ts +++ b/core/llm/utils/calculateRequestCost.vitest.ts @@ -163,6 +163,68 @@ describe("calculateRequestCost", () => { description: "GPT-3.5 Turbo", }, + // OpenAI GPT-4o with prompt caching (cached_input is half of input) + { + provider: "openai", + model: "gpt-4o", + promptTokens: 1000, + completionTokens: 500, + cachedTokens: 700, + expectedCost: 0.006625, + description: "GPT-4o with cached input tokens (uncached 300 + cached 700)", + }, + { + provider: "openai", + model: "gpt-4o", + promptTokens: 1000, + completionTokens: 500, + cachedTokens: 1000, + expectedCost: 0.00625, + description: "GPT-4o fully cached input", + }, + { + provider: "openai", + model: "gpt-4o", + promptTokens: 1000, + completionTokens: 500, + cachedTokens: 0, + expectedCost: 0.0075, + description: "GPT-4o with explicit zero cached tokens (no cache rows)", + }, + + // OpenAI GPT-4o-mini with prompt caching + { + provider: "openai", + model: "gpt-4o-mini", + promptTokens: 1000, + completionTokens: 200, + cachedTokens: 800, + expectedCost: 0.00021, + description: "GPT-4o-mini with cached input tokens (uncached 200 + cached 800)", + }, + + // OpenAI GPT-4 (no documented cached rate) — cached_tokens present but ignored + { + provider: "openai", + model: "gpt-4", + promptTokens: 1000, + completionTokens: 100, + cachedTokens: 500, + expectedCost: 0.036, + description: "GPT-4 ignores cachedTokens because no cachedInput rate is defined", + }, + + // Clamp cachedTokens > promptTokens (malformed payloads) + { + provider: "openai", + model: "gpt-4o", + promptTokens: 1000, + completionTokens: 0, + cachedTokens: 1500, + expectedCost: 0.00125, + description: "GPT-4o clamps cachedTokens to promptTokens (all input treated as cached)", + }, + // Edge cases { provider: "anthropic", From f8b31e6a0b1f27a8a20a0d4337a7d9eedf3c1780 Mon Sep 17 00:00:00 2001 From: Santhi Prakash Date: Tue, 18 Aug 2026 20:29:21 +0000 Subject: [PATCH 2/2] style(core): apply prettier formatting to calculateRequestCost (#13104) Fix the prettier-check failure on PR #13160 introduced by the previous commit's hand-formatted long lines. --- core/llm/utils/calculateRequestCost.ts | 3 +-- core/llm/utils/calculateRequestCost.vitest.ts | 12 ++++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/core/llm/utils/calculateRequestCost.ts b/core/llm/utils/calculateRequestCost.ts index b0a2264a911..10cb2b3ab38 100644 --- a/core/llm/utils/calculateRequestCost.ts +++ b/core/llm/utils/calculateRequestCost.ts @@ -204,8 +204,7 @@ function calculateOpenAICost( // the input cost — those prompt tokens are billed at the standard input // rate (the API wouldn't have returned a cached_tokens value for them in // the first place). - const reportedCachedTokens = - usage.promptTokensDetails?.cachedTokens ?? 0; + const reportedCachedTokens = usage.promptTokensDetails?.cachedTokens ?? 0; const cachedTokens = modelPricing.cachedInput !== undefined ? Math.min(reportedCachedTokens, usage.promptTokens) diff --git a/core/llm/utils/calculateRequestCost.vitest.ts b/core/llm/utils/calculateRequestCost.vitest.ts index 9a482908420..6058e4b5b79 100644 --- a/core/llm/utils/calculateRequestCost.vitest.ts +++ b/core/llm/utils/calculateRequestCost.vitest.ts @@ -171,7 +171,8 @@ describe("calculateRequestCost", () => { completionTokens: 500, cachedTokens: 700, expectedCost: 0.006625, - description: "GPT-4o with cached input tokens (uncached 300 + cached 700)", + description: + "GPT-4o with cached input tokens (uncached 300 + cached 700)", }, { provider: "openai", @@ -200,7 +201,8 @@ describe("calculateRequestCost", () => { completionTokens: 200, cachedTokens: 800, expectedCost: 0.00021, - description: "GPT-4o-mini with cached input tokens (uncached 200 + cached 800)", + description: + "GPT-4o-mini with cached input tokens (uncached 200 + cached 800)", }, // OpenAI GPT-4 (no documented cached rate) — cached_tokens present but ignored @@ -211,7 +213,8 @@ describe("calculateRequestCost", () => { completionTokens: 100, cachedTokens: 500, expectedCost: 0.036, - description: "GPT-4 ignores cachedTokens because no cachedInput rate is defined", + description: + "GPT-4 ignores cachedTokens because no cachedInput rate is defined", }, // Clamp cachedTokens > promptTokens (malformed payloads) @@ -222,7 +225,8 @@ describe("calculateRequestCost", () => { completionTokens: 0, cachedTokens: 1500, expectedCost: 0.00125, - description: "GPT-4o clamps cachedTokens to promptTokens (all input treated as cached)", + description: + "GPT-4o clamps cachedTokens to promptTokens (all input treated as cached)", }, // Edge cases