WikifitaGitHub live67e8de5
pesquisa · openai-research/codex-flow-protocol

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

Baixar raw

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)

EndpointRole
POST /responsesGeneration (streaming SSE)
POST /responses/compactServer-side compaction — unary, receives the whole conversation (ApiCompactionInput with prompt_cache_key), returns the compacted ResponseItems
POST /realtime/callsVoice — WebRTC SDP offer via REST (Realtime API)
POST /memories/trace_summarizeMemories
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 — 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)

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):

SlugDefaultEffortsLiteNotes
gpt-5.6-sollowlow, medium, high, xhigh, max, ultrafrontier agentic coding
gpt-5.6-terramediumlow…ultrabalanced
gpt-5.6-lunamediumlow…maxfast/affordable (safety-buffering target)
gpt-5.5mediumlow…xhigh
gpt-5.4mediumlow…xhigh
gpt-5.4-minimediumlow…xhigh
codex-auto-reviewmediumlow…maxvisibility=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.pathRoleObserved cadence
modelsCatalogonline polling every ~3min
responsesGenerationper turn
images/generationsImage generation via the codex backendper request
ps/plugins/listPluginson demand
alpha/searchStandalone web searchwebrun 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