Repository navigation
fix(server): usage prices Claude 1-hour cache writes at their own rate #16922
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
94682ac
3a79b7e
21b5939
0d6a856
3754b62
cccff0f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,6 @@ import type { | |
| UsageCategoryCost, | ||
| UsageCostSource, | ||
| UsageModelPriceOverride, | ||
| UsageTokenTotals, | ||
| } from "@t3tools/contracts"; | ||
|
|
||
| import type { UsageRecord, UsageSpeed } from "@t3tools/provider-core/server/usage"; | ||
|
|
@@ -12,6 +11,8 @@ export interface TokenRates { | |
| readonly outputCostPerToken: number; | ||
| readonly cacheReadCostPerToken: number; | ||
| readonly cacheCreationCostPerToken: number; | ||
| /** Cache writes with a 1-hour TTL. Equals the 5-minute rate when unpublished. */ | ||
| readonly cacheCreation1hCostPerToken: number; | ||
| } | ||
|
|
||
| /** Omits LiteLLM's above_272k_tokens, flex and batches variants, which transcripts do not record. */ | ||
|
|
@@ -26,19 +27,23 @@ export function createOverrideRateTable( | |
| overrides: Readonly<Record<string, UsageModelPriceOverride>>, | ||
| ): RateTable { | ||
| return new Map( | ||
| Object.entries(overrides).map(([model, prices]) => [ | ||
| model.trim(), | ||
| { | ||
| inputCostPerToken: prices.inputCostPerMillionTokens / 1_000_000, | ||
| outputCostPerToken: prices.outputCostPerMillionTokens / 1_000_000, | ||
| cacheReadCostPerToken: | ||
| (prices.cacheReadCostPerMillionTokens ?? prices.inputCostPerMillionTokens) / 1_000_000, | ||
| cacheCreationCostPerToken: | ||
| (prices.cacheWriteCostPerMillionTokens ?? prices.inputCostPerMillionTokens) / 1_000_000, | ||
| fast: null, | ||
| ultrafast: null, | ||
| }, | ||
| ]), | ||
| Object.entries(overrides).map(([model, prices]) => { | ||
| const cacheCreationCostPerToken = | ||
| (prices.cacheWriteCostPerMillionTokens ?? prices.inputCostPerMillionTokens) / 1_000_000; | ||
| return [ | ||
| model.trim(), | ||
| { | ||
| inputCostPerToken: prices.inputCostPerMillionTokens / 1_000_000, | ||
| outputCostPerToken: prices.outputCostPerMillionTokens / 1_000_000, | ||
| cacheReadCostPerToken: | ||
| (prices.cacheReadCostPerMillionTokens ?? prices.inputCostPerMillionTokens) / 1_000_000, | ||
| cacheCreationCostPerToken, | ||
| cacheCreation1hCostPerToken: cacheCreationCostPerToken, | ||
| fast: null, | ||
| ultrafast: null, | ||
| }, | ||
| ]; | ||
| }), | ||
| ); | ||
| } | ||
|
|
||
|
|
@@ -48,7 +53,7 @@ function finiteNumber(value: unknown): number | null { | |
| return typeof value === "number" && Number.isFinite(value) ? value : null; | ||
| } | ||
|
|
||
| // Missing cache rates fall back to input pricing; Anthropic normally discounts reads and charges extra for writes. | ||
| // Missing cache rates fall back to input pricing, and missing 1-hour write rates to the 5-minute rate. | ||
| function readTokenRates( | ||
| entry: LiteLlmEntry, | ||
| suffix: string, | ||
|
|
@@ -57,18 +62,29 @@ function readTokenRates( | |
| const input = finiteNumber(entry[`input_cost_per_token${suffix}`]); | ||
| const output = finiteNumber(entry[`output_cost_per_token${suffix}`]); | ||
| if (input === null || output === null) return null; | ||
| const cacheRate = (name: string, field: "cacheReadCostPerToken" | "cacheCreationCostPerToken") => | ||
| const cacheRate = ( | ||
| name: string, | ||
| field: "cacheReadCostPerToken" | "cacheCreationCostPerToken" | "cacheCreation1hCostPerToken", | ||
| fallback: number, | ||
| ) => | ||
| finiteNumber(entry[`${name}${suffix}`]) ?? | ||
| (standard !== undefined && standard.inputCostPerToken > 0 | ||
| ? (standard[field] / standard.inputCostPerToken) * input | ||
| : input); | ||
| : fallback); | ||
| const cacheCreationCostPerToken = cacheRate( | ||
| "cache_creation_input_token_cost", | ||
| "cacheCreationCostPerToken", | ||
| input, | ||
| ); | ||
| return { | ||
| inputCostPerToken: input, | ||
| outputCostPerToken: output, | ||
| cacheReadCostPerToken: cacheRate("cache_read_input_token_cost", "cacheReadCostPerToken"), | ||
| cacheCreationCostPerToken: cacheRate( | ||
| "cache_creation_input_token_cost", | ||
| "cacheCreationCostPerToken", | ||
| cacheReadCostPerToken: cacheRate("cache_read_input_token_cost", "cacheReadCostPerToken", input), | ||
| cacheCreationCostPerToken, | ||
| cacheCreation1hCostPerToken: cacheRate( | ||
| "cache_creation_input_token_cost_above_1hr", | ||
| "cacheCreation1hCostPerToken", | ||
| cacheCreationCostPerToken, | ||
|
Comment on lines
+84
to
+87
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use the tier's five-minute rate when its one-hour rate is missing.
|
||
| ), | ||
| }; | ||
| } | ||
|
|
@@ -79,6 +95,7 @@ function scaleTokenRates(rates: TokenRates, multiple: number): TokenRates { | |
| outputCostPerToken: rates.outputCostPerToken * multiple, | ||
| cacheReadCostPerToken: rates.cacheReadCostPerToken * multiple, | ||
| cacheCreationCostPerToken: rates.cacheCreationCostPerToken * multiple, | ||
| cacheCreation1hCostPerToken: rates.cacheCreation1hCostPerToken * multiple, | ||
| }; | ||
| } | ||
|
|
||
|
|
@@ -150,7 +167,8 @@ function sameTokenRates(a: TokenRates | null, b: TokenRates | null): boolean { | |
| a.inputCostPerToken === b.inputCostPerToken && | ||
| a.outputCostPerToken === b.outputCostPerToken && | ||
| a.cacheReadCostPerToken === b.cacheReadCostPerToken && | ||
| a.cacheCreationCostPerToken === b.cacheCreationCostPerToken | ||
| a.cacheCreationCostPerToken === b.cacheCreationCostPerToken && | ||
| a.cacheCreation1hCostPerToken === b.cacheCreation1hCostPerToken | ||
| ); | ||
| } | ||
|
|
||
|
|
@@ -228,7 +246,7 @@ function resolveRate(table: RateTable, model: string): ModelRate | null { | |
|
|
||
| export type PricedRecord = Pick< | ||
| UsageRecord, | ||
| "model" | "rateModel" | "totals" | "speed" | "reportedCostUsd" | ||
| "model" | "rateModel" | "totals" | "cacheCreation1hTokens" | "speed" | "reportedCostUsd" | ||
| >; | ||
|
|
||
| export interface PricedUsage { | ||
|
|
@@ -238,11 +256,18 @@ export interface PricedUsage { | |
| readonly speedPremiumUsd: number; | ||
| } | ||
|
|
||
| function costByCategory(totals: UsageTokenTotals, rates: TokenRates): UsageCategoryCost { | ||
| function costByCategory(record: PricedRecord, rates: TokenRates): UsageCategoryCost { | ||
| const { totals } = record; | ||
| const cacheCreation1hTokens = Math.min( | ||
| record.cacheCreation1hTokens ?? 0, | ||
| totals.cacheCreationTokens, | ||
| ); | ||
| return { | ||
| input: totals.uncachedInputTokens * rates.inputCostPerToken, | ||
| cacheRead: totals.cachedInputTokens * rates.cacheReadCostPerToken, | ||
| cacheWrite: totals.cacheCreationTokens * rates.cacheCreationCostPerToken, | ||
| cacheWrite: | ||
| (totals.cacheCreationTokens - cacheCreation1hTokens) * rates.cacheCreationCostPerToken + | ||
| cacheCreation1hTokens * rates.cacheCreation1hCostPerToken, | ||
| output: totals.outputTokens * rates.outputCostPerToken, | ||
| }; | ||
| } | ||
|
|
@@ -257,7 +282,7 @@ export function priceUsage( | |
| record: PricedRecord, | ||
| overrides?: RateTable, | ||
| ): PricedUsage { | ||
| const { model, totals, reportedCostUsd } = record; | ||
| const { model, reportedCostUsd } = record; | ||
| const override = overrides?.get(model.trim()); | ||
| const reported = | ||
| override === undefined && reportedCostUsd !== null && Number.isFinite(reportedCostUsd) | ||
|
|
@@ -274,11 +299,11 @@ export function priceUsage( | |
| return reported === null ? unsplit(0, "unpriced") : unsplit(reported, "providerReported"); | ||
| } | ||
|
|
||
| const listCost = costByCategory(totals, ratesAt(rate, record.speed)); | ||
| const listCost = costByCategory(record, ratesAt(rate, record.speed)); | ||
| const listCostUsd = sumCategories(listCost); | ||
| if (reported !== null && listCostUsd <= 0) return unsplit(reported, "providerReported"); | ||
| const premiumUsd = | ||
| record.speed === "standard" ? 0 : listCostUsd - sumCategories(costByCategory(totals, rate)); | ||
| record.speed === "standard" ? 0 : listCostUsd - sumCategories(costByCategory(record, rate)); | ||
| const scale = reported === null ? 1 : reported / listCostUsd; | ||
| return { | ||
| costUsd: reported ?? listCostUsd, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: pingdotgg/t3code
Length of output: 7842
Script executed:
Repository: pingdotgg/t3code
Length of output: 20024
Web query:
Anthropic API advisor_message iteration usage cache_creation ephemeral_1h_input_tokens advisor message schemaResult:
Script executed:
Repository: pingdotgg/t3code
Length of output: 19719
Script executed:
Repository: pingdotgg/t3code
Length of output: 4666
Carry the one-hour count into advisor records.
Advisor iterations can contain a one-hour cache-write breakdown. The parser drops that count, so pricing can use the five-minute rate for those writes. Carry the count into the advisor record and add a focused parser case.