package chinamobile_seedance import ( "bytes" "fmt" "io" "net/http" "strconv" "strings" "time" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" taskcommon "github.com/QuantumNous/new-api/relay/channel/task/taskcommon" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/service" relaytypes "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/pkg/errors" ) type responsePayload struct { ID string `json:"id"` } type upstreamError struct { Code string `json:"code"` Message string `json:"message"` } func (e *upstreamError) UnmarshalJSON(data []byte) error { if len(bytes.TrimSpace(data)) == 0 || string(bytes.TrimSpace(data)) == "null" { return nil } var message string if err := common.Unmarshal(data, &message); err == nil { e.Message = message return nil } type alias upstreamError var parsed alias if err := common.Unmarshal(data, &parsed); err != nil { return err } *e = upstreamError(parsed) return nil } type responseTask struct { ID string `json:"id"` Model string `json:"model"` Status string `json:"status"` Content struct { VideoURL string `json:"video_url"` } `json:"content"` Usage struct { CompletionTokens int `json:"completion_tokens"` TotalTokens int `json:"total_tokens"` } `json:"usage"` Error upstreamError `json:"error"` Message string `json:"message"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } type TaskAdaptor struct { taskcommon.BaseBilling ChannelType int apiKey string baseURL string newSDK seedanceSDKFactory submitTimeout time.Duration queryTimeout time.Duration } func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) { if info != nil && info.ChannelMeta != nil { a.ChannelType = info.ChannelType a.baseURL = strings.TrimRight(info.ChannelBaseUrl, "/") a.apiKey = info.ApiKey } if a.newSDK == nil { a.newSDK = newRealSDK } if a.submitTimeout == 0 { a.submitTimeout = defaultSubmitTimeout } if a.queryTimeout == 0 { a.queryTimeout = defaultQueryTimeout } } func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) { if _, err := relaycommon.GetTaskRequest(c); err == nil { info.Action = constant.TaskActionGenerate return nil } return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionGenerate) } func (a *TaskAdaptor) BuildRequestURL(info *relaycommon.RelayInfo) (string, error) { baseURL := a.baseURL if strings.TrimSpace(baseURL) == "" && info != nil { baseURL = strings.TrimRight(info.ChannelBaseUrl, "/") } if strings.TrimSpace(baseURL) == "" { baseURL = defaultBaseURL } return fmt.Sprintf("%s/contents/generations/tasks", strings.TrimRight(baseURL, "/")), nil } func (a *TaskAdaptor) BuildRequestHeader(c *gin.Context, req *http.Request, info *relaycommon.RelayInfo) error { req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") return nil } func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayInfo) (io.Reader, error) { req, err := relaycommon.GetTaskRequest(c) if err != nil { return nil, err } body, err := a.convertToRequestPayload(&req) if err != nil { return nil, errors.Wrap(err, "convert request payload failed") } if info != nil { if info.IsModelMapped { body["model"] = info.UpstreamModelName } else if modelName, _ := body["model"].(string); modelName != "" { info.UpstreamModelName = modelName } } data, err := common.Marshal(body) if err != nil { return nil, err } return bytes.NewReader(data), nil } func (a *TaskAdaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (*http.Response, error) { data, err := io.ReadAll(requestBody) if err != nil { return nil, err } payload := map[string]interface{}{} if err := common.Unmarshal(data, &payload); err != nil { return nil, err } modelName := chinaMobileSeedanceModel(modelFromPayloadOrInfo(payload, info)) payload["model"] = modelName if info != nil { info.UpstreamModelName = modelName } client, err := a.createSDK(info, modelName) if err != nil { return nil, err } submitTimeout := a.submitTimeout if submitTimeout == 0 { submitTimeout = defaultSubmitTimeout } taskID, err := runSDKCall[string](submitTimeout, func() (string, error) { return client.CreateVideoGenerationTask(payload) }) if err != nil { return chinaMobileSeedanceErrorResponse(err), nil } respBody, err := common.Marshal(map[string]any{"id": taskID}) if err != nil { return nil, err } return &http.Response{ StatusCode: http.StatusOK, Header: make(http.Header), Body: io.NopCloser(bytes.NewReader(respBody)), }, nil } func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *dto.TaskError) { responseBody, err := io.ReadAll(resp.Body) if err != nil { return "", nil, service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) } _ = resp.Body.Close() var dResp responsePayload if err := common.Unmarshal(responseBody, &dResp); err != nil { return "", nil, service.TaskErrorWrapper(errors.Wrapf(err, "body: %s", responseBody), "unmarshal_response_body_failed", http.StatusInternalServerError) } if strings.TrimSpace(dResp.ID) == "" { return "", nil, service.TaskErrorWrapper(fmt.Errorf("task_id is empty"), "invalid_response", http.StatusInternalServerError) } clientPayload := map[string]any{} if err := common.Unmarshal(responseBody, &clientPayload); err != nil { return "", nil, service.TaskErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) } clientPayload = relaytypes.CloneMapAny(clientPayload) if info.PublicTaskID != "" { clientPayload["id"] = info.PublicTaskID } if _, ok := clientPayload["created_at"]; !ok { clientPayload["created_at"] = time.Now().Unix() } if _, ok := clientPayload["model"]; !ok { clientPayload["model"] = info.OriginModelName } c.JSON(http.StatusOK, clientPayload) return dResp.ID, responseBody, nil } func (a *TaskAdaptor) FetchTask(baseUrl, key string, body map[string]any, proxy string) (*http.Response, error) { taskID, _ := body["task_id"].(string) if strings.TrimSpace(taskID) == "" { return nil, fmt.Errorf("invalid task_id") } modelName, _ := body["model"].(string) client, err := a.createSDKFromValues(baseUrl, key, modelName) if err != nil { return nil, err } queryTimeout := a.queryTimeout if queryTimeout == 0 { queryTimeout = defaultQueryTimeout } result, err := runSDKCall[map[string]interface{}](queryTimeout, func() (map[string]interface{}, error) { return client.QueryVideoGenerationTask(taskID) }) if err != nil { return nil, err } respBody, err := common.Marshal(result) if err != nil { return nil, err } return &http.Response{ StatusCode: http.StatusOK, Header: make(http.Header), Body: io.NopCloser(bytes.NewReader(respBody)), }, nil } func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, error) { resTask := responseTask{} if err := common.Unmarshal(respBody, &resTask); err != nil { return nil, errors.Wrap(err, "unmarshal task result failed") } taskResult := relaycommon.TaskInfo{Code: 0} switch strings.ToLower(resTask.Status) { case "pending", "queued": taskResult.Status = model.TaskStatusQueued taskResult.Progress = "10%" case "processing", "running": taskResult.Status = model.TaskStatusInProgress taskResult.Progress = "50%" case "succeeded", "success": taskResult.Status = model.TaskStatusSuccess taskResult.Progress = "100%" taskResult.Url = resTask.Content.VideoURL taskResult.CompletionTokens = resTask.Usage.CompletionTokens taskResult.TotalTokens = resTask.Usage.TotalTokens case "failed", "expired", "cancelled": taskResult.Status = model.TaskStatusFailure taskResult.Progress = "100%" taskResult.Reason = upstreamReason(resTask) default: if resTask.Error.Message != "" || resTask.Message != "" { taskResult.Status = model.TaskStatusFailure taskResult.Progress = "100%" taskResult.Reason = upstreamReason(resTask) } else { taskResult.Status = model.TaskStatusInProgress taskResult.Progress = "30%" } } return &taskResult, nil } func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) ([]byte, error) { var dResp responseTask if err := common.Unmarshal(originTask.Data, &dResp); err != nil { return nil, errors.Wrap(err, "unmarshal chinamobile seedance 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 originTask.Status == model.TaskStatusFailure || dResp.Status == "failed" { message := upstreamReason(dResp) if message == "" { message = "task failed" } code := dResp.Error.Code if code == "" { code = "failed" } openAIVideo.Error = &dto.OpenAIVideoError{Message: message, Code: code} } return common.Marshal(openAIVideo) } func (a *TaskAdaptor) GetModelList() []string { return ModelList } func (a *TaskAdaptor) GetChannelName() string { return ChannelName } func (a *TaskAdaptor) createSDK(info *relaycommon.RelayInfo, modelName string) (seedanceSDK, error) { baseURL := a.baseURL apiKey := a.apiKey if info != nil && info.ChannelMeta != nil { baseURL = info.ChannelBaseUrl apiKey = info.ApiKey } return a.createSDKFromValues(baseURL, apiKey, modelName) } func (a *TaskAdaptor) createSDKFromValues(baseURL, apiKey, modelName string) (seedanceSDK, error) { factory := a.newSDK if factory == nil { factory = newRealSDK } return factory(strings.TrimRight(baseURL, "/"), apiKey, modelName) } func (a *TaskAdaptor) convertToRequestPayload(req *relaycommon.TaskSubmitReq) (map[string]interface{}, error) { payload := map[string]interface{}{ "model": req.Model, } for k, v := range req.Metadata { payload[k] = v } if sec, _ := strconv.Atoi(req.Seconds); sec > 0 { if _, ok := payload["duration"]; !ok { payload["duration"] = sec } } content, err := normalizeContent(payload["content"]) if err != nil { return nil, err } if len(content) == 0 && req.HasImage() { for _, imgURL := range req.Images { content = append(content, map[string]interface{}{ "type": "image_url", "image_url": map[string]interface{}{"url": imgURL}, }) } } if strings.TrimSpace(req.Prompt) != "" && !hasTextContent(content) { content = append(content, map[string]interface{}{ "type": "text", "text": req.Prompt, }) } if len(content) > 0 { payload["content"] = content } return payload, nil } func normalizeContent(input any) ([]interface{}, error) { if input == nil { return nil, nil } data, err := common.Marshal(input) if err != nil { return nil, err } var content []interface{} if err := common.Unmarshal(data, &content); err != nil { return nil, err } return content, nil } func hasTextContent(content []interface{}) bool { for _, item := range content { m, ok := item.(map[string]interface{}) if !ok { continue } if m["type"] == "text" && strings.TrimSpace(fmt.Sprint(m["text"])) != "" { return true } } return false } func modelFromPayloadOrInfo(payload map[string]interface{}, info *relaycommon.RelayInfo) string { if modelName, _ := payload["model"].(string); strings.TrimSpace(modelName) != "" { return modelName } if info != nil { if strings.TrimSpace(info.UpstreamModelName) != "" { return info.UpstreamModelName } return info.OriginModelName } return defaultModel } func chinaMobileSeedanceModel(modelName string) string { switch strings.TrimSpace(modelName) { case "", "doubao-seedance-2-0-260128", "doubao-seedance-2-0-fast-260128": return defaultModel default: return modelName } } type chinaMobileSeedanceUpstreamError struct { ErrorCode string `json:"ErrorCode"` ErrorMessage string `json:"ErrorMessage"` } func chinaMobileSeedanceErrorResponse(err error) *http.Response { statusCode := http.StatusBadGateway body := map[string]any{ "code": "upstream_error", "message": err.Error(), } if upstreamErr, ok := parseChinaMobileSeedanceError(err); ok { statusCode = chinaMobileSeedanceStatusCode(upstreamErr.ErrorCode) body["code"] = upstreamErr.ErrorCode body["message"] = upstreamErr.ErrorMessage } respBody, marshalErr := common.Marshal(body) if marshalErr != nil { respBody = []byte(`{"code":"upstream_error","message":"upstream error"}`) } return &http.Response{ StatusCode: statusCode, Header: make(http.Header), Body: io.NopCloser(bytes.NewReader(respBody)), } } func parseChinaMobileSeedanceError(err error) (chinaMobileSeedanceUpstreamError, bool) { if err == nil { return chinaMobileSeedanceUpstreamError{}, false } text := err.Error() start := strings.Index(text, "{") end := strings.LastIndex(text, "}") if start < 0 || end < start { return chinaMobileSeedanceUpstreamError{}, false } var upstreamErr chinaMobileSeedanceUpstreamError if err := common.Unmarshal([]byte(text[start:end+1]), &upstreamErr); err != nil { return chinaMobileSeedanceUpstreamError{}, false } return upstreamErr, upstreamErr.ErrorCode != "" || upstreamErr.ErrorMessage != "" } func chinaMobileSeedanceStatusCode(code string) int { upperCode := strings.ToUpper(strings.TrimSpace(code)) switch { case strings.Contains(upperCode, "PERMISSION") || strings.Contains(upperCode, "UNAUTHORIZED"): return http.StatusForbidden case strings.Contains(upperCode, "SENSITIVE") || strings.Contains(upperCode, "INVALID") || strings.Contains(upperCode, "BAD"): return http.StatusBadRequest case strings.Contains(upperCode, "RATE") || strings.Contains(upperCode, "LIMIT"): return http.StatusTooManyRequests default: return http.StatusBadGateway } } func upstreamReason(resTask responseTask) string { if resTask.Error.Message != "" { return resTask.Error.Message } if resTask.Message != "" { return resTask.Message } if resTask.Status != "" { return "task " + resTask.Status } return "upstream error" }