fix(media): 规范比例与 OpenAI 图像尺寸参数
统一过滤非 x:x 比例,避免默认值参与候选匹配或透传上游。 支持 xK 分辨率归一化,并按 Gemini、Volces 和 OpenAI 的参数规范生成上游请求;OpenAI 显式 WxH 始终保留用户原始尺寸。 验证:在独立临时 worktree 中执行 apps/api 全量 go test ./... -count=1 通过,真实 OpenAI 请求已到达上游但受本地无效凭据阻断。
This commit is contained in:
@@ -272,24 +272,39 @@ func geminiGenerationConfig(body map[string]any, imageResponse bool) map[string]
|
||||
if out == nil {
|
||||
out = map[string]any{}
|
||||
}
|
||||
if aspectRatio := firstNonEmptyString(body["aspect_ratio"], body["aspectRatio"]); aspectRatio != "" {
|
||||
imageConfig := cloneMapAny(mapFromAny(firstPresent(out["imageConfig"], out["image_config"])))
|
||||
imageConfig := cloneMapAny(mapFromAny(firstPresent(out["imageConfig"], out["image_config"])))
|
||||
if imageConfig != nil {
|
||||
aspectRatio := normalizedProviderAspectRatio(firstNonEmptyString(imageConfig["aspectRatio"], imageConfig["aspect_ratio"]))
|
||||
imageSize := normalizedProviderImageResolution(firstNonEmptyString(imageConfig["imageSize"], imageConfig["image_size"]))
|
||||
delete(imageConfig, "aspectRatio")
|
||||
delete(imageConfig, "aspect_ratio")
|
||||
delete(imageConfig, "imageSize")
|
||||
delete(imageConfig, "image_size")
|
||||
if aspectRatio != "" {
|
||||
imageConfig["aspectRatio"] = aspectRatio
|
||||
}
|
||||
if imageSize != "" {
|
||||
imageConfig["imageSize"] = imageSize
|
||||
}
|
||||
}
|
||||
if aspectRatio := normalizedProviderAspectRatio(firstNonEmptyString(body["aspect_ratio"], body["aspectRatio"], body["ratio"])); aspectRatio != "" {
|
||||
if imageConfig == nil {
|
||||
imageConfig = map[string]any{}
|
||||
}
|
||||
imageConfig["aspectRatio"] = aspectRatio
|
||||
out["imageConfig"] = imageConfig
|
||||
delete(out, "image_config")
|
||||
}
|
||||
if imageSize := firstNonEmptyString(body["resolution"], body["imageSize"], body["image_size"], body["size"]); imageSize != "" {
|
||||
imageConfig := cloneMapAny(mapFromAny(firstPresent(out["imageConfig"], out["image_config"])))
|
||||
if imageSize := normalizedProviderImageResolution(firstNonEmptyString(body["resolution"], body["imageSize"], body["image_size"], body["size"])); imageSize != "" {
|
||||
if imageConfig == nil {
|
||||
imageConfig = map[string]any{}
|
||||
}
|
||||
imageConfig["imageSize"] = imageSize
|
||||
out["imageConfig"] = imageConfig
|
||||
delete(out, "image_config")
|
||||
}
|
||||
if len(imageConfig) > 0 {
|
||||
out["imageConfig"] = imageConfig
|
||||
} else {
|
||||
delete(out, "imageConfig")
|
||||
}
|
||||
delete(out, "image_config")
|
||||
if count := intFromAny(firstPresent(body["n"], body["candidateCount"], body["candidate_count"])); count > 0 {
|
||||
out["candidateCount"] = count
|
||||
}
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
package clients
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
)
|
||||
|
||||
func TestGeminiGenerationConfigDropsDefaultRatioTokensAndNormalizesKSize(t *testing.T) {
|
||||
config := geminiGenerationConfig(map[string]any{
|
||||
"aspect_ratio": "auto",
|
||||
"size": "2k",
|
||||
"generationConfig": map[string]any{
|
||||
"imageConfig": map[string]any{
|
||||
"aspectRatio": "adaptive",
|
||||
"imageSize": "2k",
|
||||
},
|
||||
},
|
||||
}, true)
|
||||
|
||||
imageConfig := mapFromAny(config["imageConfig"])
|
||||
if _, ok := imageConfig["aspectRatio"]; ok {
|
||||
t.Fatalf("non-ratio tokens must not be sent to Gemini: %+v", config)
|
||||
}
|
||||
if imageConfig["imageSize"] != "2K" {
|
||||
t.Fatalf("Gemini imageSize should use canonical K format: %+v", config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeOfficialOpenAIImageSizeUsesModelSpecificWireFormat(t *testing.T) {
|
||||
legacy := map[string]any{
|
||||
"model": "gpt-image-1",
|
||||
"size": "2K",
|
||||
"width": 2048,
|
||||
"height": 1152,
|
||||
"aspect_ratio": "16:9",
|
||||
}
|
||||
normalizeOfficialOpenAIImageSize(legacy, map[string]any{
|
||||
"size": "2K",
|
||||
"aspect_ratio": "16:9",
|
||||
})
|
||||
if legacy["size"] != "1536x1024" {
|
||||
t.Fatalf("legacy GPT Image should use an official orientation size: %+v", legacy)
|
||||
}
|
||||
|
||||
flexible := map[string]any{
|
||||
"model": "gpt-image-2",
|
||||
"size": "2K",
|
||||
"resolution": "2k",
|
||||
"aspect_ratio": "16:9",
|
||||
}
|
||||
normalizeOfficialOpenAIImageSize(flexible, map[string]any{
|
||||
"size": "2K",
|
||||
"aspect_ratio": "16:9",
|
||||
})
|
||||
if flexible["size"] != "2048x1152" {
|
||||
t.Fatalf("GPT Image 2 should receive flexible pixel dimensions: %+v", flexible)
|
||||
}
|
||||
|
||||
rounded := map[string]any{
|
||||
"model": "gpt-image-2",
|
||||
"size": "2K",
|
||||
"resolution": "2K",
|
||||
"aspect_ratio": "3:2",
|
||||
}
|
||||
normalizeOfficialOpenAIImageSize(rounded, map[string]any{
|
||||
"size": "2K",
|
||||
"aspect_ratio": "3:2",
|
||||
})
|
||||
if rounded["size"] != "2048x1360" {
|
||||
t.Fatalf("GPT Image 2 dimensions should satisfy the official 16-pixel multiple: %+v", rounded)
|
||||
}
|
||||
|
||||
explicit := map[string]any{
|
||||
"model": "gpt-image-2",
|
||||
"size": "1232x768",
|
||||
"width": 1232,
|
||||
"height": 768,
|
||||
}
|
||||
normalizeOfficialOpenAIImageSize(explicit, map[string]any{
|
||||
"size": "1234x777",
|
||||
})
|
||||
if explicit["size"] != "1234x777" {
|
||||
t.Fatalf("an explicit WxH size must be sent unchanged to OpenAI: %+v", explicit)
|
||||
}
|
||||
|
||||
explicitLegacy := map[string]any{
|
||||
"model": "gpt-image-1",
|
||||
"size": "2048x1152",
|
||||
}
|
||||
normalizeOfficialOpenAIImageSize(explicitLegacy, map[string]any{
|
||||
"size": "2048x1152",
|
||||
})
|
||||
if explicitLegacy["size"] != "2048x1152" {
|
||||
t.Fatalf("explicit WxH must also win for legacy OpenAI image models: %+v", explicitLegacy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIClientUsesOriginalWxHAndConvertsKResolution(t *testing.T) {
|
||||
received := make([]map[string]any, 0, 3)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var body map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decode OpenAI image request: %v", err)
|
||||
}
|
||||
received = append(received, body)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"data": []any{map[string]any{"b64_json": "aW1hZ2U="}},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
candidate := store.RuntimeModelCandidate{
|
||||
BaseURL: server.URL,
|
||||
Provider: "openai",
|
||||
ProviderModelName: "gpt-image-2",
|
||||
ModelType: "image_generate",
|
||||
Credentials: map[string]any{"apiKey": "openai-key"},
|
||||
}
|
||||
requests := []Request{
|
||||
{
|
||||
Kind: "images.generations",
|
||||
ModelType: "image_generate",
|
||||
Body: map[string]any{
|
||||
"prompt": "explicit dimensions",
|
||||
"size": "1232x768",
|
||||
"width": 1232,
|
||||
"height": 768,
|
||||
},
|
||||
OriginalBody: map[string]any{
|
||||
"size": "1234x777",
|
||||
},
|
||||
Candidate: candidate,
|
||||
},
|
||||
{
|
||||
Kind: "images.generations",
|
||||
ModelType: "image_generate",
|
||||
Body: map[string]any{
|
||||
"prompt": "resolution conversion",
|
||||
"size": "2048x1365",
|
||||
"resolution": "2K",
|
||||
"aspect_ratio": "3:2",
|
||||
"width": 2048,
|
||||
"height": 1365,
|
||||
},
|
||||
OriginalBody: map[string]any{
|
||||
"size": "2K",
|
||||
"aspect_ratio": "3:2",
|
||||
},
|
||||
Candidate: candidate,
|
||||
},
|
||||
{
|
||||
Kind: "images.generations",
|
||||
ModelType: "image_generate",
|
||||
Body: map[string]any{
|
||||
"prompt": "resolution field conversion",
|
||||
"size": "1024x1024",
|
||||
"resolution": "1K",
|
||||
"aspect_ratio": "1:1",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
},
|
||||
OriginalBody: map[string]any{
|
||||
"resolution": "1k",
|
||||
"aspect_ratio": "1:1",
|
||||
},
|
||||
Candidate: candidate,
|
||||
},
|
||||
}
|
||||
for _, request := range requests {
|
||||
if _, err := (OpenAIClient{HTTPClient: server.Client()}).Run(context.Background(), request); err != nil {
|
||||
t.Fatalf("run OpenAI image request: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(received) != 3 {
|
||||
t.Fatalf("unexpected request count: %d", len(received))
|
||||
}
|
||||
if received[0]["size"] != "1234x777" {
|
||||
t.Fatalf("explicit WxH should win over preprocessed dimensions: %+v", received[0])
|
||||
}
|
||||
if received[1]["size"] != "2048x1360" {
|
||||
t.Fatalf("2K with 3:2 should use normalized OpenAI dimensions: %+v", received[1])
|
||||
}
|
||||
if received[2]["size"] != "1024x1024" {
|
||||
t.Fatalf("resolution=1K should be converted to OpenAI dimensions: %+v", received[2])
|
||||
}
|
||||
for _, body := range received {
|
||||
for _, key := range []string{"resolution", "aspect_ratio", "width", "height"} {
|
||||
if _, ok := body[key]; ok {
|
||||
t.Fatalf("generic geometry field %q must not reach OpenAI: %+v", key, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVolcesImageBodySelectsConfiguredSizeFormat(t *testing.T) {
|
||||
body := volcesImageBody(Request{
|
||||
Kind: "images.generations",
|
||||
ModelType: "image_generate",
|
||||
Body: map[string]any{
|
||||
"model": "Seedream-5.0-Pro",
|
||||
"resolution": "2k",
|
||||
"size": "2048x1152",
|
||||
"width": 2048,
|
||||
"height": 1152,
|
||||
"aspect_ratio": "16:9",
|
||||
"platformId": "gateway-internal-platform",
|
||||
},
|
||||
Candidate: store.RuntimeModelCandidate{
|
||||
ProviderModelName: "doubao-seedream-5-0-pro-260628",
|
||||
ModelType: "image_generate",
|
||||
Capabilities: map[string]any{
|
||||
"image_generate": map[string]any{
|
||||
"size_param_format": "resolution",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if body["size"] != "2K" {
|
||||
t.Fatalf("resolution-only Volces model should receive canonical K size: %+v", body)
|
||||
}
|
||||
for _, key := range []string{"resolution", "width", "height", "aspect_ratio", "platformId"} {
|
||||
if _, ok := body[key]; ok {
|
||||
t.Fatalf("generic geometry field %q must not reach Volces: %+v", key, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
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)
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func (c OpenAIClient) Run(ctx context.Context, request Request) (Response, error
|
||||
}
|
||||
}
|
||||
body["model"] = upstreamModelName(request.Candidate)
|
||||
normalizeOpenAIImageRequestBody(endpointKind, body)
|
||||
normalizeOpenAIImageRequestBody(endpointKind, body, request.OriginalBody)
|
||||
stream := openAIEndpointSupportsStream(endpointKind) && (request.Stream || boolValue(body, "stream"))
|
||||
ensureOpenAIStreamUsage(body, endpointKind, stream)
|
||||
raw, contentType, err := openAIRequestPayload(endpointKind, body, request.Candidate)
|
||||
@@ -172,10 +172,11 @@ func (c OpenAIClient) Run(ctx context.Context, request Request) (Response, error
|
||||
}, nil
|
||||
}
|
||||
|
||||
func normalizeOpenAIImageRequestBody(endpointKind string, body map[string]any) {
|
||||
func normalizeOpenAIImageRequestBody(endpointKind string, body map[string]any, originalBody map[string]any) {
|
||||
if endpointKind != "images.generations" && endpointKind != "images.edits" {
|
||||
return
|
||||
}
|
||||
normalizeOfficialOpenAIImageSize(body, originalBody)
|
||||
// OpenAI image endpoints express output geometry through size. The Gateway
|
||||
// keeps the generic fields for capability validation and billing, but they
|
||||
// are not valid OpenAI wire parameters.
|
||||
@@ -185,6 +186,10 @@ func normalizeOpenAIImageRequestBody(endpointKind string, body map[string]any) {
|
||||
"resolution",
|
||||
"width",
|
||||
"height",
|
||||
"platform_id",
|
||||
"platformId",
|
||||
"platform_model_id",
|
||||
"platformModelId",
|
||||
} {
|
||||
delete(body, key)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ type Request struct {
|
||||
ModelType string
|
||||
Model string
|
||||
Body map[string]any
|
||||
OriginalBody map[string]any
|
||||
Candidate store.RuntimeModelCandidate
|
||||
HTTPClient *http.Client
|
||||
RemoteTaskID string
|
||||
|
||||
@@ -357,16 +357,33 @@ func volcesImageBody(request Request) map[string]any {
|
||||
body["seed"] = -1
|
||||
}
|
||||
}
|
||||
if resolution := strings.TrimSpace(stringFromAny(body["resolution"])); resolution != "" {
|
||||
resolution := normalizedProviderImageResolution(body["resolution"])
|
||||
size := widthHeightSize(body)
|
||||
if volcesImageUsesResolutionSize(request) && resolution != "" {
|
||||
body["size"] = resolution
|
||||
} else if size != "" {
|
||||
body["size"] = size
|
||||
} else if resolution != "" {
|
||||
body["size"] = resolution
|
||||
}
|
||||
if size := widthHeightSize(body); size != "" {
|
||||
body["size"] = size
|
||||
for _, key := range []string{"aspect_ratio", "aspectRatio", "ratio", "resolution", "width", "height"} {
|
||||
delete(body, key)
|
||||
}
|
||||
normalizeVolcesSequentialImageGeneration(body, request)
|
||||
return body
|
||||
}
|
||||
|
||||
func volcesImageUsesResolutionSize(request Request) bool {
|
||||
modelType := firstNonEmpty(request.ModelType, request.Candidate.ModelType)
|
||||
if capability, ok := request.Candidate.Capabilities[modelType].(map[string]any); ok {
|
||||
if strings.EqualFold(strings.TrimSpace(stringFromAny(capability["size_param_format"])), "resolution") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
model := strings.ToLower(strings.TrimSpace(upstreamModelName(request.Candidate)))
|
||||
return strings.HasPrefix(model, "doubao-seedream-5-0")
|
||||
}
|
||||
|
||||
func volcesVideoBody(request Request) map[string]any {
|
||||
body := cleanProviderBody(request.Body)
|
||||
body["model"] = upstreamModelName(request.Candidate)
|
||||
@@ -397,6 +414,10 @@ func cleanProviderBody(body map[string]any) map[string]any {
|
||||
"_gateway_target_protocol",
|
||||
"_compat_provider",
|
||||
"_kling_compat_version",
|
||||
"platform_id",
|
||||
"platformId",
|
||||
"platform_model_id",
|
||||
"platformModelId",
|
||||
} {
|
||||
delete(out, key)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user