Skip to content
Draft
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
10 changes: 7 additions & 3 deletions model/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,10 @@ func (t *Task) GetData(v any) error {
}

type Properties struct {
Input string `json:"input"`
UpstreamModelName string `json:"upstream_model_name,omitempty"`
OriginModelName string `json:"origin_model_name,omitempty"`
Input string `json:"input"`
UpstreamModelName string `json:"upstream_model_name,omitempty"`
OriginModelName string `json:"origin_model_name,omitempty"`
UpstreamRequestKey string `json:"upstream_request_key,omitempty"`
}

func (m *Properties) Scan(val interface{}) error {
Expand Down Expand Up @@ -188,6 +189,9 @@ func InitTask(platform constant.TaskPlatform, relayInfo *commonRelay.RelayInfo)
if relayInfo.OriginModelName != "" {
properties.OriginModelName = relayInfo.OriginModelName
}
if relayInfo.TaskRelayInfo != nil && relayInfo.TaskRelayInfo.UpstreamRequestKey != "" {
properties.UpstreamRequestKey = relayInfo.TaskRelayInfo.UpstreamRequestKey
}
}

// 使用预生成的公开 ID(如果有),否则新生成
Expand Down
16 changes: 16 additions & 0 deletions model/task_cas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"time"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
commonRelay "github.com/QuantumNous/new-api/relay/common"
"github.com/glebarez/sqlite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -99,6 +101,20 @@ func insertTask(t *testing.T, task *Task) {
require.NoError(t, DB.Create(task).Error)
}

func TestInitTaskPersistsUpstreamRequestKey(t *testing.T) {
task := InitTask(constant.TaskPlatform("jimeng"), &commonRelay.RelayInfo{
ChannelMeta: &commonRelay.ChannelMeta{
UpstreamModelName: "jimeng_v30",
},
TaskRelayInfo: &commonRelay.TaskRelayInfo{
UpstreamRequestKey: "jimeng_ti2v_v30_pro",
},
})

require.Equal(t, "jimeng_v30", task.Properties.UpstreamModelName)
require.Equal(t, "jimeng_ti2v_v30_pro", task.Properties.UpstreamRequestKey)
}

