Skip to content

Add WebSocket relay support for /v1/responses - #3370

Closed
ray7086 wants to merge 1 commit into
QuantumNous:mainfrom
ray7086:fix/ws-responses-relay
Closed

Add WebSocket relay support for /v1/responses#3370
ray7086 wants to merge 1 commit into
QuantumNous:mainfrom
ray7086:fix/ws-responses-relay

Conversation

@ray7086

@ray7086 ray7086 commented Mar 21, 2026

Copy link
Copy Markdown

Summary

  • add WebSocket routing for GET /v1/responses
  • select the upstream channel for Responses WS requests from the first client message
  • relay Responses WS traffic end-to-end and ensure the initial frame includes type: "response.create"

Problem

new-api handled OpenAI Realtime over WebSocket, but Responses WS was incomplete:

  • /v1/responses had no WS route
  • channel selection could not work before upgrade because there is no HTTP body to read
  • the first Responses WS frame needs type: "response.create" at the top level

This caused the first request to downgrade or fail instead of staying on WebSocket when the upstream supported Responses WS.

Verification

  • go test ./controller ./middleware ./relay/... ./dto
  • verified /v1/responses over WS returns standard Responses events end-to-end against a CPA upstream

Summary by CodeRabbit

  • New Features
    • Added WebSocket support for the Responses API endpoint, enabling real-time bidirectional communication.
    • Added support for optional Type field in Responses API requests.

@coderabbitai

coderabbitai Bot commented Mar 21, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

The changes introduce a new WebSocket relay flow for OpenAI's Responses API. A new route handler processes Responses WebSocket upgrades by parsing the first message for model and input validation, setting up appropriate downstream channels with optional token-based model enforcement, and establishing bidirectional message proxying between client and target WebSocket connections with billing settlement.

Changes

Cohort / File(s) Summary
Router & Controller
router/relay-router.go, controller/relay.go
Added new GET /v1/.../realtime/responses route with specialized Responses API WebSocket handling; implemented readFirstResponsesWSRequest for upfront message parsing and validation, and setupResponsesWSChannel for downstream channel configuration with optional token→model mapping.
Data Models
dto/openai_request.go
Added optional Type field to OpenAIResponsesRequest to allow callers to specify a top-level type property in Responses API requests.
Middleware Request Processing
middleware/distributor.go
Special-cased HTTP GET WebSocket upgrades for /v1/responses paths to extract model from query string and bypass later channel-selection logic; added compact model suffix support for /v1/responses/compact.
WebSocket Relay Implementation
relay/websocket.go, relay/channel/api_request.go
Implemented WssResponsesHelper for Responses API WebSocket relay with initial request conversion (sendInitialResponsesWSRequest), bidirectional message proxying (proxyResponsesWS), and billing settlement; updated DoWssRequest to convert upstream URLs from https:///http:// to wss:///ws:// schemes.

Sequence Diagram

sequenceDiagram
    participant Client as Client (WebSocket)
    participant Router as Router/Controller
    participant Middleware as Middleware
    participant Relay as Relay Handler
    participant Channel as Channel/Adaptor
    participant Target as Target (WebSocket)

    Client->>Router: GET /v1/responses?model=gpt-4
    Router->>Middleware: Extract model from query
    Middleware->>Router: Return model name
    Router->>Router: Read first WebSocket message
    Router->>Router: Parse & validate (model, input)
    Router->>Relay: Setup Responses channel with model
    Relay->>Channel: Initialize adaptor
    Relay->>Target: Establish WebSocket connection<br/>(URL scheme: https→wss)
    Relay->>Target: Send initial request (type: response.create)
    Target->>Relay: Acknowledge connection
    
    loop Bidirectional Message Proxying
        alt Message from Client
            Client->>Relay: Forward message
            Relay->>Target: Proxy to target
            Target->>Relay: Response
            Relay->>Client: Proxy back to client
        else Message from Target
            Target->>Relay: Stream response
            Relay->>Client: Proxy to client
        end
    end
    
    Client->>Relay: Close connection
    Relay->>Relay: Settle billing
    Relay->>Target: Send close frame
    Target->>Relay: Acknowledge close
    Relay->>Client: Connection closed
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related PRs

  • #2897: Adds/extends OpenAI Responses flow support with channel-level responses handling and adaptor conversion logic.
  • #1270: Modifies OpenAI responses relay flow with RelayFormatOpenAIResponses handling and model mapping for response requests.

Suggested Reviewers

  • creamlike1024

Poem

🐰 A WebSocket path now springs to life,
Where Responses flow without the strife,
Models matched and messages dance,
Through channels in a graceful prance,
Billing settled as connections close—
The rabbit's newest code grandiose! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and concisely summarizes the main change: adding WebSocket relay support for the /v1/responses endpoint.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Tip

CodeRabbit can scan for known vulnerabilities in your dependencies using OSV Scanner.

