统一过滤非 x:x 比例,避免默认值参与候选匹配或透传上游。 支持 xK 分辨率归一化,并按 Gemini、Volces 和 OpenAI 的参数规范生成上游请求;OpenAI 显式 WxH 始终保留用户原始尺寸。 验证:在独立临时 worktree 中执行 apps/api 全量 go test ./... -count=1 通过,真实 OpenAI 请求已到达上游但受本地无效凭据阻断。
252 lines
7.4 KiB
Go
252 lines
7.4 KiB
Go
package clients
|
|
|
|
import (
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
openAIGPTImage2MaxEdge = 3840
|
|
openAIGPTImage2MinPixels = 655360
|
|
openAIGPTImage2MaxPixels = 8294400
|
|
openAIGPTImage2MaxRatio = 3
|
|
openAIGPTImage2Multiple = 16
|
|
)
|
|
|
|
type providerImageDimensions struct {
|
|
width int
|
|
height int
|
|
}
|
|
|
|
func normalizedProviderAspectRatio(value any) string {
|
|
compact := strings.Map(func(r rune) rune {
|
|
switch r {
|
|
case ' ', '\t', '\r', '\n':
|
|
return -1
|
|
default:
|
|
return r
|
|
}
|
|
}, strings.TrimSpace(stringFromAny(value)))
|
|
parts := strings.Split(compact, ":")
|
|
if len(parts) != 2 {
|
|
return ""
|
|
}
|
|
width, widthErr := strconv.ParseFloat(parts[0], 64)
|
|
height, heightErr := strconv.ParseFloat(parts[1], 64)
|
|
if widthErr != nil || heightErr != nil || width <= 0 || height <= 0 {
|
|
return ""
|
|
}
|
|
return parts[0] + ":" + parts[1]
|
|
}
|
|
|
|
func normalizedProviderImageResolution(value any) string {
|
|
text := strings.ToLower(strings.TrimSpace(stringFromAny(value)))
|
|
if len(text) < 2 || text[len(text)-1] != 'k' {
|
|
return ""
|
|
}
|
|
magnitude, err := strconv.Atoi(text[:len(text)-1])
|
|
if err != nil || magnitude <= 0 {
|
|
return ""
|
|
}
|
|
return strconv.Itoa(magnitude) + "K"
|
|
}
|
|
|
|
func normalizeOfficialOpenAIImageSize(body map[string]any, originalBody map[string]any) {
|
|
if originalBody == nil {
|
|
originalBody = body
|
|
}
|
|
if width, height, ok := providerPixelDimensions(stringFromAny(originalBody["size"])); ok {
|
|
body["size"] = strconv.Itoa(width) + "x" + strconv.Itoa(height)
|
|
return
|
|
}
|
|
|
|
model := strings.ToLower(strings.TrimSpace(stringFromAny(body["model"])))
|
|
resolution := normalizedProviderImageResolution(firstNonEmptyString(
|
|
originalBody["resolution"],
|
|
originalBody["size"],
|
|
body["resolution"],
|
|
body["size"],
|
|
))
|
|
if resolution != "" {
|
|
width, height, ok := providerBodyPixelDimensions(body)
|
|
if !ok {
|
|
width, height, ok = providerResolutionDimensions(
|
|
resolution,
|
|
firstNonEmptyString(originalBody["aspect_ratio"], originalBody["aspectRatio"], originalBody["ratio"]),
|
|
)
|
|
}
|
|
if !ok {
|
|
delete(body, "size")
|
|
return
|
|
}
|
|
switch {
|
|
case strings.HasPrefix(model, "gpt-image-2"):
|
|
dimensions := normalizeOpenAIGPTImage2Dimensions(width, height)
|
|
body["size"] = strconv.Itoa(dimensions.width) + "x" + strconv.Itoa(dimensions.height)
|
|
case strings.HasPrefix(model, "gpt-image-"):
|
|
body["size"] = officialOpenAILegacyImageSize(width, height)
|
|
default:
|
|
body["size"] = strconv.Itoa(width) + "x" + strconv.Itoa(height)
|
|
}
|
|
return
|
|
}
|
|
|
|
if !strings.HasPrefix(model, "gpt-image-") {
|
|
return
|
|
}
|
|
size := strings.ToLower(strings.TrimSpace(stringFromAny(body["size"])))
|
|
if size == "auto" {
|
|
body["size"] = "auto"
|
|
return
|
|
}
|
|
if strings.HasPrefix(model, "gpt-image-2") {
|
|
if width, height, ok := providerPixelDimensions(size); ok {
|
|
body["size"] = strconv.Itoa(width) + "x" + strconv.Itoa(height)
|
|
}
|
|
return
|
|
}
|
|
|
|
switch size {
|
|
case "1024x1024", "1536x1024", "1024x1536":
|
|
body["size"] = size
|
|
return
|
|
}
|
|
width, height, ok := requestedProviderDimensions(body)
|
|
if !ok {
|
|
body["size"] = "auto"
|
|
return
|
|
}
|
|
body["size"] = officialOpenAILegacyImageSize(width, height)
|
|
}
|
|
|
|
func officialOpenAILegacyImageSize(width int, height int) string {
|
|
switch {
|
|
case width > height:
|
|
return "1536x1024"
|
|
case height > width:
|
|
return "1024x1536"
|
|
default:
|
|
return "1024x1024"
|
|
}
|
|
}
|
|
|
|
func providerBodyPixelDimensions(body map[string]any) (int, int, bool) {
|
|
if width, height, ok := providerPixelDimensions(stringFromAny(body["size"])); ok {
|
|
return width, height, true
|
|
}
|
|
width := intFromAny(body["width"])
|
|
height := intFromAny(body["height"])
|
|
return width, height, width > 0 && height > 0
|
|
}
|
|
|
|
func providerResolutionDimensions(resolution string, aspectRatio any) (int, int, bool) {
|
|
resolution = normalizedProviderImageResolution(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 := providerAspectRatioNumber(aspectRatio)
|
|
width, height := baseSide, baseSide
|
|
if ratio > 1 {
|
|
height = max(1, int(math.Round(float64(baseSide)/ratio)))
|
|
} else if ratio > 0 && ratio < 1 {
|
|
width = max(1, int(math.Round(float64(baseSide)*ratio)))
|
|
}
|
|
return width, height, true
|
|
}
|
|
|
|
func requestedProviderDimensions(body map[string]any) (int, int, bool) {
|
|
if width, height, ok := providerPixelDimensions(stringFromAny(body["size"])); ok {
|
|
return width, height, true
|
|
}
|
|
width := intFromAny(body["width"])
|
|
height := intFromAny(body["height"])
|
|
if width > 0 && height > 0 {
|
|
return width, height, true
|
|
}
|
|
ratio := providerAspectRatioNumber(firstNonEmptyString(body["aspect_ratio"], body["aspectRatio"], body["ratio"]))
|
|
if ratio > 1 {
|
|
return 2, 1, true
|
|
}
|
|
if ratio > 0 && ratio < 1 {
|
|
return 1, 2, true
|
|
}
|
|
if ratio == 1 {
|
|
return 1, 1, true
|
|
}
|
|
return 0, 0, false
|
|
}
|
|
|
|
func providerPixelDimensions(value string) (int, int, bool) {
|
|
parts := strings.Split(strings.ToLower(strings.TrimSpace(value)), "x")
|
|
if len(parts) != 2 {
|
|
return 0, 0, false
|
|
}
|
|
width, widthErr := strconv.Atoi(strings.TrimSpace(parts[0]))
|
|
height, heightErr := strconv.Atoi(strings.TrimSpace(parts[1]))
|
|
if widthErr != nil || heightErr != nil || width <= 0 || height <= 0 {
|
|
return 0, 0, false
|
|
}
|
|
return width, height, true
|
|
}
|
|
|
|
func providerAspectRatioNumber(value any) float64 {
|
|
normalized := normalizedProviderAspectRatio(value)
|
|
if normalized == "" {
|
|
return 0
|
|
}
|
|
parts := strings.Split(normalized, ":")
|
|
width, _ := strconv.ParseFloat(parts[0], 64)
|
|
height, _ := strconv.ParseFloat(parts[1], 64)
|
|
return width / height
|
|
}
|
|
|
|
func normalizeOpenAIGPTImage2Dimensions(width int, height int) providerImageDimensions {
|
|
if width <= 0 || height <= 0 {
|
|
return providerImageDimensions{}
|
|
}
|
|
rawRatio := float64(width) / float64(height)
|
|
ratio := rawRatio
|
|
if ratio < 1 {
|
|
ratio = 1 / ratio
|
|
}
|
|
ratio = min(ratio, float64(openAIGPTImage2MaxRatio))
|
|
landscape := rawRatio >= 1
|
|
longEdge := min(max(width, height), openAIGPTImage2MaxEdge)
|
|
dimensions := buildOpenAIGPTImage2Dimensions(longEdge, ratio, landscape, false)
|
|
for dimensions.width*dimensions.height > openAIGPTImage2MaxPixels {
|
|
longEdge = max(openAIGPTImage2Multiple, max(dimensions.width, dimensions.height)-openAIGPTImage2Multiple)
|
|
dimensions = buildOpenAIGPTImage2Dimensions(longEdge, ratio, landscape, false)
|
|
}
|
|
for dimensions.width*dimensions.height < openAIGPTImage2MinPixels && max(dimensions.width, dimensions.height) < openAIGPTImage2MaxEdge {
|
|
longEdge = min(openAIGPTImage2MaxEdge, max(dimensions.width, dimensions.height)+openAIGPTImage2Multiple)
|
|
dimensions = buildOpenAIGPTImage2Dimensions(longEdge, ratio, landscape, true)
|
|
}
|
|
return dimensions
|
|
}
|
|
|
|
func buildOpenAIGPTImage2Dimensions(longEdge int, ratio float64, landscape bool, roundUp bool) providerImageDimensions {
|
|
normalizedLongEdge := roundOpenAIGPTImage2Multiple(float64(longEdge), roundUp)
|
|
normalizedShortEdge := roundOpenAIGPTImage2Multiple(float64(normalizedLongEdge)/ratio, roundUp)
|
|
if float64(normalizedLongEdge)/float64(normalizedShortEdge) > openAIGPTImage2MaxRatio {
|
|
normalizedShortEdge += openAIGPTImage2Multiple
|
|
}
|
|
if landscape {
|
|
return providerImageDimensions{width: normalizedLongEdge, height: normalizedShortEdge}
|
|
}
|
|
return providerImageDimensions{width: normalizedShortEdge, height: normalizedLongEdge}
|
|
}
|
|
|
|
func roundOpenAIGPTImage2Multiple(value float64, roundUp bool) int {
|
|
rounded := math.Round(value / openAIGPTImage2Multiple)
|
|
if roundUp {
|
|
rounded = math.Ceil(value / openAIGPTImage2Multiple)
|
|
}
|
|
return max(openAIGPTImage2Multiple, int(rounded)*openAIGPTImage2Multiple)
|
|
}
|