Skip to content

修复豆包图像编辑(图生图)功能 - #2090

Merged
creamlike1024 merged 3 commits into
QuantumNous:mainfrom
feitianbubu:pr/doubao-image-edit
Oct 23, 2025
Merged

修复豆包图像编辑(图生图)功能#2090
creamlike1024 merged 3 commits into
QuantumNous:mainfrom
feitianbubu:pr/doubao-image-edit

Conversation

@feitianbubu

@feitianbubu feitianbubu commented Oct 23, 2025

Copy link
Copy Markdown
Member
  1. 修复豆包图生图路径请求错误问题
  2. 修复豆包图生图content-type设置错误问题
  3. 豆包图像不支持本地文件,只支持传url,需要在image输入图像url
    请求示例:
curl http://localhost:3000/v1/images/edits \
  --request POST \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'image=https://ark-project.tos-cn-beijing.volces.com/doc_image/seedream4_imagesToimages_2.png' \
  --data-urlencode 'prompt=美化一下' \
  --data-urlencode 'model=doubao-seedream-4-0-250828' \
  --data-urlencode 'response_format=url'

返回示例:

{
  "model": "doubao-seedream-4-0-250828",
  "created": 1761225217,
  "data": [
    {
      "url": "https://ark-content-generation-v2-cn-beijing.tos-cn-beijing.volces.com/doubao-seedream-4-0/0217612252072246d5ff489b765bb27d62f4fef47b36807800341_0.jpeg?X-Tos-Algorithm=TOS4-HMAC-SHA256&X-Tos-Credential=AKLTYWJkZTExNjA1ZDUyNDc3YzhjNTM5OGIyNjBhNDcyOTQ%2F20251023%2Fcn-beijing%2Ftos%2Frequest&X-Tos-Date=20251023T131337Z&X-Tos-Expires=86400&X-Tos-Signature=638bf5af7160bfeee98a4b731eaf765cc57ace0fbbc1491913537d75da38b808&X-Tos-SignedHeaders=host&x-tos-process=image%2Fwatermark%2Cimage_YXNzZXRzL3dhdGVybWFyay5wbmc_eC10b3MtcHJvY2Vzcz1pbWFnZS9yZXNpemUsUF8xNQ%3D%3D",
      "size": "2048x2048"
    }
  ],
  "usage": {
    "generated_images": 1,
    "output_tokens": 16384,
    "total_tokens": 16384
  }
}
image

Summary by CodeRabbit

  • New Features

    • Support for additional form-data submission formats for image requests.
    • Added support for an Image payload field in image requests.
  • Bug Fixes

    • More reliable content-type validation for image operation endpoints.
  • Refactor

    • Consolidated image edit and generation handling and simplified image request flow.

@coderabbitai

coderabbitai Bot commented Oct 23, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds form and multipart form parsing to request unmarshalling, introduces an Image field to the image DTO, tightens middleware Content-Type checks using explicit MIME membership, and removes server-side multipart construction for Volcengine image edits, routing edits through the generations JSON endpoint.

Changes

Cohort / File(s) Summary
Form data parsing
common/gin.go
Added encoding/json, net/url imports; extended UnmarshalBodyReusable to handle application/x-www-form-urlencoded and multipart/form-data by delegating to new unexported helpers parseFormData and parseMultipartFormData that convert form values to JSON then unmarshal into target.
Image DTO
dto/openai_image.go
Added Image json.RawMessage \json:"image,omitempty"`field toImageRequest` to accept an additional image payload.
Middleware Content-Type check
middleware/distributor.go
Added slices import; replaced substring check with explicit MIME membership check (slices.Contains) against gin.MIMEPOSTForm and gin.MIMEMultipartPOSTForm for /v1/images/edits.
Volcengine adaptor changes
relay/channel/volcengine/adaptor.go
Removed mime/multipart and net/textproto imports; commented-out/removed multipart construction in ConvertImageRequest for RelayModeImagesEdits; consolidated images edits/generations routing to a single images/generations endpoint and set Content-Type: application/json for edits in header setup.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Middleware
    participant Handler
    participant Volcengine

    Note over Client,Volcengine: Image Edits Request Flow (updated)

    Client->>Middleware: POST /v1/images/edits (form/multipart)
    activate Middleware
    Middleware->>Middleware: Check Content-Type via slices.Contains
    Middleware->>Handler: Forward validated request
    deactivate Middleware

    activate Handler
    Handler->>Handler: UnmarshalBodyReusable
    alt form-urlencoded
        Handler->>Handler: parseFormData -> map -> JSON -> unmarshal
    else multipart/form-data
        Handler->>Handler: parseMultipartFormData -> map -> JSON -> unmarshal
    end
    Handler->>Volcengine: Forward request as JSON (images/generations)
    deactivate Handler

    activate Volcengine
    Volcengine->>Volcengine: Process edits via generations endpoint
    Volcengine-->>Client: Response
    deactivate Volcengine
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 I nibble bytes in twilight code,

