Skip to content

Latest commit

 

History

History
576 lines (469 loc) · 50.3 KB

inbound-compat.md

File metadata and controls

576 lines (469 loc) · 50.3 KB

Inbound Compatibility Surfaces

Thinking enablement is separate from strength: src/responses/schema.ts accepts the OCX reasoning.enabled boolean and src/responses/parser.ts retains it as options.thinkingEnabled, independently of options.reasoning. Chat thinking.type and Anthropic thinking.type preserve this choice through their inbound translators; the existing Anthropic disabled-effort sentinel remains for legacy destinations. src/clients/reasoning-controls.ts decodes effort-only client aliases for declared toggle-only models before strength caps run. Route fallback restores the original choice alongside effort in src/server/effort-policy.ts; a cleared strength list does not clear a thinking switch. Regression coverage lives in tests/responses/responses-parser.test.ts, tests/responses/chat-inbound-reasoning-none.test.ts, tests/claude-integration/claude-inbound.test.ts, and tests/codex-integration/effort-policy.test.ts.

The names used for these paths (native, translated, legacy bridge) and the declared per-feature dispositions are owned by Protocol Paths.

Native result continuations and function-result injection follow the mode-specific result and control contract; this surface does not infer upstream support or alter its defaults.

Native steering follows the shared WebSocket contract; this surface's defaults remain unchanged.

Compatibility callers retain the public Responses ingress described by the core module ownership. This surface retains its existing behavior. Chat Completions and Messages release their owned translator budget on request abort even if no caller consumes the response body, following the shared byte-accounting lifetime contract.

Chat and Messages admission previews the xAI OAuth Fast wire destination using the same policy as final Responses serialization. Native dispatch retains its own destination scope check.

The configuration-only plaintext V2 contract is scoped to canonical ChatGPT Responses forwarding; other source-area behavior described here is unchanged. Cursor's localized native-shell names follow the routing-commentary guard contract.

Standalone file transcription

src/server/audio-transcriptions.ts owns POST /v1/audio/transcriptions, independently of Responses and Chat conversion. src/server/audio-upstream.ts resolves explicit data-plane keys on both listeners and substitutes stored OpenAI credentials. Direct stored-main access claims the enclosing admission lease and derives its account header only from that stored credential; caller-supplied account selection is never retained. Pool uses the existing sidecar account resolver. A selected ChatGPT authentication failure never falls through to the paid OpenAI provider.

The bounded multipart input accepts one nonempty file up to 25,000,000 bytes within a 32 MiB body, required model, and optional prompt, language and JSON/text response format. Unknown or duplicate fields fail. Subscription requests use the compatibility identifier gpt-4o-transcribe and send no model upstream; the keyed API also accepts gpt-4o-mini-transcribe and whisper-1. Responses retain only text. Manual redirects, capped response reads and linked cancellation keep credentials and audio content out of redirects, request logs and durable storage. tests/server/audio-transcriptions.test.ts exercises the real ingress and synthetic upstream; tests/server/api-key-attribution.test.ts uses multipart fixtures for the HTTP auth matrix.

src/server/audio-upstream.ts is also where a configured key's model and provider scope is applied, once for every audio surface that resolves through it: the model it is handed is the one the upstream will run — the transcription model, the live session model, the model a standalone socket names in its own query, or the model a bound call settled on when the same key created it — and a refused forward request releases its probe lease. LiveCallBinding records that model for exactly this reason, so a reconnect is judged on the call it rejoins rather than on a default.

The native voice path in src/server/live.ts applies the same predicate but can name less. It records only which call ids it created (src/server/live-native-calls.ts), never their model or account, so a join, and a call-create that sends no session model, name no destination at all; a key carrying a model list is refused there rather than admitted against an assumed default, while a provider-only scope and an unscoped key are unchanged, on both join credentials described under streaming audio. Coverage lives in tests/server/api-key-scope-audio.test.ts, tests/server/api-key-scope-live.test.ts and tests/server/server-live-existing-call.test.ts.

Streaming audio

src/server/audio-client.ts recognizes explicit audio keys before local legacy admission. Browser sockets offer opencodex-audio and opencodex-key.; only the public marker is selected downstream. Invalid presented keys cannot become credential-free native calls. src/server/audio-dictation.ts maps the validated desktop session.start/audio.append/session.close protocol to ChatGPT dictation with server-owned credentials and a five-minute lifetime. The src/server/index.ts byte relay retains bounded queues, handshake/session deadlines and the account/turn lifecycle until its upstream closes. src/server/ws-bridge.ts carries the in-memory callbacks and signal; handshake credentials are cleared after socket construction.

