This reference documents the API behavior owned by codex-oauth-proxy and the
supported proxy route surface. It does not reproduce the complete upstream
Responses API schema.
Examples use:
http://127.0.0.1:8317
The OpenAI-compatible client base URL is:
http://127.0.0.1:8317/v1
The server reads candidate credentials from:
Authorization: Bearer <token>and:
X-API-Key: <token>When both are present, either matching credential can authenticate the request.
| Route group | Credential |
|---|---|
/, /healthz |
None |
/v1/* |
Managed cop_... user API key |
/v0/user/* |
Managed cop_... user API key |
/v0/management/* |
Configured admin-api-key |
/v0/local-admin/* |
Loopback source address; no key |
Selected /backend-api/* compatibility routes |
Managed user API key; some routes also accept a currently loaded Codex access token |
Disabled users receive 403. Missing, unknown, or rotated managed keys receive
401.
For proxied requests, the server accepts Session-Id or Session_id headers
and the JSON fields session_id, sessionId, prompt_cache_key,
conversation_id, and conversation.id.
Valid signals keep one logical session on the same available OAuth credential across model changes, API-key rotation, concurrent first requests, and process restarts. Managed bindings are isolated by user. Compatibility-token bindings are isolated by the matched stable OAuth identity.
Values over 512 bytes, empty values, invalid UTF-8, unpaired UTF-16 surrogate escapes, and values containing control characters are ignored for affinity. JSON bodies are inspected as a token stream without an affinity-specific body-size cutoff, and the exact body is restored before forwarding. If temporary replay storage cannot be created or written, body inspection stops, body-derived signals are ignored, and the stored prefix plus untouched request stream are forwarded unchanged. Ignored or missing signals do not fail the request; selection falls back to normal round-robin behavior.
Auth health is global across sessions. A healthy affinity binding stays on its current Codex credential; a binding whose credential is disabled, cooling, credential-invalid, continued-unauthorized, or unable to serve the requested model is rebound with compare-and-swap failover.
Cross-credential retry is available only for:
- Read-only
GETandHEADrequests on whitelisted routes. - JSON
/v1/chat/completions. - JSON
/v1/zed/edit-predictions. - Responses, Responses compact, alpha search, JSON image generation, and trace summarization.
- A Responses WebSocket handshake before successful upgrade.
Replayable request bodies are buffered in memory up to and including 32 MiB. Unknown-length or larger bodies and multipart, file, realtime, side-effecting wham, hosted MCP, and unknown write requests are sent once. They are not rejected merely because automatic replay is unavailable.
Within one replayable execution, 401 first refreshes and retries the same
credential once. A credential round then tries distinct eligible credentials.
After a round, the proxy may wait for the nearest cooldown and start another
round according to request-retry, max-retry-credentials, and
max-retry-interval. Cancellation stops waiting immediately. No retry occurs
after downstream response bytes or a successful WebSocket upgrade.
Project-owned handlers return:
{
"error": {
"message": "invalid API key",
"type": "Unauthorized"
}
}Proxied upstream routes may preserve upstream status codes, headers, and response
bodies instead. Request-scoped upstream 4xx errors stop immediately and do
not penalize a credential.
When replayable candidates are exhausted, the proxy returns a safe aggregate error:
{
"error": {
"message": "upstream Codex service unavailable",
"type": "proxy_error",
"code": "upstream_unavailable"
}
}Deterministic aggregate outcomes are:
| Status | Code | Meaning |
|---|---|---|
404 |
model_not_found |
Every known candidate is excluded for the requested model. |
429 |
rate_limited |
All serviceable candidates are quota-limited, or mixed failures have a clear near-term quota recovery. The response includes the earliest known Retry-After. |
503 |
auth_unavailable |
All candidates are disabled, credential-invalid, or continued-unauthorized. |
502 |
upstream_unavailable |
Network/retryable upstream failures were exhausted, including mixed failures without a near-term recovery deadline. |
Client cancellation does not synthesize a new proxy error.
Returns:
{"message":"codex-oauth-proxy"}Returns:
{"status":"ok"}The health endpoint checks that the HTTP handler is running. It does not perform an upstream Codex request.
Remote management is available only when admin-api-key is non-empty.
Equivalent local routes are available below /v0/local-admin to loopback
clients. All management responses include Cache-Control: no-store.
Lists every logical Codex auth, including disabled and unidentified credentials:
{
"auths": [
{
"account_id": "acct_xxx",
"identity_state": "identified",
"manageable": true,
"email": "a***@example.com",
"source_file_count": 2,
"enabled": true,
"runtime_state": "cooling",
"runtime_reason": "quota",
"token_expires_at": "2026-08-02T00:00:00Z",
"last_refresh_at": "2026-07-31T00:00:00Z",
"cooldown_until": "2026-08-01T01:00:00Z",
"cooldown_reason": "quota",
"model_capability_known": true,
"known_supported_models": ["gpt-5.3-codex"],
"model_exclusions": [],
"session_binding_count": 3,
"active_connection_count": 1,
"last_error": {
"code": "rate_limit_exceeded",
"status": 429
}
}
]
}runtime_state is active, cooling, or unavailable. Model exclusions are
reported separately because they apply to one model rather than the complete
auth. Supported models are included only when the runtime catalog has a known
per-auth support set.
An auth without a recoverable account ID is returned with
identity_state: "unidentified", manageable: false, and no account_id.
It remains available for compatible proxy traffic but cannot receive
account-targeted mutations.
The response never includes OAuth tokens, raw auth JSON, source paths, raw session identifiers, or raw upstream error bodies.
Forces the OAuth refresh flow for one identified auth:
{"account_id":"acct_xxx"}The action is allowed while the auth is disabled and always enters the per-auth refresh singleflight. The OAuth operation has a 30-second overall deadline. Success clears credential-related failures, but it does not enable the auth, clear quota/model cooldowns, or change session bindings.
Response:
{"auth": { "...": "updated safe auth status" }}Token endpoint and persistence failures return sanitized errors without token or upstream body content.
Request:
{"account_id":"acct_xxx"}These actions update disabled in every auth file for the account through the
same synchronized temporary-file and atomic-rename path used by refresh.
Every source is parsed and validated before writes begin. A failed multi-file
write rolls back already changed sources when possible and never returns a
successful mixed-state response.
Enable performs no OAuth request and does not clear health, cooldown, model exclusions, active requests, or session bindings. Disable excludes the auth from new selection after the action completes. Existing HTTP, SSE, and WebSocket requests continue, and idle bindings remain until later reuse, expiry, explicit clearing, or auth removal.
Request:
{"account_id":"acct_xxx"}Response:
{
"cleared": true,
"auth": { "...": "updated safe auth status" }
}Only time-based quota/429, network, 408, and retryable 5xx cooldowns can
be cleared. The action does not clear disabled state, invalid_grant, missing
or invalid refresh credentials, continued unauthorized state, or
model-specific exclusions. In those cases cleared is false.
The request must select exactly one scope.
One raw session key for one user:
{
"user_id": "usr_xxx",
"session_key": "raw-session-key"
}All bindings for one user:
{"user_id":"usr_xxx"}User scope requires session_key to be omitted. A present empty, whitespace,
or otherwise invalid session_key returns 400 and does not clear bindings.
All bindings targeting one identified auth:
{"account_id":"acct_xxx"}Response:
{"deleted_count":2}For exact-session clearing, the server computes all supported tenant-scoped session digests from the raw key and deletes the complete alias group. The raw key is never returned, logged, or persisted. There is no unconditional global clear and no action that migrates a session to a selected auth.
Creates a user and initial API key.
Request:
{
"name": "alice",
"enabled": true
}enabled is optional and defaults to true.
Response status: 201 Created
{
"user": {
"id": "usr_xxx",
"name": "alice",
"enabled": true,
"created_at": "2026-07-30T00:00:00Z",
"updated_at": "2026-07-30T00:00:00Z"
},
"api_key": {
"id": "key_xxx",
"user_id": "usr_xxx",
"key_prefix": "cop_...",
"masked_key": "cop_...abcd",
"enabled": true,
"created_at": "2026-07-30T00:00:00Z"
},
"api_key_value": "cop_plaintext_returned_once"
}Possible errors:
400for an empty or invalid name.409for a case-insensitive duplicate name.
Lists users and their active key metadata.
Optional query:
enabled=true
enabled=false
Response:
{
"users": [
{
"user": {
"id": "usr_xxx",
"name": "alice",
"enabled": true,
"created_at": "2026-07-30T00:00:00Z",
"updated_at": "2026-07-30T00:00:00Z"
},
"api_key": {
"id": "key_xxx",
"user_id": "usr_xxx",
"key_prefix": "cop_...",
"masked_key": "cop_...abcd",
"enabled": true,
"created_at": "2026-07-30T00:00:00Z"
}
}
]
}Returns one user and active key metadata. Returns 404 when the user does not
exist.
Updates either or both fields:
{
"name": "alice2",
"enabled": false
}Returns the updated user and active key metadata.
Disables the previous active key and returns one new key.
Response status: 200 OK
The response shape matches user creation and includes api_key_value once.
Returns rolling usage snapshots.
Optional queries:
user_id=usr_xxx
api_key_id=key_xxx
Response:
{
"usage": [
{
"user_id": "usr_xxx",
"name": "alice",
"api_key_id": "key_xxx",
"masked_key": "cop_...abcd",
"windows": {
"5h": {
"request_count": 1,
"total_tokens": 100
},
"7d": {
"request_count": 2,
"total_tokens": 200
}
},
"models": []
}
]
}Counter objects can also contain:
failed_request_countinput_tokensoutput_tokensreasoning_tokenscached_input_tokenscache_read_tokenscache_creation_tokens
Query parameters:
| Parameter | Default | Values |
|---|---|---|
window |
7d |
5h, 24h, 7d, 30d, today |
step |
auto |
10m, 30m, 1h, 6h, 1d |
group_by |
user |
Repeat or comma-separate user, api_key, model, reasoning_effort, service_tier |
fill |
none | none, zero |
user_id |
empty | One user ID |
api_key_id |
empty | One API key ID |
Response:
{
"window": "7d",
"step": "1h",
"start": "2026-07-23T00:00:00Z",
"end": "2026-07-30T00:10:00Z",
"group_by": ["user"],
"series": [
{
"bucket_start": "2026-07-30T00:00:00Z",
"user_id": "usr_xxx",
"name": "alice",
"request_count": 1,
"total_tokens": 100
}
]
}Invalid window, step, grouping, fill, or filter combinations return 400.
These routes require the managed user key whose data is being accessed.
Returns the authenticated user and current key metadata. Plaintext is never returned.
Rotates the authenticated user's key and returns api_key_value once. The key
used for the reset request is disabled immediately.
Returns totals from 00:00:00 UTC through the current usage bucket:
{
"user_id": "usr_xxx",
"api_key_id": "key_xxx",
"date": "2026-07-30",
"request_count": 2,
"input_tokens": 100,
"output_tokens": 50,
"total_tokens": 150,
"models": []
}Without client_version, returns an OpenAI-style view of the synchronized
catalog for the version derived from codex-user-agent:
{
"object": "list",
"data": [
{
"id": "gpt-5.4",
"object": "model",
"owned_by": "openai"
}
]
}When the client_version query key is present, the response uses the Codex CLI
catalog shape for that exact normalized version. An empty value uses the same
configured User-Agent version:
{"models":[]}On a cold or expired version, the proxy concurrently fetches the authenticated upstream catalog for every active logical credential, waits for all results, and returns the deterministic union of model slugs. One complete per-auth object is selected for duplicate slugs; fields are not merged across accounts.
Successful per-auth snapshots have a three-hour TTL. Failed refreshes reuse
stale snapshots and schedule one deduplicated background task with three
additional retries. One failed auth does not block successful auths. If no auth
has a usable snapshot, the response falls back to
internal/codexonly/codex_client_models.json.
Versions are limited to 64 safe ASCII bytes, and invalid values return 400.
At most 16 normalized versions are retained in memory with LRU eviction. Fast
tier metadata is removed unless allow-fast-mode is enabled.
This endpoint translates Chat Completions input to an upstream Responses request. The proxy always requests an upstream stream, then either aggregates it or converts it back to Chat Completions SSE.
Required fields:
modelmessages
Supported behavior includes:
system,developer,user,assistant, andtoolmessages.- Text and
image_urlmessage content. - Function tools, tool choice, parallel tool calls, tool-call history, and tool outputs.
streamandstream_options.include_usage.response_formatwithjson_objectorjson_schema.reasoning,reasoning_effort, andextra_body.reasoning.verbosity.stopas a string or list.service_tier.
Reasoning effort aliases are normalized:
| Input | Upstream value |
|---|---|
minimal |
low |
max |
xhigh |
Sampling and other unknown Chat Completions fields are not automatically
forwarded. Clients that require full Responses behavior should call
/v1/responses directly.
The converter requires exactly one successful upstream terminal event.
response.completed is a complete success. response.incomplete is a partial
success with these standard Chat Completions finish reasons:
| Incomplete reason | finish_reason |
|---|---|
max_tokens, max_output_tokens |
length |
content_filter |
content_filter |
| unknown | length |
An incomplete reason takes precedence over tool_calls. The response does not
include a non-standard native_finish_reason field.
For streaming requests, the proxy waits for the first valid upstream event
before committing the downstream SSE response. A failure before commitment is
classified by the same OAuth refresh, health cooldown, model failover, and
request-error rules as an HTTP failure. A failure after commitment emits one
sanitized OpenAI error envelope as a data: event, closes the stream, and does
not emit a finish chunk or [DONE].
EOF before a successful terminal, malformed or oversized events, duplicate
terminals, and data after a terminal are failures. One upstream SSE event is
limited to 50 MB. response.output_item.done snapshots are reconciled by output
index with terminal output; non-stream terminal output is authoritative, while
streaming can emit only a missing suffix. Local stop matching is UTF-8 safe
across event boundaries and suppresses later text and tool deltas while the
proxy continues draining terminal state and usage.
Example:
curl http://127.0.0.1:8317/v1/chat/completions \
-H 'Authorization: Bearer cop_...' \
-H 'Content-Type: application/json' \
-d '{
"model": "gpt-5.4",
"messages": [
{"role": "user", "content": "Hello"}
],
"stream": true,
"stream_options": {"include_usage": true}
}'This managed-key-only endpoint accepts the Completion request shape emitted by
Zed's open_ai_compatible_api edit prediction provider. It is a dedicated
compatibility contract, not a generic /v1/completions endpoint.
The request must use Content-Type: application/json and include:
-
model: a non-empty model identifier of at most 128 bytes. Surrounding ASCII spaces are removed, and the remaining value may contain letters, digits,.,-,_,/,:, and@. -
prompt: exactly one ordered Qwen FIM sequence:<|fim_prefix|>{prefix}<|fim_suffix|>{suffix}<|fim_middle|>
The prompt is rejected before upstream contact when a marker is missing, duplicated, out of order, preceded by other text, or followed by other text.
Optional fields:
max_tokens: an integer from 1 through 4096; default256. It is enforced locally as a Unicode code-point output cap and is not forwarded upstream.temperature: a JSON number accepted for Zed wire compatibility but not forwarded upstream.stop: one non-empty string or a list of non-empty strings. Matching is applied locally and is Unicode-safe.
The proxy extracts the prefix and suffix, sends a tool-free low-reasoning
Responses request with stream: true and store: false, and fully validates
and aggregates the upstream SSE before returning:
{
"id": "cmpl_...",
"object": "text_completion",
"created": 0,
"model": "gpt-5.6-luna",
"choices": [
{
"index": 0,
"text": "missing code",
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0
}
}The example uses gpt-5.6-luna, but the endpoint does not maintain its own
model allowlist. Runtime catalog capability filtering, credential selection and
failover, and upstream failure classification determine whether the normalized
requested model is available.
response.completed returns finish_reason: "stop" unless a local output
budget truncates first. response.incomplete maps max-token reasons to
length, content filtering to content_filter, and unknown reasons to
length. A local budget truncation also uses length; a local stop match uses
stop.
Stop or budget matches do not cancel the upstream read. The proxy continues draining the terminal event and usage first. EOF before a terminal, malformed or oversized events, duplicate terminals, data after a terminal, tool output, and upstream failure events return deterministic safe errors. Client cancellation stops the upstream request without retrying or synthesizing a new response body.
The proxy forwards these whitelisted Codex routes without defining their full upstream request schema:
These native Responses HTTP and WebSocket routes remain transparent; the Chat Completions terminal validation and error conversion described above do not modify their event or frame payloads.
| Common method | Public path |
|---|---|
POST, GET, WebSocket upgrade |
/v1/responses |
POST |
/v1/responses/compact |
POST |
/v1/alpha/search |
POST |
/v1/images/generations |
POST |
/v1/images/edits |
POST |
/v1/memories/trace_summarize |
POST |
/v1/realtime/calls |
GET, WebSocket upgrade |
/v1/realtime |
Image generation is normally available through the Responses API image tool.
The two /v1/images/* paths are narrow compatibility routes; other image paths
are not proxied.
When Fast mode is disabled, requests containing service_tier: "fast" or
"priority" are rejected before upstream forwarding.
The following paths exist only to support known Codex CLI behavior and are not a stable third-party API contract:
/backend-api/codexaliases of the whitelisted/v1Codex routes.POST /filesPOST /files/{file_id}/uploaded/backend-api/wham/usage/backend-api/wham/profiles/me/backend-api/wham/accounts/check/backend-api/wham/accounts/send_add_credits_nudge_email/backend-api/wham/apps/backend-api/ps/mcp
The proxy does not expose arbitrary /backend-api/* paths. General clients
should use the supported /v1/* and /v0/* surfaces.