Skip to content

fix(intercept): decode zstd/gzip request bodies so Codex is compressed and tracked#194

Merged
fkiene merged 1 commit into
mainfrom
fix/codex-zstd-request-body
Jul 17, 2026
Merged

fix(intercept): decode zstd/gzip request bodies so Codex is compressed and tracked#194
fkiene merged 1 commit into
mainfrom
fix/codex-zstd-request-body

Conversation

@fkiene

@fkiene fkiene commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Fixes #193.

Root cause

Codex CLI 0.144+ sends its request bodies to chatgpt.com/backend-api/codex/responses with Content-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 why llmtrim status showed 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:

host=chatgpt.com path=/backend-api/codex/responses len=21624
content-encoding=zstd content-type=application/json head_hex=28b52ffd...

28 b5 2f fd is the zstd magic number.

Fix

  • decode_request_body (serve.rs): decodes zstd and gzip request bodies right after buffering, with a 256 MB decompressed cap, then strips content-encoding so 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.
  • New deps zstd (BSD-3-Clause) and flate2, both optional under the intercept feature.

Two attribution gaps then 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 says "Codex CLI". Added the current marker; the legacy two stay for older CLIs.
  • 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 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

  • 7 new unit tests: zstd/gzip round-trips, garbage/identity/multi-codec passthrough, Responses-shaped fingerprint (both the instructions shape and the real wire shape with the leading additional_tools item), and a pin that Claude Code under a sub=codex reroute never fingerprints as codex.
  • Full suites green: llmtrim-core 500/500, llmtrim lib 562 passing (the one failure, mcp::…::malformed_request_is_an_error, also fails on clean main). Clippy clean, fmt applied.
  • End to end, real Codex 0.144.1 through an isolated proxy: before the fix a completed turn left zero ledger rows; after it the same turn records agent=codex / provider=openai / model=gpt-5.6-sol with real token counts, and llmtrim status shows a Codex card.

Out of scope

Response-side decompression (unchanged), and any host additions — chatgpt.com has been intercepted since #124 and the traffic was arriving fine.

…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>
@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.13084% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/llmtrim-cli/src/serve.rs 96.55% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@fkiene
fkiene merged commit c6b588f into main Jul 17, 2026
31 checks passed
@fkiene
fkiene deleted the fix/codex-zstd-request-body branch July 17, 2026 23:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

llmtrim status still not working w/Codex

1 participant