Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 

274 wiersze
10 KiB

  1. package doubao_aiping
  2. import (
  3. "io"
  4. "net/http"
  5. "net/http/httptest"
  6. "strings"
  7. "testing"
  8. "github.com/QuantumNous/new-api/common"
  9. "github.com/QuantumNous/new-api/constant"
  10. "github.com/QuantumNous/new-api/model"
  11. relaycommon "github.com/QuantumNous/new-api/relay/common"
  12. "github.com/gin-gonic/gin"
  13. "github.com/stretchr/testify/require"
  14. )
  15. func TestBuildRequestURLUsesAipingVideosEndpoint(t *testing.T) {
  16. adaptor := &TaskAdaptor{}
  17. adaptor.Init(&relaycommon.RelayInfo{
  18. ChannelMeta: &relaycommon.ChannelMeta{
  19. ChannelType: constant.ChannelTypeDoubaoVideoCompatibleAiping,
  20. ChannelBaseUrl: "https://aiping.example.com",
  21. ApiKey: "sk-test",
  22. },
  23. })
  24. got, err := adaptor.BuildRequestURL(&relaycommon.RelayInfo{
  25. ChannelMeta: &relaycommon.ChannelMeta{ChannelBaseUrl: "https://aiping.example.com"},
  26. })
  27. require.NoError(t, err)
  28. require.Equal(t, "https://aiping.example.com/api/v1/multimodal/sd/videos/contents/generations/tasks", got)
  29. }
  30. func TestFetchTaskUsesAipingVideosEndpoint(t *testing.T) {
  31. adaptor := &TaskAdaptor{}
  32. req, err := adaptor.buildFetchRequest("https://aiping.example.com", "sk-test", map[string]any{"task_id": "cgt-123"})
  33. require.NoError(t, err)
  34. require.Equal(t, http.MethodGet, req.Method)
  35. require.Equal(t, "https://aiping.example.com/api/v1/multimodal/sd/videos/contents/generations/tasks/cgt-123", req.URL.String())
  36. require.Equal(t, "Bearer sk-test", req.Header.Get("Authorization"))
  37. }
  38. func TestBuildRequestBodyPassesNativeMetadataToUpstream(t *testing.T) {
  39. adaptor := &TaskAdaptor{}
  40. c := newTaskRequestContext(t, `{
  41. "model":"doubao-seedance-2-0-260128",
  42. "prompt":"current prompt",
  43. "metadata":{
  44. "content":[
  45. {"type":"video_url","video_url":{"url":"https://example.test/input.mp4"},"role":"reference_video"}
  46. ],
  47. "duration":5
  48. }
  49. }`)
  50. info := &relaycommon.RelayInfo{
  51. ChannelMeta: &relaycommon.ChannelMeta{UpstreamModelName: "doubao-seedance-2-0-260128"},
  52. }
  53. body, err := adaptor.BuildRequestBody(c, info)
  54. require.NoError(t, err)
  55. data, err := io.ReadAll(body)
  56. require.NoError(t, err)
  57. require.Contains(t, string(data), `"video_url":{"url":"https://example.test/input.mp4"}`)
  58. require.Contains(t, string(data), `"duration":5`)
  59. require.Contains(t, string(data), `"text":"current prompt"`)
  60. }
  61. func TestBuildRequestBodyPreservesNativeInterleavedContentOrder(t *testing.T) {
  62. adaptor := &TaskAdaptor{}
  63. c := newTaskRequestContext(t, `{
  64. "model":"doubao-seedance-2-0-260128",
  65. "metadata":{
  66. "content":[
  67. {"type":"text","text":"开场:海边日落"},
  68. {"type":"image_url","image_url":{"url":"asset://img1"},"role":"reference_image"},
  69. {"type":"text","text":"中段:车身环绕特写"},
  70. {"type":"video_url","video_url":{"url":"asset://vid1"},"role":"reference_video"},
  71. {"type":"text","text":"结尾:尾灯点亮"}
  72. ],
  73. "duration":5
  74. }
  75. }`)
  76. info := &relaycommon.RelayInfo{
  77. ChannelMeta: &relaycommon.ChannelMeta{UpstreamModelName: "doubao-seedance-2-0-260128"},
  78. }
  79. body, err := adaptor.BuildRequestBody(c, info)
  80. require.NoError(t, err)
  81. data, err := io.ReadAll(body)
  82. require.NoError(t, err)
  83. var payload requestPayload
  84. require.NoError(t, common.Unmarshal(data, &payload))
  85. require.Len(t, payload.Content, 5)
  86. require.Equal(t, "text", payload.Content[0].Type)
  87. require.Equal(t, "开场:海边日落", payload.Content[0].Text)
  88. require.Equal(t, "image_url", payload.Content[1].Type)
  89. require.Equal(t, "asset://img1", payload.Content[1].ImageURL.URL)
  90. require.Equal(t, "text", payload.Content[2].Type)
  91. require.Equal(t, "中段:车身环绕特写", payload.Content[2].Text)
  92. require.Equal(t, "video_url", payload.Content[3].Type)
  93. require.Equal(t, "asset://vid1", payload.Content[3].VideoURL.URL)
  94. require.Equal(t, "text", payload.Content[4].Type)
  95. require.Equal(t, "结尾:尾灯点亮", payload.Content[4].Text)
  96. }
  97. func TestConvertToRequestPayloadPreservesNativeContentTextOrder(t *testing.T) {
  98. adaptor := &TaskAdaptor{}
  99. req := &relaycommon.TaskSubmitReq{
  100. Model: "doubao-seedance-2-0-260128",
  101. Prompt: "first prompt\nsecond prompt\nthird prompt",
  102. Metadata: map[string]interface{}{
  103. "content": []interface{}{
  104. map[string]interface{}{"type": "text", "text": "first prompt"},
  105. map[string]interface{}{"type": "image_url", "image_url": map[string]interface{}{"url": "asset://img1"}, "role": "reference_image"},
  106. map[string]interface{}{"type": "text", "text": "second prompt"},
  107. map[string]interface{}{"type": "video_url", "video_url": map[string]interface{}{"url": "asset://vid1"}, "role": "reference_video"},
  108. map[string]interface{}{"type": "text", "text": "third prompt"},
  109. },
  110. },
  111. }
  112. body, err := adaptor.convertToRequestPayload(req)
  113. require.NoError(t, err)
  114. require.Len(t, body.Content, 5)
  115. require.Equal(t, "text", body.Content[0].Type)
  116. require.Equal(t, "first prompt", body.Content[0].Text)
  117. require.Equal(t, "image_url", body.Content[1].Type)
  118. require.Equal(t, "text", body.Content[2].Type)
  119. require.Equal(t, "second prompt", body.Content[2].Text)
  120. require.Equal(t, "video_url", body.Content[3].Type)
  121. require.Equal(t, "text", body.Content[4].Type)
  122. require.Equal(t, "third prompt", body.Content[4].Text)
  123. }
  124. func TestConvertToRequestPayloadPassesArkDocumentFields(t *testing.T) {
  125. adaptor := &TaskAdaptor{}
  126. req := &relaycommon.TaskSubmitReq{
  127. Model: "doubao-seedance-2-0-260128",
  128. Prompt: "documented prompt",
  129. Metadata: map[string]interface{}{
  130. "content": []interface{}{
  131. map[string]interface{}{"type": "text", "text": "documented prompt"},
  132. map[string]interface{}{"type": "image_url", "image_url": map[string]interface{}{"url": "asset://image-1"}, "role": "reference_image"},
  133. map[string]interface{}{"type": "video_url", "video_url": map[string]interface{}{"url": "asset://video-1"}, "role": "reference_video"},
  134. map[string]interface{}{"type": "audio_url", "audio_url": map[string]interface{}{"url": "asset://audio-1"}, "role": "reference_audio"},
  135. map[string]interface{}{"type": "draft_task", "draft_task": map[string]interface{}{"id": "cgt-draft"}},
  136. },
  137. "callback_url": "https://example.test/callback",
  138. "return_last_frame": true,
  139. "service_tier": "default",
  140. "execution_expires_after": float64(3600),
  141. "generate_audio": false,
  142. "draft": true,
  143. "tools": []interface{}{map[string]interface{}{"type": "web_search"}},
  144. "safety_identifier": "user-hash-1",
  145. "priority": float64(5),
  146. "resolution": "480p",
  147. "ratio": "1:1",
  148. "duration": float64(5),
  149. "frames": float64(29),
  150. "seed": float64(11),
  151. "camera_fixed": false,
  152. "watermark": true,
  153. },
  154. }
  155. body, err := adaptor.convertToRequestPayload(req)
  156. require.NoError(t, err)
  157. data, err := common.Marshal(body)
  158. require.NoError(t, err)
  159. jsonBody := string(data)
  160. require.Contains(t, jsonBody, `"image_url":{"url":"asset://image-1"}`)
  161. require.Contains(t, jsonBody, `"video_url":{"url":"asset://video-1"}`)
  162. require.Contains(t, jsonBody, `"audio_url":{"url":"asset://audio-1"}`)
  163. require.Contains(t, jsonBody, `"draft_task":{"id":"cgt-draft"}`)
  164. require.Contains(t, jsonBody, `"callback_url":"https://example.test/callback"`)
  165. require.Contains(t, jsonBody, `"return_last_frame":true`)
  166. require.Contains(t, jsonBody, `"service_tier":"default"`)
  167. require.Contains(t, jsonBody, `"execution_expires_after":3600`)
  168. require.Contains(t, jsonBody, `"generate_audio":false`)
  169. require.Contains(t, jsonBody, `"draft":true`)
  170. require.Contains(t, jsonBody, `"tools":[{"type":"web_search"}]`)
  171. require.Contains(t, jsonBody, `"safety_identifier":"user-hash-1"`)
  172. require.Contains(t, jsonBody, `"priority":5`)
  173. require.Contains(t, jsonBody, `"resolution":"480p"`)
  174. require.Contains(t, jsonBody, `"ratio":"1:1"`)
  175. require.Contains(t, jsonBody, `"duration":5`)
  176. require.Contains(t, jsonBody, `"frames":29`)
  177. require.Contains(t, jsonBody, `"seed":11`)
  178. require.Contains(t, jsonBody, `"camera_fixed":false`)
  179. require.Contains(t, jsonBody, `"watermark":true`)
  180. }
  181. func TestDoResponseRewritesIDToPublicTaskID(t *testing.T) {
  182. adaptor := &TaskAdaptor{}
  183. w := httptest.NewRecorder()
  184. c, _ := gin.CreateTestContext(w)
  185. info := &relaycommon.RelayInfo{
  186. OriginModelName: "doubao-seedance-2-0-260128",
  187. TaskRelayInfo: &relaycommon.TaskRelayInfo{PublicTaskID: "task_public"},
  188. }
  189. resp := &http.Response{
  190. StatusCode: http.StatusOK,
  191. Body: io.NopCloser(strings.NewReader(`{
  192. "id":"cgt-upstream",
  193. "aiping_id":"internal-aiping-id",
  194. "status":"queued",
  195. "created_at":1781496040,
  196. "model":"doubao-seedance-2-0-260128"
  197. }`)),
  198. }
  199. upstreamID, data, taskErr := adaptor.DoResponse(c, resp, info)
  200. require.Nil(t, taskErr)
  201. require.Equal(t, "cgt-upstream", upstreamID)
  202. require.Contains(t, string(data), `"id":"cgt-upstream"`)
  203. require.Equal(t, http.StatusOK, w.Code)
  204. require.Contains(t, w.Body.String(), `"id":"task_public"`)
  205. require.NotContains(t, w.Body.String(), "aiping_id")
  206. require.Contains(t, w.Body.String(), `"status":"queued"`)
  207. require.Contains(t, w.Body.String(), `"model":"doubao-seedance-2-0-260128"`)
  208. }
  209. func TestParseTaskResultTreatsArkTerminalStatusesAsFinished(t *testing.T) {
  210. adaptor := &TaskAdaptor{}
  211. cases := []struct {
  212. name string
  213. body string
  214. wantReason string
  215. }{
  216. {
  217. name: "expired",
  218. body: `{"id":"cgt-expired","status":"expired","error":{"code":"Expired","message":"task expired"}}`,
  219. wantReason: "task expired",
  220. },
  221. {
  222. name: "cancelled",
  223. body: `{"id":"cgt-cancelled","status":"cancelled","error":{"code":"Cancelled","message":"task cancelled"}}`,
  224. wantReason: "task cancelled",
  225. },
  226. }
  227. for _, tc := range cases {
  228. t.Run(tc.name, func(t *testing.T) {
  229. got, err := adaptor.ParseTaskResult([]byte(tc.body))
  230. require.NoError(t, err)
  231. require.Equal(t, model.TaskStatusFailure, got.Status)
  232. require.Equal(t, "100%", got.Progress)
  233. require.Equal(t, tc.wantReason, got.Reason)
  234. })
  235. }
  236. }
  237. func newTaskRequestContext(t *testing.T, body string) *gin.Context {
  238. t.Helper()
  239. w := httptest.NewRecorder()
  240. c, _ := gin.CreateTestContext(w)
  241. c.Request = httptest.NewRequest(http.MethodPost, "/api/v3/contents/generations/tasks", strings.NewReader(body))
  242. c.Request.Header.Set("Content-Type", "application/json")
  243. var req relaycommon.TaskSubmitReq
  244. require.NoError(t, common.Unmarshal([]byte(body), &req))
  245. relaycommon.StoreTaskRequest(c, &relaycommon.RelayInfo{}, constant.TaskActionGenerate, req)
  246. return c
  247. }