Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 29 additions & 8 deletions core/llm/utils/calculateRequestCost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, { input: number; output: number }> = {
// 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 },
Expand Down Expand Up @@ -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)}`,
);
}

Expand All @@ -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`;
Expand Down
53 changes: 53 additions & 0 deletions core/llm/utils/calculateRequestCost.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
);
});
});
Loading