|
- package aiping
-
- import (
- "bytes"
- "fmt"
- "io"
- "net/http"
- "strings"
- "time"
-
- "github.com/QuantumNous/new-api/common"
- "github.com/QuantumNous/new-api/dto"
- "github.com/QuantumNous/new-api/model"
- "github.com/QuantumNous/new-api/relay/channel"
- taskcommon "github.com/QuantumNous/new-api/relay/channel/task/taskcommon"
- relaycommon "github.com/QuantumNous/new-api/relay/common"
- "github.com/QuantumNous/new-api/service"
- "github.com/gin-gonic/gin"
- "github.com/pkg/errors"
- "github.com/shopspring/decimal"
- )
-
- type responsePayload struct {
- Code int `json:"code"`
- Message string `json:"message"`
- RequestID string `json:"request_id"`
- AipingID string `json:"aiping_id"`
- Data struct {
- TaskID string `json:"task_id"`
- TaskStatus string `json:"task_status"`
- TaskStatusMsg string `json:"task_status_msg"`
- TaskInfo any `json:"task_info,omitempty"`
- TaskResult struct {
- Videos []struct {
- ID string `json:"id"`
- URL string `json:"url"`
- WatermarkURL string `json:"watermark_url,omitempty"`
- Duration string `json:"duration"`
- } `json:"videos"`
- } `json:"task_result"`
- FinalUnitDeduction any `json:"final_unit_deduction"`
- CreatedAt int64 `json:"created_at"`
- UpdatedAt int64 `json:"updated_at"`
- } `json:"data"`
- }
-
- type TaskAdaptor struct {
- taskcommon.BaseBilling
- apiKey string
- baseURL string
- }
-
- func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) {
- a.apiKey = info.ApiKey
- a.baseURL = strings.TrimRight(info.ChannelBaseUrl, "/")
- }
-
- func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) *dto.TaskError {
- req, err := relaycommon.GetTaskRequest(c)
- if err != nil {
- return service.TaskErrorWrapperLocal(fmt.Errorf("task request not prepared"), "invalid_request", http.StatusBadRequest)
- }
- modelName := resolveRequestModelName(req, info)
- if isOmniModel(modelName) && (info.Action == ActionText2Video || info.Action == ActionImage2Video) {
- return service.TaskErrorWrapperLocal(
- fmt.Errorf("model %s must use /v1/videos/omni-video", modelName),
- "invalid_model_route",
- http.StatusUnprocessableEntity,
- )
- }
- return nil
- }
-
- func (a *TaskAdaptor) BuildRequestURL(info *relaycommon.RelayInfo) (string, error) {
- route, ok := FindRouteByAction(info.Action)
- if !ok {
- return "", fmt.Errorf("unsupported kling aiping action: %s", info.Action)
- }
- return a.baseURL + route.UpstreamPath, 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")
- req.Header.Set("Authorization", "Bearer "+a.apiKey)
- 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 := cloneMap(req.Metadata)
- normalizeRequestBody(body, info)
- 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) {
- return channel.DoTaskApiRequest(a, c, info, requestBody)
- }
-
- func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (string, []byte, *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 parsed responsePayload
- if err := common.Unmarshal(responseBody, &parsed); err != nil {
- return "", nil, service.TaskErrorWrapper(errors.Wrapf(err, "body: %s", responseBody), "unmarshal_response_body_failed", http.StatusInternalServerError)
- }
- if parsed.Code != 0 {
- msg := parsed.Message
- if msg == "" {
- msg = "upstream task failed"
- }
- return "", nil, service.TaskErrorWrapper(fmt.Errorf("%s", msg), "task_failed", http.StatusBadRequest)
- }
- upstreamTaskID := parsed.Data.TaskID
- if strings.TrimSpace(upstreamTaskID) == "" {
- 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)
- }
- sanitizeNativeTaskPayload(clientPayload, info.PublicTaskID)
- c.JSON(http.StatusOK, clientPayload)
- return upstreamTaskID, 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) == "" {
- taskID, _ = body["upstream_task_id"].(string)
- }
- action, _ := body["action"].(string)
- if strings.TrimSpace(taskID) == "" {
- return nil, fmt.Errorf("invalid task_id")
- }
- route, ok := FindFetchRouteByAction(action)
- if !ok {
- return nil, fmt.Errorf("unsupported kling aiping action: %s", action)
- }
- url := strings.TrimRight(baseURL, "/") + strings.Replace(route.UpstreamPath, ":task_id", taskID, 1)
- req, err := http.NewRequest(http.MethodGet, url, nil)
- if err != nil {
- return nil, err
- }
- req.Header.Set("Accept", "application/json")
- req.Header.Set("Authorization", "Bearer "+key)
- client, err := service.GetHttpClientWithProxy(proxy)
- if err != nil {
- return nil, fmt.Errorf("new proxy http client failed: %w", err)
- }
- return client.Do(req)
- }
-
- func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, error) {
- var parsed responsePayload
- if err := common.Unmarshal(respBody, &parsed); err != nil {
- return nil, errors.Wrap(err, "unmarshal kling aiping task data failed")
- }
- info := &relaycommon.TaskInfo{
- Code: parsed.Code,
- TaskID: parsed.Data.TaskID,
- Reason: parsed.Data.TaskStatusMsg,
- }
- switch parsed.Data.TaskStatus {
- case "submitted":
- info.Status = model.TaskStatusSubmitted
- info.Progress = taskcommon.ProgressSubmitted
- case "processing":
- info.Status = model.TaskStatusInProgress
- info.Progress = taskcommon.ProgressInProgress
- case "succeed":
- info.Status = model.TaskStatusSuccess
- info.Progress = taskcommon.ProgressComplete
- if tokens := finalUnitDeductionTokens(parsed.Data.FinalUnitDeduction); tokens > 0 {
- info.CompletionTokens = tokens
- info.TotalTokens = tokens
- }
- case "failed":
- info.Status = model.TaskStatusFailure
- info.Progress = taskcommon.ProgressComplete
- default:
- info.Status = model.TaskStatusInProgress
- info.Progress = taskcommon.ProgressInProgress
- }
- if len(parsed.Data.TaskResult.Videos) > 0 {
- info.Url = parsed.Data.TaskResult.Videos[0].URL
- }
- return info, nil
- }
-
- func finalUnitDeductionTokens(value any) int {
- var d decimal.Decimal
- var err error
- switch v := value.(type) {
- case string:
- if strings.TrimSpace(v) == "" {
- return 0
- }
- d, err = decimal.NewFromString(strings.TrimSpace(v))
- case float64:
- d = decimal.NewFromFloat(v)
- case int:
- d = decimal.NewFromInt(int64(v))
- case int64:
- d = decimal.NewFromInt(v)
- default:
- return 0
- }
- if err != nil || !d.IsPositive() {
- return 0
- }
- return int(d.Mul(decimal.NewFromInt(1000000)).IntPart())
- }
-
- func (a *TaskAdaptor) GetModelList() []string {
- return Models()
- }
-
- func (a *TaskAdaptor) GetChannelName() string {
- return "kling-aiping"
- }
-
- func normalizeRequestBody(body map[string]any, info *relaycommon.RelayInfo) {
- delete(body, "action_control")
- delete(body, "uid")
- delete(body, "create_at")
- delete(body, "_standard_model")
-
- modelName := stringValue(body["model_name"])
- if modelName == "" {
- modelName = stringValue(body["model"])
- }
- if info != nil && info.IsModelMapped && info.UpstreamModelName != "" {
- modelName = info.UpstreamModelName
- }
- if modelName != "" {
- body["model_name"] = canonicalAipingModelName(modelName)
- }
- delete(body, "model")
-
- duration := body["duration"]
- if duration == nil {
- duration = body["seconds"]
- }
- if duration != nil {
- body["duration"] = fmt.Sprint(duration)
- }
- delete(body, "seconds")
-
- if imageList, ok := body["reference_images"]; ok {
- if _, exists := body["image_list"]; !exists {
- body["image_list"] = imageList
- }
- delete(body, "reference_images")
- }
- if imageList, ok := normalizeImageList(body["image_list"]); ok {
- body["image_list"] = imageList
- }
-
- if body["watermark_info"] == nil {
- return
- }
- }
-
- func normalizeImageList(v any) ([]map[string]any, bool) {
- items, ok := v.([]any)
- if !ok {
- return nil, false
- }
- out := make([]map[string]any, 0, len(items))
- for _, item := range items {
- switch val := item.(type) {
- case string:
- out = append(out, map[string]any{"image": val})
- case map[string]any:
- image := firstString(val, "image", "image_url", "url", "base64")
- if image == "" {
- out = append(out, val)
- continue
- }
- out = append(out, map[string]any{"image": image})
- default:
- out = append(out, map[string]any{"image": fmt.Sprint(val)})
- }
- }
- return out, true
- }
-
- func sanitizeNativeTaskPayload(payload map[string]any, publicTaskID string) {
- delete(payload, "aiping_id")
- data, _ := payload["data"].(map[string]any)
- if data == nil {
- return
- }
- data["task_id"] = publicTaskID
- ensureWatermarkURL(data)
- if _, ok := data["created_at"]; !ok {
- data["created_at"] = time.Now().UnixMilli()
- }
- if _, ok := data["updated_at"]; !ok {
- data["updated_at"] = data["created_at"]
- }
- }
-
- func ensureWatermarkURL(data map[string]any) {
- taskResult, _ := data["task_result"].(map[string]any)
- if taskResult == nil {
- return
- }
- videos, _ := taskResult["videos"].([]any)
- for _, videoAny := range videos {
- video, _ := videoAny.(map[string]any)
- if video == nil {
- continue
- }
- if _, ok := video["watermark_url"]; !ok {
- video["watermark_url"] = ""
- }
- }
- }
-
- func cloneMap(in map[string]any) map[string]any {
- out := make(map[string]any, len(in))
- for k, v := range in {
- out[k] = v
- }
- return out
- }
-
- func stringValue(v any) string {
- if s, ok := v.(string); ok {
- return strings.TrimSpace(s)
- }
- return ""
- }
-
- func canonicalAipingModelName(modelName string) string {
- trimmed := strings.TrimSpace(modelName)
- switch strings.ToLower(trimmed) {
- case "kling-v1":
- return "Kling-V1"
- case "kling-v1-6":
- return "Kling-V1.6"
- case "kling-v2-6":
- return "Kling-V2.6"
- case "kling-v3":
- return "Kling-V3"
- case "kling-video-o1":
- return "Kling-Video-O1"
- case "kling-v3-omni":
- return "Kling-V3-Omni"
- default:
- return trimmed
- }
- }
-
- func firstString(m map[string]any, keys ...string) string {
- for _, key := range keys {
- if s := stringValue(m[key]); s != "" {
- return s
- }
- }
- return ""
- }
-
- func resolveRequestModelName(req relaycommon.TaskSubmitReq, info *relaycommon.RelayInfo) string {
- if info != nil && info.ChannelMeta != nil && info.IsModelMapped && info.UpstreamModelName != "" {
- return strings.TrimSpace(info.UpstreamModelName)
- }
- if modelName := firstString(req.Metadata, "model_name", "model"); modelName != "" {
- return modelName
- }
- return strings.TrimSpace(req.Model)
- }
-
- func isOmniModel(modelName string) bool {
- normalized := strings.ToLower(strings.TrimSpace(modelName))
- return normalized == "kling-video-o1" || normalized == "kling-v3-omni"
- }
|