From 35422b316da2919749246c6cad711c8eb39086fe Mon Sep 17 00:00:00 2001 From: feitianbubu Date: Fri, 10 Oct 2025 23:27:12 +0800 Subject: [PATCH 1/3] refactor: openAI video use OpenAIVideoConverter --- relay/channel/adapter.go | 5 +++++ relay/channel/task/sora/adaptor.go | 9 +++++++++ relay/common/relay_info.go | 19 +++++++++++++++++++ relay/relay_task.go | 17 ++++++++++++++++- 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/relay/channel/adapter.go b/relay/channel/adapter.go index 02de99567..964c33256 100644 --- a/relay/channel/adapter.go +++ b/relay/channel/adapter.go @@ -4,6 +4,7 @@ import ( "io" "net/http" "one-api/dto" + "one-api/model" relaycommon "one-api/relay/common" "one-api/types" @@ -49,3 +50,7 @@ type TaskAdaptor interface { ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, error) } + +type OpenAIVideoConverter interface { + ConvertToOpenAIVideo(originTask *model.Task) (*relaycommon.OpenAIVideo, error) +} diff --git a/relay/channel/task/sora/adaptor.go b/relay/channel/task/sora/adaptor.go index db9d9c3bf..3ceed42d8 100644 --- a/relay/channel/task/sora/adaptor.go +++ b/relay/channel/task/sora/adaptor.go @@ -184,3 +184,12 @@ func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, e return &taskResult, nil } + +func (a *TaskAdaptor) ConvertToOpenAIVideo(task *model.Task) (*relaycommon.OpenAIVideo, error) { + openAIVideo := &relaycommon.OpenAIVideo{} + err := json.Unmarshal(task.Data, openAIVideo) + if err != nil { + return nil, errors.Wrap(err, "unmarshal to OpenAIVideo failed") + } + return openAIVideo, nil +} diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index 3fc1507b2..7939b48dd 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -550,3 +550,22 @@ func RemoveDisabledFields(jsonData []byte, channelOtherSettings dto.ChannelOther } return jsonDataAfter, nil } + +type OpenAIVideo struct { + ID string `json:"id"` + TaskID string `json:"task_id,omitempty"` //兼容旧接口 + Object string `json:"object"` + Model string `json:"model"` + Status string `json:"status"` + Progress int `json:"progress"` + CreatedAt int64 `json:"created_at"` + CompletedAt int64 `json:"completed_at,omitempty"` + ExpiresAt int64 `json:"expires_at,omitempty"` + Seconds string `json:"seconds,omitempty"` + Size string `json:"size,omitempty"` + RemixedFromVideoID string `json:"remixed_from_video_id,omitempty"` + Error *struct { + Message string `json:"message"` + Code string `json:"code"` + } `json:"error,omitempty"` +} diff --git a/relay/relay_task.go b/relay/relay_task.go index d447a40aa..0c4e9604c 100644 --- a/relay/relay_task.go +++ b/relay/relay_task.go @@ -11,6 +11,7 @@ import ( "one-api/constant" "one-api/dto" "one-api/model" + "one-api/relay/channel" relaycommon "one-api/relay/common" relayconstant "one-api/relay/constant" "one-api/service" @@ -367,7 +368,21 @@ func videoFetchByIDRespBodyBuilder(c *gin.Context) (respBody []byte, taskResp *d } if strings.HasPrefix(c.Request.RequestURI, "/v1/videos/") { - respBody = originTask.Data + adaptor := GetTaskAdaptor(originTask.Platform) + if adaptor == nil { + taskResp = service.TaskErrorWrapperLocal(fmt.Errorf("invalid channel id: %d", originTask.ChannelId), "invalid_channel_id", http.StatusBadRequest) + return + } + if converter, ok := adaptor.(channel.OpenAIVideoConverter); ok { + openAIVideo, err := converter.ConvertToOpenAIVideo(originTask) + if err != nil { + taskResp = service.TaskErrorWrapper(err, "convert_to_openai_video_failed", http.StatusInternalServerError) + return + } + respBody, _ = json.Marshal(openAIVideo) + return + } + taskResp = service.TaskErrorWrapperLocal(errors.New(fmt.Sprintf("not_implemented:%s", originTask.Platform)), "not_implemented", http.StatusNotImplemented) return } respBody, err = json.Marshal(dto.TaskResponse[any]{ From 11e8e4e7a6ec2770151b9188f49fa0862e7fe3bb Mon Sep 17 00:00:00 2001 From: feitianbubu Date: Sat, 11 Oct 2025 00:05:56 +0800 Subject: [PATCH 2/3] feat: add openai sdk for kling --- relay/channel/task/kling/adaptor.go | 57 +++++++++++++++++++++++++---- relay/common/relay_info.go | 34 +++++++++-------- 2 files changed, 67 insertions(+), 24 deletions(-) diff --git a/relay/channel/task/kling/adaptor.go b/relay/channel/task/kling/adaptor.go index e91562aca..4befd8e2e 100644 --- a/relay/channel/task/kling/adaptor.go +++ b/relay/channel/task/kling/adaptor.go @@ -7,9 +7,11 @@ import ( "io" "net/http" "one-api/model" + "strconv" "strings" "time" + "github.com/bytedance/gopkg/util/logger" "github.com/samber/lo" "github.com/gin-gonic/gin" @@ -303,14 +305,6 @@ func (a *TaskAdaptor) createJWTToken() (string, error) { return a.createJWTTokenWithKey(a.apiKey) } -//func (a *TaskAdaptor) createJWTTokenWithKey(apiKey string) (string, error) { -// parts := strings.Split(apiKey, "|") -// if len(parts) != 2 { -// return "", fmt.Errorf("invalid API key format, expected 'access_key,secret_key'") -// } -// return a.createJWTTokenWithKey(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])) -//} - func (a *TaskAdaptor) createJWTTokenWithKey(apiKey string) (string, error) { if isNewAPIRelay(apiKey) { return apiKey, nil // new api relay @@ -369,3 +363,50 @@ func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, e func isNewAPIRelay(apiKey string) bool { return strings.HasPrefix(apiKey, "sk-") } + +func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) (*relaycommon.OpenAIVideo, error) { + var klingResp responsePayload + if err := json.Unmarshal(originTask.Data, &klingResp); err != nil { + return nil, errors.Wrap(err, "unmarshal kling task data failed") + } + + convertProgress := func(progress string) int { + progress = strings.TrimSuffix(progress, "%") + p, err := strconv.Atoi(progress) + if err != nil { + logger.Warnf("convert progress failed, progress: %s, err: %v", progress, err) + } + return p + } + + openAIVideo := &relaycommon.OpenAIVideo{ + ID: klingResp.Data.TaskId, + Object: "video", + //Model: "kling-v1", //todo save model + Status: string(originTask.Status), + CreatedAt: klingResp.Data.CreatedAt, + CompletedAt: klingResp.Data.UpdatedAt, + Metadata: make(map[string]any), + Progress: convertProgress(originTask.Progress), + } + + // 处理视频 URL + if len(klingResp.Data.TaskResult.Videos) > 0 { + video := klingResp.Data.TaskResult.Videos[0] + if video.Url != "" { + openAIVideo.Metadata["url"] = video.Url + } + if video.Duration != "" { + openAIVideo.Seconds = video.Duration + } + } + + if klingResp.Code != 0 && klingResp.Message != "" { + openAIVideo.Error = &relaycommon.OpenAIVideoError{ + Message: klingResp.Message, + Code: fmt.Sprintf("%d", klingResp.Code), + } + } + + return openAIVideo, nil +} diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index 7939b48dd..cb3a6709f 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -552,20 +552,22 @@ func RemoveDisabledFields(jsonData []byte, channelOtherSettings dto.ChannelOther } type OpenAIVideo struct { - ID string `json:"id"` - TaskID string `json:"task_id,omitempty"` //兼容旧接口 - Object string `json:"object"` - Model string `json:"model"` - Status string `json:"status"` - Progress int `json:"progress"` - CreatedAt int64 `json:"created_at"` - CompletedAt int64 `json:"completed_at,omitempty"` - ExpiresAt int64 `json:"expires_at,omitempty"` - Seconds string `json:"seconds,omitempty"` - Size string `json:"size,omitempty"` - RemixedFromVideoID string `json:"remixed_from_video_id,omitempty"` - Error *struct { - Message string `json:"message"` - Code string `json:"code"` - } `json:"error,omitempty"` + ID string `json:"id"` + TaskID string `json:"task_id,omitempty"` //兼容旧接口 待废弃 + Object string `json:"object"` + Model string `json:"model"` + Status string `json:"status"` + Progress int `json:"progress"` + CreatedAt int64 `json:"created_at"` + CompletedAt int64 `json:"completed_at,omitempty"` + ExpiresAt int64 `json:"expires_at,omitempty"` + Seconds string `json:"seconds,omitempty"` + Size string `json:"size,omitempty"` + RemixedFromVideoID string `json:"remixed_from_video_id,omitempty"` + Error *OpenAIVideoError `json:"error,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` +} +type OpenAIVideoError struct { + Message string `json:"message"` + Code string `json:"code"` } From 5f36e32821664345f2964143983d3a3806eb37d0 Mon Sep 17 00:00:00 2001 From: feitianbubu Date: Sat, 11 Oct 2025 02:30:26 +0800 Subject: [PATCH 3/3] feat: add openai sdk create --- middleware/distributor.go | 24 +++++++++++------ relay/common/relay_utils.go | 54 ++++++++++++++++++++++++++++--------- 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/middleware/distributor.go b/middleware/distributor.go index 525270b1a..a33ca5af9 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -174,14 +174,22 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) { relayMode := relayconstant.RelayModeUnknown if c.Request.Method == http.MethodPost { relayMode = relayconstant.RelayModeVideoSubmit - form, err := common.ParseMultipartFormReusable(c) - if err != nil { - return nil, false, errors.New("无效的video请求, " + err.Error()) - } - defer form.RemoveAll() - if form != nil { - if values, ok := form.Value["model"]; ok && len(values) > 0 { - modelRequest.Model = values[0] + contentType := c.Request.Header.Get("Content-Type") + if strings.HasPrefix(contentType, "multipart/form-data") { + form, err := common.ParseMultipartFormReusable(c) + if err != nil { + return nil, false, errors.New("无效的video请求, " + err.Error()) + } + defer form.RemoveAll() + if form != nil { + if values, ok := form.Value["model"]; ok && len(values) > 0 { + modelRequest.Model = values[0] + } + } + } else if strings.HasPrefix(contentType, "application/json") { + err = common.UnmarshalBodyReusable(c, &modelRequest) + if err != nil { + return nil, false, errors.New("无效的video请求, " + err.Error()) } } } else if c.Request.Method == http.MethodGet { diff --git a/relay/common/relay_utils.go b/relay/common/relay_utils.go index f18c43741..f76011495 100644 --- a/relay/common/relay_utils.go +++ b/relay/common/relay_utils.go @@ -106,25 +106,53 @@ func validateMultipartTaskRequest(c *gin.Context, info *RelayInfo, action string } func ValidateMultipartDirect(c *gin.Context, info *RelayInfo) *dto.TaskError { - form, err := common.ParseMultipartFormReusable(c) - if err != nil { - return createTaskError(err, "invalid_multipart_form", http.StatusBadRequest, true) - } - defer form.RemoveAll() + contentType := c.GetHeader("Content-Type") + var prompt string + var hasInputReference bool - prompts, ok := form.Value["prompt"] - if !ok || len(prompts) == 0 { - return createTaskError(fmt.Errorf("prompt field is required"), "missing_prompt", http.StatusBadRequest, true) + if strings.HasPrefix(contentType, "multipart/form-data") { + form, err := common.ParseMultipartFormReusable(c) + if err != nil { + return createTaskError(err, "invalid_multipart_form", http.StatusBadRequest, true) + } + defer form.RemoveAll() + + prompts, ok := form.Value["prompt"] + if !ok || len(prompts) == 0 { + return createTaskError(fmt.Errorf("prompt field is required"), "missing_prompt", http.StatusBadRequest, true) + } + prompt = prompts[0] + + if _, ok := form.Value["model"]; !ok { + return createTaskError(fmt.Errorf("model field is required"), "missing_model", http.StatusBadRequest, true) + } + + if _, ok := form.File["input_reference"]; ok { + hasInputReference = true + } + } else { + var req TaskSubmitReq + if err := common.UnmarshalBodyReusable(c, &req); err != nil { + return createTaskError(err, "invalid_json", http.StatusBadRequest, true) + } + + prompt = req.Prompt + + if strings.TrimSpace(req.Model) == "" { + return createTaskError(fmt.Errorf("model field is required"), "missing_model", http.StatusBadRequest, true) + } + + if req.HasImage() { + hasInputReference = true + } } - if taskErr := validatePrompt(prompts[0]); taskErr != nil { + + if taskErr := validatePrompt(prompt); taskErr != nil { return taskErr } - if _, ok := form.Value["model"]; !ok { - return createTaskError(fmt.Errorf("model field is required"), "missing_model", http.StatusBadRequest, true) - } action := constant.TaskActionTextGenerate - if _, ok := form.File["input_reference"]; ok { + if hasInputReference { action = constant.TaskActionGenerate } info.Action = action