src/server/audio-live.ts owns external keyed GPT-Live creation and joins while the original src/server/live.ts keeps the native compatibility path. src/server/live-call-bindings.ts maps opaque rtc_ocx_ aliases to the creating key, provider, physical account and protocol. Expired aliases never fall through to native joins. Reconnect resolves the recorded account freshly; keyed provider replacement fails unless its credential digest still matches. The registry is per server, holds at most 1024 entries for 30 minutes and is cleared on shutdown. It does not proxy WebRTC media or execute delegation requests. Standalone Frameless defaults to gpt-live-1-codex; gpt-live-1 is an explicit alias. Dictation and Frameless event formats remain separate. Coverage lives in tests/server/audio-client.test.ts, tests/server/audio-dictation.test.ts and tests/server/live-call-bindings.test.ts.

A native sideband join (/v1/live/<id>, /v1/realtime/calls/<id>, /v1/realtime?call_id=<id>) is authenticated as whoever owns the call. A call handleLive created was negotiated under the account the Pool selected, so its id is recorded from the create's Location — extracted exactly as openai/codex reads it — and its join keeps Pool selection and thread affinity (openai/codex #35830). Any other call was created by the client with its own ChatGPT login: ChatGPT voice handing a call to a Codex thread, or Codex Desktop when its renderer owns the call, both arriving as a V3 existingCall join. That join forwards the caller's own explicit ChatGPT credential through the Direct passthrough, never the proxy admission secret, and falls back to Pool selection only when the caller presents none. The id registry is process-local, holds ids only, keeps at most 1024 for six hours, and evicts oldest first; a restart, expiry or eviction makes a created call look caller-owned to a later rejoin. Coverage lives in tests/server/live-native-calls.test.ts and tests/server/server-live-existing-call.test.ts.

Translated Claude timeline reminders use the Chat adapter's chronological instruction ordering on every destination. This is separate from trailing-notice stabilization and from native Chat message passthrough. On native Chat, a spend ceiling checked before any physical send remains a local 429 refusal when a transient upstream response causes a later retry leg to reach that ceiling.

Shared parsing and streaming follow the request-copy and stream-buffer accounting contracts. Response-attached WebSocket telemetry follows the stage record identity contract.

Chat Completions inbound native path

Managed native Chat and translated Chat share the explicit empty client-catalog boundary: a current-turn tools: [] refuses client tool output with undeclared_tool_call in SSE and JSON. Omitted, partial, and hosted-only catalogs retain deferred delivery, and caller-forward routes retain their exemption.

Droid request defaults

src/server/droid-reasoning-default.ts reads the model-scoped x-opencodex-droid-default-effort preference at Chat ingress for both native and translated routes. A canonical value fills reasoning_effort only when neither that property nor nested reasoning.effort is present. A concrete route validates the preference against that model's effective reasoning ladder. Combo and policy routes retain it as logical request intent until each concrete attempt applies the existing target-specific reasoning normalization, so an incompatible first target cannot discard it for a compatible fallback. With no configured or metadata ladder, the routed catalog's canonical fallback ladder applies; an explicit empty per-model ladder stays empty. Unsupported or invalid defaults have no effect; even an explicit null or invalid effort suppresses the default and retains the existing request semantics. Synthetic effort rows keep their selected effort. Provider headers do not forward this internal preference. Existing pins and caps run afterward with their usual authority. The Droid integration owns the persisted per-model values.

Route selection

POST /v1/chat/completions sends eligible openai-chat routes directly to the provider's Chat Completions endpoint. Route selection reads the raw Chat body and the native request keeps that body as its wire source; a Responses projection is constructed only after the native route is declined and is never converted back into Chat. Translated Codex requests inherit the Responses compatibility and retired-evidence policy, without introducing a model-name denylist at this inbound boundary. Request construction remains owned by src/adapters/openai-chat.ts, including model normalization, credential and provider headers, capability-specific fields, and the send URL: the canonical openaiChatCompletionsUrl() path, or chatCompletionsPath when the provider declares one. That field is the openai-chat mirror of responsesPath and exists because a per-model wire override swaps the adapter without touching baseUrl, so an upstream serving the two wires under different prefixes cannot be reached by the swap alone. The passthrough builder uses an explicit Chat-field whitelist so messages (including name and separate system/developer entries), Chat token controls, sampling/logprob fields, caller identity/metadata, and caller stream options retain their wire shape. For streams, caller stream_options are merged with mandatory include_usage: true. On the native passthrough there is no canonical Fast injection and no wire mapping: every caller service_tier — canonical or foreign — is forwarded raw and only under chatServiceTier: true, and fastMode injects nothing here. Resolved-Fast-policy injection applies only to routes that take the Chat -> Responses -> Chat bridge below. parallel_tool_calls is emitted only for providers opted into parallel tools (or pinned false by the existing provider opt-out contract). A reasoning_details entry carrying Anthropic state — the anthropic-claude-v1 marker, or an unmarked reasoning.text whose signature passes the inbound spelling check — makes the request ineligible for native passthrough so its signature or redacted ciphertext cannot cross into another provider; the bridge keeps only the plaintext projection. Other provider formats retain their native wire shape. The native passthrough applies the independent reasoning capability contract: explicit false declarations constrain their respective controls, and an empty strength ladder removes caller reasoning_effort without disabling a declared thinking switch. Unknown axes remain unclassified. The two Chat builders share the explicit wire policy after provider resolution: reasoningWireFormat: "gateway-object" projects the configured object shape, and a listed tool-bearing model omits reasoning effort on both paths. For explicitly declared independent controls on a toggle wire, reasoning.enabled wins, then caller raw thinking, then aliases/defaults; unknown or adaptive raw thinking shapes remain intact. With no applicable declaration, native raw reasoning forwarding stays unchanged. This guard does not alter the separate raw service-tier contract.

