Tip
Check our Project Roadmap for current progress and upcoming features.
Goal: I aim to achieve full Anthropic API parity so this proxy can be hooked up to the leaked Claude Code source code, allowing it to serve as a complete replacement for the Anthropic API.
A Node.js proxy server that exposes an OpenAI-compatible REST API backed by a headless ChatGPT browser session (Puppeteer). Supports plain chat completions and full tool/function calling via a structured prompt translation layer.
Client (OpenAI SDK / curl)
│ POST /v1/chat/completions
▼
proxy.controller.js
│ buildPromptWithTools()
│ Serialises messages + tool schemas → plain text prompt
▼
browser.util.js (ChatGPTClient)
│ Types prompt into chatgpt.com, intercepts auth headers,
│ calls /backend-anon/f/conversation via in-page fetch
▼
ChatGPT (chatgpt.com)
│ Returns plain text — either an answer or a <tool_call> block
▼
tools.utils.js → parseToolCallReply()
│ Detects <tool_call> tags → OpenAI tool_calls shape
│ OR passes text through as content
▼
OpenAI-format JSON response → Client
src/
├── server.js # Entry point — starts Express, initialises browser
├── app.js # Express app, mounts routes
├── routes/
│ ├── proxy.route.js # POST /v1/chat/completions
│ └── models.route.js # GET /v1/models (list and individual lookups)
├── controller/
│ └── proxy.controller.js # Request validation, orchestration, response shaping
└── utils/
├── browser.util.js # ChatGPTClient — Puppeteer browser automation
├── tools.utils.js # Tool schema serialisation + tool_call reply parsing
├── token.util.js # Token counting via js-tiktoken (cl100k_base)
└── proxy.util.js # Proxy utilities
npm install
npm start # starts on PORT env var or 3000On startup the server launches a Chromium window, navigates to chatgpt.com, and waits for the prompt textarea. If Cloudflare is active this may take up to 60 seconds. The browser stays alive and is reused for all subsequent requests.
Server running on port 3000
Initializing browser...
[init] waiting for #prompt-textarea...
[init] ready
You can also run the proxy as a Docker container. The included Dockerfile handles all dependencies and sets up a virtual display (Xvfb) so the browser can run in a headless environment.
docker build -t chatgpt-proxy .docker run -d \
--name chatgpt-proxy \
-p 3000:3000 \
--shm-size=2gb \
chatgpt-proxyImportant
The --shm-size=2gb flag is required. Puppeteer/Chrome uses /dev/shm to share data between processes, and the default Docker limit (64MB) is usually too small for stable browser operation.
Liveness check. Returns immediately without touching the browser.
Response
{ "status": "ok" }Standard OpenAI endpoint to list available models, their max tokens, and metadata.
Supported Models:
gpt-5.3(Slug:gpt-5-3)gpt-5.2(Slug:gpt-5-2)gpt-5.1(Slug:gpt-5-1)gpt-5(Slug:gpt-5)gpt-5-mini(Slug:gpt-5-mini)o1,o1-minigpt-4o,gpt-4o-miniauto
Response
{
"object": "list",
"data": [
{
"id": "gpt-5.3",
"max_tokens": 34834,
"owned_by": "openai",
"object": "model",
"created": 1740614400
},
...
]
}Retrieve details for a single model.
OpenAI-compatible chat completions endpoint. Supports plain conversations and tool/function calling.
Headers
Content-Type: application/json
{
"model": "auto",
"tools": [],
"messages": [
{ "role": "system", "content": "You are a trading assistant." },
{ "role": "user", "content": "Whats your role?" }
]
}| Field | Type | Required | Description |
|---|---|---|---|
messages |
array | ✅ yes | Conversation history. See Message Roles |
model |
string | no | Label for the response. Default: "chatgpt-proxy" |
max_tokens |
number | no | Max tokens to return. Truncates and sets finish_reason: length |
mode |
string | no | Trigger specific features (reasoning, deep-research, etc.) |
The mode field in the request body can be used to trigger specific ChatGPT features.
mode value |
Description | Underlying ChatGPT Feature |
|---|---|---|
reasoning |
High-level reasoning mode | system_hints: ["reason"] |
deep-research |
ChatGPT Deep Research | connector_openai_deep_research |
tatertot |
Specialized "tatertot" hint(studying mode) | system_hints: ["tatertot"] |
quiz |
QuizGPT connector | connector_openai_quizgpt_v2 |
| (empty or other) | Standard assistant | primary_assistant |
- No streaming —
stream: truereturns a 400. The browser-based transport receives the full response before it can be forwarded. - Single concurrent request —
ChatGPTClientserialises all calls through a queue. Concurrent requests wait in order. - Auth session — The proxy relies on the browser's active ChatGPT session. If the session expires a browser restart or re-login is required.
- Headless mode — While supported via the
HEADLESS=trueenv var, Cloudflare checks are much more likely to fail in fully headless environments. If running on a server without a GUI, consider usingxvfb-runor similar virtual displays.
All four OpenAI message roles are supported.
{ "role": "system", "content": "You are a concise assistant." }{ "role": "user", "content": "Read the file notes.txt" }{ "role": "assistant", "content": "The file contains your shopping list." }{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_tuefggdxl",
"type": "function",
"function": {
"name": "Read",
"arguments": "{\"path\":\"notes.txt\"}"
}
}
]
}{
"role": "tool",
"tool_call_id": "call_tuefggdxl",
"content": "Buy milk\nCall dentist"
}
tool_call_idis optional but recommended — include it to match OpenAI's spec and keep multi-tool conversations unambiguous.
{
"model": "gpt-5.3",
"tools": [
{
"type": "function",
"function": {
"name": "Read",
"description": "Read the contents of a file",
"parameters": {
"type": "object",
"properties": {
"path": { "type": "string", "description": "The file path to read" }
},
"required": ["path"]
}
}
}
],
"messages": [{ "role": "user", "content": "What is in notes.txt?" }]
}When the model decides to use a tool, finish_reason is "tool_calls" and content is null:
{
"id": "chatcmpl-abc123",
"object": "chat.completion",
"created": 1716000000,
"model": "gpt-5.3",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_tuefggdxl",
"type": "function",
"function": {
"name": "Read",
"arguments": "{\"path\":\"notes.txt\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
],
"usage": {
"prompt_tokens": 142,
"completion_tokens": 18,
"total_tokens": 160
}
}Append the assistant's tool call message and the tool result to your messages array, then call the endpoint again:
{
"model": "gpt-5.3",
"tools": [
/* same tools array */
],
"messages": [
{ "role": "user", "content": "What is in notes.txt?" },
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_tuefggdxl",
"type": "function",
"function": {
"name": "Read",
"arguments": "{\"path\":\"notes.txt\"}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_tuefggdxl",
"content": "Buy milk\nCall dentist"
}
]
}{
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "The file notes.txt contains two items: Buy milk and Call dentist."
},
"finish_reason": "stop"
}
]
}All successful responses follow the OpenAI chat.completion shape. Below is an example of a cleaned, flattened response:
{
"id": "chatcmpl-e9f87659-dd9f-40fc-ada4-92cec1935b3e",
"object": "chat.completion",
"created": 1775385588,
"model": "auto",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Answering questions & explaining concepts (from simple to complex) Software engineering help (coding, debugging, system design) Trading & market insights (analysis, strategies, risk concepts—not financial advice) Research & summaries Planning & decision support Creative and practical writing I aim to give clear, accurate, and useful responses tailored to what you need. If you want, you can test me—ask me anything 👍"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 16,
"completion_tokens": 80,
"total_tokens": 96
}
}Token counts are calculated using
js-tiktokenwith thecl100k_baseencoding (same as GPT-4).
All errors use the OpenAI error envelope:
{
"error": {
"message": "Human-readable description",
"type": "invalid_request_error" | "proxy_error"
}
}| Status | type |
Cause |
|---|---|---|
400 |
invalid_request_error |
messages missing or empty |
400 |
invalid_request_error |
stream: true in the request |
500 |
proxy_error |
Browser/ChatGPT error (auth expired, empty response, etc.) |
Point the SDK at this server by overriding baseURL:
import OpenAI from "openai";
const client = new OpenAI({
apiKey: "not-needed", // required by SDK but ignored by proxy
baseURL: "http://localhost:3000/v1",
});
// Plain chat
const res = await client.chat.completions.create({
model: "gpt-5.3",
messages: [{ role: "user", content: "Hello!" }],
});
// With tools
const res2 = await client.chat.completions.create({
model: "gpt-5.3",
tools: [
{
type: "function",
function: {
name: "Read",
description: "Read a file",
parameters: {
type: "object",
properties: { path: { type: "string" } },
required: ["path"],
},
},
},
],
messages: [{ role: "user", content: "What is in notes.txt?" }],
});- No streaming —
stream: truereturns a 400. The browser-based transport receives the full response before it can be forwarded. - Single concurrent request —
ChatGPTClientserialises all calls through a queue. Concurrent requests wait in order. - Auth session — The proxy relies on the browser's active ChatGPT session. If the session expires a browser restart or re-login is required.
- Headless supported — Runs in
headless: truemode by default while still passing bot checks.
npm install -D vitest supertest
npm testTests mock ChatGPTClient so no browser is required. See src/tests/ for unit tests covering tools.utils.js and integration tests covering the full HTTP stack.