Skip to content

Add subscription auth support (Claude Code OAuth + Codex ChatGPT)#4

Merged
borhen68 merged 11 commits into
borhen68:mainfrom
songmeo:add-subscription-support
Jun 14, 2026
Merged

Add subscription auth support (Claude Code OAuth + Codex ChatGPT)#4
borhen68 merged 11 commits into
borhen68:mainfrom
songmeo:add-subscription-support

Conversation

@songmeo

@songmeo songmeo commented Jun 13, 2026

Copy link
Copy Markdown

Summary

Lets TokenTamer proxy two new authenticated transports:

  • Claude Code OAuth on /v1/messages — preserves Authorization: Bearer … (instead of forcing x-api-key), forwards ?beta=true, adds /v1/messages/count_tokens.
  • Codex CLI ChatGPT subscription on /v1/responses — detects subscription mode via chatgpt-account-id and routes to chatgpt.com/backend-api/codex instead of api.openai.com.

Codex compatibility notes

  • Forwards all client headers (deny-list), strips incoming Content-Type to avoid duplicates.
  • Drops /v1 prefix when routing to the ChatGPT backend (it serves /responses, not /v1/responses).
  • Decompresses request bodies (zstd / gzip / deflate / br).
  • 426 on GET /v1/responses so the WS-upgrade attempt fails fast.
  • Skips compression on Responses API requests with typed-part input (Codex's normal shape).

Tests

pytest tests/ → 69 passed. Verified live with Codex CLI on a ChatGPT subscription.

songmeo and others added 11 commits June 12, 2026 23:03
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>
@borhen68 borhen68 merged commit 35af43b into borhen68:main Jun 14, 2026
8 checks passed
@borhen68

Copy link
Copy Markdown
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!

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.

2 participants