Files
easyai-ai-gateway/apps/api/internal/clients/media_parameter_normalization_test.go
T
wangbo 95776c84f6 fix(media): 规范比例与 OpenAI 图像尺寸参数
统一过滤非 x:x 比例,避免默认值参与候选匹配或透传上游。

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

验证:在独立临时 worktree 中执行 apps/api 全量 go test ./... -count=1 通过,真实 OpenAI 请求已到达上游但受本地无效凭据阻断。
2026-07-27 23:50:17 +08:00

233 lines
6.4 KiB
Go

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)
}
}
}