Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 

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