Run a tiny OpenAI-compatible proxy for ChatGPT/Codex subscription-backed models.
codex-sub-proxy gives trusted internal tools a small /v1/chat/completions and /v1/responses API while it handles ChatGPT/Codex OAuth, the private Codex backend route, SSE adaptation, and a few compatibility quirks.
It is designed to be easy to run as a Docker sidecar, easy to audit, and honest about the OpenAI API surface it does not implement.
- Why Use It
- Warning
- Quick Start
- Docker Images
- Supported API
- How It Works
- Configuration
- Examples
- Web Search
- Files
- Smoke Test
- Client Configuration
- Development
- Contributing
- CI And Publishing
- Troubleshooting
- Security
- License
Use codex-sub-proxy when you already have ChatGPT/Codex subscription access and want a practical OpenAI-compatible HTTP surface for private infrastructure.
Good fits:
- Point LiteLLM-like clients, transcription analyzers, or internal automation at one stable base URL.
- Keep older tools that know
/v1/chat/completionsworking while using Codex-backed Responses upstream. - Run a small Docker sidecar instead of deploying a larger gateway.
- Support both streaming and non-streaming clients.
- Expose Codex-backed hosted web search to trusted clients.
- Pass inline file content to Codex model requests without adding a separate storage service.
Poor fits:
- Public API gateways.
- Full OpenAI API compatibility.
- Workloads that require stable vendor-supported backend contracts.
- File upload/list/delete workflows through
/v1/files.
This project uses unofficial/private ChatGPT/Codex backend behavior. It may violate provider terms, may stop working without notice, and should be used only where you have reviewed the legal, security, and operational risk.
Do not expose this service directly to the public internet. Put it behind a private network boundary and always set PROXY_API_KEY outside isolated local development.
cp .env.example .envdocker run --rm ghcr.io/tech-grandpa/codex-sub-proxy:latest \
node dist/src/cli/login.jsThe command prints a device URL and code to stderr. Open the URL, authorize the code, and the command writes credentials to stdout:
{
"refresh_token": "...",
"access_token": "...",
"expires_at": 1760000000
}Put those values into .env:
PROXY_API_KEY=choose-a-long-random-local-key
OPENAI_REFRESH_TOKEN=...
OPENAI_ACCESS_TOKEN=...
OPENAI_EXPIRES_AT=...docker run --rm \
--name codex-sub-proxy \
-p 3000:3000 \
--env-file .env \
ghcr.io/tech-grandpa/codex-sub-proxy:latestOr with Docker Compose:
docker compose upUse a different env file with Compose:
ENV_FILE=/path/to/proxy.env docker compose upcurl -s http://localhost:3000/healthzExpected:
{"ok":true}Images are published to GitHub Container Registry:
ghcr.io/tech-grandpa/codex-sub-proxy
Tags:
| Tag | Meaning |
|---|---|
latest |
Current default branch image |
main |
Current main branch image |
sha-<commit> |
Immutable commit image |
<git-tag> |
Release tag image, for example v0.1.0 |
Stop a named local container:
docker rm -f codex-sub-proxy| Route | Status | Notes |
|---|---|---|
GET /healthz |
Supported | Health check |
GET /v1/models |
Supported | Returns the configured local model list |
POST /v1/responses |
Supported | Streaming, non-streaming, and hosted web_search tools |
POST /v1/chat/completions |
Supported | Streaming, non-streaming, and web_search_options compatibility |
/v1/files |
Not supported | Returns 501; use inline input_file parts |
| Embeddings, audio, images, batches, Assistants | Not supported | Outside this proxy's scope |
This is not a transparent full OpenAI API proxy. It is a focused compatibility shim for model calls.
OpenAI-compatible client
|
v
codex-sub-proxy
|
v
private ChatGPT/Codex Responses backend
The proxy:
- refreshes ChatGPT OAuth access tokens with
OPENAI_REFRESH_TOKEN - authenticates callers with
PROXY_API_KEY - converts chat-completion requests into Responses payloads
- maps Chat Completions
web_search_optionsto a Responsesweb_searchtool - relays Responses SSE for streaming
/v1/responses - translates upstream SSE into
chat.completion.chunkevents for streaming chat clients - collapses upstream SSE into JSON for non-streaming clients
- preserves inline
input_filecontent parts - strips known unsupported upstream fields
Current upstream target:
${CODEX_BASE_URL}${CODEX_RESPONSES_PATH}
Default:
https://chatgpt.com/backend-api/codex/responses
| Variable | Default | Required | Description |
|---|---|---|---|
HOST |
0.0.0.0 |
No | Bind address inside the container |
PORT |
3000 |
No | HTTP port inside the container |
PROXY_API_KEY |
unset | Strongly recommended | Bearer token required from proxy callers |
OPENAI_REFRESH_TOKEN |
unset | Yes | ChatGPT OAuth refresh token |
OPENAI_ACCESS_TOKEN |
unset | No | Optional initial access token |
OPENAI_EXPIRES_AT |
unset | No | Access-token expiry as Unix seconds or milliseconds |
OPENAI_CHATGPT_ACCOUNT_ID |
unset | Sometimes | Optional ChatGPT account/workspace id |
CODEX_BASE_URL |
https://chatgpt.com/backend-api/codex |
No | Private Codex backend base URL |
CODEX_RESPONSES_PATH |
/responses |
No | Private Codex Responses path |
CODEX_MODELS |
gpt-5.5,gpt-5.5-pro,gpt-5.4,gpt-5.4-mini |
No | Comma-separated ids returned by /v1/models |
Set your proxy key first:
export PROXY_API_KEY=choose-a-long-random-local-keycurl -s http://localhost:3000/v1/models \
-H "Authorization: Bearer ${PROXY_API_KEY}"curl -s http://localhost:3000/v1/chat/completions \
-H "Authorization: Bearer ${PROXY_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.5",
"messages": [
{ "role": "system", "content": "Answer tersely." },
{ "role": "user", "content": "Say the proxy works." }
]
}'curl -N http://localhost:3000/v1/chat/completions \
-H "Authorization: Bearer ${PROXY_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.5",
"stream": true,
"messages": [
{ "role": "user", "content": "Say the proxy works." }
]
}'curl -s http://localhost:3000/v1/responses \
-H "Authorization: Bearer ${PROXY_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.5",
"instructions": "Answer tersely.",
"input": "Say the proxy works."
}'curl -N http://localhost:3000/v1/responses \
-H "Authorization: Bearer ${PROXY_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.5",
"stream": true,
"input": "Say the proxy works."
}'Web search is a hosted upstream tool. The proxy does not search the web itself; it forwards the tool request to the private Codex Responses backend and relays the resulting response or SSE events.
Use Responses-style tools when your client supports /v1/responses:
curl -s http://localhost:3000/v1/responses \
-H "Authorization: Bearer ${PROXY_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.5",
"tools": [{ "type": "web_search" }],
"tool_choice": "auto",
"input": "Search the web for one current OpenAI headline and return the URL."
}'Chat-compatible clients can use web_search_options. The proxy converts this into a Responses web_search tool before forwarding the request:
curl -s http://localhost:3000/v1/chat/completions \
-H "Authorization: Bearer ${PROXY_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.5",
"web_search_options": {
"search_context_size": "medium"
},
"messages": [
{
"role": "user",
"content": "Search the web for one current OpenAI headline and return the URL."
}
]
}'When both tools and web_search_options are present on a chat request, explicit tools win and web_search_options is removed before forwarding.
The proxy does not implement OpenAI's /v1/files upload/list/delete API. Those routes return 501.
What works today is inline file content inside model requests:
/v1/responsespreservesinput_filecontent parts./v1/chat/completionsmaps{ "type": "file", "file": { ... } }chat parts to Responsesinput_fileparts.- Inline
file_datadata URLs work when the private backend accepts them.
Example:
curl -s http://localhost:3000/v1/responses \
-H "Authorization: Bearer ${PROXY_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.5",
"input": [
{
"role": "user",
"content": [
{ "type": "input_text", "text": "Summarize this file." },
{
"type": "input_file",
"filename": "notes.txt",
"file_data": "data:text/plain;base64,aGVsbG8="
}
]
}
]
}'After the container is running:
ENV_FILE=.env ./scripts/smoke-test.shThe smoke test checks:
- health
- caller authentication
- model listing
- chat completions
- Responses
- streaming chat completions
- streaming Responses
The smoke test calls the live private backend, so it requires valid credentials.
Point OpenAI-compatible clients at:
OPENAI_BASE_URL=http://codex-sub-proxy:3000/v1
OPENAI_API_BASE=http://codex-sub-proxy:3000/v1
OPENAI_API_KEY=${PROXY_API_KEY}
OPENAI_MODEL=gpt-5.5Use /v1/chat/completions for older clients. Use /v1/responses when your client can send Responses-style payloads.
Use npm only when changing or testing the project locally:
npm install
npm test
npm run typecheck
npm run build
npm startRun the login helper without Docker:
npm run build
npm run loginBuild a local development image:
docker build -t codex-sub-proxy:local .
docker run --rm \
--name codex-sub-proxy-local \
-p 3000:3000 \
--env-file .env \
codex-sub-proxy:localThe unit tests use Node's built-in test runner and do not call the real ChatGPT backend.
All changes go through short-lived feature branches and pull requests targeting main. Pull requests are squash-merged, so the PR title is the commit title that lands on main.
Use Conventional Commit PR titles:
feat: add streaming option
fix: handle expired token refresh
docs: clarify Docker setup
The PR title is also the release signal used by release automation: fix: means patch, feat: means minor, and ! or BREAKING CHANGE: means major. See CONTRIBUTING.md for the full workflow.
GitHub Actions are defined in:
.github/workflows/ci.yml.github/workflows/codeql.yml.github/workflows/dependency-review.yml.github/workflows/pr-title.yml.github/workflows/release-from-tag.yml.github/workflows/release-please.yml.github/workflows/security.yml
On every push and pull request:
- verify whether the change is documentation-only
- install Node.js 22 dependencies with
npm ciwhen code paths changed - run
npm testwhen code paths changed - build the Docker image without pushing it when code paths changed
On pushes to main and pull requests targeting main:
- run CodeQL static analysis
- run dependency and container vulnerability checks
On pushes to main:
- build the Docker image
- push it to GitHub Container Registry
- publish
main,latest, andsha-<commit>image tags
On feature-branch pushes:
- run verification
- do not publish persistent Docker images
On pull requests:
- validate the PR title against Conventional Commits
- Dependency Review blocks newly introduced vulnerable dependencies at moderate severity or higher.
- Docker builds run with
push: false.
On a weekly schedule:
- Dependabot checks npm, Docker, and GitHub Actions updates.
- CodeQL re-runs static analysis.
npm auditchecks dependency advisories.- Trivy scans the repository filesystem and Docker image, then uploads SARIF results to GitHub code scanning.
Version tags are published as matching image tags. For example, v0.1.0 is published as:
ghcr.io/tech-grandpa/codex-sub-proxy:v0.1.0
Tags matching v* also create a GitHub Release with generated release notes. Release PRs are managed by release-please from Conventional Commit history.
For protected branches, release-please works best with a RELEASE_PLEASE_TOKEN repository secret so release PRs trigger the same required checks as human-authored PRs.
401 Unauthorized
- Set
PROXY_API_KEYin the server environment. - Send
Authorization: Bearer ${PROXY_API_KEY}from the client.
502 upstream_error
- Re-run the login helper if credentials may be expired.
- Confirm
CODEX_RESPONSES_PATH=/responses. - Confirm the account has Codex access.
- Check whether ChatGPT changed the private backend route or request contract.
Docker cannot bind port 3000
- Another process is already listening on that port.
- Stop it or map a different host port, for example
-p 3001:3000.
- Never commit
.envor real OAuth credentials. - Treat
OPENAI_REFRESH_TOKENlike a password. - Keep this service on private networks.
- Set
PROXY_API_KEYfor every non-local deployment. - Avoid logging prompts, completions, API keys, or OAuth tokens.
Apache-2.0