OSV Scanner will automatically detect and report security vulnerabilities in your project's dependencies. No additional configuration is required.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/relay.go`:
- Line 4: Replace direct use of encoding/json in the WebSocket request parser by
removing the encoding/json import and swapping json.Unmarshal calls with
common.Unmarshal; specifically, update the code that currently calls
json.Unmarshal(...) in the relay WebSocket request parsing logic to call
common.Unmarshal(...) and adjust error handling accordingly, and ensure the
import list references the package providing common.Unmarshal instead of
"encoding/json".

In `@middleware/distributor.go`:
- Around line 181-188: For WebSocket GET upgrades to /v1/responses, defer model
auth instead of letting the outer Distribute middleware apply token model-limit
checks early: add an explicit flag (e.g., DeferModelAuth bool) to the
modelRequest struct, set modelRequest.DeferModelAuth = true in the websocket
upgrade branch (the block that handles websocket.IsWebSocketUpgrade and returns
&modelRequest), and update the Distribute flow to check
modelRequest.DeferModelAuth and skip the middleware model-limit branch when true
so setupResponsesWSChannel can validate the real model from the first WS frame.

In `@relay/websocket.go`:
- Around line 79-80: The billing settle call currently uses
info.FinalPreConsumedQuota (service.SettleBilling and
info.FinalPreConsumedQuota) which never gets updated because the frame-relay
code that forwards frames (the relay/forwarding loop handling streamed frames)
doesn't extract a terminal usage event; modify the relay/frames-handling logic
to detect the terminal usage/usage-summary frame (or track streamed tokens/bytes
usage as frames are processed), populate or compute the actual final usage value
(update info.FinalPreConsumedQuota or create a new finalUsage variable) before
calling service.SettleBilling, and then call service.SettleBilling with that
actual final usage so the final billing reflects real streamed consumption
rather than the pre-consumed estimate.
- Around line 91-93: The current logic only defaults initialRequest.Type to
"response.create" when empty, allowing a client-supplied non-empty Type to slip
through; change the behavior in the websocket handler so the first upstream
event always has Type "response.create" by unconditionally setting
initialRequest.Type = "response.create" (instead of only when empty) or, if you
prefer validation, explicitly reject any initialRequest.Type !=
"response.create"; update the code that inspects/forwards initialRequest (the
variable named initialRequest in websocket.go) so it no longer forwards
client-provided Types for the first frame.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3e94198c-9756-4f2c-b1a3-d0d090fbc257

📥 Commits

Reviewing files that changed from the base of the PR and between 42846c6 and b3f71c5.

📒 Files selected for processing (6)
  • controller/relay.go
  • dto/openai_request.go
  • middleware/distributor.go
  • relay/channel/api_request.go
  • relay/websocket.go
  • router/relay-router.go

Comment thread controller/relay.go
package controller

