fix(agent): 图片尺寸 - 修复比例 preset 映射
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,避免用低档价格取到高档像素。
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user