/** * codex cost provider: rollout JSONL parsing since a cursor. * * Rollouts live at `$CODEX_HOME/sessions/YYYY/MM/DD/rollout--.jsonl`, * defaulting to `~/.codex`. `CODEX_HOME` is a real user-facing variable — it is * what `codex-rs/rollout/src/config.rs` reads — so hardcoding `~/.codex` would * give every user who moves it `detect() === false` and silent zero capture. * `ProviderEnv.home` stays a *test* override and is a different thing. * * Three properties of the format drive everything here, all measured against real * rollouts rather than inferred: * * 1. **Two token blocks per record.** Each `token_count` event carries both * `total_token_usage` (cumulative for the session) and `last_token_usage` (that * turn). Only the latter may be summed: the former overcounts 2.86x on real * data, and `total == input + output` holds for *both*, so an invariant test * alone cannot tell you which was read. * 2. **The counters overlap.** `cached_input_tokens` is *inside* `input_tokens` * and `reasoning_output_tokens` is *inside* `output_tokens` — the inverse of * Anthropic's shape, where the four counters are disjoint and the claude-code * provider maps them straight through. Mapping Codex straight through prices * cached input twice: 206,483 input-side tokens against a true 122,167 on one * observed turn. * 3. **No record carries an id.** A `token_count` line has only `payload`, * `timestamp`, `type`; its payload only `info`, `rate_limits`, `type`. The * claude-code provider resumes on a transcript entry's `uuid` and there is no * equivalent, so the cursor is synthesised — see CURSOR below. * * `model` and `cwd` are not on the usage record either. Both are carried forward * from the most recent preceding `turn_context` / `session_meta` line, because one * of those covers many usage records (measured: one `turn_context` covering five). * * Requirements: agent-portability/AC-14, AC-15, AC-17, AC-50, AC-54, AC-55 * Design: design.md A4 * * @module */ import type { UsageSegment, TokenUnits } from '../types.js'; import type { CostProvider, ProviderEnv } from './provider.js'; /** Root directory that rollouts must live under (AC-14, AC-17). */ export declare function sessionsRoot(env: ProviderEnv): string; /** A finite, non-negative number — shared with autopilot, which reads the same counters live. */ export declare function isCount(v: unknown): v is number; /** The per-turn token block, as Codex writes it. */ interface LastTokenUsage { input_tokens: number; cached_input_tokens: number; cache_write_input_tokens: number; output_tokens: number; reasoning_output_tokens: number; total_tokens: number; } /** * Map Codex's overlapping counters onto the ledger's disjoint ones (AC-15). * * The subtraction is the whole point. `cached_input_tokens` and * `cache_write_input_tokens` are already inside `input_tokens`, and cached input * is around two thirds of observed Codex input — so a straight-through mapping * bills it once at the full input rate and again at the cache-read rate. * `reasoning_output_tokens` is inside `output_tokens` and is therefore *not* * added; adding it double-counts reasoning. * * Clamped at zero rather than trusted: the containment relation is measured, not * guaranteed by the format, and a negative token count would flow into money. */ export declare function mapUnits(last: LastTokenUsage, model: string): TokenUnits; /** * Does this record satisfy the identity the mapping rests on? (AC-55, NFR-6) * * `total == input + output` is what establishes that cached and reasoning tokens * are *inside* those two figures. If it ever stops holding, the de-overlap above * is wrong and every Codex figure is wrong with it. This is the detector NFR-6's * re-derivation trigger needed in order to be able to fire at all — * `cache_write_input_tokens` was 0 in all 12 observed records, so nothing else * would notice it becoming non-zero. */ export declare function usageIdentityHolds(last: LastTokenUsage): boolean; /** * CURSOR: `:`. * * Codex records carry no id, so there is nothing to resume *on*. Rollout files * are append-only, which makes a record's ordinal within its file stable across * re-reads, and the basename identifies the file without leaking an absolute path * into the ledger. * * Crucially this names **the record**, not the session. A `session_id`-keyed * duplicate check once passed 1,752 tests on this project while producing 4,388 * phantom records, because the ledger's id and the transcript's were two names for * the same work. A cursor that identifies the record is checkable across sessions. * * The separator is `:` because the ledger's cursor rule (`ledger.ts` CURSOR_RE) * allows it. It was `#`, which that rule refuses, so every Codex record was dropped * by sync and by live capture alike — 124 a run in this repo — with nothing saying * why (cost-sync-silent-rejections/AC-5). Widening the rule instead would make every * already-installed `aidlc` skip the new lines as invalid. No ledger holds a `#` * cursor, since each was refused before it was written, so nothing reads that form. */ export declare function codexCursor(path: string, ordinal: number): string; /** Git identity of a directory, for repository scoping. Null when it has none. */ type IdentityOf = (cwd: string) => string | null; export declare const codexProvider: CostProvider; /** * The model a thread used, from the first `turn_context` of its rollout * (autopilot-reads-agent-result/AC-20, AC-21). Codex's `--json` events do not carry it. * * The rollout is `rollout--.jsonl` under the day directory it was started * in, which is local time; the launch's day and the next, in local time and UTC, cover a * launch near midnight. `threadId` is matched against directory entries, never joined * into a path, so it cannot steer the search. Null when no rollout or no usable model is * found — only a model the ledger would accept (`MODEL_ID_RE`), as `extractFrom` does. */ export declare function rolloutModel(threadId: string, launchedAt: string, env: ProviderEnv): string | null; /** * Stream a rollout into usage segments. * * One pass, carrying three pieces of context forward: `model` from the most recent * `turn_context`, `cwd` from the most recent `session_meta`, and the ordinal used * for the cursor. Fail-soft throughout — a line that does not match the expected * shape is skipped, never fatal, because the format is internal to Codex. * * `identityOf` enables per-segment repository scoping (AC-50). It is injected so * the scoping can be tested without a git repository, and passing `null` disables * scoping for callers that have already scoped by file. */ export declare function extractFrom(path: string, since: string | null, env: ProviderEnv, identityOf: IdentityOf | null, onIdentityMismatch?: (reason: string) => void): Promise; export {}; //# sourceMappingURL=codex.d.ts.map