On the response side, the upstream service_tier echo (xAI Priority Processing, OpenAI fast tier) relays to the Chat Completions caller on every delivery shape: the non-streaming body (responsesJsonToChatCompletion in src/chat/outbound.ts), the folded stream (collectChatCompletion in src/chat/outbound.ts), and each synthesized SSE chunk (jsonCompletionSse in src/server/chat-native-sse.ts). An upstream that sends no service_tier gets no injected key. The Responses lane already relayed the same field for responses-wire upstreams; the responses-lane assembly for chat-wire upstreams keeps it in attempt telemetry only.

Combo/policy routes and requests that need Responses-only hosted tools, continuation, background, or storage semantics retain the existing Chat -> Responses -> Chat bridge. With protocols.rollout.directEncoders on, the response half of that bridge is skipped for a single non-Responses route: adapter delivery encodes the adapter events straight into Chat (or, on the Messages ingress, Anthropic) frames and marks the response, and the ingress returns it without the Responses-to-client conversion. The client-visible frames are the converter's; see Protocol Paths and responses.md. On its streaming return path, typed response.heartbeat events become SSE comment-line keepalives. They preserve connection liveness without adding a Chat completion chunk, changing usage, or claiming semantic progress; see the heartbeat contract. Chat-to-Responses traffic that lands on api.meta.ai inherits the same 64-character tool-name aliasing as native Responses; see responses.md.

On that bridge, src/chat/inbound.ts decides where a system or developer message lands by where the caller wrote it. A leading block, before any conversational item exists, becomes instructions. One that arrives after the conversation has started becomes a chronological role:"developer" input item instead, the same representation src/claude/inbound.ts mints for a mid-conversation instruction, so the slot the caller chose survives to the adapter that preserves it. The role is developer rather than system because the native ChatGPT backend refuses a system item inside input and canonical forwarding folds a message-shaped system item back onto instructions. An instruction that arrives between a tool call and its result is held until the batch drains, or until the next user or assistant turn, so the pair the Kiro, Anthropic and Google mappers require to stay adjacent is never split. Placement on the wire is then owned by chronological in-conversation instructions, which also decides which role that slot carries. Regression coverage is in tests/responses/chat-inbound-developer-position.test.ts, which compares the final upstream body on the native Chat route, a combo route and the Responses endpoint.

The direct SSE relay accepts CR, LF and CRLF across arbitrary transport chunk boundaries while retaining at most one bounded event. EOF with an unterminated event and an event above the translator limit are typed upstream failures, never successful partial completions. Provider-controlled structured error messages are redacted before either JSON or SSE reaches the client. The native path uses the same request-attempt logging, reset retry, same-key 429 replay, key rotation, usage extraction, and request-signal cancellation contracts as routed Responses transport. Native Combo children expose only their own prepaid initial send and settle it at final HTTP admission; unsent exits and shared retry ceilings follow prepaid initial sends. Because src/server/chat-completions.ts never enters Responses core, src/server/chat-native.ts repeats the pre-dispatch selectProactiveApiKeyTransport call before it binds the adapter; the pick remains inert unless a strategy is configured and the committed key is cooling. See responses.md.

Native Chat completion lifecycle

src/server/chat-native-sse.ts applies the resolved stallTimeoutSec while waiting for upstream progress. Nonempty text, reasoning, refusal, tool identity/arguments, and finish frames renew the allowance; comments, role-only frames, empty deltas, and usage alone do not. Downstream backpressure pauses this wait budget. A stall emits a Chat error with upstream_stall_timeout and logs 502; the non-streaming endpoint returns HTTP 502 rather than a successful partial result.

src/chat/outbound.ts collects CR/LF/CRLF, multiline data, and split UTF-8 through the shared SSE block buffer and tracks appended output bytes incrementally. A caller cancellation before a native terminal returns 499 / client_cancelled; an already accepted terminal keeps its result. Reader, timer, turn, and translator ownership are released through the existing lifecycle.

HTTP caller conversation identity

Canonical ChatGPT Responses egress additionally normalizes caller aliases in the selected auth headers. A non-empty explicit session_id wins; otherwise, the first non-empty alias among session-id, then thread-id, supplies session_id if its value is safe, and an invalid winning alias suppresses weaker identity. Empty values are dropped before this step on every path (materializeCodexUpstreamAuth in src/codex/auth-context.ts and the Claude Messages and Chat Completions bridges), so an empty header counts as absent and an empty session-id lets a valid thread-id win. The Claude metadata-derived session applies only when no non-empty canonical header or alias remains. Aliases keep their original names on the wire; their validated raw caller values are forwarded like an explicit session_id, without principal scoping as used for promoted x-session-id. Original ingress headers and affinity computation are unchanged, as are custom and API-key destinations. No identity or originator is invented for marker-free requests.

src/server/caller-session-identity.ts promotes validated x-session-id on HTTP Responses, Messages and Chat Completions before turn admission in src/server/index/serve-options.ts. Explicit session_id, session-id, or thread-id presence wins, including empty values; managed Grok promotion runs first on Responses. The trimmed marker must start with an ASCII letter/digit, contain only letters, digits, dots, underscores, colons or hyphens, and stay within 128 characters. Loopback admission keeps it; authenticated admission scopes it with the trusted credential principal into an opaque SHA-256 identifier and skips promotion without that principal. Bodies and abort signals are preserved, and the original Request owns Bun timeout lookup. This provides continuity, not authorization or guaranteed cache hits. Existing explicit/Grok identities, WebSocket frames, compact and count_tokens retain their behavior.

Chat conversation identity forwarding

src/server/chat-completions.ts preserves caller prompt_cache_key on the Chat-to-Responses bridge. Canonical ChatGPT Responses forwarding preserves session_id, session-id, thread-id and per-request x-client-request-id under their original names, and additionally fills an absent session_id from the first present alias, session-id then thread-id, when safe, using the precedence and validation rules above. Missing conversation identity stays missing; a shared prefix/cache key is not converted into a session. The direct-mode outbound contract is covered by tests/responses/chat-conversation-affinity.test.ts. A promoted caller x-session-id arrives at this bridge already as session_id (see above). This transport contract does not prove a client's emission, Pool selection stability or cache hits.

The optional x-hermes-conversation-id header is accepted on /v1/chat/completions and /v1/responses strictly as a log-only correlation fallback (logCtx.conversationId), normalized to a 32-hex-character digest and stripped before upstream forwarding. It never modifies native session lane allocation (sessionLaneIdFromRequest), turn termination (bindTurnTerminationScope), or upstream prompt caching. Native header candidates (x-codex-parent-thread-id, session_id, session-id, thread-id) are each normalized independently in priority order before considering Hermes, ensuring that an empty or whitespace candidate does not mask a valid native identity. Configuring session_affinity_header: x-hermes-conversation-id on Hermes replaces the client's session-id header, trading away native session identity and lane scoping for log-only correlation; session-id remains the generated default. A Hermes-derived log id is marked conversationIdIsHermesFallback (in memory, never persisted). Chat sets it before routing so pre-dispatch refusals stay correlated; assignNativeLogConversationId lets any later native id replace it — prepareResponsesRequest resolution and the Cursor conversation backfills in run-turn-execution.ts, sidecar-execution.ts and request-transport.ts — while a caller-owned pre-populated id (routed Claude) is kept. Covered by tests/usage/request-log-conversation.test.ts and tests/responses/chat-conversation-affinity.test.ts.

Chat streaming client with a JSON upstream result

The translated inbound path in src/server/chat-completions.ts may receive a complete JSON Responses result even when the Chat client requested SSE. Its synthetic stream reuses responsesJsonToChatCompletion as the semantic authority: converted text, reasoning, available refusal content, tool calls, finish reason, and usage must survive this final delivery conversion. Tool calls gain their array-order stream index; the stream retains one assistant-role frame, one terminal choice, and one [DONE]. Both native and translated JSON fallbacks share jsonCompletionSse; its temporary frame strings and final body ownership are charged to the existing translator budget. Known incomplete limits take precedence over tool finish reasons; "known" means the shared incomplete-reason vocabulary, and unmapped incomplete boundaries remain errors. The existing response-body lifecycle owns translation-budget release on consumption or cancellation. Actual upstream SSE and native Chat bypass this fallback.

Decision record: ADR-0062

Chat refusal projection

src/chat/outbound.ts keeps Responses refusal parts separate from ordinary content. JSON output and the stream collector expose nullable message.refusal; jsonCompletionSse preserves it as delta.refusal, while the native SSE relay remains opaque. The translated live stream keys refusal state by raw output_index / content_index, validates present item IDs as correlation constraints, and emits buffered parts in that order only at a valid completed/incomplete terminal. Deltas append; equal, empty, absent, and shorter-prefix snapshots preserve existing text; extending snapshots add only new text. Non-string or contradictory snapshots fail with a content-free typed error.

The existing turn budget accounts for refusal text and map metadata, including empty entries, and releases that state on terminal, failure, or cancellation. Pending role/tool/refusal/finish/[DONE] frames form one terminal batch: all serialized strings and encoded frames must be admitted before any batch frame is enqueued. Admission failure releases the batch and refusal state, cancels upstream, and emits only the bounded overflow error. Collector processing failures cancel their reader before releasing its lock, so upstream translation cannot continue after failed JSON collection. The outer response finalizer continues to own retained response bytes. These are projection rules, not new refusal policy or changes to ordinary content/tool semantics.

The shared Responses path follows the bounded multipart recovery contract; credential admission and retry policy remain unchanged.

MiniMax Anthropic-compatible clients

The MiniMax platform CLI's text resource posts Anthropic Messages to /anthropic/v1/messages. ocx mmx adapts that hard-coded client path with a temporary loopback bridge instead of adding another server route. The bridge accepts only POSTs to the messages and count-tokens paths, rewrites them to the existing /v1/messages data plane, preserves the query and streaming body, strips all incoming credential headers, and pins the public loopback placeholder. It stops as soon as the MMX child exits, so the server's AUTH_MATRIX and authentication surface remain unchanged.

ocx mmx exposes only the text resource because the other MMX resources use MiniMax-specific image, video, speech, music, vision, search, quota and file endpoints. The launcher isolates ~/.mmx credentials behind a temporary config, removes ambient proxy variables so loopback traffic cannot be sent off-machine, owns the temporary bridge lifecycle, and refuses destination, region and credential overrides. It is loopback-only because MMX cannot carry the dedicated remote-admission header. MiniMax Code uses the separate reversible custom_provider.opencodex file integration and is likewise loopback-only; its generated block never changes defaultModel. Each generated MCode model copies an authoritative catalog context window into limit.context and a nonempty canonical reasoning ladder into thinking.effortOptions. Missing capabilities stay absent instead of falling back to OpenCodex guesses, and the integration does not write the removed thinking.effort / defaultEffort fields because MCode owns the active effort per session.

Usage consumers preserve positive incomplete-history metadata as specified in usage accounting; readable totals are not represented as a complete ledger. Upstream API-key usage follows the physical-attempt account attribution contract, independently of subscription quota observations.

Connected CLI usage follows the client-scoped hub usage contract; local management and account data remain separate.

Remote Workspace uses a separate, explicitly enabled server surface with structural WebSocket callbacks and awaited per-server cleanup; its contract owns that integration.

Listener startup diagnostics follow the runtime lifecycle contract; malformed optional listener blocks follow config loading. Chat helper admission in src/server/responses/core.ts follows the deferred stored-main contract: only a needed Direct OpenAI helper claims stored main, after terminal vision, routed vision and search exclusions.

The management quota DTO keeps Combo editing aligned with scoped inference evidence; see Combo editor routing quota.

Codex pool settings and their consumers follow the reset-first ordering contract, including independent-quota fallback and preserved affinity.

Canonical Spark Lite metadata follows the final serialized model and surviving nonempty Lite tool catalog; see Responses transport.

Optional Codex transport-hint suppression is scoped to canonical Responses client output; its defaults and exclusions are owned by Responses transport.

The provider summary default applies at Responses ingress; native Chat and Anthropic inbound preferences keep their existing handling. Raw content is never renamed to a summary. See bridge contract.

Chat and Messages request-log correlation

The Chat Completions HTTP boundary in src/server/index/serve-options.ts publishes its generated request-history id as x-opencodex-request-id, exposed through CORS. JSON, SSE, handler errors and logged pre-handler refusals share the final history row's id; caller and upstream ids cannot replace it. Unlogged auth, origin, drain and active-turn refusals receive no correlation id. Stream headers identify the eventual row, not completed persistence. tests/responses/chat-request-id-endpoint.test.ts covers this boundary.

The Messages HTTP boundary in src/server/index/serve-options.ts publishes its generated request-log id in both request-id and x-opencodex-request-id. Native, translated, streamed, collected and logged refusal responses use the same id as the final request-history row. Stream headers precede final accounting; they identify the row owned by the turn, rather than attest to successful persistence. Authentication, origin, drain and active-turn rejections without a request-log owner, and count_tokens, receive no OCX correlation id.

src/server/messages-response-headers.ts retains only an upstream request-id matching req_[A-Za-z0-9_-]{1,128} on native delivered results and upstream HTTP errors. At the final HTTP boundary, src/server/index/startup-warnings.ts moves this diagnostic value into x-opencodex-upstream-request-id and assigns the OCX id to request-id. Missing or nonconforming upstream ids are omitted; translated adapter ids are not inferred. The standard/custom OCX headers and the optional upstream header are exposed through CORS alongside existing names. tests/claude-integration/messages-request-id-endpoint.test.ts covers the protocol boundary; tests/claude-integration/messages-request-id-headers.test.ts covers header and stream identity. Responses retains its existing custom-header contract.

Claude context rejection

Claude Messages preserves the classified context_length_exceeded error (src/claude/outbound.ts, src/protocols/encoders/messages.ts, src/server/claude-messages.ts): one streaming invalid_request_error terminal, or HTTP 400 without a retry hint when collected or failed-JSON. anthropicErrorBody words that envelope as Anthropic does (prompt is too long: ..., counts only when the upstream states both), because Claude Code compacts reactively only on that wording; a throughput limit the classifier files under that code (per minute, rate limit, quota, TPM) keeps its text. The native Messages lane (src/server/messages-native.ts) does the same for a configured provider's own 400/413 or streamed invalid_request_error refusal without changing the envelope shape; the stream rewrite sits after the log tap so stall timing still reads raw bytes, and Anthropic pools skip it. This adds no recovery send or context pruning; replay refusal and translation-buffer limits keep their handling. History: devlog/_plan/261009_claude_1m_default/010_prompt_too_long_envelope.md.

Claude affinity at final Go dispatch

src/server/claude-messages.ts carries validated conversation affinity privately through Responses options. Configured Go headers win; otherwise explicit session/thread identity, then an explicit Go header, then valid Claude metadata, then the original request-scoped allocation supplies the lane. src/server/responses/core.ts applies it only at the final canonical Go transport, including combo selection and failover. No Go-only replay header reaches non-Go destinations. Shared system cache keys never become conversation identity. The request-scoped fallback is stable across retries and distinct across client requests.

Claude metadata also supplies a private native-session value. Only the final canonical ChatGPT attempt receives it, in copied forwarding headers; an explicit underscore session, hyphenated session or thread header suppresses synthesis. Refresh and alternate-account retries retain that value. Original request headers stay unchanged so policy fallback cannot promote a generated native identifier into a noncanonical replay. Go preliminary selection does not suppress the final native affinity, and shared-system keys do not provide either conversation value.

Opt-in Claude instruction stabilization

src/claude/inbound.ts reads only literal claudeCode.stabilizePromptCache: true from its existing configuration argument. The default is off for every translated Messages caller. src/claude/inbound-cache-stabilize.ts peels only exact single-line trailing unfenced harness notices. Recognized <total_tokens>N tokens left</total_tokens> footers are dropped, including repeated footers; TaskCreate nudges retain their trailing user input message. A footer-only suffix adds no input item, so token footers do not introduce a user turn after a function output. Unmatched and fenced text is preserved, including an open fence through EOF. Native passthrough never enters this translator. Without opt-in the original system-parts cache-key derivation remains unchanged; with opt-in the metadata-less key uses stabilized instructions. Metadata-derived keys retain their existing derivation. This configuration changes prompt roles, not conversation identity, and cannot guarantee upstream cache reuse.

Instruction notice extraction scans fence ranges once and walks original lines backwards with a decreasing cursor. It accepts exactly one ASCII space inside the token notice, preserves unmatched prefix bytes, and does not repeatedly scan or copy shrinking prompt prefixes.

Claude skill marker path bound

src/claude/inbound.ts examines at most 4,097 characters after the skill base-directory marker when deciding whether to elide a large blocked skill bundle. A marker path longer than 4,096 characters passes through unchanged; a recognized bounded path retains the existing elision behavior. This bound applies to translated Claude Messages ingress.

Native Chat applies qualifying effort ceilings independently of model pins; pin selection precedes the cap and only pins or cap rewrites enter wire mapping. The catalog effort contract records the V1/compaction exemptions and caller-preservation boundary. Pool quota producers and account commands follow the bounded raw-observation contract, separate from the latest display snapshot and capacity estimates.

Account quota surfaces use safe probe diagnostics separately from quota validity, credential health and routing authority.

Live sideband admission and its bounded upstream handshake follow the runtime contract; the ordinary Responses WebSocket exchange remains separate.

Translated Chat request construction uses the inline-image budget; the shared normalizer counts retained bytes even when a wire-specific drop callback keeps the image attached, rejects inputs above the safe decoded-pixel ceiling, caps native decode work process-wide, and stops queued work when the request is cancelled.

The explicit model-capability contract preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior.

Provider-scoped approval reviewer settings are projected by the catalog owner; this surface retains its existing routing, transport and account-selection behavior.

Claude message threads on translated routes

Claude Code enables its message-threads beta only against first-party Anthropic, so it reaches OpenCodex through the first-party intercept. A threaded request carries a thread object; a continue sends only the messages after previous_message_id and may leave system and tools to the thread Anthropic stores. src/server/claude-messages.ts forwards the request unchanged on native passthrough. On the translated path, before compatibility analysis or inference, it answers any thread object with the 400 from src/claude/message-threads.ts, whose error.details.error_code is thread_unsupported_request and whose request-log error code is claude_thread_unsupported. A translated count_tokens request with a thread object gets the same 400, because counting the delta would undercount the conversation. Claude Code then resends the turn with the full conversation and keeps that model stateless for the session. Translating the delta instead would drop the task, instructions and earlier turns without an error.

Shared inbound Chat image recognition

src/chat/image-parts.ts owns which messages[].content[] shapes count as an image on the Chat Completions ingress: OpenAI image_url in both spellings, Pi/MCP {type:"image", data, mimeType}, and Anthropic-shaped {type:"image", source} in base64 and url form. The translator and the native fast path both read it, because they previously answered that question separately and disagreed: native route-eligibility matched only image_url, so a text-only routed model kept a Pi-shaped or Anthropic-shaped image body and the native whitelist passthrough forwarded the foreign part verbatim.

Normalization is copy-on-write and lazy: replacement arrays are allocated only once a part actually needs rewriting, so an ordinary text request walks the messages and allocates nothing.

Shape normalization alone does not make a tool-result image safe on the native fast path. A standard Chat role: "tool" message accepts a string or text parts, not image_url, so rewriting a Pi or Anthropic tool image still leaves an image part inside a tool message. chatBodyCarriesToolResultImage therefore makes such a request ineligible for the native shortcut, and the translated openai-chat adapter owns it — that adapter already collects tool-result images and flushes them into a following user carrier after the complete paired tool-result batch. Ordinary user images and text-only tool results keep the native fast path.

normalizeChatImageParts runs in handleChatCompletionsWithBudget immediately after routing-body validation and before routeModel, so the text-only diversion in isNativeChatRouteEligible and the forwarded native wire observe the same parts. It rewrites only recognized foreign parts into image_url form and returns its input by reference when nothing matched, so a body with no image — and one already in OpenAI shape — stays byte-identical. Sibling parts, message fields and top-level body fields are preserved; the native path is a whitelist passthrough, so an incidental deep clone would itself be a behavior change. A remote reference is recognized and rewritten, never fetched.

Translated Chat control fidelity

Translated Chat ingress does not reshape schemas for Google's endpoint-scoped loss report. The report is produced only at the final Google adapter boundary and does not alter the ingress body.

A translated Chat turn keeps the controls the caller sent. The Chat ingress pins store:false for every openai-responses route and strips nothing else: the sampling and output-cap restrictions that the canonical ChatGPT backend requires are applied at the final outgoing body in src/adapters/openai-responses.ts, gated on isCanonicalOpenAiForwardProvider, which additionally requires authMode: "forward" and the canonical base URL.

Deciding at the ingress was wrong on two axes. Seven providers share the openai-responses adapter string, so a generic key gateway lost controls it accepts; and settledRoute is the ingress-time route, while a combo or policy route resolves its concrete child later, so the decision preceded knowledge of the real target in both directions. stripCanonicalForwardSamplingParams returns a copy and no-ops when none of its keys are present, so _rawBody stays caller-owned. The separate forward-wide max_output_tokens/metadata sanitizer is unchanged.

