From 7fc25a57cfd4a5312f27a3c23fd00ebf32c82350 Mon Sep 17 00:00:00 2001 From: creamlike1024 Date: Fri, 31 Oct 2025 18:58:03 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat(relay):=20=E6=B7=BB=E5=8A=A0=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E6=A8=A1=E5=9E=8B=E6=98=A0=E5=B0=84=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- relay/channel/task/sora/adaptor.go | 90 ++++++++++++++++++++++++++++++ relay/common/relay_utils.go | 5 ++ relay/relay_task.go | 6 ++ 3 files changed, 101 insertions(+) diff --git a/relay/channel/task/sora/adaptor.go b/relay/channel/task/sora/adaptor.go index 17aec18f08a1..d0bcc71111ae 100644 --- a/relay/channel/task/sora/adaptor.go +++ b/relay/channel/task/sora/adaptor.go @@ -2,9 +2,12 @@ package sora import ( "bytes" + "encoding/json" "fmt" "io" + "mime/multipart" "net/http" + "strings" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/dto" @@ -87,9 +90,96 @@ func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayIn if err != nil { return nil, errors.Wrap(err, "get_request_body_failed") } + + // 检查是否需要模型重定向 + if !info.IsModelMapped { + // 如果不需要重定向,直接返回原始请求体 + return bytes.NewReader(cachedBody), nil + } + + contentType := c.Request.Header.Get("Content-Type") + + // 处理multipart/form-data请求 + if strings.Contains(contentType, "multipart/form-data") { + return buildRequestBodyWithMappedModel(cachedBody, contentType, info.UpstreamModelName) + } + // 处理JSON请求 + if strings.Contains(contentType, "application/json") { + var jsonData map[string]interface{} + if err := json.Unmarshal(cachedBody, &jsonData); err != nil { + return nil, errors.Wrap(err, "unmarshal_json_failed") + } + + // 替换model字段为映射后的模型名 + jsonData["model"] = info.UpstreamModelName + + // 重新编码为JSON + newBody, err := json.Marshal(jsonData) + if err != nil { + return nil, errors.Wrap(err, "marshal_json_failed") + } + + return bytes.NewReader(newBody), nil + } + return bytes.NewReader(cachedBody), nil } +func buildRequestBodyWithMappedModel(originalBody []byte, contentType, redirectedModel string) (io.Reader, error) { + newBuffer := &bytes.Buffer{} + writer := multipart.NewWriter(newBuffer) + + r := multipart.NewReader(bytes.NewReader(originalBody), strings.TrimPrefix(contentType, "multipart/form-data; 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.CreateFormFile(fieldName, part.FileName()) + 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 { + content, err := io.ReadAll(part) + if err != nil { + return nil, errors.Wrap(err, "read_field_content_failed") + } + if err := writer.WriteField(fieldName, string(content)); err != nil { + return nil, errors.Wrap(err, "write_field_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) diff --git a/relay/common/relay_utils.go b/relay/common/relay_utils.go index b38baf13ae20..26dd2afc2b03 100644 --- a/relay/common/relay_utils.go +++ b/relay/common/relay_utils.go @@ -252,6 +252,11 @@ func ValidateBasicTaskRequest(c *gin.Context, info *RelayInfo, action string) *d } } + // 模型映射 + if info.IsModelMapped { + req.Model = info.UpstreamModelName + } + storeTaskRequest(c, info, action, req) return nil } diff --git a/relay/relay_task.go b/relay/relay_task.go index ca1b0bb1fe92..db543319a0f0 100644 --- a/relay/relay_task.go +++ b/relay/relay_task.go @@ -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" @@ -38,6 +39,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) From c651727bab5d9d2ac1e106c37bb1d12a09a7f415 Mon Sep 17 00:00:00 2001 From: creamlike1024 Date: Fri, 31 Oct 2025 19:16:55 +0800 Subject: [PATCH 2/7] =?UTF-8?q?fix(adaptor):=20=E4=BF=AE=E5=A4=8D=E8=A7=A3?= =?UTF-8?q?=E6=9E=90multipart=E8=AF=B7=E6=B1=82=E6=97=B6=E8=8E=B7=E5=8F=96?= =?UTF-8?q?boundary=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- relay/channel/task/sora/adaptor.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/relay/channel/task/sora/adaptor.go b/relay/channel/task/sora/adaptor.go index d0bcc71111ae..1024bc190648 100644 --- a/relay/channel/task/sora/adaptor.go +++ b/relay/channel/task/sora/adaptor.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "mime" "mime/multipart" "net/http" "strings" @@ -129,7 +130,15 @@ func buildRequestBodyWithMappedModel(originalBody []byte, contentType, redirecte newBuffer := &bytes.Buffer{} writer := multipart.NewWriter(newBuffer) - r := multipart.NewReader(bytes.NewReader(originalBody), strings.TrimPrefix(contentType, "multipart/form-data; boundary=")) + _, 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") + } + r := multipart.NewReader(bytes.NewReader(originalBody), boundary) for { part, err := r.NextPart() From 347ad047f9eb0e2109cd3abc1fecaccd1b6e01c3 Mon Sep 17 00:00:00 2001 From: creamlike1024 Date: Fri, 31 Oct 2025 19:45:37 +0800 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=E4=BF=9D=E5=AD=98=E9=87=8D?= =?UTF-8?q?=E5=AE=9A=E5=90=91=E4=BF=A1=E6=81=AF=E5=88=B0=20task.Properties?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- model/task.go | 5 ++++- relay/relay_task.go | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/model/task.go b/model/task.go index a8c3a7d4d526..6c5ade536147 100644 --- a/model/task.go +++ b/model/task.go @@ -72,7 +72,10 @@ func (t *Task) GetData(v any) error { } type Properties struct { - Input string `json:"input"` + Input string `json:"input"` + IsModelMapped bool `json:"is_model_mapped,omitempty"` + OriginModelName string `json:"origin_model_name,omitempty"` + UpstreamModelName string `json:"upstream_model_name,omitempty"` } func (m *Properties) Scan(val interface{}) error { diff --git a/relay/relay_task.go b/relay/relay_task.go index db543319a0f0..148b4ac38f8c 100644 --- a/relay/relay_task.go +++ b/relay/relay_task.go @@ -214,6 +214,11 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto. task.Quota = quota task.Data = taskData task.Action = info.Action + task.Properties = model.Properties{ + IsModelMapped: info.IsModelMapped, + UpstreamModelName: info.UpstreamModelName, + OriginModelName: info.OriginModelName, + } err = task.Insert() if err != nil { taskErr = service.TaskErrorWrapper(err, "insert_task_failed", http.StatusInternalServerError) From 10a473993b359b91005a6ec6681774578494663a Mon Sep 17 00:00:00 2001 From: creamlike1024 Date: Fri, 31 Oct 2025 19:53:46 +0800 Subject: [PATCH 4/7] refactor(relay): remove IsModelMapped properties --- relay/relay_task.go | 1 - 1 file changed, 1 deletion(-) diff --git a/relay/relay_task.go b/relay/relay_task.go index 211927659987..247a59a7a6d0 100644 --- a/relay/relay_task.go +++ b/relay/relay_task.go @@ -215,7 +215,6 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto. task.Data = taskData task.Action = info.Action task.Properties = model.Properties{ - IsModelMapped: info.IsModelMapped, UpstreamModelName: info.UpstreamModelName, OriginModelName: info.OriginModelName, } From f15b85f745a92bd83e230251307ed2a9e0098133 Mon Sep 17 00:00:00 2001 From: creamlike1024 Date: Fri, 31 Oct 2025 20:06:01 +0800 Subject: [PATCH 5/7] =?UTF-8?q?fix(:=20=E4=BF=AE=E5=A4=8Dmultipart?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E8=BE=B9=E7=95=8C=E8=AE=BE=E7=BD=AE=E5=92=8C?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=AD=97=E6=AE=B5=E5=A4=84=E7=90=86=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- relay/channel/task/sora/adaptor.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/relay/channel/task/sora/adaptor.go b/relay/channel/task/sora/adaptor.go index 1024bc190648..207795807496 100644 --- a/relay/channel/task/sora/adaptor.go +++ b/relay/channel/task/sora/adaptor.go @@ -138,6 +138,9 @@ func buildRequestBodyWithMappedModel(originalBody []byte, contentType, redirecte 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 { @@ -159,7 +162,7 @@ func buildRequestBodyWithMappedModel(originalBody []byte, contentType, redirecte } else { // 对于其他字段,保留原始内容 if part.FileName() != "" { - newPart, err := writer.CreateFormFile(fieldName, part.FileName()) + newPart, err := writer.CreatePart(part.Header) if err != nil { return nil, errors.Wrap(err, "create_form_file_failed") } From 797c7acd13e912c2254a361efbbeda1cf73de7a6 Mon Sep 17 00:00:00 2001 From: creamlike1024 Date: Fri, 31 Oct 2025 20:13:27 +0800 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dmultipart=E8=A1=A8?= =?UTF-8?q?=E5=8D=95=E5=AD=97=E6=AE=B5=E5=86=85=E5=AE=B9=E5=A4=8D=E5=88=B6?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- relay/channel/task/sora/adaptor.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/relay/channel/task/sora/adaptor.go b/relay/channel/task/sora/adaptor.go index 207795807496..5365c928ae7b 100644 --- a/relay/channel/task/sora/adaptor.go +++ b/relay/channel/task/sora/adaptor.go @@ -170,12 +170,12 @@ func buildRequestBodyWithMappedModel(originalBody []byte, contentType, redirecte return nil, errors.Wrap(err, "copy_file_content_failed") } } else { - content, err := io.ReadAll(part) + newPart, err := writer.CreatePart(part.Header) if err != nil { - return nil, errors.Wrap(err, "read_field_content_failed") + return nil, errors.Wrap(err, "create_form_field_failed") } - if err := writer.WriteField(fieldName, string(content)); err != nil { - return nil, errors.Wrap(err, "write_field_failed") + if _, err := io.Copy(newPart, part); err != nil { + return nil, errors.Wrap(err, "copy_field_content_failed") } } } From 06cdad5d472e95ad5c58805a36b25fa792056eb1 Mon Sep 17 00:00:00 2001 From: Seefs Date: Sat, 21 Feb 2026 14:41:55 +0800 Subject: [PATCH 7/7] =?UTF-8?q?fix=EF=BC=9A=20restore=20request=20body=20h?= =?UTF-8?q?andling=20and=20disable=20model=20rewritex:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- relay/channel/task/sora/adaptor.go | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/relay/channel/task/sora/adaptor.go b/relay/channel/task/sora/adaptor.go index 8eb70ccdf2b2..d992399af80e 100644 --- a/relay/channel/task/sora/adaptor.go +++ b/relay/channel/task/sora/adaptor.go @@ -2,7 +2,6 @@ package sora import ( "bytes" - "encoding/json" "fmt" "io" "mime" @@ -111,31 +110,35 @@ 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(cachedBody), nil + return bytes.NewReader(bodyBytes), nil } contentType := c.Request.Header.Get("Content-Type") // 处理multipart/form-data请求 if strings.Contains(contentType, "multipart/form-data") { - return buildRequestBodyWithMappedModel(cachedBody, contentType, info.UpstreamModelName) + return buildRequestBodyWithMappedModel(bodyBytes, contentType, info.UpstreamModelName) } // 处理JSON请求 if strings.Contains(contentType, "application/json") { var jsonData map[string]interface{} - if err := json.Unmarshal(cachedBody, &jsonData); err != nil { + if err := common.Unmarshal(bodyBytes, &jsonData); err != nil { return nil, errors.Wrap(err, "unmarshal_json_failed") } - // 替换model字段为映射后的模型名 - jsonData["model"] = info.UpstreamModelName + // 暂不更改返回 + // jsonData["model"] = info.UpstreamModelName // 重新编码为JSON - newBody, err := json.Marshal(jsonData) + newBody, err := common.Marshal(jsonData) if err != nil { return nil, errors.Wrap(err, "marshal_json_failed") } @@ -176,9 +179,10 @@ func buildRequestBodyWithMappedModel(originalBody []byte, contentType, redirecte if fieldName == "model" { // 修改 model 字段为映射后的模型名 - if err := writer.WriteField("model", redirectedModel); err != nil { - return nil, errors.Wrap(err, "write_model_field_failed") - } + // 暂不更改返回 + //if err := writer.WriteField("model", redirectedModel); err != nil { + // return nil, errors.Wrap(err, "write_model_field_failed") + //} } else { // 对于其他字段,保留原始内容 if part.FileName() != "" {