You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

362 lines
10 KiB

  1. package aiping
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "strings"
  8. "time"
  9. "github.com/QuantumNous/new-api/common"
  10. "github.com/QuantumNous/new-api/dto"
  11. "github.com/QuantumNous/new-api/model"
  12. "github.com/QuantumNous/new-api/relay/channel"
  13. taskcommon "github.com/QuantumNous/new-api/relay/channel/task/taskcommon"
  14. relaycommon "github.com/QuantumNous/new-api/relay/common"
  15. "github.com/QuantumNous/new-api/service"
  16. "github.com/gin-gonic/gin"
  17. "github.com/pkg/errors"
  18. )
  19. type responsePayload struct {
  20. Code int `json:"code"`
  21. Message string `json:"message"`
  22. RequestID string `json:"request_id"`
  23. AipingID string `json:"aiping_id"`
  24. Data struct {
  25. TaskID string `json:"task_id"`
  26. TaskStatus string `json:"task_status"`
  27. TaskStatusMsg string `json:"task_status_msg"`
  28. TaskInfo any `json:"task_info,omitempty"`
  29. TaskResult struct {
  30. Videos []struct {
  31. ID string `json:"id"`
  32. URL string `json:"url"`
  33. WatermarkURL string `json:"watermark_url,omitempty"`
  34. Duration string `json:"duration"`
  35. } `json:"videos"`
  36. } `json:"task_result"`
  37. CreatedAt int64 `json:"created_at"`
  38. UpdatedAt int64 `json:"updated_at"`
  39. } `json:"data"`
  40. }
  41. type TaskAdaptor struct {
  42. taskcommon.BaseBilling
  43. apiKey string
  44. baseURL string
  45. }
  46. func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) {
  47. a.apiKey = info.ApiKey
  48. a.baseURL = strings.TrimRight(info.ChannelBaseUrl, "/")
  49. }
  50. func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) *dto.TaskError {
  51. req, err := relaycommon.GetTaskRequest(c)
  52. if err != nil {
  53. return service.TaskErrorWrapperLocal(fmt.Errorf("task request not prepared"), "invalid_request", http.StatusBadRequest)
  54. }
  55. modelName := resolveRequestModelName(req, info)
  56. if isOmniModel(modelName) && (info.Action == ActionText2Video || info.Action == ActionImage2Video) {
  57. return service.TaskErrorWrapperLocal(
  58. fmt.Errorf("model %s must use /v1/videos/omni-video", modelName),
  59. "invalid_model_route",
  60. http.StatusUnprocessableEntity,
  61. )
  62. }
  63. return nil
  64. }
  65. func (a *TaskAdaptor) BuildRequestURL(info *relaycommon.RelayInfo) (string, error) {
  66. route, ok := FindRouteByAction(info.Action)
  67. if !ok {
  68. return "", fmt.Errorf("unsupported kling aiping action: %s", info.Action)
  69. }
  70. return a.baseURL + route.UpstreamPath, nil
  71. }
  72. func (a *TaskAdaptor) BuildRequestHeader(c *gin.Context, req *http.Request, info *relaycommon.RelayInfo) error {
  73. req.Header.Set("Content-Type", "application/json")
  74. req.Header.Set("Accept", "application/json")
  75. req.Header.Set("Authorization", "Bearer "+a.apiKey)
  76. return nil
  77. }
  78. func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayInfo) (io.Reader, error) {
  79. req, err := relaycommon.GetTaskRequest(c)
  80. if err != nil {
  81. return nil, err
  82. }
  83. body := cloneMap(req.Metadata)
  84. normalizeRequestBody(body, info)
  85. data, err := common.Marshal(body)
  86. if err != nil {
  87. return nil, err
  88. }
  89. return bytes.NewReader(data), nil
  90. }
  91. func (a *TaskAdaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (*http.Response, error) {
  92. return channel.DoTaskApiRequest(a, c, info, requestBody)
  93. }
  94. func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (string, []byte, *dto.TaskError) {
  95. responseBody, err := io.ReadAll(resp.Body)
  96. if err != nil {
  97. return "", nil, service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError)
  98. }
  99. _ = resp.Body.Close()
  100. var parsed responsePayload
  101. if err := common.Unmarshal(responseBody, &parsed); err != nil {
  102. return "", nil, service.TaskErrorWrapper(errors.Wrapf(err, "body: %s", responseBody), "unmarshal_response_body_failed", http.StatusInternalServerError)
  103. }
  104. if parsed.Code != 0 {
  105. msg := parsed.Message
  106. if msg == "" {
  107. msg = "upstream task failed"
  108. }
  109. return "", nil, service.TaskErrorWrapper(fmt.Errorf("%s", msg), "task_failed", http.StatusBadRequest)
  110. }
  111. upstreamTaskID := parsed.Data.TaskID
  112. if strings.TrimSpace(upstreamTaskID) == "" {
  113. return "", nil, service.TaskErrorWrapper(fmt.Errorf("task_id is empty"), "invalid_response", http.StatusInternalServerError)
  114. }
  115. clientPayload := map[string]any{}
  116. if err := common.Unmarshal(responseBody, &clientPayload); err != nil {
  117. return "", nil, service.TaskErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError)
  118. }
  119. sanitizeNativeTaskPayload(clientPayload, info.PublicTaskID)
  120. c.JSON(http.StatusOK, clientPayload)
  121. return upstreamTaskID, responseBody, nil
  122. }
  123. func (a *TaskAdaptor) FetchTask(baseURL, key string, body map[string]any, proxy string) (*http.Response, error) {
  124. taskID, _ := body["task_id"].(string)
  125. if strings.TrimSpace(taskID) == "" {
  126. taskID, _ = body["upstream_task_id"].(string)
  127. }
  128. action, _ := body["action"].(string)
  129. if strings.TrimSpace(taskID) == "" {
  130. return nil, fmt.Errorf("invalid task_id")
  131. }
  132. route, ok := FindFetchRouteByAction(action)
  133. if !ok {
  134. return nil, fmt.Errorf("unsupported kling aiping action: %s", action)
  135. }
  136. url := strings.TrimRight(baseURL, "/") + strings.Replace(route.UpstreamPath, ":task_id", taskID, 1)
  137. req, err := http.NewRequest(http.MethodGet, url, nil)
  138. if err != nil {
  139. return nil, err
  140. }
  141. req.Header.Set("Accept", "application/json")
  142. req.Header.Set("Authorization", "Bearer "+key)
  143. client, err := service.GetHttpClientWithProxy(proxy)
  144. if err != nil {
  145. return nil, fmt.Errorf("new proxy http client failed: %w", err)
  146. }
  147. return client.Do(req)
  148. }
  149. func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, error) {
  150. var parsed responsePayload
  151. if err := common.Unmarshal(respBody, &parsed); err != nil {
  152. return nil, errors.Wrap(err, "unmarshal kling aiping task data failed")
  153. }
  154. info := &relaycommon.TaskInfo{
  155. Code: parsed.Code,
  156. TaskID: parsed.Data.TaskID,
  157. Reason: parsed.Data.TaskStatusMsg,
  158. }
  159. switch parsed.Data.TaskStatus {
  160. case "submitted":
  161. info.Status = model.TaskStatusSubmitted
  162. info.Progress = taskcommon.ProgressSubmitted
  163. case "processing":
  164. info.Status = model.TaskStatusInProgress
  165. info.Progress = taskcommon.ProgressInProgress
  166. case "succeed":
  167. info.Status = model.TaskStatusSuccess
  168. info.Progress = taskcommon.ProgressComplete
  169. case "failed":
  170. info.Status = model.TaskStatusFailure
  171. info.Progress = taskcommon.ProgressComplete
  172. default:
  173. info.Status = model.TaskStatusInProgress
  174. info.Progress = taskcommon.ProgressInProgress
  175. }
  176. if len(parsed.Data.TaskResult.Videos) > 0 {
  177. info.Url = parsed.Data.TaskResult.Videos[0].URL
  178. }
  179. return info, nil
  180. }
  181. func (a *TaskAdaptor) GetModelList() []string {
  182. return Models()
  183. }
  184. func (a *TaskAdaptor) GetChannelName() string {
  185. return "kling-aiping"
  186. }
  187. func normalizeRequestBody(body map[string]any, info *relaycommon.RelayInfo) {
  188. delete(body, "action_control")
  189. delete(body, "uid")
  190. delete(body, "create_at")
  191. delete(body, "_standard_model")
  192. modelName := stringValue(body["model_name"])
  193. if modelName == "" {
  194. modelName = stringValue(body["model"])
  195. }
  196. if info != nil && info.IsModelMapped && info.UpstreamModelName != "" {
  197. modelName = info.UpstreamModelName
  198. }
  199. if modelName != "" {
  200. body["model_name"] = canonicalAipingModelName(modelName)
  201. }
  202. delete(body, "model")
  203. duration := body["duration"]
  204. if duration == nil {
  205. duration = body["seconds"]
  206. }
  207. if duration != nil {
  208. body["duration"] = fmt.Sprint(duration)
  209. }
  210. delete(body, "seconds")
  211. if imageList, ok := body["reference_images"]; ok {
  212. if _, exists := body["image_list"]; !exists {
  213. body["image_list"] = imageList
  214. }
  215. delete(body, "reference_images")
  216. }
  217. if imageList, ok := normalizeImageList(body["image_list"]); ok {
  218. body["image_list"] = imageList
  219. }
  220. if body["watermark_info"] == nil {
  221. return
  222. }
  223. }
  224. func normalizeImageList(v any) ([]map[string]any, bool) {
  225. items, ok := v.([]any)
  226. if !ok {
  227. return nil, false
  228. }
  229. out := make([]map[string]any, 0, len(items))
  230. for _, item := range items {
  231. switch val := item.(type) {
  232. case string:
  233. out = append(out, map[string]any{"image": val})
  234. case map[string]any:
  235. image := firstString(val, "image", "image_url", "url", "base64")
  236. if image == "" {
  237. out = append(out, val)
  238. continue
  239. }
  240. out = append(out, map[string]any{"image": image})
  241. default:
  242. out = append(out, map[string]any{"image": fmt.Sprint(val)})
  243. }
  244. }
  245. return out, true
  246. }
  247. func sanitizeNativeTaskPayload(payload map[string]any, publicTaskID string) {
  248. delete(payload, "aiping_id")
  249. data, _ := payload["data"].(map[string]any)
  250. if data == nil {
  251. return
  252. }
  253. data["task_id"] = publicTaskID
  254. ensureWatermarkURL(data)
  255. if _, ok := data["created_at"]; !ok {
  256. data["created_at"] = time.Now().UnixMilli()
  257. }
  258. if _, ok := data["updated_at"]; !ok {
  259. data["updated_at"] = data["created_at"]
  260. }
  261. }
  262. func ensureWatermarkURL(data map[string]any) {
  263. taskResult, _ := data["task_result"].(map[string]any)
  264. if taskResult == nil {
  265. return
  266. }
  267. videos, _ := taskResult["videos"].([]any)
  268. for _, videoAny := range videos {
  269. video, _ := videoAny.(map[string]any)
  270. if video == nil {
  271. continue
  272. }
  273. if _, ok := video["watermark_url"]; !ok {
  274. video["watermark_url"] = ""
  275. }
  276. }
  277. }
  278. func cloneMap(in map[string]any) map[string]any {
  279. out := make(map[string]any, len(in))
  280. for k, v := range in {
  281. out[k] = v
  282. }
  283. return out
  284. }
  285. func stringValue(v any) string {
  286. if s, ok := v.(string); ok {
  287. return strings.TrimSpace(s)
  288. }
  289. return ""
  290. }
  291. func canonicalAipingModelName(modelName string) string {
  292. trimmed := strings.TrimSpace(modelName)
  293. switch strings.ToLower(trimmed) {
  294. case "kling-v1":
  295. return "Kling-V1"
  296. case "kling-v1-6":
  297. return "Kling-V1.6"
  298. case "kling-v2-6":
  299. return "Kling-V2.6"
  300. case "kling-v3":
  301. return "Kling-V3"
  302. case "kling-video-o1":
  303. return "Kling-Video-O1"
  304. case "kling-v3-omni":
  305. return "Kling-V3-Omni"
  306. default:
  307. return trimmed
  308. }
  309. }
  310. func firstString(m map[string]any, keys ...string) string {
  311. for _, key := range keys {
  312. if s := stringValue(m[key]); s != "" {
  313. return s
  314. }
  315. }
  316. return ""
  317. }
  318. func resolveRequestModelName(req relaycommon.TaskSubmitReq, info *relaycommon.RelayInfo) string {
  319. if info != nil && info.ChannelMeta != nil && info.IsModelMapped && info.UpstreamModelName != "" {
  320. return strings.TrimSpace(info.UpstreamModelName)
  321. }
  322. if modelName := firstString(req.Metadata, "model_name", "model"); modelName != "" {
  323. return modelName
  324. }
  325. return strings.TrimSpace(req.Model)
  326. }
  327. func isOmniModel(modelName string) bool {
  328. normalized := strings.ToLower(strings.TrimSpace(modelName))
  329. return normalized == "kling-video-o1" || normalized == "kling-v3-omni"
  330. }