feat(codex): a second rail, and the OpenAI cache-write rate it needs - #35
Conversation
Adds `adapters/codex.ts`, reading ~/.codex/sessions rollouts into the same
Session/Span model. The two changes ship together because they are the same
change: the OpenAI cache-write bug below was latent only for want of an
adapter that reads `cache_write_input_tokens`.
- The token conventions are INVERTED between the rails. TurnUsage is additive
(Anthropic: `input` excludes the cache buckets); Codex is subset
(`input_tokens` is the total, cached/write sit inside it). Reading it straight
through double-counts the largest bucket in the file — cache reads are 303M of
313M tokens across the local corpus. Verified: 25 rollouts, 2499 turns, every
bucket matching an independent walk of the logs exactly.
- `token_count` rows repeat. Cost comes from `last_token_usage` (per request),
never a difference of the cumulative, with an exact repeat of the previous row
suppressed. Repeats are NOT always adjacent — observed at gaps of 0, 0, 1, 2
and 7 records, one spanning a task boundary — so anything buffered in the gap
belongs to the NEXT request. With the suppression all 25 rollouts reconcile to
the token; without it, two are over.
- Opt-in behind --codex, which is a correctness choice. Codex encrypts reasoning
(1598/1598 records, zero plaintext summaries), has no Read tool and no plan
mode, so thinkingChars/reads/modes come back empty — indistinguishable from
"measured and found absent". Folded in silently, a Codex-heavy user's fluency
signals would fall because of what the log format omits. Session.source now
exists so a signal can select its rail.
- --codex writes no history snapshot, never transmits, and is REFUSED alongside
--judge/--open: AggregateRecord.tool is a frozen z.literal('claude_code') and
cannot describe a two-rail corpus. Dropping the Codex half instead would make
the shared report differ from the one on screen.
fix(pricing): OpenAI cache writes were billed at the plain input rate. The
comment read "no separate write bucket" — true until the GPT-5.6 GA
(2026-07-09), when OpenAI added a write premium, so every 5.6 write was 20%
under (sol: cacheWrite 5, input 4). The same premise understated the compaction
counterfactual's write cost, biasing that recommendation toward compacting.
Fixed by re-syncing src/vendor/pricing.ts from @promptster/config-cost, which
grew the cacheWrite axis on 2026-08-12 — not by hand-editing the mirror, which
is verbatim and whose sync script refuses to overwrite local edits. Rates are
otherwise unchanged (all 29 OpenAI and 16 Anthropic keys already agreed); the
re-sync also picks up upstream's longest-key-first Anthropic lookup.
Nothing caught this for six weeks because pricingDrift compares
input/cachedInput/output — the fields present on both sides — so a missing rate
AXIS is invisible to it however green it runs. pricingPinned now asserts the
axis as a rule rather than per-row. The mirror's drift history moved to
MAINTAINING.md, since a verbatim mirror deletes any note written into it.
Note the fix is inert on the local corpus: all 2504 token_count rows report
cache_write_input_tokens: 0, so the tests use synthetic usage on purpose — a
fixture copied from disk would have passed against the broken code too.
Also: CLAUDE.md said the package manager was npm; it has been pnpm since #34.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Greptile SummaryAdds opt-in Codex rollout ingestion and extends OpenAI pricing with explicit cache-write rates.
Confidence Score: 4/5The source-sensitive fluency calculations need to be fixed before merging because Codex sessions are intentionally marked with a source discriminator and empty unobservable fields, but the shared fluency path ignores that discriminator, lowering plan-mode metrics and potentially the user’s displayed self-band. Files Needing Attention: src/cli.ts, src/model.ts, src/adapters/codex.ts
|
| Filename | Overview |
|---|---|
| src/adapters/codex.ts | Adds comprehensive Codex rollout parsing, usage normalization, duplicate suppression, and source-specific session metadata. |
| src/cli.ts | Adds opt-in Codex loading and local-only guards, but passes unobservable Codex plan-mode values into common fluency analysis. |
| src/model.ts | Adds an optional source discriminator intended to support rail-sensitive analysis. |
| src/pricing.ts | Correctly switches OpenAI cache-write billing and compaction estimates to the new per-model write tariff. |
| src/vendor/pricing.ts | Re-syncs vendor pricing with cache-write rates and more constrained, longest-key-first Anthropic model matching. |
| src/tests/codexAdapter.test.ts | Covers Codex parsing, token normalization, duplicate rows, privacy boundaries, compaction, and subagent modeling. |
| src/tests/openaiCacheWrite.test.ts | Exercises premium and legacy OpenAI cache-write rates and protects Anthropic’s separate write buckets. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
Claude[Claude Code transcripts] --> ClaudeAdapter[Claude adapter]
Codex[Codex rollouts under --codex] --> CodexAdapter[Codex adapter]
ClaudeAdapter --> Sessions[Normalized Session array]
CodexAdapter --> Sessions
Sessions --> Audit[runAudit]
Audit --> Pricing[Spend and token pricing]
Audit --> Fluency[Fluency analysis]
Pricing --> Report[Local report / JSON]
Fluency --> Report
CodexAdapter -. source: codex .-> Fluency
Reviews (1): Last reviewed commit: "feat(codex): a second rail, and the Open..." | Re-trigger Greptile
| 'plan mode are NOT observable on that rail and are absent rather than zero.\n', | ||
| ); | ||
| } | ||
| sessions.push(...codex); |
There was a problem hiding this comment.
Unobservable plan mode lowers fluency
When --codex adds substantive Codex sessions, the shared fluency analysis counts their unobservable modes: [] values in the plan-mode denominator, causing the reported plan-mode rate and potentially the self-band to be understated; filter source-sensitive signals using Session.source or represent them as unknown.
Knowledge Base Used: Usage economics and pricing
Codex rollouts always report modes: [] — plan mode is unobservable on that rail, not absent. Counting those sessions in the substantive-session denominator (without a plan-mode credit) understated planModeRate for any --codex run, exactly the "absent, not zero" pitfall Session.source exists to guard against. Exclude source === 'codex' sessions from both sides of the ratio instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
Adds a Codex adapter and fixes an OpenAI cache-write rate. They ship together because they are the same change — the pricing bug was latent only for want of an adapter that reads
cache_write_input_tokens.The adapter (
src/adapters/codex.ts,--codex) reads~/.codex/sessions/**/rollout-*.jsonlinto the existingSession/Spanmodel. Verified against the whole local corpus: 25 rollouts, 2499 turns, every token bucket matching an independent walk of the logs exactly. Two traps the format sets:TurnUsageis additive (Anthropic:inputexcludes the cache buckets); Codex is subset (input_tokensis the total,cached_input_tokens/cache_write_input_tokensinside it). Reading it straight through double-counts the largest bucket in the file — cache reads are 303M of 313M tokens locally, so this is not a rounding error.token_countrows repeat. Cost comes fromlast_token_usage(per request), never a difference of the cumulativetotal_token_usage. An exact repeat of the previous row is suppressed — and repeats are not always adjacent (observed at gaps of 0, 0, 1, 2 and 7 records, one spanning a task boundary), so anything buffered in the gap belongs to the next request. With the suppression all 25 rollouts reconcile to the token; without it, two are over.--codexis opt-in, and that is a correctness choice rather than caution. Codex encrypts reasoning (1598/1598 records, zero plaintext summaries), has noReadtool and no plan mode, sothinkingChars/reads/modescome back empty — indistinguishable from "measured and found absent". Folded in silently, a Codex-heavy user's fluency signals would drop because of what the log format omits.Session.sourcenow exists so a signal can select its rail. It also writes no history snapshot, never transmits, and is refused alongside--judge/--open:AggregateRecord.toolis a frozenz.literal('claude_code')and cannot describe a two-rail corpus (dropping the Codex half instead would make the shared report differ from the one on screen).The pricing fix.
pricing.tsbilled written tokens ato.inputunder the comment "no separate write bucket" — true until the GPT-5.6 GA (2026-07-09), when OpenAI added a write premium. Every 5.6 cache write was 20% under (sol:cacheWrite5,input4), and the same premise understated the compaction counterfactual's write cost, biasing that recommendation toward compacting.Fixed by re-syncing
src/vendor/pricing.tsfrom@promptster/config-cost(which grew the axis on 2026-08-12), not by hand-editing the mirror — it is verbatim, andsync-pricing.mjsrefuses to overwrite local edits. Checked before forcing: all 29 OpenAI and 16 Anthropic keys already agreed on every rate, so the sync is axis-only; it also picks up upstream's longest-key-first Anthropic lookup, which the mirror lacked.Nothing caught this for six weeks because
pricingDriftcomparesinput/cachedInput/output— the fields present on both sides — so a missing rate axis is invisible to it however green it runs.pricingPinnednow asserts the axis as a rule (cacheWrite === input * 1.25on 5.6+,=== inputbelow) rather than per-row.Two things worth knowing before reviewing:
token_countrows reportcache_write_input_tokens: 0. The tests use synthetic usage deliberately — a fixture copied from disk would have passed against the broken code too.CLAUDE.mdclaims--rootnever transmits, butmayTransmitgates onlysendCapture.postReport(--open) andjudgeFootprints(--judge) are ungated, so--root /fixtures --openuploads a fixture-derived aggregate. That is why--codexrefuses the combination rather than trustingmayTransmit. Left alone as a separate product decision about two consent paths.Also fixes
CLAUDE.mdsaying the package manager is npm — it has been pnpm since #34. The mirror's drift history moved toMAINTAINING.md, since a verbatim mirror deletes any note written into it.Test plan
pnpm typecheck— cleanpnpm lint— cleanpnpm test— 460 passed, 44 files (up from 437/42)pnpm build:npm— bundles, and--codexreaches the published artifact's--helpsrc/__tests__/codexAdapter.test.ts(17) — subset→additive conversion and its clamp, the double-count stated as arithmetic, non-adjacent duplicate suppression keeping gap content for the next turn, patch paths with the diff body asserted absent from the serialized session, post-compaction span flagging, subagent thread → sidechain + parent link, unpricedcodex-auto-reviewsrc/__tests__/openaiCacheWrite.test.ts(6) — 5.6 writes at 1.25x with the pre-fix arithmetic named, pre-5.6 unchanged and not zero, both write slots against the one rate, three input buckets at three distinct rates, Anthropic arm untouched~/.codex: 2499 turns, uncached 9,667,619 / cacheRead 303,627,392 / write 0 / output 486,886 — exact match on every bucket, including the 5 suppressed duplicatesnode dist/cli.js --codex --json— stdout stays pure JSON, notice on stderr;--codex --openexits 2 with the reason🤖 Generated with Claude Code