mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 02:57:36 +00:00
* 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>
58 lines
2.0 KiB
TypeScript
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
|
|
)
|
|
}
|