import (
"encoding/json"

Copy link
Copy Markdown
Contributor

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:

# Check if the file exists and examine the import section
head -20 controller/relay.go | cat -n

Repository: QuantumNous/new-api

Length of output: 609


🏁 Script executed:

# Check lines 290-300 to see the json.Unmarshal usage
sed -n '290,300p' controller/relay.go | cat -n

Repository: QuantumNous/new-api

Length of output: 511


🏁 Script executed:

# Verify that common.Unmarshal exists in common/json.go
grep -n "func.*Unmarshal" common/json.go | head -5

Repository: QuantumNous/new-api

Length of output: 160


Use common.Unmarshal instead of direct encoding/json in the WebSocket request parser.

Line 4 imports encoding/json and line 294 uses json.Unmarshal directly in business code. Replace with common.Unmarshal to comply with the repo's JSON wrapper contract.

Fix
-	"encoding/json"
 	"errors"
@@
-	if err := json.Unmarshal(payload, request); err != nil {
+	if err := common.Unmarshal(payload, request); err != nil {
 		return nil, fmt.Errorf("parse first websocket message failed: %w", err)
 	}

Per coding guidelines: All JSON marshal/unmarshal operations MUST use wrapper functions from common/json.go (common.Marshal, common.Unmarshal, common.UnmarshalJsonStr, common.DecodeJson, common.GetJsonType). Do NOT directly call encoding/json in business code.

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

In `@controller/relay.go` at line 4, Replace direct use of encoding/json in the
WebSocket request parser by removing the encoding/json import and swapping
json.Unmarshal calls with common.Unmarshal; specifically, update the code that
currently calls json.Unmarshal(...) in the relay WebSocket request parsing logic
to call common.Unmarshal(...) and adjust error handling accordingly, and ensure
the import list references the package providing common.Unmarshal instead of
"encoding/json".

Comment thread middleware/distributor.go
Comment on lines +181 to +188
if c.Request.Method == http.MethodGet &&
websocket.IsWebSocketUpgrade(c.Request) &&
strings.HasPrefix(c.Request.URL.Path, "/v1/responses") {
modelRequest.Model = c.Query("model")
if strings.HasPrefix(c.Request.URL.Path, "/v1/responses/compact") && modelRequest.Model != "" {
modelRequest.Model = ratio_setting.WithCompactModelSuffix(modelRequest.Model)
}
return &modelRequest, false, nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Defer token model checks until the first Responses WS frame is parsed.

Line 188 returns with shouldSelectChannel=false, but the outer Distribute flow still applies token model-limit checks before it looks at that flag. For restricted tokens, a client that sends model only in the first WebSocket frame will be rejected before setupResponsesWSChannel can validate the real model. Please skip the middleware model-limit branch for this path, or carry an explicit “defer model auth” flag into the post-upgrade flow.

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

In `@middleware/distributor.go` around lines 181 - 188, For WebSocket GET upgrades
to /v1/responses, defer model auth instead of letting the outer Distribute
middleware apply token model-limit checks early: add an explicit flag (e.g.,
DeferModelAuth bool) to the modelRequest struct, set modelRequest.DeferModelAuth
= true in the websocket upgrade branch (the block that handles
websocket.IsWebSocketUpgrade and returns &modelRequest), and update the
Distribute flow to check modelRequest.DeferModelAuth and skip the middleware
model-limit branch when true so setupResponsesWSChannel can validate the real
model from the first WS frame.

Comment thread relay/websocket.go
Comment on lines +79 to +80
if err := service.SettleBilling(c, info, info.FinalPreConsumedQuota); err != nil {
logger.LogError(c, "responses websocket settle billing failed: "+err.Error())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This path never settles against actual streamed usage.

Line 79 passes info.FinalPreConsumedQuota into service.SettleBilling, and Lines 108-151 only relay frames; they never extract a terminal usage event to update that value. On successful sessions, the pre-consumed estimate therefore becomes the final bill, so any delta between reserved quota and actual usage is lost.

Also applies to: 108-151

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

In `@relay/websocket.go` around lines 79 - 80, The billing settle call currently
uses info.FinalPreConsumedQuota (service.SettleBilling and
info.FinalPreConsumedQuota) which never gets updated because the frame-relay
code that forwards frames (the relay/forwarding loop handling streamed frames)
doesn't extract a terminal usage event; modify the relay/frames-handling logic
to detect the terminal usage/usage-summary frame (or track streamed tokens/bytes
usage as frames are processed), populate or compute the actual final usage value
(update info.FinalPreConsumedQuota or create a new finalUsage variable) before
calling service.SettleBilling, and then call service.SettleBilling with that
actual final usage so the final billing reflects real streamed consumption
rather than the pre-consumed estimate.

Comment thread relay/websocket.go
Comment on lines +91 to +93
if initialRequest.Type == "" {
initialRequest.Type = "response.create"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Always force the first upstream event to response.create.

Lines 91-93 only default Type when it is empty. If the client sends any other non-empty value, this helper will still forward that as the first upstream frame, which breaks the exact protocol guarantee this PR is trying to add. Reject non-response.create values or overwrite the field unconditionally here.

Minimal fix
 	initialRequest := *request
-	if initialRequest.Type == "" {
-		initialRequest.Type = "response.create"
-	}
+	if initialRequest.Type != "" && initialRequest.Type != "response.create" {
+		return fmt.Errorf("first responses websocket message must have type %q", "response.create")
+	}
+	initialRequest.Type = "response.create"
 	converted, err := adaptor.ConvertOpenAIResponsesRequest(c, info, initialRequest)
📝 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
if initialRequest.Type == "" {
initialRequest.Type = "response.create"
}
initialRequest := *request
if initialRequest.Type != "" && initialRequest.Type != "response.create" {
return fmt.Errorf("first responses websocket message must have type %q", "response.create")
}
initialRequest.Type = "response.create"
converted, err := adaptor.ConvertOpenAIResponsesRequest(c, info, initialRequest)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/websocket.go` around lines 91 - 93, The current logic only defaults
initialRequest.Type to "response.create" when empty, allowing a client-supplied
non-empty Type to slip through; change the behavior in the websocket handler so
the first upstream event always has Type "response.create" by unconditionally
setting initialRequest.Type = "response.create" (instead of only when empty) or,
if you prefer validation, explicitly reject any initialRequest.Type !=
"response.create"; update the code that inspects/forwards initialRequest (the
variable named initialRequest in websocket.go) so it no longer forwards
client-provided Types for the first frame.

@constansino

Copy link
Copy Markdown

赶紧加啊

@seefs001

seefs001 commented Mar 22, 2026

Copy link
Copy Markdown
Collaborator

粗略看了下,缺少了对usage的解析,没有计费逻辑;没有选择渠道类型为OpenAI和Codex的逻辑;这两个都是有必要的。

@ghost

This comment was marked as spam.

@haoziqi778

Copy link
Copy Markdown

伟大,无需多言,管理员辛苦快点加一下,真的是个很有帮助的更新

@seefs001 seefs001 closed this Apr 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants