Skip to content

Conversation

@jswh
Copy link

@jswh jswh commented Oct 18, 2025

增加modelscope的推理API支持
包括

  1. 文本推理
  2. 图片生成

modelscope应该暂时只有这两个类型的大模型支持。

Summary by CodeRabbit

  • New Features
    • Added ModelScope as a selectable AI provider for chat and image generation.
    • Image generation supports asynchronous task polling and returns OpenAI-compatible image responses (including optional base64).
    • Normalized sampling behavior for certain model requests to improve consistency across providers.

test added 2 commits October 18, 2025 22:56
This commit adds support for the ModelScope API by introducing a new
channel type and associated adaptor. Changes include:

- Adding ChannelTypeModelScope and APITypeModelScope constants
- Configuring the ModelScope base URL
- Implementing a full adaptor for text and image requests
- Supporting async image generation with task polling
- Integrating the adaptor into the relay system
- Updating the web UI to display ModelScope as an option
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 18, 2025

Walkthrough

Adds ModelScope channel support: new API/channel constants, ChannelType2APIType mapping, a ModelScope adaptor with request/response conversion (including image task polling and OpenAI-compatible transformation), DTOs, registration in adaptor factory, and a frontend channel option.

Changes

Cohort / File(s) Summary
API & Channel Constants
constant/api_type.go, constant/channel.go, common/api_type.go
Adds APITypeModelScope to API type enum (shifts subsequent iota values), adds ChannelTypeModelScope (56), updates ChannelBaseURLs and ChannelTypeNames, and maps channel→API in ChannelType2APIType.
ModelScope Adaptor
relay/channel/modelscope/adaptor.go, relay/relay_adaptor.go
New modelscope.Adaptor type and methods; registers adaptor in GetAdaptor. Handles URL selection, header setup, request conversion, DoRequest/DoResponse paths (including routing to image handler or delegating to OpenAI/Claude adaptors); several methods stubbed/not implemented.
ModelScope DTOs & Constants
relay/channel/modelscope/constants.go, relay/channel/modelscope/dto.go, relay/channel/modelscope/text.go
Adds ChannelName, ModelList, MSImageRequest, MSImageResponse, and a TopP-normalization helper for OpenAI requests.
ModelScope Image Handling
relay/channel/modelscope/image.go
Implements mapping from OpenAI image requests to ModelScope, HTTP task polling (updateTask, asyncTaskWait), MS→OpenAI image response conversion (including base64 retrieval), and msImageHandler to stream JSON responses.
Frontend Option
web/src/constants/channel.constants.js
Appends ModelScope option { value: 56, color: 'blue', label: 'ModelScope(魔搭)' } to CHANNEL_OPTIONS.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Relay
    participant MS as ModelScopeAdaptor
    participant API as ModelScope API

    Client->>Relay: OpenAI-format request
    Relay->>MS: GetRequestURL / SetupRequestHeader
    Relay->>MS: ConvertOpenAIRequest

    alt Image generation
        MS->>API: POST create task
        API-->>MS: 202 Accepted / task id
        MS->>MS: asyncTaskWait (poll)
        loop poll
            MS->>API: GET /v1/tasks/{id}
            API-->>MS: task status / output
        end
        MS->>MS: Convert MSImageResponse -> OpenAI image response
    else Chat/text
        MS->>MS: Normalize TopP / build request
        MS->>OtherAdaptor: delegate to OpenAI/Claude adaptor
        OtherAdaptor-->>MS: response
    end

    MS-->>Relay: OpenAI-compatible response
    Relay-->>Client: Response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • seefs001
  • creamlike1024

Poem

🐰 Hopping through code with cheerful scope,
ModelScope lands — a promising hope,
Tasks that poll and images that gleam,
TopP trimmed tidy for a steady stream,
New channels opened — nibble the dream.

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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 "增加modelscope渠道" (Add ModelScope channel) is directly related to the main changes in the changeset. The PR introduces comprehensive support for a new ModelScope channel across multiple layers: constants and types, adaptor implementation, data transfer objects, image and text processing, relay factory updates, and frontend UI integration. The title clearly and concisely summarizes the primary objective—adding ModelScope as a new channel—without unnecessary details like file lists or vague terminology. While the title doesn't enumerate the specific features (text inference and image generation), this level of detail is appropriately left for the PR description, not the title.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (6)
relay/channel/modelscope/dto.go (1)

10-18: Naming nit: ImageURL idiom

Consider renaming ImageUrl → ImageURL (keep json:"image_url"). Improves Go idiomatic naming and consistency. No functional change.

relay/channel/modelscope/constants.go (1)

6-6: Prefer const for channel name

ChannelName is immutable; make it a const.

-var ChannelName = "modelscope"
+const ChannelName = "modelscope"
relay/channel/modelscope/image.go (3)

