Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

WebSocket relay drops upgrade-only x-reasoning-included response header

Open
#7,462 4 comments 0 reactions 0 assignees View on GitHub

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
Domain
api, backend

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

bug streaming
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
  1. Run a loopback WebSocket upstream under Bun 1.4.2. Set headers: { "x-reasoning-included": "true" } on server.upgrade().
  2. After receiving response.create, send response.created, followed by a successful terminal response with usage. Do not duplicate the header into a metadata frame.
  3. Connect through the production CodexWsSession, reserve the session, and invoke codexWsExchange with the canonical Responses URL and a request produced by prepareCodexWsRequest.
  4. Inspect the returned Response.headers and consume its SSE body.
  5. 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
  • CodexWsSession observes open/message/close/error but not the upgrade response.
  • CodexWsMetadata obtains headers from metadata frames; commitResponse in codex-ws-exchange.ts freezes their snapshot.
  • Codex 0.163.0-alpha.10.2 reads 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 handshake extension event used by its built-in ws shim. 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from lidge-jun/opencodex

All issues in lidge-jun/opencodex

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.