An OpenAI-compatible API gateway for ChatGPT Web with a browserless direct transport — no Chrome, no Playwright, no headless browser farm. One small Go binary serves chat, streaming, images, vision, files, and million-token contexts over plain HTTPS using your own ChatGPT account credentials.
OpenAI SDK/client ──► gptweb2api (:8788) ──► chatgpt.com backend-api
~22 MB RSS pure HTTP/2 + SSE
- Browserless by default — the gateway performs ChatGPT Web's requirements/preparation round trip in-process (bootstrap proof, SHA3-512 proof-of-work solving, conduit acquisition). Observed footprint: ~22 MB RSS for the whole gateway versus hundreds of MB for a Chrome tab.
- OpenAI-compatible surface — 39 routes covering chat, responses, images, vision, files, uploads, conversations, stored completions, and batches.
- Huge contexts — prompts beyond the Web request ceiling are serialized losslessly to JSONL, split into ≤8 MiB part files, uploaded through the Web file-service, and attached to one compact wire message. Qualified live at an estimated 1,000,000 tokens with 64/64 marker recall.
- Images without a browser — generation, edits, and variations ride the
same direct transport; vision and image attachments use
image_asset_pointerparts, up to 10 attachments per turn. - Automatic fallback — if upstream bot-defense blocks the direct path, an optional local browser relay (Playwright + Chrome) takes over those turns.
- Production hygiene — request IDs, JSON logs,
/metrics,/healthz+/readyz, graceful shutdown, hot-reloadable OAuth credential files with automatic refresh.
- A ChatGPT account with an OAuth credential file. The gateway reads the same
auth.jsonwritten by OpenAI Codex CLI (~/.codex/auth.jsonby default) and refreshes tokens automatically. - Go 1.24+ to build, or use the Dockerfile.
- Optional: Node 18+ and Chrome only for the browser-relay fallback.
go build -o gptweb2api ./cmd/gptweb2api
cp .env.example .env # set GPTWEB_API_KEY to your own secret
./gptweb2apiAny OpenAI SDK works — point base_url at http://127.0.0.1:8788/v1:
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:8788/v1", api_key="your-gptweb-key")
reply = client.chat.completions.create(
model="auto",
messages=[{"role": "user", "content": "Hello!"}],
)
print(reply.choices[0].message.content)docker build -t gptweb2api .
docker run -p 8788:8788 -v ~/.codex:/root/.codex:ro \
-e GPTWEB_API_KEY=change-me gptweb2apiAll routes require Authorization: Bearer $GPTWEB_API_KEY except /healthz.
Errors follow the OpenAI error shape
({"error": {"message", "type", "param", "code"}}). Parameters that ChatGPT
Web cannot honor (for example max_tokens, temperature) are rejected with
unsupported_parameter rather than silently ignored.
| Route | Purpose |
|---|---|
GET /healthz |
Process liveness; no auth |
GET /readyz |
Readiness: auth + requirements provider (+ relay in browser mode) |
GET /metrics |
Prometheus text metrics |
curl http://127.0.0.1:8788/healthz
curl http://127.0.0.1:8788/readyz -H "Authorization: Bearer $KEY"
curl http://127.0.0.1:8788/metrics -H "Authorization: Bearer $KEY"| Route | Purpose |
|---|---|
GET /v1/models |
Live catalog from the account |
GET /v1/models/{model} |
One catalog entry |
curl http://127.0.0.1:8788/v1/models -H "Authorization: Bearer $KEY"{"object": "list", "data": [
{"id": "auto", "object": "model", "owned_by": "chatgpt-web"},
{"id": "gpt-5.6", "object": "model", "owned_by": "chatgpt-web"}
]}curl http://127.0.0.1:8788/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{
"model": "auto",
"stream": false,
"messages": [
{"role": "system", "content": "You are concise."},
{"role": "user", "content": "Explain SSE in one paragraph."}
]
}'Streaming — standard OpenAI data: chunks ending with data: [DONE]:
curl -N http://127.0.0.1:8788/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"model":"auto","stream":true,"messages":[{"role":"user","content":"Count to five."}]}'With images (data URLs or public HTTPS URLs), up to 10 attachments:
{
"model": "auto",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "What is in this picture?"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}}
]
}]
}With documents (text-like MIME types ride the file-service automatically):
{
"model": "auto",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "Summarize this file."},
{"type": "file", "file": {"filename": "notes.txt", "file_data": "data:text/plain;base64,..."}}
]
}]
}store: true persists the completion locally for later retrieval.
| Route | Purpose |
|---|---|
GET /v1/chat/completions |
List stored completions |
POST /v1/chat/completions/{completion_id} |
Update stored metadata |
GET /v1/chat/completions/{completion_id} |
Retrieve one |
DELETE /v1/chat/completions/{completion_id} |
Delete one |
GET /v1/chat/completions/{completion_id}/messages |
Stored messages |
curl http://127.0.0.1:8788/v1/chat/completions -H "Authorization: Bearer $KEY"
curl http://127.0.0.1:8788/v1/chat/completions/{completion_id}/messages -H "Authorization: Bearer $KEY"curl http://127.0.0.1:8788/v1/completions \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"model":"auto","prompt":"Say hi in three words"}'Stateful continuation via previous_response_id; text, image, and local
input_file inputs; streaming supported.
curl http://127.0.0.1:8788/v1/responses \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"model":"auto","input":"Remember the codeword: LANTERN-7"}'
# continue the same thread
curl http://127.0.0.1:8788/v1/responses \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"model":"auto","previous_response_id":"resp_...","input":"What was the codeword?"}'Input with a stored file:
{
"model": "auto",
"input": [{
"role": "user",
"content": [
{"type": "input_text", "text": "Summarize"},
{"type": "input_file", "file_id": "file_..."}
]
}]
}| Route | Purpose |
|---|---|
GET /v1/responses/{response_id} |
Retrieve |
DELETE /v1/responses/{response_id} |
Delete |
GET /v1/responses/{response_id}/input_items |
Input items |
GET /v1/responses/{response_id}/input_items/{item_id} |
One input item |
curl http://127.0.0.1:8788/v1/images/generations \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"model":"auto","prompt":"a red circle on white background, flat vector","n":1}'{"created": 1787..., "data": [{"b64_json": "iVBORw0KGgo..."}]}curl http://127.0.0.1:8788/v1/images/edits \
-H "Authorization: Bearer $KEY" \
-F "prompt=Change the circle color from red to blue" \
-F "image=@input.png;type=image/png"Up to 10 input images, 20 MiB total. PNG/JPEG/GIF/WebP.
curl http://127.0.0.1:8788/v1/images/variations \
-H "Authorization: Bearer $KEY" \
-F "image=@input.png;type=image/png"Convenience endpoint: one or more images plus a prompt, no data URLs needed.
curl http://127.0.0.1:8788/v1/vision/analyze \
-H "Authorization: Bearer $KEY" \
-F "prompt=What is in this image?" \
-F "image=@photo.jpg;type=image/jpeg"| Route | Purpose |
|---|---|
POST /v1/files |
Upload (multipart: purpose, file) |
GET /v1/files |
List |
GET /v1/files/{file_id} |
Metadata |
GET /v1/files/{file_id}/content |
Download bytes |
DELETE /v1/files/{file_id} |
Delete |
curl http://127.0.0.1:8788/v1/files \
-H "Authorization: Bearer $KEY" \
-F "purpose=assistants" -F "file=@notes.txt;type=text/plain"
curl http://127.0.0.1:8788/v1/files/{file_id}/content \
-H "Authorization: Bearer $KEY" -o notes.txtLocal storage only (.gptweb2api-files/) — files referenced by Responses
input_file and large-context conversion.
| Route | Purpose |
|---|---|
POST /v1/uploads |
Create an upload session (multipart: purpose, filename, mime_type) |
POST /v1/uploads/{upload_id}/parts |
Append a chunk (multipart: data) |
POST /v1/uploads/{upload_id}/complete |
Finish (JSON: part_ids, optional md5) |
POST /v1/uploads/{upload_id}/cancel |
Abort |
| Route | Purpose |
|---|---|
POST /v1/conversations |
Create |
POST /v1/conversations/{conversation_id} |
Update metadata |
GET /v1/conversations/{conversation_id} |
Retrieve |
DELETE /v1/conversations/{conversation_id} |
Delete |
POST /v1/conversations/{conversation_id}/items |
Append items |
GET /v1/conversations/{conversation_id}/items |
List items |
GET /v1/conversations/{conversation_id}/items/{item_id} |
One item |
DELETE /v1/conversations/{conversation_id}/items/{item_id} |
Remove item |
curl http://127.0.0.1:8788/v1/conversations \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{}'
curl http://127.0.0.1:8788/v1/conversations/{conversation_id}/items \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"items":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}]}'JSONL batches over the local Files store, processed sequentially.
| Route | Purpose |
|---|---|
POST /v1/batches |
Create (JSON: input_file_id, endpoint, completion_window) |
GET /v1/batches |
List |
GET /v1/batches/{batch_id} |
Status |
POST /v1/batches/{batch_id}/cancel |
Cancel |
curl http://127.0.0.1:8788/v1/batches \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"input_file_id":"file_...","endpoint":"/v1/chat/completions","completion_window":"24h"}'All configuration comes from environment variables (.env is loaded
automatically; see .env.example for the full annotated list).
| Variable | Default | Meaning |
|---|---|---|
GPTWEB_LISTEN |
127.0.0.1:8787 |
Listen address |
GPTWEB_API_KEY |
— | Bearer key clients must present |
GPTWEB_OAUTH_TOKEN_FILE |
— | OAuth credential JSON (auth.json) |
GPTWEB_TRANSPORT_MODE |
auto |
auto / direct / browser |
GPTWEB_STREAM_MAX_HANDOFFS |
64 |
Resumed-segment budget per turn |
GPTWEB_TIMEOUT_SECONDS |
180 |
Upstream connect/header timeout |
GPTWEB_UPSTREAM_MAX_RETRIES |
2 |
Transient 502/503/504 retries |
GPTWEB_BROWSER_RELAY_URL |
— | Enable the browser-relay fallback |
GPTWEB_FIDELITY_MODE |
api |
api adds a stateless output-only baseline prompt |
GPTWEB_LANGUAGE / GPTWEB_TIMEZONE |
en-US / UTC |
Web client locale hints |
A conversation turn performs the same short-lived integrity dance a real Web client performs — requirements fetch, proof-of-work solve, conduit preparation — and then streams the same v1 JSON-patch SSE the browser receives, including resumed segments. Long answers are segmented upstream; the gateway reconciles replays against a turn-wide emission ledger so output is delivered exactly once, in order, without truncation.
Oversized prompts travel as file attachments; generated images and uploads come back through the file-service. Full protocol notes, live qualification numbers, and debugging tools:
- docs/DIRECT_TRANSPORT.md — browserless transport deep dive
- docs/API_COMPATIBILITY.md — parameter contracts per route
- docs/OPERATIONS.md — running and monitoring
- docs/PROJECT_STATUS.md — engineering status report
| Tool | Purpose |
|---|---|
cmd/gptwebprobe |
Sanitized live checks: transport, file upload, gateway end-to-end |
cmd/gptweb2api-smoke |
Credential-safe operator smoke client |
cmd/gptweb2api-context-smoke |
Deterministic large-context marker-recall qualification |
GPTWEB_DEBUG_WIRE=1 |
Per-turn integrity and stream diagnostics |
cmd/gptweb2api gateway entry point
cmd/gptweb2api-smoke operator smoke client
cmd/gptweb2api-context-smoke large-context qualification CLI
cmd/gptwebprobe live transport probe
internal/chatgpt Web transport: sentinel, SSE, uploads, images
internal/openai OpenAI-compatible HTTP surface and stores
internal/config environment configuration
tools/browser-relay optional Playwright/Chrome fallback relay
docs/ protocol notes, status, compatibility
This project talks to ChatGPT Web's private, undocumented interface using your own account and is not affiliated with or endorsed by OpenAI. The private interface can change at any time; nothing here bypasses account limits or grants access you do not already have. Use it for personal automation at your own risk and in accordance with the terms that apply to your account.