Skip to content

bug(proxy): switching Fast mode on a live WebSocket reuses a stale upstream routing contract #2000

Description

@dianshili

Pre-flight checklist

codex-lb version

1.24.0-beta.4

Deployment method

Local macOS LaunchAgent, bound to 127.0.0.1:2455, using the SQLite store.

Client used against codex-lb

  • Codex Desktop / Codex CLI 0.151.0-alpha.7.1
  • Responses WebSocket transport

ChatGPT account plan(s) involved

Plus accounts. A successful native Fast control using the same account class is documented in #1454.

Model(s) involved

gpt-5.6-sol (the reuse defect is model-independent)

Summary

When a Codex task starts in Standard mode and Fast mode is enabled while the downstream Responses WebSocket remains open, subsequent response.create frames correctly contain service_tier: "priority", but codex-lb continues using the already-open upstream WebSocket created under the old routing contract.

The request succeeds, but upstream returns response.completed.response.service_tier = "default". This is a silent downgrade, not a request failure.

This is narrower than #1454:

What happened?

In a controlled long-lived task:

Interval Requested tier Completed actual tier
Before Fast was enabled omitted default
After Fast was enabled priority in 138/138 requests default in 138/138 requests

The database and completed-event telemetry agree: the Fast intent reached codex-lb, but no request in that interval was granted priority.

Across the current local history there are 9,969 requests with requested_service_tier=priority and zero with actual_service_tier=priority. That aggregate also contains the broader fresh-connection problem from #1454; the 138-request interval above is the bounded reproduction for this report.

Code-path diagnosis

The relevant implementation is app/modules/proxy/_service/websocket/mixin.py in 1.24.0-beta.4:

  1. Downstream WebSocket handshake headers are captured once when the connection starts (around line 1356).
  2. The implementation has explicit upstream-reuse guards for model-source compatibility (around line 1772) and response ownership (around line 2184).
  3. The upstream WebSocket is opened only when upstream is None (around line 2356).
  4. Later response.create frames can carry a different service_tier, but no equivalent reuse guard compares the effective tier, routing hint, or attestation/installation identity against the active upstream connection.

This produces an internally inconsistent request:

response.create body: service_tier=priority
active upstream WS: handshake created under the previous/default routing contract
reuse decision: connection is open, so it is retained
completed result: service_tier=default

The native Codex client sends x-codex-routing-hint: model=<model>;tier=priority on the upstream WebSocket handshake. OpenAI PR openai/codex#37345 describes that header as carrying the model and tier to the Codex backend, including for WebSocket handshakes and prewarm.

Therefore the effective service tier is part of the connection-level routing contract, not merely a per-frame body field.

Interaction with PR #1995

PR #1995 adds native Codex traffic parity, but its current patch classifies x-codex-routing-hint as hop-local and removes it from both HTTP and WebSocket upstream traffic (app/core/clients/proxy.py, IGNORE_INBOUND_HEADERS). Its unit test explicitly expects that header to be absent upstream.

That conflicts with openai/codex#37345. Unless codex-lb consumes the incoming hint and recreates an equivalent server-facing routing decision, stripping it can preserve or worsen the Fast downgrade.

Also, the fast-canary in #1995 means a quick canary; it does not assert that a Fast request receives response.completed.response.service_tier=priority. The B/C comparator can pass when both proxied captures return default, while the native A capture returning priority is informational.

Steps to reproduce

  1. Run codex-lb 1.24.0-beta.4 with Responses WebSocket transport enabled.
  2. Point Codex Desktop or CLI at http://127.0.0.1:2455/backend-api/codex.
  3. Start a task in Standard mode and submit one turn so the downstream and upstream WebSockets are established.
  4. Enable Fast mode without restarting the task or connection.
  5. Submit one or more turns.
  6. Inspect the outbound response.create and the completed event/request log.
  7. Observe requested_service_tier=priority, no new upstream handshake attributable to the tier change, and actual_service_tier=default.

Expected behavior

Changing the effective service tier on a live downstream WebSocket should not reuse an upstream connection carrying a stale routing contract.

At minimum:

  • the active upstream connection contract should include endpoint, model, effective service tier, routing hint, selected account, and installation identity;
  • changing any routing-relevant field should trigger a fresh upstream handshake;
  • the new handshake should preserve or reconstruct the correct x-codex-routing-hint;
  • tests should cover Standard → Fast and Fast → Standard on the same downstream connection;
  • telemetry should warn when requested_service_tier=priority completes as a different actual tier.

Relevant sanitized evidence

bounded task interval:
requested priority: 138
actual priority:       0
actual default:      138

local aggregate:
requested priority: 9,969
actual priority:         0

No tokens, full account IDs, installation IDs, email addresses, request IDs, or conversation contents are included here. I can provide a targeted sanitized connection timeline or run a maintainer-provided probe if needed.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions