Files
orca/src/main/codex-usage/codex-usage-cost-estimate.ts
T
Jinwoo HongandClaude Opus 5.5 2155673da3 fix(usage): price Codex long context per request, not per aggregate (#22360)
* fix(usage): price Codex long context per request, not per aggregate

OpenAI bills a request whose prompt exceeds 272K input tokens at the
long-context rates for that whole request. Orca applied 272K as a
marginal threshold to daily, per-range and per-session token totals,
so ordinary days of short requests were billed mostly at long-context
rates and the summary disagreed with the model breakdown.

Classify each token_count at parse time and carry long-context token
counts through every rollup; the estimate bills that subset at the
long-context rates and the rest at base. Bump the cache schema to 6 so
existing caches rescan and pick up the split.

* fix(usage): never class a total-only Codex delta as long context

A token_count with only running totals yields the increase since the
previous total, which can span several short requests; classing that
aggregate as one >272K request would bill short requests at long-context
rates. Only a last_token_usage record is one response. Also note that
OpenAI's GPT-5.4/5.5 pages say "full session" where 5.6/6 say "full
request"; the per-request reading is the stated assumption.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 20:33:18 -04:00

58 lines
2.0 KiB
TypeScript

import { MODEL_PRICING, normalizeModelForPricing } from './codex-model-pricing'
/** Token counts to price. The long-context fields are the subset of each total that came from
* requests whose prompt exceeded the long-context threshold. */
export type CodexBillableTokens = {
inputTokens: number
cachedInputTokens: number
outputTokens: number
longContextInputTokens: number
longContextCachedInputTokens: number
longContextOutputTokens: number
}
function priceTokens(
rates: { input: number; cachedInput: number; output: number },
inputTokens: number,
cachedInputTokens: number,
outputTokens: number
): number {
const clampedCached = Math.min(Math.max(cachedInputTokens, 0), Math.max(inputTokens, 0))
// Why: Codex cached tokens are part of the input bucket. Charge uncached
// input on (input-cached) so cached tokens are not billed once at full input
// price and again at cache-read price.
const nonCachedInputTokens = Math.max(inputTokens - clampedCached, 0)
return (
nonCachedInputTokens * rates.input +
clampedCached * rates.cachedInput +
Math.max(outputTokens, 0) * rates.output
)
}
export function estimateCostUsd(model: string | null, tokens: CodexBillableTokens): number | null {
const normalized = normalizeModelForPricing(model)
if (!normalized) {
return null
}
const pricing = MODEL_PRICING[normalized]
if (!pricing.longContext) {
return (
priceTokens(pricing, tokens.inputTokens, tokens.cachedInputTokens, tokens.outputTokens) /
1_000_000
)
}
const longInput = Math.min(tokens.longContextInputTokens, tokens.inputTokens)
const longCached = Math.min(tokens.longContextCachedInputTokens, tokens.cachedInputTokens)
const longOutput = Math.min(tokens.longContextOutputTokens, tokens.outputTokens)
return (
(priceTokens(
pricing,
tokens.inputTokens - longInput,
tokens.cachedInputTokens - longCached,
tokens.outputTokens - longOutput
) +
priceTokens(pricing.longContext, longInput, longCached, longOutput)) /
1_000_000
)
}