Wire Protocol Reference: Claude Code, Codex, and Grok

September 3, 2026 · View on GitHub

This document records observable wire-level behavior of Claude Code, Codex, and Grok as seen by the ccxray proxy. It is not a spec — it documents what each agent actually sends on the wire, which may differ from official documentation and may change without notice.

Confidence tags (per-field):

TagMeaningAction when coding against it
contractualAppears in official API docsSafe to depend on
obs-stableConsistent across multiple versions, undocumentedDepend on with defensive fallback
obs-fragileSeen in current version only, or already changed onceGuard with try-catch, log when violated

Version baseline: Claude Code CLI 2.1.159 · Codex CLI 0.133.0-alpha.1 · ccxray 1.10.0 · 2026-06-01

Official references:


Changelog

DateAgentVersionChange
2026-09-03ccxray#611Claude launch account snapshot. ccxray claude reads <CLAUDE_CONFIG_DIR or ~/.claude>/.claude.json once at launch and appends X-Ccxray-Account: <oauthAccount.emailAddress> to ANTHROPIC_CUSTOM_HEADERS. The header is ccxray-internal (never forwarded upstream); the HTTP proxy normalizes and persists its email plus exact post-@ domain as the account logged in for that launch, not as a per-request identity. Missing or malformed config is ignored.
2026-08-31Claude CodeCC 2.1.251 (absent at 2.1.206) · #601/#604Local transcript event cost-state carries the [1m] window-variant declaration. New record type {"type":"cost-state", modelUsage: {...}} aggregates per-model usage keyed by the client-internal model id, [1m] suffix included (claude-fable-5[1m] observed) — the only per-session window-variant declaration reachable by the import path (transcript message.model stays bare; system prompt is not stored). obs-fragile: positive-only — bare/empty modelUsage keys occur on sessions whose usage proves 1M (7/13 in the measured corpus, mechanism undetermined), so absence must never be read as 200K; coverage starts at 2.1.251 and the record is written late in a session's life (see the enrichment-timing note in docs/solutions/history-only-1m-context-denominator.md §F1a). Not a wire event — local transcript shape, same class as the 2026-08-25 Codex compacted row.
2026-08-25Codex CLI / ccxrayCodex 0.148+ · ccxray · #606Compaction boundaries are observable in the local Codex transcript. Codex emits a top-level {"type":"compacted"} record without a token_count payload. ccxray latches that marker across zero-token/non-turn records and persists compacted: true on the next indexed turn, so live SSE and cold-load rendering agree. This is a local transcript event rather than an OpenAI Responses API wire event (obs-stable in Codex CLI transcript shape; version may change).
2026-08-24Codex CLI / Grok CLI / ccxrayccxray · #585Gateway tool extraction is now covered. Codex custom_tool_call items are counted, MCP names embedded in exec JavaScript input are extracted, and Grok use_tool arguments.tool_name is normalized to the underlying MCP name. When both WebSocket .added and .done events exist, the complete .done payload wins over the empty .added stub (obs-stable). Live smoke confirmed Codex {mcp__node_repl__js: 1, Bash: 1} and Grok {github__pull_request_read: 1}.
2026-08-17Claude Code / ccxrayccxray · #533/#535/#536Refines the 2026-07-10 and 2026-06-09 rows. The context-1m-2025-08-07 value is now persisted verbatim as the index field ctxBeta (whitelisted, shape-filtered to context-<n><k|m>- so the unrelated context-management-* beta cannot land in a window field); beta1m remains its capability-gated interpretation, and the two may legitimately disagree on a model the gate refuses. Verified live on 2026-08-16: claude-opus-5[1m] sends context-1m-2025-08-07 and the bare model sends nothing, matching the fable-5 era observation, so the per-model [1m] selection rule holds across model families. The capability gate itself is no longer the hand-maintained SUPPORTS_1M regex alone but a UNION of that list and LiteLLM's max_input_tokens — LiteLLM may ADD 1M capability, never DENY it, because the field is semantically inconsistent (capability for claude-fable-5 → 1M, default serving window for claude-sonnet-4-5 → 200K although 4.5 serves 1M under the beta). That does not weaken the 2026-07-10 rule below: LiteLLM still never supplies the session-window denominator, only the answer to "could this model serve 1M at all".
2026-08-15Codex CLI / ccxrayCodex 0.148.0-alpha.9 · ccxray 2.3.1ChatGPT platform routing is path-scoped, not one universal /backend-api/codex prefix. Responses remain /backend-api/codex/responses; plugin catalogs and connectors use /backend-api/{plugins,ps/plugins,connectors}; analytics uses /backend-api/codex/analytics-events/events; Apps MCP normalizes the launcher's local /v1/api/codex/ps/mcp back to /backend-api/ps/mcp. Confirmed against Codex source tests and a real OAuth run: plugin, Apps MCP, and analytics 404 warnings all fell to zero.
2026-08-11Claude Code / ccxrayccxray · #504Documented the observed global-instructions marker Contents of <configDir>/CLAUDE.md (user's private global instructions for all projects) used to recover the session config directory. Unix separators are observed; ccxray defensively accepts Windows backslashes, but that variant has not been tested on Windows (obs-fragile).
2026-08-10Codex CLI / Grok CLI / ccxrayccxray · #485Three decoder fixes. (1) Grok decodeGrokToolOutput now accepts both old footer format (EXIT_CODE[=:]N on last line) and new first-line format (exit: N); footer has priority when both are present (real 2026-08-07 capture: exit: 0 first-line was unreliable in dual-format payloads). (2) Codex async commands (>~11s) split into a start segment (custom_tool_call_output, "Script running...") and a retrieval segment (function_call_output, carries real exit_code); codex:function_call_output is now registered in the decoder table. Start segments are detected and marked eligible:false so the retrieval pairs correctly with the pending call in weather. (3) decodeCodexToolOutput now unwraps top-level array-shaped payloads with per-element exit_code validation and failure-dominates aggregation (#482).
2026-08-08Codex CLI / ccxrayCodex 0.145.0 · ccxray 2.3.0 · #475Real Codex WebSocket traffic confirms custom_tool_call_output.output reaches the parser as an already-parsed input_text[], while older captures may retain the serialized JSON-array form; both envelopes are accepted. A single envelope may contain multiple command-result JSON texts, so any decoded non-zero top-level safe-integer exit_code dominates clean results. The matching response-side custom_tool_call.name is exec; it maps to Bash. Weather eligibility requires both an eligible decoder and a call name positively normalized to Bash by the process/shell allowlist; non-process and unrecognized tool names are excluded.
2026-08-08ccxray2.3.0 · #475OpenAI tool-failure weather attribution now persists response-side tool call IDs and request-side result facts add-only in the index, then joins them by call_id at assessment time. It does not rewrite a prior entry or infer tool use from response.status; only paired process/shell results whose allowlisted decoder is eligible enter failure rates, and undecodable eligible results remain unknown and visibly reduce known-rate.
2026-08-08OpenAI Responses / ccxrayResponses API (2026-08-08) · #473Responses tool invocations are a type family, not only names containing tool_call: computer_call, local_shell_call, web_search_call, file_search_call, code_interpreter_call, mcp_call, and image_generation_call are tool-call output items, with corresponding *_call_output result items where applicable (contractual). ccxray's defensive renderer now classifies unfamiliar *_call / *_call_output pairs symmetrically while leaving message, reasoning, and mcp_list_tools outside the tool path.
2026-08-08ccxray2.3.0#472 review hardening: metadata.client is proxy-asserted only on HTTP and WS turns. A matched wire client overwrites any body claim; without a match, body-supplied client is removed before raw-session selection or tool-result decoding. Codex tool-result exit_code is decoded only when it is a safe integer; non-integers remain unchecked while negative integers remain valid failure codes.
2026-08-08ccxray2.3.0#472: OpenAI-wire HTTP input[] is cumulative history, so per-request tool-result attribution uses only the trailing contiguous *_call_output block; earlier output items are historical and must not be reported again. No trailing output block means the turn is unchecked (turnToolFail: undefined), not checked-clean.
2026-08-07Codex CLI / Grok CLICodex 0.145.0 · Grok 0.2.114Tool-call item types differ per client AND per version — do not treat either as a provider constant (obs-fragile, #470). Codex 0.145.0 emits custom_tool_call (response output_item.done item.type, plus response.custom_tool_call_input.delta/.done events) and custom_tool_call_output in request input[] — whereas this repo's own fixture test/fixtures/wire-parsers/openai/turn1_res.json records the same shell tool as function_call on an earlier version. Grok 0.2.114 uses standard function_call / function_call_output. Item type is understood to follow the request-side tool declaration (custom/freeform vs function tool), which is a per-tool CLI choice, not a client identity — so a single-key dispatch on type will mis-route a future client. Failure-signal payloads also differ: Codex output is an outer input_text[] envelope (already parsed in real WS request data, but serialized in older fixtures), whose text fields may contain inner JSON carrying "exit_code":N; Grok output is plain text whose wrapper footer uses EXIT_CODE=N on failure but EXIT_CODE:N on success; 2026-08-09/10 captures show the footer absent, replaced by first-line exit: N (obs-fragile). ccxray now accepts both: last-line EXIT_CODE[=:]N (footer priority) then first-line exit: N; middle lines are never scanned. Both payload conventions are CLI-wrapper output, not API contract. ccxray records the checked result in tri-state turnToolFail using a (metadata.client, item.type) allowlist; unknown combinations remain unknown. stopReason remains the response status (completed/incomplete, never tool_use/max_tokens); since #475 weather instead pairs the response-side call with a later request-side result by call_id. Measured in isolated instances (own port + own CCXRAY_HOME); three combinations covered (Codex/WS, Codex/HTTP-SSE via rejected upgrade, Grok/HTTP-SSE). Full diagnosis: docs/solutions/openai-tool-failure-signal.md.
2026-08-01Grok CLI / ccxray0.2.114Supersedes the 2026-07-30 row below. buildPricingTable spread restored to { ...DEFAULT_PRICING, ...mirrored } (PR #405, issue #397): LiteLLM wins again and DEFAULT_PRICING is the offline floor, so no hardcoded row shadows live data. Mirroring is now restricted to an xai/ allow-list — previously any provider/model key could claim a bare id, and azure_ai/grok-code-fast-1 was in fact winning bare grok-code-fast-1 with a 10x wrong cached-input rate (0.20 vs xAI's 0.02).
2026-08-01Grok CLI0.2.114Title-gen attribution has a narrower success condition than the 60s window implies: the parent must still be in flight when the title turn completes, not merely start within the window. Measured over four runs — three short -p prompts had the title turn arrive ~0.5s before the parent and resolve; one agentic prompt had the title turn complete 7.2s before the parent's first request even opened, and attribution returned null. The CLI emits the title request at session start, then may spend seconds on tool setup before the first main turn exists (obs-stable). See issue #399.
2026-07-30Grok CLI / ccxray0.2.93+Wire model id grok-4.5-build price pinned in DEFAULT_PRICING at input 2 / output 6 / cache_read 0.50 USD per 1M tokens (obs-fragile). Spread order { ...mirrored, ...DEFAULT_PRICING } means the pin permanently shadows LiteLLM until the row is deleted once LiteLLM lists xai/grok-4.5-build. Superseded 2026-08-01 — the spread was inverted back; see the entry above.
2026-07-19Grok CLI / ccxray0.2.93+Title-gen attribution: parallel model=grok-build + forced session_title tool; empty x-grok-session-idgrok-raw bucket. Generated title is function_call args {"session_title":"…"} on Responses SSE (response.function_call_arguments.done / response.completed.output). Anchor match uses normalized <user_query> body so main turns (user_info first) link to title-gen. Attribution window 60s while parent is inflight. Wire model id grok-4.5-build seen on main turns.
2026-07-10Claude Code2.1.206Discovered via loopback wire capture (fable-5 era, #211): per-model [1m] selection changes the wire signals per mode. /model claude-fable-5 (bare, 200K session): no context-1m-* in anthropic-beta, marker says ...is claude-fable-5. — even on an account with 1M available. /model claude-fable-5[1m] (1M session): context-1m-2025-08-07 present + marker says ...is claude-fable-5[1m].; request model field stays bare in both modes. Refines the 2026-06-09 observation: header presence follows the selected model mode, not bare account capability. Also: LiteLLM max_input_tokens records API max capability (fable-5 → 1M, the API default) which is NOT the Claude Code session window — ccxray now clamps LiteLLM data to 200K for claude-* and relies on the wire signals for 1M.
2026-07-09Grok CLI0.2.93First wire capture + integration: POST /v1/responses SSE (no WS) via cli-chat-proxy.grok.com; client redirect GROK_CLI_CHAT_PROXY_BASE_URL. System prompt in input[role=system] (string content), not instructions. Session via x-grok-session-id / x-grok-conv-id. Header-based upstream routing to UPSTREAMS.xai keeps Codex on api.openai.com in a shared hub. Control-plane /v1/* (settings/feedback) classified as noise for Grok clients. Full notes: docs/grok-wire-experiment-2026-07-09.md.
2026-07-07ccxray1.10.xCodex parity fix: ccxray now treats Codex/OpenAI thread_id as a session-id fallback when session_id is absent, normalizes metadata.workspaces / x-codex-turn-metadata.workspaces into metadata.cwd, and promotes WS sessions from the synthetic codex-raw bucket once response.create.metadata arrives. codex-raw remains only for OpenAI traffic with no session/thread signal.
2026-07-06Claude Code2.1.xDiscovered: POST /v1/messages/count_tokens calls (token pre-counting for large content). Body is bare {model, messages} — no system, no metadata, no tools, no max_tokens; response is exactly {"input_tokens": N} (non-SSE). Satisfied every subagent heuristic and polluted sessions with fake single-turn subagent entries (#146). ccxray now classifies the path as noise (skipEntry), matching quota-check / codex-platform-ping handling.
2026-06-09Claude Code2.1.xConfirmed via loopback wire capture: anthropic-beta carries context-1m-2025-08-07 on every request when the account's 1M context is enabled — including haiku title-gen turns (it is a client/account-level capability flag, not a per-turn window declaration). ccxray now uses it as the non-lagging 1M-window signal, gated by model capability (SUPPORTS_1M), replacing sole reliance on the lagging system-prompt [1m] marker (#58).
2026-06-05ccxray1.11.xUsage normalization: OpenAI input_tokens includes cached_tokens (subset), unlike Anthropic's disjoint fields. normalizeUsageForProvider now subtracts the overlap so canonical input_tokens + cache_read + cache_creation = total context holds for both providers. Normalized entries carry _ccxrayUsageNormalized: true. Historical entries normalized on restore (in-memory, index unchanged). Cache display: Codex sessions show cache N% hit instead of TTL countdown; topbar adapts per provider (ephemeral-ttl vs server-managed). UPSTREAM_PROFILES registry added to providers.js.
2026-06-04ccxray1.10.xFix: WS stopReason now extracts response.status from terminal events (completed/incomplete/failed/cancelled) instead of WS close reason. WS title extracts user input summary via getOpenAIInputSummary instead of hardcoded string. Non-terminal statuses (in_progress/queued) are ignored to prevent masking close/error reasons.
2026-06-02ccxray1.10.0Doc audit: 13 major + 25 minor corrections applied (F1–F38)
2026-06-01Codex0.133Baseline: all observations below recorded
2026-06-01Claude Code2.1.159Baseline: all observations below recorded
2026-06-01ccxray1.9.x→1.10.0Discovered: meta-tools without .name, generate:false warm-up pattern, ephemeral cache fields, ChatGPT OAuth path-based routing

1. Transport

AspectClaude CodeCodexGrok CLIConfidence
ProtocolHTTP POST + SSE streamingHTTP POST (SSE) + WebSocket upgradeHTTP POST + SSE streaming (no WS on normal turns)contractual / obs-stable
Primary pathPOST /v1/messagesPOST /v1/responses (HTTP) or WS upgrade on /v1/responsesPOST /v1/responses (SSE) via cli-chat-proxy.grok.comcontractual / obs-stable Grok ≥0.2.93
Alternative pathsPOST /v1/messages/count_tokens (token pre-counting; bare {model, messages} body, non-SSE {"input_tokens": N} response; classified as noise by ccxray, #146)/v1/realtime (Realtime API, not used by Codex CLI for chat)contractual
WS upgrade detectionN/Aupgrade: websocket header on /v1/responses or /v1/realtime; ccxray also requires upstream.provider === 'openai'contractual
WS handshake headerN/Aopenai-beta: responses_websockets=2026-02-06 (observed value from Codex wire traffic; ccxray passes through without validation)obs-stable codex ≥0.131
WS idle timeout (proxy)N/Accxray: 60s default, configurable via CCXRAY_WS_IDLE_TIMEOUT_MSobs-stable
WS connection limit (OpenAI)N/AOpenAI docs: 60-minute absolute connection limitcontractual
WS keepaliveN/APing/pong frames relayed bidirectionally between client and upstreamobs-stable
Content-Typeapplication/jsonapplication/json (HTTP); binary/text frames (WS)contractual
Upstream hostapi.anthropic.com:443api.openai.com:443 (API key) or chatgpt.com:443 (ChatGPT OAuth)contractual / obs-stable

Note: WS warm-up (generate: false) is a request-payload pattern, not a transport mechanism — see Section 6.3.


2. Auth & Routing

AspectClaude CodeCodexConfidence
Auth headerx-api-key: sk-ant-...authorization: Bearer sk-... (API key) or JWT (ChatGPT OAuth)contractual
ChatGPT OAuth detectionN/Achatgpt-account-id header present AND JWT-shaped (dot-separated) authorization tokenobs-stable codex ≥0.131
ChatGPT OAuth routingN/Achatgpt-account-id header present OR request matches Codex platform paths. Responses route under /backend-api/codex; plugin catalogs, connectors, and Apps MCP route under /backend-api; /v1/codex/* already carries its codex segmentobs-stable codex ≥0.148
ChatGPT base pathsN/APath-scoped: /v1/responses/backend-api/codex/responses; /v1/ps/plugins/*/backend-api/ps/plugins/*; /v1/connectors/*/backend-api/connectors/*; /v1/codex/*/backend-api/codex/*; launcher-derived /v1/api/codex/ps/mcp/backend-api/ps/mcpobs-stable codex 0.148 (Codex source + real OAuth run)
Version headeranthropic-version: 2023-06-01N/Acontractual
Beta featuresanthropic-beta: ... (comma-separated)openai-beta: ...contractual
ccxray launch account headerccxray claude appends X-Ccxray-Account: <oauthAccount.emailAddress> from the effective config directory's .claude.json; the header is consumed by ccxray only and removed from the forwarded set by the x-ccxray-* prefix rule.N/Aobs-stable ccxray #611
1M context window signalanthropic-beta list contains context-1m-2025-08-07 (present on every request when 1M enabled — a client-level flag, also on haiku turns; does not lag a mid-session model switch, unlike the system-prompt [1m] marker). Persisted verbatim as ctxBeta since #533, so a future tier such as context-400k-* is legible without a code change; the tier is parsed out of the id rather than tabulatedN/Aobs-stable Claude Code ≥2.1.x
Per-model [1m] selection/model <id>[1m] (fable-5 era): request model field stays bare in both modes; the system-prompt marker carries the suffix (...is claude-fable-5[1m].); context-1m-2025-08-07 appears only in [1m] mode — the bare model sends neither signal even on an account with 1M available. Session window follows the selection (200K bare / 1M with [1m]), NOT the model's API capability: LiteLLM max_input_tokens records the API max (fable-5 → 1M, the API default) and must never be used as the session-window denominator (#211)N/Aobs-stable Claude Code ≥2.1.206
Rate-limit ≠ context windowanthropic-ratelimit-tokens-limit (e.g. 80000) is a per-window quota, not the context window — never use it to size the denominatorN/Aobs-stable

3. Request Shape

3.1 Top-level fields

FieldClaude CodeCodexConfidence
Modelmodel: "claude-sonnet-4-6"model: "gpt-5.5"contractual
System promptsystem: [{type:"text", text:"...", cache_control?}] (array of blocks)instructions: "..." (string)contractual
Global CLAUDE.md markerSystem-prompt text includes Contents of <configDir>/CLAUDE.md (user's private global instructions for all projects). The observed form uses /; ccxray also accepts \ because Windows paths use backslashes, but the Windows form has not been observed or tested on Windows.N/Aobs-fragile Claude Code 2.1.x (#504)
Conversationmessages: [{role, content}]input: [{type, role, content}]contractual
Toolstools: [{name, description, input_schema}]tools: [{type:"function", name, description, parameters}]contractual
Max outputmax_tokens: 16384max_output_tokens: 4000obs-stable (typical observed values; vary by model and plan)
Streamingstream: trueImplicit (SSE mode) or WS mode (no stream field)contractual
Turn chainingN/A (full history in messages)previous_response_id: "resp_..." (WS mode)contractual
Session metadatametadata: {session_id: "..."} (in body)metadata: {session_id, thread_id, turn_id, workspaces, ...} (in body) + x-codex-turn-metadata header. ccxray consumes session_id, thread_id, and workspaces; turn_id is present on the wire but not consumedcontractual (body) / obs-stable (header)
Tool choicetool_choice: {type:"auto"}tool_choice: "auto" (string; OpenAI also accepts object form)obs-stable
Model rewritingccxray supports CCXRAY_MODEL_PREFIX/REWRITE_MODEL_PREFIX to rewrite model names in-flightSameobs-stable (ccxray feature)

3.2 Message/Input item structure

AspectClaude CodeCodexConfidence
User text{role:"user", content:[{type:"text", text:"..."}]}{type:"message", role:"user", content:[{type:"input_text", text:"..."}]}contractual
Assistant text{role:"assistant", content:[{type:"text", text:"..."}]}{type:"message", role:"assistant", content:[{type:"output_text", text:"..."}]}contractual
System in conversation{role:"user", content:[{type:"text", text:"<system>..."}]} (injected tags){type:"message", role:"developer", content:[...]}obs-stable
Tool invocationIn assistant message: {type:"tool_use", id, name, input}Response output items include function_call, tool_call, custom_tool_call, computer_call, local_shell_call, web_search_call, file_search_call, code_interpreter_call, mcp_call, and image_generation_callcontractual
Tool result{role:"user", content:[{type:"tool_result", tool_use_id, content}]}Input/result items use the corresponding *_call_output type where applicable and pair through call_idcontractual
Thinking{type:"thinking", thinking:"..."} in assistant content{type:"reasoning", ...} as separate input item (content is null/opaque)contractual (Anthropic) / obs-stable (Codex)
Image input{type:"image", source:{type:"base64", ...}}{type:"input_image", image_url:"..."}contractual

3.3 WebSocket client→server frames (Codex only)

Frame typePayloadConfidence
response.createFull request body: {model, instructions, input, tools, tool_choice, previous_response_id, metadata?, generate?}. metadata may carry thread_id and workspaces. generate: false sends a warm-up frame (see Section 6.3)contractual / obs-stable (metadata shape)
session.update{session: {instructions: "..."}} — updates system prompt mid-sessionobs-stable codex ≥0.131

4. Response Shape & Events

4.1 Claude Code — SSE events

SSE event typedata payloadConfidence
message_start{type:"message_start", message:{id, model, usage:{input_tokens, cache_creation_input_tokens, cache_read_input_tokens}}}contractual
content_block_start{type:"content_block_start", index, content_block:{type:"text"|"thinking"|"tool_use", ...}}contractual
content_block_delta{type:"content_block_delta", index, delta:{type:"text_delta"|"thinking_delta"|"input_json_delta", ...}}contractual
content_block_stop{type:"content_block_stop", index}contractual
message_delta{type:"message_delta", delta:{stop_reason:"end_turn"|"tool_use"|"max_tokens"}, usage:{output_tokens}}contractual
message_stop{type:"message_stop"}contractual

4.2 Codex — WebSocket server→client events

WS event typePayload (key fields)Confidence
response.created{response:{id, model, status:"in_progress", ...}} (contains full instructions+tools; typically ~35KB)contractual
response.in_progressSame shape as response.createdcontractual
response.output_item.added{item:{id, type:"message"|"reasoning"|<tool>_call, name?, call_id?, status:"in_progress"}, output_index}; tool call types include the family listed in §3.2contractual
response.function_call_arguments.delta{delta:"...", item_id, output_index}contractual
response.function_call_arguments.done{arguments:"{...}", item_id, output_index}contractual
response.output_text.delta{delta:"...", item_id, content_index, output_index}contractual
response.output_text.done{text:"...", item_id, content_index}contractual
response.content_part.added{part:{type:"output_text"}, item_id, content_index}contractual
response.content_part.done{part:{type:"output_text", text:"..."}, item_id}contractual
response.output_item.done{item:{id, type:"message"|"reasoning"|<tool>_call, name?, call_id?, arguments?, status:"completed"}}; tool call types include the family listed in §3.2contractual
response.completed{response:{id, model, status, usage, ...}} — ccxray extracts only usage and model before discarding; output: null, input: null observed but unverifiedobs-fragile codex 0.133
response.doneSame shape as response.completed; observed as a separate event type in some Codex versions. Proxies should handle bothobs-fragile
codex.rate_limits{...} rate limit info (non-standard, Codex-specific). Payload shape is not parsed by ccxray; exact fields undocumentedobs-stable codex ≥0.131

4.3 Usage/cost fields

FieldClaude CodeCodexConfidence
Wire model id grok-4.5-build (Grok CLI)N/AN/A — Grok only: DEFAULT_PRICING carries input 2 / output 6 / cache_create 0 / cache_read 0.50 (USD per 1M) as the offline floor. Since PR #405 the spread is {...DEFAULT_PRICING, ...mirrored}, so a live LiteLLM row wins whenever the fetch succeeds; the bare id resolves today via the xai/grok-4.5 mirror plus longest-prefix match. Delete the pinned row once LiteLLM lists xai/grok-4.5-build (2026-08-01)obs-fragile
Input tokensmessage_start.message.usage.input_tokens (non-cached only)response.usage.input_tokens or prompt_tokens (includes cached — ccxray subtracts cached_tokens via normalizeUsageForProvider so canonical input_tokens = non-cached for both providers)contractual
Output tokensmessage_delta.usage.output_tokensresponse.usage.output_tokens or completion_tokenscontractual
Cache creationusage.cache_creation_input_tokensN/A (no equivalent field)contractual (Anthropic)
Cache creation breakdownusage.cache_creation.ephemeral_5m_input_tokens, usage.cache_creation.ephemeral_1h_input_tokensN/Aobs-fragile
Cache readusage.cache_read_input_tokensusage.input_tokens_details.cached_tokens (ccxray maps to canonical cache_read_input_tokens)contractual (Anthropic) / obs-stable (Codex)
Stop reason (HTTP)message_delta.delta.stop_reason (end_turn, tool_use, max_tokens)response.status (completed, failed, cancelled)contractual
Stop reason (WS)N/Aresponse.completed / response.done events carry response.status on the wire (completed, incomplete, failed, cancelled). ccxray extracts terminal status before WS_SKIP_EVENTS discards the envelope; non-terminal (in_progress, queued) ignored to preserve close/error fallback.contractual (wire) / obs-stable (ccxray storage)

5. Session & Turn Lifecycle

AspectClaude CodeCodexConfidence
Session ID sourcebody.metadata.session_idHeader session_id or x-openai-session-id, or x-codex-turn-metadata JSON → .session_id, or body.metadata.session_id, then x-codex-turn-metadata.thread_id / body.metadata.thread_id as fallback. Falls back to literal codex-raw sentinel when no session/thread source yields an IDcontractual (body) / obs-stable (headers, thread_id)
Session ID formatUUID v4 (e.g. 06e8a0f7-...)UUID v7 (e.g. 019e809a-...)obs-stable
Turn IDNot explicit; each HTTP request = one turnx-codex-turn-metadataturn_id (present on wire but not consumed by ccxray)obs-stable codex ≥0.131
Agent type (Codex)N/APriority: x-openai-agent-type / x-codex-agent-type header, then x-codex-turn-metadata JSON → .agent_type, then x-openai-subagent as fallback. Values: explorer, worker, defaultobs-stable
Subagent flag (Claude)Heuristic: absence of cwd in system prompt metadata. Also: stricter isLikelySubagent() heuristic in store.js for session inference (multi-condition: inflight + temporal)N/Aobs-stable
Subagent flag (Codex)N/AHeader x-openai-subagent (truthy, checked first) or body.metadata.is_subagent/isSubagent (fallback). WS path derives from agentType === 'explorer' || agentType === 'worker'obs-stable codex ≥0.131
CWD detection (WS)Extracted from system prompt content (regex on cwd path)response.create.metadata.cwd / .workspaces, x-codex-turn-metadata.cwd / .workspaces, then instructions CWD: line. Workspace extraction uses 5 strategies: (1) workspaces.cwd, (2) workspaces.current, (3) first string value, (4) nested object with .cwd, (5) first key starting with /obs-fragile (format varies across Codex versions)
CWD detection (HTTP)(same as WS)parsedBody.metadata.cwd, parsedBody.metadata.workspaces, x-codex-turn-metadata.cwd/workspaces, instructions CWD: line, then hub client CWD or process.cwd()obs-stable / obs-fragile (workspaces)
Multi-turnFull messages[] history in every requestWS: previous_response_id + incremental input. HTTP: full cumulative input[] history; the immediately preceding turn's tool results form the trailing contiguous *_call_output blockcontractual (history) / obs-stable (trailing output block)

6. Error & Edge Cases

6.1 HTTP error shapes

AspectClaude CodeCodexConfidence
Error body{type:"error", error:{type:"...", message:"..."}}{error:{message:"...", type:"...", code:"..."}}contractual
Rate limit (429)x-ratelimit-limit-tokens, x-ratelimit-remaining-tokens headersx-ratelimit-limit-tokens, x-ratelimit-remaining-tokenscontractual
Overloaded (529)Anthropic-specific overloaded statusN/Acontractual

6.2 WebSocket close codes (Codex only)

CodeMeaningConfidence
1000Normal closurecontractual
1001Going away (proxy shutdown)obs-stable
1009Client→upstream send buffer overflow (> 4 MiB queued while upstream is still connecting). Once upstream is OPEN, messages forward with no size limitobs-stable
1011Internal error (idle timeout, upstream reject, socket error)obs-stable

Note: Close reason strings are clamped to 120 bytes per RFC 6455 section 7.1.6.

6.3 Codex-specific edge cases

BehaviorDetailConfidence
WS warm-up before real turnCodex sends response.create with generate: false (warm-up) before the real response.create. ccxray gates on generate !== false; the input: [] claim is an unverified wire observation. Proxies must skip generate: false frames when capturing request data. The generate parameter itself is contractual; the warm-up pattern is Codex CLI behaviorobs-stable codex ≥0.131 (pattern) / contractual (generate param)
response.completed stripped fieldsoutput: null, input: null — large fields omitted from WS event despite being present in HTTP response. ccxray extracts only usage and model before discarding; null fields are unverified wire observationobs-fragile codex 0.133
Meta-tools without .nameAt minimum tool_search, web_search, image_generation — these tool definitions have no name field. Any t.name.startsWith(...) crashes. Current code has guards (historical bug, now fixed)obs-stable codex ≥0.131
Startup platform pingsCodex 0.133+ sends requests to /v1/plugins/*, /v1/ps/plugins/*, /v1/connectors/*, /v1/api/codex/*, /v1/codex/*, and /v1/models. They are noise-filtered (skipEntry: true) but still forwarded to path-scoped ChatGPT upstreams. Codex 0.148 real OAuth validation returned no plugin, Apps MCP, or analytics 404 after path normalizationobs-stable codex 0.148
codex-raw sessionAny OpenAI request lacking both session_id and thread_id (WS or HTTP) is grouped under the synthetic codex-raw session ID. WS sessions can be promoted out of codex-raw once response.create.metadata provides a thread/session idobs-stable (ccxray convention)
Proxy client identitymetadata.client is never trusted from the request body. ccxray derives it from the wire-client matcher, overwrites conflicting body values, and removes unmatched claims before session attribution and decoder dispatchobs-stable ccxray ≥2.3.0 (ccxray convention)
Codex tool-result exit codeCodex tool output is an input_text[] envelope, observed already parsed on WebSocket and retained as a serialized JSON array in older fixtures. In each input_text.text inner JSON object, only a top-level safe-integer exit_code is decoded. 0 is clean; any non-zero integer, including a negative integer, is failure; fractional or unsafe numeric values are undecodable. Across multiple decoded command results, failure dominatesobs-fragile Codex 0.145.0 (CLI payload) / obs-stable ccxray ≥2.3.0 (ccxray convention)