Skip to content

skanga/AIProxyOauth

Repository files navigation

AIProxyOauth

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.

Prerequisites

  • Java 21 or later
  • Maven 3.9+ (for building from source)
  • A valid auth.json file (if you don't have one run npx @openai/codex login to create it in ~/.codex)

Quick Start

Download from Github releases

https://github.com/skanga/AIProxyOauth/releases or

Build manually

mvn clean package -DskipTests

This produces a fat JAR at target/AIProxyOauth-1.2.0.jar.

Run

java -jar target/AIProxyOauth-1.2.0.jar

The server starts on http://127.0.0.1:10531/v1 by default.

Startup output

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>

Verify

# 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!"}]}]}'

CLI Options

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.

Examples

# 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/requests

API Key Enforcement

By 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.

Generating a key

# 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-a3f9c2d1e4b5f6a7b8c9d0e1f2a3b4c5

Keys 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.

Configuring keys

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.txt

If 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.

Using a key from a client

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.

Security Considerations

Network Access

The --host option determines which network interfaces the proxy binds to:

  • Local access only (Default): --host 127.0.0.1 The 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.0 The 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.

Token Security

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.


API Endpoints

GET /health

Returns liveness/status information that is safe for unauthenticated callers.

Response:

{
  "ok": true,
  "service": "AIProxyOauth",
  "version": "1.2.0",
  "uptime_seconds": 1234
}

GET /v1/models

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"}
  ]
}

POST /v1/chat/completions

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, tool roles)
  • stream — boolean for SSE streaming
  • temperature, top_p — sampling parameters
  • max_tokens — maximum output tokens
  • stop — stop sequence(s)
  • tools — function tool definitions
  • tool_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].

GET /v1/usage

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

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.

Authentication

Auth File Discovery

The server searches for auth.json in this order:

  1. Path provided via --oauth-file
  2. $CHATGPT_LOCAL_HOME/auth.json
  3. $CODEX_HOME/auth.json
  4. ~/.chatgpt-local/auth.json
  5. ~/.codex/auth.json

Auth File Format

{
  "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 login

Token Refresh

Tokens are automatically refreshed when:

  • The access token expires within 5 minutes (based on JWT exp claim)
  • More than 55 minutes have passed since the last refresh

After a successful refresh, the updated tokens are written back to the auth file.

Model Discovery

If --models is not specified, models are discovered automatically:

  1. The Codex client version is resolved: --codex-version flag, then local Codex CLI detection (codex --version, or codex.cmd --version on Windows), then npm registry, then fallback 0.121.0
  2. The upstream /models?client_version=X endpoint is queried
  3. Results are cached for 5 minutes

Using with OpenAI Client Libraries

Point any OpenAI-compatible client at the proxy's base URL. You can use either Chat Completions or Responses API methods.

Python

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)

Node.js

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);

curl

# 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?"}]}'

Troubleshooting

"No auth file was found"

Run npx @openai/codex login to create the auth file, or specify its location with --oauth-file.

"ChatGPT access token not found"

Your auth.json exists but contains no valid access token. Re-run npx @openai/codex login.

"ChatGPT account id not found"

The id_token in your auth file does not contain the expected account ID claim. Re-run npx @openai/codex login.

Model discovery fails

Ensure your auth tokens are valid. You can bypass discovery by specifying models explicitly: --models gpt-5.5,gpt-5.4.

Connection refused

Check that no other process is using port 10531, or specify a different port with --port.

License

See repository for license details.

About

A lightweight Java proxy that turns your ChatGPT OAuth key session into a local OpenAI-compatible API with no need for a valid OPENAI_API_KEY so it allows any OpenAI client to access Codex models, with streaming, token refresh, and optional multi-tenant API key management.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages