Repository navigation
Description
Client or integration
Codex App
Area
Streaming
Summary
The native upstream WebSocket relay drops x-reasoning-included when it is present only in the HTTP 101 upgrade response. The same value survives when supplied in a codex.response.metadata frame before the first Responses event. A metadata frame arriving later remains in the SSE body but cannot update the already committed HTTP headers.
This is a reproduced transport compatibility gap. It is not established as the cause of the observed early-compaction incident: separate completed native generations, without OpenCodex, also omitted the flag while including old reasoning in reported input usage. That client/server accounting issue is already tracked in openai/codex#53913.
Reproduction
- Run a loopback WebSocket upstream under Bun 1.4.2. Set
headers: { "x-reasoning-included": "true" }onserver.upgrade(). - After receiving
response.create, sendresponse.created, followed by a successful terminal response with usage. Do not duplicate the header into a metadata frame. - Connect through the production
CodexWsSession, reserve the session, and invokecodexWsExchangewith the canonical Responses URL and a request produced byprepareCodexWsRequest. - Inspect the returned
Response.headersand consume its SSE body. - As controls, move the flag to a metadata frame before/after
response.created, then omit it altogether.
The isolated test used the release-identical runtime import closure, a real Bun.serve loopback server and real WebSocket clients. An independent raw TCP handshake verified that the first case's 101 actually contained the header. All four cases completed; no HTTP fallback ran, and the sessions/server were closed.
| Upstream signal | Downstream SSE HTTP header | SSE metadata event |
|---|---|---|
| HTTP 101 only | Missing | None |
| Metadata before first Responses event | Preserved | Preserved |
| Metadata after first Responses event | Missing | Preserved |
| No signal | Missing, as expected | None |
The terminal usage value was preserved in every case. The issue is the inclusion signal, not a demonstrated usage-number rewrite.
Minimal loopback reproducer (save as .tmp/reasoning-header.ts in the checkout)
Run bun .tmp/reasoning-header.ts. No credentials or real provider calls are needed.
import { CodexWsSession } from "../src/server/responses/codex-ws-session";
import { codexWsExchange } from "../src/server/responses/codex-ws-exchange";
import { prepareCodexWsRequest, CODEX_RESPONSES_HTTP_URL as url }
from "../src/server/responses/codex-ws-request";
const server = Bun.serve({
hostname: "127.0.0.1", port: 0,
fetch(req, server) {
if (server.upgrade(req, { headers: { "x-reasoning-included": "true" } })) return;
return new Response(null, { status: 400 });
},
websocket: {
message(ws) {
ws.send(JSON.stringify({ type: "response.created",
response: { id: "r1", status: "in_progress", output: [] } }));
ws.send(JSON.stringify({ type: "response.done", response: {
id: "r1", status: "completed", output: [],
usage: { input_tokens: 3, output_tokens: 4, total_tokens: 7 },
} }));
},
},
});
const init = { method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ model: "gpt-6-astra", stream: true, input: [] }) };
const prepared = prepareCodexWsRequest(url, init)!;
const session = new CodexWsSession(`ws://127.0.0.1:${server.port}`, prepared.headers);
session.reserve();
let timer: ReturnType<typeof setTimeout>;
try {
const response = await Promise.race([
codexWsExchange({ session, url, init, prepared, bunVersion: Bun.version,
sseFallback: async () => { throw new Error("unexpected HTTP fallback"); } }),
new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new Error("deadline exceeded")), 5000);
}),
]);
console.log("reasoning header:", response.headers.get("x-reasoning-included"));
console.log("completed:", (await response.text()).includes("response.completed"));
// v2.89.0: null / true. Expected: true / true.
} finally {
clearTimeout(timer!);
session.dispose();
await server.stop(true);
}This is the reduced handshake-only case; the four-case fixture additionally
checked the raw 101 on the wire and both metadata-order controls.
Version
OpenCodex 2.89.0, release commit dd2bdd2fe3ff6fc2d75019c8b7dc201b835b4109; Bun 1.4.2. The relevant files also match current dev commit 12025981e5a304cdb6b1b54f525ba8554f623977.
Operating system
Windows 11 Pro, build 26300, x64
Provider and model
OpenAI native Responses transport. The transport reproduction uses only a loopback mock; the separate live observation used gpt-6-astra.
Logs or error output
handshake-only: raw 101 header=true, downstream header=false, completed=true
prelude-metadata: downstream header=true, completed=true
late-metadata: downstream header=false, metadata event preserved=true, completed=true
absent-control: downstream header=false, completed=true
fallback calls=0; non-loopback calls in transport fixture=0
Implementation pointers
CodexWsSessionobserves open/message/close/error but not the upgrade response.CodexWsMetadataobtains headers from metadata frames;commitResponseincodex-ws-exchange.tsfreezes their snapshot.- Codex
0.163.0-alpha.10.2reads this flag from the HTTP response headers or WebSocket upgrade headers. Preserving it only as a later control frame does not set its accounting flag. - Bun 1.4.2 has a
handshakeextension event used by its built-inwsshim. Capturing and validating actual upgrade headers may avoid replacing the whole transport. Reuse must retain connection-scoped headers, filtering must continue to exclude credentials/cookies, and metadata precedence needs explicit tests.
The downstream client-WebSocket case is a separate lifecycle constraint: its upgrade is already complete before an upstream response is available. An upstream-header capture alone would not solve that case. Do not synthesize x-reasoning-included merely from a model name or configuration.
Accounting compatibility option to evaluate separately
An opt-in correction for the native HTTP/SSE lane could potentially supply the missing inclusion signal when an authoritative response/accounting contract establishes that old reasoning is included. Replayed encrypted input alone is not sufficient: a backend can discard incompatible or unused reasoning. Our direct native A/B demonstrates inclusion for the tested Astra/Lite/all-turns route, not every route.
There is also a client lifecycle constraint: this Codex version treats header presence as true and latches it on the session; emitting false does not revoke it. A correction must therefore account for later model/provider changes and fallback, not merely the response being fixed. Until that contract is bounded, globally synthesizing the flag would exchange premature compaction for possible under-counting. The native client issue remains the appropriate place to repair usage/flag pairing across turns and resume.
Redacted configuration
Native openai-responses provider; canonical ChatGPT backend URL; upstream WebSocket enabled. No credentials or account-specific configuration are required for the mock reproduction.
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
github-actions commentedon Oct 21, 2026
Automated translation bookkeeping — detected language: English.
Ingwannu commentedon Oct 21, 2026
Confirmed the capture gap against current
dev(12025981e5a3). An independent loopback probe on Bun 1.4.2 and 1.4.3 also confirms that the native client emits the 101 headers through itshandshakeevent beforeopen. I am preparing a narrowly scoped regression fix for the actual upstreamx-reasoning-includedsignal on the WS-to-HTTP/SSE path, reusing the existing bounded metadata owner. Missing signals will remain missing; this does not establish the cause of the early-compaction incident, synthesize reasoning accounting, or retrofit an already-committed client WebSocket upgrade.Ingwannu commentedon Oct 21, 2026
Draft fix: #7465 at
3ee3537d398c98a1d9f612aa9a4d4ecc136d0fd1. The real Bun 1.4.2 regression fails before (2 pass / 4 fail for the original six cases) and passes after. Final production-runtime transport coverage: 232 passed, 1 intentional older-runtime-only skip, 0 failed; Bun 1.4.3 plus layout/ratchet coverage: 52 passed. Structure/privacy gates and the 585-page docs build pass. The PR carries only an observed connection reasoning signal, preserves frame precedence and usage totals, and documents the native client-upgrade/accounting limitations. Draft pending exact-head hosted typecheck/tests and independent review; this issue remains open until verified integration.Ingwannu commentedon Oct 21, 2026
Coordination update: #7464 was opened by the contributor while my independent implementation was in progress. I independently validated its exact head
4e9212662e3e173ab7e5372b712da6c1fdecd54d(250 passed / 1 intentional older-runtime-only skip on Bun 1.4.2; 64 passed on Bun 1.4.3 including layout/ratchet). I closed my redundant #7465 in favor of #7464 as the single implementation under review. Nothing is merged/released yet; the narrow header-preservation scope and accounting/native-client-upgrade limitations remain.luvs01 commentedon Oct 22, 2026
Post-merge follow-up note: #7464 has now been merged as-is. The optional design considerations discussed in this PR comment were not incorporated before merge.
For future consideration only, not as blockers or a request to undo the fix:
x-reasoning-includedupgrade headers should be treated as ambiguous and omitted, rather than forwarded as a comma-combined value (as in the alternative fix(responses): preserve WS upgrade reasoning signal #7465).The merged
seed()path and stricter handshake/hop-by-hop validation have clear advantages and need not change for these items. I defer to the maintainers' preferred contract and priorities; no immediate additional change is requested. The separate native accounting/early-compaction question remains outside the scope of this merged relay fix.luvs01 commentedon Oct 22, 2026
Runtime follow-up on OpenCodex 2.90.0, which includes #7464 (
845ac6b875db).x-reasoning-includedin the response headers or observed metadata frames. Replaying compatible old reasoning increased reported input usage on all three routes. The direct connection also lacked the signal, so these observations do not indicate a remaining relay-header loss.0.164.0-alpha.10, but not the tested Desktop engine0.163.0-alpha.11.thread/resumestill triggered premature compaction: reported usage 127, limit 200, same prompts/catalog/mock responses; the uninterrupted control did not compact. The native follow-up report contains reproduction details, limitations, and a proposal to persist accounting semantics with the usage snapshot.The upgrade-header preservation fix tracked here is merged and released. The remaining reproduced resume problem belongs to native client accounting and does not change #7464's scope. Current evidence does not call for another OpenCodex patch or synthesizing an inclusion header; usage figures were left unchanged throughout testing.
Ingwannu commentedon Oct 22, 2026
Verified the integration/release boundary: #7464 merged as
845ac6b875db3b8f4d4ea0ab70e6b80d4dc6e4c0, and the publishedv2.90.0tag (994d15e8a3d071e231ff0de41eab9dfbee8817aa) contains that commit. Closing this narrowly scoped upgrade-header preservation issue as completed.The follow-up observations are appropriately separate: no upstream signal means none should be synthesized, and native retained-reasoning accounting / restart-resume compaction is not certified fixed by this relay change. The optional post-merge suggestions do not identify an outstanding blocker or require another OpenCodex patch.