// ---------------------------------------------------------------------------
// Snapshot / Equal — pure logic tests (no DB)
// ---------------------------------------------------------------------------
Expand Down
55 changes: 37 additions & 18 deletions relay/channel/task/jimeng/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ type responseTask struct {

const (
// 即梦限制单个文件最大4.7MB https://www.volcengine.com/docs/85621/1747301
MaxFileSize int64 = 4*1024*1024 + 700*1024 // 4.7MB (4MB + 724KB)
MaxFileSize int64 = 4*1024*1024 + 700*1024 // 4.7MB (4MB + 724KB)
defaultJimengTaskReqKey = "jimeng_vgfm_t2v_l20"
)

// ============================
Expand Down Expand Up @@ -170,6 +171,9 @@ func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayIn
if err != nil {
return nil, errors.Wrap(err, "convert request payload failed")
}
if info.TaskRelayInfo != nil {
info.TaskRelayInfo.UpstreamRequestKey = body.ReqKey
}
data, err := common.Marshal(body)
if err != nil {
return nil, err
Expand Down Expand Up @@ -224,7 +228,7 @@ func (a *TaskAdaptor) FetchTask(baseUrl, key string, body map[string]any, proxy
uri = fmt.Sprintf("%s/jimeng/?Action=CVSync2AsyncGetResult&Version=2022-08-31", a.baseURL)
}
payload := map[string]string{
"req_key": "jimeng_vgfm_t2v_l20", // This is fixed value from doc: https://www.volcengine.com/docs/85621/1544774
"req_key": jimengFetchReqKey(body),
"task_id": taskID,
}
payloadBytes, err := common.Marshal(payload)
Expand Down Expand Up @@ -404,26 +408,41 @@ func (a *TaskAdaptor) convertToRequestPayload(req *relaycommon.TaskSubmitReq, in
return nil, errors.Wrap(err, "unmarshal metadata failed")
}

imageLen := lo.Max([]int{len(req.Images), len(r.BinaryDataBase64), len(r.ImageUrls)})
r.ReqKey = resolveJimengReqKey(r.ReqKey, imageLen)

return &r, nil
}

func resolveJimengReqKey(reqKey string, imageLen int) string {
// 即梦视频3.0 ReqKey转换
// https://www.volcengine.com/docs/85621/1792707
imageLen := lo.Max([]int{len(req.Images), len(r.BinaryDataBase64), len(r.ImageUrls)})
if strings.Contains(r.ReqKey, "jimeng_v30") {
if r.ReqKey == "jimeng_v30_pro" {
// 3.0 pro只有固定的jimeng_ti2v_v30_pro
r.ReqKey = "jimeng_ti2v_v30_pro"
} else if imageLen > 1 {
// 多张图片:首尾帧生成
r.ReqKey = strings.TrimSuffix(strings.Replace(r.ReqKey, "jimeng_v30", "jimeng_i2v_first_tail_v30", 1), "p")
} else if imageLen == 1 {
// 单张图片:图生视频
r.ReqKey = strings.TrimSuffix(strings.Replace(r.ReqKey, "jimeng_v30", "jimeng_i2v_first_v30", 1), "p")
} else {
// 无图片:文生视频
r.ReqKey = strings.Replace(r.ReqKey, "jimeng_v30", "jimeng_t2v_v30", 1)
}
if !strings.Contains(reqKey, "jimeng_v30") {
return reqKey
}
if reqKey == "jimeng_v30_pro" {
// 3.0 pro只有固定的jimeng_ti2v_v30_pro
return "jimeng_ti2v_v30_pro"
}
if imageLen > 1 {
// 多张图片:首尾帧生成
return strings.TrimSuffix(strings.Replace(reqKey, "jimeng_v30", "jimeng_i2v_first_tail_v30", 1), "p")
}
if imageLen == 1 {
// 单张图片:图生视频
return strings.TrimSuffix(strings.Replace(reqKey, "jimeng_v30", "jimeng_i2v_first_v30", 1), "p")
}
// 无图片:文生视频
return strings.Replace(reqKey, "jimeng_v30", "jimeng_t2v_v30", 1)
}

return &r, nil
func jimengFetchReqKey(body map[string]any) string {
for _, field := range []string{"upstream_request_key", "req_key"} {
if value, ok := body[field].(string); ok && strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return defaultJimengTaskReqKey
}

func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, error) {
Expand Down
202 changes: 202 additions & 0 deletions relay/channel/task/jimeng/adaptor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package jimeng

import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/QuantumNous/new-api/constant"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)

func TestResolveJimengReqKeyV30Variants(t *testing.T) {
tests := []struct {
name string
reqKey string
imageLen int
want string
}{
{
name: "v30 pro",
reqKey: "jimeng_v30_pro",
imageLen: 0,
want: "jimeng_ti2v_v30_pro",
},
{
name: "v30 text to video",
reqKey: "jimeng_v30",
imageLen: 0,
want: "jimeng_t2v_v30",
},
{
name: "v30 image to video",
reqKey: "jimeng_v30",
imageLen: 1,
want: "jimeng_i2v_first_v30",
},
{
name: "v30 first tail",
reqKey: "jimeng_v30",
imageLen: 2,
want: "jimeng_i2v_first_tail_v30",
},
{
name: "legacy model",
reqKey: defaultJimengTaskReqKey,
imageLen: 0,
want: defaultJimengTaskReqKey,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, resolveJimengReqKey(tt.reqKey, tt.imageLen))
})
}
}

func TestConvertToRequestPayloadStoresActualReqKey(t *testing.T) {
adaptor := &TaskAdaptor{}
info := &relaycommon.RelayInfo{
ChannelMeta: &relaycommon.ChannelMeta{
UpstreamModelName: "jimeng_v30",
},
TaskRelayInfo: &relaycommon.TaskRelayInfo{},
}
req := &relaycommon.TaskSubmitReq{
Prompt: "make a video",
Images: []string{"https://example.com/first.png", "https://example.com/last.png"},
}

payload, err := adaptor.convertToRequestPayload(req, info)

require.NoError(t, err)
require.Equal(t, "jimeng_i2v_first_tail_v30", payload.ReqKey)
}

func TestBuildRequestBodyRecordsActualReqKey(t *testing.T) {
gin.SetMode(gin.TestMode)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest(http.MethodPost, "/v1/video/generations", nil)
c.Set("task_request", relaycommon.TaskSubmitReq{
Prompt: "make a video",
Images: []string{"https://example.com/first.png", "https://example.com/last.png"},
})
info := &relaycommon.RelayInfo{
ChannelMeta: &relaycommon.ChannelMeta{
UpstreamModelName: "jimeng_v30",
},
TaskRelayInfo: &relaycommon.TaskRelayInfo{},
}

body, err := (&TaskAdaptor{}).BuildRequestBody(c, info)

require.NoError(t, err)
bodyBytes, err := io.ReadAll(body)
require.NoError(t, err)
require.Contains(t, string(bodyBytes), `"req_key":"jimeng_i2v_first_tail_v30"`)
require.Equal(t, "jimeng_i2v_first_tail_v30", info.TaskRelayInfo.UpstreamRequestKey)
}

func TestJimengFetchTaskUsesPersistedRequestKey(t *testing.T) {
var gotPayload map[string]string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/jimeng/", r.URL.Path)
require.Equal(t, "CVSync2AsyncGetResult", r.URL.Query().Get("Action"))
require.Equal(t, "Bearer sk-test", r.Header.Get("Authorization"))
require.NoError(t, json.NewDecoder(r.Body).Decode(&gotPayload))
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"code":10000}`))
}))
t.Cleanup(server.Close)

service.InitHttpClient()
adaptor := &TaskAdaptor{baseURL: server.URL}

resp, err := adaptor.FetchTask(server.URL, "sk-test", map[string]any{
"task_id": "upstream-task",
"upstream_model_name": "jimeng_v30_pro",
"upstream_request_key": "jimeng_ti2v_v30_pro",
}, "")

require.NoError(t, err)
require.NotNil(t, resp)
_ = resp.Body.Close()
require.Equal(t, map[string]string{
"task_id": "upstream-task",
"req_key": "jimeng_ti2v_v30_pro",
}, gotPayload)
}

func TestJimengFetchReqKeyKeepsLegacyDefault(t *testing.T) {
require.Equal(t, "metadata_req_key", jimengFetchReqKey(map[string]any{
"req_key": " metadata_req_key ",
}))
require.Equal(t, defaultJimengTaskReqKey, jimengFetchReqKey(map[string]any{
"upstream_model_name": "jimeng_v30_pro",
}))
require.Equal(t, defaultJimengTaskReqKey, jimengFetchReqKey(map[string]any{
"upstream_model_name": "jimeng_v30",
"action": constant.TaskActionFirstTailGenerate,
}))
require.Equal(t, defaultJimengTaskReqKey, jimengFetchReqKey(map[string]any{}))
}

func TestJimengFetchTaskRejectsMissingTaskID(t *testing.T) {
_, err := (&TaskAdaptor{}).FetchTask("https://example.com", "sk-test", map[string]any{}, "")
require.Error(t, err)
}

func TestSignedFetchTaskUsesLegacyDefaultReqKey(t *testing.T) {
var gotPayload map[string]string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.NoError(t, json.NewDecoder(r.Body).Decode(&gotPayload))
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"code":10000}`))
}))
t.Cleanup(server.Close)

