-
Notifications
You must be signed in to change notification settings - Fork 11.1k
Add WebSocket relay support for /v1/responses #3370
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ import ( | |
| "github.com/QuantumNous/new-api/types" | ||
|
|
||
| "github.com/gin-gonic/gin" | ||
| "github.com/gorilla/websocket" | ||
| ) | ||
|
|
||
| type ModelRequest struct { | ||
|
|
@@ -177,6 +178,15 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) { | |
| var modelRequest ModelRequest | ||
| shouldSelectChannel := true | ||
| var err error | ||
| 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 | ||
|
Comment on lines
+181
to
+188
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Defer token model checks until the first Responses WS frame is parsed. Line 188 returns with 🤖 Prompt for AI Agents |
||
| } | ||
| if strings.Contains(c.Request.URL.Path, "/mj/") { | ||
| relayMode := relayconstant.Path2RelayModeMidjourney(c.Request.URL.Path) | ||
| if relayMode == relayconstant.RelayModeMidjourneyTaskFetch || | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,13 +1,19 @@ | ||||||||||||||||||||
| package relay | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import ( | ||||||||||||||||||||
| "errors" | ||||||||||||||||||||
| "fmt" | ||||||||||||||||||||
| "time" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| "github.com/QuantumNous/new-api/dto" | ||||||||||||||||||||
| "github.com/QuantumNous/new-api/logger" | ||||||||||||||||||||
| "github.com/QuantumNous/new-api/relay/channel" | ||||||||||||||||||||
| relaycommon "github.com/QuantumNous/new-api/relay/common" | ||||||||||||||||||||
| "github.com/QuantumNous/new-api/relay/helper" | ||||||||||||||||||||
| "github.com/QuantumNous/new-api/service" | ||||||||||||||||||||
| "github.com/QuantumNous/new-api/types" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| "github.com/bytedance/gopkg/util/gopool" | ||||||||||||||||||||
| "github.com/gin-gonic/gin" | ||||||||||||||||||||
| "github.com/gorilla/websocket" | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
@@ -44,3 +50,103 @@ func WssHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types. | |||||||||||||||||||
| service.PostWssConsumeQuota(c, info, info.UpstreamModelName, usage.(*dto.RealtimeUsage), "") | ||||||||||||||||||||
| return nil | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| func WssResponsesHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types.NewAPIError) { | ||||||||||||||||||||
| info.InitChannelMeta(c) | ||||||||||||||||||||
| if info.ClientWs == nil { | ||||||||||||||||||||
| return types.NewError(errors.New("client websocket connection is nil"), types.ErrorCodeBadResponse, types.ErrOptionWithSkipRetry()) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| adaptor := GetAdaptor(info.ApiType) | ||||||||||||||||||||
| if adaptor == nil { | ||||||||||||||||||||
| return types.NewError(fmt.Errorf("invalid api type: %d", info.ApiType), types.ErrorCodeInvalidApiType, types.ErrOptionWithSkipRetry()) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| adaptor.Init(info) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| targetWs, err := channel.DoWssRequest(adaptor, c, info, nil) | ||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||
| return types.NewError(err, types.ErrorCodeDoRequestFailed) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| info.TargetWs = targetWs | ||||||||||||||||||||
| defer info.TargetWs.Close() | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if err := sendInitialResponsesWSRequest(c, adaptor, info); err != nil { | ||||||||||||||||||||
| return types.NewError(err, types.ErrorCodeDoRequestFailed, types.ErrOptionWithSkipRetry()) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| if err := proxyResponsesWS(c, info.ClientWs, info.TargetWs); err != nil { | ||||||||||||||||||||
| return types.NewError(err, types.ErrorCodeBadResponse, types.ErrOptionWithSkipRetry()) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| if err := service.SettleBilling(c, info, info.FinalPreConsumedQuota); err != nil { | ||||||||||||||||||||
| logger.LogError(c, "responses websocket settle billing failed: "+err.Error()) | ||||||||||||||||||||
|
Comment on lines
+79
to
+80
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This path never settles against actual streamed usage. Line 79 passes Also applies to: 108-151 🤖 Prompt for AI Agents |
||||||||||||||||||||
| } | ||||||||||||||||||||
| return nil | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| func sendInitialResponsesWSRequest(c *gin.Context, adaptor channel.Adaptor, info *relaycommon.RelayInfo) error { | ||||||||||||||||||||
| request, ok := info.Request.(*dto.OpenAIResponsesRequest) | ||||||||||||||||||||
| if !ok || request == nil { | ||||||||||||||||||||
| return errors.New("invalid responses websocket request") | ||||||||||||||||||||
| } | ||||||||||||||||||||
| initialRequest := *request | ||||||||||||||||||||
| if initialRequest.Type == "" { | ||||||||||||||||||||
| initialRequest.Type = "response.create" | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+91
to
+93
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Always force the first upstream event to Lines 91-93 only default 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| converted, err := adaptor.ConvertOpenAIResponsesRequest(c, info, initialRequest) | ||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||
| return err | ||||||||||||||||||||
| } | ||||||||||||||||||||
| switch v := converted.(type) { | ||||||||||||||||||||
| case string: | ||||||||||||||||||||
| return helper.WssString(c, info.TargetWs, v) | ||||||||||||||||||||
| case []byte: | ||||||||||||||||||||
| return info.TargetWs.WriteMessage(websocket.TextMessage, v) | ||||||||||||||||||||
| default: | ||||||||||||||||||||
| return helper.WssObject(c, info.TargetWs, v) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| func proxyResponsesWS(c *gin.Context, clientConn, targetConn *websocket.Conn) error { | ||||||||||||||||||||
| errChan := make(chan error, 2) | ||||||||||||||||||||
| forward := func(src, dst *websocket.Conn, direction string) { | ||||||||||||||||||||
| defer func() { | ||||||||||||||||||||
| if r := recover(); r != nil { | ||||||||||||||||||||
| errChan <- fmt.Errorf("%s panic: %v", direction, r) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| }() | ||||||||||||||||||||
| for { | ||||||||||||||||||||
| messageType, payload, err := src.ReadMessage() | ||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||
| if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) { | ||||||||||||||||||||
| errChan <- nil | ||||||||||||||||||||
| return | ||||||||||||||||||||
| } | ||||||||||||||||||||
| errChan <- fmt.Errorf("%s read failed: %w", direction, err) | ||||||||||||||||||||
| return | ||||||||||||||||||||
| } | ||||||||||||||||||||
| if err := dst.WriteMessage(messageType, payload); err != nil { | ||||||||||||||||||||
| errChan <- fmt.Errorf("%s write failed: %w", direction, err) | ||||||||||||||||||||
| return | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| gopool.Go(func() { | ||||||||||||||||||||
| forward(clientConn, targetConn, "client->target") | ||||||||||||||||||||
| }) | ||||||||||||||||||||
| gopool.Go(func() { | ||||||||||||||||||||
| forward(targetConn, clientConn, "target->client") | ||||||||||||||||||||
| }) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| select { | ||||||||||||||||||||
| case <-c.Request.Context().Done(): | ||||||||||||||||||||
| return nil | ||||||||||||||||||||
| case err := <-errChan: | ||||||||||||||||||||
| if err == nil { | ||||||||||||||||||||
| deadline := time.Now().Add(500 * time.Millisecond) | ||||||||||||||||||||
| _ = clientConn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), deadline) | ||||||||||||||||||||
| _ = targetConn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), deadline) | ||||||||||||||||||||
| return nil | ||||||||||||||||||||
| } | ||||||||||||||||||||
| return err | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: QuantumNous/new-api
Length of output: 609
🏁 Script executed:
Repository: QuantumNous/new-api
Length of output: 511
🏁 Script executed:
Repository: QuantumNous/new-api
Length of output: 160
Use
common.Unmarshalinstead of directencoding/jsonin the WebSocket request parser.Line 4 imports
encoding/jsonand line 294 usesjson.Unmarshaldirectly in business code. Replace withcommon.Unmarshalto comply with the repo's JSON wrapper contract.Fix
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 callencoding/jsonin business code.🤖 Prompt for AI Agents