Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 

563 строки
18 KiB

  1. package relay
  2. import (
  3. "bytes"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "net/http"
  8. "strconv"
  9. "strings"
  10. "github.com/QuantumNous/new-api/common"
  11. "github.com/QuantumNous/new-api/constant"
  12. "github.com/QuantumNous/new-api/dto"
  13. "github.com/QuantumNous/new-api/model"
  14. "github.com/QuantumNous/new-api/relay/channel"
  15. "github.com/QuantumNous/new-api/relay/channel/task/taskcommon"
  16. relaycommon "github.com/QuantumNous/new-api/relay/common"
  17. relayconstant "github.com/QuantumNous/new-api/relay/constant"
  18. "github.com/QuantumNous/new-api/relay/helper"
  19. "github.com/QuantumNous/new-api/service"
  20. "github.com/gin-gonic/gin"
  21. )
  22. type TaskSubmitResult struct {
  23. UpstreamTaskID string
  24. TaskData []byte
  25. Platform constant.TaskPlatform
  26. Quota int
  27. //PerCallPrice types.PriceData
  28. }
  29. // ResolveOriginTask 处理基于已有任务的提交(remix / continuation):
  30. // 查找原始任务、从中提取模型名称、将渠道锁定到原始任务的渠道
  31. // (通过 info.LockedChannel,重试时复用同一渠道并轮换 key),
  32. // 以及提取 OtherRatios(时长、分辨率)。
  33. // 该函数在控制器的重试循环之前调用一次,其结果通过 info 字段和上下文持久化。
  34. func ResolveOriginTask(c *gin.Context, info *relaycommon.RelayInfo) *dto.TaskError {
  35. // 检测 remix action
  36. path := c.Request.URL.Path
  37. if strings.Contains(path, "/v1/videos/") && strings.HasSuffix(path, "/remix") {
  38. info.Action = constant.TaskActionRemix
  39. }
  40. if info.Action == constant.TaskActionRemix {
  41. videoID := c.Param("video_id")
  42. if strings.TrimSpace(videoID) == "" {
  43. return service.TaskErrorWrapperLocal(fmt.Errorf("video_id is required"), "invalid_request", http.StatusBadRequest)
  44. }
  45. info.OriginTaskID = videoID
  46. }
  47. if info.OriginTaskID == "" {
  48. return nil
  49. }
  50. // 查找原始任务
  51. originTask, exist, err := model.GetByTaskId(info.UserId, info.OriginTaskID)
  52. if err != nil {
  53. return service.TaskErrorWrapper(err, "get_origin_task_failed", http.StatusInternalServerError)
  54. }
  55. if !exist {
  56. return service.TaskErrorWrapperLocal(errors.New("task_origin_not_exist"), "task_not_exist", http.StatusBadRequest)
  57. }
  58. // 从原始任务推导模型名称
  59. if info.OriginModelName == "" {
  60. if originTask.Properties.OriginModelName != "" {
  61. info.OriginModelName = originTask.Properties.OriginModelName
  62. } else if originTask.Properties.UpstreamModelName != "" {
  63. info.OriginModelName = originTask.Properties.UpstreamModelName
  64. } else {
  65. var taskData map[string]interface{}
  66. _ = common.Unmarshal(originTask.Data, &taskData)
  67. if m, ok := taskData["model"].(string); ok && m != "" {
  68. info.OriginModelName = m
  69. }
  70. }
  71. }
  72. // 锁定到原始任务的渠道(重试时复用同一渠道,轮换 key)
  73. ch, err := model.GetChannelById(originTask.ChannelId, true)
  74. if err != nil {
  75. return service.TaskErrorWrapperLocal(err, "channel_not_found", http.StatusBadRequest)
  76. }
  77. if ch.Status != common.ChannelStatusEnabled {
  78. return service.TaskErrorWrapperLocal(errors.New("the channel of the origin task is disabled"), "task_channel_disable", http.StatusBadRequest)
  79. }
  80. info.LockedChannel = ch
  81. if originTask.ChannelId != info.ChannelId {
  82. key, _, newAPIError := ch.GetNextEnabledKey()
  83. if newAPIError != nil {
  84. return service.TaskErrorWrapper(newAPIError, "channel_no_available_key", newAPIError.StatusCode)
  85. }
  86. common.SetContextKey(c, constant.ContextKeyChannelKey, key)
  87. common.SetContextKey(c, constant.ContextKeyChannelType, ch.Type)
  88. common.SetContextKey(c, constant.ContextKeyChannelBaseUrl, ch.GetBaseURL())
  89. common.SetContextKey(c, constant.ContextKeyChannelId, originTask.ChannelId)
  90. info.ChannelBaseUrl = ch.GetBaseURL()
  91. info.ChannelId = originTask.ChannelId
  92. info.ChannelType = ch.Type
  93. info.ApiKey = key
  94. }
  95. // 提取 remix 参数(时长、分辨率 → OtherRatios)
  96. if info.Action == constant.TaskActionRemix {
  97. if originTask.PrivateData.BillingContext != nil {
  98. // 新的 remix 逻辑:直接从原始任务的 BillingContext 中提取 OtherRatios(如果存在)
  99. for s, f := range originTask.PrivateData.BillingContext.OtherRatios {
  100. info.PriceData.AddOtherRatio(s, f)
  101. }
  102. } else {
  103. // 旧的 remix 逻辑:直接从 task data 解析 seconds 和 size(如果存在)
  104. var taskData map[string]interface{}
  105. _ = common.Unmarshal(originTask.Data, &taskData)
  106. secondsStr, _ := taskData["seconds"].(string)
  107. seconds, _ := strconv.Atoi(secondsStr)
  108. if seconds <= 0 {
  109. seconds = 4
  110. }
  111. sizeStr, _ := taskData["size"].(string)
  112. if info.PriceData.OtherRatios == nil {
  113. info.PriceData.OtherRatios = map[string]float64{}
  114. }
  115. info.PriceData.OtherRatios["seconds"] = float64(seconds)
  116. info.PriceData.OtherRatios["size"] = 1
  117. if sizeStr == "1792x1024" || sizeStr == "1024x1792" {
  118. info.PriceData.OtherRatios["size"] = 1.666667
  119. }
  120. }
  121. }
  122. return nil
  123. }
  124. // RelayTaskSubmit 完成 task 提交的全部流程(每次尝试调用一次):
  125. // 刷新渠道元数据 → 确定 platform/adaptor → 验证请求 →
  126. // 估算计费(EstimateBilling) → 计算价格 → 预扣费(仅首次)→
  127. // 构建/发送/解析上游请求 → 提交后计费调整(AdjustBillingOnSubmit)。
  128. // 控制器负责 defer Refund 和成功后 Settle。
  129. func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (*TaskSubmitResult, *dto.TaskError) {
  130. info.InitChannelMeta(c)
  131. // 1. 确定 platform → 创建适配器 → 验证请求
  132. platform := constant.TaskPlatform(c.GetString("platform"))
  133. if platform == "" {
  134. platform = GetTaskPlatform(c)
  135. }
  136. adaptor := GetTaskAdaptor(platform)
  137. if adaptor == nil {
  138. return nil, service.TaskErrorWrapperLocal(fmt.Errorf("invalid api platform: %s", platform), "invalid_api_platform", http.StatusBadRequest)
  139. }
  140. adaptor.Init(info)
  141. if taskErr := adaptor.ValidateRequestAndSetAction(c, info); taskErr != nil {
  142. return nil, taskErr
  143. }
  144. // 2. 确定模型名称
  145. modelName := info.OriginModelName
  146. if modelName == "" {
  147. modelName = service.CoverTaskActionToModelName(platform, info.Action)
  148. }
  149. // 2.5 应用渠道的模型映射(与同步任务对齐)
  150. info.OriginModelName = modelName
  151. info.UpstreamModelName = modelName
  152. if err := helper.ModelMappedHelper(c, info, nil); err != nil {
  153. return nil, service.TaskErrorWrapperLocal(err, "model_mapping_failed", http.StatusBadRequest)
  154. }
  155. // 3. 预生成公开 task ID(仅首次)
  156. if info.PublicTaskID == "" {
  157. info.PublicTaskID = model.GenerateTaskID()
  158. }
  159. // 4. 价格计算:基础模型价格
  160. info.OriginModelName = modelName
  161. priceData, pricingErr := helper.ModelPriceHelperPerCall(c, info)
  162. if pricingErr != nil {
  163. return nil, pricingErr
  164. }
  165. info.PriceData = priceData
  166. // 5. 计费估算:让适配器根据用户请求提供 OtherRatios(时长、分辨率等)
  167. // 必须在 ModelPriceHelperPerCall 之后调用(它会重建 PriceData)。
  168. // ResolveOriginTask 可能已在 remix 路径中预设了 OtherRatios,此处合并。
  169. if estimatedRatios := adaptor.EstimateBilling(c, info); len(estimatedRatios) > 0 {
  170. for k, v := range estimatedRatios {
  171. info.PriceData.AddOtherRatio(k, v)
  172. }
  173. }
  174. // 6. 将 OtherRatios 应用到基础额度
  175. if !common.StringsContains(constant.TaskPricePatches, modelName) {
  176. for _, ra := range info.PriceData.OtherRatios {
  177. if ra != 1.0 {
  178. info.PriceData.Quota = int(float64(info.PriceData.Quota) * ra)
  179. }
  180. }
  181. }
  182. // 7. 预扣费(仅首次 — 重试时 info.Billing 已存在,跳过)
  183. if info.Billing == nil && !info.PriceData.FreeModel {
  184. info.ForcePreConsume = true
  185. if apiErr := service.PreConsumeBilling(c, info.PriceData.Quota, info); apiErr != nil {
  186. return nil, service.TaskErrorFromAPIError(apiErr)
  187. }
  188. }
  189. // 8. 构建请求体
  190. requestBody, err := adaptor.BuildRequestBody(c, info)
  191. if err != nil {
  192. return nil, service.TaskErrorWrapper(err, "build_request_failed", http.StatusInternalServerError)
  193. }
  194. // 9. 发送请求
  195. resp, err := adaptor.DoRequest(c, info, requestBody)
  196. if err != nil {
  197. return nil, service.TaskErrorWrapper(err, "do_request_failed", http.StatusInternalServerError)
  198. }
  199. if resp != nil && resp.StatusCode != http.StatusOK {
  200. responseBody, _ := io.ReadAll(resp.Body)
  201. return nil, service.TaskErrorWrapper(fmt.Errorf("%s", string(responseBody)), "fail_to_fetch_task", resp.StatusCode)
  202. }
  203. // 10. 返回 OtherRatios 给下游(header 必须在 DoResponse 写 body 之前设置)
  204. otherRatios := info.PriceData.OtherRatios
  205. if otherRatios == nil {
  206. otherRatios = map[string]float64{}
  207. }
  208. ratiosJSON, _ := common.Marshal(otherRatios)
  209. c.Header("X-New-Api-Other-Ratios", string(ratiosJSON))
  210. // 11. 解析响应
  211. upstreamTaskID, taskData, taskErr := adaptor.DoResponse(c, resp, info)
  212. if taskErr != nil {
  213. return nil, taskErr
  214. }
  215. // 11. 提交后计费调整:让适配器根据上游实际返回调整 OtherRatios
  216. finalQuota := info.PriceData.Quota
  217. if adjustedRatios := adaptor.AdjustBillingOnSubmit(info, taskData); len(adjustedRatios) > 0 {
  218. // 基于调整后的 ratios 重新计算 quota
  219. finalQuota = recalcQuotaFromRatios(info, adjustedRatios)
  220. info.PriceData.OtherRatios = adjustedRatios
  221. info.PriceData.Quota = finalQuota
  222. }
  223. return &TaskSubmitResult{
  224. UpstreamTaskID: upstreamTaskID,
  225. TaskData: taskData,
  226. Platform: platform,
  227. Quota: finalQuota,
  228. }, nil
  229. }
  230. // recalcQuotaFromRatios 根据 adjustedRatios 重新计算 quota。
  231. // 公式: baseQuota × ∏(ratio) — 其中 baseQuota 是不含 OtherRatios 的基础额度。
  232. func recalcQuotaFromRatios(info *relaycommon.RelayInfo, ratios map[string]float64) int {
  233. // 从 PriceData 获取不含 OtherRatios 的基础价格
  234. baseQuota := info.PriceData.Quota
  235. // 先除掉原有的 OtherRatios 恢复基础额度
  236. for _, ra := range info.PriceData.OtherRatios {
  237. if ra != 1.0 && ra > 0 {
  238. baseQuota = int(float64(baseQuota) / ra)
  239. }
  240. }
  241. // 应用新的 ratios
  242. result := float64(baseQuota)
  243. for _, ra := range ratios {
  244. if ra != 1.0 {
  245. result *= ra
  246. }
  247. }
  248. return int(result)
  249. }
  250. var fetchRespBuilders = map[int]func(c *gin.Context) (respBody []byte, taskResp *dto.TaskError){
  251. relayconstant.RelayModeSunoFetchByID: sunoFetchByIDRespBodyBuilder,
  252. relayconstant.RelayModeSunoFetch: sunoFetchRespBodyBuilder,
  253. relayconstant.RelayModeVideoFetchByID: videoFetchByIDRespBodyBuilder,
  254. }
  255. func RelayTaskFetch(c *gin.Context, relayMode int) (taskResp *dto.TaskError) {
  256. respBuilder, ok := fetchRespBuilders[relayMode]
  257. if !ok {
  258. taskResp = service.TaskErrorWrapperLocal(errors.New("invalid_relay_mode"), "invalid_relay_mode", http.StatusBadRequest)
  259. }
  260. respBody, taskErr := respBuilder(c)
  261. if taskErr != nil {
  262. return taskErr
  263. }
  264. if len(respBody) == 0 {
  265. respBody = []byte("{\"code\":\"success\",\"data\":null}")
  266. }
  267. c.Writer.Header().Set("Content-Type", "application/json")
  268. _, err := io.Copy(c.Writer, bytes.NewBuffer(respBody))
  269. if err != nil {
  270. taskResp = service.TaskErrorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError)
  271. return
  272. }
  273. return
  274. }
  275. func sunoFetchRespBodyBuilder(c *gin.Context) (respBody []byte, taskResp *dto.TaskError) {
  276. userId := c.GetInt("id")
  277. var condition = struct {
  278. IDs []any `json:"ids"`
  279. Action string `json:"action"`
  280. }{}
  281. err := c.BindJSON(&condition)
  282. if err != nil {
  283. taskResp = service.TaskErrorWrapper(err, "invalid_request", http.StatusBadRequest)
  284. return
  285. }
  286. var tasks []any
  287. if len(condition.IDs) > 0 {
  288. taskModels, err := model.GetByTaskIds(userId, condition.IDs)
  289. if err != nil {
  290. taskResp = service.TaskErrorWrapper(err, "get_tasks_failed", http.StatusInternalServerError)
  291. return
  292. }
  293. for _, task := range taskModels {
  294. tasks = append(tasks, TaskModel2Dto(task))
  295. }
  296. } else {
  297. tasks = make([]any, 0)
  298. }
  299. respBody, err = common.Marshal(dto.TaskResponse[[]any]{
  300. Code: "success",
  301. Data: tasks,
  302. })
  303. return
  304. }
  305. func sunoFetchByIDRespBodyBuilder(c *gin.Context) (respBody []byte, taskResp *dto.TaskError) {
  306. taskId := c.Param("id")
  307. userId := c.GetInt("id")
  308. originTask, exist, err := model.GetByTaskId(userId, taskId)
  309. if err != nil {
  310. taskResp = service.TaskErrorWrapper(err, "get_task_failed", http.StatusInternalServerError)
  311. return
  312. }
  313. if !exist {
  314. taskResp = service.TaskErrorWrapperLocal(errors.New("task_not_exist"), "task_not_exist", http.StatusBadRequest)
  315. return
  316. }
  317. respBody, err = common.Marshal(dto.TaskResponse[any]{
  318. Code: "success",
  319. Data: TaskModel2Dto(originTask),
  320. })
  321. return
  322. }
  323. func videoFetchByIDRespBodyBuilder(c *gin.Context) (respBody []byte, taskResp *dto.TaskError) {
  324. taskId := c.Param("task_id")
  325. if taskId == "" {
  326. taskId = c.GetString("task_id")
  327. }
  328. userId := c.GetInt("id")
  329. originTask, exist, err := model.GetByTaskId(userId, taskId)
  330. if err != nil {
  331. taskResp = service.TaskErrorWrapper(err, "get_task_failed", http.StatusInternalServerError)
  332. return
  333. }
  334. if !exist {
  335. taskResp = service.TaskErrorWrapperLocal(errors.New("task_not_exist"), "task_not_exist", http.StatusBadRequest)
  336. return
  337. }
  338. isOpenAIVideoAPI := strings.HasPrefix(c.Request.RequestURI, "/v1/videos/")
  339. // Gemini/Vertex 支持实时查询:用户 fetch 时直接从上游拉取最新状态
  340. if realtimeResp := tryRealtimeFetch(originTask, isOpenAIVideoAPI); len(realtimeResp) > 0 {
  341. respBody = realtimeResp
  342. return
  343. }
  344. // OpenAI Video API 格式: 走各 adaptor 的 ConvertToOpenAIVideo
  345. if isOpenAIVideoAPI {
  346. adaptor := GetTaskAdaptor(originTask.Platform)
  347. if adaptor == nil {
  348. taskResp = service.TaskErrorWrapperLocal(fmt.Errorf("invalid channel id: %d", originTask.ChannelId), "invalid_channel_id", http.StatusBadRequest)
  349. return
  350. }
  351. if converter, ok := adaptor.(channel.OpenAIVideoConverter); ok {
  352. openAIVideoData, err := converter.ConvertToOpenAIVideo(originTask)
  353. if err != nil {
  354. taskResp = service.TaskErrorWrapper(err, "convert_to_openai_video_failed", http.StatusInternalServerError)
  355. return
  356. }
  357. respBody = openAIVideoData
  358. return
  359. }
  360. taskResp = service.TaskErrorWrapperLocal(fmt.Errorf("not_implemented:%s", originTask.Platform), "not_implemented", http.StatusNotImplemented)
  361. return
  362. }
  363. // 通用 TaskDto 格式
  364. respBody, err = common.Marshal(dto.TaskResponse[any]{
  365. Code: "success",
  366. Data: TaskModel2Dto(originTask),
  367. })
  368. if err != nil {
  369. taskResp = service.TaskErrorWrapper(err, "marshal_response_failed", http.StatusInternalServerError)
  370. }
  371. return
  372. }
  373. // tryRealtimeFetch 尝试从上游实时拉取 Gemini/Vertex 任务状态。
  374. // 仅当渠道类型为 Gemini 或 Vertex 时触发;其他渠道或出错时返回 nil。
  375. // 当非 OpenAI Video API 时,还会构建自定义格式的响应体。
  376. func tryRealtimeFetch(task *model.Task, isOpenAIVideoAPI bool) []byte {
  377. channelModel, err := model.GetChannelById(task.ChannelId, true)
  378. if err != nil {
  379. return nil
  380. }
  381. if channelModel.Type != constant.ChannelTypeVertexAi && channelModel.Type != constant.ChannelTypeGemini {
  382. return nil
  383. }
  384. baseURL := constant.ChannelBaseURLs[channelModel.Type]
  385. if channelModel.GetBaseURL() != "" {
  386. baseURL = channelModel.GetBaseURL()
  387. }
  388. proxy := channelModel.GetSetting().Proxy
  389. adaptor := GetTaskAdaptor(constant.TaskPlatform(strconv.Itoa(channelModel.Type)))
  390. if adaptor == nil {
  391. return nil
  392. }
  393. resp, err := adaptor.FetchTask(baseURL, channelModel.Key, map[string]any{
  394. "task_id": task.GetUpstreamTaskID(),
  395. "action": task.Action,
  396. }, proxy)
  397. if err != nil || resp == nil {
  398. return nil
  399. }
  400. defer resp.Body.Close()
  401. body, err := io.ReadAll(resp.Body)
  402. if err != nil {
  403. return nil
  404. }
  405. ti, err := adaptor.ParseTaskResult(body)
  406. if err != nil || ti == nil {
  407. return nil
  408. }
  409. snap := task.Snapshot()
  410. // 将上游最新状态更新到 task
  411. if ti.Status != "" {
  412. task.Status = model.TaskStatus(ti.Status)
  413. }
  414. if ti.Progress != "" {
  415. task.Progress = ti.Progress
  416. }
  417. if strings.HasPrefix(ti.Url, "data:") {
  418. // data: URI — kept in Data, not ResultURL
  419. } else if ti.Url != "" {
  420. task.PrivateData.ResultURL = ti.Url
  421. } else if task.Status == model.TaskStatusSuccess {
  422. // No URL from adaptor — construct proxy URL using public task ID
  423. task.PrivateData.ResultURL = taskcommon.BuildProxyURL(task.TaskID)
  424. }
  425. if !snap.Equal(task.Snapshot()) {
  426. _, _ = task.UpdateWithStatus(snap.Status)
  427. }
  428. // OpenAI Video API 由调用者的 ConvertToOpenAIVideo 分支处理
  429. if isOpenAIVideoAPI {
  430. return nil
  431. }
  432. // 非 OpenAI Video API: 构建自定义格式响应
  433. format := detectVideoFormat(body)
  434. out := map[string]any{
  435. "error": nil,
  436. "format": format,
  437. "metadata": nil,
  438. "status": mapTaskStatusToSimple(task.Status),
  439. "task_id": task.TaskID,
  440. "url": task.GetResultURL(),
  441. }
  442. respBody, _ := common.Marshal(dto.TaskResponse[any]{
  443. Code: "success",
  444. Data: out,
  445. })
  446. return respBody
  447. }
  448. // detectVideoFormat 从 Gemini/Vertex 原始响应中探测视频格式
  449. func detectVideoFormat(rawBody []byte) string {
  450. var raw map[string]any
  451. if err := common.Unmarshal(rawBody, &raw); err != nil {
  452. return "mp4"
  453. }
  454. respObj, ok := raw["response"].(map[string]any)
  455. if !ok {
  456. return "mp4"
  457. }
  458. vids, ok := respObj["videos"].([]any)
  459. if !ok || len(vids) == 0 {
  460. return "mp4"
  461. }
  462. v0, ok := vids[0].(map[string]any)
  463. if !ok {
  464. return "mp4"
  465. }
  466. mt, ok := v0["mimeType"].(string)
  467. if !ok || mt == "" || strings.Contains(mt, "mp4") {
  468. return "mp4"
  469. }
  470. return mt
  471. }
  472. // mapTaskStatusToSimple 将内部 TaskStatus 映射为简化状态字符串
  473. func mapTaskStatusToSimple(status model.TaskStatus) string {
  474. switch status {
  475. case model.TaskStatusSuccess:
  476. return "succeeded"
  477. case model.TaskStatusFailure:
  478. return "failed"
  479. case model.TaskStatusQueued, model.TaskStatusSubmitted:
  480. return "queued"
  481. default:
  482. return "processing"
  483. }
  484. }
  485. func TaskModel2Dto(task *model.Task) *dto.TaskDto {
  486. return &dto.TaskDto{
  487. ID: task.ID,
  488. CreatedAt: task.CreatedAt,
  489. UpdatedAt: task.UpdatedAt,
  490. TaskID: task.TaskID,
  491. Platform: string(task.Platform),
  492. UserId: task.UserId,
  493. Group: task.Group,
  494. ChannelId: task.ChannelId,
  495. Quota: task.Quota,
  496. Action: task.Action,
  497. Status: string(task.Status),
  498. FailReason: task.FailReason,
  499. ResultURL: task.GetResultURL(),
  500. SubmitTime: task.SubmitTime,
  501. StartTime: task.StartTime,
  502. FinishTime: task.FinishTime,
  503. Progress: task.Progress,
  504. Properties: task.Properties,
  505. Username: task.Username,
  506. Data: task.Data,
  507. }
  508. }