Files
codex-proxy-rs/docs/api.md
T
2026-06-15 08:39:28 +08:00

18 KiB

API Contract

codex-proxy-rs exposes two API families. The /admin/* path is reserved for the future human Web console; machine-readable management endpoints live under /api/admin/*.

/v1/*

These endpoints are OpenAI-compatible and are authenticated only by enabled local client API keys created through /api/admin/api-keys and sent as Authorization: Bearer cpr_.... Responses include an X-Request-Id header for tracing, but the body stays OpenAI-compatible and does not include a custom requestId field.

Error body:

{
  "error": {
    "message": "Invalid client API key",
    "type": "invalid_request_error",
    "param": null,
    "code": "invalid_api_key"
  }
}

Streaming errors after SSE has started use:

event: error
data: {"error":{"message":"Upstream failed","type":"server_error","param":null,"code":"upstream_error"}}

Model Routes

GET /v1/models returns the OpenAI-compatible model list. The default static Codex model is gpt-5.5.

GET /v1/models/catalog returns Codex model metadata for UI use. Aliases are used for request parsing but are not exposed as standalone models.

GET /v1/models/{id} returns an OpenAI-compatible model object or a 404 OpenAI error body with code=model_not_found.

GET /v1/models/{id}/info returns the extended Codex catalog entry for a known model.

Model name parsing supports configured aliases plus -low, -medium, -high, -xhigh, -fast, and -flex suffixes.

GET /debug/fingerprint

Returns the local runtime Codex Desktop fingerprint summary. The route is local-only and rejects forwarded remote requests using x-forwarded-for or x-real-ip.

Success response:

{
  "source": "staticDefault",
  "originator": "Codex Desktop",
  "appVersion": "26.519.81530",
  "buildNumber": "3178",
  "platform": "darwin",
  "arch": "arm64",
  "chromiumVersion": "146",
  "userAgent": "Codex/26.519.81530 (darwin; arm64) Chromium/146"
}

GET /debug/upstream

Runs a local-only Codex backend connectivity probe against the Codex models endpoint using the runtime TLS and Desktop fingerprint headers. The route rejects forwarded remote requests using x-forwarded-for or x-real-ip, returns only status metadata, and does not expose account tokens or upstream response bodies.

Success response for a reachable backend that rejects the synthetic diagnostic token:

{
  "target": "codexModels",
  "status": "reachable",
  "backendBaseUrl": "https://chatgpt.com/backend-api",
  "endpoint": "https://chatgpt.com/backend-api/codex/models?client_version=26.519.81530",
  "reachable": true,
  "statusCode": 401,
  "authorization": "rejected",
  "durationMs": 23,
  "error": null
}

GET /debug/diagnostics

Returns a local-only runtime diagnostics summary. The route is intended for production-local troubleshooting and rejects forwarded remote requests using x-forwarded-for or x-real-ip.

Success response:

{
  "status": "ok",
  "runtime": {
    "packageName": "codex-proxy-rs",
    "packageVersion": "0.1.0"
  },
  "paths": {
    "config": "config.yaml",
    "localConfig": "local.yaml",
    "databaseUrl": "sqlite://data/codex-proxy.sqlite",
    "logsDirectory": "logs",
    "masterKeyFile": "data/master.key",
    "apiKeyPepperFile": "data/api-key-pepper.key"
  },
  "transport": {
    "backendBaseUrl": "https://chatgpt.com/backend-api",
    "tls": {
      "forceHttp11": false
    },
    "fingerprint": {
      "source": "staticDefault",
      "originator": "Codex Desktop",
      "appVersion": "26.519.81530",
      "buildNumber": "3178",
      "platform": "darwin",
      "arch": "arm64",
      "chromiumVersion": "146",
      "userAgent": "Codex/26.519.81530 (darwin; arm64) Chromium/146"
    }
  },
  "accounts": {
    "repositoryAvailable": true,
    "authenticatedState": true,
    "pool": {
      "total": 1,
      "active": 1,
      "expired": 0,
      "quotaExhausted": 0,
      "refreshing": 0,
      "disabled": 0,
      "banned": 0
    },
    "capacity": {
      "maxConcurrentPerAccount": 3,
      "totalSlots": 3,
      "usedSlots": 0,
      "availableSlots": 3
    }
  },
  "settings": {
    "defaultModel": "gpt-5.5",
    "refreshEnabled": true,
    "rotationStrategy": "least_used",
    "quotaSkipExhausted": true,
    "logsEnabled": false
  }
}

POST /v1/responses

Uses imported Codex accounts to call POST /codex/responses on the configured Codex backend. The upstream request is sent with Codex Desktop headers, account bearer token, optional account id, request id, and encrypted account-scoped Cookie replay.

When the client omits stream or sets "stream": false, the Rust route collects upstream HTTP SSE until response.completed and returns the completed OpenAI-compatible response JSON. If upstream sends assistant output through response.output_item.done or response.output_text.delta events while the completed payload has an empty output, the route reconstructs a standard assistant output message and synchronizes output_text.

When the client sets "stream": true, the Rust route returns text/event-stream and passes through upstream SSE frames while collecting usage for the account after the stream finishes. Both modes capture upstream Set-Cookie, replay encrypted account-scoped Cookies, record account usage when usage appears in SSE, and write a v1.response event log with requestId, accountId, route, model, statusCode, latencyMs, and non-secret metadata.

For non-streaming clients, upstream HTTP SSE event: error and event: response.failed terminal frames return a 502 OpenAI-compatible upstream_error JSON body instead of an empty-response error. For streaming clients, those terminal frames remain passthrough SSE output and the lifecycle log is marked as a failed upstream SSE response.

If Codex returns 401 and the account has a stored refresh token, the route refreshes the account through the OpenAI OAuth token endpoint once, persists the rotated access token, preserves the old refresh token when the server omits a new one, updates the in-process account pool, and retries the same request once. Refresh failures are recorded as non-secret account.refresh event logs with the request id, account id, model, trigger, failure, and resulting accountStatus.

Responses use the Codex WebSocket upstream by default while keeping the client-facing response as SSE. previous_response_id always uses WebSocket so upstream conversation history is preserved. The local extension field use_websocket: false forces HTTP SSE for requests that do not require upstream history; the removed camelCase useWebSocket alias is ignored and no longer toggles transport.

/api/admin/*

Admin endpoints are authenticated only by HttpOnly admin session cookies. Admin JSON uses lower camelCase field names. Every admin response includes an X-Request-Id header, and every JSON body includes requestId.

Use real HTTP status codes and body-level frontend codes together. Do not return HTTP 200 for failed requests. The body code exists for frontend branching; the HTTP status remains the transport truth.

GET /api/admin/diagnostics

Returns the same non-secret runtime diagnostics summary as /debug/diagnostics, wrapped in the admin envelope and gated by the admin session cookie.

Success response:

{
  "code": 200,
  "message": "OK",
  "data": {
    "status": "ok",
    "runtime": {
      "packageName": "codex-proxy-rs",
      "packageVersion": "0.1.0"
    },
    "transport": {
      "backendBaseUrl": "https://chatgpt.com/backend-api"
    },
    "accounts": {
      "authenticatedState": true
    }
  },
  "requestId": "req_01"
}

POST /api/admin/login

Authenticates only with the configured admin password stored in admin_users. Client API keys (Bearer cpr_...) are ignored by the admin login flow and cannot create admin sessions.

Request:

{
  "password": "admin-password"
}

Success response:

{
  "code": 200,
  "message": "OK",
  "data": {
    "expiresAt": "2026-06-11T12:00:00Z"
  },
  "requestId": "req_01"
}

Success also returns:

Set-Cookie: cpr_admin_session=...; Path=/; HttpOnly; SameSite=Lax; Max-Age=3600

Invalid password response uses HTTP 401 and body code 40102.

GET /api/admin/settings

Returns the in-scope runtime settings visible to the admin UI. This endpoint uses admin sessions, not client API keys.

Success response:

{
  "code": 200,
  "message": "OK",
  "data": {
    "defaultModel": "gpt-5.5",
    "defaultReasoningEffort": null,
    "serviceTier": null,
    "modelAliases": {},
    "refreshEnabled": true,
    "refreshMarginSeconds": 300,
    "refreshConcurrency": 2,
    "maxConcurrentPerAccount": 3,
    "requestIntervalMs": 50,
    "rotationStrategy": "least_used",
    "tierPriority": [],
    "quotaRefreshIntervalMinutes": 5,
    "quotaWarningThresholds": {
      "primary": [80, 90],
      "secondary": [80, 90]
    },
    "quotaSkipExhausted": true,
    "logsEnabled": false,
    "logsCapacity": 2000,
    "logsCaptureBody": false,
    "usageHistoryRetentionDays": null
  },
  "requestId": "req_01"
}

PATCH /api/admin/settings

Updates retained settings and writes them to root local.yaml as a config overlay. The route requires the admin session cookie. Client API keys and removed TypeScript-era fields are not accepted.

Request:

{
  "defaultModel": "gpt-6",
  "defaultReasoningEffort": "medium",
  "serviceTier": "priority",
  "modelAliases": {
    "fast": "gpt-6-fast"
  },
  "refreshEnabled": false,
  "refreshMarginSeconds": 180,
  "refreshConcurrency": 3,
  "maxConcurrentPerAccount": 5,
  "requestIntervalMs": 125,
  "rotationStrategy": "round_robin",
  "tierPriority": ["pro", "plus"],
  "quotaRefreshIntervalMinutes": 15,
  "quotaWarningThresholds": {
    "primary": [75, 90],
    "secondary": [65, 95]
  },
  "quotaSkipExhausted": false,
  "logsEnabled": false,
  "logsCapacity": 3000,
  "logsCaptureBody": true,
  "usageHistoryRetentionDays": 60
}

Success response uses the same data shape as GET /api/admin/settings.

Validation:

  • rotationStrategy must be least_used, round_robin, or sticky.
  • Count/capacity/retention fields that represent concurrency, capacity, intervals, or retention must be greater than zero where zero would disable required service behavior.
  • Quota warning thresholds must be between 0 and 100.
  • Unknown settings fields return HTTP 400 with body code 40001.

GET /api/admin/logs

Returns cursor-paginated event logs for admin troubleshooting. The list uses the standard admin page envelope and lower camelCase fields.

Supported query filters:

  • kind
  • level
  • requestId
  • accountId
  • route
  • model
  • statusCode
  • search across message and metadata text

Example item:

{
  "id": "log_01",
  "requestId": "req_01",
  "kind": "v1.response",
  "level": "info",
  "accountId": "acct_01",
  "route": "/v1/responses",
  "model": "gpt-5.5",
  "statusCode": 200,
  "latencyMs": 123,
  "message": "v1 responses 已完成",
  "metadata": {
    "stream": false,
    "usage": {
      "inputTokens": 7,
      "outputTokens": 4,
      "cachedTokens": 2,
      "totalTokens": 11
    }
  },
  "createdAt": "2026-06-11T12:00:00Z"
}

GET /api/admin/logs/state

Returns the current retained logging configuration plus the number of stored SQLite event logs.

Success response:

{
  "code": 200,
  "message": "OK",
  "data": {
    "enabled": false,
    "capacity": 2000,
    "captureBody": false,
    "storedCount": 42
  },
  "requestId": "req_01"
}

PATCH /api/admin/logs/state

Updates the in-process logging state used by admin log controls. This is runtime-only; persistent configuration changes still go through PATCH /api/admin/settings.

Request:

{
  "enabled": true,
  "capacity": 2000,
  "captureBody": false
}

Success response uses the same data shape as GET /api/admin/logs/state.

GET /api/admin/logs/{id}

Returns a single stored event log by id using the same event shape as list items. Missing ids return HTTP 404 with body code 40401.

DELETE /api/admin/logs

Clears stored SQLite event logs.

Success response:

{
  "code": 200,
  "message": "OK",
  "data": {
    "cleared": 42
  },
  "requestId": "req_01"
}

GET /api/admin/usage-stats

Returns cursor-paginated account usage counters recorded after /v1/* calls. The endpoint never returns account access tokens, refresh tokens, Cookie values, or client API keys.

Example item:

{
  "accountId": "acct_01",
  "email": "user@example.com",
  "label": "primary",
  "planType": "plus",
  "requestCount": 2,
  "inputTokens": 14,
  "outputTokens": 8,
  "cachedTokens": 3,
  "lastUsedAt": "2026-06-11T12:00:00Z"
}

GET /api/admin/usage-stats/summary

Returns global usage totals across accounts.

Success response:

{
  "code": 200,
  "message": "OK",
  "data": {
    "accountCount": 1,
    "requestCount": 2,
    "inputTokens": 14,
    "outputTokens": 8,
    "cachedTokens": 3
  },
  "requestId": "req_01"
}

GET /api/admin/api-keys

Returns local client API keys with cursor pagination. The plaintext key and hash are never returned by list endpoints.

POST /api/admin/api-keys

Creates a local client API key for /v1/*. The plaintext value is returned only in this response; store it client-side and use it as Authorization: Bearer <plaintext>.

Request:

{
  "name": "cursor"
}

Success response:

{
  "code": 200,
  "message": "OK",
  "data": {
    "id": "key_01",
    "name": "cursor",
    "prefix": "cpr_xxxxxxxx",
    "enabled": true,
    "createdAt": "2026-06-11T12:00:00Z",
    "lastUsedAt": null,
    "plaintext": "cpr_full_key_returned_once"
  },
  "requestId": "req_01"
}

GET /api/admin/api-keys/export

Exports Rust-local client API key metadata for backup or migration. The export never includes plaintext keys, key hashes, or the raw auth pepper. Because cpr_ keys are stored only as peppered HMAC hashes, imports rotate keys instead of preserving the original plaintext.

Optional query:

GET /api/admin/api-keys/export?ids=key_01,key_02

Success response:

{
  "code": 200,
  "message": "OK",
  "data": {
    "sourceFormat": "rustLocalClientApiKeys",
    "rotationRequired": true,
    "apiKeys": [
      {
        "id": "key_01",
        "name": "cursor",
        "label": "automation",
        "prefix": "cpr_xxxxxxxx",
        "enabled": true,
        "createdAt": "2026-06-11T12:00:00Z",
        "lastUsedAt": null
      }
    ]
  },
  "requestId": "req_01"
}

POST /api/admin/api-keys/import

Imports Rust-local client API key metadata exported by this service. Import creates new local cpr_ keys using the target instance's pepper and returns the new plaintext values only in this response. Clients must replace the old keys with these rotated plaintext values.

Request:

{
  "sourceFormat": "rustLocalClientApiKeys",
  "rotationRequired": true,
  "apiKeys": [
    {
      "id": "key_01",
      "name": "cursor",
      "label": "automation",
      "prefix": "cpr_xxxxxxxx",
      "enabled": true
    }
  ]
}

Success response:

{
  "code": 200,
  "message": "OK",
  "data": {
    "imported": 1,
    "skipped": 0,
    "rotated": true,
    "keys": [
      {
        "sourceId": "key_01",
        "sourcePrefix": "cpr_xxxxxxxx",
        "id": "key_02",
        "name": "cursor",
        "label": "automation",
        "prefix": "cpr_yyyyyyyy",
        "enabled": true,
        "createdAt": "2026-06-11T12:05:00Z",
        "lastUsedAt": null,
        "plaintext": "cpr_new_full_key_returned_once"
      }
    ]
  },
  "requestId": "req_01"
}

GET /api/admin/accounts

Returns stored Codex accounts with cursor pagination. Access tokens and refresh tokens are never decrypted or returned.

GET /api/admin/accounts/quota-warnings

Returns active quota warnings computed from cached account quota snapshots in SQLite. The route requires an admin session cookie and does not call the upstream Codex backend.

Success response:

{
  "code": 200,
  "message": "OK",
  "data": {
    "warnings": [
      {
        "accountId": "acct_01",
        "email": "user@example.com",
        "window": "primary",
        "level": "warning",
        "usedPercent": 85.0,
        "resetAt": 1770000100
      }
    ],
    "updatedAt": "2026-06-13T00:00:00Z"
  },
  "requestId": "req_01"
}

POST /api/admin/accounts/import

Imports accounts into encrypted SQLite storage. The request body uses the native Rust import format. The only supported shape is an object with accounts; unknown container or account fields are rejected by producing no importable accounts.

Request:

{
  "accounts": [
    {
      "id": "acct_01",
      "email": "user@example.com",
      "accountId": "chatgpt-account",
      "userId": "chatgpt-user",
      "label": "primary",
      "planType": "plus",
      "token": "access-token",
      "refreshToken": "refresh-token",
      "status": "active"
    }
  ]
}

Success response:

{
  "code": 200,
  "message": "OK",
  "data": {
    "imported": 1,
    "skipped": 0,
    "sourceFormat": "native"
  },
  "requestId": "req_01"
}

GET /api/admin/accounts/export

Exports native Rust account backups. The default format is native; format=native is the only explicit format accepted. Any other format returns HTTP 400.

Optional query:

GET /api/admin/accounts/export?ids=acct_01,acct_02&format=native

Success response:

{
  "code": 200,
  "message": "OK",
  "data": {
    "sourceFormat": "native",
    "accounts": [
      {
        "id": "acct_01",
        "email": "user@example.com",
        "accountId": "chatgpt-account",
        "userId": "chatgpt-user",
        "label": "primary",
        "planType": "plus",
        "token": "access-token",
        "refreshToken": "refresh-token",
        "status": "active",
        "accessTokenExpiresAt": null,
        "addedAt": "2026-06-11T12:00:00Z",
        "updatedAt": "2026-06-11T12:00:00Z"
      }
    ]
  },
  "requestId": "req_01"
}

List body:

{
  "code": 200,
  "message": "OK",
  "data": [],
  "page": {
    "limit": 50,
    "nextCursor": null
  },
  "requestId": "req_01"
}

Error body:

{
  "code": 40101,
  "message": "Admin login required",
  "data": null,
  "requestId": "req_01"
}

Pagination uses cursor ordering by (created_at desc, id desc), default limit=50, max limit=200.

Rust structs use PascalCase type names and snake_case fields internally, then expose lower camelCase through serde:

#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiEnvelope<T> {
    pub code: u32,
    pub message: String,
    pub data: T,
    pub request_id: String,
}