Files
orca/src/main/codex-usage/codex-usage-aggregation.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

37 lines
1.4 KiB
TypeScript

import { createUsageEventAggregation } from '../usage/usage-event-aggregation'
import type { CodexLongContextTokens, CodexUsageAttributedEvent } from './types'
type CodexUsageMetric = { hasInferredPricing: boolean } & CodexLongContextTokens
export const codexUsageAggregation = createUsageEventAggregation<
CodexUsageAttributedEvent,
CodexUsageMetric
>({
metric: {
empty: () => ({
hasInferredPricing: false,
longContextInputTokens: 0,
longContextCachedInputTokens: 0,
longContextOutputTokens: 0
}),
fromEvent: (event) => ({
hasInferredPricing: event.hasInferredPricing,
longContextInputTokens: event.longContextInputTokens,
longContextCachedInputTokens: event.longContextCachedInputTokens,
longContextOutputTokens: event.longContextOutputTokens
}),
fold: (target, source) => {
target.hasInferredPricing ||= source.hasInferredPricing
target.longContextInputTokens += source.longContextInputTokens
target.longContextCachedInputTokens += source.longContextCachedInputTokens
target.longContextOutputTokens += source.longContextOutputTokens
}
},
cloneSessionForMerge: (session) => ({
...session,
locationBreakdown: session.locationBreakdown.map((entry) => ({ ...entry })),
modelBreakdown: session.modelBreakdown.map((entry) => ({ ...entry })),
locationModelBreakdown: session.locationModelBreakdown.map((entry) => ({ ...entry }))
})
})