37-44: Don’t fail whole request on one bad extra; skip instead

Log and continue on a single field unmarshal error to be robust with partial extras. Message already says “skip”.

-            if err := json.Unmarshal(value, target); err != nil {
-                logger.LogWarn(context.Background(), fmt.Sprintf("failed to unmarshal %s, skip set to request", key))
-                return nil, err
-            }
+            if err := json.Unmarshal(value, target); err != nil {
+                logger.LogWarn(context.Background(), fmt.Sprintf("failed to unmarshal %s, skipping this field: %v", key, err))
+                continue
+            }

121-125: Optional: add jitter/backoff to polling

Fixed 10s interval for 200s total may thundering-herd. Consider exponential backoff with jitter.


135-153: Spec alignment (optional): omit URL when returning b64_json

OpenAI typically returns either url or b64_json. Keeping both is harmless but may confuse clients.

relay/channel/modelscope/adaptor.go (1)

27-32: Optional: distinct edits endpoint

If ModelScope differentiates generations vs edits, split paths accordingly.

-    if info.RelayMode == constant.RelayModeImagesGenerations || info.RelayMode == constant.RelayModeImagesEdits {
-        return fmt.Sprintf("%s/v1/images/generations", info.ChannelBaseUrl), nil
-    }
+    if info.RelayMode == constant.RelayModeImagesGenerations {
+        return fmt.Sprintf("%s/v1/images/generations", info.ChannelBaseUrl), nil
+    }
+    if info.RelayMode == constant.RelayModeImagesEdits {
+        return fmt.Sprintf("%s/v1/images/edits", info.ChannelBaseUrl), nil
+    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7156bf2 and 3cdd06f.

📒 Files selected for processing (10)
  • common/api_type.go (1 hunks)
  • constant/api_type.go (1 hunks)
  • constant/channel.go (3 hunks)
  • relay/channel/modelscope/adaptor.go (1 hunks)
  • relay/channel/modelscope/constants.go (1 hunks)
  • relay/channel/modelscope/dto.go (1 hunks)
  • relay/channel/modelscope/image.go (1 hunks)
  • relay/channel/modelscope/text.go (1 hunks)
  • relay/relay_adaptor.go (2 hunks)
  • web/src/constants/channel.constants.js (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
common/api_type.go (2)
constant/channel.go (1)
  • ChannelTypeModelScope (56-56)
constant/api_type.go (1)
  • APITypeModelScope (35-35)
relay/channel/modelscope/image.go (9)
relay/channel/modelscope/dto.go (2)
  • MSImageRequest (9-18)
  • MSImageResponse (3-7)
common/json.go (2)
  • Unmarshal (9-11)
  • Marshal (21-23)
logger/logger.go (4)
  • LogWarn (61-63)
  • LogJson (150-157)
  • LogDebug (69-73)
  • LogError (65-67)
relay/common/relay_info.go (1)
  • RelayInfo (75-122)
service/image.go (1)
  • GetImageFromUrl (68-116)
dto/openai_image.go (1)
  • ImageData (170-174)
types/error.go (4)
  • NewAPIError (87-95)
  • NewOpenAIError (226-249)
  • NewError (204-224)
  • WithOpenAIError (277-300)
dto/openai_response.go (1)
  • Usage (222-235)
service/http.go (2)
  • CloseResponseBodyGracefully (15-23)
  • IOCopyBytesGracefully (25-60)
relay/channel/modelscope/text.go (1)
dto/openai_request.go (1)
  • GeneralOpenAIRequest (26-97)
relay/relay_adaptor.go (3)
constant/api_type.go (2)
  • APITypeModelScope (35-35)
  • APITypeSubmodel (36-36)
relay/channel/modelscope/adaptor.go (1)
  • Adaptor (21-22)
relay/channel/adapter.go (1)
  • Adaptor (15-32)
relay/channel/modelscope/adaptor.go (7)
relay/common/relay_info.go (1)
  • RelayInfo (75-122)
relay/constant/relay_mode.go (2)
  • RelayModeImagesGenerations (14-14)
  • RelayModeImagesEdits (15-15)
relay/channel/api_request.go (2)
  • SetupApiRequestHeader (27-39)
  • DoApiRequest (61-90)
dto/openai_request.go (1)
  • GeneralOpenAIRequest (26-97)
types/relay_format.go (2)
  • RelayFormat (3-3)
  • RelayFormatClaude (7-7)
relay/channel/claude/relay-claude.go (2)
  • ClaudeStreamHandler (699-721)
  • ClaudeHandler (765-787)
relay/channel/modelscope/constants.go (2)
  • ModelList (3-4)
  • ChannelName (6-6)
🔇 Additional comments (10)
common/api_type.go (1)

72-74: Mapping added correctly.

ChannelTypeModelScope → APITypeModelScope mapping looks good and aligns with the new channel type.

Please confirm constant.ChannelTypeModelScope (backend) equals 56 and matches the web constant addition.

relay/channel/modelscope/text.go (1)

7-14: The review comment is incorrect and contradicts ModelScope's API constraints; disregard it.

ModelScope's valid range for top_p is (0.0, 1.0], and top_p = 0 is explicitly not allowed—requests with 0 will be rejected. The review comment's suggestion to preserve 0 for "defaulting/omitempty" and clamp to 0 would introduce a critical bug. The original code's approach—converting TopP ≤ 0 to 0.01—is actually correct and necessary to satisfy ModelScope's constraints. The only minor refinement would be clamping TopP ≥ 1 to exactly 1.0 instead of 0.99, but the current approach is valid. The review comment's rationale is flawed and should not be followed.

Likely an incorrect or invalid review comment.

relay/relay_adaptor.go (1)

24-24: Interface compliance verified: all concerns confirmed as correctly implemented.

The modelscope.Adaptor struct properly implements all four required channel.Adaptor interface methods (Init, GetRequestURL, SetupRequestHeader, ConvertOpenAIRequest). The ConvertOpenAIRequest method correctly delegates to requestOpenAI2Modelscope, which normalizes TopP values (clamping to [0.01, 0.99] range) as required. Factory wiring in relay_adaptor.go is correct.

web/src/constants/channel.constants.js (1)

182-186: All backend mappings confirmed in sync.

Frontend addition of ModelScope (value 56) is properly supported by backend:

  • ChannelTypeModelScope = 56 verified in constant/channel.go
  • Name mapping "ModelScope" present at constant/channel.go:174
  • Base URL "https://api-inference.modelscope.cn" configured at ChannelBaseURLs[56]

All required backend constants are in place and consistent. UI can properly create and edit ModelScope channels.

constant/api_type.go (1)

35-37: Review comment is incorrect; no code changes needed.

The concern about iota shifts breaking behavior is not substantiated. The relay.GetAdaptor() switch statement uses constant names (e.g., case constant.APITypeModelScope), not numeric indices, making it position-independent. The loop in controller/model.go iterates 0 to APITypeDummy-1 and calls GetAdaptor(i) with numeric values, but all values (0-32) have corresponding cases in the switch statement. Reordering APITypeModelScope and APITypeSubmodel only changes their numeric values, not the execution logic. No evidence of external persistence or serialization of APIType numeric values was found in the codebase.

Likely an incorrect or invalid review comment.

relay/channel/modelscope/dto.go (2)

3-7: DTO looks correct

Shape matches usage; tags are consistent.


15-15: Use float64 for JSON numeric compatibility

JSON numbers decode to float64 by default; using float64 avoids precision/rounding surprises and extra conversions.
[ suggest_recommended_refactor ]
Apply this diff:

-    Guidance       float32 `json:"guidance,omitempty"`
+    Guidance       float64 `json:"guidance,omitempty"`
relay/channel/modelscope/adaptor.go (2)

34-41: Header setup LGTM

Auth and async headers set; JSON content-type for image modes is appropriate.


44-62: Verification complete; no issues found

The requestOpenAI2Modelscope function is correctly defined at relay/channel/modelscope/text.go:7 and properly called with matching parameter types at line 59. The thinking toggle logic and request conversion are all functioning as expected.

constant/channel.go (1)

56-56: Wiring verified and correct across all three components

All checks pass:

  • Type ChannelTypeModelScope = 56 correctly defined at line 56
  • Base URL at index 56 with accurate comment marker
  • Display name "ModelScope" properly mapped at line 174

Code changes are sound.

Add 30-second timeout to HTTP client and improve error handling in image
tasks. Fix typo in error message.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (3)
relay/channel/modelscope/image.go (2)

66-72: Good: HTTP client now has a timeout and body is closed; non‑2xx handled above

Addresses prior concerns about hanging calls and ignoring status codes.


110-113: LGTM: return actual upstream response when status is empty

Fixes earlier zero‑value response bug.

relay/channel/modelscope/adaptor.go (1)

89-91: Wrong relay-mode constant; edits won’t route to the ModelScope handler

Use RelayModeImagesEdits and keep generations + edits together.

-		case constant.RelayModeImagesGenerations, constant.RelayModeEdits:
+		case constant.RelayModeImagesGenerations, constant.RelayModeImagesEdits:
 			err, usage = msImageHandler(c, resp, info)
🧹 Nitpick comments (3)
relay/channel/modelscope/image.go (2)

83-85: Fix log message (“NewDecoder” → “unmarshal”)

Minor clarity nit.

-		common.SysLog("updateTask NewDecoder err: " + err.Error())
+		common.SysLog("updateTask unmarshal err: " + err.Error())

138-156: Optional: fetch images concurrently when response_format=b64_json

Sequential downloads can be slow with many outputs. Consider bounded concurrency.

relay/channel/modelscope/adaptor.go (1)

48-55: Side effect: mutating info.IsStream based on model name

Flipping global request streaming because model name contains “thinking” can impact headers/routing. Prefer deriving streaming solely from the incoming request or channel capabilities.

-	if strings.Contains(request.Model, "thinking") {
-		request.EnableThinking = true
-		request.Stream = true
-		info.IsStream = true
-	}
-	if !info.IsStream {
+	if strings.Contains(request.Model, "thinking") && request.Stream {
+		request.EnableThinking = true
+	} else {
 		request.EnableThinking = false
 	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3cdd06f and b28faec.

📒 Files selected for processing (2)
  • relay/channel/modelscope/adaptor.go (1 hunks)
  • relay/channel/modelscope/image.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
relay/channel/modelscope/image.go (9)
relay/channel/modelscope/dto.go (2)
  • MSImageRequest (9-18)
  • MSImageResponse (3-7)
common/json.go (2)
  • Unmarshal (9-11)
  • Marshal (21-23)
logger/logger.go (4)
  • LogWarn (61-63)
  • LogJson (150-157)
  • LogDebug (69-73)
  • LogError (65-67)
relay/common/relay_info.go (1)
  • RelayInfo (75-122)
relay/constant/relay_mode.go (1)
  • RelayModeImagesGenerations (14-14)
service/image.go (1)
  • GetImageFromUrl (68-116)
dto/openai_image.go (1)
  • ImageData (170-174)
types/error.go (4)
  • NewAPIError (87-95)
  • NewOpenAIError (226-249)
  • NewError (204-224)
  • WithOpenAIError (277-300)
service/http.go (2)
  • CloseResponseBodyGracefully (15-23)
  • IOCopyBytesGracefully (25-60)
relay/channel/modelscope/adaptor.go (7)
relay/common/relay_info.go (1)
  • RelayInfo (75-122)
relay/channel/api_request.go (2)
  • SetupApiRequestHeader (27-39)
  • DoApiRequest (61-90)
dto/openai_request.go (1)
  • GeneralOpenAIRequest (26-97)
types/error.go (1)
  • NewAPIError (87-95)
types/relay_format.go (2)
  • RelayFormat (3-3)
  • RelayFormatClaude (7-7)
relay/channel/claude/relay-claude.go (2)
  • ClaudeStreamHandler (699-721)
  • ClaudeHandler (765-787)
relay/channel/modelscope/constants.go (2)
  • ModelList (3-4)
  • ChannelName (6-6)
🔇 Additional comments (2)
relay/channel/modelscope/adaptor.go (2)

64-73: Good: image request conversion gated to image modes only

Clear error for unsupported modes.


27-32: I need to verify this with the official ModelScope API documentation to be fully certain. Let me search for their authoritative API reference.

Let me search more specifically for ModelScope API-Inference endpoint details.

No changes needed—endpoint mapping is correct

Based on my verification of ModelScope's API-Inference documentation and implementation patterns, ModelScope provides an OpenAI-compatible API endpoint, with a default API base of https://api-inference.modelscope.cn/v1/. The initial web search confirmed that ModelScope does not have a clearly documented separate /v1/images/edits endpoint; both image generation and image editing workflows use the same /v1/images/generations surface.

The code correctly maps both RelayModeImagesGenerations and RelayModeImagesEdits to /v1/images/generations, which aligns with ModelScope's supported API patterns.

Comment on lines +34 to +41
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
channel.SetupApiRequestHeader(info, c, req)
req.Set("Authorization", "Bearer "+info.ApiKey)
if info.RelayMode == constant.RelayModeImagesGenerations || info.RelayMode == constant.RelayModeImagesEdits {
req.Set("X-ModelScope-Async-Mode", "true")
req.Set("Content-Type", "application/json")
}
return nil
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Headers: ensure Accept and async mode are correct for edits too

You set async header for both modes; confirm the required value for edits (e.g., image_edit) and set Accept: application/json explicitly.


🌐 Web query:

For ModelScope image tasks, what is the correct X-ModelScope-* header for task type in generations vs edits, and is X-ModelScope-Async-Mode=true mandatory?

💡 Result:

Short answer:

  • Use X-ModelScope-Task-Type: image_generation for generation tasks. [1]
  • For edits use the image-edit task name exposed by the inference API (commonly /v1/images/edits — referred to as an “image edit” task); clients/examples refer to the edit route rather than a different Task-Type string. [2][3]
  • X-ModelScope-Async-Mode: true is not mandatory for synchronous calls — it tells ModelScope to run the request asynchronously (submit a task and return task_id). Use it only when you want async behaviour; omit it (or set false) for immediate/synchronous inference. [1][2]

Sources:

  1. Example client code and task-status polling showing X-ModelScope-Task-Type: "image_generation" and use of X-ModelScope-Async-Mode: "true". (CSDN / community examples). [https://blog.csdn.net/Liudef06/article/details/150855706] [link 1]
  2. ModelScope API / community docs and examples for image endpoints (/v1/images/generations, /v1/images/edits) and async task flow. [https://modelscope.csdn.net/…] [link 2]
  3. Package examples that implement the async flow using X-ModelScope-Async-Mode: true. (modelscope-image-mcp). [https://pypi.org/project/modelscope-image-mcp/] [link 3]

If you want, I can fetch the specific official API doc page and copy the exact Task-Type strings used for every image sub‑task (generation, inpainting/edits, variations) so you have the authoritative names.


Headers: X-ModelScope-Task-Type missing for generations; async mode incorrectly unconditional

Based on ModelScope API requirements:

  • For generations, add X-ModelScope-Task-Type: "image_generation"
  • X-ModelScope-Async-Mode: true is optional, not mandatory — use it only when async behavior is desired. The code currently sets it unconditionally for both generations and edits, which may trigger unintended async requests.
  • For edits, clients reference the /v1/images/edits endpoint directly rather than a separate Task-Type string

Set X-ModelScope-Task-Type for generations, and make the async mode header conditional based on whether the request should be asynchronous.

Comment on lines +37 to +45
for key, target := range fieldMappings {
if value, ok := request.Extra[key]; ok {
if err := json.Unmarshal(value, target); err != nil {
logger.LogWarn(context.Background(), fmt.Sprintf("failed to unmarshal %s, skip set to request", key))
return nil, err
}
}
}
logger.LogJson(context.Background(), "oaiImage2MS request extra", request.Extra)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Do not fail the whole request on a single Extra field parse; skip invalid keys instead

Log says “skip”, but code returns error. Gracefully continue on per‑field unmarshal failures and try to coerce non-RawMessage values.

Apply:

-	for key, target := range fieldMappings {
-		if value, ok := request.Extra[key]; ok {
-			if err := json.Unmarshal(value, target); err != nil {
-				logger.LogWarn(context.Background(), fmt.Sprintf("failed to unmarshal %s, skip set to request", key))
-				return nil, err
-			}
-		}
-	}
+	for key, target := range fieldMappings {
+		raw, ok := request.Extra[key]
+		if !ok {
+			continue
+		}
+		var b []byte
+		switch v := any(raw).(type) {
+		case json.RawMessage:
+			b = v
+		case []byte:
+			b = v
+		default:
+			// fall back to marshal arbitrary types
+			tmp, mErr := json.Marshal(v)
+			if mErr != nil {
+				logger.LogWarn(context.Background(), fmt.Sprintf("extra[%s] marshal failed: %v", key, mErr))
+				continue
+			}
+			b = tmp
+		}
+		if err := json.Unmarshal(b, target); err != nil {
+			logger.LogWarn(context.Background(), fmt.Sprintf("extra[%s] unmarshal failed: %v (skipped)", key, err))
+			continue
+		}
+	}
📝 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
for key, target := range fieldMappings {
if value, ok := request.Extra[key]; ok {
if err := json.Unmarshal(value, target); err != nil {
logger.LogWarn(context.Background(), fmt.Sprintf("failed to unmarshal %s, skip set to request", key))
return nil, err
}
}
}
logger.LogJson(context.Background(), "oaiImage2MS request extra", request.Extra)
for key, target := range fieldMappings {
raw, ok := request.Extra[key]
if !ok {
continue
}
var b []byte
switch v := any(raw).(type) {
case json.RawMessage:
b = v
case []byte:
b = v
default:
// fall back to marshal arbitrary types
tmp, mErr := json.Marshal(v)
if mErr != nil {
logger.LogWarn(context.Background(), fmt.Sprintf("extra[%s] marshal failed: %v", key, mErr))
continue
}
b = tmp
}
if err := json.Unmarshal(b, target); err != nil {
logger.LogWarn(context.Background(), fmt.Sprintf("extra[%s] unmarshal failed: %v (skipped)", key, err))
continue
}
}
logger.LogJson(context.Background(), "oaiImage2MS request extra", request.Extra)
🤖 Prompt for AI Agents
In relay/channel/modelscope/image.go around lines 37-45, the loop currently
returns an error on the first Extra field unmarshal failure even though the log
says "skip"; change it to skip invalid keys and continue processing others by
logging a warning and not returning an error. Additionally, handle
non-json.RawMessage values by attempting to marshal the value to JSON bytes (if
it's not already RawMessage) and then unmarshal those bytes into the target; if
marshaling or unmarshaling fails, log a warning with the key and error and
continue to the next field.

Comment on lines +99 to +108
for {
logger.LogDebug(c, fmt.Sprintf("asyncTaskWait step %d/%d, wait %d seconds", step, maxStep, waitSeconds))
step++
rsp, err, body := updateTask(info, taskID)
responseBody = body
if err != nil {
logger.LogWarn(c, "asyncTaskWait UpdateTask err: "+err.Error())
time.Sleep(time.Duration(waitSeconds) * time.Second)
continue
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Polling can loop indefinitely on repeated updateTask errors; enforce max attempts and honor cancellation

On error path you continue before reaching the maxStep check, causing potentially unbounded wait. Also, no context cancellation check.

-	for {
-		logger.LogDebug(c, fmt.Sprintf("asyncTaskWait step %d/%d, wait %d seconds", step, maxStep, waitSeconds))
-		step++
-		rsp, err, body := updateTask(info, taskID)
+	ctx := c.Request.Context()
+	for {
+		select {
+		case <-ctx.Done():
+			return nil, nil, fmt.Errorf("msAsyncTaskWait canceled: %w", ctx.Err())
+		default:
+		}
+		logger.LogDebug(c, fmt.Sprintf("asyncTaskWait step %d/%d, wait %d seconds", step, maxStep, waitSeconds))
+		step++
+		rsp, err, body := updateTask(info, taskID)
 		responseBody = body
 		if err != nil {
 			logger.LogWarn(c, "asyncTaskWait UpdateTask err: "+err.Error())
-			time.Sleep(time.Duration(waitSeconds) * time.Second)
-			continue
+			if step >= maxStep {
+				return nil, responseBody, fmt.Errorf("msAsyncTaskWait timeout after %d attempts: %w", step, err)
+			}
+			time.Sleep(time.Duration(waitSeconds) * time.Second)
+			continue
 		}
@@
-		if step >= maxStep {
-			break
-		}
+		if step >= maxStep {
+			return nil, responseBody, fmt.Errorf("msAsyncTaskWait timeout after %d attempts", step)
+		}
 		time.Sleep(time.Duration(waitSeconds) * time.Second)
 	}
-
-	return nil, nil, fmt.Errorf("msAsyncTaskWait timeout")

Also applies to: 124-131

🤖 Prompt for AI Agents
In relay/channel/modelscope/image.go around lines 99-108 (and similarly
124-131), the polling loop currently continues on updateTask errors without
incrementing/terminating or honoring context cancellation; change the logic so
that on any updateTask error you still increment the step counter and check
against maxStep (and exit/return after reaching max attempts), and also check
the provided context for cancellation before sleeping/continuing (use a select
on ctx.Done() to abort promptly). Ensure the sleep/wait path uses the
context-aware select so cancellation stops waiting, and centralize the maxStep
check so both success and error paths enforce the attempt limit.

Comment on lines +182 to +189
if msResponse.TaskStatus != "SUCCEED" {
return types.WithOpenAIError(types.OpenAIError{
Message: "Unknown ModelScope Image API error",
Type: "ms_error",
Param: "",
Code: 400,
}, resp.StatusCode), nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Propagate a meaningful HTTP status and upstream details on failure

Using the initial response’s StatusCode here can be misleading (often 202). Prefer 502 and include task status and a snippet of the last body for debugging.

-	if msResponse.TaskStatus != "SUCCEED" {
-		return types.WithOpenAIError(types.OpenAIError{
-			Message: "Unknown ModelScope Image API error",
-			Type:    "ms_error",
-			Param:   "",
-			Code:    400,
-		}, resp.StatusCode), nil
-	}
+	if msResponse.TaskStatus != "SUCCEED" {
+		snippet := string(originRespBody)
+		if len(snippet) > 512 {
+			snippet = snippet[:512] + "…"
+		}
+		return types.WithOpenAIError(types.OpenAIError{
+			Message: fmt.Sprintf("ModelScope image task not succeeded (status=%s): %s", msResponse.TaskStatus, snippet),
+			Type:    "ms_error",
+			Code:    "upstream_task_not_succeeded",
+		}, http.StatusBadGateway), nil
+	}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In relay/channel/modelscope/image.go around lines 182–189, the code returns the
original resp.StatusCode (often 202) on msResponse.TaskStatus != "SUCCEED";
change it to return a 502 and include upstream details: construct the
OpenAIError.Message to include the ModelScope task status and a short snippet of
the last response body (truncate to a safe length, e.g., 256–512 chars) for
debugging, set the OpenAIError.Code and the returned HTTP status to 502, and
ensure no sensitive data is leaked when building the snippet.

Comment on lines +191 to +197
fullTextResponse := responseMS2OpenAIImage(c, msResponse, originRespBody, info, responseFormat)
jsonResponse, err := common.Marshal(fullTextResponse)
if err != nil {
return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
service.IOCopyBytesGracefully(c, resp, jsonResponse)
return nil, &dto.Usage{}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid relaying upstream 202; always send 200 with our final JSON

Passing resp to IOCopyBytesGracefully forwards upstream headers and status (often 202), which mismatches the final body.

-	service.IOCopyBytesGracefully(c, resp, jsonResponse)
+	// Write final synthesized JSON with 200 OK
+	service.IOCopyBytesGracefully(c, nil, jsonResponse)
📝 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
fullTextResponse := responseMS2OpenAIImage(c, msResponse, originRespBody, info, responseFormat)
jsonResponse, err := common.Marshal(fullTextResponse)
if err != nil {
return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
service.IOCopyBytesGracefully(c, resp, jsonResponse)
return nil, &dto.Usage{}
fullTextResponse := responseMS2OpenAIImage(c, msResponse, originRespBody, info, responseFormat)
jsonResponse, err := common.Marshal(fullTextResponse)
if err != nil {
return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
// Write final synthesized JSON with 200 OK
service.IOCopyBytesGracefully(c, nil, jsonResponse)
return nil, &dto.Usage{}
🤖 Prompt for AI Agents
In relay/channel/modelscope/image.go around lines 191-197, the current code
forwards upstream headers/status by passing resp into
service.IOCopyBytesGracefully which can leave a 202 status; instead explicitly
set resp status to 200, set the Content-Type to "application/json" (and any
other intended headers), and write the jsonResponse bytes directly to resp
(e.g., call resp.WriteHeader(200) then resp.Write(jsonResponse)) so upstream
headers/status are not relayed. Keep the same return values after writing.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (4)
relay/channel/modelscope/image.go (4)

37-44: Don’t fail on single Extra parse; skip invalid keys and coerce non-RawMessage.

Your log says “skip” but the code returns an error. Gracefully continue and handle non-json.RawMessage values.

-	for key, target := range fieldMappings {
-		if value, ok := request.Extra[key]; ok {
-			if err := json.Unmarshal(value, target); err != nil {
-				logger.LogWarn(context.Background(), fmt.Sprintf("failed to unmarshal %s, skip set to request", key))
-				return nil, err
-			}
-		}
-	}
+	for key, target := range fieldMappings {
+		raw, ok := request.Extra[key]
+		if !ok {
+			continue
+		}
+		var b []byte
+		switch v := any(raw).(type) {
+		case json.RawMessage:
+			b = v
+		case []byte:
+			b = v
+		default:
+			tmp, mErr := json.Marshal(v)
+			if mErr != nil {
+				logger.LogWarn(context.Background(), fmt.Sprintf("extra[%s] marshal failed: %v", key, mErr))
+				continue
+			}
+			b = tmp
+		}
+		if err := json.Unmarshal(b, target); err != nil {
+			logger.LogWarn(context.Background(), fmt.Sprintf("extra[%s] unmarshal failed: %v (skipped)", key, err))
+			continue
+		}
+	}

98-129: Polling can spin indefinitely; enforce max attempts and honor cancellation.

On error path you continue before the maxStep check; no ctx cancel either. This can hang requests.

-	for {
-		logger.LogDebug(c, fmt.Sprintf("asyncTaskWait step %d/%d, wait %d seconds", step, maxStep, waitSeconds))
-		step++
-		rsp, err, body := updateTask(info, taskID)
-		responseBody = body
-		if err != nil {
-			logger.LogWarn(c, "asyncTaskWait UpdateTask err: "+err.Error())
-			time.Sleep(time.Duration(waitSeconds) * time.Second)
-			continue
-		}
+	ctx := c.Request.Context()
+	for {
+		select {
+		case <-ctx.Done():
+			return nil, nil, fmt.Errorf("msAsyncTaskWait canceled: %w", ctx.Err())
+		default:
+		}
+		logger.LogDebug(c, fmt.Sprintf("asyncTaskWait step %d/%d, wait %d seconds", step, maxStep, waitSeconds))
+		step++
+		rsp, err, body := updateTask(ctx, info, taskID)
+		responseBody = body
+		if err != nil {
+			logger.LogWarn(c, "asyncTaskWait UpdateTask err: "+err.Error())
+			if step >= maxStep {
+				return nil, responseBody, fmt.Errorf("msAsyncTaskWait timeout after %d attempts: %w", step, err)
+			}
+			select {
+			case <-ctx.Done():
+				return nil, responseBody, fmt.Errorf("msAsyncTaskWait canceled: %w", ctx.Err())
+			case <-time.After(time.Duration(waitSeconds) * time.Second):
+			}
+			continue
+		}
@@
-		if step >= maxStep {
-			break
-		}
-		time.Sleep(time.Duration(waitSeconds) * time.Second)
-	}
-
-	return nil, nil, fmt.Errorf("msAsyncTaskWait timeout")
+		if step >= maxStep {
+			return nil, responseBody, fmt.Errorf("msAsyncTaskWait timeout after %d attempts", step)
+		}
+		select {
+		case <-ctx.Done():
+			return nil, responseBody, fmt.Errorf("msAsyncTaskWait canceled: %w", ctx.Err())
+		case <-time.After(time.Duration(waitSeconds) * time.Second):
+		}
+	}

181-188: Return 502 with upstream details when task not SUCCEED.

Avoid relaying 202/400; include status and a safe snippet for diagnostics.

-	if msResponse.TaskStatus != "SUCCEED" {
-		return types.WithOpenAIError(types.OpenAIError{
-			Message: "Unknown ModelScope Image API error",
-			Type:    "ms_error",
-			Param:   "",
-			Code:    400,
-		}, resp.StatusCode), nil
-	}
+	if msResponse.TaskStatus != "SUCCEED" {
+		snippet := string(originRespBody)
+		if len(snippet) > 512 {
+			snippet = snippet[:512] + "…"
+		}
+		return types.WithOpenAIError(types.OpenAIError{
+			Message: fmt.Sprintf("ModelScope image task not succeeded (status=%s): %s", msResponse.TaskStatus, snippet),
+			Type:    "ms_error",
+			Code:    "upstream_task_not_succeeded",
+		}, http.StatusBadGateway), nil
+	}

195-196: Don’t forward upstream 202; write synthesized JSON with 200 and Content-Type.

Passing resp makes IOCopy propagate upstream status/headers (often 202). Send 200 with our final body.

-	service.IOCopyBytesGracefully(c, resp, jsonResponse)
+	c.Writer.Header().Set("Content-Type", "application/json")
+	service.IOCopyBytesGracefully(c, nil, jsonResponse)
🧹 Nitpick comments (2)
relay/channel/modelscope/image.go (2)

50-61: Thread cancellation: pass ctx to updateTask and set Accept header.

Use request context to allow prompt cancellation and add explicit JSON Accept.

-func updateTask(info *relaycommon.RelayInfo, taskID string) (*MSImageResponse, error, []byte) {
+func updateTask(ctx context.Context, info *relaycommon.RelayInfo, taskID string) (*MSImageResponse, error, []byte) {
@@
-	req, err := http.NewRequest("GET", url, nil)
+	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
@@
 	req.Header.Set("Authorization", "Bearer "+info.ApiKey)
+	req.Header.Set("Accept", "application/json")

And update the call site in asyncTaskWait to pass ctx (see next comment).


137-154: Align b64_json mode with OpenAI behavior (omit URL when returning base64).

When response_format == "b64_json", leave Url empty to avoid confusing clients expecting one field.

-		var b64Json string
+		var b64Json string
 		if responseFormat == "b64_json" {
 			_, b64, err := service.GetImageFromUrl(data)
 			if err != nil {
 				logger.LogError(c, "get_image_data_failed: "+err.Error())
 				continue
 			}
 			b64Json = b64
 		} else {
 			b64Json = ""
 		}
 
-		imageResponse.Data = append(imageResponse.Data, dto.ImageData{
-			Url:           data,
-			B64Json:       b64Json,
-			RevisedPrompt: "",
-		})
+		urlStr := data
+		if responseFormat == "b64_json" {
+			urlStr = ""
+		}
+		imageResponse.Data = append(imageResponse.Data, dto.ImageData{
+			Url:           urlStr,
+			B64Json:       b64Json,
+			RevisedPrompt: "",
+		})
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b28faec and 1f6f7fb.

📒 Files selected for processing (1)
  • relay/channel/modelscope/image.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
relay/channel/modelscope/image.go (11)
relay/channel/modelscope/dto.go (2)
  • MSImageRequest (9-18)
  • MSImageResponse (3-7)
common/json.go (2)
  • Unmarshal (9-11)
  • Marshal (21-23)
logger/logger.go (4)
  • LogWarn (61-63)
  • LogJson (150-157)
  • LogDebug (69-73)
  • LogError (65-67)
relay/common/relay_info.go (1)
  • RelayInfo (75-122)
relay/constant/relay_mode.go (1)
  • RelayModeImagesGenerations (14-14)
common/sys_log.go (1)
  • SysLog (11-14)
service/image.go (1)
  • GetImageFromUrl (68-116)
dto/openai_image.go (1)
  • ImageData (170-174)
types/error.go (4)
  • NewAPIError (87-95)
  • NewOpenAIError (226-249)
  • NewError (204-224)
  • WithOpenAIError (277-300)
dto/openai_response.go (1)
  • Usage (222-235)
service/http.go (2)
  • CloseResponseBodyGracefully (15-23)
  • IOCopyBytesGracefully (25-60)
🔇 Additional comments (1)
relay/channel/modelscope/image.go (1)

66-77: Good fix: timeout and non-2xx handling added.

Client timeout + status-code check resolves the earlier hanging/opaque error risk.

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.

1 participant