From b2d43b6c293887fab204f132cbe1f180a53a94d5 Mon Sep 17 00:00:00 2001 From: fengsilin Date: Wed, 19 Aug 2026 17:21:54 +0800 Subject: [PATCH] fix(doubao-video): parse camelCase fields from Volcengine task responses The taskdoubao adaptor expected snake_case JSON keys (video_url, completion_tokens, total_tokens, framespersecond, service_tier, created_at, updated_at) but Volcengine returns camelCase (videoUrl, completionTokens, totalTokens, framesPerSecond, serviceTier, createdAt, updatedAt). As a result the video URL was never parsed: successful tasks fell back to the platform proxy URL (BuildProxyURL) as result_url, which broke the video proxy in containerized deployments (localhost:port not reachable inside the container) and zeroed the usage tokens used for billing. Align the responseTask JSON tags with the real Volcengine response shape and add a regression test with the exact production payload. Co-Authored-By: ZCode --- relay/channel/task/doubao/adaptor.go | 14 ++++----- relay/channel/task/doubao/adaptor_test.go | 38 +++++++++++++++++++++-- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/relay/channel/task/doubao/adaptor.go b/relay/channel/task/doubao/adaptor.go index fcb0608..b44f5a2 100644 --- a/relay/channel/task/doubao/adaptor.go +++ b/relay/channel/task/doubao/adaptor.go @@ -71,24 +71,24 @@ type responseTask struct { Model string `json:"model"` Status string `json:"status"` Content struct { - VideoURL string `json:"video_url"` + VideoURL string `json:"videoUrl"` } `json:"content"` Seed int `json:"seed"` Resolution string `json:"resolution"` Duration int `json:"duration"` Ratio string `json:"ratio"` - FramesPerSecond int `json:"framespersecond"` - ServiceTier string `json:"service_tier"` + FramesPerSecond int `json:"framesPerSecond"` + ServiceTier string `json:"serviceTier"` Usage struct { - CompletionTokens int `json:"completion_tokens"` - TotalTokens int `json:"total_tokens"` + CompletionTokens int `json:"completionTokens"` + TotalTokens int `json:"totalTokens"` } `json:"usage"` Error struct { Code string `json:"code"` Message string `json:"message"` } `json:"error"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + CreatedAt int64 `json:"createdAt"` + UpdatedAt int64 `json:"updatedAt"` } // ============================ diff --git a/relay/channel/task/doubao/adaptor_test.go b/relay/channel/task/doubao/adaptor_test.go index 9353818..8a7938e 100644 --- a/relay/channel/task/doubao/adaptor_test.go +++ b/relay/channel/task/doubao/adaptor_test.go @@ -13,8 +13,8 @@ func TestParseTaskResult_SuccessWithUsage(t *testing.T) { adaptor := &TaskAdaptor{} taskInfo, err := adaptor.ParseTaskResult([]byte(`{ "status":"succeeded", - "content":{"video_url":"https://example.test/video.mp4"}, - "usage":{"completion_tokens":12,"total_tokens":3456} + "content":{"videoUrl":"https://example.test/video.mp4"}, + "usage":{"completionTokens":12,"totalTokens":3456} }`)) require.NoError(t, err) @@ -28,7 +28,7 @@ func TestParseTaskResult_SuccessWithoutUsage(t *testing.T) { adaptor := &TaskAdaptor{} taskInfo, err := adaptor.ParseTaskResult([]byte(`{ "status":"succeeded", - "content":{"video_url":"https://example.test/video.mp4"} + "content":{"videoUrl":"https://example.test/video.mp4"} }`)) require.NoError(t, err) @@ -241,6 +241,38 @@ func TestConvertToOpenAIVideo_FailedUsesUpstreamError(t *testing.T) { require.Contains(t, string(data), `"code":"InvalidParameter"`) } +// Parses the exact Volcengine response shape observed in production: +// camelCase videoUrl / usage tokens, not snake_case. +func TestParseTaskResult_RealVolcengineResponse(t *testing.T) { + adaptor := &TaskAdaptor{} + taskInfo, err := adaptor.ParseTaskResult([]byte(`{ + "id":"cgt-20260819170015-glgnd", + "seed":46303, + "draft":false, + "model":"doubao-seedance-2-0-260128", + "ratio":"16:9", + "usage":{"totalTokens":216900,"completionTokens":216900}, + "status":"succeeded", + "content":{"videoUrl":"https://ark-acg-cn-beijing.tos-cn-beijing.volces.com/output.mp4"}, + "duration":10, + "priority":0, + "createdAt":1787130015, + "updatedAt":1787130344, + "resolution":"720p", + "serviceTier":"default", + "outputFormat":"mp4", + "generateAudio":false, + "framesPerSecond":24, + "executionExpiresAfter":172800 + }`)) + + require.NoError(t, err) + require.Equal(t, string(model.TaskStatusSuccess), taskInfo.Status) + require.Equal(t, "https://ark-acg-cn-beijing.tos-cn-beijing.volces.com/output.mp4", taskInfo.Url) + require.Equal(t, 216900, taskInfo.CompletionTokens) + require.Equal(t, 216900, taskInfo.TotalTokens) +} + func TestGetModelList_IncludesSeedance20Models(t *testing.T) { adaptor := &TaskAdaptor{} models := adaptor.GetModelList()