Skip to content
106 changes: 106 additions & 0 deletions relay/channel/task/sora/adaptor.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package sora

import (
"bytes"
"fmt"
"io"
"mime"
"mime/multipart"
"net/http"
"strings"

Expand Down Expand Up @@ -107,9 +110,112 @@ func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayIn
if err != nil {
return nil, errors.Wrap(err, "get_request_body_failed")
}
bodyBytes, err := storage.Bytes()
if err != nil {
return nil, errors.Wrap(err, "read_request_body_failed")
}

// 检查是否需要模型重定向
if !info.IsModelMapped {
// 如果不需要重定向,直接返回原始请求体
return bytes.NewReader(bodyBytes), nil
}

contentType := c.Request.Header.Get("Content-Type")

// 处理multipart/form-data请求
if strings.Contains(contentType, "multipart/form-data") {
return buildRequestBodyWithMappedModel(bodyBytes, contentType, info.UpstreamModelName)
}
// 处理JSON请求
if strings.Contains(contentType, "application/json") {
var jsonData map[string]interface{}
if err := common.Unmarshal(bodyBytes, &jsonData); err != nil {
return nil, errors.Wrap(err, "unmarshal_json_failed")
}

// 暂不更改返回
// jsonData["model"] = info.UpstreamModelName

// 重新编码为JSON
newBody, err := common.Marshal(jsonData)
if err != nil {
return nil, errors.Wrap(err, "marshal_json_failed")
}

return bytes.NewReader(newBody), nil
}

return common.ReaderOnly(storage), nil
Comment on lines +119 to 149

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 | 🔴 Critical

Model rewrite is commented out, but the multipart path silently drops the model field — this is a data-loss bug.

When info.IsModelMapped is true and the content type is multipart/form-data, the code enters buildRequestBodyWithMappedModel. Inside that helper, when fieldName == "model" (Line 180), the WriteField call is commented out (Lines 183–185) and the else branch that preserves the part is skipped. The result: the model field is silently removed from the rebuilt multipart body sent upstream. The upstream API will receive a request with no model field at all.

Additionally, the JSON path (Lines 131–147) unmarshals and re-marshals the body with zero modifications (Line 138 is commented out), adding unnecessary overhead.

If the intent is to not rewrite the model field for now, the multipart path must still preserve the original model field rather than dropping it. The simplest fix: when IsModelMapped is false (or rewrite is disabled), return the body unchanged — which is already done at Line 119–122. The current code reaches Lines 127+ only when IsModelMapped is true, making the commented-out rewrite contradictory.

🐛 Proposed fix: preserve the model field in the multipart path

Option A — If intent is to defer model rewrite entirely, just return the original body when mapped too:

 	if !info.IsModelMapped {
-		// 如果不需要重定向,直接返回原始请求体
 		return bytes.NewReader(bodyBytes), nil
 	}
+	// TODO: model rewrite for mapped models is not yet enabled;
+	// return the original body to avoid dropping the model field.
+	return bytes.NewReader(bodyBytes), nil

Option B — If intent is to rewrite the model, uncomment the write:

 		if fieldName == "model" {
-			// 修改 model 字段为映射后的模型名
-			// 暂不更改返回
-			//if err := writer.WriteField("model", redirectedModel); err != nil {
-			//	return nil, errors.Wrap(err, "write_model_field_failed")
-			//}
-		} else {
+			if err := writer.WriteField("model", redirectedModel); err != nil {
+				return nil, errors.Wrap(err, "write_model_field_failed")
+			}
+		} else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/task/sora/adaptor.go` around lines 119 - 149, When
info.IsModelMapped is true the multipart path calls
buildRequestBodyWithMappedModel which currently drops the "model" form field
(the WriteField call for fieldName == "model" is commented out), causing data
loss; fix by preserving the original "model" part when not rewriting (or by
performing the actual rewrite) inside buildRequestBodyWithMappedModel so that
the "model" field is written into the new multipart body (i.e., restore or
replace the WriteField behavior for fieldName == "model"); also avoid
unnecessary JSON unmarshal/remarshal in the JSON branch of the handler (the
jsonData["model"] rewrite is commented out) — either perform the model rewrite
there or simply return the original body (bytes.NewReader(bodyBytes)) when no
change is needed.

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

Potential stale reader: common.ReaderOnly(storage) may return an exhausted reader.

Line 113 calls storage.Bytes() which may consume the underlying reader. Line 149 then returns common.ReaderOnly(storage) for non-multipart/non-JSON content types. If storage doesn't support re-reading after Bytes() is called, this will return an empty body.

This line is only reached when info.IsModelMapped is true (the !IsModelMapped early return is at Line 119), so for mapped models with an unrecognized content type, the upstream would receive an empty body.

Proposed fix: use the already-read bytes
-	return common.ReaderOnly(storage), nil
+	return bytes.NewReader(bodyBytes), nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/task/sora/adaptor.go` at line 149, The returned reader may be
exhausted because storage.Bytes() was already called earlier; replace the final
return of common.ReaderOnly(storage) (in the branch reached when
info.IsModelMapped is true) with a reader constructed from the already-read
bytes buffer produced by storage.Bytes() (use the variable holding those bytes
instead of re-wrapping storage), so the upstream receives the actual body for
unrecognized content types instead of an empty reader.

}

func buildRequestBodyWithMappedModel(originalBody []byte, contentType, redirectedModel string) (io.Reader, error) {
newBuffer := &bytes.Buffer{}
writer := multipart.NewWriter(newBuffer)

_, params, err := mime.ParseMediaType(contentType)
if err != nil {
return nil, errors.Wrap(err, "parse_content_type_failed")
}
boundary, ok := params["boundary"]
if !ok {
return nil, errors.New("boundary_not_found_in_content_type")
}
if err := writer.SetBoundary(boundary); err != nil {
return nil, errors.Wrap(err, "set_boundary_failed")
}
r := multipart.NewReader(bytes.NewReader(originalBody), boundary)

for {
part, err := r.NextPart()
if err == io.EOF {
break
}
if err != nil {
return nil, errors.Wrap(err, "read_multipart_part_failed")
}

fieldName := part.FormName()

if fieldName == "model" {
// 修改 model 字段为映射后的模型名
// 暂不更改返回
//if err := writer.WriteField("model", redirectedModel); err != nil {
// return nil, errors.Wrap(err, "write_model_field_failed")
//}
} else {
// 对于其他字段,保留原始内容
if part.FileName() != "" {
newPart, err := writer.CreatePart(part.Header)
if err != nil {
return nil, errors.Wrap(err, "create_form_file_failed")
}
if _, err := io.Copy(newPart, part); err != nil {
return nil, errors.Wrap(err, "copy_file_content_failed")
}
} else {
newPart, err := writer.CreatePart(part.Header)
if err != nil {
return nil, errors.Wrap(err, "create_form_field_failed")
}
if _, err := io.Copy(newPart, part); err != nil {
return nil, errors.Wrap(err, "copy_field_content_failed")
}
}
}

if err := part.Close(); err != nil {
return nil, errors.Wrap(err, "close_part_failed")
}
}

if err := writer.Close(); err != nil {
return nil, errors.Wrap(err, "close_multipart_writer_failed")
}

return newBuffer, nil
}

// DoRequest delegates to common helper.
func (a *TaskAdaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (*http.Response, error) {
return channel.DoTaskApiRequest(a, c, info, requestBody)
Expand Down
17 changes: 17 additions & 0 deletions relay/common/relay_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,23 @@ func ValidateBasicTaskRequest(c *gin.Context, info *RelayInfo, action string) *d
req.Images = []string{req.Image}
}

if req.HasImage() {
action = constant.TaskActionGenerate
if info.ChannelType == constant.ChannelTypeVidu {
// vidu 增加 首尾帧生视频和参考图生视频
if len(req.Images) == 2 {
action = constant.TaskActionFirstTailGenerate
} else if len(req.Images) > 2 {
action = constant.TaskActionReferenceGenerate
}
}
}

// 模型映射
if info.IsModelMapped {
req.Model = info.UpstreamModelName
}

storeTaskRequest(c, info, action, req)
return nil
}
10 changes: 10 additions & 0 deletions relay/relay_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/QuantumNous/new-api/relay/channel"
relaycommon "github.com/QuantumNous/new-api/relay/common"
relayconstant "github.com/QuantumNous/new-api/relay/constant"
"github.com/QuantumNous/new-api/relay/helper"
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/setting/ratio_setting"

Expand Down Expand Up @@ -125,6 +126,11 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.
}

info.InitChannelMeta(c)

// 模型映射
if err := helper.ModelMappedHelper(c, info, nil); err != nil {
return service.TaskErrorWrapper(err, "model_mapped_failed", http.StatusBadRequest)
}
adaptor := GetTaskAdaptor(platform)
if adaptor == nil {
return service.TaskErrorWrapperLocal(fmt.Errorf("invalid api platform: %s", platform), "invalid_api_platform", http.StatusBadRequest)
Expand Down Expand Up @@ -275,6 +281,10 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.
task.Quota = quota
task.Data = taskData
task.Action = info.Action
task.Properties = model.Properties{
UpstreamModelName: info.UpstreamModelName,
OriginModelName: info.OriginModelName,
}
err = task.Insert()
if err != nil {
taskErr = service.TaskErrorWrapper(err, "insert_task_failed", http.StatusInternalServerError)
Expand Down