Skip to content

feat: add model redirection for task requests - #2978

Closed
seefs001 wants to merge 9 commits into
QuantumNous:mainfrom
seefs001:feature/task-model-mapper
Closed

feat: add model redirection for task requests#2978
seefs001 wants to merge 9 commits into
QuantumNous:mainfrom
seefs001:feature/task-model-mapper

Conversation

@seefs001

@seefs001 seefs001 commented Feb 21, 2026

Copy link
Copy Markdown
Collaborator

fix #2130

Summary by CodeRabbit

  • New Features
    • Added model mapping support to enable flexible routing of requests to different upstream model versions while preserving request data integrity.
    • Implemented automatic model tracking to record both original and mapped model identifiers for request transparency.

@coderabbitai

coderabbitai Bot commented Feb 21, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR adds model-mapping support to the Sora channel adaptor. It introduces conditional request body handling based on whether a model is mapped, validates task actions based on image presence and channel type, initializes model mapping via a helper function, and persists upstream and origin model names in task properties.

Changes

Cohort / File(s) Summary
Sora Adaptor Request Handling
relay/channel/task/sora/adaptor.go
Adds model-mapping logic to BuildRequestBody with conditional content-type handling; introduces buildRequestBodyWithMappedModel helper to rebuild multipart/form-data and handle application/json while preserving request parts and implementing model mapping.
Task Validation and Relay
relay/common/relay_utils.go, relay/relay_task.go
Updates ValidateBasicTaskRequest to determine task action based on image presence and channel type for Vidu; initializes model mapping via helper.ModelMappedHelper in relay_task.go with error handling; stores UpstreamModelName and OriginModelName in task properties before persistence.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant RelayTask
    participant Validator as Validator<br/>(relay_utils)
    participant ModelMapper as ModelMapper<br/>(helper)
    participant Adaptor as Sora Adaptor
    participant Storage

    Client->>RelayTask: Request with model
    RelayTask->>Validator: ValidateBasicTaskRequest()
    Validator->>Validator: Determine action from images
    Validator->>Validator: Check IsModelMapped
    Validator-->>RelayTask: Validation complete

    RelayTask->>ModelMapper: ModelMappedHelper()
    ModelMapper->>ModelMapper: Map model if needed
    ModelMapper-->>RelayTask: Mapped model info

    RelayTask->>RelayTask: Set task.Properties<br/>(UpstreamModelName,<br/>OriginModelName)
    RelayTask->>Storage: Insert task

    RelayTask->>Adaptor: BuildRequestBody()
    Adaptor->>Adaptor: Check IsModelMapped
    Adaptor->>Adaptor: Parse content-type
    alt multipart/form-data
        Adaptor->>Adaptor: buildRequestBodyWithMappedModel()
        Adaptor->>Adaptor: Rebuild multipart with<br/>mapped model
    else application/json
        Adaptor->>Adaptor: Unmarshal, preserve payload,<br/>re-marshal
    end
    Adaptor-->>RelayTask: Rebuilt request body
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested reviewers

  • creamlike1024
  • xyfacai

Poem

🐰 A model redirected, but requests went astray,
Till mapping logic stitched the pieces back to stay,
Now Sora channels bounce with upstream names,
Multipart forms rebuilt—no more missing frames! 🎭

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding model redirection support for task requests to address the sora channel issue.
Linked Issues check ✅ Passed Code changes implement model mapping/redirection logic to enable sora channel requests to properly use redirected model names and route to available channels.
Out of Scope Changes check ✅ Passed All changes focus on implementing model redirection support: adaptor request body handling, relay utilities validation logic, and task relay initialization with model mapping.

✏️ 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

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.

@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: 2

🧹 Nitpick comments (1)
relay/channel/task/sora/adaptor.go (1)

152-217: buildRequestBodyWithMappedModel has duplicate logic for file vs non-file parts.

Lines 188–204: the if part.FileName() != "" and else branches both do exactly the same thing — writer.CreatePart(part.Header) followed by io.Copy. The file-name check is redundant here since CreatePart already preserves headers (including Content-Disposition with filename).

♻️ Simplify by removing the redundant branch
 		} 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")
-				}
-			}
+			newPart, err := writer.CreatePart(part.Header)
+			if err != nil {
+				return nil, errors.Wrap(err, "create_part_failed")
+			}
+			if _, err := io.Copy(newPart, part); err != nil {
+				return nil, errors.Wrap(err, "copy_part_content_failed")
+			}
 		}
🤖 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 152 - 217, The function
buildRequestBodyWithMappedModel contains redundant branching that checks
part.FileName() and executes identical logic in both branches; simplify by
removing the file-vs-non-file if/else and always create a new part with
writer.CreatePart(part.Header) followed by io.Copy from part, ensuring error
wraps remain (use the existing error messages like
"create_form_part_failed"/"copy_part_content_failed" or consolidate to the
existing ones), which preserves file headers (including filename) while
eliminating duplicate code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@relay/channel/task/sora/adaptor.go`:
- 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.
- Around line 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.

---

Nitpick comments:
In `@relay/channel/task/sora/adaptor.go`:
- Around line 152-217: The function buildRequestBodyWithMappedModel contains
redundant branching that checks part.FileName() and executes identical logic in
both branches; simplify by removing the file-vs-non-file if/else and always
create a new part with writer.CreatePart(part.Header) followed by io.Copy from
part, ensuring error wraps remain (use the existing error messages like
"create_form_part_failed"/"copy_part_content_failed" or consolidate to the
existing ones), which preserves file headers (including filename) while
eliminating duplicate code.

Comment on lines +119 to 149
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

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.

return bytes.NewReader(newBody), nil
}

return common.ReaderOnly(storage), 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

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.

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.

sora接口对重定向后的模型不生效

3 participants