Skip to content

feat(ws): add OpenAI Responses WebSocket support#887

Open
ding113 wants to merge 4 commits intodevfrom
feat/responses-websocket
Open

feat(ws): add OpenAI Responses WebSocket support#887
ding113 wants to merge 4 commits intodevfrom
feat/responses-websocket

Conversation

@ding113
Copy link
Owner

@ding113 ding113 commented Mar 8, 2026

Summary

  • Add OpenAI /v1/responses WebSocket mode to Claude Code Hub with global system toggle, same-port HTTP+WS server, outbound WS adapter, inbound ingress with delayed bridging, billing/observability parity, and provider WS testing
  • 59 files changed, 10,584 lines added across 3 commits (Wave 1: foundation, Wave 2: core pipeline, Wave 3: parity + product surface)
  • 241 WS-specific tests across 12 test files; 3,775 total tests passing

Related PRs:

Wave 1: Foundation

  • enableResponsesWebSocket system toggle (DB schema, types, cache, actions, settings UI, 5 locales)
  • Zod-based WS frame schemas and validation (response.create, terminal events, error frames)
  • Same-port custom Node server wrapping Next.js standalone + ws on /v1/responses
  • Transport classifier (HTTP vs WS decision based on 5 conditions) with ws_fallback reason

Wave 2: Core Pipeline

  • Outbound WS adapter (request-scoped, flex tier timeout support, terminal event collection)
  • Inbound ingress handler (delayed bridging state machine: waiting/processing/closed)
  • Event bridge with bounded ring buffer and terminal settlement (response.completed/failed/incomplete)
  • Session continuity (turn context, prompt_cache_key binding, disconnect classification, neutral fallback)

Wave 3: Parity + Product Surface

  • Billing parity: WS terminal events feed through same cost/usage/redaction pipeline as HTTP
  • Provider WS probe backend with transport/handshake/event reporting
  • Provider testing UI component (WsTestStatus) with transport badge and 5-language i18n
  • Full regression: all quality gates pass (lint, typecheck, test, build)

Test plan

  • bun run typecheck passes
  • bun run lint passes (warnings only, pre-existing)
  • bun run test passes (3,775 tests, 0 failures)
  • bun run build passes (production build with custom server entry)
  • Focused WS suites pass: bunx vitest run tests/unit/lib/ws/ tests/unit/ws/ tests/unit/server/ tests/unit/proxy/transport-classifier.test.ts tests/unit/provider-testing/ (241 tests)
  • Existing HTTP /v1/responses POST route unaffected
  • Manual: toggle enableResponsesWebSocket in settings UI, verify persistence
  • Manual: provider test with Codex provider shows WS status in result card

Description enhanced by Claude AI

Greptile Summary

This PR adds a full OpenAI /v1/responses WebSocket mode to Claude Code Hub, spanning a custom Node.js server, inbound ingress handler, outbound WS adapter, event bridge, billing parity, session continuity, and provider testing UI. The foundation (server setup, Zod schemas, billing parity, session continuity) is well-engineered with comprehensive test coverage (241 WS-specific tests).

However, there are several critical issues that must be addressed:

Critical logic issues:

  • Path double-append bug: toWebSocketUrl incorrectly appends /v1/responses to provider base URLs that already contain /v1, resulting in URLs like wss://api.openai.com/v1/v1/responses. This breaks connectivity for the most common provider configuration (OpenAI).
  • Streaming pipeline not functional: OutboundWsAdapter collects ALL upstream events in an array, then resolves only when the terminal event arrives. The ingress handler then replays this array to the client after the turn completes. This means clients receive zero events during generation, then get everything in a burst — defeating the primary purpose of WebSocket streaming. A real-time callback mechanism is needed to forward events as they arrive.
  • Placeholder handler not registered: The server still uses a placeholder handler that returns "WebSocket ingress not yet initialized" instead of calling registerIngressHandler. Auth validation, toggle checking, and connection state management are bypassed.
  • Client IP extraction reversed: extractClientIp uses the last IP from the x-forwarded-for header instead of the first, reducing client identification accuracy when behind proxies.

Secondary concern:

  • Unbounded event array: The events array in OutboundWsAdapter has no capacity limit, which could cause memory pressure on verbose model responses at scale.

Confidence Score: 1/5

  • Not safe to merge: the WebSocket streaming pipeline does not actually deliver real-time streaming, the ingress handler is not wired up, and common provider configurations will fail due to URL path bugs.
  • The PR has foundational engineering issues that prevent it from functioning as intended. Most critically: (1) OutboundWsAdapter buffers all events until the terminal event arrives, then the ingress handler replays them as a batch — clients see zero output until the full response completes, completely defeating the WebSocket streaming benefit. This is a core architectural flaw, not a minor optimization. (2) The server entry point still uses a placeholder handler instead of calling registerIngressHandler, so auth validation and feature toggle checks are completely bypassed. (3) toWebSocketUrl has a path double-append bug that breaks the most common provider configuration (OpenAI). (4) Client IP extraction is reversed. While the foundation (billing parity, event bridge structure, session continuity) is well-designed, the core streaming path has fundamental issues that must be resolved before this feature can deliver its advertised behavior.
  • src/app/v1/_lib/ws/outbound-adapter.ts (batch buffering), src/app/v1/_lib/ws/ingress-handler.ts (batch relay + IP extraction), src/server/index.ts (placeholder handler), src/app/v1/_lib/proxy/transport-classifier.ts (path double-append)

Sequence Diagram

