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
+175 -12
View File
@@ -8,6 +8,7 @@ import (
)
const unsupportedRequestResolutionCode = "unsupported_request_resolution"
const unsupportedRequestAspectRatioCode = "unsupported_request_aspect_ratio"
type requestResolutionRequirement struct {
Kind string
@@ -29,6 +30,15 @@ type videoResolutionReferenceStats struct {
}
func filterRuntimeCandidatesByRequest(kind string, requestedModel string, modelType string, body map[string]any, candidates []store.RuntimeModelCandidate) ([]store.RuntimeModelCandidate, map[string]any, error) {
filtered, resolutionSummary, err := filterRuntimeCandidatesByRequestResolution(kind, requestedModel, modelType, body, candidates)
if err != nil {
return filtered, resolutionSummary, err
}
filtered, aspectRatioSummary, err := filterRuntimeCandidatesByRequestAspectRatio(kind, requestedModel, modelType, body, filtered)
return filtered, mergeCandidateFilterSummaries(resolutionSummary, aspectRatioSummary), err
}
func filterRuntimeCandidatesByRequestResolution(kind string, requestedModel string, modelType string, body map[string]any, candidates []store.RuntimeModelCandidate) ([]store.RuntimeModelCandidate, map[string]any, error) {
requirement, ok := requestResolutionRequirementFor(kind, requestedModel, modelType, body)
if !ok || len(candidates) == 0 {
return candidates, nil, nil
@@ -82,13 +92,13 @@ func requestResolutionRequirementFor(kind string, requestedModel string, modelTy
}
func requestResolutionValue(body map[string]any, modelType string) (string, string) {
if value := normalizedRequestResolution(stringFromAny(body["resolution"])); value != "" {
if value := normalizedRequestResolution(stringFromAny(body["resolution"]), modelType); value != "" {
if _, _, ok := parsePixelSizeString(value); ok {
return "", ""
}
return value, "resolution"
}
size := normalizedRequestResolution(stringFromAny(body["size"]))
size := normalizedRequestResolution(stringFromAny(body["size"]), modelType)
if size == "" {
return "", ""
}
@@ -98,7 +108,7 @@ func requestResolutionValue(body map[string]any, modelType string) (string, stri
return "", ""
}
func normalizedRequestResolution(value string) string {
func normalizedRequestResolution(value string, modelType string) string {
value = strings.TrimSpace(value)
if value == "" || isEmptyParamString(value) {
return ""
@@ -106,9 +116,11 @@ func normalizedRequestResolution(value string) string {
switch strings.ToLower(value) {
case "auto", "automatic", "adaptive", "default":
return ""
default:
}
if _, _, ok := parsePixelSizeString(value); ok {
return value
}
return normalizeMediaResolution(modelType, value)
}
func isResolutionFilteredModelType(modelType string) bool {
@@ -120,10 +132,9 @@ func candidateSupportsRequestResolution(candidate store.RuntimeModelCandidate, r
capability := capabilityForType(effectiveModelCapability(candidate), modelType)
detail := candidateResolutionDetail(candidate, requirement, modelType)
if capability == nil {
detail["reason"] = "capability_missing"
detail["message"] = "候选平台模型未配置对应模型类型能力。"
detail["capabilityPath"] = capabilityPath(modelType, "output_resolutions")
return false, detail
detail["reason"] = "capability_unrestricted"
detail["message"] = "候选平台模型未配置分辨率白名单,按未限制处理。"
return true, detail
}
allowed, configured := outputResolutionAllowedValues(capability["output_resolutions"], requirement.Scopes)
@@ -131,9 +142,19 @@ func candidateSupportsRequestResolution(candidate store.RuntimeModelCandidate, r
detail["capabilityPath"] = capabilityPath(modelType, "output_resolutions")
detail["capabilityValue"] = cloneAny(capability["output_resolutions"])
if !configured {
detail["reason"] = "output_resolutions_missing"
detail["message"] = "候选平台模型未声明 output_resolutions。"
return false, detail
detail["reason"] = "output_resolutions_unrestricted"
detail["message"] = "候选平台模型未声明 output_resolutions,按未限制处理。"
return true, detail
}
if len(allowed) == 0 {
if _, scoped := capability["output_resolutions"].(map[string]any); scoped && len(requirement.Scopes) > 0 && !outputResolutionScopeConfigured(capability["output_resolutions"], requirement.Scopes) {
detail["reason"] = "resolution_scope_not_allowed"
detail["message"] = "候选平台模型未声明当前请求场景的 output_resolutions。"
return false, detail
}
detail["reason"] = "output_resolutions_empty"
detail["message"] = "候选平台模型声明了空分辨率白名单,按未限制处理。"
return true, detail
}
if containsResolution(allowed, requirement.Resolution) {
detail["reason"] = "supported"
@@ -162,7 +183,7 @@ func outputResolutionAllowedValues(value any, scopes []string) ([]string, bool)
for _, raw := range typed {
values = append(values, stringListFromAny(raw)...)
}
return uniqueStringList(values), len(values) > 0
return uniqueStringList(values), true
}
return nil, true
default:
@@ -170,6 +191,148 @@ func outputResolutionAllowedValues(value any, scopes []string) ([]string, bool)
}
}
func outputResolutionScopeConfigured(value any, scopes []string) bool {
typed, ok := value.(map[string]any)
if !ok {
return false
}
for _, scope := range append(append([]string(nil), scopes...), "default", "*", "all") {
if scope == "" {
continue
}
if _, exists := typed[scope]; exists {
return true
}
}
return false
}
type requestAspectRatioRequirement struct {
Kind string
RequestedModel string
ModelType string
AspectRatio string
Source string
Resolution string
}
func filterRuntimeCandidatesByRequestAspectRatio(kind string, requestedModel string, modelType string, body map[string]any, candidates []store.RuntimeModelCandidate) ([]store.RuntimeModelCandidate, map[string]any, error) {
requirement, ok := requestAspectRatioRequirementFor(kind, requestedModel, modelType, body)
if !ok || len(candidates) == 0 {
return candidates, nil, nil
}
filtered := make([]store.RuntimeModelCandidate, 0, len(candidates))
rejected := make([]map[string]any, 0)
supportedRatios := make([]string, 0)
for _, candidate := range candidates {
modelType := firstNonEmptyString(candidate.ModelType, requirement.ModelType)
capability := capabilityForType(effectiveModelCapability(candidate), modelType)
detail := candidateAspectRatioDetail(candidate, requirement, modelType)
if capability == nil {
detail["reason"] = "capability_unrestricted"
filtered = append(filtered, candidate)
continue
}
allowed, configured := outputResolutionAllowedValues(capability["aspect_ratio_allowed"], []string{requirement.Resolution})
detail["allowedAspectRatios"] = allowed
detail["capabilityPath"] = capabilityPath(modelType, "aspect_ratio_allowed")
detail["capabilityValue"] = cloneAny(capability["aspect_ratio_allowed"])
for _, value := range allowed {
appendUniqueString(&supportedRatios, value)
}
if !configured || len(allowed) == 0 || containsResolution(allowed, requirement.AspectRatio) {
detail["reason"] = "supported"
filtered = append(filtered, candidate)
continue
}
detail["reason"] = "aspect_ratio_not_allowed"
detail["message"] = "请求比例不在候选平台模型 aspect_ratio_allowed 中。"
rejected = append(rejected, detail)
}
summary := map[string]any{
"code": unsupportedRequestAspectRatioCode,
"filter": "request_aspect_ratio",
"kind": requirement.Kind,
"requestedModel": requirement.RequestedModel,
"modelType": requirement.ModelType,
"requestedAspectRatio": requirement.AspectRatio,
"aspectRatioSource": requirement.Source,
"resolution": requirement.Resolution,
"capabilityPath": capabilityPath(requirement.ModelType, "aspect_ratio_allowed"),
"candidateCount": len(candidates),
"supportedCandidateCount": len(filtered),
"filteredCandidateCount": len(rejected),
"supportedAspectRatios": uniqueStringList(supportedRatios),
"rejectedCandidates": rejected,
}
if len(filtered) == 0 {
return nil, summary, &store.ModelCandidateUnavailableError{
Code: unsupportedRequestAspectRatioCode,
Message: fmt.Sprintf("请求的%s比例 %s 没有可用平台模型支持,已过滤 %d 个候选平台模型", mediaResourceName(requirement.ModelType), requirement.AspectRatio, len(rejected)),
Details: summary,
}
}
return filtered, summary, nil
}
func requestAspectRatioRequirementFor(kind string, requestedModel string, modelType string, body map[string]any) (requestAspectRatioRequirement, bool) {
if !isResolutionFilteredModelType(modelType) {
return requestAspectRatioRequirement{}, false
}
for _, key := range []string{"aspect_ratio", "aspectRatio", "ratio"} {
raw, exists := body[key]
if !exists {
continue
}
aspectRatio, ok := normalizedAspectRatio(stringFromAny(raw))
if !ok {
return requestAspectRatioRequirement{}, false
}
resolution, _ := requestResolutionValue(body, modelType)
return requestAspectRatioRequirement{
Kind: kind,
RequestedModel: requestedModel,
ModelType: modelType,
AspectRatio: aspectRatio,
Source: key,
Resolution: resolution,
}, true
}
return requestAspectRatioRequirement{}, false
}
func candidateAspectRatioDetail(candidate store.RuntimeModelCandidate, requirement requestAspectRatioRequirement, modelType string) map[string]any {
return map[string]any{
"platformId": candidate.PlatformID,
"platformKey": candidate.PlatformKey,
"platformName": candidate.PlatformName,
"provider": candidate.Provider,
"platformModelId": candidate.PlatformModelID,
"modelName": candidate.ModelName,
"modelAlias": candidate.ModelAlias,
"displayName": candidate.DisplayName,
"providerModelName": candidate.ProviderModelName,
"modelType": modelType,
"requested": map[string]any{
"aspectRatio": requirement.AspectRatio,
"source": requirement.Source,
"resolution": requirement.Resolution,
},
}
}
func mediaResourceName(modelType string) string {
if modelType == "image_generate" || modelType == "image_edit" {
return "图像"
}
if isVideoModelType(modelType) {
return "视频"
}
return "媒体"
}
func containsResolution(values []string, target string) bool {
for _, value := range values {
if strings.EqualFold(strings.TrimSpace(value), strings.TrimSpace(target)) {
@@ -0,0 +1,173 @@
package runner
import (
"testing"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
func TestParamProcessorTreatsNonRatioValuesAsUnspecifiedForImagesAndVideos(t *testing.T) {
for _, modelType := range []string{"image_generate", "video_generate"} {
t.Run(modelType, func(t *testing.T) {
result := preprocessRequestWithLog(mediaKindForTest(modelType), map[string]any{
"aspect_ratio": "auto",
"aspectRatio": "adaptive",
"ratio": "keep_ratio",
}, store.RuntimeModelCandidate{
ModelType: modelType,
Capabilities: map[string]any{
modelType: map[string]any{
"aspect_ratio_allowed": []any{"adaptive"},
},
},
})
for _, key := range []string{"aspect_ratio", "aspectRatio", "ratio"} {
if _, ok := result.Body[key]; ok {
t.Fatalf("%s should be removed for non-ratio input: %+v", key, result.Body)
}
}
if result.Err != nil {
t.Fatalf("non-ratio input should use provider default instead of failing: %v", result.Err)
}
})
}
}
func TestParamProcessorNormalizesImageKSizeAndPreservesProviderResolutionChoice(t *testing.T) {
tests := []struct {
name string
capability map[string]any
wantSize string
}{
{
name: "dimension upstream",
capability: map[string]any{
"output_resolutions": []any{"1K", "2K", "4K"},
"aspect_ratio_allowed": []any{"16:9"},
},
wantSize: "2048x1152",
},
{
name: "resolution upstream",
capability: map[string]any{
"output_resolutions": []any{"1K", "2K"},
"aspect_ratio_allowed": []any{"16:9"},
"size_param_format": "resolution",
},
wantSize: "2K",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := preprocessRequestWithLog("images.generations", map[string]any{
"size": "2k",
"aspect_ratio": "16 : 9",
}, store.RuntimeModelCandidate{
ModelType: "image_generate",
Capabilities: map[string]any{
"image_generate": test.capability,
},
})
if result.Err != nil {
t.Fatalf("preprocess image size: %v", result.Err)
}
if result.Body["resolution"] != "2K" || result.Body["size"] != test.wantSize {
t.Fatalf("unexpected normalized size: %+v", result.Body)
}
if result.Body["width"] != 2048 || result.Body["height"] != 1152 || result.Body["aspect_ratio"] != "16:9" {
t.Fatalf("unexpected normalized dimensions: %+v", result.Body)
}
})
}
}
func TestParamProcessorNormalizesVideo4KSize(t *testing.T) {
result := preprocessRequestWithLog("videos.generations", map[string]any{
"size": "4K",
"aspect_ratio": "auto",
}, store.RuntimeModelCandidate{
ModelType: "video_generate",
Capabilities: map[string]any{
"video_generate": map[string]any{
"output_resolutions": []any{"2160p"},
"aspect_ratio_allowed": []any{"16:9"},
},
},
})
if result.Body["resolution"] != "2160p" {
t.Fatalf("video 4K size should normalize to 2160p: %+v", result.Body)
}
if _, ok := result.Body["aspect_ratio"]; ok {
t.Fatalf("video auto ratio should stay unspecified: %+v", result.Body)
}
}
func TestCandidateFilterIgnoresNonRatioValuesAndFiltersRealRatios(t *testing.T) {
candidates := []store.RuntimeModelCandidate{
candidateWithAspectRatios("square", "image_generate", "1:1"),
candidateWithAspectRatios("landscape", "image_generate", "16:9"),
}
filtered, summary, err := filterRuntimeCandidatesByRequest("images.generations", "demo-image", "image_generate", map[string]any{
"aspect_ratio": "auto",
}, candidates)
if err != nil || len(filtered) != 2 || summary != nil {
t.Fatalf("auto ratio should not participate in candidate matching: filtered=%+v summary=%+v err=%v", filtered, summary, err)
}
filtered, summary, err = filterRuntimeCandidatesByRequest("images.generations", "demo-image", "image_generate", map[string]any{
"aspect_ratio": "16 : 9",
}, candidates)
if err != nil || len(filtered) != 1 || filtered[0].PlatformKey != "landscape" {
t.Fatalf("real ratio should filter candidates: filtered=%+v summary=%+v err=%v", filtered, summary, err)
}
}
func TestCandidateFilterNormalizesKSizeAndAllowsUnconfiguredResolution(t *testing.T) {
candidates := []store.RuntimeModelCandidate{
candidateWithImageResolutions("one-k", "1K"),
{
PlatformID: "platform-unrestricted",
PlatformKey: "unrestricted",
PlatformModelID: "model-unrestricted",
ModelName: "demo-image",
ModelType: "image_generate",
Capabilities: map[string]any{
"image_generate": map[string]any{},
},
},
}
filtered, _, err := filterRuntimeCandidatesByRequest("images.generations", "demo-image", "image_generate", map[string]any{
"size": "2k",
}, candidates)
if err != nil {
t.Fatalf("2k size should be accepted and normalized for matching: %v", err)
}
if len(filtered) != 1 || filtered[0].PlatformKey != "unrestricted" {
t.Fatalf("unexpected candidates for normalized 2K size: %+v", filtered)
}
}
func candidateWithAspectRatios(platformKey string, modelType string, ratios ...string) store.RuntimeModelCandidate {
return store.RuntimeModelCandidate{
PlatformID: "platform-" + platformKey,
PlatformKey: platformKey,
PlatformModelID: "model-" + platformKey,
ModelName: "demo-media",
ModelType: modelType,
Capabilities: map[string]any{
modelType: map[string]any{
"aspect_ratio_allowed": stringsToAny(ratios),
},
},
}
}
func mediaKindForTest(modelType string) string {
if modelType == "image_generate" {
return "images.generations"
}
return "videos.generations"
}
+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 {
@@ -393,23 +393,39 @@ func numberPair(value any) ([2]float64, bool) {
}
func validAspectRatio(value string) bool {
if value == "adaptive" || value == "keep_ratio" {
return true
}
_, ok := aspectRatioNumber(value)
_, ok := normalizedAspectRatio(value)
return ok
}
func aspectRatioNumber(value string) (float64, bool) {
parts := strings.Split(value, ":")
func normalizedAspectRatio(value string) (string, bool) {
compact := strings.Map(func(r rune) rune {
switch r {
case ' ', '\t', '\r', '\n':
return -1
default:
return r
}
}, strings.TrimSpace(value))
parts := strings.Split(compact, ":")
if len(parts) != 2 {
return 0, false
return "", false
}
width := parsePositiveFloat(parts[0])
height := parsePositiveFloat(parts[1])
if width <= 0 || height <= 0 {
return "", false
}
return parts[0] + ":" + parts[1], true
}
func aspectRatioNumber(value string) (float64, bool) {
normalized, ok := normalizedAspectRatio(value)
if !ok {
return 0, false
}
parts := strings.Split(normalized, ":")
width := parsePositiveFloat(parts[0])
height := parsePositiveFloat(parts[1])
return width / height, true
}
@@ -537,11 +553,53 @@ func isEmptyParamString(value string) bool {
}
func isImageResolution(modelType string, value string) bool {
return (modelType == "image_generate" || modelType == "image_edit") && containsString([]string{"1K", "2K", "3K", "4K", "8K"}, value)
if modelType != "image_generate" && modelType != "image_edit" {
return false
}
return normalizeImageResolution(value) != ""
}
func isVideoResolution(modelType string, value string) bool {
return isVideoModelType(modelType) && containsString([]string{"480p", "720p", "1080p", "1440p", "2160p"}, value)
return isVideoModelType(modelType) && normalizeVideoResolution(value) != ""
}
func normalizeMediaResolution(modelType string, value string) string {
if modelType == "image_generate" || modelType == "image_edit" {
return normalizeImageResolution(value)
}
if isVideoModelType(modelType) {
return normalizeVideoResolution(value)
}
return ""
}
func normalizeImageResolution(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
if len(value) < 2 || value[len(value)-1] != 'k' {
return ""
}
magnitude, err := strconv.Atoi(value[:len(value)-1])
if err != nil || magnitude <= 0 {
return ""
}
return strconv.Itoa(magnitude) + "K"
}
func normalizeVideoResolution(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "480p":
return "480p"
case "720p":
return "720p"
case "1080p":
return "1080p"
case "1440p":
return "1440p"
case "2160p", "4k":
return "2160p"
default:
return ""
}
}
func isVideoModelType(modelType string) bool {
+1
View File
@@ -1118,6 +1118,7 @@ func (s *Service) runCandidate(ctx context.Context, task store.GatewayTask, user
ModelType: candidate.ModelType,
Model: task.Model,
Body: providerBody,
OriginalBody: preprocessing.Input,
Candidate: candidate,
HTTPClient: requestHTTPClient,
RemoteTaskID: task.RemoteTaskID,