Codex Backend (App-Server) — Responses, Sticky Routing, Server-Side Compact
The codex backend protocol of the embedded app-server — official endpoints, encrypted reasoning, quota snapshot, and the Rust HTTP surface
Codex Backend (App-Server) — Responses, Sticky Routing, Server-Side Compact
Part of openai-research. Primary sources: the embedded Resources/codex binary (via real logs) + codex-rs open source (~/workdir/codex, codex-rs/core/src/client.rs).
This is the backend of the embedded app-server (https://chatgpt.com/backend-api/codex), consumed by the WORK SUBMODE of the ChatGPT mode (via handoff → local Codex thread) and by the CODEX MODE. The backend name is shared; the surfaces are distinct.
Official endpoints (client.rs)
| Endpoint | Role |
|---|---|
POST /responses | Generation (streaming SSE) |
POST /responses/compact | Server-side compaction — unary, receives the whole conversation (ApiCompactionInput with prompt_cache_key), returns the compacted ResponseItems |
POST /realtime/calls | Voice — WebRTC SDP offer via REST (Realtime API) |
POST /memories/trace_summarize | Memories |
GET /models?client_version= | Catalog with plan gating |
Protocol headers (codex-rs)
Authorization: Bearer <token>
chatgpt-account-id: <id>
originator: codex_cli_rs (CLI) | Codex Desktop (app)
session-id: <uuid> — per session
thread-id: <uuid> — per thread
x-codex-installation-id: <id>
OAI-Language: en
OpenAI-Beta: responses_websockets=2026-02-06 (alternate WebSocket transport)
x-oai-attestation: … (OPTIONAL — attestation provider when configured)
Generation payload
{
"model": "gpt-5.6-sol",
"instructions": "",
"input": [{"role": "user", "content": [{"type": "input_text", "text": "..."}]}],
"store": false,
"stream": true,
"include": ["reasoning.encrypted_content"],
"reasoning": {"effort": "low|medium|high|xhigh"},
"prompt_cache_key": "<session-stable key>"
}
Multi-turn: encrypted reasoning + prompt_cache_key
include: ["reasoning.encrypted_content"]always — theReasoningitems return with encrypted content- Multi-turn = re-send the encrypted Reasoning items in
input(server-validated stateless CoT) withstore: false - The stable
prompt_cache_keykeeps the server KV cache aligned prepare_response_items_for_request: non-prefixed ids are removed before send- Effort scale (
protocol/src/openai_models.rs):None, Minimal, Low, Medium (default), High, XHigh, Max— plusultrain the desktop picker
Sticky routing (x-codex-turn-state)
- Emitted at turn start as a response header (Fernet token
gAAAAAB…) - Re-sent as a request header in all requests of the same turn — pins the turn to a specific backend
- New turn → new token
NOTE (2026-08-26 runtime): on the desktop app this header appears 0 times (Codex mode + work submode traces) — the desktop uses the responses_websocket session instead; the sticky turn-state was observed on the self-contained API probe (2026-08-24), not the desktop.
Quota (RateLimitSnapshot — protocol.rs:2174)
RateLimitSnapshot {
primary: Option<RateLimitWindow>, // main window
secondary: Option<RateLimitWindow>, // secondary window — Option: absent/0 when disabled
credits: Option<CreditsSnapshot>,
individual_limit, spend_control_reached, plan_type, rate_limit_reached_type,
}
RateLimitWindow { used_percent, window_minutes: Option<i64>, reset_at }
Parsed from the x-codex-primary-* / x-codex-secondary-* headers (used-percent, reset-at, window-minutes) + x-codex-plan-type, x-codex-credits-*, x-codex-active-limit, x-codex-safety-buffering-*.
Observed on Plus (2026-08-24): primary = 10080 min (7 days) at 1% used; secondary = 0 (disabled) — the app UI filters windows with windowDurationMins > 0, so only the weekly limit shows. x-codex-safety-buffering-enabled: true with faster-model: gpt-5.6-luna — automatic degradation when quota tightens.
Reset credits: /wham/rate-limit-reset-credits[(/consume)].
Catalog (GET /models)
Real catalog of 2026-08-24 (desktop cache ~/.codex/models_cache.json, client 0.149.0 — the client_version tracks the app, not fixed):
| Slug | Default | Efforts | Lite | Notes |
|---|---|---|---|---|
gpt-5.6-sol | low | low, medium, high, xhigh, max, ultra | ✓ | frontier agentic coding |
gpt-5.6-terra | medium | low…ultra | ✓ | balanced |
gpt-5.6-luna | medium | low…max | ✓ | fast/affordable (safety-buffering target) |
gpt-5.5 | medium | low…xhigh | ✗ | |
gpt-5.4 | medium | low…xhigh | ✗ | |
gpt-5.4-mini | medium | low…xhigh | ✓ | |
codex-auto-review | medium | low…max | ✓ | visibility=hide (internal; isPublicCodexModel excludes it) |
The payload shape varies per client — the app cache carries visibility/supported_reasoning_levels/description; a call with client_version= returned available_in_plans. supported_in_api=true for all. The client spies on the npm registry (@openai/codex/latest) to track minimal_client_version.
HTTP calls of the embedded app-server (real desktop logs, logs_2.sqlite)
The Rust binary calls the backend directly (besides the Electron main), carrying x-oai-attestation and the codex headers in its own codex_http_client (X-OAI-IS: not observed in the executed flows — A/A1/manual (0 occurrences on the captured main headers); flow B: unknown (not executed); rust request headers never captured):
api.path | Role | Observed cadence |
|---|---|---|
models | Catalog | online polling every ~3min |
responses | Generation | per turn |
images/generations | Image generation via the codex backend | per request |
ps/plugins/list | Plugins | on demand |
alpha/search | Standalone web search — webrun tool (code mode) POST {model, commands: {search_query: [{q}]}, settings: {allowed_callers: [direct], external_web_access}} | on demand |
Everything else of the chat submode flow (f/*, sentinel/*, wham/*, /transcribe) goes through the Electron main. Two coordinated HTTP clients (Rust + main); NOTE (2026-08-27): X-OAI-IS was never observed in the executed flows (A/A1/manual; 0 occurrences in the captured main headers); flow B: unknown (not executed); rust headers not captured — treat the web-flow claim as unverified (flow A scope), not as a law of the app; the app-server exposes auth to the main via stdio JSON-RPC (getAuthStatus, getAuthToken).
Server-side compact (the cache differentiator)
The Codex does not compact locally: POST /responses/compact receives {model, input, instructions, tools, parallel_tool_calls, reasoning, service_tier, prompt_cache_key, text} and returns the compacted conversation as ResponseItems. Because the prompt_cache_key persists, the server KV cache survives compaction — no cache miss. This is the opposite of client-side compaction strategies that miss.
TypeScript types (the backend surface)
// ── protocol headers ─────────────────────────────────────────────────────
export interface CodexRequestHeaders {
Authorization: `Bearer ${string}`
'chatgpt-account-id': string
originator: 'codex_cli_rs' | 'Codex Desktop'
'session-id': string
'thread-id': string
'x-codex-installation-id': string // the resolve_installation_id uuid
'OAI-Language': string
'OpenAI-Beta'?: 'responses_websockets=2026-02-06'
'x-oai-attestation'?: string // AttestationProvider header (desktop supplies the DeviceCheck impl)
}
// ── generation payloads ──────────────────────────────────────────────────
export type ReasoningEffort = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'ultra'
export type ReasoningEffortOverride = Partial<Record<ReasoningEffort, boolean>> // catalog shape
export interface GenerationPayload {
model: string // 'gpt-5.6-sol' / 'gpt-5.6-luna' / 'gpt-5.5' …
instructions?: string
input: Array<{ role: 'user' | 'developer' | 'assistant' | 'tool'; content: Array<{ type: 'input_text' } & { text: string }> }>
store: false
stream: true
include: ['reasoning.encrypted_content'] // always
reasoning?: { effort: ReasoningEffort }
prompt_cache_key: string // session-stable — survives compaction
}
export interface ResponseItem {
id: string // 'rs_…' reasoning id; non-prefixed ids removed before send
type: 'reasoning' | 'message' | 'web_search_call' | 'function_call' | 'custom_tool_call' | string
encrypted_content?: string // 'gAAAA…' — stateless CoT (resent verbatim in multi-turn)
summary?: Array<{ type: 'summary_text'; text: string }>
[key: string]: unknown
}
// ── quota snapshot (mirrors RateLimitSnapshot; headers x-codex-*) ─────────
export interface RateLimitWindow {
used_percent: number
window_minutes: number | null // null/0 = disabled
reset_at: number
}
export interface CreditsSnapshot {
used_percent?: number
remaining_credits?: number
[k: string]: unknown
}
export interface RateLimitSnapshot {
primary: RateLimitWindow | null
secondary: RateLimitWindow | null // observed: disabled (0)
credits: CreditsSnapshot | null
individual_limit: unknown | null
spend_control_reached: boolean
plan_type: 'plus' | 'pro' | 'free' | string
rate_limit_reached_type: string | null
}
export interface RlHeaders { // parsed from: x-codex-{primary,secondary,credits,active-limit,safety-buffering}-*
[header: string]: string | undefined
}
export type RlSource = { snapshot: RateLimitSnapshot; headers: RlHeaders }
// ── catalog ──────────────────────────────────────────────────────────────
export interface CatalogModel {
slug: string
is_public_codex_model?: boolean
supported_reasoning_levels?: ReasoningEffort[]
default_reasoning_level?: ReasoningEffort
visibility?: 'hide' | string // 'codex-auto-review' is hidden
use_responses_lite?: boolean
available_in_plans?: string[] // from client_version calls
description?: string
}
// ── standalone web search (webrun tool / alpha.search) ───────────────────
export interface AlphaSearchBody {
model: string
commands: { search_query: Array<{ q: string }> }
settings: { allowed_callers: ['direct']; external_web_access: boolean | 'indexed' }
}
export type WebSearchMode = 'live' | 'indexed' | 'cached' | 'disabled' // per-turn config
// ── compaction (server-side) ─────────────────────────────────────────────
export interface ApiCompactionInput {
model: string
input: ResponseItem[] & Array<{ role: string; content: unknown }>
instructions?: string
tools?: unknown[]
parallel_tool_calls?: boolean
reasoning?: { effort: ReasoningEffort }
service_tier?: string
prompt_cache_key: string
text?: string
}
Cross-references
- Work submode (handoff web → local codex): work-submode-protocol
- Quota in the UI: chat-submode-protocol
- Probes: probes
- Flow map: flow-definitions