WebSocket relay drops upgrade-only x-reasoning-included response header
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 45/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- bun, typescript
Research direction
Read src/server/responses/codex-ws-session.ts, codex-ws-metadata.ts, and codex-ws-exchange.ts to trace how upgrade headers and metadata frames reach the committed response headers. Start by reproducing the loopback handshake case in the issue, then inspect Bun’s handshake event and the existing filtering and metadata precedence. Done means the upgrade-only header reaches the downstream response while the metadata-order controls, credential filtering, and downstream client-WebSocket lifecycle remain correct.
Written by the indexing model from the issue text.
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.
- Dominant language
- TypeScript
- Stars
- 16.9k
- Forks
- 1.3k
- Avg merge
- 7h 50m
- Merged PRs (30d)
- 645
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from lidge-jun/opencodex
-
[Bug]: Business Premium seat plan self_serve_business_prolite is counted at baseline capacity weight 1 instead of 5Possibly taken A pull request linked to this issue is open or already merged. Openaccount-pool bug
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
lidge-jun/opencodex#7440 · 2 comments ·
Maintainers usually reply within 1 day
-
[Docs]: translate the #7395 OpenGateway and Mistral provider-guide sectionsPossibly taken @MannXo claimed this 2 days ago. Opendocumentation
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
lidge-jun/opencodex#7405 · 3 comments ·
Maintainers usually reply within 1 day
-
[Bug]: resolveCodexRuntime test depends on the host's installed CodexPossibly taken A pull request linked to this issue is open or already merged. Openbug cli
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
lidge-jun/opencodex#7403 · 2 comments ·
Maintainers usually reply within 1 day
-
account-pool enhancement needs-design proxy
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
lidge-jun/opencodex#6737 · 1 comment ·
Maintainers usually reply within 1 day
-
[Bug] Responses combo treats completed text as success despite tool_choice=required (NVIDIA target)Possibly taken A pull request linked to this issue is open or already merged. Openbug tools
Difficulty 5/5 Over a week Newbie friendliness 30/100
lidge-jun/opencodex#7456 · 7 comments ·
Maintainers usually reply within 1 day
All issues in lidge-jun/opencodex
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
phivk/rubiks-lab#33 ·
Maintainers usually reply within 1 day
-
BUG:Edit Album strips spaces from the password so albums with a leading/trailing space in the password can't be editedPossibly taken @ManyaJain111 claimed this today. Openbug enhancement possible-duplicate
Difficulty 2/5 1-3 hours Newbie friendliness 77/100
AOSSIE-Org/PictoPy#1637 · 4 comments ·
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
chaitin/MonkeyCode#1437 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
area:cards enhancement migration ready-for-agent
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
fil-donadoni/tolaria#6309 ·
Maintainers usually reply within 1 day