feat(proxy): add responses websocket fallback support - #1100
Closed
ding113 wants to merge 7 commits into
Closed
Conversation
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
/v1/responsesWebSocket runtime support via the standalone Node upgrade wrapper while preserving ordinary HTTP/v1/responsesbehavior.enableOpenAIResponsesWebSocketsetting with admin UI/i18n and optional provider diagnostics metadata.Problem
Codex CLI and other OpenAI-compatible clients are transitioning to WebSocket transport for the
/v1/responsesendpoint for real-time streaming. The proxy previously only supported HTTP/SSE transport for the Responses API, requiring all clients to use HTTP regardless of transport capability.Related Issues:
Related PRs:
src/server/pipeline, FIFO queueing, and decision-chain observabilitySolution
Architecture
The implementation uses a standalone Node.js HTTP upgrade wrapper (
scripts/responses-websocket-standalone-server.ts) that intercepts WebSocket upgrade requests on/v1/responseswhile forwarding all other requests to the Next.js server. The pipeline consists of:responses-websocket-runtime.ts) - Manages WebSocket server lifecycle, connection acceptance, and graceful shutdownresponses-websocket-protocol.ts) - Frame validation, message parsing, and WebSocket protocol handlingresponses-websocket-session-state.ts) - Per-socket FIFO queue with socket-scoped cache boundaries (store=false)responses-websocket-proxy-executor.ts) - Bridges WS frames to the existing HTTP guard pipeline via loopbackresponses-websocket-upstream-adapter.ts) - Codex-only upstream WS connection with configurable timeoutsresponses-websocket-fallback-bridge.ts) - Graceful HTTP/SSE/non-stream fallback when upstream WS is unavailableKey Design Decisions
Changes
Core Changes (6 new files, ~3,032 lines)
src/server/responses-websocket-upstream-adapter.ts(+884) - Outbound WebSocket adapter with timeout management and event collectionsrc/server/responses-websocket-proxy-executor.ts(+673) - Proxy execution bridge between WS ingress and HTTP guard pipelinesrc/server/responses-websocket-protocol.ts(+589) - Frame validation and WebSocket protocol handlingsrc/server/responses-websocket-runtime.ts(+414) - WebSocket server runtime and connection managementsrc/server/responses-websocket-session-state.ts(+291) - Per-socket FIFO queue and session statesrc/server/responses-websocket-fallback-bridge.ts(+171) - HTTP/SSE/non-stream fallback bridgingDatabase / Schema
drizzle/0099_violet_richard_fisk.sql- Migration:ALTER TABLE "system_settings" ADD COLUMN "enable_openai_responses_websocket" boolean DEFAULT true NOT NULLsrc/drizzle/schema.ts- Schema definition for new columnSettings UI & i18n
src/app/[locale]/settings/config/_components/system-settings-form.tsx- Toggle in System Settings formmessages/{en,ja,ru,zh-CN,zh-TW}/settings/config.json- i18n strings for all 5 languagesProvider Testing
src/lib/provider-testing/responses-websocket-probe.ts(+130) - WebSocket capability probing with transport/handshake metadatasrc/lib/provider-testing/test-service.ts- Extended with WS test supportsrc/lib/provider-testing/types.ts- New WS-specific typesSupporting Changes
scripts/responses-websocket-standalone-server.ts- Standalone server entry point for WS upgradescripts/smoke-responses-websocket-runtime.ts- Smoke test scriptscripts/copy-version-to-standalone.cjs- Extended to copy WS server assetssrc/app/v1/_lib/proxy/session.ts- Session context for WS transportsrc/repository/system-config.ts- Config persistence for new settingsrc/types/system-config.ts,src/types/message.ts- Type definitionssrc/lib/config/system-settings-cache.ts- Cache support for new settingBreaking Changes
None. This is a fully backwards-compatible feature:
true(enabled) but existing HTTP/v1/responsesbehavior is preserved as fallbackVerification
timeout 420s bun run build-> exit 0; standalone WebSocket wrapper installed.timeout 240s bun run lint-> exit 0; 8 existing warnings / 3 infos remain.timeout 240s bun run lint:fix-> exit 0; no safe fixes applied.timeout 240s bun run typecheck-> exit 0.timeout 420s bun run test-> exit 0; 558 files passed, 2 skipped; 5130 tests passed, 13 skipped.Testing
Automated Tests (13 new test files, ~3,852 lines)
tests/unit/runtime/responses-websocket-runtime.test.ts(666 lines) - Runtime lifecycletests/unit/runtime/responses-websocket-upstream-adapter.test.ts(795 lines) - Upstream WS adaptertests/unit/runtime/responses-websocket-proxy-executor.test.ts(364 lines) - Proxy executortests/unit/runtime/responses-websocket-session-queue-cache.test.ts(324 lines) - Session queue/cachetests/unit/runtime/responses-websocket-codex-session-continuity.test.ts(361 lines) - Multi-turn continuitytests/unit/runtime/responses-websocket-contract.test.ts(238 lines) - Protocol contracttests/unit/runtime/responses-websocket-decision-chain-observability.test.ts(196 lines) - Observabilitytests/unit/runtime/responses-websocket-fallback-bridge.test.ts(148 lines) - Fallback bridgingtests/unit/runtime/responses-websocket-inbound-handler.test.ts(133 lines) - Inbound frame handlingtests/unit/runtime/responses-websocket-inbound-errors.test.ts(74 lines) - Error scenariostests/unit/actions/system-config-openai-responses-websocket-setting.test.ts(200 lines) - Config persistencetests/unit/provider-testing-test-service.test.ts(164 lines) - Provider testing WS supporttests/unit/settings/system-settings-form-openai-responses-websocket.test.tsx(188 lines) - UI toggleManual Testing
enableOpenAIResponsesWebSocketin Settings UI, verify persistence/v1/responsesPOST route unaffected when toggle is on or offDecision-chain / fallback notes
clientTransport, upstream WS attempt/connection, downgrade status/reason, queue wait, cache hit, and unsupported-cache hits.UI evidence
Notes
claude-code-hub-responses-websocket-support..sisyphus/evidence/*was used for local verification evidence and is intentionally not included in this PR.Description enhanced by Claude AI
Greptile Summary
This PR adds WebSocket transport support for
/v1/responsesvia a standalone Node.js upgrade-interceptor wrapper, a six-file pipeline (runtime → protocol → session state → proxy executor → upstream adapter → fallback bridge), a newenableOpenAIResponsesWebSocketsystem setting, and provider-testing WS capability probing. The implementation is architecturally complete with comprehensive test coverage and backward-compatible HTTP fallback.NodeWebSocketConnection.readFrame()has no abort-signal listener; once a WS handshake succeeds theAbortControllerused for the probe deadline fires but never breaks out of the frame-reading loop, so a silent upstream causesrunDefaultResponsesWebSocketProbeto block indefinitely.isWebSocketDecision || …short-circuits the existingid + reason + attemptNumberdeduplication guard; every inbound frame appends a new chain entry, growingproviderChainat O(N) requests per session before serialisation to the database.Confidence Score: 4/5
Safe to merge with one functional issue to address: the probe timeout is a no-op after WS handshake succeeds, which can cause provider tests to hang indefinitely.
One P1 (probe abort signal not wired into readFrame) caps the score at 4. The P2 (provider chain deduplication bypass) is a quality concern with no immediate data integrity impact. The core proxy path is well-tested and architecturally sound. Existing HTTP behaviour is fully preserved.
src/server/responses-websocket-upstream-adapter.ts (readFrame abort support), src/lib/provider-testing/responses-websocket-probe.ts (timeout propagation), src/app/v1/_lib/proxy/session.ts (deduplication bypass)
Important Files Changed
Sequence Diagram
sequenceDiagram participant C as Codex CLI (WS) participant RT as responses-websocket-runtime participant PH as ResponsesWebSocketInboundHandler participant PE as responses-websocket-proxy-executor participant UA as responses-websocket-upstream-adapter participant US as Upstream Provider (WS) participant FB as responses-websocket-fallback-bridge participant HTTP as Upstream Provider (HTTP/SSE) C->>RT: GET /v1/responses (WS upgrade) RT->>RT: RFC 6455 handshake + accept C->>RT: text frame (response.create JSON) RT->>PH: handleFrame(frame) PH->>PH: FIFO queue enqueue PH->>PE: executor(input) PE->>PE: GuardPipeline (auth/rate-limit) PE->>UA: createResponsesWebSocketUpstreamEventStream alt Codex provider + WS enabled + not cached-unsupported UA->>US: TCP/TLS connect + WS handshake US-->>UA: 101 Switching Protocols UA-->>PE: events AsyncIterable PE-->>PH: AsyncIterable (collectResponsesWebSocketEvents buffers all) PH-->>RT: ResponsesWebSocketJsonEvent[] RT-->>C: text frames (burst after full collection) else non-Codex / disabled / cached-unsupported UA-->>PE: skipped PE->>FB: httpFallback() FB->>HTTP: POST /v1/responses HTTP-->>FB: Response (response.text() buffers all) FB-->>PE: events (burst after full buffer) PE-->>PH: events PH-->>RT: frames RT-->>C: text frames endPrompt To Fix All With AI
Reviews (7): Last reviewed commit: "test(proxy): unify responses websocket f..." | Re-trigger Greptile