mirror of
https://github.com/stablyai/orca.git
synced 2026-11-06 03:57:35 +00:00
Codex usage no longer double-counts forked sessions: forks copy the parent's token_count events with fresh timestamps, so cumulative events are now keyed on turn id + totals (timestamp fallback when no turn id); the turn id is kept in the incremental-scan checkpoint and the usage cache is rebuilt once. Fixes #19139 Supersedes #19152 Co-authored-by: Jio Kim <26668612+MeroZemory@users.noreply.github.com>
173 lines
5.2 KiB
TypeScript
173 lines
5.2 KiB
TypeScript
import { extractString } from '../usage/usage-record-coercion'
|
|
import {
|
|
buildCodexUsageEventKey,
|
|
normalizeRawUsage,
|
|
resolveCodexUsageDelta,
|
|
type CodexUsageRawUsage
|
|
} from './codex-usage-token-delta'
|
|
import { LONG_CONTEXT_THRESHOLD_TOKENS } from './codex-model-pricing'
|
|
import type { CodexUsageParsedEvent } from './types'
|
|
|
|
type CodexUsageRawRecord = {
|
|
timestamp?: string
|
|
type?: string
|
|
payload?: Record<string, unknown>
|
|
}
|
|
|
|
export type CodexUsageParseContext = {
|
|
sessionId: string
|
|
sessionCwd: string | null
|
|
currentCwd: string | null
|
|
currentModel: string | null
|
|
currentTurnId?: string | null
|
|
previousTotals: CodexUsageRawUsage | null
|
|
totalOnlyBaselinePending?: boolean
|
|
}
|
|
|
|
function extractModel(value: unknown): string | null {
|
|
if (value == null || typeof value !== 'object') {
|
|
return null
|
|
}
|
|
|
|
const record = value as Record<string, unknown>
|
|
const direct = [extractString(record.model), extractString(record.model_name)].find(
|
|
(candidate) => candidate !== null
|
|
)
|
|
if (direct) {
|
|
return direct
|
|
}
|
|
|
|
if (record.info && typeof record.info === 'object') {
|
|
const info = record.info as Record<string, unknown>
|
|
const infoDirect = [extractString(info.model), extractString(info.model_name)].find(
|
|
(candidate) => candidate !== null
|
|
)
|
|
if (infoDirect) {
|
|
return infoDirect
|
|
}
|
|
if (info.metadata && typeof info.metadata === 'object') {
|
|
const metadata = info.metadata as Record<string, unknown>
|
|
const metadataModel = extractString(metadata.model)
|
|
if (metadataModel) {
|
|
return metadataModel
|
|
}
|
|
}
|
|
}
|
|
|
|
if (record.metadata && typeof record.metadata === 'object') {
|
|
const metadata = record.metadata as Record<string, unknown>
|
|
return extractString(metadata.model)
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
export function parseCodexUsageRecord(
|
|
line: string,
|
|
context: CodexUsageParseContext
|
|
): CodexUsageParsedEvent | null {
|
|
let parsed: CodexUsageRawRecord
|
|
try {
|
|
parsed = JSON.parse(line) as CodexUsageRawRecord
|
|
} catch {
|
|
return null
|
|
}
|
|
|
|
if (!parsed.type || !parsed.payload) {
|
|
return null
|
|
}
|
|
|
|
if (parsed.type === 'session_meta') {
|
|
context.sessionId = extractString(parsed.payload.id) ?? context.sessionId
|
|
context.sessionCwd = extractString(parsed.payload.cwd)
|
|
if (!context.currentCwd && context.sessionCwd) {
|
|
context.currentCwd = context.sessionCwd
|
|
}
|
|
return null
|
|
}
|
|
|
|
if (parsed.type === 'turn_context') {
|
|
context.currentTurnId = extractString(parsed.payload.turn_id)
|
|
context.currentCwd =
|
|
extractString(parsed.payload.cwd) ?? context.currentCwd ?? context.sessionCwd
|
|
context.currentModel = extractModel(parsed.payload) ?? context.currentModel
|
|
return null
|
|
}
|
|
|
|
if (parsed.type !== 'event_msg' || parsed.payload.type !== 'token_count' || !parsed.timestamp) {
|
|
return null
|
|
}
|
|
|
|
const info = parsed.payload.info
|
|
if (info == null || typeof info !== 'object') {
|
|
// Rate-limit updates legitimately emit token_count records with null info.
|
|
return null
|
|
}
|
|
|
|
const record = info as Record<string, unknown>
|
|
const totalUsage = normalizeRawUsage(record.total_token_usage)
|
|
const lastUsage = normalizeRawUsage(record.last_token_usage)
|
|
if (context.totalOnlyBaselinePending) {
|
|
context.totalOnlyBaselinePending = false
|
|
if (totalUsage && !lastUsage && !context.previousTotals) {
|
|
context.previousTotals = totalUsage
|
|
return null
|
|
}
|
|
}
|
|
const resolvedUsage = resolveCodexUsageDelta(totalUsage, lastUsage, context.previousTotals)
|
|
if (!resolvedUsage) {
|
|
return null
|
|
}
|
|
if (resolvedUsage.kind === 'baseline') {
|
|
context.previousTotals = resolvedUsage.nextTotals
|
|
return null
|
|
}
|
|
|
|
const delta = {
|
|
...resolvedUsage.delta,
|
|
cachedInputTokens: Math.min(
|
|
resolvedUsage.delta.cachedInputTokens,
|
|
resolvedUsage.delta.inputTokens
|
|
)
|
|
}
|
|
|
|
if (
|
|
delta.inputTokens === 0 &&
|
|
delta.cachedInputTokens === 0 &&
|
|
delta.outputTokens === 0 &&
|
|
delta.reasoningOutputTokens === 0 &&
|
|
delta.totalTokens === 0
|
|
) {
|
|
return null
|
|
}
|
|
|
|
context.previousTotals = resolvedUsage.nextTotals
|
|
|
|
// Price long context per request; total-only deltas may span several requests.
|
|
const isLongContext = lastUsage !== null && delta.inputTokens > LONG_CONTEXT_THRESHOLD_TOKENS
|
|
const resolvedModel = extractModel(parsed.payload) ?? context.currentModel
|
|
const hasInferredPricing = resolvedModel === null
|
|
|
|
return {
|
|
sessionId: context.sessionId,
|
|
timestamp: parsed.timestamp,
|
|
eventKey: buildCodexUsageEventKey(
|
|
// Fork copies rewrite timestamps but keep turn ids; last-only deltas need the timestamp to stay distinct.
|
|
totalUsage && context.currentTurnId ? context.currentTurnId : parsed.timestamp,
|
|
totalUsage,
|
|
lastUsage
|
|
),
|
|
cwd: context.currentCwd ?? context.sessionCwd,
|
|
model: resolvedModel,
|
|
hasInferredPricing,
|
|
inputTokens: delta.inputTokens,
|
|
cachedInputTokens: delta.cachedInputTokens,
|
|
outputTokens: delta.outputTokens,
|
|
reasoningOutputTokens: delta.reasoningOutputTokens,
|
|
totalTokens: delta.totalTokens,
|
|
longContextInputTokens: isLongContext ? delta.inputTokens : 0,
|
|
longContextCachedInputTokens: isLongContext ? delta.cachedInputTokens : 0,
|
|
longContextOutputTokens: isLongContext ? delta.outputTokens : 0
|
|
}
|
|
}
|