fix(media): 规范比例与 OpenAI 图像尺寸参数

统一过滤非 x:x 比例,避免默认值参与候选匹配或透传上游。

支持 xK 分辨率归一化,并按 Gemini、Volces 和 OpenAI 的参数规范生成上游请求;OpenAI 显式 WxH 始终保留用户原始尺寸。

验证:在独立临时 worktree 中执行 apps/api 全量 go test ./... -count=1 通过,真实 OpenAI 请求已到达上游但受本地无效凭据阻断。
This commit is contained in:
2026-07-27 23:50:17 +08:00
parent 039220c835
commit 95776c84f6
11 changed files with 1086 additions and 71 deletions
+132 -37
View File
@@ -3,6 +3,7 @@ package runner
import (
"fmt"
"math"
"strconv"
"strings"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
@@ -13,28 +14,43 @@ type resolutionNormalizeProcessor struct{}
func (resolutionNormalizeProcessor) Name() string { return "ResolutionNormalizeProcessor" }
func (resolutionNormalizeProcessor) ShouldProcess(params map[string]any, modelType string, context *paramProcessContext) bool {
if stringFromAny(params["resolution"]) != "" {
return false
if normalizeMediaResolution(modelType, stringFromAny(params["resolution"])) != "" {
return true
}
size := stringFromAny(params["size"])
if size == "" {
return false
}
return isImageResolution(modelType, size) || isVideoResolution(modelType, size)
return normalizeMediaResolution(modelType, stringFromAny(params["size"])) != ""
}
func (resolutionNormalizeProcessor) Process(params map[string]any, modelType string, context *paramProcessContext) bool {
if resolution := normalizeMediaResolution(modelType, stringFromAny(params["resolution"])); resolution != "" {
before := params["resolution"]
params["resolution"] = resolution
context.resolution = resolution
if stringFromAny(before) != resolution {
_, capabilityValue := capabilityEvidence(context.modelCapability, modelType, "output_resolutions")
context.recordChange(
"ResolutionNormalizeProcessor",
"adjust",
"resolution",
before,
resolution,
"resolution 使用了可识别的媒体分辨率格式,已归一为标准大小写和值。",
capabilityPath(modelType, "output_resolutions"),
capabilityValue,
)
}
return true
}
size := stringFromAny(params["size"])
if stringFromAny(params["resolution"]) == "" && (isImageResolution(modelType, size) || isVideoResolution(modelType, size)) {
if resolution := normalizeMediaResolution(modelType, size); resolution != "" {
_, capabilityValue := capabilityEvidence(context.modelCapability, modelType, "output_resolutions")
params["resolution"] = size
context.resolution = size
params["resolution"] = resolution
context.resolution = resolution
context.recordChange(
"ResolutionNormalizeProcessor",
"set",
"resolution",
nil,
size,
resolution,
"size 使用分辨率格式,归一到 resolution 供后续能力校验和计费使用。",
capabilityPath(modelType, "output_resolutions"),
capabilityValue,
@@ -48,30 +64,69 @@ type aspectRatioProcessor struct{}
func (aspectRatioProcessor) Name() string { return "AspectRatioProcessor" }
func (aspectRatioProcessor) ShouldProcess(params map[string]any, modelType string, context *paramProcessContext) bool {
return modelType != "text_generate" && (stringFromAny(params["aspect_ratio"]) != "" || stringFromAny(params["size"]) != "")
if modelType == "text_generate" {
return false
}
for _, key := range []string{"aspect_ratio", "aspectRatio", "ratio"} {
if _, ok := params[key]; ok {
return true
}
}
return stringFromAny(params["size"]) != ""
}
func (aspectRatioProcessor) Process(params map[string]any, modelType string, context *paramProcessContext) bool {
capability := capabilityForType(context.modelCapability, modelType)
if capability == nil {
aspectRatio, ratioSource, ratioPresent := requestAspectRatio(params)
if ratioPresent {
originalAspectRatio := aspectRatio
normalized, valid := normalizedAspectRatio(aspectRatio)
if !valid {
before := map[string]any{}
for _, key := range []string{"aspect_ratio", "aspectRatio", "ratio"} {
if value, ok := params[key]; ok {
before[key] = cloneAny(value)
delete(params, key)
}
}
aspectRatio = ""
context.aspectRatio = ""
context.recordChange(
"AspectRatioProcessor",
"remove",
ratioSource,
before,
nil,
"比例不是有效的正数 x:x 格式,按未指定比例处理。",
"",
nil,
)
return true
} else {
aspectRatio = normalized
params["aspect_ratio"] = normalized
for _, key := range []string{"aspectRatio", "ratio"} {
delete(params, key)
}
if ratioSource != "aspect_ratio" || normalized != originalAspectRatio {
context.recordChange(
"AspectRatioProcessor",
"adjust",
"aspect_ratio",
map[string]any{ratioSource: originalAspectRatio},
normalized,
"比例已归一为标准 x:x 格式。",
"",
nil,
)
}
}
}
if !ratioPresent {
return true
}
aspectRatio := stringFromAny(params["aspect_ratio"])
if isEmptyParamString(aspectRatio) {
before := params["aspect_ratio"]
delete(params, "aspect_ratio")
context.aspectRatio = ""
context.recordChange(
"AspectRatioProcessor",
"remove",
"aspect_ratio",
before,
nil,
"aspect_ratio 是空值字符串,不能作为有效比例传给上游。",
"",
nil,
)
capability := capabilityForType(context.modelCapability, modelType)
if capability == nil {
return true
}
@@ -174,6 +229,15 @@ func (aspectRatioProcessor) Process(params map[string]any, modelType string, con
return true
}
func requestAspectRatio(params map[string]any) (string, string, bool) {
for _, key := range []string{"aspect_ratio", "aspectRatio", "ratio"} {
if value, ok := params[key]; ok {
return stringFromAny(value), key, true
}
}
return "", "aspect_ratio", false
}
func validateExplicitAspectRatio(aspectRatio string, capability map[string]any, allowed []string, modelType string) (string, bool) {
if isVideoModelType(modelType) && len(allowed) > 0 && !containsString(allowed, aspectRatio) {
if aspectRatio == "adaptive" || aspectRatio == "keep_ratio" {
@@ -194,17 +258,14 @@ func (imageSizeProcessor) ShouldProcess(params map[string]any, modelType string,
if modelType != "image_generate" && modelType != "image_edit" {
return false
}
if _, _, ok := imageDimensionsFromParams(params); !ok {
return false
}
capability := capabilityForType(context.modelCapability, modelType)
return capability != nil && imageSizeCapabilityConfigured(capability)
_, _, ok := imageDimensionsFromParams(params)
return ok
}
func (imageSizeProcessor) Process(params map[string]any, modelType string, context *paramProcessContext) bool {
capability := capabilityForType(context.modelCapability, modelType)
if capability == nil {
return true
capability = map[string]any{}
}
width, height, ok := imageDimensionsFromParams(params)
if !ok {
@@ -222,7 +283,11 @@ func (imageSizeProcessor) Process(params map[string]any, modelType string, conte
params["width"] = width
params["height"] = height
_, _, sizeHasPixelDimensions := parsePixelSizeString(stringFromAny(params["size"]))
if stringFromAny(params["aspect_ratio"]) == "" || sizeHasPixelDimensions {
_, _, resolutionHasPixelDimensions := parsePixelSizeString(stringFromAny(params["resolution"]))
explicitWidthHeight := positiveIntegerFromAny(params["width"]) > 0 && positiveIntegerFromAny(params["height"]) > 0 &&
normalizeImageResolution(stringFromAny(params["size"])) == "" &&
normalizeImageResolution(stringFromAny(params["resolution"])) == ""
if sizeHasPixelDimensions || resolutionHasPixelDimensions || (stringFromAny(params["aspect_ratio"]) == "" && explicitWidthHeight) {
aspectRatio := aspectRatioFromDimensions(width, height)
allowed := aspectRatioAllowed(capability["aspect_ratio_allowed"], firstNonEmptyString(stringFromAny(params["resolution"]), context.resolution))
if processed, ok := validateAndAdjustAspectRatio(aspectRatio, capability, allowed); ok && processed != "" {
@@ -274,7 +339,37 @@ func imageDimensionsFromParams(params map[string]any) (int, int, bool) {
if width > 0 && height > 0 {
return width, height, true
}
return parsePixelSizeString(stringFromAny(params["resolution"]))
if width, height, ok := parsePixelSizeString(stringFromAny(params["resolution"])); ok {
return width, height, true
}
resolution := firstNonEmptyString(
normalizeImageResolution(stringFromAny(params["resolution"])),
normalizeImageResolution(stringFromAny(params["size"])),
)
if resolution == "" {
return 0, 0, false
}
return imageDimensionsForResolution(resolution, stringFromAny(params["aspect_ratio"]))
}
func imageDimensionsForResolution(resolution string, aspectRatio string) (int, int, bool) {
resolution = normalizeImageResolution(resolution)
if resolution == "" {
return 0, 0, false
}
magnitude, err := strconv.Atoi(strings.TrimSuffix(resolution, "K"))
if err != nil || magnitude <= 0 {
return 0, 0, false
}
baseSide := magnitude * 1024
ratio, validRatio := aspectRatioNumber(aspectRatio)
if !validRatio {
return baseSide, baseSide, true
}
if ratio >= 1 {
return baseSide, max(1, int(math.Round(float64(baseSide)/ratio))), true
}
return max(1, int(math.Round(float64(baseSide)*ratio))), baseSide, true
}
func imageSizeCapabilityConfigured(capability map[string]any) bool {