service.InitHttpClient()
resp, err := (&TaskAdaptor{}).FetchTask(server.URL, "ak|sk", map[string]any{
"task_id": "upstream-task",
"upstream_model_name": "jimeng_v30_pro",
}, "")

require.NoError(t, err)
require.NotNil(t, resp)
_ = resp.Body.Close()
require.Equal(t, defaultJimengTaskReqKey, gotPayload["req_key"])
require.Equal(t, "upstream-task", gotPayload["task_id"])
}

func TestBuildRequestBodyKeepsMetadataReqKey(t *testing.T) {
gin.SetMode(gin.TestMode)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest(http.MethodPost, "/v1/video/generations", nil)
c.Set("task_request", relaycommon.TaskSubmitReq{
Prompt: "make a video",
Metadata: map[string]interface{}{
"req_key": "custom_req_key",
},
})
info := &relaycommon.RelayInfo{
ChannelMeta: &relaycommon.ChannelMeta{
UpstreamModelName: "jimeng_v30",
},
TaskRelayInfo: &relaycommon.TaskRelayInfo{},
}

body, err := (&TaskAdaptor{}).BuildRequestBody(c, info)

require.NoError(t, err)
bodyBytes, err := io.ReadAll(body)
require.NoError(t, err)
require.True(t, bytes.Contains(bodyBytes, []byte(`"req_key":"custom_req_key"`)))
require.Equal(t, "custom_req_key", info.TaskRelayInfo.UpstreamRequestKey)
}
3 changes: 3 additions & 0 deletions relay/common/relay_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,9 @@ type TaskRelayInfo struct {
// PublicTaskID 是提交时预生成的 task_xxxx 格式公开 ID,
// 供 DoResponse 在返回给客户端时使用(避免暴露上游真实 ID)。
PublicTaskID string
// UpstreamRequestKey stores provider-specific request keys that are needed
// to poll an async task after the public task has been persisted.
UpstreamRequestKey string

ConsumeQuota bool

Expand Down
22 changes: 18 additions & 4 deletions service/task_polling.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,23 @@ type TaskPollingAdaptor interface {
// 打破 service -> relay -> relay/channel -> service 的循环依赖。
var GetTaskAdaptorFunc func(platform constant.TaskPlatform) TaskPollingAdaptor

func BuildTaskFetchBody(task *model.Task) map[string]any {
body := map[string]any{
"task_id": task.GetUpstreamTaskID(),
"action": task.Action,
}
if task.Properties.UpstreamModelName != "" {
body["upstream_model_name"] = task.Properties.UpstreamModelName
}
if task.Properties.OriginModelName != "" {
body["origin_model_name"] = task.Properties.OriginModelName
}
if task.Properties.UpstreamRequestKey != "" {
body["upstream_request_key"] = task.Properties.UpstreamRequestKey
}
return body
}

// sweepTimedOutTasks 在主轮询之前独立清理超时任务。
// 每次最多处理 100 条,剩余的下个周期继续处理。
// 使用 per-task CAS (UpdateWithStatus) 防止覆盖被正常轮询已推进的任务。
Expand Down Expand Up @@ -459,10 +476,7 @@ func updateVideoSingleTask(ctx context.Context, adaptor TaskPollingAdaptor, ch *
if privateData.Key != "" {
key = privateData.Key
}
resp, err := adaptor.FetchTask(baseURL, key, map[string]any{
"task_id": task.GetUpstreamTaskID(),
"action": task.Action,
}, proxy)
resp, err := adaptor.FetchTask(baseURL, key, BuildTaskFetchBody(task), proxy)
if err != nil {
return fmt.Errorf("fetchTask failed for task %s: %w", taskId, err)
}
Expand Down
Loading