Forms unfold down every road,
Images hum in JSON light,
Multipart trimmed for simpler flight,
Hops of change — a tidy mode.

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 PR title "修复豆包图像编辑(图生图)功能" (Fix Doubao image editing/image-to-image feature) is directly related to the primary objective of this pull request, which is to fix incorrect request path and Content-Type settings for Doubao's image editing functionality. The title is clear, specific, and concise. While the changeset includes supporting modifications to general form data parsing infrastructure (common/gin.go, dto/openai_image.go, middleware/distributor.go), the most significant and targeted changes are in relay/channel/volcengine/adaptor.go, which are exactly what the title describes. The title accurately represents the developer's main intent and primary feature being fixed.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3ac9ff6 and 95a2d02.

📒 Files selected for processing (1)
  • middleware/distributor.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • middleware/distributor.go

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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
relay/channel/volcengine/adaptor.go (1)

105-210: Remove commented code instead of leaving it in place.

The commented-out multipart handling (lines 106-205) is extensive and will not be used. Leaving large blocks of commented code reduces readability and can confuse future maintainers. Remove it entirely to keep the codebase clean.

Apply this diff to remove the dead code:

 	switch info.RelayMode {
 	case constant.RelayModeImagesGenerations:
 		return request, nil
-	// 根据官方文档,并没有发现豆包生图支持表单请求:https://www.volcengine.com/docs/82379/1824121
-	//case constant.RelayModeImagesEdits:
-	//
-	//	var requestBody bytes.Buffer
-	//	writer := multipart.NewWriter(&requestBody)
-	//
-	//	writer.WriteField("model", request.Model)
-	//
-	//	formData := c.Request.PostForm
-	//	for key, values := range formData {
-	//		if key == "model" {
-	//			continue
-	//		}
-	//		for _, value := range values {
-	//			writer.WriteField(key, value)
-	//		}
-	//	}
-	//
-	//	if err := c.Request.ParseMultipartForm(32 << 20); err != nil {
-	//		return nil, errors.New("failed to parse multipart form")
-	//	}
-	//
-	//	if c.Request.MultipartForm != nil && c.Request.MultipartForm.File != nil {
-	//		var imageFiles []*multipart.FileHeader
-	//		var exists bool
-	//
-	//		if imageFiles, exists = c.Request.MultipartForm.File["image"]; !exists || len(imageFiles) == 0 {
-	//			if imageFiles, exists = c.Request.MultipartForm.File["image[]"]; !exists || len(imageFiles) == 0 {
-	//				foundArrayImages := false
-	//				for fieldName, files := range c.Request.MultipartForm.File {
-	//					if strings.HasPrefix(fieldName, "image[") && len(files) > 0 {
-	//						foundArrayImages = true
-	//						for _, file := range files {
-	//							imageFiles = append(imageFiles, file)
-	//						}
-	//					}
-	//				}
-	//
-	//				if !foundArrayImages && (len(imageFiles) == 0) {
-	//					return nil, errors.New("image is required")
-	//				}
-	//			}
-	//		}
-	//
-	//		for i, fileHeader := range imageFiles {
-	//			file, err := fileHeader.Open()
-	//			if err != nil {
-	//				return nil, fmt.Errorf("failed to open image file %d: %w", i, err)
-	//			}
-	//			defer file.Close()
-	//
-	//			fieldName := "image"
-	//			if len(imageFiles) > 1 {
-	//				fieldName = "image[]"
-	//			}
-	//
-	//			mimeType := detectImageMimeType(fileHeader.Filename)
-	//
-	//			h := make(textproto.MIMEHeader)
-	//			h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, fieldName, fileHeader.Filename))
-	//			h.Set("Content-Type", mimeType)
-	//
-	//			part, err := writer.CreatePart(h)
-	//			if err != nil {
-	//				return nil, fmt.Errorf("create form part failed for image %d: %w", i, err)
-	//			}
-	//
-	//			if _, err := io.Copy(part, file); err != nil {
-	//				return nil, fmt.Errorf("copy file failed for image %d: %w", i, err)
-	//			}
-	//		}
-	//
-	//		if maskFiles, exists := c.Request.MultipartForm.File["mask"]; exists && len(maskFiles) > 0 {
-	//			maskFile, err := maskFiles[0].Open()
-	//			if err != nil {
-	//				return nil, errors.New("failed to open mask file")
-	//			}
-	//			defer maskFile.Close()
-	//
-	//			mimeType := detectImageMimeType(maskFiles[0].Filename)
-	//
-	//			h := make(textproto.MIMEHeader)
-	//			h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="mask"; filename="%s"`, maskFiles[0].Filename))
-	//			h.Set("Content-Type", mimeType)
-	//
-	//			maskPart, err := writer.CreatePart(h)
-	//			if err != nil {
-	//				return nil, errors.New("create form file failed for mask")
-	//			}
-	//
-	//			if _, err := io.Copy(maskPart, maskFile); err != nil {
-	//				return nil, errors.New("copy mask file failed")
-	//			}
-	//		}
-	//	} else {
-	//		return nil, errors.New("no multipart form data found")
-	//	}
-	//
-	//	writer.Close()
-	//	c.Request.Header.Set("Content-Type", writer.FormDataContentType())
-	//	return bytes.NewReader(requestBody.Bytes()), nil
 
 	default:
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b99099f and 3ac9ff6.

📒 Files selected for processing (4)
  • common/gin.go (3 hunks)
  • dto/openai_image.go (1 hunks)
  • middleware/distributor.go (2 hunks)
  • relay/channel/volcengine/adaptor.go (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
middleware/distributor.go (1)
dto/request_common.go (1)
  • Request (8-12)
relay/channel/volcengine/adaptor.go (1)
relay/constant/relay_mode.go (2)
  • RelayModeImagesGenerations (14-14)
  • RelayModeImagesEdits (15-15)
common/gin.go (1)
common/json.go (2)
  • Marshal (21-23)
  • Unmarshal (9-11)
🔇 Additional comments (4)
dto/openai_image.go (1)

30-31: LGTM! Image field addition supports URL-based image edits.

The new Image field enables passing image data (likely as URLs) in JSON requests, which aligns with the PR's goal of supporting Doubao's URL-based image editing API.

relay/channel/volcengine/adaptor.go (2)

253-257: LGTM! Correct endpoint consolidation for Doubao.

Consolidating both ImagesGenerations and ImagesEdits under the /images/generations endpoint with JSON content type correctly reflects the Doubao API behavior documented in the official docs.


281-283: LGTM! Explicit JSON Content-Type for image edits.

Setting Content-Type to application/json for RelayModeImagesEdits aligns with the shift away from multipart form data and ensures the request is properly formatted for the Doubao API.

common/gin.go (1)

45-48: The review comment is based on an incorrect assumption about the request flow.

The generic form parsers (parseFormData and parseMultipartFormData) in common/gin.go correctly only extract text fields—but this is not a problem for image edits endpoints because they don't use these generic parsers.

Image edits requests (RelayModeImagesEdits) bypass UnmarshalBodyReusable entirely:

  1. valid_request.go (lines 141-151): Image edit validation calls c.MultipartForm() directly and extracts text fields from c.Request.PostForm, bypassing the generic parsers.

  2. Volcengine/Doubao adapter (line 281): For image edits, the Content-Type is explicitly set to "application/json", not multipart/form-data. The request is converted to JSON before sending upstream.

  3. File upload pattern: When other channels require file uploads (OpenAI audio, Dify files, Cloudflare audio), they use explicit c.Request.FormFile() extraction, not the generic parsers.

The generic form parsers are intentionally text-only utilities for simple form data serialization, not for file upload handling. Image edits leverage URL-based image inputs (as noted in the review) and don't require file upload support—which is correctly reflected in the implementation.

Comment thread common/gin.go
Comment on lines +176 to +178
if boundary == "" {
return json.Unmarshal(data, v) // Fallback to JSON
}

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 | 🟡 Minor

Questionable fallback: Missing boundary should be an error.

If Content-Type is multipart/form-data but the boundary parameter is missing, that's a malformed request. Falling back to JSON parsing (line 177) masks this error and could lead to confusing behavior.

Consider returning an error instead:

 	if boundary == "" {
-		return json.Unmarshal(data, v) // Fallback to JSON
+		return errors.New("multipart/form-data Content-Type is missing required boundary parameter")
 	}
📝 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
if boundary == "" {
return json.Unmarshal(data, v) // Fallback to JSON
}
if boundary == "" {
return errors.New("multipart/form-data Content-Type is missing required boundary parameter")
}
🤖 Prompt for AI Agents
In common/gin.go around lines 176-178, do not silently fall back to JSON when
Content-Type is multipart/form-data but the boundary is missing; instead return
a clear error indicating a malformed multipart request (e.g., "missing multipart
boundary" or a wrapped error) so callers can handle it as a bad request; remove
the json.Unmarshal fallback and return that error (ensure the function's
signature supports returning the error type expected by callers).

Comment thread middleware/distributor.go
"errors"
"fmt"
"net/http"
"slices"

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.

🛠️ Refactor suggestion | 🟠 Major

Unused import after fixing the Content-Type check.

After applying the fix above, the slices import will no longer be needed and should be removed.

🤖 Prompt for AI Agents
In middleware/distributor.go at line 7 the import "slices" is now unused after
the Content-Type check fix; remove the "slices" entry from the import block (and
tidy imports if needed) so the file compiles without unused-import errors.

Comment thread middleware/distributor.go Outdated
Comment on lines 249 to 252
contentType := c.Request.Header.Get("Content-Type")
if slices.Contains([]string{gin.MIMEPOSTForm, gin.MIMEMultipartPOSTForm}, contentType) {
modelRequest.Model = c.PostForm("model")
}

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

Critical: Content-Type matching will fail for real requests.

The exact match check using slices.Contains will fail because Content-Type headers typically include parameters. For example, multipart/form-data; boundary=----WebKitFormBoundary... will not match gin.MIMEMultipartPOSTForm ("multipart/form-data").

Note that common/gin.go (lines 45-48) correctly uses strings.Contains() for the same purpose, which handles parameterized headers properly.

Apply this diff to fix the Content-Type matching:

-	contentType := c.Request.Header.Get("Content-Type")
-	if slices.Contains([]string{gin.MIMEPOSTForm, gin.MIMEMultipartPOSTForm}, contentType) {
+	contentType := c.Request.Header.Get("Content-Type")
+	if strings.Contains(contentType, gin.MIMEPOSTForm) || strings.Contains(contentType, gin.MIMEMultipartPOSTForm) {
 		modelRequest.Model = c.PostForm("model")
 	}
📝 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
contentType := c.Request.Header.Get("Content-Type")
if slices.Contains([]string{gin.MIMEPOSTForm, gin.MIMEMultipartPOSTForm}, contentType) {
modelRequest.Model = c.PostForm("model")
}
contentType := c.Request.Header.Get("Content-Type")
if strings.Contains(contentType, gin.MIMEPOSTForm) || strings.Contains(contentType, gin.MIMEMultipartPOSTForm) {
modelRequest.Model = c.PostForm("model")
}
🤖 Prompt for AI Agents
In middleware/distributor.go around lines 249 to 252, the Content-Type check
uses slices.Contains which does exact matches and fails for parameterized
headers (e.g. "multipart/form-data; boundary=..."); change the logic to use
strings.Contains(c.Request.Header.Get("Content-Type"), gin.MIMEPOSTForm) ||
strings.Contains(..., gin.MIMEMultipartPOSTForm) so parameterized types match,
and add an import for the strings package if not already present; keep the
existing c.PostForm("model") assignment.

multipart/form-data; boundary
@creamlike1024
creamlike1024 merged commit 032f159 into QuantumNous:main Oct 23, 2025
1 check passed
ennnnny pushed a commit to ennnnny/new-api that referenced this pull request Mar 17, 2026
…edit

修复豆包图像编辑(图生图)功能
jiutubaba pushed a commit to jiutubaba/fx-api that referenced this pull request May 17, 2026
…n-zero

feat(ops): allow retention days = 0 to wipe table on each scheduled cleanup
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.

2 participants