Merge pull request #22 from CyrusAuyeung/agent/fix-xai-video-payload

fix(media): xAI 视频 - 使用官方生成请求字段
This commit is contained in:
ddcat666
2026-07-22 23:24:22 +08:00
committed by GitHub
2 changed files with 149 additions and 0 deletions

View File

@@ -993,6 +993,10 @@ func validateGenerationInterface(mode string, interfaceType string) error {
}
func grokVideoBody(input canvasGenerationInput) (map[string]interface{}, error) {
if input.Config.InterfaceType == "xai-video" {
return xaiVideoBody(input)
}
seconds := defaultString(input.Config.VideoSeconds, "6")
duration, err := strconv.Atoi(seconds)
if err != nil || duration <= 0 {
@@ -1022,6 +1026,29 @@ func grokVideoBody(input canvasGenerationInput) (map[string]interface{}, error)
return body, nil
}
// xAI 生成接口与 legacy /videos 使用不同字段,保持独立可避免兼容字段触发上游 422。
func xaiVideoBody(input canvasGenerationInput) (map[string]interface{}, error) {
body := map[string]interface{}{
"model": input.Config.Model,
"prompt": strings.TrimSpace(input.Prompt),
"duration": normalizeXAIVideoDuration(input.Config.VideoSeconds),
"aspect_ratio": normalizeXAIVideoAspectRatio(input.Config.Size),
"resolution": normalizeXAIVideoResolution(input.Config.VQuality),
}
if !shouldSendNewAPIVideoImages(input) || len(input.ReferenceImages) == 0 {
return body, nil
}
if len(input.ReferenceImages) > 1 {
return nil, fmt.Errorf("xAI 图生视频只支持 1 张起始图,当前连接了 %d 张", len(input.ReferenceImages))
}
imageURL, err := openAIImageInputURL(input.ReferenceImages[0])
if err != nil {
return nil, err
}
body["image"] = map[string]interface{}{"url": imageURL}
return body, nil
}
func runSeedanceVideosTask(ctx context.Context, input canvasGenerationInput) (map[string]interface{}, error) {
id := resumedProviderRequestID(ctx)
var created map[string]interface{}
@@ -1787,6 +1814,74 @@ func normalizeVideoResolution(value string) string {
return value + "p"
}
func normalizeXAIVideoDuration(value string) int {
duration, err := strconv.Atoi(strings.TrimSpace(value))
if err != nil || duration <= 0 {
return 6
}
if duration > 15 {
return 15
}
return duration
}
func normalizeXAIVideoResolution(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "480", "480p", "low":
return "480p"
case "1080", "1080p":
return "1080p"
default:
return "720p"
}
}
func normalizeXAIVideoAspectRatio(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
allowed := map[string]bool{
"1:1": true, "16:9": true, "9:16": true, "4:3": true,
"3:4": true, "3:2": true, "2:3": true,
}
if allowed[value] {
return value
}
parts := strings.Split(value, "x")
if len(parts) != 2 {
return "16:9"
}
width, widthErr := strconv.Atoi(strings.TrimSpace(parts[0]))
height, heightErr := strconv.Atoi(strings.TrimSpace(parts[1]))
if widthErr != nil || heightErr != nil || width <= 0 || height <= 0 {
return "16:9"
}
ratio := float64(width) / float64(height)
candidates := []struct {
name string
ratio float64
}{
{name: "1:1", ratio: 1},
{name: "16:9", ratio: 16.0 / 9},
{name: "9:16", ratio: 9.0 / 16},
{name: "4:3", ratio: 4.0 / 3},
{name: "3:4", ratio: 3.0 / 4},
{name: "3:2", ratio: 3.0 / 2},
{name: "2:3", ratio: 2.0 / 3},
}
bestName := "16:9"
bestDifference := 2.0
for _, candidate := range candidates {
difference := ratio - candidate.ratio
if difference < 0 {
difference = -difference
}
if difference < bestDifference {
bestName = candidate.name
bestDifference = difference
}
}
return bestName
}
func normalizeSeedanceDuration(value string) int {
if strings.TrimSpace(value) == "-1" {
return -1

View File

@@ -387,6 +387,14 @@ func TestRunVideoTaskUsesXAIVideoGenerationEndpoint(t *testing.T) {
if body["model"] != "grok-imagine-video-1.5" || body["prompt"] != "make it move" {
t.Errorf("request body = %#v", body)
}
if body["duration"] != float64(10) || body["aspect_ratio"] != "1:1" || body["resolution"] != "720p" {
t.Errorf("xAI settings = %#v", body)
}
for _, legacyField := range []string{"seconds", "size", "images"} {
if _, exists := body[legacyField]; exists {
t.Errorf("request body includes legacy field %q: %#v", legacyField, body)
}
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"request_id":"video-1"}`))
case "GET /v1/videos/video-1":
@@ -412,6 +420,8 @@ func TestRunVideoTaskUsesXAIVideoGenerationEndpoint(t *testing.T) {
Model: "grok-imagine-video-1.5",
InterfaceType: "xai-video",
VideoSeconds: "10",
Size: "1:1",
VQuality: "720",
},
})
if err != nil {
@@ -427,6 +437,50 @@ func TestRunVideoTaskUsesXAIVideoGenerationEndpoint(t *testing.T) {
}
}
func TestXAIVideoBodyUsesOfficialImageShapeAndNormalizesSettings(t *testing.T) {
body, err := grokVideoBody(canvasGenerationInput{
Prompt: "make it move",
Config: providerConfig{
Model: "grok-imagine-video-1.5",
InterfaceType: "xai-video",
VideoSeconds: "20",
Size: "1024x1792",
VQuality: "1080",
},
ReferenceImages: []providerMedia{{ID: "image-1", DataURL: testReferenceImageDataURL}},
Metadata: map[string]interface{}{"videoEditOperation": "image_to_video"},
})
if err != nil {
t.Fatalf("grokVideoBody() error = %v", err)
}
if body["duration"] != 15 || body["aspect_ratio"] != "9:16" || body["resolution"] != "1080p" {
t.Fatalf("xAI settings = %#v", body)
}
image, ok := body["image"].(map[string]interface{})
if !ok || image["url"] != testReferenceImageDataURL {
t.Fatalf("image = %#v", body["image"])
}
for _, legacyField := range []string{"seconds", "size", "images"} {
if _, exists := body[legacyField]; exists {
t.Fatalf("body includes legacy field %q: %#v", legacyField, body)
}
}
}
func TestXAIVideoBodyRejectsMultipleStartImages(t *testing.T) {
_, err := grokVideoBody(canvasGenerationInput{
Config: providerConfig{Model: "grok-imagine-video-1.5", InterfaceType: "xai-video"},
ReferenceImages: []providerMedia{
{ID: "image-1", DataURL: testReferenceImageDataURL},
{ID: "image-2", DataURL: testReferenceImageDataURL},
},
Metadata: map[string]interface{}{"videoEditOperation": "image_to_video"},
})
if err == nil || !strings.Contains(err.Error(), "只支持 1 张起始图") {
t.Fatalf("grokVideoBody() error = %v", err)
}
}
func TestNewAPIVideoPromptKeepsTextOnlyPromptUnchanged(t *testing.T) {
input := canvasGenerationInput{
Prompt: "make it move",