fix(agent): 图片尺寸 - 修复比例 preset 映射

This commit is contained in:
ddcat666
2026-10-14 09:17:42 +08:00
parent 7ce07a4c04
commit 6a118ae93b
5 changed files with 119 additions and 3 deletions

View File

@@ -262,6 +262,36 @@ func imageParameterValue(p protocol.ImageParameter, value any) any {
return value
}
func imageParameterUnsupportedMessage(p protocol.ImageParameter) string {
message := "图片参数超出支持范围:" + p.Label
if len(p.Values) > 0 {
values := make([]string, 0, min(len(p.Values), 12))
for _, value := range p.Values[:min(len(p.Values), 12)] {
values = append(values, fmt.Sprint(value))
}
message += ";支持值:" + strings.Join(values, "、")
if len(p.Values) > len(values) {
message += "等"
}
} else if p.Min != nil || p.Max != nil {
message += fmt.Sprintf(";范围:%s-%s", formatImageParameterBound(p.Min), formatImageParameterBound(p.Max))
}
if p.Default != nil {
message += ";默认值:" + fmt.Sprint(p.Default)
}
if p.Binding == "aspectRatio" && p.Encoding == "pixels" {
message += ";若使用比例,按 model_list 的 imageSize.presets 选择对应像素尺寸"
}
return message
}
func formatImageParameterBound(value *float64) string {
if value == nil {
return "不限"
}
return strconv.FormatFloat(*value, 'f', -1, 64)
}
func imageParameterAdmissionError(field, issue, message string) error {
return NewModelError(ErrCodeModelCapabilityNotSupported, message).WithDetails(map[string]any{
"field": field,
@@ -285,7 +315,7 @@ func resolveImageParameterValues(parameters []protocol.ImageParameter, values ma
}
value = imageParameterValue(p, value)
if !p.Accepts(value) {
return nil, imageParameterAdmissionError(p.Name, "unsupported_value", "图片参数超出支持范围:"+p.Label)
return nil, imageParameterAdmissionError(p.Name, "unsupported_value", imageParameterUnsupportedMessage(p))
}
if value != nil && value != "" {
result[name] = value

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
@@ -35,6 +36,36 @@ func imageParameterTestAdapter(t *testing.T, packageID, providerID string) proto
return nil
}
func TestImageParameterRatioUsesDeclaredPixelPreset(t *testing.T) {
profile := DefaultModelCapabilityConfigForModel("openai-image", "GPT Image 2.5")
profile.Image.Parameters = []protocol.ImageParameter{{
Name: "size", Label: "画幅 / 尺寸", Type: "string",
Values: []any{"auto", "1824x1024", "1024x1824", "1024x1024"}, Default: "1824x1024",
Binding: "aspectRatio", Encoding: "pixels", Pricing: true,
}}
profile.Image.Size = ImageSizeConfig{
Parameter: "size", Values: []string{"auto", "1824x1024", "1024x1824", "1024x1024"}, Default: "1824x1024",
Presets: []ImageSizePreset{
{Tier: "1k", Ratio: "16:9", Size: "1824x1024"},
{Tier: "1k", Ratio: "9:16", Size: "1024x1824"},
{Tier: "1k", Ratio: "1:1", Size: "1024x1024"},
},
}
if got := imageSizeValueForDeclaredPreset(profile.Image, "16:9", ""); got != "1824x1024" {
t.Fatalf("ratio was not encoded by the declared preset: %q", got)
}
if got := imageSizeValueForDeclaredPreset(profile.Image, "1024x1824", ""); got != "1024x1824" {
t.Fatalf("pixel preset was not preserved: %q", got)
}
values := map[string]any{"size": imageSizeValueForDeclaredPreset(profile.Image, "16:9", "")}
if _, err := resolveImageParameterValues(profile.Image.Parameters, values); err != nil {
t.Fatalf("encoded preset was rejected by image parameter admission: %v", err)
}
if _, err := resolveImageParameterValues(profile.Image.Parameters, map[string]any{"size": "16:9"}); err == nil || !strings.Contains(err.Error(), "支持值") || !strings.Contains(err.Error(), "imageSize.presets") {
t.Fatalf("unsupported ratio error lacks actionable correction guidance: %v", err)
}
}
func TestImageParametersResolutionOnlyAndLogicalReconciliation(t *testing.T) {
a := imageParameterTestAdapter(t, "google-gemini-image", "gemini-image")
registry, err := protocol.NewRegistry(a)

View File

@@ -490,6 +490,20 @@ func validateImagePresetSelection(profile *ImageCapabilityConfig, quality, ratio
return newModelCapabilityArgumentError("size", "unsupported_value", BadAuthRequest("当前分辨率不支持所选图片宽高比"))
}
func imageSizeValueForDeclaredPreset(profile *ImageCapabilityConfig, size string, quality string) string {
if profile == nil {
return ""
}
preset, ok := declaredImageSizePreset(profile.Size.Presets, size, quality, imagePresetDefaultTier(profile))
if !ok {
return ""
}
if profile.Size.Parameter == "size" {
return strings.TrimSpace(preset.Size)
}
return normalizeDeclaredImageRatio(preset.Ratio)
}
// imageSizeRatioForDeclaredPreset 把已声明的 preset 像素值还原成它对应的比例。比例型线路
// 会把画布比例换算成 preset 像素值(OpenAI Images 只收 WIDTHxHEIGHT),换算结果必须继续
// 通过同一套尺寸与分辨率校验。只认当前档位的 preset,避免用低档价格取到高档像素。

View File

@@ -533,8 +533,8 @@ func (s *Service) resolveSystemChannelModelSelection(input map[string]any, taskT
}
if value, ok := values["size"]; ok && capabilitySpec != nil && capabilityImageSizePresetMatches(*capabilitySpec, value, values) {
values = maps.Clone(values)
if ratio := imageSizeRatioForDeclaredPreset(capabilityConfig.Image, fmt.Sprint(value), fmt.Sprint(values["imageResolution"])); ratio != "" {
values["size"] = ratio
if encoded := imageSizeValueForDeclaredPreset(capabilityConfig.Image, fmt.Sprint(value), stringValue(values["imageResolution"])); encoded != "" {
values["size"] = encoded
}
}
resolved, parameterErr := resolveImageParameterValues(parameters, values)

View File

@@ -6,6 +6,7 @@ import (
"testing"
"yingce/backend/internal/model"
"yingce/backend/internal/protocol"
"yingce/backend/internal/repository"
"gorm.io/driver/sqlite"
@@ -475,6 +476,46 @@ func TestImageResolutionPricingOnSystemChannel(t *testing.T) {
}
}
func TestParameterizedOpenAIAspectPresetSystemChannelPersistsPixelExecutionSize(t *testing.T) {
profile := DefaultModelCapabilityConfigForModel(string(model.ChannelInterfaceOpenAIImage), "GPT Image 2.5")
profile.Image.Parameters = []protocol.ImageParameter{{
Name: "size", Label: "画幅 / 尺寸", Type: "string",
Values: []any{"auto", "1824x1024", "1024x1824", "1024x1024"}, Default: "1824x1024",
Pricing: true, Binding: "aspectRatio", Encoding: "pixels",
}}
profile.Image.Size = ImageSizeConfig{
Parameter: "size", Values: []string{"auto", "1824x1024", "1024x1824", "1024x1024"}, Default: "1824x1024",
Presets: []ImageSizePreset{
{Tier: "1k", Ratio: "16:9", Size: "1824x1024", Width: 1824, Height: 1024},
{Tier: "1k", Ratio: "9:16", Size: "1024x1824", Width: 1024, Height: 1824},
{Tier: "1k", Ratio: "1:1", Size: "1024x1024", Width: 1024, Height: 1024},
},
}
if err := projectImageParameters(profile.Image); err != nil {
t.Fatal(err)
}
svc, _, channel, channelModel := createSystemChannelSelectionFixture(t, "image", model.ChannelInterfaceOpenAIImage, profile, []model.ChannelModelPriceTier{
newSelectionPriceTier("tier-1k", `{"size":"1824x1024"}`, "provider-1k", "fixed_request"),
})
input := map[string]any{
"mode": "image",
"config": map[string]any{
"channelId": channel.ID,
"model": channelModel.ModelKey,
"imageParameters": map[string]any{"size": "16:9"},
},
}
resolved, err := svc.resolveSystemChannelModelSelection(input, "canvas_image", "")
if err != nil {
t.Fatalf("resolve error = %v", err)
}
config := resolved["config"].(map[string]any)
parameters := config["imageParameters"].(map[string]any)
if parameters["size"] != "1824x1024" || config["size"] != "1824x1024" {
t.Fatalf("parameterized size = %#v, config = %#v", parameters["size"], config["size"])
}
}
func TestOpenAIAspectPresetSystemChannelPersistsPixelExecutionSize(t *testing.T) {
profile := DefaultModelCapabilityConfigForModel(string(model.ChannelInterfaceOpenAIImage), "gpt-image-2.5-4k")
profile.Image.Size = ImageSizeConfig{