Add subscription auth support (Claude Code OAuth + Codex ChatGPT)#4
Merged
Conversation
Codex CLI signed in via ChatGPT subscription talks to a different backend (chatgpt.com/backend-api/codex) and sends codex-specific headers (chatgpt-account-id, originator, session_id, version). Route to the ChatGPT backend whenever chatgpt-account-id is present and pass those headers through; API-key Codex still goes to api.openai.com unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous allow-list dropped User-Agent and other Codex-specific headers, which caused ChatGPT's backend to throttle proxied requests with "high demand" errors. Switch to a deny-list that strips only hop-by-hop and connection-scoped headers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex CLI sends request bodies with Content-Encoding: zstd (magic 0x28 0xb5 0x2f 0xfd), which crashed request.json() with a UTF-8 decode error on byte 0xb5. Add a small helper that transparently decompresses zstd / gzip / deflate / br before JSON parsing, and strip Content-Encoding from forwarded headers so upstream doesn't expect compressed bytes. Also add an explicit GET /v1/responses handler returning 426, so Codex's WebSocket upgrade attempts stop falling into the catch-all and getting routed to chatgpt.com (which returns its branded 403 HTML page). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous zstd path used ZstdDecompressor().decompress(raw), which rejects streaming frames with "could not determine content size in frame header". Codex CLI emits exactly that frame format, so every /v1/responses request hit the warning and got a 415. Switch to stream_reader, which handles both sized and streaming zstd frames transparently. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ChatGPT backend (chatgpt.com/backend-api/codex) serves endpoints at /responses, /models, etc. — without a /v1 segment. When Codex CLI is configured with openai_base_url ending in /v1, every request lands on us at /v1/<endpoint>, and we were forwarding /v1/<endpoint> onto backend-api/codex/, which 403s on every call. Add _openai_upstream_path() that strips the leading /v1 when the upstream is the ChatGPT backend (detected via chatgpt-account-id, the same routing signal we already use). API-key mode is unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
To debug the 400s coming back from chatgpt.com/backend-api/codex/responses, capture the upstream error body in both the streaming and non-streaming paths and log it (truncated to 1000 bytes). Lets us see the actual server complaint instead of guessing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex CLI sends `input` items whose `content` is a list of typed parts
(e.g. [{"type":"input_text","text":"..."}]), not a plain string. Our
compressor only knows how to rewrite strings; substituting a string
for a typed-part array broke the wire shape and chatgpt.com responded
with 400 Bad Request to every /responses call.
Detect typed-part content in any input item and skip compression for
the whole request, so the body forwards verbatim. Plain-string input
(the simple `client.responses.create(input="...")` shape) still gets
compressed as before.
Verified end-to-end with a Codex-shaped body: all schema fields and
the typed-part content array are preserved exactly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
httpx merges case-different dict keys (e.g. "content-type" and
"Content-Type") into a single comma-joined header
("application/json, application/json"). The ChatGPT backend strict-
parses Content-Type and rejected the merged value as "Unsupported
content type" — every /responses call from Codex got a 400.
Add "content-type" to the strip set so the forwarded request has
exactly one canonical Content-Type header.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Was added to debug the chatgpt.com 400s; the underlying issues are fixed, so drop the noisy WARNING that logged 4xx response bodies. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Owner
|
@songmeo Thank you so much for your contribution and for taking the time to improve the project. I really appreciate the effort and thought you put into this PR. I hope you've enjoyed working on TokenTamer so far. Your insights and technical contributions are incredibly valuable, and I'd be happy to have you continue exploring the project and helping shape its future. Looking forward to collaborating more with you! |
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.
Summary
Lets TokenTamer proxy two new authenticated transports:
/v1/messages— preservesAuthorization: Bearer …(instead of forcingx-api-key), forwards?beta=true, adds/v1/messages/count_tokens./v1/responses— detects subscription mode viachatgpt-account-idand routes tochatgpt.com/backend-api/codexinstead ofapi.openai.com.Codex compatibility notes
Content-Typeto avoid duplicates./v1prefix when routing to the ChatGPT backend (it serves/responses, not/v1/responses).zstd/gzip/deflate/br).GET /v1/responsesso the WS-upgrade attempt fails fast.input(Codex's normal shape).Tests
pytest tests/→ 69 passed. Verified live with Codex CLI on a ChatGPT subscription.