fix(intercept): decode zstd/gzip request bodies so Codex is compressed and tracked#194
Merged
Conversation
…d and tracked Codex CLI 0.144+ sends its request bodies to chatgpt.com/backend-api/codex/responses with `Content-Encoding: zstd`. The interceptor handed the raw compressed bytes to the compression pipeline, which could not parse them as JSON and declined, so every Codex turn was forwarded verbatim with no pending state: no compression, no attribution, no ledger row. `llmtrim status` showed no Codex activity at all (#193) — the host was already intercepted (#124); the body was just unreadable. The interceptor now decodes zstd- and gzip-encoded POST bodies (256 MB decompressed cap) right after buffering, strips `content-encoding`, and runs the normal pipeline on the plain JSON. Any decode failure — unknown codec, multi-codec list, corrupt stream, over-cap — falls back to today's verbatim forward, so a decode problem can never break the call. The replay-on-error copy is captured after decoding, which keeps it consistent with the stripped header. Two attribution gaps kept the recorded turns from being labeled `codex`: - The fingerprint markers predate Codex 0.144, whose prompt opens with "You are Codex" and no longer contains "Codex CLI". Added the current marker; kept the legacy two. - `system_text` read only the first system/developer item of a Responses body. Codex now leads with a non-message `additional_tools` item, so the identity message behind it was never seen. Identity extraction now concatenates every system/developer *message*, skipping tool registrations — whose schemas vary turn-to-turn and would also destabilize the session hash. Verified end to end against a live Codex 0.144.1 on an isolated proxy port: before the fix a completed turn left zero ledger rows; after it the turn records agent=codex / provider=openai / model=gpt-5.6-sol with real token counts. Fixes #193. Signed-off-by: François Kiene <46886660+fkiene@users.noreply.github.com>
2 tasks
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #193.
Root cause
Codex CLI 0.144+ sends its request bodies to
chatgpt.com/backend-api/codex/responseswithContent-Encoding: zstd. The interceptor handed the compressed bytes straight to the compression pipeline, which could not parse them as JSON and declined. A declined body is forwarded verbatim with no pending state — no compression, no attribution, no ledger row. That is whyllmtrim statusshowed no Codex activity: the host was already intercepted (#124), but the body was unreadable. The reporter's screenshot carries the proof: a healthy banner with "last request 412h ago" and a flat $0 savings trend — the ledger had been frozen for 17 days while Codex ran through the proxy daily.I confirmed this against a live Codex 0.144.1 on an isolated proxy port. A diagnostic tap showed:
28 b5 2f fdis the zstd magic number.Fix
decode_request_body(serve.rs): decodeszstdandgziprequest bodies right after buffering, with a 256 MB decompressed cap, then stripscontent-encodingso the pipeline and the forwarded body work on plain JSON. Every failure mode — unknown codec, multi-codec list, corrupt stream, over-cap — falls back to today's verbatim forward, so decoding can never break a call. The replay-on-error copy is captured after decoding, keeping it consistent with the stripped header.zstd(BSD-3-Clause) andflate2, both optional under theinterceptfeature.Two attribution gaps then kept the recorded turns from being labeled
codex:system_textread only the first system/developer item of a Responses body. Codex now leads with a non-messageadditional_toolsitem, so the identity message behind it was skipped. Identity extraction now concatenates every system/developer message, skipping tool registrations — their schemas vary turn to turn and would also destabilize the session hash.Verification
instructionsshape and the real wire shape with the leadingadditional_toolsitem), and a pin that Claude Code under asub=codexreroute never fingerprints ascodex.mcp::…::malformed_request_is_an_error, also fails on clean main). Clippy clean, fmt applied.agent=codex / provider=openai / model=gpt-5.6-solwith real token counts, andllmtrim statusshows a Codex card.Out of scope
Response-side decompression (unchanged), and any host additions —
chatgpt.comhas been intercepted since #124 and the traffic was arriving fine.