From c2352fd9b6ccb879d58fe89c47ad9dd347f02e13 Mon Sep 17 00:00:00 2001 From: ddCat <2695092045@qq.com> Date: Mon, 17 Aug 2026 12:56:51 +0800 Subject: [PATCH] =?UTF-8?q?fix(models):=20=E6=A8=A1=E5=9E=8B=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=20-=20=E4=BF=9D=E7=95=99=E4=BA=BA=E5=B7=A5=E8=83=BD?= =?UTF-8?q?=E5=8A=9B=E5=B9=B6=E6=8C=89=E5=A3=B0=E6=98=8E=E5=8F=91=E9=80=81?= =?UTF-8?q?=E8=A7=86=E9=A2=91=E5=88=86=E8=BE=A8=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/service/model_capability.go | 19 ++++- .../internal/service/model_capability_test.go | 29 +++++++ backend/internal/service/provider.go | 5 +- .../service/provider_request_types.go | 2 +- backend/internal/service/provider_test.go | 50 +++++++++++- .../components/model-capability-editor.tsx | 9 ++- web/src/lib/channel-model-catalog.ts | 81 ++++++++++++------- web/src/lib/model-capabilities.ts | 12 ++- web/src/lib/model-protocols.ts | 6 +- web/src/services/api/video.ts | 9 ++- web/test/channel-model-catalog.test.ts | 44 ++++++++++ 11 files changed, 218 insertions(+), 48 deletions(-) diff --git a/backend/internal/service/model_capability.go b/backend/internal/service/model_capability.go index c90eaed94..a45ade68a 100644 --- a/backend/internal/service/model_capability.go +++ b/backend/internal/service/model_capability.go @@ -67,6 +67,7 @@ type VideoCapabilityConfig struct { type VideoReferenceConfig struct { PromptMaxChars int `json:"promptMaxChars"` + MinImages int `json:"minImages"` MaxImages int `json:"maxImages"` MaxImageBytes int64 `json:"maxImageBytes"` MaxVideos int `json:"maxVideos"` @@ -154,7 +155,7 @@ func defaultImageSizeValues() []string { func DefaultModelCapabilityConfigForModel(protocol string, modelName string) *ModelCapabilityConfig { video := &VideoCapabilityConfig{ - References: VideoReferenceConfig{PromptMaxChars: 1000, MaxImages: 9, MaxImageBytes: 30 * 1024 * 1024, MaxVideos: 0, MaxVideoBytes: 0, MaxVideoDuration: 0, MaxAudios: 0, MaxAudioBytes: 0, MaxAudioDuration: 0}, + References: VideoReferenceConfig{PromptMaxChars: 1000, MinImages: 0, MaxImages: 9, MaxImageBytes: 30 * 1024 * 1024, MaxVideos: 0, MaxVideoBytes: 0, MaxVideoDuration: 0, MaxAudios: 0, MaxAudioBytes: 0, MaxAudioDuration: 0}, Duration: VideoDurationConfig{Selection: "range", Min: 1, Max: 15, Step: 1, Default: 6}, Ratios: []string{"16:9", "9:16", "1:1", "4:3", "3:4", "21:9"}, DefaultRatio: "16:9", @@ -277,11 +278,14 @@ func validateVideoCapabilityConfig(value *VideoCapabilityConfig) error { if value.References.PromptMaxChars < 1 || value.References.PromptMaxChars > 1000000 { return BadAuthRequest("提示词最大字符数必须在 1-1000000 之间") } - for name, number := range map[string]int{"最大图片引用数": value.References.MaxImages, "最大视频引用数": value.References.MaxVideos, "最大音频引用数": value.References.MaxAudios} { + for name, number := range map[string]int{"最少图片引用数": value.References.MinImages, "最大图片引用数": value.References.MaxImages, "最大视频引用数": value.References.MaxVideos, "最大音频引用数": value.References.MaxAudios} { if number < 0 || number > 100 { return BadAuthRequest(name + "必须在 0-100 之间") } } + if value.References.MinImages > value.References.MaxImages { + return BadAuthRequest("最少图片引用数不能超过最大图片引用数") + } if value.References.MaxImageBytes < 0 || value.References.MaxVideoBytes < 0 || value.References.MaxAudioBytes < 0 || value.References.MaxVideoDuration < 0 || value.References.MaxAudioDuration < 0 { return BadAuthRequest("引用素材限制不能小于 0") } @@ -291,8 +295,12 @@ func validateVideoCapabilityConfig(value *VideoCapabilityConfig) error { if len(value.Ratios) == 0 || strings.TrimSpace(value.DefaultRatio) == "" || !containsCapabilityString(value.Ratios, value.DefaultRatio) { return BadAuthRequest("请至少配置一个画面比例,并选择默认比例") } - if len(value.Resolutions) == 0 || strings.TrimSpace(value.DefaultResolution) == "" || !containsCapabilityString(value.Resolutions, value.DefaultResolution) { - return BadAuthRequest("请至少配置一个输出分辨率,并选择默认分辨率") + if len(value.Resolutions) == 0 { + if strings.TrimSpace(value.DefaultResolution) != "" { + return BadAuthRequest("未配置输出分辨率时不能设置默认分辨率") + } + } else if strings.TrimSpace(value.DefaultResolution) == "" || !containsCapabilityString(value.Resolutions, value.DefaultResolution) { + return BadAuthRequest("默认输出分辨率必须属于支持值") } if len(value.Operations) == 0 || strings.TrimSpace(value.DefaultOperation) == "" || !containsCapabilityString(value.Operations, value.DefaultOperation) { return BadAuthRequest("请至少配置一个生成模式,并选择默认模式") @@ -377,6 +385,9 @@ func validateVideoTask(profile *VideoCapabilityConfig, input canvasGenerationInp if len(input.ReferenceImages) > profile.References.MaxImages || len(input.ReferenceVideos) > profile.References.MaxVideos || len(input.ReferenceAudios) > profile.References.MaxAudios { return BadAuthRequest("参考素材数量超过当前模型限制") } + if len(input.ReferenceImages) < profile.References.MinImages { + return BadAuthRequest(fmt.Sprintf("当前视频模型至少需要 %d 张参考图", profile.References.MinImages)) + } for _, media := range input.ReferenceImages { if profile.References.MaxImageBytes > 0 && media.Bytes > profile.References.MaxImageBytes { return BadAuthRequest("参考图片文件超过当前模型大小限制") diff --git a/backend/internal/service/model_capability_test.go b/backend/internal/service/model_capability_test.go index 7ac821b8f..852e047e0 100644 --- a/backend/internal/service/model_capability_test.go +++ b/backend/internal/service/model_capability_test.go @@ -116,3 +116,32 @@ func TestValidateVideoTaskIgnoresGlobalResolutionWhenCatalogDeclaresNone(t *test t.Fatalf("validateVideoTask() error = %v", err) } } + +func TestNormalizeVideoCapabilityAllowsOmittedResolution(t *testing.T) { + profile := DefaultModelCapabilityConfigForModel("newapi-channel-2", "endpoint-video").Video + profile.Resolutions = nil + profile.DefaultResolution = "" + + result, err := NormalizeModelCapabilityConfig("video", "newapi-channel-2", &ModelCapabilityConfig{Version: 1, Video: profile}) + if err != nil { + t.Fatalf("NormalizeModelCapabilityConfig() error = %v", err) + } + if result.Video == nil || len(result.Video.Resolutions) != 0 || result.Video.DefaultResolution != "" { + t.Fatalf("normalized video resolution = %#v", result.Video) + } +} + +func TestValidateVideoTaskRequiresDeclaredMinimumImages(t *testing.T) { + profile := DefaultModelCapabilityConfigForModel("newapi-channel-2", "image-required-video").Video + profile.References.MinImages = 1 + profile.References.MaxImages = 2 + profile.Operations = []string{"image_to_video"} + profile.DefaultOperation = "image_to_video" + + err := validateVideoTask(profile, canvasGenerationInput{ + Config: providerConfig{Model: "image-required-video", VideoSeconds: "6", Size: "16:9", VQuality: "720"}, + }) + if err == nil || !strings.Contains(err.Error(), "至少需要 1 张参考图") { + t.Fatalf("validateVideoTask() error = %v", err) + } +} diff --git a/backend/internal/service/provider.go b/backend/internal/service/provider.go index 2449a93e8..46b9cd6cb 100644 --- a/backend/internal/service/provider.go +++ b/backend/internal/service/provider.go @@ -1973,7 +1973,10 @@ func newAPIChannel2VideoRequestBody(input canvasGenerationInput) (newAPIVideoReq seconds = 6 } ratio := normalizeNewAPIChannel2Ratio(input.Config.Size, modelName) - resolution := normalizeNewAPIChannel2Resolution(input.Config.VQuality, modelName) + resolution := videoResolutionNameRequest(input.VideoCapability, input.Config.VQuality) + if modelName == "grok-video-1.5-1080p" { + resolution = "1080p" + } body := newAPIVideoRequest{ Model: input.Config.Model, Prompt: strings.TrimSpace(input.Prompt), diff --git a/backend/internal/service/provider_request_types.go b/backend/internal/service/provider_request_types.go index 91e4e996d..a646018a0 100644 --- a/backend/internal/service/provider_request_types.go +++ b/backend/internal/service/provider_request_types.go @@ -19,7 +19,7 @@ type newAPIVideoRequest struct { Prompt string `json:"prompt"` Seconds string `json:"seconds"` AspectRatio string `json:"aspect_ratio"` - Resolution string `json:"resolution"` + Resolution string `json:"resolution,omitempty"` GenerateAudio *bool `json:"generate_audio,omitempty"` ImageURLs []string `json:"image_urls,omitempty"` VideoURLs []string `json:"video_urls,omitempty"` diff --git a/backend/internal/service/provider_test.go b/backend/internal/service/provider_test.go index 0e6b5bb4e..981843ea8 100644 --- a/backend/internal/service/provider_test.go +++ b/backend/internal/service/provider_test.go @@ -1278,9 +1278,12 @@ func TestRunNewAPIChannel2VideoTaskDownloadsTemporaryResult(t *testing.T) { })) defer server.Close() + profile := DefaultModelCapabilityConfigForModel("newapi-channel-2", "grok-image-video").Video + profile.Resolutions = []string{"720p"} + profile.DefaultResolution = "720p" result, err := runVideoTask(context.Background(), canvasGenerationInput{ Prompt: "make it move", - Config: providerConfig{BaseURL: server.URL, APIKey: "test-key", Model: "grok-image-video", InterfaceType: "newapi-channel-2", VideoSeconds: "15", Size: "720x1280", VQuality: "high"}, + Config: providerConfig{BaseURL: server.URL, APIKey: "test-key", Model: "grok-image-video", InterfaceType: "newapi-channel-2", VideoSeconds: "15", Size: "720x1280", VQuality: "720"}, ReferenceImages: []providerMedia{ {ID: "image-1", DataURL: testReferenceImageDataURL}, {ID: "image-2", DataURL: testReferenceImageDataURL}, @@ -1288,6 +1291,7 @@ func TestRunNewAPIChannel2VideoTaskDownloadsTemporaryResult(t *testing.T) { ReferenceVideos: []providerMedia{{ID: "video-1", URL: server.URL + "/reference.mp4"}}, ReferenceAudios: []providerMedia{{ID: "audio-1", URL: server.URL + "/reference.mp3"}}, Metadata: map[string]interface{}{"videoEditOperation": "image_to_video"}, + VideoCapability: profile, }) if err != nil { t.Fatalf("runVideoTask() error = %v", err) @@ -1354,6 +1358,50 @@ func TestNewAPIChannel2SingleImageModelsRequireOneReference(t *testing.T) { } } +func TestNewAPIChannel2SendsOnlyDeclaredResolution(t *testing.T) { + tests := []struct { + name string + model string + quality string + resolutions []string + want string + }{ + {name: "catalog omits resolution", model: "endpoint-video", quality: "720"}, + {name: "declared 2K alias", model: "declared-video", quality: "2K", resolutions: []string{"1440p"}, want: "1440p"}, + {name: "fixed grok 1080p model", model: "grok-video-1.5-1080p", quality: "720", want: "1080p"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + profile := DefaultModelCapabilityConfigForModel("newapi-channel-2", test.model).Video + profile.Resolutions = test.resolutions + profile.DefaultResolution = "" + input := canvasGenerationInput{ + Config: providerConfig{Model: test.model, VideoSeconds: "6", Size: "16:9", VQuality: test.quality}, + VideoCapability: profile, + } + if strings.HasPrefix(test.model, "grok-video-1.5") { + input.ReferenceImages = []providerMedia{{ID: "image-1", DataURL: testReferenceImageDataURL}} + } + + body, err := newAPIChannel2VideoRequestBody(input) + if err != nil { + t.Fatalf("newAPIChannel2VideoRequestBody() error = %v", err) + } + if body.Resolution != test.want { + t.Fatalf("resolution = %q, want %q", body.Resolution, test.want) + } + mapped, err := requestAsMap(body) + if err != nil { + t.Fatalf("requestAsMap() error = %v", err) + } + _, hasResolution := mapped["resolution"] + if hasResolution != (test.want != "") { + t.Fatalf("resolution presence = %v, want %v; body = %#v", hasResolution, test.want != "", mapped) + } + }) + } +} + func TestNewAPIChannel2RejectsAudioWithoutReferenceVideo(t *testing.T) { _, err := newAPIChannel2VideoRequestBody(canvasGenerationInput{ Config: providerConfig{Model: "grok-image-video", VideoSeconds: "6"}, diff --git a/web/src/components/model-capability-editor.tsx b/web/src/components/model-capability-editor.tsx index 8186a2963..5f44eef3d 100644 --- a/web/src/components/model-capability-editor.tsx +++ b/web/src/components/model-capability-editor.tsx @@ -47,7 +47,8 @@ export function ModelCapabilityEditor({ value, onChange, protocol, capability =
updateReferences({ promptMaxChars: value || 1 })} /> - updateReferences({ maxImages: value || 0 })} /> + updateReferences({ minImages: value || 0 })} /> + { const maxImages = value || 0; updateReferences({ maxImages, minImages: Math.min(profile.references.minImages, maxImages) }); }} /> updateReferences({ maxImageBytes: mbToBytes(value) })} /> updateReferences({ maxVideos: value || 0 })} /> updateReferences({ maxVideoBytes: mbToBytes(value) })} /> @@ -76,7 +77,7 @@ export function ModelCapabilityEditor({ value, onChange, protocol, capability =
({ label: item, value: item }))} onChange={(defaultRatio) => update({ defaultRatio })} /> - ({ label: item.toUpperCase(), value: item }))} onChange={(resolutions) => update({ resolutions, defaultResolution: resolutions.includes(profile.defaultResolution) ? profile.defaultResolution : resolutions[0] || "" })} />