Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions dto/values.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package dto

import (
"encoding/json"
"strconv"
)

type IntValue int

func (i *IntValue) UnmarshalJSON(b []byte) error {
var n int
if err := json.Unmarshal(b, &n); err == nil {
*i = IntValue(n)
return nil
}
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
v, err := strconv.Atoi(s)
if err != nil {
return err
}
*i = IntValue(v)
return nil
}

func (i IntValue) MarshalJSON() ([]byte, error) {
return json.Marshal(int(i))
}

type BoolValue bool

func (b *BoolValue) UnmarshalJSON(data []byte) error {
var boolean bool
if err := json.Unmarshal(data, &boolean); err == nil {
*b = BoolValue(boolean)
return nil
}
var str string
if err := json.Unmarshal(data, &str); err != nil {
return err
}
if str == "true" {
*b = BoolValue(true)
} else if str == "false" {
*b = BoolValue(false)
} else {
return json.Unmarshal(data, &boolean)
}
return nil
}
func (b BoolValue) MarshalJSON() ([]byte, error) {
return json.Marshal(bool(b))
}
92 changes: 75 additions & 17 deletions relay/channel/task/doubao/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import (
"fmt"
"io"
"net/http"
"time"

"github.com/QuantumNous/new-api/common"

"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
Expand All @@ -23,18 +26,36 @@ import (
// ============================

type ContentItem struct {
Type string `json:"type"` // "text" or "image_url"
Text string `json:"text,omitempty"` // for text type
ImageURL *ImageURL `json:"image_url,omitempty"` // for image_url type
Type string `json:"type"` // "text", "image_url" or "video"
Text string `json:"text,omitempty"` // for text type
ImageURL *ImageURL `json:"image_url,omitempty"` // for image_url type
Video *VideoReference `json:"video,omitempty"` // for video (sample) type
}

type ImageURL struct {
URL string `json:"url"`
}

type VideoReference struct {
URL string `json:"url"` // Draft video URL
}

type requestPayload struct {
Model string `json:"model"`
Content []ContentItem `json:"content"`
Model string `json:"model"`
Content []ContentItem `json:"content"`
CallbackURL string `json:"callback_url,omitempty"`
ReturnLastFrame *dto.BoolValue `json:"return_last_frame,omitempty"`
ServiceTier string `json:"service_tier,omitempty"`
ExecutionExpiresAfter dto.IntValue `json:"execution_expires_after,omitempty"`
GenerateAudio *dto.BoolValue `json:"generate_audio,omitempty"`
Draft *dto.BoolValue `json:"draft,omitempty"`
Resolution string `json:"resolution,omitempty"`
Ratio string `json:"ratio,omitempty"`
Duration dto.IntValue `json:"duration,omitempty"`
Frames dto.IntValue `json:"frames,omitempty"`
Seed dto.IntValue `json:"seed,omitempty"`
CameraFixed *dto.BoolValue `json:"camera_fixed,omitempty"`
Watermark *dto.BoolValue `json:"watermark,omitempty"`
}

type responsePayload struct {
Expand All @@ -53,6 +74,7 @@ type responseTask struct {
Duration int `json:"duration"`
Ratio string `json:"ratio"`
FramesPerSecond int `json:"framespersecond"`
ServiceTier string `json:"service_tier"`
Usage struct {
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
Expand Down Expand Up @@ -98,16 +120,16 @@ func (a *TaskAdaptor) BuildRequestHeader(c *gin.Context, req *http.Request, info

// BuildRequestBody converts request into Doubao specific format.
func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayInfo) (io.Reader, error) {
v, exists := c.Get("task_request")
if !exists {
return nil, fmt.Errorf("request not found in context")
req, err := relaycommon.GetTaskRequest(c)
if err != nil {
return nil, err
}
req := v.(relaycommon.TaskSubmitReq)

body, err := a.convertToRequestPayload(&req)
if err != nil {
return nil, errors.Wrap(err, "convert request payload failed")
}
info.UpstreamModelName = body.Model
data, err := json.Marshal(body)
if err != nil {
return nil, err
Expand Down Expand Up @@ -141,7 +163,13 @@ func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *rela
return
}

c.JSON(http.StatusOK, gin.H{"task_id": dResp.ID})
ov := dto.NewOpenAIVideo()
ov.ID = dResp.ID
ov.TaskID = dResp.ID
ov.CreatedAt = time.Now().Unix()
ov.Model = info.OriginModelName

c.JSON(http.StatusOK, ov)
return dResp.ID, responseBody, nil
}

Expand Down Expand Up @@ -204,12 +232,15 @@ func (a *TaskAdaptor) convertToRequestPayload(req *relaycommon.TaskSubmitReq) (*
}
}

// TODO: Add support for additional parameters from metadata
// such as ratio, duration, seed, etc.
// metadata := req.Metadata
// if metadata != nil {
// // Parse and apply metadata parameters
// }
metadata := req.Metadata
medaBytes, err := json.Marshal(metadata)
if err != nil {
return nil, errors.Wrap(err, "metadata marshal metadata failed")
}
err = json.Unmarshal(medaBytes, &r)
if err != nil {
return nil, errors.Wrap(err, "unmarshal metadata failed")
}
Comment on lines +235 to +243

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

Security: metadata “unmarshal into payload” enables callback_url SSRF + overrides content/model.

json.Unmarshal(medaBytes, &r) (Line 240) allows user-supplied metadata to set callback_url, content, model, etc. This is risky (SSRF/callback abuse + bypassing server-side model/content validation). Prefer an explicit allowlist of metadata fields and map them onto requestPayload (and validate CallbackURL if it remains supported).

Safer pattern sketch (allowlist-only)
- err = json.Unmarshal(medaBytes, &r)
- if err != nil {
-   return nil, errors.Wrap(err, "unmarshal metadata failed")
- }
+ type payloadMeta struct {
+   CallbackURL           string         `json:"callback_url,omitempty"`
+   ReturnLastFrame       *dto.BoolValue `json:"return_last_frame,omitempty"`
+   ServiceTier           string         `json:"service_tier,omitempty"`
+   ExecutionExpiresAfter dto.IntValue   `json:"execution_expires_after,omitempty"`
+   GenerateAudio         *dto.BoolValue `json:"generate_audio,omitempty"`
+   Draft                 *dto.BoolValue `json:"draft,omitempty"`
+   Resolution            string         `json:"resolution,omitempty"`
+   Ratio                 string         `json:"ratio,omitempty"`
+   Duration              dto.IntValue   `json:"duration,omitempty"`
+   Frames                dto.IntValue   `json:"frames,omitempty"`
+   Seed                  dto.IntValue   `json:"seed,omitempty"`
+   CameraFixed           *dto.BoolValue `json:"camera_fixed,omitempty"`
+   Watermark             *dto.BoolValue `json:"watermark,omitempty"`
+ }
+ var m payloadMeta
+ if err := json.Unmarshal(medaBytes, &m); err != nil {
+   return nil, errors.Wrap(err, "unmarshal metadata failed")
+ }
+ r.CallbackURL = m.CallbackURL
+ r.ReturnLastFrame = m.ReturnLastFrame
+ r.ServiceTier = m.ServiceTier
+ r.ExecutionExpiresAfter = m.ExecutionExpiresAfter
+ r.GenerateAudio = m.GenerateAudio
+ r.Draft = m.Draft
+ r.Resolution = m.Resolution
+ r.Ratio = m.Ratio
+ r.Duration = m.Duration
+ r.Frames = m.Frames
+ r.Seed = m.Seed
+ r.CameraFixed = m.CameraFixed
+ r.Watermark = m.Watermark
📝 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
metadata := req.Metadata
medaBytes, err := json.Marshal(metadata)
if err != nil {
return nil, errors.Wrap(err, "metadata marshal metadata failed")
}
err = json.Unmarshal(medaBytes, &r)
if err != nil {
return nil, errors.Wrap(err, "unmarshal metadata failed")
}
metadata := req.Metadata
medaBytes, err := json.Marshal(metadata)
if err != nil {
return nil, errors.Wrap(err, "metadata marshal metadata failed")
}
type payloadMeta struct {
CallbackURL string `json:"callback_url,omitempty"`
ReturnLastFrame *dto.BoolValue `json:"return_last_frame,omitempty"`
ServiceTier string `json:"service_tier,omitempty"`
ExecutionExpiresAfter dto.IntValue `json:"execution_expires_after,omitempty"`
GenerateAudio *dto.BoolValue `json:"generate_audio,omitempty"`
Draft *dto.BoolValue `json:"draft,omitempty"`
Resolution string `json:"resolution,omitempty"`
Ratio string `json:"ratio,omitempty"`
Duration dto.IntValue `json:"duration,omitempty"`
Frames dto.IntValue `json:"frames,omitempty"`
Seed dto.IntValue `json:"seed,omitempty"`
CameraFixed *dto.BoolValue `json:"camera_fixed,omitempty"`
Watermark *dto.BoolValue `json:"watermark,omitempty"`
}
var m payloadMeta
if err := json.Unmarshal(medaBytes, &m); err != nil {
return nil, errors.Wrap(err, "unmarshal metadata failed")
}
r.CallbackURL = m.CallbackURL
r.ReturnLastFrame = m.ReturnLastFrame
r.ServiceTier = m.ServiceTier
r.ExecutionExpiresAfter = m.ExecutionExpiresAfter
r.GenerateAudio = m.GenerateAudio
r.Draft = m.Draft
r.Resolution = m.Resolution
r.Ratio = m.Ratio
r.Duration = m.Duration
r.Frames = m.Frames
r.Seed = m.Seed
r.CameraFixed = m.CameraFixed
r.Watermark = m.Watermark


return &r, nil
}
Expand All @@ -229,7 +260,7 @@ func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, e
case "pending", "queued":
taskResult.Status = model.TaskStatusQueued
taskResult.Progress = "10%"
case "processing":
case "processing", "running":
taskResult.Status = model.TaskStatusInProgress
taskResult.Progress = "50%"
case "succeeded":
Expand All @@ -251,3 +282,30 @@ func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, e

return &taskResult, nil
}

func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) ([]byte, error) {
var dResp responseTask
if err := json.Unmarshal(originTask.Data, &dResp); err != nil {
return nil, errors.Wrap(err, "unmarshal doubao task data failed")
}

openAIVideo := dto.NewOpenAIVideo()
openAIVideo.ID = originTask.TaskID
openAIVideo.TaskID = originTask.TaskID
openAIVideo.Status = originTask.Status.ToVideoStatus()
openAIVideo.SetProgressStr(originTask.Progress)
openAIVideo.SetMetadata("url", dResp.Content.VideoURL)
openAIVideo.CreatedAt = originTask.CreatedAt
openAIVideo.CompletedAt = originTask.UpdatedAt
openAIVideo.Model = originTask.Properties.OriginModelName

if dResp.Status == "failed" {
openAIVideo.Error = &dto.OpenAIVideoError{
Message: "task failed",
Code: "failed",
}
}

jsonData, _ := common.Marshal(openAIVideo)
return jsonData, nil
}
Comment on lines +286 to +311

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

🧩 Analysis chain

🏁 Script executed:

cd /tmp && find . -type f -name "*.go" | grep -E "(adaptor|task_video)" | head -20

Repository: QuantumNous/new-api

Length of output: 45


🏁 Script executed:

# First, let's locate the file and examine the actual code
fd -t f "adaptor.go" | grep doubao

Repository: QuantumNous/new-api

Length of output: 99


🏁 Script executed:

# Get the file structure and check the doubao adaptor
git ls-files | grep -E "(relay/channel|controller)" | head -30

Repository: QuantumNous/new-api

Length of output: 774


🏁 Script executed:

# Search for the doubao adaptor file
find . -name "adaptor.go" -path "*/doubao/*" 2>/dev/null

Repository: QuantumNous/new-api

Length of output: 101


🏁 Script executed:

# Read the doubao adaptor file to examine the ConvertToOpenAIVideo function
cat -n relay/channel/task/doubao/adaptor.go | head -320

Repository: QuantumNous/new-api

Length of output: 11781


🏁 Script executed:

# Search for task_video.go to understand the sanitization patterns
find . -name "task_video.go" 2>/dev/null

Repository: QuantumNous/new-api

Length of output: 89


🏁 Script executed:

# Check the model definitions for TaskStatus
rg "TaskStatus" --type go -A 3 | head -50

Repository: QuantumNous/new-api

Length of output: 2328


🏁 Script executed:

# Examine the task_video.go to understand sanitization patterns
cat -n controller/task_video.go | head -350

Repository: QuantumNous/new-api

Length of output: 12733


🏁 Script executed:

# Look for the redactVideoResponseBody function
rg "redactVideoResponseBody|bytesBase64Encoded" --type go -B 5 -A 10

Repository: QuantumNous/new-api

Length of output: 6983


🏁 Script executed:

# Check model/task.go to verify TaskStatus constants
cat -n model/task.go | grep -A 20 "type TaskStatus"

Repository: QuantumNous/new-api

Length of output: 711


🏁 Script executed:

# Check for other similar adaptor implementations to understand patterns
find relay/channel/task -name "adaptor.go" -type f | head -5

Repository: QuantumNous/new-api

Length of output: 244


🏁 Script executed:

# Look at another adaptor to see how they handle ConvertToOpenAIVideo
rg "ConvertToOpenAIVideo" --type go -A 20 | head -80

Repository: QuantumNous/new-api

Length of output: 5038


🏁 Script executed:

# Check the common.Marshal function to understand what errors it can return
rg "func Marshal" --type go -B 2 -A 8 | head -40

Repository: QuantumNous/new-api

Length of output: 423


🏁 Script executed:

# Verify the data: URL sanitization pattern in task_video.go more carefully
sed -n '143,155p' controller/task_video.go

Repository: QuantumNous/new-api

Length of output: 416


🏁 Script executed:

# Check if there are any other implementations that show how to properly handle CompletedAt
rg "CompletedAt = " relay/channel/task --type go -B 2 -A 2

Repository: QuantumNous/new-api

Length of output: 2751


🏁 Script executed:

# Look at the vertex adaptor more carefully to understand the data: URL pattern
sed -n '410,450p' relay/channel/task/vertex/adaptor.go

Repository: QuantumNous/new-api

Length of output: 110


🏁 Script executed:

# Check if doubao's dResp.Status could be inconsistent with originTask.Status
sed -n '248,283p' relay/channel/task/doubao/adaptor.go

Repository: QuantumNous/new-api

Length of output: 1202


ConvertToOpenAIVideo: don't ignore marshal errors; avoid setting CompletedAt for non-terminal states; use normalized task status instead of raw API response.

  • jsonData, _ := common.Marshal(...) (line 309) silently drops errors that can occur during marshaling.
  • CompletedAt = originTask.UpdatedAt (line 299) is set unconditionally, even for queued/running tasks. Should only set for terminal states (SUCCESS/FAILURE), following the pattern in gemini/adaptor.go.
  • if dResp.Status == "failed" (line 302) checks the raw upstream status instead of the normalized originTask.Status. Since ParseTaskResult already maps dResp.Status to internal TaskStatus, use originTask.Status for consistency.
Proposed fix
 openAIVideo.CreatedAt = originTask.CreatedAt
-openAIVideo.CompletedAt = originTask.UpdatedAt
+if originTask.Status == model.TaskStatusSuccess || originTask.Status == model.TaskStatusFailure {
+  openAIVideo.CompletedAt = originTask.UpdatedAt
+}
 openAIVideo.Model = originTask.Properties.OriginModelName

-if dResp.Status == "failed" {
+if originTask.Status == model.TaskStatusFailure {
   openAIVideo.Error = &dto.OpenAIVideoError{
     Message: "task failed",
     Code:    "failed",
   }
 }

-jsonData, _ := common.Marshal(openAIVideo)
-return jsonData, nil
+jsonData, err := common.Marshal(openAIVideo)
+if err != nil {
+  return nil, errors.Wrap(err, "marshal openai video failed")
+}
+return jsonData, nil
🤖 Prompt for AI Agents
In @relay/channel/task/doubao/adaptor.go around lines 286 - 311, In
TaskAdaptor.ConvertToOpenAIVideo, stop ignoring marshal errors, only set
CompletedAt for terminal task states, and use the normalized originTask.Status
when marking errors: check the internal originTask.Status (not dResp.Status) to
decide if the task failed and to determine terminal state (follow the same
terminal-status check used elsewhere, e.g., gemini/adaptor.go), set
openAIVideo.CompletedAt = originTask.UpdatedAt only when originTask.Status is a
terminal state (SUCCESS/FAILURE), and capture/return any error from
common.Marshal(openAIVideo) instead of discarding it.

1 change: 1 addition & 0 deletions relay/channel/task/doubao/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ var ModelList = []string{
"doubao-seedance-1-0-pro-250528",
"doubao-seedance-1-0-lite-t2v",
"doubao-seedance-1-0-lite-i2v",
"doubao-seedance-1-5-pro-251215",
}

var ChannelName = "doubao-video"
2 changes: 1 addition & 1 deletion relay/relay_adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ func GetTaskAdaptor(platform constant.TaskPlatform) channel.TaskAdaptor {
return &taskvertex.TaskAdaptor{}
case constant.ChannelTypeVidu:
return &taskVidu.TaskAdaptor{}
case constant.ChannelTypeDoubaoVideo:
case constant.ChannelTypeDoubaoVideo, constant.ChannelTypeVolcEngine:
return &taskdoubao.TaskAdaptor{}
Comment on lines +151 to 152

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Where is GetTaskAdaptor used (which routes/endpoints)?
echo "=== GetTaskAdaptor usage ==="
rg -n 'GetTaskAdaptor\s*\(' -S

# 2) How is platform/channel_type set for task/video flows?
echo -e "\n=== Channel type / Platform assignments ==="
rg -n 'Set\("channel_type"|GetInt\("channel_type"\)|Set\("platform"|GetString\("platform"\)' -S

# 3) Confirm whether VolcEngine task routes exist and what they expect.
echo -e "\n=== VolcEngine channel/API type references ==="
rg -n 'ChannelTypeVolcEngine|APITypeVolcEngine' -S

# 4) Check the full context of the relay_adaptor.go file around line 151
echo -e "\n=== relay_adaptor.go context (lines 140-160) ==="
sed -n '140,160p' relay/relay_adaptor.go

Repository: QuantumNous/new-api

Length of output: 3230


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check what "seedream" is and how it's handled
echo "=== Seedream references ==="
rg -n 'seedream' -i

# Find all task types/models associated with ChannelTypeVolcEngine
echo -e "\n=== VolcEngine channel context ==="
rg -B5 -A5 'ChannelTypeVolcEngine' relay/relay_adaptor.go controller/channel-test.go

# Check if taskdoubao handles seedream or only Doubao video
echo -e "\n=== Taskdoubao adaptor ==="
head -50 relay/channel/doubao/adaptor.go | grep -i 'seedream\|func\|adaptor' || echo "Checking adaptor structure..."

# Look for task type mappings to understand adaptor selection logic
echo -e "\n=== Task type to adaptor mapping ==="
rg -n 'GetTaskAdaptor.*case' relay/relay_adaptor.go -A30

Repository: QuantumNous/new-api

Length of output: 1968


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find doubao adaptor files
echo "=== Doubao adaptor files ==="
find . -type f -name "*doubao*" | grep -E '\.(go|ts|js)$'

# Check the complete relay_adaptor.go switch statement
echo -e "\n=== Full GetTaskAdaptor function (lines 132-165) ==="
sed -n '132,165p' relay/relay_adaptor.go

# Check what task adaptors are available
echo -e "\n=== Available task adaptors ==="
ls -la relay/channel/ | grep -E '^d'

# Search for how image generation tasks are handled
echo -e "\n=== Image generation task handling ==="
rg -n 'image.*generation|images/generations' -i

Repository: QuantumNous/new-api

Length of output: 91


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check relay_adaptor.go imports and full function
echo "=== relay_adaptor.go (first 180 lines) ==="
head -180 relay/relay_adaptor.go

# List all task channel adaptors
echo -e "\n=== relay/channel directory structure ==="
find relay/channel -maxdepth 1 -type d | sort

# Search for taskdoubao package/import definition
echo -e "\n=== taskdoubao references and imports ==="
rg -n 'taskdoubao\|import.*doubao' -B2 -A2

Repository: QuantumNous/new-api

Length of output: 6832


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if there's a volcengine task adaptor that should be used
echo "=== Looking for volcengine task adaptor ==="
find relay/channel -name "*volcengine*" -o -name "*volc*"

# Check what task types volcengine has in its constants
echo -e "\n=== VolcEngine models in constants ==="
cat relay/channel/volcengine/constants.go | head -30

Repository: QuantumNous/new-api

Length of output: 572


VolcEngine routes multiple task types—seedream (image) and seedance (video)—but this mapping misroutes them all to the Doubao video task adaptor.

VolcEngine's model list includes seedream (image generation) and seedance (video generation) in addition to Doubao chat models. Routing all ChannelTypeVolcEngine to taskdoubao.TaskAdaptor at line 151 will misroute image and video generation tasks to the wrong adaptor. Create a dedicated VolcEngine task adaptor or introduce a more specific channel type (e.g., ChannelTypeVolcEngineDoubaoChat) for the Doubao-only subset.

case constant.ChannelTypeSora, constant.ChannelTypeOpenAI:
return &tasksora.TaskAdaptor{}
Expand Down