mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 16:02:56 +00:00
The three usage scanners and their stores, plus the renderer usage-overview model, each carried a file-level `eslint-disable max-lines` and had grown to 338-769 counted lines against a 300-line budget. AGENTS.md calls for splitting rather than suppressing, and config/max-lines-baseline.txt is a shrink-only ratchet, so this removes all seven suppressions and prunes their entries (341 -> 334). Each file is cut along the seams it already had -- and that several of the suppression comments named out loud: filesystem discovery / record parsing / attribution / aggregation for the scanners, and pricing policy / scope filters / rollups / session rows / automation attribution for the stores. Pure move, no behavior change. Code is relocated verbatim; the only edits are import plumbing and, where a private class method became a free function, the mechanical `this.state` -> `state` parameter threading. Every converted call site passes `this.state` at call time and the automation path takes a live `getState: () => this.state` getter, so no state is snapshotted. No barrel exports: each new module owns real logic and importers point at the owner. Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green (remaining failures are pre-existing load flakes in untouched files, each green when re-run serially), no import cycles among the 64 affected modules, and a statement-level diff of every split confirms the moves are verbatim.
179 lines
6.0 KiB
TypeScript
179 lines
6.0 KiB
TypeScript
import { ensureNumber } from '../usage/usage-record-coercion'
|
|
|
|
export type CodexUsageRawUsage = {
|
|
inputTokens: number
|
|
cachedInputTokens: number
|
|
outputTokens: number
|
|
reasoningOutputTokens: number
|
|
totalTokens: number
|
|
}
|
|
|
|
type CodexUsageDeltaResolution =
|
|
| { kind: 'event'; delta: CodexUsageRawUsage; nextTotals: CodexUsageRawUsage | null }
|
|
| { kind: 'baseline'; nextTotals: CodexUsageRawUsage }
|
|
|
|
export function normalizeRawUsage(value: unknown): CodexUsageRawUsage | null {
|
|
if (value == null || typeof value !== 'object') {
|
|
return null
|
|
}
|
|
|
|
const record = value as Record<string, unknown>
|
|
const inputTokens = ensureNumber(record.input_tokens)
|
|
const cachedInputTokens = ensureNumber(
|
|
record.cached_input_tokens ?? record.cache_read_input_tokens
|
|
)
|
|
const outputTokens = ensureNumber(record.output_tokens)
|
|
const reasoningOutputTokens = ensureNumber(record.reasoning_output_tokens)
|
|
const totalTokens = ensureNumber(record.total_tokens)
|
|
|
|
return {
|
|
inputTokens,
|
|
cachedInputTokens,
|
|
outputTokens,
|
|
reasoningOutputTokens,
|
|
// Why: legacy Codex logs can omit total_tokens. Reasoning is already billed
|
|
// inside output, so synthesizing input+output matches Codex pricing instead
|
|
// of double-counting reasoning as another billable bucket.
|
|
totalTokens: totalTokens > 0 ? totalTokens : inputTokens + outputTokens
|
|
}
|
|
}
|
|
|
|
function subtractRawUsage(
|
|
current: CodexUsageRawUsage,
|
|
previous: CodexUsageRawUsage | null
|
|
): CodexUsageRawUsage {
|
|
return {
|
|
inputTokens: Math.max(current.inputTokens - (previous?.inputTokens ?? 0), 0),
|
|
cachedInputTokens: Math.max(current.cachedInputTokens - (previous?.cachedInputTokens ?? 0), 0),
|
|
outputTokens: Math.max(current.outputTokens - (previous?.outputTokens ?? 0), 0),
|
|
reasoningOutputTokens: Math.max(
|
|
current.reasoningOutputTokens - (previous?.reasoningOutputTokens ?? 0),
|
|
0
|
|
),
|
|
totalTokens: Math.max(current.totalTokens - (previous?.totalTokens ?? 0), 0)
|
|
}
|
|
}
|
|
|
|
function addRawUsage(left: CodexUsageRawUsage, right: CodexUsageRawUsage): CodexUsageRawUsage {
|
|
return {
|
|
inputTokens: left.inputTokens + right.inputTokens,
|
|
cachedInputTokens: left.cachedInputTokens + right.cachedInputTokens,
|
|
outputTokens: left.outputTokens + right.outputTokens,
|
|
reasoningOutputTokens: left.reasoningOutputTokens + right.reasoningOutputTokens,
|
|
totalTokens: left.totalTokens + right.totalTokens
|
|
}
|
|
}
|
|
|
|
function rawUsageEquals(left: CodexUsageRawUsage, right: CodexUsageRawUsage): boolean {
|
|
return (
|
|
left.inputTokens === right.inputTokens &&
|
|
left.cachedInputTokens === right.cachedInputTokens &&
|
|
left.outputTokens === right.outputTokens &&
|
|
left.reasoningOutputTokens === right.reasoningOutputTokens
|
|
)
|
|
}
|
|
|
|
function rawUsageIsMonotonic(current: CodexUsageRawUsage, previous: CodexUsageRawUsage): boolean {
|
|
return (
|
|
current.inputTokens >= previous.inputTokens &&
|
|
current.cachedInputTokens >= previous.cachedInputTokens &&
|
|
current.outputTokens >= previous.outputTokens &&
|
|
current.reasoningOutputTokens >= previous.reasoningOutputTokens
|
|
)
|
|
}
|
|
|
|
function rawUsageMagnitude(usage: CodexUsageRawUsage): number {
|
|
return (
|
|
usage.inputTokens + usage.cachedInputTokens + usage.outputTokens + usage.reasoningOutputTokens
|
|
)
|
|
}
|
|
|
|
function looksLikeStaleRegression(
|
|
current: CodexUsageRawUsage,
|
|
previous: CodexUsageRawUsage,
|
|
last: CodexUsageRawUsage
|
|
): boolean {
|
|
const previousTotal = rawUsageMagnitude(previous)
|
|
const currentTotal = rawUsageMagnitude(current)
|
|
const lastTotal = rawUsageMagnitude(last)
|
|
if (previousTotal <= 0 || currentTotal <= 0 || lastTotal <= 0) {
|
|
return false
|
|
}
|
|
return currentTotal * 100 >= previousTotal * 98 || currentTotal + lastTotal * 2 >= previousTotal
|
|
}
|
|
|
|
export function resolveCodexUsageDelta(
|
|
totalUsage: CodexUsageRawUsage | null,
|
|
lastUsage: CodexUsageRawUsage | null,
|
|
previousTotals: CodexUsageRawUsage | null
|
|
): CodexUsageDeltaResolution | null {
|
|
if (totalUsage && lastUsage && previousTotals) {
|
|
if (rawUsageEquals(totalUsage, previousTotals)) {
|
|
return null
|
|
}
|
|
if (
|
|
!rawUsageIsMonotonic(totalUsage, previousTotals) &&
|
|
looksLikeStaleRegression(totalUsage, previousTotals, lastUsage)
|
|
) {
|
|
return null
|
|
}
|
|
// Why: Codex totals are mutable snapshots after compaction/resume. The
|
|
// last_token_usage payload is the billable increment; totals are the baseline.
|
|
return { kind: 'event', delta: lastUsage, nextTotals: totalUsage }
|
|
}
|
|
|
|
if (totalUsage && lastUsage) {
|
|
return { kind: 'event', delta: lastUsage, nextTotals: totalUsage }
|
|
}
|
|
|
|
if (totalUsage && previousTotals) {
|
|
if (rawUsageEquals(totalUsage, previousTotals)) {
|
|
return null
|
|
}
|
|
if (!rawUsageIsMonotonic(totalUsage, previousTotals)) {
|
|
return { kind: 'baseline', nextTotals: totalUsage }
|
|
}
|
|
return {
|
|
kind: 'event',
|
|
delta: subtractRawUsage(totalUsage, previousTotals),
|
|
nextTotals: totalUsage
|
|
}
|
|
}
|
|
|
|
if (totalUsage) {
|
|
return { kind: 'event', delta: totalUsage, nextTotals: totalUsage }
|
|
}
|
|
|
|
if (lastUsage && previousTotals) {
|
|
return { kind: 'event', delta: lastUsage, nextTotals: addRawUsage(previousTotals, lastUsage) }
|
|
}
|
|
|
|
if (lastUsage) {
|
|
return { kind: 'event', delta: lastUsage, nextTotals: null }
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
export function buildCodexUsageEventKey(
|
|
timestamp: string,
|
|
totalUsage: CodexUsageRawUsage | null,
|
|
lastUsage: CodexUsageRawUsage | null
|
|
): string {
|
|
// Why: fork/resume copies token_count records byte-for-byte into a new
|
|
// rollout file, but session_meta.id is often rewritten to the new session.
|
|
// Key only on the raw record fields (timestamp + usage tuples) so the copy
|
|
// matches the original regardless of surrounding parse context / session id.
|
|
const tupleOf = (usage: CodexUsageRawUsage | null): string =>
|
|
usage
|
|
? [
|
|
usage.inputTokens,
|
|
usage.cachedInputTokens,
|
|
usage.outputTokens,
|
|
usage.reasoningOutputTokens,
|
|
usage.totalTokens
|
|
].join(',')
|
|
: ''
|
|
return [timestamp, tupleOf(totalUsage), tupleOf(lastUsage)].join('|')
|
|
}
|