sequenceDiagram
    participant C as Client (WS)
    participant I as WsIngressHandler
    participant G as GuardPipeline
    participant T as TransportClassifier
    participant O as OutboundWsAdapter
    participant U as Upstream Provider (WS)
    participant B as WsEventBridge
    participant DB as Database (billing)

    C->>I: WS Upgrade (Authorization header)
    I->>I: isResponsesWebSocketEnabled()
    I->>I: validateApiKeyAndGetUser()
    I-->>C: Connection Accepted

    C->>I: response.create frame
    I->>I: state = "processing"
    I->>G: pipeline.run(session) [model, provider, messageContext]
    G-->>I: guard passed
    I->>T: classifyTransport(session, provider)
    T-->>I: transport = "websocket"
    I->>O: executeTurn(requestBody)
    O->>U: WS Upgrade (wss://provider/v1/responses)
    U-->>O: Connection Open
    O->>U: response.create frame
    U->>O: streaming events (tokens, deltas...)
    Note over O,U: ⚠️ Events collected in memory array
    U->>O: response.completed (terminal)
    O-->>I: OutboundTurnResult {events[], completed}
    Note over I,O: ⚠️ Critical: All events arrive at once AFTER turn completes
    loop for each event in turnResult.events
        I->>B: bridge.relayEvent(clientWs, event)
        B-->>C: send event (batched, not real-time)
    end
    I->>DB: updateMessageRequestCost()
    I->>DB: updateMessageRequestDetails()
    I->>I: state = "waiting"
    C->>I: (next response.create or close)
Loading

Last reviewed commit: 30fb85c

Greptile also left 5 inline comments on this PR.

ding113 added 3 commits March 9, 2026 00:42
…transport classifier

Wave 1 of OpenAI Responses WebSocket support:

- Add enableResponsesWebSocket system setting across all layers (schema,
  types, repository, cache, actions, UI, 5 locales) mirroring enableHttp2
- Create WS frame Zod schemas (response.create, terminal events, errors)
  with forward-compatible .passthrough() and byte-for-byte encrypted_content
- Add same-port custom Node server (src/server/) wrapping Next.js + ws
  library for /v1/responses WebSocket upgrades with heartbeat/graceful shutdown
- Add transport classifier deciding HTTP vs WebSocket based on toggle,
  endpoint, provider type, URL scheme, and proxy config
- Add ws_fallback provider chain reason mirroring http2_fallback semantics
  (no circuit breaker penalty, no provider switch)
…and session continuity

Wave 2 of OpenAI Responses WebSocket support:
- OutboundWsAdapter: request-scoped upstream WS connection with configurable
  timeouts, flex tier support, and terminal event collection
- WsIngressHandler: delayed bridging state machine (waiting/processing/closed)
  with auth validation, first-frame timeout, and sequential turn support
- WsEventBridge: bounded ring buffer for event relay with terminal settlement
  (response.completed/failed/incomplete) and usage extraction
- Session continuity: turn context tracking, prompt_cache_key session binding,
  disconnect classification (retryable vs terminal), neutral fallback rules
…verage

Wave 3 of OpenAI Responses WebSocket support:
- Billing parity: WS terminal events feed through same cost/usage pipeline
  as HTTP (settleWsTurnBilling, wsUsageToMetrics, redactWsEventPayload)
- Provider testing: WS probe backend (probeProviderWebSocket) reports
  transport, handshake latency, event count, fallback reason
- Provider testing UI: WsTestStatus component with transport badge,
  metrics display, and 5-language i18n
- Trace metadata builder for Langfuse with WS transport fields
@coderabbitai
Copy link

coderabbitai bot commented Mar 8, 2026

📝 Walkthrough

Walkthrough

本次变更在项目中引入完整的 WebSocket 路径:架构定义、服务器与管理器、入站/出站适配器、传输分类、会话连续性、账单对齐、帧模式与解析、设置开关与本地化,以及大量相关单元测试和数据库迁移(新增 enableResponsesWebSocket)。

Changes

Cohort / File(s) Summary
容器部署配置
deploy/Dockerfile, deploy/Dockerfile.dev
修改最终镜像启动命令为 server.js 并新增注释提示未来切换到自定义 WebSocket 入口。
数据库迁移与模式
drizzle/0079_quick_blink.sql, drizzle/meta/_journal.json, src/drizzle/schema.ts
新增系统设置字段 enable_responses_websocket(布尔,默认 false),并添加对应迁移与 journal 条目。
本地化文案
messages/en/.../provider-chain.json, messages/{en,ja,ru,zh-CN,zh-TW}/settings/config.json, messages/{en,ja,ru,zh-CN,zh-TW}/settings/providers/form/apiTest.json
添加 WebSocket 相关翻译键(如 ws_fallbackenableResponsesWebSocket 及描述、apiTest.ws 文案)。
构建与依赖
package.json, next.config.ts
新增依赖 ws@types/ws,新增 start:ws 脚本;将 ws 列入 Next 外部包与输出跟踪包含列表。
系统设置集成
src/lib/config/system-settings-cache.ts, src/lib/validation/schemas.ts, src/repository/system-config.ts, src/repository/_shared/transformers.ts, src/types/system-config.ts, src/actions/system-config.ts
在类型、验证、仓储、缓存、变换器与保存接口中加入 enableResponsesWebSocket 字段,并提供缓存查询 API。
设置 UI 与状态组件
src/app/[locale]/settings/config/page.tsx, src/app/[locale]/settings/config/_components/system-settings-form.tsx, src/app/[locale]/settings/providers/_components/forms/ws-test-status.tsx
在设置界面添加 WebSocket 开关与描述;新增 WsTestStatus 组件展示探测结果。
WebSocket 帧与解析
src/lib/ws/frames.ts, src/lib/ws/frame-parser.ts
新增 Zod 模式与类型定义(客户端帧、终端事件、服务器错误等)及解析/验证工具。
事件中继与账单对齐
src/app/v1/_lib/ws/event-bridge.ts, src/app/v1/_lib/ws/billing-parity.ts
实现事件中继(环形缓冲、终结检测)与将 WS 使用量适配至现有计费流程及敏感字段脱敏。
会话连续性与断开分类
src/app/v1/_lib/ws/session-continuity.ts
新增会话/回合上下文、断开分类逻辑、显式协议错误集及会话更新函数(含 Redis SessionManager 调用点)。
出站适配器与探针
src/app/v1/_lib/ws/outbound-adapter.ts, src/lib/provider-testing/ws-probe.ts, src/lib/provider-testing/ws-types.ts
新增 OutboundWsAdapter 用于对上游建立 WS 会话并聚合事件;新增探针工具检测提供方 WS 支持与结果类型。
传输分类与代理会话
src/app/v1/_lib/proxy/transport-classifier.ts, src/app/v1/_lib/proxy/session.ts, src/types/message.ts
新增传输判定逻辑(http
WebSocket 管理器与入站处理
src/server/index.ts, src/server/ws-manager.ts, src/app/v1/_lib/ws/ingress-handler.ts
新增 HTTP 启动脚本创建 WsManager、WsManager 实现(升级、心跳、连接管理)和 WsIngressHandler(升级认证、帧循环、回合管理与错误信号)。
测试套件
tests/unit/... (大量新增测试文件,见下述汇总)
为帧模式、解析器、WsEventBridge、billing-parity、session-continuity、outbound-adapter、ingress-handler(单元与集成)、ws 探针与 UI 组件、ws 管理器、系统设置缓存等新增大量单元/集成测试。

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • PR #773 — 也修改了提供商链原因联合类型(与本 PR 在 src/app/v1/_lib/proxy/session.ts / src/types/message.tsws_fallback 变更存在代码级关联)。
  • PR #601 — 也在系统设置相关路径(模式、缓存、仓储、验证、UI 与保存流程)中添加布尔功能标志,与本次对 enableResponsesWebSocket 的改动重叠。
  • PR #801 — 同样修改了 src/app/v1/_lib/proxy/session.ts(与本 PR 新增的 ProxySession.fromWebSocket 与会话相关扩展存在交叉影响)。
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 标题准确描述了主要变更:添加OpenAI Responses WebSocket支持。清晰、简洁,反映了PR的核心目标。
Description check ✅ Passed The PR description is highly detailed and directly relates to the changeset, covering WebSocket feature additions, database schema changes, UI components, and comprehensive testing across 59 files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/responses-websocket

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the Claude Code Hub by integrating WebSocket support for OpenAI's /v1/responses API. This foundational change allows for more efficient and real-time interactions with compatible providers, improving the overall responsiveness of the system. The implementation covers the entire lifecycle from connection establishment and authentication to event relay, billing, and error handling, ensuring a seamless and robust experience.

Highlights

  • OpenAI Responses WebSocket Support: Added comprehensive WebSocket support for OpenAI /v1/responses endpoints, enabling real-time, full-duplex communication.
  • Global System Toggle: Implemented a new system setting enableResponsesWebSocket to globally enable or disable the WebSocket functionality, with corresponding database schema and UI updates.
  • Same-Port HTTP+WS Server: Configured the application to run a custom Node.js server that handles both HTTP and WebSocket traffic on the same port, simplifying deployment.
  • Core Pipeline Components: Developed an outbound WebSocket adapter for upstream communication, an inbound ingress handler with delayed bridging, and an event bridge for relaying messages.
  • Billing and Observability Parity: Ensured that WebSocket terminal events feed into the existing billing, logging, and content redaction pipelines, maintaining consistency with HTTP requests.
  • Provider WebSocket Testing: Introduced a provider WebSocket probe backend and a new UI component (WsTestStatus) to test and display the WebSocket capabilities of configured providers.
  • Session Continuity: Integrated session continuity logic for WebSocket turns, including turn context, prompt_cache_key binding, and disconnect classification.
  • Transport Classification: Implemented a transport classifier to intelligently decide between WebSocket and HTTP based on various conditions, with automatic fallback.
  • Zod-based Frame Validation: Defined and validated WebSocket client and server frames using Zod schemas, ensuring robust data integrity.
Changelog
  • Dockerfile
    • Added a TODO comment to switch to a custom server entry for WebSocket support.
  • deploy/Dockerfile
    • Added a TODO comment to switch to a custom server entry for WebSocket support.
  • deploy/Dockerfile.dev
    • Added a TODO comment to switch to a custom server entry for WebSocket support.
  • drizzle/0079_quick_blink.sql
    • Added a new enable_responses_websocket boolean column to the system_settings table.
  • drizzle/meta/_journal.json
    • Updated the Drizzle migration journal to include 0079_quick_blink.
  • messages/en/provider-chain.json
    • Added ws_fallback translation key.
  • messages/en/settings/config.json
    • Added enableResponsesWebSocket and enableResponsesWebSocketDesc translation keys.
  • messages/en/settings/providers/form/apiTest.json
    • Added new translation keys for WebSocket test status display.
  • messages/ja/provider-chain.json
    • Added Japanese translation for ws_fallback.
  • messages/ja/settings/config.json
    • Added Japanese translations for WebSocket settings.
  • messages/ja/settings/providers/form/apiTest.json
    • Added Japanese translations for WebSocket test status.
  • messages/ru/provider-chain.json
    • Added Russian translation for ws_fallback.
  • messages/ru/settings/config.json
    • Added Russian translations for WebSocket settings.
  • messages/ru/settings/providers/form/apiTest.json
    • Added Russian translations for WebSocket test status.
  • messages/zh-CN/provider-chain.json
    • Added Simplified Chinese translation for ws_fallback.
  • messages/zh-CN/settings/config.json
    • Added Simplified Chinese translations for WebSocket settings.
  • messages/zh-CN/settings/providers/form/apiTest.json
    • Added Simplified Chinese translations for WebSocket test status.
  • messages/zh-TW/provider-chain.json
    • Added Traditional Chinese translation for ws_fallback.
  • messages/zh-TW/settings/config.json
    • Added Traditional Chinese translations for WebSocket settings.
  • messages/zh-TW/settings/providers/form/apiTest.json
    • Added Traditional Chinese translations for WebSocket test status.
  • next.config.ts
    • Added ws to outputFileTracingIncludes for standalone builds.
  • package.json
    • Added start:ws script and ws dependency with its types.
  • src/actions/system-config.ts
    • Updated saveSystemSettings to include enableResponsesWebSocket.
  • src/app/[locale]/settings/config/_components/system-settings-form.tsx
    • Added a UI switch for enableResponsesWebSocket and imported the Wifi icon.
  • src/app/[locale]/settings/config/page.tsx
    • Updated SettingsConfigContent to pass enableResponsesWebSocket.
  • src/app/[locale]/settings/providers/_components/forms/ws-test-status.tsx
    • Added a new React component WsTestStatus for displaying WebSocket test results.
  • src/app/v1/_lib/proxy/session.ts
    • Added ws_fallback to ProviderChainReason type.
  • src/app/v1/_lib/proxy/transport-classifier.ts
    • Added a new module for classifying requests as WebSocket or HTTP and converting URLs.
  • src/app/v1/_lib/ws/billing-parity.ts
    • Added a new module for ensuring billing, logging, and redaction parity for WebSocket events.
  • src/app/v1/_lib/ws/event-bridge.ts
    • Added a new module for managing bidirectional WebSocket event flow and settlement.
  • src/app/v1/_lib/ws/ingress-handler.ts
    • Added a new module for handling incoming WebSocket connections, authentication, and frame processing.
  • src/app/v1/_lib/ws/outbound-adapter.ts
    • Added a new module for making outbound WebSocket requests to the OpenAI Responses API.
  • src/app/v1/_lib/ws/session-continuity.ts
    • Added a new module for managing WebSocket session context and disconnect classification.
  • src/drizzle/schema.ts
    • Added enableResponsesWebSocket column to the systemSettings table schema.
  • src/lib/config/system-settings-cache.ts
    • Updated default settings and cache logic to include enableResponsesWebSocket, and added isResponsesWebSocketEnabled function.
  • src/lib/provider-testing/ws-probe.ts
    • Added a new module for probing provider WebSocket support.
  • src/lib/provider-testing/ws-types.ts
    • Added a new type definition WsTestResultFields for WebSocket test results.
  • src/lib/validation/schemas.ts
    • Updated UpdateSystemSettingsSchema to include enableResponsesWebSocket.
  • src/lib/ws/frame-parser.ts
    • Added a new module for parsing and validating WebSocket frames.
  • src/lib/ws/frames.ts
    • Added new Zod schemas and TypeScript types for WebSocket client frames, terminal events, and error frames.
  • src/repository/_shared/transformers.ts
    • Updated toSystemSettings to include enableResponsesWebSocket.
  • src/repository/system-config.ts
    • Updated system configuration functions to include enableResponsesWebSocket.
  • src/server/index.ts
    • Added a custom Node.js HTTP server with WebSocket upgrade handling.
  • src/server/ws-manager.ts
    • Added a new WsManager class for managing WebSocket connections.
  • src/types/message.ts
    • Added ws_fallback to ProviderChainReason type.
  • src/types/system-config.ts
    • Added enableResponsesWebSocket to SystemSettings and UpdateSystemSettingsInput interfaces.
  • tests/unit/lib/config/system-settings-responses-websocket-toggle.test.ts
    • Added unit tests for the enableResponsesWebSocket system setting toggle.
  • tests/unit/lib/ws/frame-parser.test.ts
    • Added unit tests for WebSocket frame parsing.
  • tests/unit/lib/ws/frames.test.ts
    • Added unit tests for WebSocket frame Zod schemas.
  • tests/unit/provider-testing/ws-probe.test.ts
    • Added unit tests for the WebSocket provider probe.
  • tests/unit/provider-testing/ws-test-status.test.tsx
    • Added unit tests for the WsTestStatus React component.
  • tests/unit/proxy/transport-classifier.test.ts
    • Added unit tests for WebSocket transport classification.
  • tests/unit/server/ws-manager.test.ts
    • Added unit tests for the WebSocket manager.
  • tests/unit/ws/billing-parity.test.ts
    • Added unit tests for WebSocket billing parity logic.
  • tests/unit/ws/event-bridge.test.ts
    • Added unit tests for the WebSocket event bridge.
  • tests/unit/ws/ingress-handler.test.ts
    • Added unit tests for the WebSocket ingress handler.
  • tests/unit/ws/outbound-adapter.test.ts
    • Added unit tests for the WebSocket outbound adapter.
  • tests/unit/ws/session-continuity.test.ts
    • Added unit tests for WebSocket session continuity logic.
Activity
  • The pull request introduces a major new feature: OpenAI Responses WebSocket support.
  • The implementation was structured into three waves: foundational setup, core pipeline development, and integration with existing systems.
  • Wave 1 established the global system toggle, defined WebSocket frame schemas, and set up the same-port HTTP+WS server.
  • Wave 2 focused on building the outbound WebSocket adapter, inbound ingress handler, event bridge, and session continuity mechanisms.
  • Wave 3 ensured seamless integration with billing, observability, and added a new UI for provider WebSocket testing.
  • A total of 241 new WebSocket-specific tests were added, contributing to 3,775 passing tests overall.
  • Manual testing steps are provided to verify the functionality through the settings UI and provider test card.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions bot added enhancement New feature or request area:core area:OpenAI area:provider area:UI size/XL Extra Large PR (> 1000 lines) labels Mar 8, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Mar 8, 2026

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces comprehensive support for WebSocket transport for OpenAI /v1/responses. The changes are well-structured across multiple modules, covering the full lifecycle from connection handling and authentication to billing and testing parity. The implementation is robust, with detailed error handling, state management, and extensive test coverage. I have one suggestion to improve the WebSocket URL construction logic to make it more resilient to different provider base URL formats.

Note: Security Review did not run due to the size of the PR.

Comment on lines +66 to +74
export function toWebSocketUrl(providerBaseUrl: string): string {
const url = new URL(providerBaseUrl);
url.protocol = "wss:";
// Ensure path ends with /v1/responses
if (!url.pathname.endsWith("/v1/responses")) {
url.pathname = url.pathname.replace(/\/$/, "") + "/v1/responses";
}
return url.toString();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation for constructing the WebSocket URL might lead to incorrect paths if the provider's base URL already contains path segments. For example, if providerBaseUrl is https://example.com/v1, the resulting URL would be wss://example.com/v1/v1/responses, which is likely not intended.

To make this more robust and align with the apparent intent of always targeting the /v1/responses endpoint on the given host, consider directly setting the pathname.

export function toWebSocketUrl(providerBaseUrl: string): string {
  const url = new URL(providerBaseUrl);
  url.protocol = "wss:";
  url.pathname = "/v1/responses";
  return url.toString();
}

Comment on lines +66 to +74
export function toWebSocketUrl(providerBaseUrl: string): string {
const url = new URL(providerBaseUrl);
url.protocol = "wss:";
// Ensure path ends with /v1/responses
if (!url.pathname.endsWith("/v1/responses")) {
url.pathname = url.pathname.replace(/\/$/, "") + "/v1/responses";
}
return url.toString();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path double-append bug when base URL includes /v1

toWebSocketUrl appends /v1/responses to the URL's existing pathname without clearing it first. When a provider is configured with the common base URL pattern https://api.openai.com/v1, the resulting WebSocket URL becomes wss://api.openai.com/v1/v1/responses instead of the correct wss://api.openai.com/v1/responses.

Example:

Input:  https://api.openai.com/v1
Output: wss://api.openai.com/v1/v1/responses  ← wrong
Wanted: wss://api.openai.com/v1/responses

The fix is to reset the pathname to the target path:

Suggested change
export function toWebSocketUrl(providerBaseUrl: string): string {
const url = new URL(providerBaseUrl);
url.protocol = "wss:";
// Ensure path ends with /v1/responses
if (!url.pathname.endsWith("/v1/responses")) {
url.pathname = url.pathname.replace(/\/$/, "") + "/v1/responses";
}
return url.toString();
}
export function toWebSocketUrl(providerBaseUrl: string): string {
const url = new URL(providerBaseUrl);
url.protocol = "wss:";
url.pathname = "/v1/responses";
return url.toString();
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/transport-classifier.ts
Line: 66-74

Comment:
**Path double-append bug when base URL includes `/v1`**

`toWebSocketUrl` appends `/v1/responses` to the URL's existing pathname without clearing it first. When a provider is configured with the common base URL pattern `https://api.openai.com/v1`, the resulting WebSocket URL becomes `wss://api.openai.com/v1/v1/responses` instead of the correct `wss://api.openai.com/v1/responses`.

Example:
```
Input:  https://api.openai.com/v1
Output: wss://api.openai.com/v1/v1/responses  ← wrong
Wanted: wss://api.openai.com/v1/responses
```

The fix is to reset the pathname to the target path:
```suggestion
export function toWebSocketUrl(providerBaseUrl: string): string {
  const url = new URL(providerBaseUrl);
  url.protocol = "wss:";
  url.pathname = "/v1/responses";
  return url.toString();
}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +24 to +40
// Placeholder connection handler (will be replaced by ingress-handler in T6)
wsManager.onConnection((ws, req) => {
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
console.log(`[WS] New connection on ${url.pathname}`);

ws.on("message", () => {
ws.send(
JSON.stringify({
type: "error",
error: {
type: "server_error",
message: "WebSocket ingress not yet initialized",
},
})
);
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Placeholder handler is never replaced with registerIngressHandler

src/server/index.ts registers a placeholder onConnection handler but never calls registerIngressHandler from src/app/v1/_lib/ws/ingress-handler.ts (line 352-363). The WsIngressHandler class implements auth validation, toggle checking, and connection state management, but the real handler is never integrated. The placeholder currently returns "WebSocket ingress not yet initialized" to all messages, bypassing the auth and toggle checks that WsIngressHandler.start() would perform.

Replace the placeholder handler with a call to registerIngressHandler:

Suggested change
// Placeholder connection handler (will be replaced by ingress-handler in T6)
wsManager.onConnection((ws, req) => {
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
console.log(`[WS] New connection on ${url.pathname}`);
ws.on("message", () => {
ws.send(
JSON.stringify({
type: "error",
error: {
type: "server_error",
message: "WebSocket ingress not yet initialized",
},
})
);
});
});
import { registerIngressHandler } from "@/app/v1/_lib/ws/ingress-handler";
// ... after creating wsManager ...
registerIngressHandler(wsManager);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/server/index.ts
Line: 24-40

Comment:
**Placeholder handler is never replaced with `registerIngressHandler`**

`src/server/index.ts` registers a placeholder `onConnection` handler but never calls `registerIngressHandler` from `src/app/v1/_lib/ws/ingress-handler.ts` (line 352-363). The `WsIngressHandler` class implements auth validation, toggle checking, and connection state management, but the real handler is never integrated. The placeholder currently returns `"WebSocket ingress not yet initialized"` to all messages, bypassing the auth and toggle checks that `WsIngressHandler.start()` would perform.

Replace the placeholder handler with a call to `registerIngressHandler`:
```suggestion
import { registerIngressHandler } from "@/app/v1/_lib/ws/ingress-handler";

// ... after creating wsManager ...
registerIngressHandler(wsManager);
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +328 to +342
function extractClientIp(req: IncomingMessage): string {
const realIp = req.headers["x-real-ip"];
if (typeof realIp === "string" && realIp.trim()) return realIp.trim();

const forwarded = req.headers["x-forwarded-for"];
if (typeof forwarded === "string") {
const ips = forwarded
.split(",")
.map((s) => s.trim())
.filter(Boolean);
if (ips.length > 0) return ips[ips.length - 1];
}

return req.socket?.remoteAddress ?? "unknown";
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

x-forwarded-for takes last hop instead of first

if (ips.length > 0) return ips[ips.length - 1];

The x-forwarded-for header is structured as client, proxy1, proxy2, …. The first entry is the originating client IP. The last entry is the most recently appended proxy, which can be forged if the ingress doesn't validate the header. This should use the first entry for correct client IP identification:

Suggested change
function extractClientIp(req: IncomingMessage): string {
const realIp = req.headers["x-real-ip"];
if (typeof realIp === "string" && realIp.trim()) return realIp.trim();
const forwarded = req.headers["x-forwarded-for"];
if (typeof forwarded === "string") {
const ips = forwarded
.split(",")
.map((s) => s.trim())
.filter(Boolean);
if (ips.length > 0) return ips[ips.length - 1];
}
return req.socket?.remoteAddress ?? "unknown";
}
if (ips.length > 0) return ips[0];
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/ws/ingress-handler.ts
Line: 328-342

Comment:
**`x-forwarded-for` takes last hop instead of first**

```ts
if (ips.length > 0) return ips[ips.length - 1];
```

The `x-forwarded-for` header is structured as `client, proxy1, proxy2, …`. The **first** entry is the originating client IP. The last entry is the most recently appended proxy, which can be forged if the ingress doesn't validate the header. This should use the first entry for correct client IP identification:

```suggestion
    if (ips.length > 0) return ips[0];
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

This PR introduces comprehensive WebSocket infrastructure for OpenAI /v1/responses (~10,500 lines across 59 files). While the architecture is well-structured with thorough test coverage (241 WS-specific tests), three critical integration bugs prevent the feature from functioning and must be fixed before merge.

PR Size: XL

  • Lines changed: 10,584 additions, 5 deletions
  • Files changed: 59
  • Recommendation: This PR is very large. Consider splitting future work: Wave 1 (foundation/toggle), Wave 2 (core pipeline), Wave 3 (parity/UI) could be separate sequential PRs.

Critical Issues (Must Fix)

Category File Issue Impact
Logic/Bug toWebSocketUrl double-appends /v1/responses causing paths like /v1/v1/responses All WS connections to standard /v1 providers fail with 404
Logic/Bug X-Forwarded-For takes LAST ip instead of FIRST Client IP logging, rate limiting use proxy IP instead of actual client
Standard-Violation Placeholder handler never replaced; registerIngressHandler never called WS feature is non-functional - all connections receive placeholder error

High Priority Issues (Should Fix)

Category File Issue
Error-Handling Non-JSON messages from server are silently dropped with only a warn log
Type-Design isNeutralFallback uses 'message?.includes()' on potentially undefined message

Testing

  • 241 WS-specific tests across 12 test files - excellent coverage
  • All 3,775 tests passing
  • Pre-commit checklist verified (typecheck, lint, test, build)

Review Coverage

  • Logic and correctness - 3 critical bugs found
  • Security (OWASP Top 10) - IP spoofing risk from X-Forwarded-For bug
  • Error handling - Generally good, some silent failures
  • Type safety - Good use of Zod schemas
  • Documentation accuracy - Good comments and JSDoc
  • Test coverage - Excellent (80%+ required, 241 WS tests)
  • Code clarity - Well-structured architecture

Automated review by Claude AI

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/[locale]/settings/config/_components/system-settings-form.tsx (1)

199-225: ⚠️ Potential issue | 🟡 Minor

保存后未从服务器响应更新 enableResponsesWebSocket 状态。

其他设置字段(如 enableHttp2verboseProviderError 等)在保存成功后会从 result.data 更新本地状态,但 enableResponsesWebSocket 缺失了这一步。如果服务器端对该值有任何规范化处理,UI 状态可能与实际保存的值不一致。

建议添加状态更新
        setEnableHttp2(result.data.enableHttp2);
+       setEnableResponsesWebSocket(result.data.enableResponsesWebSocket);
        setInterceptAnthropicWarmupRequests(result.data.interceptAnthropicWarmupRequests);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/settings/config/_components/system-settings-form.tsx around
lines 199 - 225, The save-result handling block updates many local state setters
but omits updating enableResponsesWebSocket, which can leave the UI out of sync
with server-normalized values; add a call to
setEnableResponsesWebSocket(result.data.enableResponsesWebSocket) inside the if
(result.data) { ... } block (near existing setters like setEnableHttp2 and
setVerboseProviderError) so the local state reflects the server response after
saving.
🟡 Minor comments (2)
src/app/v1/_lib/proxy/transport-classifier.ts-66-74 (1)

66-74: ⚠️ Potential issue | 🟡 Minor

toWebSocketUrl 在特定边界情况下可能产生冗余路径

当输入 URL 已包含 /v1/responses/(带尾部斜杠)时,检查 !url.pathname.endsWith("/v1/responses") 为 true,会导致追加额外的 /v1/responses

例如:https://api.com/v1/responses/wss://api.com/v1/responses/v1/responses

建议的修复
 export function toWebSocketUrl(providerBaseUrl: string): string {
   const url = new URL(providerBaseUrl);
   url.protocol = "wss:";
   // Ensure path ends with /v1/responses
-  if (!url.pathname.endsWith("/v1/responses")) {
+  const normalizedPath = url.pathname.replace(/\/+$/, "");
+  if (!normalizedPath.endsWith("/v1/responses")) {
-    url.pathname = url.pathname.replace(/\/$/, "") + "/v1/responses";
+    url.pathname = normalizedPath + "/v1/responses";
   }
   return url.toString();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/proxy/transport-classifier.ts` around lines 66 - 74,
toWebSocketUrl currently appends "/v1/responses" even when the incoming pathname
already ends with "/v1/responses/" which creates duplicate segments; inside
toWebSocketUrl normalize url.pathname by trimming any trailing slash (or
otherwise normalizing) before checking endsWith("/v1/responses"), and only set
url.pathname = (trimmedPath === "" ? "/v1/responses" : trimmedPath +
"/v1/responses") when the trimmed path does not already end with
"/v1/responses", ensuring inputs like "/v1/responses/" become "/v1/responses"
and avoiding duplicate "/v1/responses/v1/responses".
src/server/index.ts-4-4 (1)

4-4: ⚠️ Potential issue | 🟡 Minor

统一用 @/ 引用 src 内模块。

这里直接相对引用 ./ws-manager,和仓库里对 src 内部依赖的路径约定不一致,后续移动文件时也更容易出现路径漂移。

建议修改
-import { WsManager } from "./ws-manager";
+import { WsManager } from "@/server/ws-manager";

As per coding guidelines, "Use path alias @/ to reference files in ./src/ directory".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/index.ts` at line 4, The import in src/server/index.ts uses a
relative path for WsManager ("./ws-manager"); change it to use the project
path-alias by importing WsManager from "@/ws-manager" so it follows the repo
convention and avoids future path-drift (update the import statement that
references WsManager accordingly).
🧹 Nitpick comments (7)
src/lib/ws/frame-parser.ts (1)

38-42: 路径为空时错误消息格式略有瑕疵

当 Zod 校验的 path 为空数组时,path.join(".") 返回空字符串,导致错误消息格式为 ": Required" 而非 "Required"

这是一个细微的展示问题,不影响功能。

可选修复:处理空路径情况
   const firstIssue = result.error.issues[0];
-  const message = firstIssue
-    ? `${firstIssue.path.join(".")}: ${firstIssue.message}`
+  const message = firstIssue
+    ? firstIssue.path.length > 0
+      ? `${firstIssue.path.join(".")}: ${firstIssue.message}`
+      : firstIssue.message
     : "Schema validation failed";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/ws/frame-parser.ts` around lines 38 - 42, The error message
construction in the frame-parser's validation handling uses
firstIssue.path.join(".") which yields an empty string when path is [],
producing messages like ": Required"; update the logic that builds message
(where result.error.issues[0] / firstIssue is used) to detect an empty path
(e.g., firstIssue.path.length === 0) and omit the leading colon/empty prefix so
the message becomes just firstIssue.message, otherwise keep
`${firstIssue.path.join(".")}: ${firstIssue.message}`.
tests/unit/proxy/transport-classifier.test.ts (1)

233-236: 建议添加尾部斜杠边界情况测试

当前测试覆盖了 /v1/responses 路径保留的情况,但缺少 /v1/responses/(带尾部斜杠)的测试用例。根据 toWebSocketUrl 实现分析,这可能是一个边界情况。

   it("preserves existing /v1/responses path", () => {
     const result = toWebSocketUrl("https://api.openai.com/v1/responses");
     expect(result).toBe("wss://api.openai.com/v1/responses");
   });
+
+  it("handles /v1/responses with trailing slash", () => {
+    const result = toWebSocketUrl("https://api.openai.com/v1/responses/");
+    expect(result).toBe("wss://api.openai.com/v1/responses");
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/proxy/transport-classifier.test.ts` around lines 233 - 236, Add a
unit test covering the trailing-slash edge case for toWebSocketUrl: create an it
block (similar to the existing "preserves existing /v1/responses path") that
calls toWebSocketUrl("https://api.openai.com/v1/responses/") and asserts the
returned value preserves the trailing slash and uses wss (e.g.,
"wss://api.openai.com/v1/responses/"); reference the existing test name and the
toWebSocketUrl function so the new test sits alongside the current case and
verifies the "/v1/responses/" boundary behavior.
src/app/v1/_lib/proxy/transport-classifier.ts (1)

38-42: 端点检查使用 endsWith 可能过于宽松

当前检查 pathname.endsWith("/responses") 会匹配 /v1/responses,但也会匹配 /foo/responses 等非预期路径。

考虑到代理路由定义明确,这在实际场景中可能不是问题。如需更严格匹配,可考虑:

- if (!pathname.endsWith("/responses")) {
+ if (pathname !== "/v1/responses" && !pathname.endsWith("/v1/responses")) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/proxy/transport-classifier.ts` around lines 38 - 42, The
endpoint check in transport-classifier.ts currently uses
pathname.endsWith("/responses") which is too permissive; update the logic in the
block that reads const pathname = session.requestUrl.pathname so it only accepts
the exact proxy route (e.g., compare pathname === "/v1/responses") or validate
path segments (e.g., split pathname by "/" and ensure segments match ["", "v1",
"responses"] or use a strict RegExp like /^\/v1\/responses(?:\/)?$/) and return
the same { transport: "http", reason: "not_responses_endpoint" } for anything
that doesn't match.
src/app/v1/_lib/ws/outbound-adapter.ts (1)

5-5: src/ 内部引用请统一走 @/ 别名

这里新加了相对路径导入,和仓库的 src 引用约定不一致。请改成 @/app/v1/_lib/proxy/transport-classifier

As per coding guidelines, **/*.{ts,tsx,js,jsx}: Use path alias @/ to reference files in ./src/ directory.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/ws/outbound-adapter.ts` at line 5, Import in
outbound-adapter.ts uses a relative path; change the import of toWebSocketUrl to
use the project alias by replacing "../proxy/transport-classifier" with
"@/app/v1/_lib/proxy/transport-classifier" so the file imports toWebSocketUrl
via the `@/` src alias consistent with repository conventions.
src/server/ws-manager.ts (1)

73-84: 优雅关闭实现良好。

关闭流程正确:先清理心跳定时器,然后用 1001(Going Away)状态码关闭所有客户端连接,最后关闭 WebSocketServer。

可选改进:可以考虑为 wss.close() 添加超时保护,防止某些客户端连接关闭缓慢导致阻塞。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/ws-manager.ts` around lines 73 - 84, 当前 close() 返回的 Promise
可能会被某些挂起的客户端关闭操作阻塞;在 close()(使用 heartbeatInterval、this.wss 和 this.wss.close())里为
this.wss.close() 的异步等待添加超时保护:将现有 new Promise 包装成带超时的 Promise.race,一方面保留
clearInterval(this.heartbeatInterval) 和遍历 this.wss.clients 调用 ws.close(1001,...
),另一方面在超时(例如 5s)触发时调用 this.wss.clients.forEach(ws => ws.terminate()) 或直接调用
this.wss.close() 的强制完成分支并 resolve,以确保 close() 在可控时间内返回。
src/app/v1/_lib/ws/session-continuity.ts (1)

4-5: 这里也建议改成 @/ 别名导入。

这两个目标都在 src/ 下,继续走相对路径会让模块边界和移动成本都更差。As per coding guidelines "Use path alias @/ to reference files in ./src/ directory".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/ws/session-continuity.ts` around lines 4 - 5, Replace the
relative imports with the project path-alias `@/` so the module uses aliases
rather than relative paths: change the import references for the types
SettlementResult, TurnMeta, and WsAuthContext in session-continuity.ts to use
`@/`-prefixed paths that point into the src tree (e.g., import SettlementResult
from '@/...' and import { TurnMeta, WsAuthContext } from '@/...'), ensuring the
alias matches your tsconfig/webpack config and that the exported type names
(SettlementResult, TurnMeta, WsAuthContext) remain unchanged.
src/app/v1/_lib/ws/ingress-handler.ts (1)

12-12: 这里的内部导入建议统一改成 @/ 别名。

src/ 内再走 ../proxy/auth-guard,后续挪文件时很容易把路径关系一起打断,也和仓库约定不一致。As per coding guidelines "Use path alias @/ to reference files in ./src/ directory".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/ws/ingress-handler.ts` at line 12, In
src/app/v1/_lib/ws/ingress-handler.ts replace the relative import of
extractApiKeyFromHeaders from "../proxy/auth-guard" with the project path-alias
style using "@/..." (e.g. import extractApiKeyFromHeaders from
"@/app/v1/_lib/proxy/auth-guard") so internal imports consistently use the `@/`
alias; keep the imported symbol name extractApiKeyFromHeaders unchanged and
ensure your tsconfig/paths supports the alias.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@deploy/Dockerfile`:
- Around line 61-63: Update the Dockerfile CMD to run the custom server entry
used for WebSocket handling instead of the default Next.js standalone server:
replace the existing CMD that launches server.js with one that starts your
custom server entry (src/server/index.ts compiled/started appropriately) and
remove or correct the outdated TODO/comment that references src/server/index.js;
ensure the Docker runtime/start command aligns with how you build and run
TypeScript (e.g., use the compiled JS entry or a node wrapper) so the custom
entry that references ws-manager.ts is actually launched.

In `@Dockerfile`:
- Around line 28-30: The Dockerfile currently launches the Next.js standalone
server (CMD running server.js) which skips the custom HTTP+WS bootstrap in
src/server/index.ts; update the Dockerfile to run the custom server entry
instead (start node with tsx importing src/server/index.ts) or alternatively
change the build so that the compiled server.js includes the custom server code
from src/server/index.ts; ensure the container runs the entry that initializes
WsManager/WebSocketServer and mounts the /v1/responses route rather than the
standalone Next output.

In `@package.json`:
- Line 9: package.json 中的 "start:ws" 脚本引用了 node --import tsx 但项目未声明 tsx
依赖,导致在干净安装后启动失败;修复方法是要么在 package.json 的 devDependencies 中添加 "tsx"(并运行 npm/yarn
安装),要么修改 "start:ws" 脚本为直接使用 tsx 可执行命令(例如 "tsx src/server/index.ts"),请更新
package.json 中的 "start:ws" 脚本或 devDependencies 以确保 tsx 在运行时可用。

In `@src/app/`[locale]/settings/providers/_components/forms/ws-test-status.tsx:
- Around line 53-56: Do not render result.wsFallbackReason directly; instead map
that internal reason code to an i18n key and pass the key to t() with a
fallback. Add a small mapping object (e.g., wsFallbackReasonKeyMap) that maps
known reason codes to translation keys like "ws.fallbackReason.network",
"ws.fallbackReason.auth", etc., then compute const reasonKey =
wsFallbackReasonKeyMap[result.wsFallbackReason] ?? "ws.fallbackReason.unknown"
and render t(reasonKey) where result.wsFallbackReason is currently shown;
optionally keep the raw value in a debug log but never expose it in the UI.
Ensure translation keys exist for all five supported locales.

In `@src/app/v1/_lib/ws/billing-parity.ts`:
- Around line 206-225: calculateRequestCost() is called with costMultiplier but
calculateRequestCostBreakdown() is not, causing mismatched total vs breakdown
when a provider multiplier is applied; update the
calculateRequestCostBreakdown(...) call to accept and forward costMultiplier
(same as calculateRequestCost) along with usageMetrics, priceData,
context1mApplied, priorityServiceTierApplied so result.costBreakdown uses the
same multiplier and matches result.costUsd (ensure the
calculateRequestCostBreakdown function signature is updated where defined to
accept the new costMultiplier parameter).
- Around line 145-164: The wsUsageToMetrics function currently only reads
top-level passthrough fields and misses the nested cached tokens fallback;
update wsUsageToMetrics (and its usage of ResponseUsage/raw) to check for
input_tokens_details?.cached_tokens and use that value as
cache_read_input_tokens when raw.cache_read_input_tokens is undefined or not a
number, mirroring the logic in response-handler.ts; add a unit test that
supplies a ResponseUsage with no top-level cache_read_input_tokens but with
input_tokens_details.cached_tokens to ensure the converted UsageMetrics includes
cache_read_input_tokens.

In `@src/app/v1/_lib/ws/ingress-handler.ts`:
- Around line 134-138: The debug log in the WsIngress auth path currently
records raw PII (authResult.user.name and this.ip); change the logger.debug call
(the one that currently uses authResult.user.id, authResult.user.name, this.ip)
to only include userId and a non-reversible or truncated IP representation
(e.g., hash the IP with SHA-256 or mask/truncate it) and remove
authResult.user.name entirely; ensure the new fields are clearly named (e.g.,
userId and clientIpHash or clientIpMask) so callers can identify them when
searching logs.
- Around line 88-142: The start() method can throw when calling
isResponsesWebSocketEnabled() or validateApiKeyAndGetUser() (and likewise at the
other call site around lines 356-359); wrap the async startup sequence in a
try/catch that catches any exception, logs the error (include context like
user/IP), calls this.ws.close(...) with a protocol-appropriate code and message,
sets this.state = "closed", and returns false; ensure the same defensive
try/catch is applied where start() is invoked (e.g., in the onConnection
handler) so no exception bubbles out unhandled and setupListeners() is only
called after successful, non-throwing authentication.
- Around line 269-279: The handleTurn method currently always sends a
server_error and never forwards response.create frames upstream; replace the
unconditional this.sendError with logic that builds a synthetic ProxySession
from this.req and this.auth, runs the deferred guard pipeline (model, rateLimit,
provider) and then attempts to forward the ResponseCreateFrame to the outbound
bridge/adapter (use existing outbound adapter or HTTP fallback), relaying
upstream events back to the client; only call this.sendError if both the bridge
attempt and HTTP fallback fail, and ensure any error includes contextual details
for debugging.
- Around line 332-338: The code handling the "x-forwarded-for" header in
ingress-handler.ts is returning the last IP in the list (ips[ips.length - 1])
which is reversed; change the logic to return the first IP (the
left-most/ips[0]) as the original client IP. In the block that reads const
forwarded = req.headers["x-forwarded-for"] and builds ips, replace the return to
use ips[0] (or otherwise select the first trimmed, non-empty element) so
downstream rate-limiting/audit/provider decisions use the true client IP.
- Line 93: Replace hardcoded English close/reason strings passed to
this.ws.close(...) with stable error codes/constants and ensure the client/UI
performs i18n mapping; specifically change the call using this.ws.close(4003,
"Responses WebSocket is disabled") to send a predefined error identifier (e.g.,
"ERR_WS_RESPONSES_DISABLED" or an exported enum value) and update all other
similar calls (the other this.ws.close(...) occurrences flagged) to use the same
pattern so server only emits codes and the presentation layer handles language
mapping for zh-CN/zh-TW/en/ja/ru.

In `@src/app/v1/_lib/ws/outbound-adapter.ts`:
- Around line 158-234: The issue is that late-arriving WebSocket callbacks
(open/message/error/close) can run after finish() has resolved the request,
causing duplicate sends and mutating events; update each WS event handler (the
"open", "message", "error", and "close" listeners in outbound-adapter.ts) to
immediately short-circuit if the local resolved flag is true, and ensure
finish() both sets resolved and removes/cleans the socket listeners and any
timers (e.g., clearTimeout(handshakeTimer) and remove listeners via ws.off or
ws.removeAllListeners) before attempting close; also add a regression test
asserting that a delayed "open" after a timeout does not call send() and that
late "message" events do not modify the returned events array.

In `@src/app/v1/_lib/ws/session-continuity.ts`:
- Around line 132-146: The logs in SessionContinuity that write promptCacheKey
and sessionId (see logger.debug and logger.error in the session update block)
expose sensitive correlating identifiers; change both debug and error logging to
avoid writing the raw promptCacheKey and full sessionId by instead logging a
presence boolean and a deterministic redaction (e.g., truncated substring) or a
hash of promptCacheKey and sessionId; update the logging calls around the
return/catch in the function handling session binding (references:
promptCacheKey, sessionId, logger.debug, logger.error, result.sessionId,
result.updated) so logs include only non-sensitive indicators (exists:
true/false, masked: "xxxx...abcd" or sha256(promptCacheKey)) and retain other
contextual fields like providerId.

In `@src/lib/ws/frames.ts`:
- Around line 9-15: ReasoningConfigSchema currently restricts reasoning.effort
to ["minimal","low","medium","high"] which rejects valid OpenAI Responses API
values; update the enum in ReasoningConfigSchema (the z.object that defines
effort) to include "none" and "xhigh" so it accepts
["none","minimal","low","medium","high","xhigh"], and ensure any downstream
validation/type checks that reference reasoning.effort are compatible with these
additional values.
- Around line 41-64: The ResponseCreateFrameSchema currently enforces
response.input as an array only; update the schema (ResponseCreateFrameSchema)
so response.input accepts either a string or an array by replacing the input
type with z.union([z.string(), z.array(InputItemSchema)]).optional() (keep
.passthrough() on the parent object) and add unit tests for the parser/validator
to cover both input: "Hello!" and input: ["..."] cases to ensure HTTP/WS
semantics accept single-string input as well as arrays; reference
InputItemSchema when implementing the union so array validation remains
unchanged.

In `@src/repository/system-config.ts`:
- Around line 341-344: The update routine sets payload.enableResponsesWebSocket
into updates but doesn’t include that column in the returning(...) projection,
so toSystemSettings(updated) receives undefined; modify the DB update call to
include enableResponsesWebSocket in the returning(...) list (the same projection
used for other fields) so the updated row returned to toSystemSettings contains
the real value for enableResponsesWebSocket, and verify the
toSystemSettings/updates mapping uses the same property name.

In `@src/server/index.ts`:
- Around line 24-39: The placeholder WebSocket connection handler
(wsManager.onConnection) immediately replies with a server_error and never wires
the real ingress/bridge; replace the stubbed message handler with the actual
WsIngressHandler/pipeline so connections to /v1/responses are forwarded to the
real ingress logic. Locate the wsManager.onConnection callback, instantiate or
retrieve the WsIngressHandler (or existing pipeline) and attach it to the ws
connection and req (handing over message/event handling and close/error
propagation), removing the current hardcoded ws.on("message", ...) that returns
the "WebSocket ingress not yet initialized" error.
- Around line 7-8: The code uses process.env.HOSTNAME for the bind address
(const hostname), which is unsafe in containers; change it to read from
process.env.HOST with a fallback to "0.0.0.0" (i.e., replace references to
process.env.HOSTNAME in the hostname initialization with process.env.HOST ||
"0.0.0.0") so the server binds to a proper network interface instead of a
pod/container name; keep the existing parseInt for port unchanged.
- Around line 43-47: The shutdown function currently calls server.close() and
immediately process.exit(0), which can terminate in-flight HTTP requests; change
shutdown to await server.close by wrapping server.close in a Promise (e.g., new
Promise((resolve, reject) => server.close(err => err ? reject(err) :
resolve()))) before calling process.exit(0), keep awaiting wsManager.close() as
is, and handle errors from the awaited server.close to log and exit with an
appropriate code instead of exiting immediately.

---

Outside diff comments:
In `@src/app/`[locale]/settings/config/_components/system-settings-form.tsx:
- Around line 199-225: The save-result handling block updates many local state
setters but omits updating enableResponsesWebSocket, which can leave the UI out
of sync with server-normalized values; add a call to
setEnableResponsesWebSocket(result.data.enableResponsesWebSocket) inside the if
(result.data) { ... } block (near existing setters like setEnableHttp2 and
setVerboseProviderError) so the local state reflects the server response after
saving.

---

Minor comments:
In `@src/app/v1/_lib/proxy/transport-classifier.ts`:
- Around line 66-74: toWebSocketUrl currently appends "/v1/responses" even when
the incoming pathname already ends with "/v1/responses/" which creates duplicate
segments; inside toWebSocketUrl normalize url.pathname by trimming any trailing
slash (or otherwise normalizing) before checking endsWith("/v1/responses"), and
only set url.pathname = (trimmedPath === "" ? "/v1/responses" : trimmedPath +
"/v1/responses") when the trimmed path does not already end with
"/v1/responses", ensuring inputs like "/v1/responses/" become "/v1/responses"
and avoiding duplicate "/v1/responses/v1/responses".

In `@src/server/index.ts`:
- Line 4: The import in src/server/index.ts uses a relative path for WsManager
("./ws-manager"); change it to use the project path-alias by importing WsManager
from "@/ws-manager" so it follows the repo convention and avoids future
path-drift (update the import statement that references WsManager accordingly).

---

Nitpick comments:
In `@src/app/v1/_lib/proxy/transport-classifier.ts`:
- Around line 38-42: The endpoint check in transport-classifier.ts currently
uses pathname.endsWith("/responses") which is too permissive; update the logic
in the block that reads const pathname = session.requestUrl.pathname so it only
accepts the exact proxy route (e.g., compare pathname === "/v1/responses") or
validate path segments (e.g., split pathname by "/" and ensure segments match
["", "v1", "responses"] or use a strict RegExp like /^\/v1\/responses(?:\/)?$/)
and return the same { transport: "http", reason: "not_responses_endpoint" } for
anything that doesn't match.

In `@src/app/v1/_lib/ws/ingress-handler.ts`:
- Line 12: In src/app/v1/_lib/ws/ingress-handler.ts replace the relative import
of extractApiKeyFromHeaders from "../proxy/auth-guard" with the project
path-alias style using "@/..." (e.g. import extractApiKeyFromHeaders from
"@/app/v1/_lib/proxy/auth-guard") so internal imports consistently use the `@/`
alias; keep the imported symbol name extractApiKeyFromHeaders unchanged and
ensure your tsconfig/paths supports the alias.

In `@src/app/v1/_lib/ws/outbound-adapter.ts`:
- Line 5: Import in outbound-adapter.ts uses a relative path; change the import
of toWebSocketUrl to use the project alias by replacing
"../proxy/transport-classifier" with "@/app/v1/_lib/proxy/transport-classifier"
so the file imports toWebSocketUrl via the `@/` src alias consistent with
repository conventions.

In `@src/app/v1/_lib/ws/session-continuity.ts`:
- Around line 4-5: Replace the relative imports with the project path-alias `@/`
so the module uses aliases rather than relative paths: change the import
references for the types SettlementResult, TurnMeta, and WsAuthContext in
session-continuity.ts to use `@/`-prefixed paths that point into the src tree
(e.g., import SettlementResult from '@/...' and import { TurnMeta, WsAuthContext
} from '@/...'), ensuring the alias matches your tsconfig/webpack config and
that the exported type names (SettlementResult, TurnMeta, WsAuthContext) remain
unchanged.

In `@src/lib/ws/frame-parser.ts`:
- Around line 38-42: The error message construction in the frame-parser's
validation handling uses firstIssue.path.join(".") which yields an empty string
when path is [], producing messages like ": Required"; update the logic that
builds message (where result.error.issues[0] / firstIssue is used) to detect an
empty path (e.g., firstIssue.path.length === 0) and omit the leading colon/empty
prefix so the message becomes just firstIssue.message, otherwise keep
`${firstIssue.path.join(".")}: ${firstIssue.message}`.

In `@src/server/ws-manager.ts`:
- Around line 73-84: 当前 close() 返回的 Promise 可能会被某些挂起的客户端关闭操作阻塞;在 close()(使用
heartbeatInterval、this.wss 和 this.wss.close())里为 this.wss.close()
的异步等待添加超时保护:将现有 new Promise 包装成带超时的 Promise.race,一方面保留
clearInterval(this.heartbeatInterval) 和遍历 this.wss.clients 调用 ws.close(1001,...
),另一方面在超时(例如 5s)触发时调用 this.wss.clients.forEach(ws => ws.terminate()) 或直接调用
this.wss.close() 的强制完成分支并 resolve,以确保 close() 在可控时间内返回。

In `@tests/unit/proxy/transport-classifier.test.ts`:
- Around line 233-236: Add a unit test covering the trailing-slash edge case for
toWebSocketUrl: create an it block (similar to the existing "preserves existing
/v1/responses path") that calls
toWebSocketUrl("https://api.openai.com/v1/responses/") and asserts the returned
value preserves the trailing slash and uses wss (e.g.,
"wss://api.openai.com/v1/responses/"); reference the existing test name and the
toWebSocketUrl function so the new test sits alongside the current case and
verifies the "/v1/responses/" boundary behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0cdcc897-d445-40a8-8c2f-46aad480eb67

📥 Commits

Reviewing files that changed from the base of the PR and between d74d4b7 and f768c63.

📒 Files selected for processing (59)
  • Dockerfile
  • deploy/Dockerfile
  • deploy/Dockerfile.dev
  • drizzle/0079_quick_blink.sql
  • drizzle/meta/0079_snapshot.json
  • drizzle/meta/_journal.json
  • messages/en/provider-chain.json
  • messages/en/settings/config.json
  • messages/en/settings/providers/form/apiTest.json
  • messages/ja/provider-chain.json
  • messages/ja/settings/config.json
  • messages/ja/settings/providers/form/apiTest.json
  • messages/ru/provider-chain.json
  • messages/ru/settings/config.json
  • messages/ru/settings/providers/form/apiTest.json
  • messages/zh-CN/provider-chain.json
  • messages/zh-CN/settings/config.json
  • messages/zh-CN/settings/providers/form/apiTest.json
  • messages/zh-TW/provider-chain.json
  • messages/zh-TW/settings/config.json
  • messages/zh-TW/settings/providers/form/apiTest.json
  • next.config.ts
  • package.json
  • src/actions/system-config.ts
  • src/app/[locale]/settings/config/_components/system-settings-form.tsx
  • src/app/[locale]/settings/config/page.tsx
  • src/app/[locale]/settings/providers/_components/forms/ws-test-status.tsx
  • src/app/v1/_lib/proxy/session.ts
  • src/app/v1/_lib/proxy/transport-classifier.ts
  • src/app/v1/_lib/ws/billing-parity.ts
  • src/app/v1/_lib/ws/event-bridge.ts
  • src/app/v1/_lib/ws/ingress-handler.ts
  • src/app/v1/_lib/ws/outbound-adapter.ts
  • src/app/v1/_lib/ws/session-continuity.ts
  • src/drizzle/schema.ts
  • src/lib/config/system-settings-cache.ts
  • src/lib/provider-testing/ws-probe.ts
  • src/lib/provider-testing/ws-types.ts
  • src/lib/validation/schemas.ts
  • src/lib/ws/frame-parser.ts
  • src/lib/ws/frames.ts
  • src/repository/_shared/transformers.ts
  • src/repository/system-config.ts
  • src/server/index.ts
  • src/server/ws-manager.ts
  • src/types/message.ts
  • src/types/system-config.ts
  • tests/unit/lib/config/system-settings-responses-websocket-toggle.test.ts
  • tests/unit/lib/ws/frame-parser.test.ts
  • tests/unit/lib/ws/frames.test.ts
  • tests/unit/provider-testing/ws-probe.test.ts
  • tests/unit/provider-testing/ws-test-status.test.tsx
  • tests/unit/proxy/transport-classifier.test.ts
  • tests/unit/server/ws-manager.test.ts
  • tests/unit/ws/billing-parity.test.ts
  • tests/unit/ws/event-bridge.test.ts
  • tests/unit/ws/ingress-handler.test.ts
  • tests/unit/ws/outbound-adapter.test.ts
  • tests/unit/ws/session-continuity.test.ts

Comment on lines +61 to 63
# TODO: Switch to custom server entry for WebSocket support once ingress handler is ready
# CMD ["node", "src/server/index.js"]
CMD ["node", "server.js"]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 检查 server.js 和 src/server/index.js 的存在及内容

# 查找服务器相关文件
echo "=== 查找服务器入口文件 ==="
fd -t f -e js -e ts "^(server|index)\.(js|ts)$" --max-depth 2

echo ""
echo "=== 检查 package.json 中的启动脚本 ==="
cat package.json | jq '.scripts | to_entries | map(select(.key | test("start")))' 2>/dev/null || grep -A 5 '"scripts"' package.json

Repository: ding113/claude-code-hub

Length of output: 259


🏁 Script executed:

#!/bin/bash
# 检查 Next.js 构建配置和服务器文件结构

echo "=== 查找 Next.js 配置文件 ==="
fd -t f "next\.config\.(js|ts|mjs)" .

echo ""
echo "=== 检查 src/server 目录结构 ==="
fd -t f . src/server/ 2>/dev/null || echo "src/server/ 目录不存在或为空"

echo ""
echo "=== 检查 .next 构建输出目录 ==="
ls -la .next/ 2>/dev/null | head -20 || echo ".next/ 目录不存在"

echo ""
echo "=== 查找所有 server 相关文件 ==="
fd -t f "server" . --exclude "node_modules" --exclude ".git" --exclude ".next"

echo ""
echo "=== 检查 Dockerfile 的完整内容 ==="
cat deploy/Dockerfile

Repository: ding113/claude-code-hub

Length of output: 2454


TODO 注释已过时,Dockerfile 应更新以使用自定义服务器入口。

WebSocket 入站处理器已在本 PR 中实现(src/server/index.tsws-manager.ts 存在),但 Dockerfile 仍在使用默认的 Next.js 服务器(server.js 来自 .next/standalone)。请将 CMD 更新为使用自定义服务器入口点 src/server/index.ts,或澄清为何目前仍使用默认服务器。

同时,注释中对 src/server/index.js 的引用不正确,实际文件为 src/server/index.ts

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@deploy/Dockerfile` around lines 61 - 63, Update the Dockerfile CMD to run the
custom server entry used for WebSocket handling instead of the default Next.js
standalone server: replace the existing CMD that launches server.js with one
that starts your custom server entry (src/server/index.ts compiled/started
appropriately) and remove or correct the outdated TODO/comment that references
src/server/index.js; ensure the Docker runtime/start command aligns with how you
build and run TypeScript (e.g., use the compiled JS entry or a node wrapper) so
the custom entry that references ws-manager.ts is actually launched.

Comment on lines +28 to 30
# TODO: Switch to custom server entry for WebSocket support once ingress handler is ready
# CMD ["node", "src/server/index.js"]
CMD ["node", "server.js"]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Dockerfile CMD =="
sed -n '28,30p' Dockerfile

echo
echo "== package.json scripts =="
node <<'NODE'
const pkg = require("./package.json");
console.log("start    =", pkg.scripts?.start);
console.log("start:ws =", pkg.scripts?.["start:ws"]);
NODE

echo
echo "== custom server entry =="
fd index.ts src/server -x sed -n '1,220p' {}

echo
echo "== runtime/build references =="
rg -n --glob '!node_modules' 'src/server/index|server\.js|/v1/responses|WebSocketServer|ws-manager' .

Repository: ding113/claude-code-hub

Length of output: 18518


Docker 当前运行的是 Next.js 独立输出,会跳过自定义 WS 服务器。

容器启动 server.js(Next.js standalone from .next/standalone/),而完整的 HTTP+WS 引导逻辑在 src/server/index.ts 中。该自定义服务器已正确实现:

  • 在同一端口附加 WebSocket 管理器
  • /v1/responses 路径配置完成
  • WsManager 和 WebSocketServer 依赖已导入

问题:Docker 目前运行的 standalone 输出不包含自定义服务器代码,导致容器内 WebSocket 无法工作。

修复:修改 Dockerfile 第 28-30 行,将 CMD ["node", "server.js"] 改为:

CMD ["node", "--import", "tsx", "src/server/index.ts"]

或更新构建流程,确保自定义服务器逻辑被编译进 server.js

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 28 - 30, The Dockerfile currently launches the
Next.js standalone server (CMD running server.js) which skips the custom HTTP+WS
bootstrap in src/server/index.ts; update the Dockerfile to run the custom server
entry instead (start node with tsx importing src/server/index.ts) or
alternatively change the build so that the compiled server.js includes the
custom server code from src/server/index.ts; ensure the container runs the entry
that initializes WsManager/WebSocketServer and mounts the /v1/responses route
rather than the standalone Next output.

"dev": "next dev --port 13500",
"build": "next build && (node scripts/copy-version-to-standalone.cjs || bun scripts/copy-version-to-standalone.cjs)",
"start": "next start",
"start:ws": "node --import tsx src/server/index.ts",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

node <<'NODE'
const pkg = require("./package.json");
console.log("start:ws =", pkg.scripts?.["start:ws"]);
console.log("tsx =", pkg.dependencies?.tsx ?? pkg.devDependencies?.tsx ?? "MISSING");
NODE

Repository: ding113/claude-code-hub

Length of output: 129


🏁 Script executed:

# Check for tsx references in package-lock.json or yarn.lock
if [ -f package-lock.json ]; then
  echo "=== Checking package-lock.json for tsx ===" 
  grep -c "\"tsx\"" package-lock.json || echo "tsx not in package-lock.json"
fi

# Check yarn.lock
if [ -f yarn.lock ]; then
  echo "=== Checking yarn.lock for tsx ===" 
  grep -c "tsx" yarn.lock || echo "tsx not in yarn.lock"
fi

# Check if src/server/index.ts exists
echo "=== Checking if src/server/index.ts exists ==="
if [ -f src/server/index.ts ]; then
  echo "File exists"
  head -5 src/server/index.ts
else
  echo "File does NOT exist"
fi

# Check full package.json for any tsx references
echo "=== Full dependencies and devDependencies ==="
node <<'NODE'
const pkg = require("./package.json");
console.log("All dependencies:", Object.keys(pkg.dependencies || {}));
console.log("All devDependencies:", Object.keys(pkg.devDependencies || {}));
NODE

Repository: ding113/claude-code-hub

Length of output: 2159


🏁 Script executed:

# Check for tsconfig.json
echo "=== Checking for tsconfig.json ==="
if [ -f tsconfig.json ]; then
  echo "tsconfig.json exists"
  head -10 tsconfig.json
fi

# Check if tsx is used in other scripts
echo "=== Checking all npm scripts for tsx usage ==="
node <<'NODE'
const pkg = require("./package.json");
const scripts = pkg.scripts || {};
for (const [name, cmd] of Object.entries(scripts)) {
  if (cmd.includes('tsx')) {
    console.log(`${name}: ${cmd}`);
  }
}
NODE

# Check if there's a compiled TypeScript somewhere
echo "=== Checking for dist or build directories ==="
ls -la | grep -E "^d" | grep -E "(dist|build|out)" || echo "No dist/build directories found"

Repository: ding113/claude-code-hub

Length of output: 531


start:ws 缺少 tsx 依赖声明。

脚本使用 node --import tsxtsx 未在 package.json 中声明,干净安装后会在启动时失败。

建议方案:

  • 添加 tsx 到 devDependencies,或
  • 改用 tsx src/server/index.ts 命令
修正示例
-    "start:ws": "node --import tsx src/server/index.ts",
+    "start:ws": "tsx src/server/index.ts",
@@
   "devDependencies": {
+    "tsx": "^4.20.3",
     "@biomejs/biome": "^2",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 9, package.json 中的 "start:ws" 脚本引用了 node --import tsx
但项目未声明 tsx 依赖,导致在干净安装后启动失败;修复方法是要么在 package.json 的 devDependencies 中添加 "tsx"(并运行
npm/yarn 安装),要么修改 "start:ws" 脚本为直接使用 tsx 可执行命令(例如 "tsx src/server/index.ts"),请更新
package.json 中的 "start:ws" 脚本或 devDependencies 以确保 tsx 在运行时可用。

Comment on lines +53 to +56
{result.wsFallbackReason && (
<div data-testid="ws-fallback-reason" className="mt-2 text-xs">
<span className="text-muted-foreground">{t("ws.fallbackReason")}:</span>{" "}
<span className="text-destructive">{result.wsFallbackReason}</span>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

不要直接向用户渲染 wsFallbackReason 原始值。

这里会把分类器/探测层返回的 reason code 直接显示到界面,跨语言场景下会暴露未翻译的内部标识,并且把 UI 文案和后端枚举值绑死。请改成稳定的 reason code → t() 映射,未知值再回退到通用提示。

As per coding guidelines, "All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/settings/providers/_components/forms/ws-test-status.tsx
around lines 53 - 56, Do not render result.wsFallbackReason directly; instead
map that internal reason code to an i18n key and pass the key to t() with a
fallback. Add a small mapping object (e.g., wsFallbackReasonKeyMap) that maps
known reason codes to translation keys like "ws.fallbackReason.network",
"ws.fallbackReason.auth", etc., then compute const reasonKey =
wsFallbackReasonKeyMap[result.wsFallbackReason] ?? "ws.fallbackReason.unknown"
and render t(reasonKey) where result.wsFallbackReason is currently shown;
optionally keep the raw value in a debug log but never expose it in the UI.
Ensure translation keys exist for all five supported locales.

Comment on lines +145 to +164
export function wsUsageToMetrics(usage?: ResponseUsage): UsageMetrics | null {
if (!usage) return null;

// Access passthrough fields via Record cast
const raw = usage as Record<string, unknown>;

return {
input_tokens: usage.input_tokens,
output_tokens: usage.output_tokens,
cache_creation_input_tokens:
typeof raw.cache_creation_input_tokens === "number"
? raw.cache_creation_input_tokens
: undefined,
cache_read_input_tokens:
typeof raw.cache_read_input_tokens === "number" ? raw.cache_read_input_tokens : undefined,
// WS terminal payloads do not include 5m/1h split or cache_ttl;
// leaving these undefined causes downstream pricing to fall back
// to the unified cache_creation_input_tokens path.
};
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, understand the file structure and related code
fd "billing-parity\|response-handler\|key\.ts" --type f

Repository: ding113/claude-code-hub

Length of output: 49


🏁 Script executed:

# Search for input_tokens_details usage in the codebase
rg "input_tokens_details" --type ts --type tsx -B 2 -A 2

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

# Search for cached_tokens references
rg "cached_tokens" --type ts --type tsx -B 2 -A 2

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

# Check the ResponseUsage type definition
rg "type ResponseUsage\|interface ResponseUsage" --type ts --type tsx -A 20

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

# Look for how billing is calculated and if there's a difference between WS and HTTP paths
rg "calculateRequestCost\|wsUsageToMetrics" --type ts --type tsx -B 3 -A 5

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

# List files in the repository to understand structure
fd . --type f --name "*.ts" | grep -E "(billing|response)" | head -20

Repository: ding113/claude-code-hub

Length of output: 317


🏁 Script executed:

# Search for input_tokens_details and cached_tokens with correct syntax
rg "input_tokens_details" --type ts -B 2 -A 2

Repository: ding113/claude-code-hub

Length of output: 1965


🏁 Script executed:

# Search for ResponseUsage type definition
rg "ResponseUsage" --type ts -B 2 -A 10

Repository: ding113/claude-code-hub

Length of output: 11025


🏁 Script executed:

# Read the billing-parity.ts file completely
cat src/app/v1/_lib/ws/billing-parity.ts

Repository: ding113/claude-code-hub

Length of output: 10672


🏁 Script executed:

# Check for response-handler and how it handles cache tokens
fd "response-handler" --type f

Repository: ding113/claude-code-hub

Length of output: 361


🏁 Script executed:

# Check the ResponseUsage schema definition in frames.ts
rg "UsageSchema\|ResponseUsage" src/lib/ws/frames.ts -A 30

Repository: ding113/claude-code-hub

Length of output: 49


🏁 Script executed:

# Check if input_tokens_details is mentioned in the WS frames or documentation
rg "input_tokens_details" src/lib/ws/ --type ts

Repository: ding113/claude-code-hub

Length of output: 49


🏁 Script executed:

# Look at the HTTP response-handler to see how it handles input_tokens_details
rg -A 10 "input_tokens_details" src/app/v1/_lib/proxy/response-handler.ts

Repository: ding113/claude-code-hub

Length of output: 685


🏁 Script executed:

# Check test cases for wsUsageToMetrics to see if nested cached_tokens is tested
cat tests/unit/ws/billing-parity.test.ts | grep -A 20 "wsUsageToMetrics"

Repository: ding113/claude-code-hub

Length of output: 2055


wsUsageToMetrics 缺少对嵌套 input_tokens_details.cached_tokens 的处理,导致 WS 账单与 HTTP 路径失配

HTTP 路径在 response-handler.ts 中已实现从嵌套 input_tokens_details.cached_tokens 提取缓存读 token 的逻辑(当顶层 cache_read_input_tokens 不存在时),但 WS 路径的 wsUsageToMetrics 函数仅读取顶层 passthrough 字段,未实现这个回退逻辑。当 OpenAI 仅在嵌套结构中返回缓存 token 时,WS 终端事件会漏掉该数据,导致 usage 和 cost 计算与 HTTP 路径发生偏差。

建议修改 wsUsageToMetrics 添加对 input_tokens_details.cached_tokens 的检查(作为 cache_read_input_tokens 的回退),并补充对应的回归测试用例。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/ws/billing-parity.ts` around lines 145 - 164, The
wsUsageToMetrics function currently only reads top-level passthrough fields and
misses the nested cached tokens fallback; update wsUsageToMetrics (and its usage
of ResponseUsage/raw) to check for input_tokens_details?.cached_tokens and use
that value as cache_read_input_tokens when raw.cache_read_input_tokens is
undefined or not a number, mirroring the logic in response-handler.ts; add a
unit test that supplies a ResponseUsage with no top-level
cache_read_input_tokens but with input_tokens_details.cached_tokens to ensure
the converted UsageMetrics includes cache_read_input_tokens.

Comment on lines +41 to +64
export const ResponseCreateFrameSchema = z.object({
type: z.literal("response.create"),
response: z
.object({
model: z.string().min(1),
input: z.array(InputItemSchema).optional(),
instructions: z.string().optional(),
max_output_tokens: z.number().int().positive().optional(),
metadata: z.record(z.string(), z.string()).optional(),
parallel_tool_calls: z.boolean().optional(),
previous_response_id: z.string().optional(),
reasoning: ReasoningConfigSchema.optional(),
store: z.boolean().optional(),
temperature: z.number().optional(),
tool_choice: z.union([z.string(), z.object({}).passthrough()]).optional(),
tools: z.array(z.any()).optional(),
top_p: z.number().optional(),
truncation: z.enum(["auto", "disabled"]).optional(),
user: z.string().optional(),
service_tier: ServiceTierSchema.optional(),
stream: z.boolean().optional(),
prompt_cache_key: z.string().optional(),
})
.passthrough(),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/lib/ws/frames.ts | head -100

Repository: ding113/claude-code-hub

Length of output: 4334


🏁 Script executed:

# Search for tests related to ResponseCreateFrameSchema
find . -type f \( -name "*.test.ts" -o -name "*.spec.ts" \) | head -20

Repository: ding113/claude-code-hub

Length of output: 1109


🏁 Script executed:

# Look for usages of ResponseCreateFrameSchema in the codebase
rg "ResponseCreateFrameSchema" -A 3 -B 3

Repository: ding113/claude-code-hub

Length of output: 6276


🏁 Script executed:

# Check if there are any other related schemas that might define input handling
rg "InputItemSchema" -A 2 -B 2

Repository: ding113/claude-code-hub

Length of output: 623


🏁 Script executed:

# Search for ResponseRequest type definition
rg "type ResponseRequest" -A 20

Repository: ding113/claude-code-hub

Length of output: 49


🏁 Script executed:

# Look at the test file to see what input patterns are being tested
cat -n tests/unit/lib/ws/frames.test.ts | grep -A 50 "ResponseCreateFrameSchema"

Repository: ding113/claude-code-hub

Length of output: 7122


🏁 Script executed:

# Search for input field usage in tests
rg "input:" tests/unit/lib/ws/frames.test.ts -B 3 -A 3

Repository: ding113/claude-code-hub

Length of output: 325


🌐 Web query:

OpenAI Responses API input field documentation string array

💡 Result:

In the Responses API (POST /v1/responses), the request body field input is documented as:

  • input: optional string or array of ResponseInputItem — “Text, image, or file inputs to the model, used to generate a response.” [1]

If you pass an array, it’s not an array of plain strings; it’s an array of typed input items (e.g., messages, or content parts like input_text, input_image, input_file). For text parts specifically, the docs define:

  • TextInput = string (single text input), or
  • ResponseInputMessageContentList = array of ResponseInputContent, where a text part is {"type":"input_text","text":"..."}. [2]

Minimal examples

1) Single string input

{ "model": "gpt-4.1", "input": "Hello!" }

[1]

2) Array input with explicit text items

{
  "model": "gpt-4.1",
  "input": [
    {
      "role": "user",
      "content": [
        { "type": "input_text", "text": "Hello!" },
        { "type": "input_text", "text": "Second chunk of text." }
      ]
    }
  ]
}

[2]

Sources: OpenAI API Reference — Responses: Create a response (input type + content item schema). [1] [2]


🏁 Script executed:

# Search more broadly for ResponseRequest
find . -type f \( -name "*.ts" -o -name "*.tsx" \) -path "*/codex/types/*" | head -20

Repository: ding113/claude-code-hub

Length of output: 152


🏁 Script executed:

# Search for where ResponseRequest is defined
rg "type ResponseRequest\s*=" -A 15

Repository: ding113/claude-code-hub

Length of output: 49


🏁 Script executed:

# Check if there are any usages of input as string anywhere in the codebase
rg "input\s*:\s*['\"]" --type ts --type tsx

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

# Check the response.ts file for ResponseRequest definition
cat -n src/app/v1/_lib/codex/types/response.ts | head -100

Repository: ding113/claude-code-hub

Length of output: 2847


🏁 Script executed:

# Also search in compatible.ts
cat -n src/app/v1/_lib/codex/types/compatible.ts | head -100

Repository: ding113/claude-code-hub

Length of output: 2833


🏁 Script executed:

# Search for input type in response-related files
rg "input" src/app/v1/_lib/codex/types/ -A 2 -B 2

Repository: ding113/claude-code-hub

Length of output: 1312


🏁 Script executed:

# Search for any conversion or parsing logic for input field
rg "parseClientFrame\|parseInput\|input.*string" src/ -A 3 -B 3

Repository: ding113/claude-code-hub

Length of output: 49


🏁 Script executed:

# Check if there are any comments about input handling
rg "input" src/lib/ws/frames.ts -B 2 -A 2

Repository: ding113/claude-code-hub

Length of output: 711


response.input 应支持字符串和数组两种格式

OpenAI Responses API 的 input 字段支持单个字符串或数组两种格式,但当前模式仅接受数组。这导致 input: "Hello!" 这类合法请求被拒绝,造成 HTTP/WS 语义不一致。

建议修改为:z.union([z.string(), z.array(InputItemSchema)]).optional(),并补充测试用例验证字符串输入。

建议修改
 export const ResponseCreateFrameSchema = z.object({
   type: z.literal("response.create"),
   response: z
     .object({
       model: z.string().min(1),
-      input: z.array(InputItemSchema).optional(),
+      input: z.union([z.string(), z.array(InputItemSchema)]).optional(),
       instructions: z.string().optional(),
       max_output_tokens: z.number().int().positive().optional(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const ResponseCreateFrameSchema = z.object({
type: z.literal("response.create"),
response: z
.object({
model: z.string().min(1),
input: z.array(InputItemSchema).optional(),
instructions: z.string().optional(),
max_output_tokens: z.number().int().positive().optional(),
metadata: z.record(z.string(), z.string()).optional(),
parallel_tool_calls: z.boolean().optional(),
previous_response_id: z.string().optional(),
reasoning: ReasoningConfigSchema.optional(),
store: z.boolean().optional(),
temperature: z.number().optional(),
tool_choice: z.union([z.string(), z.object({}).passthrough()]).optional(),
tools: z.array(z.any()).optional(),
top_p: z.number().optional(),
truncation: z.enum(["auto", "disabled"]).optional(),
user: z.string().optional(),
service_tier: ServiceTierSchema.optional(),
stream: z.boolean().optional(),
prompt_cache_key: z.string().optional(),
})
.passthrough(),
export const ResponseCreateFrameSchema = z.object({
type: z.literal("response.create"),
response: z
.object({
model: z.string().min(1),
input: z.union([z.string(), z.array(InputItemSchema)]).optional(),
instructions: z.string().optional(),
max_output_tokens: z.number().int().positive().optional(),
metadata: z.record(z.string(), z.string()).optional(),
parallel_tool_calls: z.boolean().optional(),
previous_response_id: z.string().optional(),
reasoning: ReasoningConfigSchema.optional(),
store: z.boolean().optional(),
temperature: z.number().optional(),
tool_choice: z.union([z.string(), z.object({}).passthrough()]).optional(),
tools: z.array(z.any()).optional(),
top_p: z.number().optional(),
truncation: z.enum(["auto", "disabled"]).optional(),
user: z.string().optional(),
service_tier: ServiceTierSchema.optional(),
stream: z.boolean().optional(),
prompt_cache_key: z.string().optional(),
})
.passthrough(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/ws/frames.ts` around lines 41 - 64, The ResponseCreateFrameSchema
currently enforces response.input as an array only; update the schema
(ResponseCreateFrameSchema) so response.input accepts either a string or an
array by replacing the input type with z.union([z.string(),
z.array(InputItemSchema)]).optional() (keep .passthrough() on the parent object)
and add unit tests for the parser/validator to cover both input: "Hello!" and
input: ["..."] cases to ensure HTTP/WS semantics accept single-string input as
well as arrays; reference InputItemSchema when implementing the union so array
validation remains unchanged.

Comment on lines +341 to +344
// Responses WebSocket 配置字段(如果提供)
if (payload.enableResponsesWebSocket !== undefined) {
updates.enableResponsesWebSocket = payload.enableResponsesWebSocket;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

enableResponsesWebSocket 一并放进 returning(...) 投影。

这里已经把 payload.enableResponsesWebSocket 写进 updates,但更新后的返回行没有选出这个列,toSystemSettings(updated) 只能看到 undefined 并回退默认值。结果是写库成功后,调用方马上拿到的仍可能是 false,保存开关时 UI/缓存状态会立刻反弹。

建议修改
         verboseProviderError: systemSettings.verboseProviderError,
         enableHttp2: systemSettings.enableHttp2,
+        enableResponsesWebSocket: systemSettings.enableResponsesWebSocket,
         interceptAnthropicWarmupRequests: systemSettings.interceptAnthropicWarmupRequests,

Also applies to: 409-442

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/repository/system-config.ts` around lines 341 - 344, The update routine
sets payload.enableResponsesWebSocket into updates but doesn’t include that
column in the returning(...) projection, so toSystemSettings(updated) receives
undefined; modify the DB update call to include enableResponsesWebSocket in the
returning(...) list (the same projection used for other fields) so the updated
row returned to toSystemSettings contains the real value for
enableResponsesWebSocket, and verify the toSystemSettings/updates mapping uses
the same property name.

Comment on lines +7 to +8
const hostname = process.env.HOSTNAME || "0.0.0.0";
const port = parseInt(process.env.PORT || "3000", 10);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

不要用 process.env.HOSTNAME 作为监听地址。

在容器和 Kubernetes 环境里,HOSTNAME 通常是 pod/container 名称,不是可绑定的地址;这里会让服务优先绑定到一个随机主机名,启动时很容易直接失败。建议改成专门的 HOST 变量,默认仍然回落到 0.0.0.0

建议修改
-const hostname = process.env.HOSTNAME || "0.0.0.0";
+const hostname = process.env.HOST || "0.0.0.0";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const hostname = process.env.HOSTNAME || "0.0.0.0";
const port = parseInt(process.env.PORT || "3000", 10);
const hostname = process.env.HOST || "0.0.0.0";
const port = parseInt(process.env.PORT || "3000", 10);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/index.ts` around lines 7 - 8, The code uses process.env.HOSTNAME
for the bind address (const hostname), which is unsafe in containers; change it
to read from process.env.HOST with a fallback to "0.0.0.0" (i.e., replace
references to process.env.HOSTNAME in the hostname initialization with
process.env.HOST || "0.0.0.0") so the server binds to a proper network interface
instead of a pod/container name; keep the existing parseInt for port unchanged.

Comment on lines +24 to +39
// Placeholder connection handler (will be replaced by ingress-handler in T6)
wsManager.onConnection((ws, req) => {
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
console.log(`[WS] New connection on ${url.pathname}`);

ws.on("message", () => {
ws.send(
JSON.stringify({
type: "error",
error: {
type: "server_error",
message: "WebSocket ingress not yet initialized",
},
})
);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

这里还是占位 WS 处理,/v1/responses 实际不会工作。

当前连接建立后没有接入真正的 ingress / bridge 流程,任意消息都会被回成 server_error: "WebSocket ingress not yet initialized"。如果这个文件就是生产入口,那么开关打开后 WebSocket 模式会在建连后立刻失败。请在这里接线真实的 WsIngressHandler/pipeline,而不是保留占位实现。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/index.ts` around lines 24 - 39, The placeholder WebSocket
connection handler (wsManager.onConnection) immediately replies with a
server_error and never wires the real ingress/bridge; replace the stubbed
message handler with the actual WsIngressHandler/pipeline so connections to
/v1/responses are forwarded to the real ingress logic. Locate the
wsManager.onConnection callback, instantiate or retrieve the WsIngressHandler
(or existing pipeline) and attach it to the ws connection and req (handing over
message/event handling and close/error propagation), removing the current
hardcoded ws.on("message", ...) that returns the "WebSocket ingress not yet
initialized" error.

Comment on lines +43 to +47
const shutdown = async () => {
console.log("[Server] Shutting down...");
await wsManager.close();
server.close();
process.exit(0);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

等待 HTTP server 真正关闭后再退出进程。

server.close() 是异步的,这里马上 process.exit(0) 会截断仍在处理中的 HTTP 请求,和“graceful shutdown”的目标相反。至少要把 server.close 包成 Promise 并等待完成。

建议修改
   const shutdown = async () => {
     console.log("[Server] Shutting down...");
     await wsManager.close();
-    server.close();
+    await new Promise<void>((resolve, reject) => {
+      server.close((error) => {
+        if (error) reject(error);
+        else resolve();
+      });
+    });
     process.exit(0);
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const shutdown = async () => {
console.log("[Server] Shutting down...");
await wsManager.close();
server.close();
process.exit(0);
const shutdown = async () => {
console.log("[Server] Shutting down...");
await wsManager.close();
await new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) reject(error);
else resolve();
});
});
process.exit(0);
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/index.ts` around lines 43 - 47, The shutdown function currently
calls server.close() and immediately process.exit(0), which can terminate
in-flight HTTP requests; change shutdown to await server.close by wrapping
server.close in a Promise (e.g., new Promise((resolve, reject) =>
server.close(err => err ? reject(err) : resolve()))) before calling
process.exit(0), keep awaiting wsManager.close() as is, and handle errors from
the awaited server.close to log and exit with an appropriate code instead of
exiting immediately.

"build": "next build && (node scripts/copy-version-to-standalone.cjs || bun scripts/copy-version-to-standalone.cjs)",
"start": "next start",
"start:ws": "node --import tsx src/server/index.ts",
"lint": "biome check .",
Copy link
Contributor

@github-actions github-actions bot Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] [LOGIC-BUG] start:ws will fail in clean installs (missing tsx)

Evidence (package.json:9):

"start:ws": "node --import tsx src/server/index.ts"

Why this is a problem: node --import tsx ... requires the tsx loader to be installed in node_modules. tsx is not listed in dependencies/devDependencies, so bun run start:ws breaks in CI/containers and for any fresh checkout.

Suggested fix (prefer Bun since this repo already uses Bun):

{
  "scripts": {
    "start:ws": "bun src/server/index.ts"
  }
}


// Placeholder connection handler (will be replaced by ingress-handler in T6)
wsManager.onConnection((ws, req) => {
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] [LOGIC-BUG] WS connections are hard-wired to a placeholder handler

Evidence (src/server/index.ts:25):

wsManager.onConnection((ws, req) => {
  // ...
  message: "WebSocket ingress not yet initialized",
});

Why this is a problem: Starting the custom server will accept /v1/responses upgrades, but every client message is answered with a hardcoded error frame. This makes the WS endpoint unusable even when the system toggle is enabled.

Suggested fix: Replace the placeholder with the real ingress registration (and only expose start:ws / Docker entrypoint once WsIngressHandler.handleTurn() is implemented).

import { registerIngressHandler } from "../app/v1/_lib/ws/ingress-handler";

// ...
const wsManager = new WsManager(server);
registerIngressHandler(wsManager);

const apiKey = extractApiKeyFromHeaders({
authorization: this.req.headers.authorization ?? null,
"x-api-key": (this.req.headers["x-api-key"] as string) ?? null,
"x-goog-api-key": (this.req.headers["x-goog-api-key"] as string) ?? null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] [LOGIC-BUG] Unsafe header casts can crash auth parsing on duplicate headers

Evidence (src/app/v1/_lib/ws/ingress-handler.ts:101):

"x-api-key": (this.req.headers["x-api-key"] as string) ?? null,

Why this is a problem: In Node, IncomingMessage.headers[...] values can be string | string[] | undefined. The as string cast does not coerce at runtime; if a client sends duplicate x-api-key (or x-goog-api-key) headers, this becomes an array and extractApiKeyFromHeaders() will throw when it calls .trim(). That turns a malformed client request into a server-side exception.

Suggested fix: Normalize header values before passing them into extractApiKeyFromHeaders().

const xApiKey = this.req.headers["x-api-key"];
const xGoogApiKey = this.req.headers["x-goog-api-key"];
const authorization = this.req.headers.authorization;

const apiKey = extractApiKeyFromHeaders({
  authorization: typeof authorization === "string" ? authorization : null,
  "x-api-key": typeof xApiKey === "string" ? xApiKey : null,
  "x-goog-api-key": typeof xGoogApiKey === "string" ? xGoogApiKey : null,
});

options?: IngressHandlerOptions
): void {
wsManager.onConnection(async (ws, req) => {
const handler = new WsIngressHandler(ws, req, options);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] [ERROR-SILENT] Unhandled rejection risk in registerIngressHandler connection callback

Evidence (src/app/v1/_lib/ws/ingress-handler.ts:356):

wsManager.onConnection(async (ws, req) => {
  const handler = new WsIngressHandler(ws, req, options);
  const ok = await handler.start();
  // ...
});

Why this is a problem: WsManager.onConnection() does not await the handler return value. If handler.start() throws (DB outage, settings cache error, unexpected repo error), this becomes an unhandled promise rejection and can crash the process or drop the connection without a controlled close.

Suggested fix: Make the callback non-async and explicitly .catch() initialization failures with logging and a 1011 close.

wsManager.onConnection((ws, req) => {
  const handler = new WsIngressHandler(ws, req, options);
  handler.start().catch((err) => {
    logger.error("[WsIngress] Connection init failed", { error: err });
    ws.close(1011, "Internal server error");
  });
});

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

This PR is extremely large (10,589 LOC delta across 59 files), which makes regression risk and reviewability high. The WS server/ingress path currently contains multiple runtime blockers (placeholder connection handler, unsafe header assumptions, and unhandled async init), so enabling or relying on /v1/responses WebSocket transport is likely to fail in real deployments.

PR Size: XL

  • Lines changed: 10589
  • Files changed: 59

Split suggestions (recommended for XL):

  • PR 1: DB/schema + cache + settings UI + i18n keys for enableResponsesWebSocket
  • PR 2: WS frame schemas/parser + unit tests
  • PR 3: Provider WS probe + UI surface (WsTestStatus) + tests
  • PR 4: Custom server entry + WsManager + ingress wiring + production entrypoint changes
  • PR 5: (If intended) turn bridging/outbound adapter/event bridge/billing parity/session continuity integration

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 3 0 0
Security 0 0 0 0
Error Handling 0 1 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

Critical Issues (Must Fix)

None.

High Priority Issues (Should Fix)

  • package.json:9 (confidence 90): start:ws uses node --import tsx ... but tsx is not declared, so the command fails in clean installs.
  • src/server/index.ts:25 (confidence 88): WS connections are wired to a placeholder handler that always returns an error frame; additionally, even after wiring ingress, WsIngressHandler.handleTurn() is still a placeholder.
  • src/app/v1/_lib/ws/ingress-handler.ts:101 (confidence 86): IncomingMessage.headers[...] can be string[]; current casts can throw when .trim() is called.
  • src/app/v1/_lib/ws/ingress-handler.ts:356 (confidence 85): wsManager.onConnection(async ...) can create unhandled promise rejections if init throws.

Review Coverage

  • Logic and correctness
  • Security (OWASP Top 10)
  • Error handling
  • Type safety
  • Documentation accuracy
  • Test coverage
  • Code clarity

Automated review by Codex AI

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied the size/XL label to PR #887 and submitted the review.

Inline review comments posted (4 total), focused on diff lines with high-confidence runtime blockers:

  • package.json:9 (start:ws depends on undeclared tsx)
  • src/server/index.ts:25 (WS path is still a placeholder handler)
  • src/app/v1/_lib/ws/ingress-handler.ts:101 (unsafe header casts can throw on string[] headers)
  • src/app/v1/_lib/ws/ingress-handler.ts:356 (async connection callback can create unhandled rejections)

A full summary review comment was also posted on the PR (includes XL split suggestions and the issue table).

Connect all WebSocket components into a working end-to-end pipeline:
- Add ProxySession.fromWebSocket() factory for synthetic sessions
- Implement handleTurn() with guard pipeline, outbound adapter, event
  relay, billing settlement, and session continuity
- Wire response.cancel to terminate upstream connections
- Add integration tests for full orchestration flow
Comment on lines +66 to +74
export function toWebSocketUrl(providerBaseUrl: string): string {
const url = new URL(providerBaseUrl);
url.protocol = "wss:";
// Ensure path ends with /v1/responses
if (!url.pathname.endsWith("/v1/responses")) {
url.pathname = url.pathname.replace(/\/$/, "") + "/v1/responses";
}
return url.toString();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path double-append bug when base URL includes /v1

toWebSocketUrl appends /v1/responses to the URL's existing pathname without clearing it first. When a provider is configured with the common base URL pattern https://api.openai.com/v1, the resulting WebSocket URL becomes wss://api.openai.com/v1/v1/responses instead of the correct wss://api.openai.com/v1/responses.

Example:

Input:  https://api.openai.com/v1
Output: wss://api.openai.com/v1/v1/responses  ← wrong
Wanted: wss://api.openai.com/v1/responses

The fix is to reset the pathname to the target path:

export function toWebSocketUrl(providerBaseUrl: string): string {
  const url = new URL(providerBaseUrl);
  url.protocol = "wss:";
  url.pathname = "/v1/responses";  // Always set to target, don't append
  return url.toString();
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/transport-classifier.ts
Line: 66-74

Comment:
**Path double-append bug when base URL includes `/v1`**

`toWebSocketUrl` appends `/v1/responses` to the URL's existing pathname without clearing it first. When a provider is configured with the common base URL pattern `https://api.openai.com/v1`, the resulting WebSocket URL becomes `wss://api.openai.com/v1/v1/responses` instead of the correct `wss://api.openai.com/v1/responses`.

Example:
```
Input:  https://api.openai.com/v1
Output: wss://api.openai.com/v1/v1/responses  ← wrong
Wanted: wss://api.openai.com/v1/responses
```

The fix is to reset the pathname to the target path:
```ts
export function toWebSocketUrl(providerBaseUrl: string): string {
  const url = new URL(providerBaseUrl);
  url.protocol = "wss:";
  url.pathname = "/v1/responses";  // Always set to target, don't append
  return url.toString();
}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +24 to +40
// Placeholder connection handler (will be replaced by ingress-handler in T6)
wsManager.onConnection((ws, req) => {
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
console.log(`[WS] New connection on ${url.pathname}`);

ws.on("message", () => {
ws.send(
JSON.stringify({
type: "error",
error: {
type: "server_error",
message: "WebSocket ingress not yet initialized",
},
})
);
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Placeholder handler is never replaced with registerIngressHandler

src/server/index.ts registers a placeholder onConnection handler (lines 25–40) but never calls registerIngressHandler from src/app/v1/_lib/ws/ingress-handler.ts (exported at line 521). The WsIngressHandler class implements auth validation, toggle checking, and connection state management, but the real handler is never integrated. The placeholder currently returns "WebSocket ingress not yet initialized" to all messages, bypassing the auth and toggle checks that WsIngressHandler.start() would perform.

Replace the placeholder handler with a call to registerIngressHandler:

import { registerIngressHandler } from "@/app/v1/_lib/ws/ingress-handler";

// Replace lines 25-40 with:
registerIngressHandler(wsManager);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/server/index.ts
Line: 24-40

Comment:
**Placeholder handler is never replaced with `registerIngressHandler`**

`src/server/index.ts` registers a placeholder `onConnection` handler (lines 25–40) but never calls `registerIngressHandler` from `src/app/v1/_lib/ws/ingress-handler.ts` (exported at line 521). The `WsIngressHandler` class implements auth validation, toggle checking, and connection state management, but the real handler is never integrated. The placeholder currently returns `"WebSocket ingress not yet initialized"` to all messages, bypassing the auth and toggle checks that `WsIngressHandler.start()` would perform.

Replace the placeholder handler with a call to `registerIngressHandler`:
```ts
import { registerIngressHandler } from "@/app/v1/_lib/ws/ingress-handler";

// Replace lines 25-40 with:
registerIngressHandler(wsManager);
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +497 to +508
function extractClientIp(req: IncomingMessage): string {
const realIp = req.headers["x-real-ip"];
if (typeof realIp === "string" && realIp.trim()) return realIp.trim();

const forwarded = req.headers["x-forwarded-for"];
if (typeof forwarded === "string") {
const ips = forwarded
.split(",")
.map((s) => s.trim())
.filter(Boolean);
if (ips.length > 0) return ips[ips.length - 1];
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

x-forwarded-for takes last hop instead of first

In extractClientIp at line 507:

if (ips.length > 0) return ips[ips.length - 1];

The x-forwarded-for header is structured as client, proxy1, proxy2, …. The first entry is the originating client IP. The last entry is the most recently appended proxy, which can be forged if the ingress doesn't validate the header. This should use the first entry for correct client IP identification:

if (ips.length > 0) return ips[0];
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/ws/ingress-handler.ts
Line: 497-508

Comment:
**`x-forwarded-for` takes last hop instead of first**

In `extractClientIp` at line 507:
```ts
if (ips.length > 0) return ips[ips.length - 1];
```

The `x-forwarded-for` header is structured as `client, proxy1, proxy2, …`. The **first** entry is the originating client IP. The last entry is the most recently appended proxy, which can be forged if the ingress doesn't validate the header. This should use the first entry for correct client IP identification:

```ts
if (ips.length > 0) return ips[0];
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +342 to +360
// 5. Execute turn via outbound adapter
const adapter = new OutboundWsAdapter({
providerBaseUrl: provider.url,
apiKey: provider.key,
});
this.activeAdapter = adapter;

try {
const turnResult = await adapter.executeTurn(frame.response as Record<string, unknown>);

// 6. Relay all events to client via event bridge
const bridge = new WsEventBridge();
for (const event of turnResult.events) {
bridge.relayEvent(
this.ws,
event as { type: string; data: unknown },
JSON.stringify(event.data)
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Events batched after turn completes — no real-time streaming

adapter.executeTurn() awaits the entire upstream response before returning (it resolves only when a terminal event arrives). The events array collected inside executeTurn() is then replayed to the client in a tight loop after the turn is complete (lines 354–360). This means the client receives no events until the upstream LLM finishes generating, then gets all chunks in a rapid burst — the opposite of real-time streaming.

For a working WS streaming proxy, events must be forwarded to the client as they arrive from the upstream socket. The OutboundWsAdapter needs a callback/sink interface so the ingress-handler can relay each event immediately:

// Example: pass a relay callback into executeTurn
const turnResult = await adapter.executeTurn(
  frame.response as Record<string, unknown>,
  (event, rawJson) => {
    bridge.relayEvent(this.ws, event, rawJson);
  }
);

And inside OutboundWsAdapter.executeTurn(), call the callback in the "message" handler (around line 186) instead of (or in addition to) pushing to events:

this.ws.on("message", (data: Buffer | string) => {
  // ...parse...
  const event = { type: eventType, data: parsed };
  events.push(event);
  onEvent?.(event, raw);  // ← relay immediately
  // ...terminal check...
});

Without this change, every response will appear to hang for its full generation time before any output is visible — making the WebSocket transport behaviorally identical to a slow HTTP request from the client's perspective.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/ws/ingress-handler.ts
Line: 342-360

Comment:
**Events batched after turn completes — no real-time streaming**

`adapter.executeTurn()` awaits the **entire upstream response** before returning (it resolves only when a terminal event arrives). The `events` array collected inside `executeTurn()` is then replayed to the client in a tight loop after the turn is complete (lines 354–360). This means the client receives no events until the upstream LLM finishes generating, then gets all chunks in a rapid burst — the opposite of real-time streaming.

For a working WS streaming proxy, events must be forwarded to the client **as they arrive** from the upstream socket. The `OutboundWsAdapter` needs a callback/sink interface so the `ingress-handler` can relay each event immediately:

```ts
// Example: pass a relay callback into executeTurn
const turnResult = await adapter.executeTurn(
  frame.response as Record<string, unknown>,
  (event, rawJson) => {
    bridge.relayEvent(this.ws, event, rawJson);
  }
);
```

And inside `OutboundWsAdapter.executeTurn()`, call the callback in the `"message"` handler (around line 186) instead of (or in addition to) pushing to `events`:
```ts
this.ws.on("message", (data: Buffer | string) => {
  // ...parse...
  const event = { type: eventType, data: parsed };
  events.push(event);
  onEvent?.(event, raw);  // ← relay immediately
  // ...terminal check...
});
```

Without this change, every response will appear to hang for its full generation time before any output is visible — making the WebSocket transport behaviorally identical to a slow HTTP request from the client's perspective.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +97 to +100
async executeTurn(requestBody: Record<string, unknown>): Promise<OutboundTurnResult> {
const events: Array<{ type: string; data: unknown }> = [];
const wsUrl = toWebSocketUrl(this.opts.providerBaseUrl);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unbounded events array may cause high memory usage on long turns

The events array (line 98) accumulates every upstream server-sent event with no capacity limit. A verbose model (e.g. long chain-of-thought with many response.output_text.delta events) could produce tens of thousands of entries; at scale this results in significant heap pressure per concurrent connection.

Consider either capping the array (e.g. keep only the last N events for debugging purposes, similar to how WsEventBridge uses a ring buffer at line 79) or removing event accumulation from the adapter entirely once event relay is moved to a real-time callback (see comment on ingress-handler.ts). If events need to be replayed, the ring buffer in WsEventBridge is the right place for that bounded storage.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/ws/outbound-adapter.ts
Line: 97-100

Comment:
**Unbounded `events` array may cause high memory usage on long turns**

The `events` array (line 98) accumulates every upstream server-sent event with no capacity limit. A verbose model (e.g. long chain-of-thought with many `response.output_text.delta` events) could produce tens of thousands of entries; at scale this results in significant heap pressure per concurrent connection.

Consider either capping the array (e.g. keep only the last N events for debugging purposes, similar to how `WsEventBridge` uses a ring buffer at line 79) or removing event accumulation from the adapter entirely once event relay is moved to a real-time callback (see comment on `ingress-handler.ts`). If events need to be replayed, the ring buffer in `WsEventBridge` is the right place for that bounded storage.

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (4)
src/app/v1/_lib/ws/ingress-handler.ts (4)

501-511: ⚠️ Potential issue | 🔴 Critical

x-forwarded-for 取值顺序错误,会导致限流/审计使用错误的 IP。

标准 x-forwarded-for 格式为 client, proxy1, proxy2,最左侧是原始客户端 IP。当前代码返回 ips[ips.length - 1](最后一个/最近代理的 IP),而非 ips[0](真实客户端 IP)。

这会影响:

  • 基于 IP 的限流决策
  • 审计日志的准确性
  • 任何依赖客户端 IP 的 provider 决策
修复建议
 function extractClientIp(req: IncomingMessage): string {
   const realIp = req.headers["x-real-ip"];
   if (typeof realIp === "string" && realIp.trim()) return realIp.trim();

   const forwarded = req.headers["x-forwarded-for"];
   if (typeof forwarded === "string") {
     const ips = forwarded
       .split(",")
       .map((s) => s.trim())
       .filter(Boolean);
-    if (ips.length > 0) return ips[ips.length - 1];
+    if (ips.length > 0) return ips[0];
   }

   return req.socket?.remoteAddress ?? "unknown";
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/ws/ingress-handler.ts` around lines 501 - 511, The code
incorrectly treats the last entry of the parsed x-forwarded-for header as the
client IP; change the logic to return the first non-empty entry instead (use
ips[0] rather than ips[ips.length - 1]) so the original client IP is used for
rate-limiting/auditing; keep the existing parsing (split on ",", trim,
filter(Boolean)) and the fallback to req.socket?.remoteAddress ?? "unknown"
unchanged, and update any function that uses the forwarded/ips logic (the
variable forwarded and the parsed ips array in this ingress-handler) to return
the left-most IP.

109-113: ⚠️ Potential issue | 🟠 Major

Header 类型强转可能导致重复 header 时运行时崩溃。

Node.js 的 IncomingMessage.headers[key] 类型是 string | string[] | undefined。当客户端发送重复的 x-api-keyx-goog-api-key header 时,值会变成 string[]。此时 as string 强转不会在运行时生效,后续 extractApiKeyFromHeaders 内部调用 .trim() 会抛出 TypeError

修复建议:在传入前规范化 header 值
 // 2. Authenticate using request headers
+const xApiKey = this.req.headers["x-api-key"];
+const xGoogApiKey = this.req.headers["x-goog-api-key"];
 const apiKey = extractApiKeyFromHeaders({
   authorization: this.req.headers.authorization ?? null,
-  "x-api-key": (this.req.headers["x-api-key"] as string) ?? null,
-  "x-goog-api-key": (this.req.headers["x-goog-api-key"] as string) ?? null,
+  "x-api-key": typeof xApiKey === "string" ? xApiKey : null,
+  "x-goog-api-key": typeof xGoogApiKey === "string" ? xGoogApiKey : null,
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/ws/ingress-handler.ts` around lines 109 - 113, The headers
for "x-api-key" and "x-goog-api-key" are being force-cast to string which will
throw if the incoming header is a string[]; before calling
extractApiKeyFromHeaders, normalize these header values from this.req.headers by
checking for undefined, string, or string[] and convert to a single trimmed
string or null (e.g., pick the first value or join with ',' then trim) so
extractApiKeyFromHeaders always receives string|null and cannot trigger a
.trim() TypeError; update the call site that builds the object passed to
extractApiKeyFromHeaders to perform this normalization for "x-api-key" and
"x-goog-api-key" (and optionally authorization) instead of using as string
casts.

144-148: ⚠️ Potential issue | 🟡 Minor

Debug 日志包含 PII(用户名和客户端 IP)。

userNameclientIp 属于用户可识别信息,记录在 WS 连接路径的日志中会增加泄露风险。建议:

  • 移除 userName,仅保留 userId
  • 对 IP 进行截断或哈希处理
修复建议
 logger.debug("[WsIngress] Authenticated", {
   userId: authResult.user.id,
-  userName: authResult.user.name,
-  clientIp: this.ip,
+  // 如需 IP 用于调试,考虑使用哈希或截断
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/ws/ingress-handler.ts` around lines 144 - 148, The debug log
in WsIngress currently writes PII (authResult.user.name and this.ip); modify the
logger.debug call in ingress-handler.ts (the call referencing logger.debug and
authResult.user) to remove userName and only include authResult.user.id, and
replace this.ip with a non-PII form (e.g., truncate or hash the IP before
logging — implement a helper function like truncateIp(ip) or hashIp(ip) and call
it where this.ip is used) so the log contains userId and a redacted/hashed
client IP instead of raw PII.

521-531: ⚠️ Potential issue | 🟠 Major

registerIngressHandler 的 async 回调可能产生未处理的 Promise rejection。

wsManager.onConnection() 不会 await 回调返回值(参见 ws-manager.ts:56-65)。如果 handler.start() 抛出异常(例如 isResponsesWebSocketEnabled() 的缓存/数据库错误),会导致未处理的 Promise rejection,可能使进程崩溃或留下未清理的连接。

修复建议:改用非 async 回调并显式 .catch()
 export function registerIngressHandler(
   wsManager: import("@/server/ws-manager").WsManager,
   options?: IngressHandlerOptions
 ): void {
-  wsManager.onConnection(async (ws, req) => {
+  wsManager.onConnection((ws, req) => {
     const handler = new WsIngressHandler(ws, req, options);
-    const ok = await handler.start();
-    if (!ok) {
-      logger.debug("[WsIngress] Connection rejected during init");
-    }
+    handler.start().then((ok) => {
+      if (!ok) {
+        logger.debug("[WsIngress] Connection rejected during init");
+      }
+    }).catch((err) => {
+      logger.error("[WsIngress] Connection init failed", { error: err });
+      ws.close(1011, "Internal server error");
+    });
   });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/ws/ingress-handler.ts` around lines 521 - 531,
registerIngressHandler uses an async callback for wsManager.onConnection which
can produce unhandled Promise rejections if handler.start() throws; change the
callback to a non-async function and call handler.start().catch(...) (or attach
.then(...) to handle the boolean return and .catch to log and clean up) so all
errors are handled. Specifically, update the wsManager.onConnection callback
that creates new WsIngressHandler(ws, req, options) to invoke handler.start()
with an explicit .catch that logs the error (use logger.error or logger.debug)
and ensures any necessary cleanup/close of ws to avoid leaking connections;
reference WsIngressHandler, registerIngressHandler, wsManager.onConnection, and
handler.start when locating the change.
🧹 Nitpick comments (1)
tests/unit/ws/ingress-handler.test.ts (1)

224-241: 建议补充 x-forwarded-for 的 IP 提取测试。

当前测试覆盖了 x-real-ip 优先级,但缺少对 x-forwarded-for 多 IP 情况的测试。鉴于当前实现存在取值顺序 bug(返回最后一个 IP 而非第一个),建议添加测试用例以验证修复后的行为:

test("extracts first IP from x-forwarded-for chain", async () => {
  const ws = createMockWs();
  const handler = new WsIngressHandler(
    ws as any,
    createMockReq({ "x-forwarded-for": "203.0.113.50, 10.0.0.1, 192.168.1.1" })
  );
  await handler.start();

  expect(handler.clientIp).toBe("203.0.113.50");
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/ws/ingress-handler.test.ts` around lines 224 - 241, Add a unit
test that verifies WsIngressHandler extracts the first IP from an
x-forwarded-for header chain: create a mock socket with createMockWs(),
construct a WsIngressHandler with createMockReq({ "x-forwarded-for":
"203.0.113.50, 10.0.0.1, 192.168.1.1" }), call handler.start(), and assert
handler.clientIp === "203.0.113.50"; this will catch the current bug where the
last IP is returned instead of the first.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/app/v1/_lib/ws/ingress-handler.ts`:
- Around line 501-511: The code incorrectly treats the last entry of the parsed
x-forwarded-for header as the client IP; change the logic to return the first
non-empty entry instead (use ips[0] rather than ips[ips.length - 1]) so the
original client IP is used for rate-limiting/auditing; keep the existing parsing
(split on ",", trim, filter(Boolean)) and the fallback to
req.socket?.remoteAddress ?? "unknown" unchanged, and update any function that
uses the forwarded/ips logic (the variable forwarded and the parsed ips array in
this ingress-handler) to return the left-most IP.
- Around line 109-113: The headers for "x-api-key" and "x-goog-api-key" are
being force-cast to string which will throw if the incoming header is a
string[]; before calling extractApiKeyFromHeaders, normalize these header values
from this.req.headers by checking for undefined, string, or string[] and convert
to a single trimmed string or null (e.g., pick the first value or join with ','
then trim) so extractApiKeyFromHeaders always receives string|null and cannot
trigger a .trim() TypeError; update the call site that builds the object passed
to extractApiKeyFromHeaders to perform this normalization for "x-api-key" and
"x-goog-api-key" (and optionally authorization) instead of using as string
casts.
- Around line 144-148: The debug log in WsIngress currently writes PII
(authResult.user.name and this.ip); modify the logger.debug call in
ingress-handler.ts (the call referencing logger.debug and authResult.user) to
remove userName and only include authResult.user.id, and replace this.ip with a
non-PII form (e.g., truncate or hash the IP before logging — implement a helper
function like truncateIp(ip) or hashIp(ip) and call it where this.ip is used) so
the log contains userId and a redacted/hashed client IP instead of raw PII.
- Around line 521-531: registerIngressHandler uses an async callback for
wsManager.onConnection which can produce unhandled Promise rejections if
handler.start() throws; change the callback to a non-async function and call
handler.start().catch(...) (or attach .then(...) to handle the boolean return
and .catch to log and clean up) so all errors are handled. Specifically, update
the wsManager.onConnection callback that creates new WsIngressHandler(ws, req,
options) to invoke handler.start() with an explicit .catch that logs the error
(use logger.error or logger.debug) and ensures any necessary cleanup/close of ws
to avoid leaking connections; reference WsIngressHandler,
registerIngressHandler, wsManager.onConnection, and handler.start when locating
the change.

---

Nitpick comments:
In `@tests/unit/ws/ingress-handler.test.ts`:
- Around line 224-241: Add a unit test that verifies WsIngressHandler extracts
the first IP from an x-forwarded-for header chain: create a mock socket with
createMockWs(), construct a WsIngressHandler with createMockReq({
"x-forwarded-for": "203.0.113.50, 10.0.0.1, 192.168.1.1" }), call
handler.start(), and assert handler.clientIp === "203.0.113.50"; this will catch
the current bug where the last IP is returned instead of the first.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f010be5c-fa8c-443d-b768-251cd717faea

📥 Commits

Reviewing files that changed from the base of the PR and between f768c63 and 30fb85c.

📒 Files selected for processing (4)
  • src/app/v1/_lib/proxy/session.ts
  • src/app/v1/_lib/ws/ingress-handler.ts
  • tests/unit/ws/ingress-handler-integration.test.ts
  • tests/unit/ws/ingress-handler.test.ts

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

Labels

area:core area:OpenAI area:provider area:UI enhancement New feature or request size/XL Extra Large PR (> 1000 lines)

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant