Skip to content

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

Closed

Description

@luvs01

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.

Activity

  1. github-actions commented on Oct 21, 2026

    @github-actions
    Contributor

    Automated translation bookkeeping — detected language: English.

  2. Ingwannu commented on Oct 21, 2026

    @Ingwannu
    Owner

    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 its handshake event before open. I am preparing a narrowly scoped regression fix for the actual upstream x-reasoning-included signal 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.

  3. Ingwannu commented on Oct 21, 2026

    @Ingwannu
    Owner

    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.

  4. Ingwannu commented on Oct 21, 2026

    @Ingwannu
    Owner

    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.

  5. luvs01 commented on Oct 22, 2026

    @luvs01
    CollaboratorAuthor

    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:

    • Whether duplicate x-reasoning-included upgrade 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).
    • Whether to add an explicit regression test for caller-mutated header snapshots, session reuse isolation, and cleanup on disposal.
    • Whether a short ADR documenting the protocol decision would help future maintenance.

    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.

  6. luvs01 commented on Oct 22, 2026

    @luvs01
    CollaboratorAuthor

    Runtime follow-up on OpenCodex 2.90.0, which includes #7464 (845ac6b875db).

    • Nine fresh synthetic generation requests completed across direct native WebSocket, OpenCodex HTTP/SSE, and OpenCodex WebSocket. None carried x-reasoning-included in 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.
    • OpenAI's #55991 now explicitly accounts for retained reasoning in Responses Lite usage without that header. It is included in 0.164.0-alpha.10, but not the tested Desktop engine 0.163.0-alpha.11.
    • The official alpha.10 binary passed the continuous-session accounting comparison. A separate, controlled app-server restart followed by thread/resume still 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.

  7. Ingwannu commented on Oct 22, 2026

    @Ingwannu
    Owner

    Verified the integration/release boundary: #7464 merged as 845ac6b875db3b8f4d4ea0ab70e6b80d4dc6e4c0, and the published v2.90.0 tag (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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingstreamingSSE, WebSocket, terminal stream frames

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions