---
type: research
title: Codex Backend (App-Server) — Responses, Sticky Routing, Server-Side Compact
description: The codex backend protocol of the embedded app-server — official endpoints, encrypted reasoning, quota snapshot, and the Rust HTTP surface
tags: [openai, codex, protocol, responses-api, reverse-engineering]
timestamp: 2026-08-24
---

# Codex Backend (App-Server) — Responses, Sticky Routing, Server-Side Compact

Part of [openai-research](openai-research.md). 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 `ResponseItem`s |
| `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

```json
{
  "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 — the `Reasoning` items return with encrypted content
- Multi-turn = re-send the encrypted Reasoning items in `input` (server-validated stateless CoT) with `store: false`
- The stable `prompt_cache_key` keeps 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` — plus `ultra` in 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)

```rust
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 `ResponseItem`s. 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)

```ts
// ── 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](work-submode-protocol.md)
- Quota in the UI: [chat-submode-protocol](chat-submode-protocol.md)
- Probes: [probes](probes.md)
- Flow map: [flow-definitions](flow-definitions.md)