An assistant turn's reasoning_content or reasoning_details is carried into the projection as a reasoning input item emitted immediately before its assistant message, matching the parser's buffer-and-prepend adjacency. Only representable plaintext crosses: no signature, encrypted payload or provider item id is reconstructed, because those attest to content this proxy never received. Opaque reasoning replay across a Chat boundary has exactly one exception: Anthropic thinking signatures carried in reasoning_details (#7429), documented in chat-reasoning-details.md. presence_penalty and frequency_penalty are carried too; per-model noPenaltyModels opt-outs still apply at the adapter.

Explicit reasoning disable on the Chat ingress

The Chat inbound effort allowlist accepts none alongside the ladder values. none is the runtime's disable sentinel — src/reasoning-effort.ts maps it to omitting the wire parameter, and the Pi client export maps Pi's off thinking level onto it. Dropping it let a provider default re-enable reasoning the caller had explicitly turned off, which is not neutral for the Anthropic families that think by default and require an explicit thinking:{type:"disabled"} to stop.

Media at the Chat translation boundary

The native Chat path retains provider-native file/audio blocks. When a request instead needs Chat-to-Responses projection, src/chat/inbound.ts rejects recognized audio/file content before it can become empty text. The one exception is a file part carrying inline base64 bytes in a user message: that projection builds an input_file block and the bytes survive to any wire with a counterpart. src/responses/inline-document.ts checks the base64 alphabet and quantum/padding lengths without decoding the payload; valid unpadded bytes remain valid, while malformed one-character or incomplete padded encodings follow the explicit refusal. The same part in a system, developer, assistant or tool message is still refused, because those branches flatten their content to a string. Legacy function-role images also return an explicit error; their call/result pairing is not implemented by this projection. Modern tool images continue through the existing following-user carrier. These errors state an OpenCodex conversion limit, not a provider capability claim. Final Responses-to-adapter admission follows the registry contract. The opposite Responses-to-Chat direction supports inline WAV/MP3 audio under the same contract; this Chat-to-Responses projection still has no lossless audio carrier. Canonical Responses identity sanitation and narrowly scoped pre-output combo recovery follow request-local target compatibility; other adapter contracts remain unchanged.

Shared response-log retention and native SSE inspection pacing follow the bounded inspection contract; other subsystem behavior remains unchanged.

Native steering retains fixed phase deadlines and reconciled replay output; see the steering stability contract.

Native steering generation overrides, explicit public-API eligibility and the consent-gated wire probe follow the shared control contract; this owner does not change routing or execute diagnostic tools.

Unicode pattern normalization uses copy-on-write traversal while preserving the existing schema and wire semantics.

Dashboard Fast-row persistence and client refresh follow the Fast selector rows setting contract.

The compaction routing override requires original Responses ingress; translated Chat and Messages calls retain their own routing.

Native Messages and translated Anthropic requests share revoked-token recovery; only pre-output recovery may send a sibling.

Managed native Anthropic OAuth metadata follows the native Messages binding contract: the serving credential's provider UUID replaces only recognized account metadata, with each attempt rebuilt from the source.

Managed native Messages retain a coherent observed CLI identity bundle only for first-party Anthropic; native Messages owns its bounds and credential separation. Header identity never selects an account or authorizes a request.

Native Chat thinking continuation

src/adapters/openai-chat/reasoning-replay.ts repairs assistant history missing reasoning_content before native serialization. Caller-supplied nonempty text wins. The native lane honors the same preserve/placeholder policy as translated Chat; absent preserve declarations recognize DeepSeek Flash/Pro families and Reflection/Stealth families (isReflectionThinkingModel). Explicit preserve lists (including []) remain authoritative, and an explicit placeholder list can disable fabricated placeholders while allowing original cached reasoning. While DeepSeek models require reasoning replay for tool-enabled requests, Reflection and Stealth families require continuous reasoning replay across all prior assistant turns even when tools are absent (hasTools: false), synthesizing the whitespace placeholder [" "] when placeholder is enabled. Upstream reasoning capture in src/server/chat-native-reasoning.ts extracts thinking from both reasoning_content and reasoning across SSE stream deltas and buffered choices. In src/adapters/openai-chat/passthrough.ts, passthrough body sizing measures baseBytes unconditionally against maxSerializedBytes - replayPlan.serializedBytes via jsonUtf8Bytes(transformedBody(), ...). For changed replays, it reserves baseBytes + replayPlan.serializedBytes as transient request_copies against the remaining translator budget before serializing the body, releasing the reservation in finally. Exceeding either limit returns HTTP 413 translation_buffer_limit (TranslatorBudgetExceededError("request_copies", maxSerializedBytes)); history is never silently truncated. Key rotation and retry rebuild and charge the request after releasing the previous body, preserving scope and normal replay behavior within budget. Model-contract evidence and compatibility policy: Verification date: 2026-10-25. The Reflection/Stealth family default is a defensive compatibility fallback policy based on observed vendor behavior: live testing against Vercel AI Gateway (reflection-beam, 2026-10-23) observed rejection of tool-free multi-turn requests with HTTP 400. Because this destination-specific observation is not a vendor guarantee across all possible stealth or reflection hosts, OpenCodex applies this heuristic as a fallback policy while providing explicit operator overrides via preserveReasoningContentModels and requiresReasoningPlaceholderModels. Continuous CI verification exercises hermetic local fixtures in tests/responses/chat-native-reasoning-scope.test.ts (pinning multi-turn non-tool replay, reasoning delta capture, and passthrough size checks) without live network requests.

src/server/chat-native-reasoning.ts records upstream JSON and fragmented SSE reasoning under all parallel call ids, separately for each choice. Replay requires a sanitized thread-id, session_id or session-id and the existing exact provider/destination/adapter/model/credential scope. The native send rebinds that scope before every request build, including key rotation. Missing identity, a restarted/expired/evicted cache, and another route receive only the configured placeholder; they never recover another conversation's raw text. SSE capture retains at most 256 KiB and 64 calls within the translator budget and releases on terminal/error/cancellation. Unfinished tool streams cannot seed replay. This leaves the native wire and response fields intact.

Codex runtime agent assertions

Authorization scheme parsing lives in src/lib/authorization-scheme.ts, and every former replace(/^Bearer\s+/i, "") read uses it. Codex 0.164 runtime agent assertions add a value that is neither an admission secret nor a caller credential. Authorization: AgentAssertion (assertion_only) is never a bearer, never admitted, and never forwarded: the request fetch handler refuses it with agent_assertion_unsupported before admission or routing, and Responses auth plus materializeCodexUpstreamAuth refuse it again so no pool or substituted credential can stand in for it. The dual_header x-openai-agent-assertion is outside FORWARD_HEADERS and blocked from forwardClientHeaders. It is carried only for canonical-forward Direct when the outbound Bearer is the caller's original one, and is dropped under a pool override. Coverage: tests/server/agent-assertion-auth.test.ts and tests/responses/agent-assertion-routes.test.ts.