4.8 KiB
API Contract
codex-proxy-rs exposes two API families.
/v1/*
These endpoints are OpenAI-compatible and are authenticated only by client API keys using 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.
/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.
POST /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 /admin/settings
Returns the in-scope runtime settings visible to the admin UI. This endpoint is read-only; persistent settings mutation will be added separately and must use 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"
}
GET /admin/accounts
Returns stored Codex accounts with cursor pagination. Access tokens and refresh tokens are never decrypted or returned.
POST /admin/accounts/import
Imports accounts into encrypted SQLite storage. The request body uses the Rust import format, which accepts the exported account object fields needed by this service and ignores unrelated fields.
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
},
"requestId": "req_01"
}
Success body:
{
"code": 200,
"message": "OK",
"data": {},
"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,
}