Two distinct usage metrics: BILLING vs CONTEXT WINDOW. Token consumption has two orthogonal meanings, and conflating them is the most common Codex usage bug:
| Metric | Field on result | Bounded by | Use for |
|---|
| USAGE BILLING TOKENS | result.usage (per-call) | unbounded across resumed turns (replay re-billed at cache rate) | cost, billing alarms, USD estimation |
| USAGE CONTEXT WINDOW | result.contextSize | model's window (e.g. 400k for gpt-5-codex) | "tokens left", IDE-style X / 400k utilization bar |
contextSize = usage.inputTokens + usage.outputTokens for the LAST turn (NOT a sum across turns). It tells you how full the model's window is now. usage summed across turns tells you how many tokens you paid for total — that sum can far exceed the window, because every resumed turn re-bills the replayed history (mostly as cheap cache reads).
BILLING — cumulative-as-delta conversion. Codex is the only wrapped SDK that reports session-level cumulative usage in turn.completed.usage — see openai/codex#17539 (event_processor_with_jsonl_output.rs::usage_from_last_total drops the rust core's per-request ThreadTokenUsage.last and only emits .total). The unified contract in src/types.ts requires per-execute() delta in result.usage (consistent with claude-code/gemini/opencode), so the adapter subtracts the prior cumulative per threadId via a module-scoped LRU (codexSessionLastUsage, cap 256). Lookup priority for the prior:
params.priorUsage — caller-supplied (cross-process scenarios, see below)
- module LRU — populated by previous
execute() calls in this process
{0,0} — fresh thread, OR first resume after process restart with no priorUsage
Cross-process caveat: the LRU is in-memory only — no disk persistence (the library does not write to ~/). If your runtime spawns a new Node process per execute() call (per-request workers, serverless, CLI invoked per turn), the LRU starts empty every turn. Without params.priorUsage, the first resumed turn after each restart returns the full session cumulative as result.usage.inputTokens (typical symptom: In: grows linearly across turns: 12k → 25k → 38k → 51k…). Fix: persist the previous turn's raw cumulative on your side (read it from turn.completed.usage if you proxy SDK events, or track result.usage summed since session start) and pass it as priorUsage on the next call. Note: Codex CLI replays full thread history server-side on each runStreamed, so per-turn LLM input legitimately grows with conversation length even when the per-turn delta is correct.
For session-level billing totals: the unified contract says result.usage is per-call. Sum across calls on your side via sumUsage() from the public API, exactly as the Anthropic Agent SDK cost-tracking docs recommend ("The SDK does not provide a session-level total… accumulate the totals yourself.").
CONTEXT WINDOW — derived, never cumulative. result.contextSize is computed as usage.inputTokens + usage.outputTokens after the cumulative-to-delta subtraction. Because the post-subtract inputTokens represents "context posted to the LLM on this turn" (system + full history up to this turn), adding outputTokens (the assistant response just appended) yields the post-turn conversation size. Use the LAST turn's contextSize only — summing it across turns is meaningless. Compare against getModelContextWindow('codex', model) from the public API for the per-model cap.
Cache reads are part of inputTokens, not separate. Codex SDK reports cached_input_tokens alongside input_tokens in turn.completed.usage. The OpenAI convention is that cached_input_tokens is a subset of input_tokens — cache reads count toward input_tokens (just billed at a discounted rate), they aren't a separate bucket. The adapter forwards this as cacheReadInputTokens on result.usage (same field name as claude-code, where Anthropic's cache_read_input_tokens is normalized identically). To compute "fresh" input that was actually sent to the LLM at full price: inputTokens - (cacheReadInputTokens ?? 0). On long replayed Codex threads, cache hit rates of 80–90% are typical — so a 25k cumulative inputTokens over a few resumed turns might mean only ~3k tokens billed at full rate, with the rest as cheap cache reads. If your UI reports raw inputTokens without showing the cached split, users will perceive the bill as much larger than it actually is. (Cache hit ratio does NOT affect contextSize — caching is a billing-side optimization only.)
USD cost. @anthropic-ai/claude-agent-sdk emits total_cost_usd natively on its ResultMessage (claude-code adapter does not currently surface it on UnifiedEvent — sum across usage × Anthropic pricing yourself). OpenAI / Codex SDK does not expose USD; estimate from freshInputTokens × price_in + cacheReadInputTokens × price_cache + outputTokens × price_out against the OpenAI pricing page for the model.