A local HTTP proxy server that exposes OpenAI-compatible API endpoints using your ChatGPT OAuth tokens. It authenticates via an auth.json file (produced by codex login) and forwards requests to OpenAI's upstream Codex backend, translating between standard OpenAI API formats and the internal Responses API.
This is a Java 21+ project, packaged as a single self-contained JAR.
- Java 21 or later
- Maven 3.9+ (for building from source)
- A valid
auth.jsonfile (if you don't have one runnpx @openai/codex loginto create it in ~/.codex)
https://github.com/skanga/AIProxyOauth/releases or
mvn clean package -DskipTestsThis produces a fat JAR at target/AIProxyOauth-1.2.0.jar.
java -jar target/AIProxyOauth-1.2.0.jarThe server starts on http://127.0.0.1:10531/v1 by default.
On startup, the proxy prints the endpoint, discovered models, client API key enforcement status, network exposure, and the auth.json file it selected from the auth file discovery paths. It also performs a startup self-check by sending a streaming chat completion through the proxy and printing the actual model response text.
Example:
OpenAI OAuth Proxy Server started
Endpoint: http://127.0.0.1:10531/v1
Models: gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.3-codex-spark, codex-auto-review
Client API key enforcement: disabled
Network access: Local access only
Auth file: C:\Users\skanga\.codex\auth.json
Startup check: chat completion OK (model: gpt-5.4)
Startup response: Hello! How can I help?
If the startup self-check receives HTTP 200 but no model text, it is reported as a failed check:
Startup check: chat completion failed (HTTP 200, no model response text) (model: gpt-5.4)
Startup response: <missing streaming choices[].delta.content>
# Health check
curl http://127.0.0.1:10531/health
# List available models
curl http://127.0.0.1:10531/v1/models
# Chat completion (non-streaming)
curl -X POST http://127.0.0.1:10531/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"gpt-5.4","messages":[{"role":"user","content":"Hello!"}]}'
# Chat completion (streaming)
curl -X POST http://127.0.0.1:10531/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"gpt-5.4","messages":[{"role":"user","content":"Hello!"}],"stream":true}'
# Responses API (non-streaming)
curl -X POST http://127.0.0.1:10531/v1/responses \
-H "Content-Type: application/json" \
-d '{"model":"gpt-5.4","input":"Hello!"}'
# Responses API (streaming)
curl -X POST http://127.0.0.1:10531/v1/responses \
-H "Content-Type: application/json" \
-d '{"model":"gpt-5.4","stream":true,"input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"Hello!"}]}]}'| Option | Default | Description |
|---|---|---|
--host <host> |
127.0.0.1 |
Network interface to bind to |
--port <port> |
10531 |
Port to listen on |
--models <ids> |
auto-discover | Comma-separated model IDs to expose |
--codex-version <ver> |
auto-detect | Codex API version for model discovery |
--base-url <url> |
https://chatgpt.com/backend-api/codex |
Upstream Codex base URL |
--oauth-client-id <id> |
app_EMoamEEZ73f0CkXaXp7hrann |
OAuth client ID for token refresh |
--oauth-token-url <url> |
https://auth.openai.com/oauth/token |
OAuth token endpoint |
--oauth-file <path> |
auto-discover | Path to auth.json |
--api-key <keys> |
(open mode) | Comma-separated API keys clients must present |
--api-keys-file <path> |
(open mode) | Path to a file with one API key per line |
--generate-key [name] |
Print a new random API key and exit; optional name produces name:key output |
|
--admin-key <key> |
(none) | Owner key that sees all users' stats at GET /v1/usage |
--store |
false |
Whether to ask upstream to store responses. The proxy itself does not persist responses locally |
--allow-any-cors |
false |
Allow browser requests from any origin |
--cors-origin <origin> |
(none) | Allow a specific browser origin; can be repeated or comma-separated |
--log-requests |
false |
Log full proxied request/response metadata to disk with sensitive headers redacted |
--request-log-dir <path> |
./logs/requests |
Directory for --log-requests output |
--forward-prompt-cache-headers |
false |
Experimental: forward prompt_cache_key as upstream conversation_id and session_id headers |
--codex-instructions <mode> |
configured |
Instruction source: configured or latest-codex |
--codex-instructions-cache-dir <path> |
./cache/codex-instructions |
Cache directory for latest-codex instructions |
--help |
Show help and exit | |
--version |
Show version and exit |
Request logging warning: --log-requests may write prompts, tool outputs, file paths, and other sensitive body content to disk. Authorization, API key, cookie, token, secret, and key-like headers are redacted case-insensitively, but log files should still be protected.
The CLI also prints a simple access-log line for each request, similar to a web server log. It includes timestamp, method, path, status, duration, request ID, stream/sync mode when known, request content length, response status, and response byte count. It does not include headers, query strings, request bodies, API keys, or OAuth tokens.
# Bind to all interfaces on port 8080
java -jar AIProxyOauth-1.2.0.jar --host 0.0.0.0 --port 8080
# Expose only specific models
java -jar AIProxyOauth-1.2.0.jar --models gpt-5.4,gpt-5.5
# Use a custom auth file location
java -jar AIProxyOauth-1.2.0.jar --oauth-file /path/to/auth.json
# Generate a new API key for a client
java -jar AIProxyOauth-1.2.0.jar --generate-key
# Require clients to present a key (inline)
java -jar AIProxyOauth-1.2.0.jar --api-key sk-proxy-a3f9c2d1e4b5f6a7b8c9d0e1f2a3b4c5
# Require clients to present a key (from file)
java -jar AIProxyOauth-1.2.0.jar --api-keys-file /path/to/keys.txt
# Enable redacted request logs
java -jar AIProxyOauth-1.2.0.jar --log-requests --request-log-dir ./logs/requestsBy default the proxy runs in open mode — any client on the network can make requests. To restrict access, configure one or more API keys. When keys are configured, all requests except GET /health must include a valid key in the Authorization: Bearer <key> header; otherwise the proxy returns 401 Unauthorized.
Note: If an --admin-key is set (either via CLI or in the keys file), the proxy automatically disables open mode and enforces authentication for all endpoints (except /health), even if no regular --api-key entries are provided. In this case, only the admin key will be accepted for general API requests.
# Without a name (bare key)
java -jar AIProxyOauth-1.2.0.jar --generate-key
# sk-proxy-a3f9c2d1e4b5f6a7b8c9d0e1f2a3b4c5
# With a name — output is ready to paste into a keys file or --api-key
java -jar AIProxyOauth-1.2.0.jar --generate-key cursor
# cursor:sk-proxy-a3f9c2d1e4b5f6a7b8c9d0e1f2a3b4c5Keys use the format sk-proxy- followed by 32 random lowercase hex characters, matching OpenAI's visual style so existing clients accept them as API keys. The name is stored alongside the key so the server can identify which client made each request.
Each key can optionally carry a human-readable name using the name:key format. Names are used in startup output and future per-key reporting (token usage, etc.). If no name is given the key value is used as its own name.
Inline (one or more, comma-separated):
# Named keys
java -jar AIProxyOauth-1.2.0.jar --api-key cursor:sk-proxy-a3f9c2d1e4b5f6a7b8c9d0e1f2a3b4c5
# Multiple, mixed named/unnamed
java -jar AIProxyOauth-1.2.0.jar --api-key cursor:sk-proxy-a3f9...,vscode:sk-proxy-1a2b...From a file (one entry per line; blank lines and # comments are ignored):
# keys.txt — format: [name:]key
cursor:sk-proxy-a3f9c2d1e4b5f6a7b8c9d0e1f2a3b4c5
vscode:sk-proxy-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d
admin:sk-proxy-99887766554433221100aabbccddeeff # promoted to --admin-key
sk-proxy-ffeeddccbbaa00112233445566778899 # unnamed — key used as its own name
java -jar AIProxyOauth-1.2.0.jar --api-keys-file keys.txtIf an entry in the file uses the reserved name admin, it is automatically promoted to the admin key (equivalent to --admin-key). If both are provided, the CLI flag takes precedence.
Both options can be combined; keys from all sources are merged.
curl http://127.0.0.1:10531/v1/models \
-H "Authorization: Bearer sk-proxy-a3f9c2d1e4b5f6a7b8c9d0e1f2a3b4c5"The GET /health endpoint is always open regardless of key configuration.
The --host option determines which network interfaces the proxy binds to:
- Local access only (Default):
--host 127.0.0.1The proxy is only accessible from the machine it is running on. This is the most secure setting for personal use. - Full network access:
--host 0.0.0.0The proxy is accessible from any device on your local network (or the internet, if your port is forwarded).
Warning: If you use --host 0.0.0.0, it is strongly recommended to configure API keys (see API Key Enforcement) to prevent unauthorized use of your ChatGPT account.
The proxy stores sensitive OAuth tokens in auth.json. On the first write (or refresh), the proxy attempts to set strict file permissions (read/write by owner only, chmod 600) to protect these tokens from other users on the same machine.
Returns liveness/status information that is safe for unauthenticated callers.
Response:
{
"ok": true,
"service": "AIProxyOauth",
"version": "1.2.0",
"uptime_seconds": 1234
}Returns the list of available models. Models are auto-discovered from your account and cached for 5 minutes, unless overridden with --models.
Response:
{
"object": "list",
"data": [
{"id": "gpt-5.6-sol", "object": "model", "created": 0, "owned_by": "codex-oauth"},
{"id": "gpt-5.6-terra", "object": "model", "created": 0, "owned_by": "codex-oauth"},
{"id": "gpt-5.6-luna", "object": "model", "created": 0, "owned_by": "codex-oauth"},
{"id": "gpt-5.5", "object": "model", "created": 0, "owned_by": "codex-oauth"},
{"id": "gpt-5.4", "object": "model", "created": 0, "owned_by": "codex-oauth"},
{"id": "gpt-5.4-mini", "object": "model", "created": 0, "owned_by": "codex-oauth"},
{"id": "gpt-5.3-codex-spark", "object": "model", "created": 0, "owned_by": "codex-oauth"},
{"id": "codex-auto-review", "object": "model", "created": 0, "owned_by": "codex-oauth"}
]
}Standard OpenAI Chat Completions API. Supports both streaming ("stream": true) and non-streaming requests.
Internally, chat completions are sent to the upstream Responses API as a stream. For non-streaming client requests, the proxy collects the upstream SSE events and returns one OpenAI-style chat.completion response. If a model emits text only as response.output_text.delta events and omits it from the final completed response object, the collector uses those deltas to populate choices[0].message.content.
The collector also reconstructs function calls from output-item and argument events when the final completed object omits them. If a completed stream contains no recoverable text, tool call, or refusal, the proxy returns a 502 upstream_protocol_error rather than a successful empty assistant message.
Supported request fields:
model— model ID (default:gpt-5.5)messages— array of message objects (system,developer,user,assistant,toolroles)stream— boolean for SSE streamingtemperature,top_p— sampling parametersmax_tokens— maximum output tokensstop— stop sequence(s)tools— function tool definitionstool_choice—"auto","none","required", or{"type":"function","function":{"name":"..."}}reasoning_effort—"low","medium", or"high"
Model aliases can set backend model and reasoning effort together. For example, gpt-5.3-codex-spark-xhigh is forwarded as model: "gpt-5.3-codex-spark" with reasoning.effort: "xhigh" when no explicit reasoning_effort is provided. Unsupported reasoning values are clamped before forwarding: minimal becomes low, none is upgraded for Codex models, unsupported xhigh becomes high, and Codex Mini is limited to medium or high.
Non-streaming response:
{
"id": "chatcmpl_...",
"object": "chat.completion",
"created": 1710000000,
"model": "gpt-5.5",
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": "Hello!"},
"finish_reason": "stop"
}],
"usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}
}Streaming response: Server-Sent Events with chat.completion.chunk objects, followed by data: [DONE].
Returns accumulated token usage broken down by API key name, plus an overall total. Counts are in-memory and reset when the server restarts.
Response:
{
"keys": [
{"name": "cursor", "prompt_tokens": 1234, "completion_tokens": 567, "total_tokens": 1801},
{"name": "vscode", "prompt_tokens": 890, "completion_tokens": 234, "total_tokens": 1124}
],
"total": {"prompt_tokens": 2124, "completion_tokens": 801, "total_tokens": 2925}
}Token counts are tracked for POST /v1/chat/completions and POST /v1/responses, for both streaming and non-streaming requests. In open mode (no API keys configured), usage is reported under the aggregate key name *.
Each key sees only its own stats. The proxy owner can configure an --admin-key to see all users' stats:
java -jar AIProxyOauth-1.2.0.jar \
--api-key cursor:sk-proxy-... \
--api-key vscode:sk-proxy-... \
--admin-key sk-proxy-...POST /v1/responses is a passthrough to the upstream Responses API with normalization. Requests are always sent upstream as streaming requests; if the client asked for stream:false, the proxy collects the upstream SSE response and returns one JSON object.
input accepts either a string or an array of typed input items. String input is normalized to a typed user message before replay expansion. See COMPATIBILITY.md for the translation matrix and upstream limitations.
The proxy does not persist responses, input items, or conversations locally. previous_response_id and item_reference are expanded only from the bounded in-memory same-process replay cache when available; otherwise the request is forwarded as-is and the upstream service decides whether it can resolve the reference.
store is forwarded upstream but does not enable local storage. Both streaming and non-streaming Responses can populate the bounded in-memory replay cache after a completed response event is seen.
When the effective request is stateless (store:false, including the default), the proxy removes unresolved item_reference entries, strips item id fields, and converts orphaned tool output items into assistant messages before forwarding. This avoids upstream store:false lookup errors while preserving useful context.
If --forward-prompt-cache-headers is enabled and the normalized request contains prompt_cache_key, the proxy also sends that value as upstream conversation_id and session_id headers. The original body field is preserved. This is experimental and defaults to off.
Usage-limit style upstream 404 responses containing usage_limit_reached, usage_not_included, rate_limit_exceeded, or usage limit are returned to clients as 429 errors.
--codex-instructions latest-codex enables optional model-family instruction lookup with a 15-minute cache and stale-cache fallback. The default configured mode uses the configured instructions exactly as before and performs no instruction fetch.
The server searches for auth.json in this order:
- Path provided via
--oauth-file $CHATGPT_LOCAL_HOME/auth.json$CODEX_HOME/auth.json~/.chatgpt-local/auth.json~/.codex/auth.json
{
"tokens": {
"id_token": "eyJ...",
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"account_id": "acct_..."
},
"last_refresh": "2026-01-15T10:30:00.000Z"
}To create this file, run:
npx @openai/codex loginTokens are automatically refreshed when:
- The access token expires within 5 minutes (based on JWT
expclaim) - More than 55 minutes have passed since the last refresh
After a successful refresh, the updated tokens are written back to the auth file.
If --models is not specified, models are discovered automatically:
- The Codex client version is resolved:
--codex-versionflag, then local Codex CLI detection (codex --version, orcodex.cmd --versionon Windows), then npm registry, then fallback0.121.0 - The upstream
/models?client_version=Xendpoint is queried - Results are cached for 5 minutes
Point any OpenAI-compatible client at the proxy's base URL. You can use either Chat Completions or Responses API methods.
from openai import OpenAI
client = OpenAI(
base_url="http://127.0.0.1:10531/v1",
api_key="not-needed" # open mode: any non-empty string works
# api_key="sk-proxy-a3f9c2d1e4b5f6a7b8c9d0e1f2a3b4c5" # enforcement mode
)
chat_response = client.chat.completions.create(
model="gpt-5.5",
messages=[{"role": "user", "content": "Hello!"}]
)
print(chat_response.choices[0].message.content)
responses_response = client.responses.create(
model="gpt-5.5",
input=[{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "Hello from Responses!"}],
}],
)
print(responses_response.output_text)import OpenAI from 'openai';
const client = new OpenAI({
baseURL: 'http://127.0.0.1:10531/v1',
apiKey: 'not-needed', // open mode: any non-empty string works
// apiKey: 'sk-proxy-a3f9c2d1e4b5f6a7b8c9d0e1f2a3b4c5', // enforcement mode
});
const chatResponse = await client.chat.completions.create({
model: 'gpt-5.5',
messages: [{ role: 'user', content: 'Hello!' }],
});
console.log(chatResponse.choices[0].message.content);
const responsesResponse = await client.responses.create({
model: 'gpt-5.5',
input: [{
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: 'Hello from Responses!' }],
}],
});
console.log(responsesResponse.output_text);# Chat Completions, open mode
curl http://127.0.0.1:10531/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"gpt-5.5","messages":[{"role":"user","content":"What is 2+2?"}]}'
# Responses API, open mode
curl http://127.0.0.1:10531/v1/responses \
-H "Content-Type: application/json" \
-d '{"model":"gpt-5.5","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"What is 2+2?"}]}]}'
# Responses API, streaming
curl http://127.0.0.1:10531/v1/responses \
-H "Content-Type: application/json" \
-d '{"model":"gpt-5.5","stream":true,"input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"Write one sentence."}]}]}'
# Enforcement mode: add the proxy API key to either endpoint
curl http://127.0.0.1:10531/v1/chat/completions \
-H "Authorization: Bearer sk-proxy-a3f9c2d1e4b5f6a7b8c9d0e1f2a3b4c5" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-5.5","messages":[{"role":"user","content":"What is 2+2?"}]}'Run npx @openai/codex login to create the auth file, or specify its location with --oauth-file.
Your auth.json exists but contains no valid access token. Re-run npx @openai/codex login.
The id_token in your auth file does not contain the expected account ID claim. Re-run npx @openai/codex login.
Ensure your auth tokens are valid. You can bypass discovery by specifying models explicitly: --models gpt-5.5,gpt-5.4.
Check that no other process is using port 10531, or specify a different port with --port.
See repository for license details.